mirror of
https://github.com/ershisan99/vacancies-trends-front.git
synced 2025-12-16 05:09:24 +00:00
initial commit
This commit is contained in:
@@ -71,6 +71,17 @@ module.exports = {
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
rules:{
|
||||
"@typescript-eslint/ban-ts-comment": [
|
||||
"error",
|
||||
{
|
||||
"ts-expect-error":false,
|
||||
"ts-ignore": "allow-with-description",
|
||||
"ts-nocheck": "allow-with-description",
|
||||
"ts-check": "allow-with-description"
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
// Node
|
||||
@@ -80,5 +91,6 @@ module.exports = {
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
|
||||
],
|
||||
};
|
||||
|
||||
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/jsLibraryMappings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/vacancies-trends-front.iml" filepath="$PROJECT_DIR$/.idea/vacancies-trends-front.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/prettier.xml
generated
Normal file
8
.idea/prettier.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PrettierConfiguration">
|
||||
<option name="myConfigurationMode" value="AUTOMATIC" />
|
||||
<option name="myRunOnSave" value="true" />
|
||||
<option name="myFilesPattern" value="**/*.{js,ts,jsx,tsx,vue,astro,html,css,json}" />
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/vacancies-trends-front.iml
generated
Normal file
12
.idea/vacancies-trends-front.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
146
app/components/vacancies-chart.tsx
Normal file
146
app/components/vacancies-chart.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { ALL_KEYWORDS, Keyword, KEYWORDS } from '~/services/vacancies/vacancies.constants'
|
||||
import { VacancyData } from '~/services/vacancies/vacancies.types'
|
||||
import { AreaChart, MultiSelect, MultiSelectItem, Select, SelectItem } from '@tremor/react'
|
||||
import { useSearchParams } from '@remix-run/react'
|
||||
|
||||
type Props = {
|
||||
data: VacancyData
|
||||
}
|
||||
|
||||
const presets = {
|
||||
None: [],
|
||||
All: ALL_KEYWORDS,
|
||||
Backend: KEYWORDS.BACKEND,
|
||||
Databases: KEYWORDS.DATABASES,
|
||||
DevOps: KEYWORDS.DEVOPS,
|
||||
Frontend: KEYWORDS.FRONTEND,
|
||||
'Frontend Frameworks': KEYWORDS.FRONTEND_FRAMEWORK,
|
||||
Mobile: KEYWORDS.MOBILE,
|
||||
ORM: KEYWORDS.ORM,
|
||||
Styles: KEYWORDS.STYLES,
|
||||
'State Management': KEYWORDS.STATE_MANAGEMENT,
|
||||
Testing: KEYWORDS.TESTING,
|
||||
}
|
||||
|
||||
const presetsForSelect = Object.entries(presets).map(
|
||||
([label, value]) =>
|
||||
({
|
||||
label,
|
||||
value,
|
||||
}) as const
|
||||
)
|
||||
|
||||
export function VacanciesChart({ data }: Props) {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const [preset, setPreset] = useState('None' as keyof typeof presets)
|
||||
|
||||
const selectedCategories = useMemo(
|
||||
() => searchParams.get('categories')?.split(',') || [],
|
||||
[searchParams]
|
||||
)
|
||||
|
||||
const setSelectedCategories = useCallback((value: Keyword[]) => {
|
||||
if (value.length === 0) {
|
||||
searchParams.delete('categories')
|
||||
} else {
|
||||
searchParams.set('categories', value.join(','))
|
||||
}
|
||||
|
||||
setSearchParams(searchParams)
|
||||
}, [])
|
||||
|
||||
const sortedCategories = useMemo(
|
||||
() => sortCategoriesByVacancies(selectedCategories, data),
|
||||
[selectedCategories, data]
|
||||
)
|
||||
|
||||
const filteredData = useMemo(
|
||||
() =>
|
||||
data.filter(row => {
|
||||
for (const category of selectedCategories) {
|
||||
// @ts-expect-error
|
||||
if (row[category] > 0) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}),
|
||||
[data, selectedCategories]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={'flex h-full flex-col gap-6 p-8'}>
|
||||
<div className={'flex gap-4'}>
|
||||
<div className="max-w-xl">
|
||||
<label
|
||||
htmlFor="categories"
|
||||
className="text-tremor-default text-tremor-content dark:text-dark-tremor-content"
|
||||
>
|
||||
Technologies to compare
|
||||
</label>
|
||||
<MultiSelect
|
||||
id={'categories'}
|
||||
onValueChange={value => setSelectedCategories(value)}
|
||||
value={selectedCategories}
|
||||
>
|
||||
{ALL_KEYWORDS.map(category => (
|
||||
<MultiSelectItem key={'category-select-' + category} value={category} />
|
||||
))}
|
||||
</MultiSelect>
|
||||
</div>
|
||||
<div className="max-w-xl">
|
||||
<label
|
||||
htmlFor="presets"
|
||||
className="text-tremor-default text-tremor-content dark:text-dark-tremor-content"
|
||||
>
|
||||
Preset
|
||||
</label>
|
||||
<Select
|
||||
value={preset}
|
||||
id={'presets'}
|
||||
defaultValue={'All'}
|
||||
onValueChange={value => {
|
||||
setPreset(value as keyof typeof presets)
|
||||
setSelectedCategories(presets[value as keyof typeof presets] as Keyword[])
|
||||
}}
|
||||
>
|
||||
{presetsForSelect.map(category => (
|
||||
<SelectItem key={category.label} value={category.label} />
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<AreaChart
|
||||
connectNulls={true}
|
||||
className="h-full"
|
||||
data={filteredData}
|
||||
index="date"
|
||||
categories={sortedCategories}
|
||||
yAxisWidth={60}
|
||||
startEndOnly={false}
|
||||
intervalType="preserveStartEnd"
|
||||
showAnimation
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const sortCategoriesByVacancies = (categories: string[], data: VacancyData) => {
|
||||
const entryToCompare = data.at(-1)
|
||||
return categories.sort((a, b) => {
|
||||
if (!entryToCompare) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (entryToCompare[a] === undefined && entryToCompare[b] === undefined) {
|
||||
return 0
|
||||
}
|
||||
if (entryToCompare[a] > entryToCompare[b]) {
|
||||
return -1
|
||||
}
|
||||
if (entryToCompare[a] < entryToCompare[b]) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
6
app/lib/utils.ts
Normal file
6
app/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import clsx, { type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cx(...args: ClassValue[]) {
|
||||
return twMerge(clsx(...args))
|
||||
}
|
||||
23
app/root.tsx
23
app/root.tsx
@@ -1,29 +1,28 @@
|
||||
import {
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from "@remix-run/react";
|
||||
import { LinksFunction } from '@remix-run/node'
|
||||
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react'
|
||||
import stylesheet from '~/tailwind.css?url'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
export const links: LinksFunction = () => [{ rel: 'stylesheet', href: stylesheet }]
|
||||
|
||||
export function Layout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" className="antialiased dark:bg-gray-950 dark:text-slate-50 h-full">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<body className={'h-full'}>
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
return <Outlet />
|
||||
}
|
||||
|
||||
@@ -1,41 +1,18 @@
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { json, MetaFunction } from '@remix-run/node'
|
||||
import { vacanciesService } from '~/services/vacancies/vacancies.service'
|
||||
import { useLoaderData } from '@remix-run/react'
|
||||
import { VacanciesChart } from '~/components/vacancies-chart'
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "New Remix App" },
|
||||
{ name: "description", content: "Welcome to Remix!" },
|
||||
];
|
||||
};
|
||||
return [{ title: 'New Remix App' }, { name: 'description', content: 'Welcome to Remix!' }]
|
||||
}
|
||||
|
||||
export const loader = async () => {
|
||||
const vacancies = await vacanciesService.getAggregateByCreatedAt()
|
||||
return json({ vacancies })
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
return (
|
||||
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
|
||||
<h1>Welcome to Remix</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://remix.run/start/quickstart"
|
||||
rel="noreferrer"
|
||||
>
|
||||
5m Quick Start
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://remix.run/start/tutorial"
|
||||
rel="noreferrer"
|
||||
>
|
||||
30m Tutorial
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" href="https://remix.run/docs" rel="noreferrer">
|
||||
Remix Docs
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
const { vacancies } = useLoaderData<typeof loader>()
|
||||
return <VacanciesChart data={vacancies} />
|
||||
}
|
||||
|
||||
56
app/services/vacancies/vacancies.constants.ts
Normal file
56
app/services/vacancies/vacancies.constants.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
export const KEYWORDS = {
|
||||
BACKEND: [
|
||||
'.net',
|
||||
'asp.net',
|
||||
'django',
|
||||
'express',
|
||||
'go',
|
||||
'java',
|
||||
'laravel',
|
||||
'nest.js',
|
||||
'nestjs',
|
||||
'node.js',
|
||||
'php',
|
||||
],
|
||||
DATABASES: ['mysql', 'cassandra', 'firebase', 'mongodb', 'postgres', 'redis', 'sqlite'],
|
||||
DEVOPS: ['ansible', 'jenkins', 'docker', 'kubernetes', 'terraform'],
|
||||
get FRONTEND() {
|
||||
return [
|
||||
...new Set([
|
||||
...this.FRONTEND_FRAMEWORK,
|
||||
...this.STYLES,
|
||||
...this.STATE_MANAGEMENT,
|
||||
...this.TESTING,
|
||||
'fsd',
|
||||
]),
|
||||
]
|
||||
},
|
||||
FRONTEND_FRAMEWORK: [
|
||||
'angular',
|
||||
'jquery',
|
||||
'next.js',
|
||||
'nextjs',
|
||||
'nuxt',
|
||||
'react',
|
||||
'remix',
|
||||
'svelte',
|
||||
'vue',
|
||||
],
|
||||
MOBILE: ['flutter', 'kotlin', 'swift', 'react native', 'xamarin'],
|
||||
ORM: ['prisma', 'sequelize', 'drizzle', 'typeorm'],
|
||||
STATE_MANAGEMENT: [
|
||||
'effector',
|
||||
'mobx',
|
||||
'react-query',
|
||||
'redux toolkit query',
|
||||
'redux toolkit',
|
||||
'redux',
|
||||
'rtk',
|
||||
],
|
||||
STYLES: ['material ui', 'mui', 'styled-components', 'tailwind', 'bootstrap', 'css', 'sass'],
|
||||
TESTING: ['cypress', 'jasmine', 'playwright', 'puppeteer', 'selenium', 'vitest', 'jest', 'mocha'],
|
||||
} as const
|
||||
|
||||
export const ALL_KEYWORDS = [...new Set(Object.values(KEYWORDS).flat().sort())]
|
||||
|
||||
export type Keyword = (typeof ALL_KEYWORDS)[number]
|
||||
23
app/services/vacancies/vacancies.service.ts
Normal file
23
app/services/vacancies/vacancies.service.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Vacancies, VacancyData } from '~/services/vacancies/vacancies.types'
|
||||
|
||||
export class VacanciesService {
|
||||
async getAll(): Promise<Vacancies> {
|
||||
return await fetch('http://localhost:4321/vacancies').then(res => res.json())
|
||||
}
|
||||
async getAggregateByCreatedAt(): Promise<VacancyData> {
|
||||
return await fetch('http://localhost:4321/vacancies/aggregated')
|
||||
.then(res => res.json())
|
||||
.then(this.formatDateOnData)
|
||||
}
|
||||
|
||||
formatDateOnData(data: VacancyData): VacancyData {
|
||||
return data.map(item => {
|
||||
return {
|
||||
...item,
|
||||
date: new Date(item.date).toLocaleTimeString('ru'),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const vacanciesService = new VacanciesService()
|
||||
11
app/services/vacancies/vacancies.types.ts
Normal file
11
app/services/vacancies/vacancies.types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type Vacancies = VacancyDataEntry[]
|
||||
|
||||
export interface VacancyDataEntry {
|
||||
createdAt: string
|
||||
id: number
|
||||
technology: string
|
||||
updatedAt: string
|
||||
vacancies: number
|
||||
}
|
||||
|
||||
export type VacancyData = Array<{ date: string; [key: string]: string | number }>
|
||||
10
app/tailwind.css
Normal file
10
app/tailwind.css
Normal file
@@ -0,0 +1,10 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist Sans';
|
||||
src: url('/fonts/GeistVariableVF.woff2') format('woff2');
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
}
|
||||
19
package.json
19
package.json
@@ -11,30 +11,43 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "1.7.18",
|
||||
"@headlessui/tailwindcss": "0.2.0",
|
||||
"@remix-run/node": "^2.9.2",
|
||||
"@remix-run/react": "^2.9.2",
|
||||
"@remix-run/serve": "^2.9.2",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@tremor/react": "^3.17.2",
|
||||
"clsx": "^2.1.1",
|
||||
"isbot": "^4.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwind-merge": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@it-incubator/prettier-config": "^0.1.2",
|
||||
"@remix-run/dev": "^2.9.2",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.3.1",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^5.1.0",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"prettier": "@it-incubator/prettier-config"
|
||||
}
|
||||
|
||||
7877
pnpm-lock.yaml
generated
Normal file
7877
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
public/fonts/GeistVariableVF.woff2
Normal file
BIN
public/fonts/GeistVariableVF.woff2
Normal file
Binary file not shown.
133
tailwind.config.ts
Normal file
133
tailwind.config.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
import colors from 'tailwindcss/colors'
|
||||
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||
|
||||
export default {
|
||||
content: [
|
||||
'./app/**/*.{js,jsx,ts,tsx}',
|
||||
|
||||
// Path to Tremor module
|
||||
'./node_modules/@tremor/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
theme: {
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
extend: {
|
||||
fontFamily: { sans: ['Geist Sans', ...defaultTheme.fontFamily.sans] },
|
||||
|
||||
colors: {
|
||||
// light mode
|
||||
tremor: {
|
||||
brand: {
|
||||
faint: colors.blue[50],
|
||||
muted: colors.blue[200],
|
||||
subtle: colors.blue[400],
|
||||
DEFAULT: colors.blue[500],
|
||||
emphasis: colors.blue[700],
|
||||
inverted: colors.white,
|
||||
},
|
||||
background: {
|
||||
muted: colors.gray[50],
|
||||
subtle: colors.gray[100],
|
||||
DEFAULT: colors.white,
|
||||
emphasis: colors.gray[700],
|
||||
},
|
||||
border: {
|
||||
DEFAULT: colors.gray[200],
|
||||
},
|
||||
ring: {
|
||||
DEFAULT: colors.gray[200],
|
||||
},
|
||||
content: {
|
||||
subtle: colors.gray[400],
|
||||
DEFAULT: colors.gray[500],
|
||||
emphasis: colors.gray[700],
|
||||
strong: colors.gray[900],
|
||||
inverted: colors.white,
|
||||
},
|
||||
},
|
||||
// dark mode
|
||||
'dark-tremor': {
|
||||
brand: {
|
||||
faint: '#0B1229',
|
||||
muted: colors.blue[950],
|
||||
subtle: colors.blue[800],
|
||||
DEFAULT: colors.blue[500],
|
||||
emphasis: colors.blue[400],
|
||||
inverted: colors.blue[950],
|
||||
},
|
||||
background: {
|
||||
muted: '#131A2B',
|
||||
subtle: colors.gray[800],
|
||||
DEFAULT: colors.gray[900],
|
||||
emphasis: colors.gray[300],
|
||||
},
|
||||
border: {
|
||||
DEFAULT: colors.gray[800],
|
||||
},
|
||||
ring: {
|
||||
DEFAULT: colors.gray[800],
|
||||
},
|
||||
content: {
|
||||
subtle: colors.gray[600],
|
||||
DEFAULT: colors.gray[500],
|
||||
emphasis: colors.gray[200],
|
||||
strong: colors.gray[50],
|
||||
inverted: colors.gray[950],
|
||||
},
|
||||
},
|
||||
},
|
||||
boxShadow: {
|
||||
// light
|
||||
'tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
||||
'tremor-card': '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
||||
'tremor-dropdown': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
||||
// dark
|
||||
'dark-tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
||||
'dark-tremor-card': '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
||||
'dark-tremor-dropdown': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
||||
},
|
||||
borderRadius: {
|
||||
'tremor-small': '0.375rem',
|
||||
'tremor-default': '0.5rem',
|
||||
'tremor-full': '9999px',
|
||||
},
|
||||
fontSize: {
|
||||
'tremor-label': ['0.75rem', { lineHeight: '1rem' }],
|
||||
'tremor-default': ['0.875rem', { lineHeight: '1.25rem' }],
|
||||
'tremor-title': ['1.125rem', { lineHeight: '1.75rem' }],
|
||||
'tremor-metric': ['1.875rem', { lineHeight: '2.25rem' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
safelist: [
|
||||
{
|
||||
pattern:
|
||||
/^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||
variants: ['hover', 'ui-selected'],
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||
variants: ['hover', 'ui-selected'],
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||
variants: ['hover', 'ui-selected'],
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
|
||||
},
|
||||
],
|
||||
plugins: [require('@headlessui/tailwindcss'), require('@tailwindcss/forms')],
|
||||
} satisfies Config
|
||||
Reference in New Issue
Block a user