@plexui/styles 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +28 -2
  3. package/dist/es/components/Accordion/Accordion.module.css +89 -0
  4. package/dist/es/components/Alert/Alert.module.css +210 -0
  5. package/dist/es/components/Avatar/Avatar.module.css +168 -0
  6. package/dist/es/components/Avatar/AvatarGroup.module.css +26 -0
  7. package/dist/es/components/Badge/Badge.module.css +196 -0
  8. package/dist/es/components/Breadcrumb/Breadcrumb.module.css +59 -0
  9. package/dist/es/components/Button/Button.module.css +863 -0
  10. package/dist/es/components/ButtonGroup/ButtonGroup.module.css +158 -0
  11. package/dist/es/components/Card/Card.module.css +125 -0
  12. package/dist/es/components/Checkbox/Checkbox.module.css +225 -0
  13. package/dist/es/components/CodeBlock/CodeBlock.module.css +139 -0
  14. package/dist/es/components/DataTable/DataTable.module.css +117 -0
  15. package/dist/es/components/DatePicker/Calendar.module.css +271 -0
  16. package/dist/es/components/DateRangePicker/Calendar.module.css +295 -0
  17. package/dist/es/components/DateRangePicker/Trigger.module.css +25 -0
  18. package/dist/es/components/Dialog/Dialog.module.css +240 -0
  19. package/dist/es/components/EmptyMessage/EmptyMessage.module.css +116 -0
  20. package/dist/es/components/Field/Field.module.css +152 -0
  21. package/dist/es/components/FieldError/FieldError.module.css +28 -0
  22. package/dist/es/components/FileUpload/FileUpload.module.css +133 -0
  23. package/dist/es/components/FloatingLabelInput/FloatingLabelInput.module.css +220 -0
  24. package/dist/es/components/FloatingLabelSelect/FloatingLabelSelect.module.css +152 -0
  25. package/dist/es/components/Image/Image.module.css +9 -0
  26. package/dist/es/components/Indicator/CircularProgress.module.css +28 -0
  27. package/dist/es/components/Indicator/LoadingDots.module.css +35 -0
  28. package/dist/es/components/Indicator/LoadingIndicator.module.css +41 -0
  29. package/dist/es/components/Input/Input.module.css +519 -0
  30. package/dist/es/components/InputGroup/InputGroup.module.css +309 -0
  31. package/dist/es/components/Kbd/Kbd.module.css +47 -0
  32. package/dist/es/components/Label/Label.module.css +16 -0
  33. package/dist/es/components/Markdown/Markdown.module.css +233 -0
  34. package/dist/es/components/MarkdownEditor/MarkdownEditor.module.css +297 -0
  35. package/dist/es/components/Menu/Menu.module.css +386 -0
  36. package/dist/es/components/OTPInput/OTPInput.module.css +70 -0
  37. package/dist/es/components/Popover/Popover.module.css +82 -0
  38. package/dist/es/components/Progress/Progress.module.css +34 -0
  39. package/dist/es/components/ProgressSteps/ProgressSteps.module.css +246 -0
  40. package/dist/es/components/RadioGroup/RadioGroup.module.css +159 -0
  41. package/dist/es/components/Select/Select.module.css +338 -0
  42. package/dist/es/components/SelectControl/SelectControl.module.css +558 -0
  43. package/dist/es/components/Separator/Separator.module.css +16 -0
  44. package/dist/es/components/ShimmerText/ShimmerText.module.css +43 -0
  45. package/dist/es/components/Sidebar/Sidebar.module.css +913 -0
  46. package/dist/es/components/Skeleton/Skeleton.module.css +17 -0
  47. package/dist/es/components/Slider/Slider.module.css +199 -0
  48. package/dist/es/components/StatCard/StatCard.module.css +234 -0
  49. package/dist/es/components/Switch/Switch.module.css +121 -0
  50. package/dist/es/components/Table/Table.module.css +56 -0
  51. package/dist/es/components/Tabs/Tabs.module.css +688 -0
  52. package/dist/es/components/TagInput/TagInput.module.css +320 -0
  53. package/dist/es/components/TextLink/TextLink.module.css +50 -0
  54. package/dist/es/components/Textarea/Textarea.module.css +272 -0
  55. package/dist/es/components/Toast/Toast.module.css +99 -0
  56. package/dist/es/components/Tooltip/Tooltip.module.css +91 -0
  57. package/dist/es/components/Transition/Animate.module.css +54 -0
  58. package/dist/es/components/Transition/AnimateLayout.module.css +101 -0
  59. package/dist/es/components/Transition/AnimateLayoutGroup.module.css +88 -0
  60. package/dist/es/components/Transition/TransitionGroup.module.css +4 -0
  61. package/dist/es/styles/base.css +102 -0
  62. package/dist/es/styles/globals.css +50 -0
  63. package/dist/es/styles/index.css +22 -0
  64. package/dist/es/styles/katex.min.css +1296 -0
  65. package/dist/es/styles/tailwind-utilities.css +1316 -0
  66. package/dist/es/styles/variables-components.css +626 -0
  67. package/dist/es/styles/variables-primitive.css +327 -0
  68. package/dist/es/styles/variables-semantic.css +629 -0
  69. package/dist/es/styles/variables-tailwind-theme.css +34 -0
  70. package/dist/es/styles/variables-tokens-root.css +40 -0
  71. package/dist/es/types.js +4 -0
  72. package/dist/es/types.js.map +1 -0
  73. package/dist/es/vite/index.js +54 -0
  74. package/dist/es/vite/index.js.map +1 -0
  75. package/dist/types/types.d.ts +15 -0
  76. package/dist/types/vite/index.d.ts +55 -0
  77. package/package.json +109 -4
  78. package/postcss/functions.mjs +23 -0
  79. package/postcss/injectBreakpoints.mjs +47 -0
  80. package/postcss/mixins.mjs +100 -0
  81. package/postcss/parseLightDark.mjs +174 -0
  82. package/postcss/wrapModulesInLayer.mjs +21 -0
  83. package/postcss.config.mjs +52 -0
@@ -0,0 +1,40 @@
1
+ /*
2
+ * Plex UI tokens that semantic + components tiers reference via var(...).
3
+ *
4
+ * These live in a plain `:root` block (NOT in `@theme static`), so Plex UI
5
+ * does not reset Tailwind's theme namespaces with `--radius-*: initial`.
6
+ * Being outside any cascade layer, they still win over the same names in
7
+ * Tailwind's `theme` layer: the consumer's `rounded-*`, `font-sans` and
8
+ * `font-*` weight utilities resolve to the Plex values (`rounded-md` is
9
+ * 8px). The installation guide documents this.
10
+ *
11
+ * The standalone `@plexui/ui/styles/variables-tailwind-theme.css` file also
12
+ * maps the rest of the Plex scale (text sizes, breakpoints, ...) onto
13
+ * Tailwind's theme (see /docs/installation).
14
+ */
15
+ :root {
16
+ /* Radii */
17
+ --radius-2xs: 0.125rem; /* 2px */
18
+ --radius-xs: 0.25rem; /* 4px */
19
+ --radius-sm: 0.375rem; /* 6px */
20
+ --radius-md: 0.5rem; /* 8px */
21
+ --radius-lg: 0.625rem; /* 10px */
22
+ --radius-xl: 0.75rem; /* 12px */
23
+ --radius-2xl: 1rem; /* 16px */
24
+ --radius-3xl: 1.25rem; /* 20px */
25
+ --radius-4xl: 1.5rem; /* 24px */
26
+ --radius-full: 9999px;
27
+
28
+ /* Font stacks */
29
+ --font-sans: ui-sans-serif, -apple-system, system-ui, "Segoe UI", "Noto Sans", "Helvetica",
30
+ "Arial", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
31
+ --font-serif: ui-serif, Georgia, "Times New Roman", serif;
32
+ --font-mono: ui-monospace, "SFMono-Regular", "SF Mono", "Menlo", "Monaco", "Consolas",
33
+ "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace;
34
+
35
+ /* Font weights */
36
+ --font-weight-normal: 400;
37
+ --font-weight-medium: 500;
38
+ --font-weight-semibold: 600;
39
+ --font-weight-bold: 700;
40
+ }
@@ -0,0 +1,4 @@
1
+ // Shared prop contract (sizes, colors, variants) for every Plex UI framework
2
+ // package. Keep it free of framework imports.
3
+ export {};
4
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,8CAA8C","sourcesContent":["// Shared prop contract (sizes, colors, variants) for every Plex UI framework\n// package. Keep it free of framework imports.\n\nexport type Size =\n | \"5xs\"\n | \"4xs\"\n | \"3xs\"\n | \"2xs\"\n | \"xs\"\n | \"sm\"\n | \"md\"\n | \"lg\"\n | \"xl\"\n | \"2xl\"\n | \"3xl\"\n | \"4xl\"\n | \"5xl\"\n | \"6xl\"\nexport type Sizes<T extends Size = Size> = T\n\nexport type ControlSize = Sizes<\"3xs\" | \"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\">\n\nexport type Radius = \"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\" | \"full\"\nexport type Radii<T extends Radius = Radius> = T\n\nexport type TextColor = \"base\" | \"emphasis\" | \"secondary\" | \"tertiary\"\nexport type TextColors<T extends TextColor = TextColor> = T\n\nexport type SemanticColor =\n | \"primary\"\n | \"secondary\"\n | \"danger\"\n | \"success\"\n | \"warning\"\n | \"caution\"\n | \"discovery\"\n | \"info\"\nexport type SemanticColors<T extends SemanticColor = SemanticColor> = T\n\nexport type Variant = \"solid\" | \"soft\" | \"outline\" | \"ghost\"\nexport type Variants<T extends Variant = Variant> = T\n\nexport type Alignment = \"start\" | \"center\" | \"end\"\nexport type Alignments<T extends Alignment = Alignment> = T\n\nexport type FontWeight = \"inherit\" | \"normal\" | \"medium\" | \"semibold\" | \"bold\"\nexport type FontWeights<T extends FontWeight = FontWeight> = T\n"]}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Vite plugin for Plex UI.
3
+ *
4
+ * Importing `@plexui/styles/vite` (React apps can keep `@plexui/ui/vite`,
5
+ * which re-exports it) and adding `plexui()` to your `plugins` array
6
+ * eliminates every install-time gotcha documented in /docs/installation:
7
+ *
8
+ * 1. Pins `css.transformer = 'postcss'` so Lightning CSS doesn't
9
+ * reorder cascade layers strict-by-first-occurrence.
10
+ * 2. Pins `build.cssMinify = 'esbuild'` so Vite 8's Lightning CSS
11
+ * minifier doesn't strip the bare `@layer` declaration.
12
+ * 3. Injects `<style>@layer theme, base, components, utilities;</style>`
13
+ * into <head> as a runtime failsafe — the browser sees the layer
14
+ * order before any stylesheet rule arrives, regardless of how the
15
+ * bundler shuffles things.
16
+ *
17
+ * Usage:
18
+ *
19
+ * // vite.config.ts
20
+ * import { defineConfig } from 'vite';
21
+ * import plexui from '@plexui/styles/vite';
22
+ *
23
+ * export default defineConfig({
24
+ * plugins: [plexui()],
25
+ * });
26
+ *
27
+ * That replaces the manual `@layer …;` declaration in main.css and the
28
+ * manual `css.transformer` / `build.cssMinify` overrides. Your global
29
+ * stylesheet still needs `@import "@plexui/styles/css"` and `@source` —
30
+ * those wire the runtime tokens and Tailwind class-extraction, which a
31
+ * Vite plugin can't do for you.
32
+ */
33
+ const LAYER_DECLARATION = "<style>@layer theme, base, components, utilities;</style>";
34
+ export default function plexui(options = {}) {
35
+ const { injectLayerOrder = true } = options;
36
+ return {
37
+ name: "@plexui/styles",
38
+ enforce: "pre",
39
+ config() {
40
+ return {
41
+ css: { transformer: "postcss" },
42
+ build: { cssMinify: "esbuild" },
43
+ };
44
+ },
45
+ transformIndexHtml(html) {
46
+ if (!injectLayerOrder)
47
+ return html;
48
+ // Place the layer declaration right after <head> so it lands
49
+ // before any bundler-injected <link rel="stylesheet">.
50
+ return html.replace(/(<head[^>]*>)/i, `$1\n ${LAYER_DECLARATION}`);
51
+ },
52
+ };
53
+ }
54
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/vite/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAkBH,MAAM,iBAAiB,GACrB,2DAA2D,CAAA;AAW7D,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,UAA+B,EAAE;IAC9D,MAAM,EAAE,gBAAgB,GAAG,IAAI,EAAE,GAAG,OAAO,CAAA;IAE3C,OAAO;QACL,IAAI,EAAE,gBAAgB;QACtB,OAAO,EAAE,KAAK;QACd,MAAM;YACJ,OAAO;gBACL,GAAG,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE;gBAC/B,KAAK,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE;aAChC,CAAA;QACH,CAAC;QACD,kBAAkB,CAAC,IAAI;YACrB,IAAI,CAAC,gBAAgB;gBAAE,OAAO,IAAI,CAAA;YAClC,6DAA6D;YAC7D,uDAAuD;YACvD,OAAO,IAAI,CAAC,OAAO,CACjB,gBAAgB,EAChB,WAAW,iBAAiB,EAAE,CAC/B,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * Vite plugin for Plex UI.\n *\n * Importing `@plexui/styles/vite` (React apps can keep `@plexui/ui/vite`,\n * which re-exports it) and adding `plexui()` to your `plugins` array\n * eliminates every install-time gotcha documented in /docs/installation:\n *\n * 1. Pins `css.transformer = 'postcss'` so Lightning CSS doesn't\n * reorder cascade layers strict-by-first-occurrence.\n * 2. Pins `build.cssMinify = 'esbuild'` so Vite 8's Lightning CSS\n * minifier doesn't strip the bare `@layer` declaration.\n * 3. Injects `<style>@layer theme, base, components, utilities;</style>`\n * into <head> as a runtime failsafe — the browser sees the layer\n * order before any stylesheet rule arrives, regardless of how the\n * bundler shuffles things.\n *\n * Usage:\n *\n * // vite.config.ts\n * import { defineConfig } from 'vite';\n * import plexui from '@plexui/styles/vite';\n *\n * export default defineConfig({\n * plugins: [plexui()],\n * });\n *\n * That replaces the manual `@layer …;` declaration in main.css and the\n * manual `css.transformer` / `build.cssMinify` overrides. Your global\n * stylesheet still needs `@import \"@plexui/styles/css\"` and `@source` —\n * those wire the runtime tokens and Tailwind class-extraction, which a\n * Vite plugin can't do for you.\n */\n\n// We deliberately do NOT import from \"vite\". Keeping vite out of the\n// build keeps it out of the runtime dependency graph entirely (the\n// plugin file is a few dozen lines and only describes the shape vite\n// expects). Consumers get full type-safety by declaring their config\n// `defineConfig({ plugins: [plexui()] })` — vite's own types cover the\n// `Plugin[]` slot, so our return value is just shaped to match it.\ntype ViteLikePlugin = {\n name: string\n enforce?: \"pre\" | \"post\"\n config?: () => {\n css?: { transformer?: \"postcss\" | \"lightningcss\" }\n build?: { cssMinify?: boolean | \"esbuild\" | \"lightningcss\" }\n }\n transformIndexHtml?: (html: string) => string\n}\n\nconst LAYER_DECLARATION =\n \"<style>@layer theme, base, components, utilities;</style>\"\n\nexport type PlexUIPluginOptions = {\n /**\n * Inject the cascade-layer `<style>` tag into <head> as a runtime\n * failsafe. Defaults to `true`. Disable only if you've already added\n * the equivalent `<style>` tag to your `index.html` by hand.\n */\n injectLayerOrder?: boolean\n}\n\nexport default function plexui(options: PlexUIPluginOptions = {}): ViteLikePlugin {\n const { injectLayerOrder = true } = options\n\n return {\n name: \"@plexui/styles\",\n enforce: \"pre\",\n config() {\n return {\n css: { transformer: \"postcss\" },\n build: { cssMinify: \"esbuild\" },\n }\n },\n transformIndexHtml(html) {\n if (!injectLayerOrder) return html\n // Place the layer declaration right after <head> so it lands\n // before any bundler-injected <link rel=\"stylesheet\">.\n return html.replace(\n /(<head[^>]*>)/i,\n `$1\\n ${LAYER_DECLARATION}`,\n )\n },\n }\n}\n"]}
@@ -0,0 +1,15 @@
1
+ export type Size = "5xs" | "4xs" | "3xs" | "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "6xl";
2
+ export type Sizes<T extends Size = Size> = T;
3
+ export type ControlSize = Sizes<"3xs" | "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl">;
4
+ export type Radius = "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "full";
5
+ export type Radii<T extends Radius = Radius> = T;
6
+ export type TextColor = "base" | "emphasis" | "secondary" | "tertiary";
7
+ export type TextColors<T extends TextColor = TextColor> = T;
8
+ export type SemanticColor = "primary" | "secondary" | "danger" | "success" | "warning" | "caution" | "discovery" | "info";
9
+ export type SemanticColors<T extends SemanticColor = SemanticColor> = T;
10
+ export type Variant = "solid" | "soft" | "outline" | "ghost";
11
+ export type Variants<T extends Variant = Variant> = T;
12
+ export type Alignment = "start" | "center" | "end";
13
+ export type Alignments<T extends Alignment = Alignment> = T;
14
+ export type FontWeight = "inherit" | "normal" | "medium" | "semibold" | "bold";
15
+ export type FontWeights<T extends FontWeight = FontWeight> = T;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Vite plugin for Plex UI.
3
+ *
4
+ * Importing `@plexui/styles/vite` (React apps can keep `@plexui/ui/vite`,
5
+ * which re-exports it) and adding `plexui()` to your `plugins` array
6
+ * eliminates every install-time gotcha documented in /docs/installation:
7
+ *
8
+ * 1. Pins `css.transformer = 'postcss'` so Lightning CSS doesn't
9
+ * reorder cascade layers strict-by-first-occurrence.
10
+ * 2. Pins `build.cssMinify = 'esbuild'` so Vite 8's Lightning CSS
11
+ * minifier doesn't strip the bare `@layer` declaration.
12
+ * 3. Injects `<style>@layer theme, base, components, utilities;</style>`
13
+ * into <head> as a runtime failsafe — the browser sees the layer
14
+ * order before any stylesheet rule arrives, regardless of how the
15
+ * bundler shuffles things.
16
+ *
17
+ * Usage:
18
+ *
19
+ * // vite.config.ts
20
+ * import { defineConfig } from 'vite';
21
+ * import plexui from '@plexui/styles/vite';
22
+ *
23
+ * export default defineConfig({
24
+ * plugins: [plexui()],
25
+ * });
26
+ *
27
+ * That replaces the manual `@layer …;` declaration in main.css and the
28
+ * manual `css.transformer` / `build.cssMinify` overrides. Your global
29
+ * stylesheet still needs `@import "@plexui/styles/css"` and `@source` —
30
+ * those wire the runtime tokens and Tailwind class-extraction, which a
31
+ * Vite plugin can't do for you.
32
+ */
33
+ type ViteLikePlugin = {
34
+ name: string;
35
+ enforce?: "pre" | "post";
36
+ config?: () => {
37
+ css?: {
38
+ transformer?: "postcss" | "lightningcss";
39
+ };
40
+ build?: {
41
+ cssMinify?: boolean | "esbuild" | "lightningcss";
42
+ };
43
+ };
44
+ transformIndexHtml?: (html: string) => string;
45
+ };
46
+ export type PlexUIPluginOptions = {
47
+ /**
48
+ * Inject the cascade-layer `<style>` tag into <head> as a runtime
49
+ * failsafe. Defaults to `true`. Disable only if you've already added
50
+ * the equivalent `<style>` tag to your `index.html` by hand.
51
+ */
52
+ injectLayerOrder?: boolean;
53
+ };
54
+ export default function plexui(options?: PlexUIPluginOptions): ViteLikePlugin;
55
+ export {};
package/package.json CHANGED
@@ -1,6 +1,111 @@
1
1
  {
2
2
  "name": "@plexui/styles",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic CSS for Plex UI: design tokens, globals and component styles",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "private": false,
8
+ "sideEffects": [
9
+ "*.css"
10
+ ],
11
+ "homepage": "https://plexui.com",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/plex-ui/ui.git",
15
+ "directory": "packages/styles"
16
+ },
17
+ "keywords": [
18
+ "css",
19
+ "css-modules",
20
+ "design-system",
21
+ "design-tokens",
22
+ "tailwind",
23
+ "plexui",
24
+ "ui-kit"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "files": [
30
+ "dist/es/**",
31
+ "dist/types/**",
32
+ "postcss/*.mjs",
33
+ "!postcss/*.test.mjs",
34
+ "postcss.config.mjs"
35
+ ],
36
+ "exports": {
37
+ "./css": "./dist/es/styles/index.css",
38
+ "./styles/*": "./dist/es/styles/*",
39
+ "./components/*": "./dist/es/components/*",
40
+ "./vite": {
41
+ "types": "./dist/types/vite/index.d.ts",
42
+ "default": "./dist/es/vite/index.js"
43
+ },
44
+ "./types": {
45
+ "types": "./dist/types/types.d.ts",
46
+ "default": "./dist/es/types.js"
47
+ },
48
+ "./postcss": "./postcss.config.mjs"
49
+ },
50
+ "scripts": {
51
+ "build:clean": "rm -rf dist",
52
+ "build:js": "tsc -p tsconfig.build.json",
53
+ "build:css": "node ./scripts/build-css.mjs",
54
+ "dev:js": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
55
+ "dev:css": "node ./scripts/build-css.mjs --watch",
56
+ "dev": "npm run build:clean && npm run build:js && npm run build:css && (npm run dev:js & npm run dev:css & wait)",
57
+ "build": "npm run build:clean && npm run build:js && npm run build:css",
58
+ "prepublishOnly": "npm run build"
59
+ },
60
+ "peerDependencies": {
61
+ "@tailwindcss/postcss": "^4.0.10",
62
+ "autoprefixer": "^10.4.20",
63
+ "postcss": "^8",
64
+ "postcss-functions": "^4.0.2",
65
+ "postcss-mixins": "^11.0.3",
66
+ "postcss-nested": "^7.0.2",
67
+ "postcss-selector-parser": "^7.1.0",
68
+ "postcss-value-parser": "^4.2.0",
69
+ "tailwindcss": "^4.0.10"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "@tailwindcss/postcss": {
73
+ "optional": true
74
+ },
75
+ "autoprefixer": {
76
+ "optional": true
77
+ },
78
+ "postcss": {
79
+ "optional": true
80
+ },
81
+ "postcss-functions": {
82
+ "optional": true
83
+ },
84
+ "postcss-mixins": {
85
+ "optional": true
86
+ },
87
+ "postcss-nested": {
88
+ "optional": true
89
+ },
90
+ "postcss-selector-parser": {
91
+ "optional": true
92
+ },
93
+ "postcss-value-parser": {
94
+ "optional": true
95
+ }
96
+ },
97
+ "devDependencies": {
98
+ "@tailwindcss/postcss": "4.0.17",
99
+ "@types/postcss-functions": "4.0.4",
100
+ "@types/postcss-mixins": "9.0.6",
101
+ "autoprefixer": "10.4.20",
102
+ "postcss": "^8",
103
+ "postcss-functions": "^4.0.2",
104
+ "postcss-mixins": "^11.0.3",
105
+ "postcss-nested": "^7.0.2",
106
+ "postcss-selector-parser": "7.1.0",
107
+ "postcss-value-parser": "4.2.0",
108
+ "tailwindcss": "4.0.10",
109
+ "typescript": "5.7.3"
110
+ }
111
+ }
@@ -0,0 +1,23 @@
1
+ import postcssFunctions from "postcss-functions"
2
+
3
+ /** @type {import('postcss').PluginCreator} */
4
+ export default function platformUIFunctions() {
5
+ return {
6
+ ...postcssFunctions({
7
+ functions: {
8
+ alpha: (value, amount) => {
9
+ const formattedAmount = amount.includes("%")
10
+ ? amount
11
+ : amount.startsWith(".") || amount.startsWith("0.")
12
+ ? `${parseFloat(amount) * 100}%`
13
+ : `${amount}%`
14
+ return `color-mix(in oklab, ${value} ${formattedAmount}, transparent)`
15
+ },
16
+ spacing: (value) => `calc(var(--spacing) * ${value})`,
17
+ },
18
+ }),
19
+ postcssPlugin: "plexui/components/postcss-functions",
20
+ }
21
+ }
22
+
23
+ platformUIFunctions.postcss = true
@@ -0,0 +1,47 @@
1
+ // @ts-check
2
+ import postcss from "postcss"
3
+
4
+ /**
5
+ * @typedef {import('postcss').PluginCreator<{ breakpoints: Record<string, number> }>} PostcssPluginCreator
6
+ */
7
+
8
+ /**
9
+ * Injects the breakpoint variables into the CSS. Must run before tailwindcss.
10
+ *
11
+ * @type {PostcssPluginCreator}
12
+ */
13
+ export default function injectBreakpoints(
14
+ /** @type {{ breakpoints: Record<string, number> }} */
15
+ { breakpoints },
16
+ ) {
17
+ return {
18
+ postcssPlugin: "plexui/components/inject-breakpoints",
19
+ Once(root) {
20
+ root.walkComments((comment) => {
21
+ // Look for comment containing "@inject_breakpoints"
22
+ if (comment.text.trim() === "@inject_breakpoints") {
23
+ // Generate breakpoint declarations
24
+ const declarations = []
25
+ for (const [key, value] of Object.entries(breakpoints)) {
26
+ declarations.push(
27
+ postcss.decl({
28
+ prop: `--breakpoint-${key}`,
29
+ value: `${value}px`,
30
+ }),
31
+ )
32
+ }
33
+
34
+ // Insert all declarations after the comment
35
+ declarations.reverse().forEach((decl) => {
36
+ comment.parent.insertAfter(comment, decl)
37
+ })
38
+
39
+ // Remove the comment
40
+ comment.remove()
41
+ }
42
+ })
43
+ },
44
+ }
45
+ }
46
+
47
+ injectBreakpoints.postcss = true
@@ -0,0 +1,100 @@
1
+ // @ts-check
2
+ import postcssMixins from "postcss-mixins"
3
+
4
+ /**
5
+ * @typedef {import('postcss').PluginCreator<{ breakpoints: Record<string, number> }>} PostcssPluginCreator
6
+ */
7
+
8
+ /** @type {PostcssPluginCreator} */
9
+ export default function platformUIMixins(
10
+ /** @type {{ breakpoints: Record<string, number> }} */
11
+ { breakpoints },
12
+ ) {
13
+ /** @type {Record<string, string | undefined>} */
14
+ const mediaQueries = Object.entries(breakpoints).reduce((acc, [key, value]) => {
15
+ acc[key] = `(min-width: ${value}px)`
16
+ return acc
17
+ }, {})
18
+
19
+ const supportedBreakpoints = Object.keys(mediaQueries)
20
+ .join(", ")
21
+ .replace(/, ([^,]*)$/, " and $1")
22
+
23
+ return {
24
+ ...postcssMixins({
25
+ mixins: {
26
+ // @mixin breakpoint md { ... } // scoped to current selector (default)
27
+ // @mixin breakpoint md global { ... } // global (no selector wrapper)
28
+ breakpoint(_, targetBreakpointAndScope) {
29
+ const [breakpointName, scope = "scoped"] = targetBreakpointAndScope.trim().split(" ")
30
+
31
+ const mediaQuery = mediaQueries[breakpointName]
32
+
33
+ if (!mediaQuery) {
34
+ throw new Error(
35
+ `"${breakpointName}" is not a valid breakpoint. Supported breakpoints are ${supportedBreakpoints}.`,
36
+ )
37
+ }
38
+
39
+ const isGlobal = typeof scope === "string" && scope.toLowerCase() === "global"
40
+
41
+ return isGlobal
42
+ ? {
43
+ [`@media ${mediaQuery}`]: {
44
+ "@mixin-content": {},
45
+ },
46
+ }
47
+ : {
48
+ [`@media ${mediaQuery}`]: {
49
+ "&": {
50
+ "@mixin-content": {},
51
+ },
52
+ },
53
+ }
54
+ },
55
+
56
+ light() {
57
+ return {
58
+ ':where([data-theme="light"]) &': {
59
+ "@mixin-content": {},
60
+ },
61
+ }
62
+ },
63
+
64
+ dark() {
65
+ return {
66
+ ':where([data-theme="dark"]) &': {
67
+ "@mixin-content": {},
68
+ },
69
+ }
70
+ },
71
+
72
+ hover() {
73
+ return {
74
+ "@media (hover: hover) and (pointer: fine)": {
75
+ "&:hover": {
76
+ "@mixin-content": {},
77
+ },
78
+ },
79
+ }
80
+ },
81
+
82
+ autofill() {
83
+ return {
84
+ "&:-webkit-autofill, &:-webkit-autofill:focus, &:-webkit-autofill:hover": {
85
+ "@mixin-content": {},
86
+ },
87
+ // Identical to block above, but must keep separate for browsers to recognize their own selector
88
+ "&:-webkit-autofill-and-obscured, &:-webkit-autofill-strong-password, &:-webkit-autofill-strong-password-viewable":
89
+ {
90
+ "@mixin-content": {},
91
+ },
92
+ }
93
+ },
94
+ },
95
+ }),
96
+ postcssPlugin: "plexui/components/postcss-mixins",
97
+ }
98
+ }
99
+
100
+ platformUIMixins.postcss = true
@@ -0,0 +1,174 @@
1
+ import postcss from "postcss"
2
+ import selectorParser from "postcss-selector-parser"
3
+ import valueParser from "postcss-value-parser"
4
+
5
+ /** @type {import('postcss').PluginCreator} */
6
+ export default function platformUILightDark() {
7
+ return {
8
+ postcssPlugin: "plexui/components/parse-light-dark",
9
+ OnceExit(root) {
10
+ const stringifyThemeValue = (tokens, which) =>
11
+ tokens.map((tok) => (tok.isLD ? tok[which] : tok.raw)).join("")
12
+ // Split selector list into individual strings using selector parser
13
+ const splitSelectorList = (selector) => {
14
+ const list = []
15
+ selectorParser((rootSel) => {
16
+ rootSel.each((sel) => list.push(sel.toString().trim()))
17
+ }).processSync(selector)
18
+ return list
19
+ }
20
+
21
+ // Determine if selector is exactly :root or [data-theme], optionally wrapped in :where()
22
+ const isSimpleThemeScopeUsingParser = (selectorStr) => {
23
+ let result = false
24
+ selectorParser((rootSel) => {
25
+ if (rootSel.nodes.length !== 1) return
26
+ const sel = rootSel.nodes[0]
27
+ const onlyNode = () => (sel.nodes.length === 1 ? sel.nodes[0] : null)
28
+ const isSimpleNode = (node) =>
29
+ !!node &&
30
+ ((node.type === "pseudo" && node.value === ":root") ||
31
+ (node.type === "attribute" && node.attribute === "data-theme" && !node.operator))
32
+ // Direct simple
33
+ const n = onlyNode()
34
+ if (isSimpleNode(n)) {
35
+ result = true
36
+ return
37
+ }
38
+ // :where(simple)
39
+ if (n && n.type === "pseudo" && n.value === ":where" && n.nodes && n.nodes.length === 1) {
40
+ const innerSel = n.nodes[0]
41
+ if (innerSel.nodes.length === 1 && isSimpleNode(innerSel.nodes[0])) {
42
+ result = true
43
+ }
44
+ }
45
+ }).processSync(selectorStr)
46
+ return result
47
+ }
48
+ /** @type {Map<import('postcss').Rule, Array<{ prop: string; full: Array<any>; important: boolean }>>} */
49
+ const themeMap = new Map()
50
+
51
+ // 1) Collect & remove light-dark() decls, capturing !important
52
+ root.walkDecls((decl) => {
53
+ if (!decl.value.includes("light-dark(")) return
54
+
55
+ const parsed = valueParser(decl.value)
56
+ let sawLD = false
57
+ const tokens = []
58
+
59
+ for (const node of parsed.nodes) {
60
+ if (node.type === "function" && node.value === "light-dark") {
61
+ // inside node.nodes we expect [lightArg, comma, darkArg]
62
+ const [lightNode, , darkNode] = node.nodes
63
+ const light = valueParser.stringify(lightNode).trim()
64
+ const dark = valueParser.stringify(darkNode).trim()
65
+ tokens.push({ isLD: true, light, dark })
66
+ sawLD = true
67
+ } else {
68
+ tokens.push({ isLD: false, raw: valueParser.stringify(node) })
69
+ }
70
+ }
71
+ if (!sawLD) return
72
+
73
+ // store entries
74
+ const arr = themeMap.get(decl.parent) || []
75
+ arr.push({ prop: decl.prop, full: tokens, important: decl.important })
76
+ themeMap.set(decl.parent, arr)
77
+ decl.remove()
78
+ })
79
+
80
+ // 2) Emit transformed rules
81
+ const isSimpleThemeScope = (s) => isSimpleThemeScopeUsingParser(s)
82
+ const appendThemeRules = (parent, afterNode, entries, lightSelector, darkSelector) => {
83
+ const lightRule = postcss.rule({ selector: lightSelector })
84
+ const darkRule = postcss.rule({ selector: darkSelector })
85
+ for (const { prop, full, important } of entries) {
86
+ lightRule.append(
87
+ postcss.decl({ prop, value: stringifyThemeValue(full, "light"), important }),
88
+ )
89
+ darkRule.append(
90
+ postcss.decl({ prop, value: stringifyThemeValue(full, "dark"), important }),
91
+ )
92
+ }
93
+ parent.insertAfter(afterNode, lightRule)
94
+ parent.insertAfter(lightRule, darkRule)
95
+ }
96
+ for (const [rule, entries] of themeMap) {
97
+ const parent = rule.parent
98
+ if (!parent || !rule.selector) continue
99
+
100
+ // root selector
101
+ if (rule.selector === ":root") {
102
+ appendThemeRules(
103
+ parent,
104
+ rule,
105
+ entries,
106
+ ':where(:root), :where([data-theme="light"])',
107
+ ':where([data-theme="dark"])',
108
+ )
109
+
110
+ if (rule.nodes.length === 0) {
111
+ rule.remove()
112
+ }
113
+ } else {
114
+ // non-root selector: flatten to top-level grouped theme rules
115
+ const selList = splitSelectorList(rule.selector)
116
+
117
+ // Special-case: when selectors are only top-level theme scopes
118
+ // (:root and/or [data-theme]), treat them like :root by replacing
119
+ // with :where(:root) and :where([data-theme="light"]) instead of nesting.
120
+ const simpleScopes = selList.filter((s) => isSimpleThemeScope(s))
121
+ const otherSelectors = selList.filter((s) => !isSimpleThemeScope(s))
122
+ if (simpleScopes.length > 0 && otherSelectors.length === 0) {
123
+ appendThemeRules(
124
+ parent,
125
+ rule,
126
+ entries,
127
+ ':where(:root), :where([data-theme="light"])',
128
+ ':where([data-theme="dark"])',
129
+ )
130
+ if (rule.nodes.length === 0) {
131
+ rule.remove()
132
+ }
133
+ } else {
134
+ // Mixed or no simple scopes.
135
+ // - Include base selectors (light defaults) ONLY when there are no simple scopes.
136
+ // - Never nest simple scopes; instead add bare replacements.
137
+ const lightParts = []
138
+ const darkParts = []
139
+
140
+ // Base selectors (light defaults) only if no simple scopes are present
141
+ if (simpleScopes.length === 0) {
142
+ lightParts.push(...selList)
143
+ }
144
+
145
+ // Bare replacements for simple scopes
146
+ if (simpleScopes.includes(":root")) {
147
+ lightParts.push(":where(:root)")
148
+ }
149
+ if (simpleScopes.length > 0) {
150
+ lightParts.push(':where([data-theme="light"])')
151
+ darkParts.push(':where([data-theme="dark"])')
152
+ }
153
+
154
+ // Themed nested only for non-simple selectors
155
+ lightParts.push(...otherSelectors.map((s) => `:where([data-theme="light"]) ${s}`))
156
+ darkParts.push(...otherSelectors.map((s) => `:where([data-theme="dark"]) ${s}`))
157
+
158
+ // Dedupe while preserving order
159
+ const uniq = (arr) => Array.from(new Set(arr))
160
+ const lightSelector = uniq(lightParts).join(", ")
161
+ const darkSelector = uniq(darkParts).join(", ")
162
+
163
+ appendThemeRules(parent, rule, entries, lightSelector, darkSelector)
164
+ if (rule.nodes.length === 0) {
165
+ rule.remove()
166
+ }
167
+ }
168
+ }
169
+ }
170
+ },
171
+ }
172
+ }
173
+
174
+ platformUILightDark.postcss = true
@@ -0,0 +1,21 @@
1
+ // @ts-check
2
+ import postcss from "postcss"
3
+
4
+ /** @type {import('postcss').PluginCreator<{ layer?: string }>} */
5
+ export default function wrapModulesInLayer({ layer = "components" } = {}) {
6
+ return {
7
+ postcssPlugin: "plexui/components/postcss-wrap-modules-in-layer",
8
+ Once(root, { result }) {
9
+ const file = result.opts.from
10
+ if (file && /\.module\.css$/.test(file)) {
11
+ const layerAtRule = postcss.atRule({ name: "layer", params: layer })
12
+ while (root.first) {
13
+ layerAtRule.append(root.first)
14
+ }
15
+ root.append(layerAtRule)
16
+ }
17
+ },
18
+ }
19
+ }
20
+
21
+ wrapModulesInLayer.postcss = true