diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6b1aa16f..bd1988cc 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -4,5 +4,6 @@ "packages/ui-hooks": "2.2.0", "packages/ui-system": "1.0.1", "packages/ui-private": "1.1.0", - "packages/ui-icons": "1.1.0" + "packages/ui-icons": "1.1.0", + "packages/ui-plugins": "0.0.0" } diff --git a/packages/documentation/package.json b/packages/documentation/package.json index d074da03..bf447587 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -23,6 +23,7 @@ "@tailwindcss/typography": "0.5.10", "@versini/ui-components": "workspace:../ui-components", "@versini/ui-icons": "workspace:../ui-icons", + "@versini/ui-plugins": "workspace:../ui-plugins", "@versini/ui-system": "workspace:../ui-system", "clsx": "2.1.0", "react": "18.2.0", diff --git a/packages/documentation/tailwind.config.js b/packages/documentation/tailwind.config.js index fe0610da..2bb25dcf 100644 --- a/packages/documentation/tailwind.config.js +++ b/packages/documentation/tailwind.config.js @@ -1,11 +1,7 @@ /** @type {import('tailwindcss').Config} */ -import { twPlugin as componentsPlugin } from "@versini/ui-components/dist/utilities"; -import { twPlugin as systemPlugin } from "@versini/ui-system/dist/utilities"; +import { twPlugin } from "@versini/ui-plugins"; -export default systemPlugin.merge( - componentsPlugin.merge({ - darkMode: "selector", - content: ["./src/**/*.{js,ts,jsx,tsx}", "./.ladle/**/*.tsx"], - }), -); +export default twPlugin.merge({ + content: ["./src/**/*.{js,ts,jsx,tsx}", "./.ladle/**/*.tsx"], +}); diff --git a/packages/ui-plugins/README.md b/packages/ui-plugins/README.md new file mode 100644 index 00000000..1ebe379f --- /dev/null +++ b/packages/ui-plugins/README.md @@ -0,0 +1,27 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/packages/ui-plugins/package.json b/packages/ui-plugins/package.json new file mode 100644 index 00000000..6e9e5176 --- /dev/null +++ b/packages/ui-plugins/package.json @@ -0,0 +1,33 @@ +{ + "name": "@versini/ui-plugins", + "version": "0.0.0", + "license": "MIT", + "author": "Arno Versini", + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/aversini/ui-components", + "repository": { + "type": "git", + "url": "git@github.com:aversini/ui-components.git" + }, + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build:check": "tsc", + "build:js": "vite build", + "build:types": "tsup", + "build": "npm-run-all --serial clean build:check build:js build:types", + "clean": "rimraf dist", + "dev:js": "vite build --watch --mode development", + "dev:types": "tsup --watch src", + "dev": "npm-run-all clean --parallel dev:js dev:types", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --fix --color", + "test:coverage": "echo \"WARNING: no test specified\" && exit 0", + "test": "echo \"WARNING: no test specified\" && exit 0" + } +} diff --git a/packages/ui-plugins/src/plugins/customCSS.ts b/packages/ui-plugins/src/plugins/customCSS.ts new file mode 100644 index 00000000..0d418e8b --- /dev/null +++ b/packages/ui-plugins/src/plugins/customCSS.ts @@ -0,0 +1,33 @@ +export const customCSS = { + [`.av-text-input-wrapper label[aria-hidden="true"], + .av-text-area-wrapper label[aria-hidden="true"]`]: { + /* move the label inline */ + transform: "translate(18px, 0) scale(1)", + transformOrigin: "top left", + transition: "var(--av-text-area-wrapper-transition, all 0.2s ease-out)", + }, + '.av-text-input:focus + label[aria-hidden="true"],\n\t.av-text-input:not(:placeholder-shown) + label[aria-hidden="true"],\n\t.av-text-area:focus + label[aria-hidden="true"],\n\t.av-text-area:not(:placeholder-shown) + label[aria-hidden="true"]': + { + transform: + "translate(18px, var(--av-text-area-label, -25px)) scale(0.75)", + }, + + '.av-text-input-simple:focus + label[aria-hidden="true"],\n\t.av-text-input-simple:not(:placeholder-shown) + label[aria-hidden="true"]': + { + transform: + "translate(18px, var(--av-text-area-label, -12px)) scale(0.75)", + }, + + ".av-text-input-helper-text,\n\t.av-text-area-helper-text": { + transform: + "translate(18px, var(--av-text-area-helper-text, 32px))\n\t\t\tscale(0.75)", + transformOrigin: "top left", + }, + ".av-text-input__control--right,\n\t.av-text-area__control--right": { + right: "18px", + }, + "@keyframes blink": { "50%": { fill: "transparent" } }, + ".av-spinner__dot": { animation: "1s blink infinite" }, + ".av-spinner__dot:nth-child(2)": { animationDelay: "250ms" }, + ".av-spinner__dot:nth-child(3)": { animationDelay: "500ms" }, +}; diff --git a/packages/ui-plugins/src/plugins/index.ts b/packages/ui-plugins/src/plugins/index.ts new file mode 100644 index 00000000..19d7f0a4 --- /dev/null +++ b/packages/ui-plugins/src/plugins/index.ts @@ -0,0 +1 @@ +export { twPlugin } from "./tailwindPlugin"; diff --git a/packages/ui-plugins/src/plugins/tailwindPlugin.ts b/packages/ui-plugins/src/plugins/tailwindPlugin.ts new file mode 100644 index 00000000..02daa09f --- /dev/null +++ b/packages/ui-plugins/src/plugins/tailwindPlugin.ts @@ -0,0 +1,187 @@ +import typography from "@tailwindcss/typography"; +import { converter } from "culori"; +import plugin from "tailwindcss/plugin"; +import type { Config, OptionalConfig } from "tailwindcss/types/config"; + +import { customCSS } from "./customCSS"; +import { tokens } from "./tokens"; + +type TailwindConfig = { + content: string[]; +} & OptionalConfig; + +const parse = converter("rgb"); + +const dynamicColors = () => { + const result: { [key: string]: string } = {}; + Object.entries(tokens.colors).forEach(([name, color]) => { + const rgb = parse(color); + const variable = `--av-${name}`; + const fallbackValue = rgb + ? `${rgb.r * 255} ${rgb.g * 255} ${rgb.b * 255}` + : "0 0 0"; + result[name] = `var(${variable}, rgb(${fallbackValue} / ))`; + }); + return result; +}; + +const dynamicMargins = () => { + const allowed = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 44, + 48, 52, 56, 60, 64, 72, 80, 96, + ]; + const margins: string[] = []; + allowed.forEach((num) => { + margins.push(`mt-${num}`); + margins.push(`mr-${num}`); + margins.push(`mb-${num}`); + margins.push(`ml-${num}`); + }); + return margins; +}; + +const myComponentLibraryConfig = { + theme: { + extend: { + colors: dynamicColors(), + typography: ({ theme }: { theme: (arg0: string) => any }) => ({ + DEFAULT: { + css: { + maxWidth: "inherit", + "h1, h2, h3, h4, h5, h6": { + fontFamily: "Open Sans, ui-sans-serif, system-ui, sans-serif", + }, + blockquote: { + borderLeftWidth: "6px", + }, + "blockquote p": { + fontFamily: "Georgia, Cambria, Times New Roman, Times, serif", + }, + li: { + fontSize: "1rem", + }, + pre: { + marginTop: "2rem", + marginBottom: "2rem", + fontSize: "0.875rem", + lineHeight: "1.25rem", + }, + code: { + fontFamily: + 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', + }, + }, + }, + light: { + css: { + "--tw-prose-body": tokens.colors["copy-light"], + "--tw-prose-headings": tokens.colors["copy-medium"], + "--tw-prose-lead": tokens.colors["copy-light"], + "--tw-prose-links": tokens.colors["copy-light"], + "--tw-prose-bold": tokens.colors["copy-light"], + "--tw-prose-counters": tokens.colors["copy-medium"], + "--tw-prose-bullets": tokens.colors["copy-medium"], + "--tw-prose-hr": tokens.colors["copy-medium"], + "--tw-prose-quotes": tokens.colors["copy-light"], + "--tw-prose-quote-borders": tokens.colors["copy-light"], + "--tw-prose-captions": tokens.colors["copy-light"], + "--tw-prose-code": tokens.colors["copy-light"], + "--tw-prose-pre-code": tokens.colors["copy-lighter"], + "--tw-prose-pre-bg": tokens.colors["surface-medium"], + "--tw-prose-kbd": tokens.colors["copy-light"], + li: { + color: tokens.colors["copy-light"], + }, + }, + }, + lighter: { + css: { + "--tw-prose-body": tokens.colors["copy-lighter"], + "--tw-prose-headings": tokens.colors["copy-light"], + "--tw-prose-lead": tokens.colors["copy-lighter"], + "--tw-prose-links": tokens.colors["copy-lighter"], + "--tw-prose-bold": tokens.colors["copy-lighter"], + "--tw-prose-counters": tokens.colors["copy-light"], + "--tw-prose-bullets": tokens.colors["copy-light"], + "--tw-prose-hr": tokens.colors["copy-light"], + "--tw-prose-quotes": tokens.colors["copy-lighter"], + "--tw-prose-quote-borders": tokens.colors["copy-lighter"], + "--tw-prose-captions": tokens.colors["copy-lighter"], + "--tw-prose-code": tokens.colors["copy-lighter"], + "--tw-prose-pre-code": tokens.colors["copy-lighter"], + "--tw-prose-pre-bg": tokens.colors["surface-darker"], + "--tw-prose-kbd": tokens.colors["copy-lighter"], + li: { + color: tokens.colors["copy-lighter"], + }, + }, + }, + dark: { + css: { + "--tw-prose-body": theme("colors.slate[800]"), + "--tw-prose-headings": theme("colors.slate[900]"), + "--tw-prose-lead": theme("colors.slate[700]"), + "--tw-prose-links": theme("colors.slate[900]"), + "--tw-prose-bold": theme("colors.slate[900]"), + "--tw-prose-counters": theme("colors.slate[600]"), + "--tw-prose-bullets": theme("colors.slate[400]"), + "--tw-prose-hr": theme("colors.slate[300]"), + "--tw-prose-quotes": theme("colors.slate[900]"), + "--tw-prose-quote-borders": theme("colors.slate[300]"), + "--tw-prose-captions": theme("colors.slate[700]"), + "--tw-prose-code": theme("colors.slate[900]"), + + "--tw-prose-pre-code": tokens.colors["copy-lighter"], + "--tw-prose-pre-bg": tokens.colors["surface-medium"], + + // "--tw-prose-pre-code": theme("colors.slate[100]"), + // "--tw-prose-pre-bg": theme("colors.slate[900]"), + + "--tw-prose-kbd": theme("colors.slate[800]"), + li: { + color: tokens.colors["copy-dark"], + }, + }, + }, + }), + }, + }, +}; + +const tailwindContentPath = [ + (__dirname + "/**/*.{js,ts,jsx,tsx}").replace( + "ui-plugins/dist", + "ui-system/dist", + ), + (__dirname + "/**/*.{js,ts,jsx,tsx}").replace( + "ui-plugins/dist", + "ui-components/dist", + ), +]; + +const tailwindPlugins = [ + typography, + plugin(function ({ addUtilities }) { + addUtilities(customCSS); + }, myComponentLibraryConfig), +]; + +const tailwindSafelist = [...dynamicMargins()]; + +export const twPlugin = { + content: tailwindContentPath, + safelist: tailwindSafelist, + plugins: tailwindPlugins, + + merge: (config: TailwindConfig) => { + const safelist = tailwindSafelist; + const content = tailwindContentPath; + const plugins = tailwindPlugins; + + config.safelist = [...(config.safelist || []), ...safelist]; + config.content = [...(config.content || []), ...content]; + config.plugins = [...(config.plugins || []), ...plugins]; + + return config as Config; + }, +}; diff --git a/packages/ui-plugins/src/plugins/tokens.ts b/packages/ui-plugins/src/plugins/tokens.ts new file mode 100644 index 00000000..626f9541 --- /dev/null +++ b/packages/ui-plugins/src/plugins/tokens.ts @@ -0,0 +1,94 @@ +import colors from "tailwindcss/colors"; + +const errorColorDark = "#d80000"; +const errorColorLight = "#ff3f3f"; + +export const tokens = { + colors: { + /** + * Action tokens. + */ + "action-dark": colors.slate[900], + "action-dark-hover": colors.slate[700], + "action-dark-active": colors.slate[600], + + "action-light": colors.slate[500], + "action-light-hover": colors.slate[600], + "action-light-active": colors.slate[700], + + /** + * Surface tokens. + */ + "surface-darker": colors.slate[900], + "surface-dark": colors.slate[700], + "surface-medium": colors.slate[500], + "surface-light": colors.slate[300], + "surface-lighter": colors.slate[200], + "surface-accent": "#0B93F6", + "surface-information": colors.violet[200], + "surface-success": colors.green[200], + "surface-warning": colors.orange[200], + "surface-error": colors.red[200], + + /** + * Typography tokens. + */ + "copy-dark": colors.slate[900], + "copy-dark-hover": colors.slate[900], + "copy-dark-active": colors.slate[900], + + "copy-medium": colors.slate[400], + "copy-medium-hover": colors.slate[400], + "copy-medium-active": colors.slate[400], + + "copy-light": colors.slate[200], + "copy-light-hover": colors.slate[200], + "copy-light-active": colors.slate[400], + + "copy-lighter": "#ffffff", + "copy-lighter-hover": "#ffffff", + "copy-lighter-active": "#ffffff", + + "copy-error-dark": errorColorDark, + "copy-error-light": errorColorLight, + + "copy-information": colors.violet[800], + "copy-success": colors.green[800], + "copy-warning": colors.orange[800], + "copy-error": colors.red[800], + + /** + * Border tokens. + */ + "border-dark": colors.slate[900], + "border-medium": colors.slate[400], + "border-light": colors.slate[300], + + "border-white": "#ffffff", + "border-error-dark": errorColorDark, + "border-error-light": errorColorLight, + + "border-information": colors.violet[400], + "border-success": colors.green[400], + "border-warning": colors.orange[400], + "border-error": colors.red[400], + + /** + * Focus tokens. + */ + "focus-dark": colors.green[700], + "focus-light": colors.green[300], + "focus-error-dark": errorColorDark, + "focus-error-light": errorColorLight, + + /** + * Table tokens + */ + "table-dark": colors.gray[700], + "table-dark-odd": colors.gray[800], + "table-dark-even": colors.gray[900], + "table-light": colors.gray[100], + "table-light-odd": colors.gray[200], + "table-light-even": colors.gray[300], + }, +}; diff --git a/packages/ui-plugins/src/vite-env.d.ts b/packages/ui-plugins/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/packages/ui-plugins/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/ui-plugins/tsconfig.json b/packages/ui-plugins/tsconfig.json new file mode 100644 index 00000000..ffbdf148 --- /dev/null +++ b/packages/ui-plugins/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "types": ["vitest/globals", "@testing-library/jest-dom"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/ui-plugins/tsconfig.node.json b/packages/ui-plugins/tsconfig.node.json new file mode 100644 index 00000000..f7d9f9ea --- /dev/null +++ b/packages/ui-plugins/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["./vite.config.ts", "./vitest.setup.ts"] +} diff --git a/packages/ui-plugins/tsup.config.ts b/packages/ui-plugins/tsup.config.ts new file mode 100644 index 00000000..4e6ef90c --- /dev/null +++ b/packages/ui-plugins/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + format: "esm", + entry: ["src/plugins/index.ts"], + dts: { + only: true, + }, +}); diff --git a/packages/ui-plugins/vite.config.ts b/packages/ui-plugins/vite.config.ts new file mode 100644 index 00000000..8812f4d1 --- /dev/null +++ b/packages/ui-plugins/vite.config.ts @@ -0,0 +1,31 @@ +import { resolve } from "node:path"; + +export default { + build: { + copyPublicDir: false, + lib: { + entry: resolve(__dirname, "src/plugins/index.ts"), + formats: ["es"], + name: "UIPlugins", + }, + rollupOptions: { + input: { + index: resolve(__dirname, "src/plugins/index.ts"), + }, + treeshake: "smallest", + output: { + compact: true, + minifyInternalExports: false, + assetFileNames: "style[extname]", + entryFileNames: "[name].js", + chunkFileNames: "chunks/[name].[hash].js", + }, + }, + }, + esbuild: { + supported: { + "top-level-await": true, + }, + }, + plugins: [], +}; diff --git a/packages/ui-plugins/vitest.config.ts b/packages/ui-plugins/vitest.config.ts new file mode 100644 index 00000000..10f38968 --- /dev/null +++ b/packages/ui-plugins/vitest.config.ts @@ -0,0 +1,28 @@ +/// + +import { defineConfig, mergeConfig } from "vitest/config"; + +import viteConfig from "./vite.config"; + +export default defineConfig((configEnv) => + mergeConfig( + viteConfig(configEnv), + defineConfig({ + test: { + globals: true, + setupFiles: ["./vitest.setup.ts"], + environment: "happy-dom", + coverage: { + include: ["src/**/*.ts", "src/**/*.tsx", "!src/style.ts"], + provider: "v8", + thresholds: { + statements: 100, + branches: 100, + functions: 100, + lines: 100, + }, + }, + }, + }), + ), +); diff --git a/packages/ui-plugins/vitest.setup.ts b/packages/ui-plugins/vitest.setup.ts new file mode 100644 index 00000000..b2e2ca5a --- /dev/null +++ b/packages/ui-plugins/vitest.setup.ts @@ -0,0 +1,19 @@ +/* eslint-disable no-console */ + +import "@testing-library/jest-dom/vitest"; + +import util from "node:util"; + +const originalConsoleError = console.error; +console.error = (...args: any) => { + const message = util.format(...args); + if ( + /(Warning: validateDOMNesting|Invalid prop|Failed prop type|React does not recognize|Unknown event handler property)/gi.test( + message, + ) + ) { + throw new Error(message); + } else { + originalConsoleError.apply(console, [...args]); + } +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08510939..2107a854 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: '@versini/ui-icons': specifier: workspace:../ui-icons version: link:../ui-icons + '@versini/ui-plugins': + specifier: workspace:../ui-plugins + version: link:../ui-plugins '@versini/ui-system': specifier: workspace:../ui-system version: link:../ui-system @@ -152,6 +155,8 @@ importers: specifier: 3.4.1 version: 3.4.1 + packages/ui-plugins: {} + packages/ui-private: dependencies: '@floating-ui/react': diff --git a/release-please-config.json b/release-please-config.json index 7506fe58..087be822 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -5,6 +5,7 @@ "packages/ui-hooks": {}, "packages/ui-system": {}, "packages/ui-private": {}, - "packages/ui-icons": {} + "packages/ui-icons": {}, + "packages/ui-plugins": {} } }