@robomous/ui-core 0.1.1 → 0.2.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 (135) hide show
  1. package/README.md +15 -7
  2. package/dist/{primitives → components}/alert.js +1 -1
  3. package/dist/{primitives → components}/badge.js +1 -1
  4. package/dist/{primitives → components}/button.d.ts +1 -1
  5. package/dist/{primitives → components}/button.js +4 -1
  6. package/dist/{primitives → components}/card.d.ts +1 -1
  7. package/dist/{primitives → components}/card.js +2 -2
  8. package/dist/{primitives → components}/combobox.js +3 -3
  9. package/dist/{primitives → components}/dialog.js +1 -1
  10. package/dist/{primitives → components}/dropdown-menu.js +11 -6
  11. package/dist/{primitives → components}/field.js +2 -4
  12. package/dist/{primitives → components}/input-group.js +1 -1
  13. package/dist/{primitives → components}/input.js +1 -1
  14. package/dist/{primitives → components}/label.js +1 -1
  15. package/dist/components/progress.js +7 -0
  16. package/dist/{primitives → components}/select.d.ts +3 -1
  17. package/dist/{primitives → components}/select.js +6 -4
  18. package/dist/{primitives → components}/separator.js +1 -1
  19. package/dist/{primitives → components}/sheet.js +1 -1
  20. package/dist/{primitives → components}/skeleton.js +1 -1
  21. package/dist/{primitives → components}/sonner.js +17 -12
  22. package/dist/{primitives → components}/table.d.ts +1 -1
  23. package/dist/{primitives → components}/table.js +3 -3
  24. package/dist/{primitives → components}/tabs.js +1 -1
  25. package/dist/{primitives → components}/textarea.js +1 -1
  26. package/dist/{primitives → components}/tooltip.js +1 -1
  27. package/dist/gates/index.d.ts +28 -0
  28. package/dist/gates/index.js +142 -0
  29. package/dist/index.d.ts +31 -32
  30. package/dist/index.js +32 -33
  31. package/dist/{tokens.d.ts → theme/tokens.d.ts} +5 -5
  32. package/dist/{tokens.js → theme/tokens.js} +12 -12
  33. package/package.json +23 -12
  34. package/{shadcn → src/components}/alert.tsx +14 -21
  35. package/src/{primitives → components}/badge.tsx +15 -21
  36. package/src/{primitives → components}/button.tsx +13 -10
  37. package/{shadcn → src/components}/card.tsx +16 -31
  38. package/src/{primitives → components}/combobox.test.tsx +15 -2
  39. package/src/{primitives → components}/combobox.tsx +45 -74
  40. package/src/{primitives/primitives.test.tsx → components/components.test.tsx} +106 -69
  41. package/src/{primitives → components}/dialog.tsx +31 -54
  42. package/src/{primitives → components}/dropdown-menu.tsx +51 -65
  43. package/src/{primitives → components}/field.tsx +48 -62
  44. package/src/{primitives → components}/input-group.tsx +37 -49
  45. package/{shadcn → src/components}/input.tsx +5 -5
  46. package/{shadcn → src/components}/label.tsx +7 -10
  47. package/src/{primitives → components}/progress.tsx +7 -6
  48. package/{shadcn → src/components}/select.tsx +39 -41
  49. package/{shadcn → src/components}/separator.tsx +7 -7
  50. package/src/{primitives → components}/sheet.tsx +27 -44
  51. package/{shadcn → src/components}/skeleton.tsx +3 -3
  52. package/src/{primitives → components}/sonner.test.tsx +12 -4
  53. package/src/components/sonner.tsx +67 -0
  54. package/{shadcn → src/components}/table.tsx +17 -44
  55. package/{shadcn → src/components}/tabs.tsx +16 -26
  56. package/{shadcn → src/components}/textarea.tsx +5 -5
  57. package/{shadcn → src/components}/tooltip.tsx +12 -16
  58. package/src/gates/design.test.ts +188 -0
  59. package/src/gates/index.test.ts +11 -0
  60. package/src/gates/index.ts +165 -0
  61. package/src/gates/tokens.test.ts +215 -0
  62. package/src/harness.test.tsx +1 -1
  63. package/src/index.ts +63 -33
  64. package/src/theme/imports.test.ts +160 -0
  65. package/src/{statusTone.test.ts → theme/statusTone.test.ts} +5 -1
  66. package/src/{styles.css → theme/styles.css} +45 -31
  67. package/src/theme/tailwind.css +646 -0
  68. package/src/{tokens.test.ts → theme/tokens.test.ts} +30 -23
  69. package/src/{tokens.ts → theme/tokens.ts} +12 -12
  70. package/components.json +0 -13
  71. package/dist/lib/button.d.ts +0 -1
  72. package/dist/lib/button.js +0 -4
  73. package/dist/lib/cn.d.ts +0 -18
  74. package/dist/lib/cn.js +0 -21
  75. package/dist/lib/menu.d.ts +0 -1
  76. package/dist/lib/menu.js +0 -16
  77. package/dist/lib/progress.d.ts +0 -3
  78. package/dist/lib/progress.js +0 -6
  79. package/dist/lib/select.d.ts +0 -1
  80. package/dist/lib/select.js +0 -3
  81. package/dist/primitives/progress.js +0 -7
  82. package/gates/canonical.test.mjs +0 -71
  83. package/gates/extensions.test.mjs +0 -373
  84. package/gates/index.d.mts +0 -51
  85. package/gates/index.mjs +0 -514
  86. package/gates/tokens.test.mjs +0 -276
  87. package/shadcn/README.md +0 -13
  88. package/shadcn/badge.tsx +0 -49
  89. package/shadcn/button.tsx +0 -67
  90. package/shadcn/combobox.tsx +0 -299
  91. package/shadcn/dialog.tsx +0 -166
  92. package/shadcn/dropdown-menu.tsx +0 -267
  93. package/shadcn/field.tsx +0 -236
  94. package/shadcn/input-group.tsx +0 -154
  95. package/shadcn/progress.tsx +0 -29
  96. package/shadcn/sheet.tsx +0 -145
  97. package/shadcn/sonner.tsx +0 -49
  98. package/src/lib/button.ts +0 -4
  99. package/src/lib/cn.test.ts +0 -33
  100. package/src/lib/cn.ts +0 -23
  101. package/src/lib/menu.ts +0 -16
  102. package/src/lib/progress.ts +0 -6
  103. package/src/lib/select.ts +0 -4
  104. package/src/primitives/alert.tsx +0 -76
  105. package/src/primitives/card.tsx +0 -103
  106. package/src/primitives/input.tsx +0 -19
  107. package/src/primitives/label.tsx +0 -22
  108. package/src/primitives/select.tsx +0 -192
  109. package/src/primitives/separator.tsx +0 -28
  110. package/src/primitives/skeleton.tsx +0 -13
  111. package/src/primitives/sonner.tsx +0 -65
  112. package/src/primitives/table.tsx +0 -114
  113. package/src/primitives/tabs.tsx +0 -88
  114. package/src/primitives/textarea.tsx +0 -18
  115. package/src/primitives/tooltip.tsx +0 -57
  116. /package/dist/{primitives → components}/alert.d.ts +0 -0
  117. /package/dist/{primitives → components}/badge.d.ts +0 -0
  118. /package/dist/{primitives → components}/combobox.d.ts +0 -0
  119. /package/dist/{primitives → components}/dialog.d.ts +0 -0
  120. /package/dist/{primitives → components}/dropdown-menu.d.ts +0 -0
  121. /package/dist/{primitives → components}/field.d.ts +0 -0
  122. /package/dist/{primitives → components}/input-group.d.ts +0 -0
  123. /package/dist/{primitives → components}/input.d.ts +0 -0
  124. /package/dist/{primitives → components}/label.d.ts +0 -0
  125. /package/dist/{primitives → components}/progress.d.ts +0 -0
  126. /package/dist/{primitives → components}/separator.d.ts +0 -0
  127. /package/dist/{primitives → components}/sheet.d.ts +0 -0
  128. /package/dist/{primitives → components}/skeleton.d.ts +0 -0
  129. /package/dist/{primitives → components}/sonner.d.ts +0 -0
  130. /package/dist/{primitives → components}/tabs.d.ts +0 -0
  131. /package/dist/{primitives → components}/textarea.d.ts +0 -0
  132. /package/dist/{primitives → components}/tooltip.d.ts +0 -0
  133. /package/dist/{statusTone.d.ts → theme/statusTone.d.ts} +0 -0
  134. /package/dist/{statusTone.js → theme/statusTone.js} +0 -0
  135. /package/src/{statusTone.ts → theme/statusTone.ts} +0 -0
package/README.md CHANGED
@@ -4,9 +4,9 @@
4
4
 
5
5
  # @robomous/ui-core
6
6
 
7
- The Robomous design system: shadcn Nova primitives under Radix behaviour, the foundation design
8
- tokens, and the gates that keep them canonical. Extracted from
9
- [Robomous/VisionSet](https://github.com/Robomous/VisionSet), where it grew up.
7
+ The Robomous design system: twenty-one React components this package owns outright, the design
8
+ tokens they resolve through, and the gates that hold the rules the design system is made of.
9
+ Extracted from [Robomous/VisionSet](https://github.com/Robomous/VisionSet), where it grew up.
10
10
 
11
11
  ## Install
12
12
 
@@ -27,13 +27,21 @@ The stylesheet is the contract: Tailwind v4 reads its `@theme` block, so `bg-pri
27
27
  component here and `bg-primary` in a consuming app are the same colour by construction. A
28
28
  consumer adds its own extensions in its own stylesheet after importing this one.
29
29
 
30
+ ## Adding a component
31
+
32
+ Write it. A component is a file in `src/components/` that wraps Radix or Base UI behaviour,
33
+ spells colour only through the tokens, keeps its own geometry, and is exported by name from
34
+ `src/index.ts`. [DESIGN.md](DESIGN.md) has the checklist and the reasoning behind each item; the
35
+ gates hold the parts of it that a review would otherwise have to catch by eye.
36
+
30
37
  ## The gates
31
38
 
32
39
  The rules that keep the design system honest are published alongside it:
33
- `import { ... } from "@robomous/ui-core/gates"` gives a consumer the same pure scan helpers and
34
- foundation facts this repo's own gate tests run — canonical-plus-additive primitives, one status
35
- palette with one home, no colour in a class string, brand as identity only. The governance, and
36
- what each gate holds, is [DESIGN.md](DESIGN.md).
40
+ `import { ... } from "@robomous/ui-core/gates"` gives a consumer the same eight pure scan helpers
41
+ and token facts this repo's own gates run — no colour in a class string, one status palette with
42
+ one home, no rival colour family standing in for it, brand as identity only, and the token names
43
+ read off the shipped stylesheet so a consumer can prove none of its own extensions shadows one.
44
+ The rules themselves, and which gate holds each, are in [DESIGN.md](DESIGN.md).
37
45
 
38
46
  ## Release
39
47
 
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cva } from "class-variance-authority";
3
- import { cn } from "../lib/cn.js";
3
+ import { cn } from "cn";
4
4
  const alertVariants = cva("group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4", {
5
5
  variants: {
6
6
  variant: {
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cva } from "class-variance-authority";
3
3
  import { Slot } from "radix-ui";
4
- import { cn } from "../lib/cn.js";
4
+ import { cn } from "cn";
5
5
  const badgeVariants = cva("group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", {
6
6
  variants: {
7
7
  variant: {
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const buttonVariants: (props?: ({
4
4
  variant?: "secondary" | "destructive" | "default" | "outline" | "ghost" | "link" | null | undefined;
5
- size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
5
+ size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | "inline" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
8
8
  asChild?: boolean;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cva } from "class-variance-authority";
3
3
  import { Slot } from "radix-ui";
4
- import { cn } from "../lib/cn.js";
4
+ import { cn } from "cn";
5
5
  const buttonVariants = cva("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
6
6
  variants: {
7
7
  variant: {
@@ -21,6 +21,9 @@ const buttonVariants = cva("group/button inline-flex shrink-0 items-center justi
21
21
  "icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
22
22
  "icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
23
23
  "icon-lg": "size-9",
24
+ // A link button is inline prose, not a boxed control: no height of its
25
+ // own and no padding, so it sits inside a sentence or a table cell.
26
+ inline: "h-auto p-0",
24
27
  },
25
28
  },
26
29
  defaultVariants: {
@@ -8,4 +8,4 @@ declare function CardDescription({ className, ...props }: React.ComponentProps<"
8
8
  declare function CardAction({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
9
9
  declare function CardContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
10
10
  declare function CardFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
11
- export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
11
+ export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cn } from "../lib/cn.js";
2
+ import { cn } from "cn";
3
3
  function Card({ className, size = "default", ...props }) {
4
4
  return (_jsx("div", { "data-slot": "card", "data-size": size, className: cn("group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", className), ...props }));
5
5
  }
@@ -21,4 +21,4 @@ function CardContent({ className, ...props }) {
21
21
  function CardFooter({ className, ...props }) {
22
22
  return (_jsx("div", { "data-slot": "card-footer", className: cn("flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)", className), ...props }));
23
23
  }
24
- export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
24
+ export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
@@ -2,9 +2,9 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Combobox as ComboboxPrimitive } from "@base-ui/react";
5
- import { cn } from "../lib/cn.js";
5
+ import { cn } from "cn";
6
6
  import { Button } from "./button.js";
7
- import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, } from "./input-group.js";
7
+ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "./input-group.js";
8
8
  import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react";
9
9
  const Combobox = ComboboxPrimitive.Root;
10
10
  function ComboboxValue({ ...props }) {
@@ -35,7 +35,7 @@ function ComboboxLabel({ className, ...props }) {
35
35
  return (_jsx(ComboboxPrimitive.GroupLabel, { "data-slot": "combobox-label", className: cn("px-2 py-1.5 text-xs text-muted-foreground", className), ...props }));
36
36
  }
37
37
  function ComboboxCollection({ ...props }) {
38
- return (_jsx(ComboboxPrimitive.Collection, { "data-slot": "combobox-collection", ...props }));
38
+ return _jsx(ComboboxPrimitive.Collection, { "data-slot": "combobox-collection", ...props });
39
39
  }
40
40
  function ComboboxEmpty({ className, ...props }) {
41
41
  return (_jsx(ComboboxPrimitive.Empty, { "data-slot": "combobox-empty", className: cn("hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex", className), ...props }));
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Dialog as DialogPrimitive } from "radix-ui";
3
- import { cn } from "../lib/cn.js";
3
+ import { cn } from "cn";
4
4
  import { Button } from "./button.js";
5
5
  import { XIcon } from "lucide-react";
6
6
  function Dialog({ ...props }) {
@@ -1,21 +1,26 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
3
- import { cn } from "../lib/cn.js";
3
+ import { cn } from "cn";
4
4
  import { CheckIcon, ChevronRightIcon } from "lucide-react";
5
5
  function DropdownMenu({ ...props }) {
6
6
  return _jsx(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
7
7
  }
8
8
  function DropdownMenuPortal({ ...props }) {
9
- return (_jsx(DropdownMenuPrimitive.Portal, { "data-slot": "dropdown-menu-portal", ...props }));
9
+ return _jsx(DropdownMenuPrimitive.Portal, { "data-slot": "dropdown-menu-portal", ...props });
10
10
  }
11
11
  function DropdownMenuTrigger({ ...props }) {
12
- return (_jsx(DropdownMenuPrimitive.Trigger, { "data-slot": "dropdown-menu-trigger", ...props }));
12
+ return _jsx(DropdownMenuPrimitive.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
13
13
  }
14
+ // A menu sizes to its items, not to its trigger — behind an icon-sized button
15
+ // the trigger width is a 128px ceiling that wraps every longer item; min-w-32
16
+ // survives as the floor it was meant to be. And it leaves on the frame it is
17
+ // dismissed: while an exit animation runs, the dismissable layer stays mounted
18
+ // and swallows the press that should open the next menu.
14
19
  function DropdownMenuContent({ className, align = "start", sideOffset = 4, ...props }) {
15
- return (_jsx(DropdownMenuPrimitive.Portal, { children: _jsx(DropdownMenuPrimitive.Content, { "data-slot": "dropdown-menu-content", sideOffset: sideOffset, align: align, className: cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className), ...props }) }));
20
+ return (_jsx(DropdownMenuPrimitive.Portal, { children: _jsx(DropdownMenuPrimitive.Content, { "data-slot": "dropdown-menu-content", sideOffset: sideOffset, align: align, className: cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-auto min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95", className), ...props }) }));
16
21
  }
17
22
  function DropdownMenuGroup({ ...props }) {
18
- return (_jsx(DropdownMenuPrimitive.Group, { "data-slot": "dropdown-menu-group", ...props }));
23
+ return _jsx(DropdownMenuPrimitive.Group, { "data-slot": "dropdown-menu-group", ...props });
19
24
  }
20
25
  function DropdownMenuItem({ className, inset, variant = "default", ...props }) {
21
26
  return (_jsx(DropdownMenuPrimitive.Item, { "data-slot": "dropdown-menu-item", "data-inset": inset, "data-variant": variant, className: cn("group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive", className), ...props }));
@@ -24,7 +29,7 @@ function DropdownMenuCheckboxItem({ className, children, checked, inset, ...prop
24
29
  return (_jsxs(DropdownMenuPrimitive.CheckboxItem, { "data-slot": "dropdown-menu-checkbox-item", "data-inset": inset, className: cn("relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), checked: checked, ...props, children: [_jsx("span", { className: "pointer-events-none absolute right-2 flex items-center justify-center", "data-slot": "dropdown-menu-checkbox-item-indicator", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(CheckIcon, {}) }) }), children] }));
25
30
  }
26
31
  function DropdownMenuRadioGroup({ ...props }) {
27
- return (_jsx(DropdownMenuPrimitive.RadioGroup, { "data-slot": "dropdown-menu-radio-group", ...props }));
32
+ return _jsx(DropdownMenuPrimitive.RadioGroup, { "data-slot": "dropdown-menu-radio-group", ...props });
28
33
  }
29
34
  function DropdownMenuRadioItem({ className, children, inset, ...props }) {
30
35
  return (_jsxs(DropdownMenuPrimitive.RadioItem, { "data-slot": "dropdown-menu-radio-item", "data-inset": inset, className: cn("relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [_jsx("span", { className: "pointer-events-none absolute right-2 flex items-center justify-center", "data-slot": "dropdown-menu-radio-item-indicator", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(CheckIcon, {}) }) }), children] }));
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { cva } from "class-variance-authority";
4
- import { cn } from "../lib/cn.js";
4
+ import { cn } from "cn";
5
5
  import { Label } from "./label.js";
6
6
  import { Separator } from "./separator.js";
7
7
  function FieldSet({ className, ...props }) {
@@ -51,9 +51,7 @@ function FieldError({ className, children, errors, ...props }) {
51
51
  if (!errors?.length) {
52
52
  return null;
53
53
  }
54
- const uniqueErrors = [
55
- ...new Map(errors.map((error) => [error?.message, error])).values(),
56
- ];
54
+ const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()];
57
55
  if (uniqueErrors?.length == 1) {
58
56
  return uniqueErrors[0]?.message;
59
57
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cva } from "class-variance-authority";
3
- import { cn } from "../lib/cn.js";
3
+ import { cn } from "cn";
4
4
  import { Button } from "./button.js";
5
5
  import { Input } from "./input.js";
6
6
  import { Textarea } from "./textarea.js";
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cn } from "../lib/cn.js";
2
+ import { cn } from "cn";
3
3
  function Input({ className, type, ...props }) {
4
4
  return (_jsx("input", { type: type, "data-slot": "input", className: cn("h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className), ...props }));
5
5
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Label as LabelPrimitive } from "radix-ui";
3
- import { cn } from "../lib/cn.js";
3
+ import { cn } from "cn";
4
4
  function Label({ className, ...props }) {
5
5
  return (_jsx(LabelPrimitive.Root, { "data-slot": "label", className: cn("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", className), ...props }));
6
6
  }
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Progress as ProgressPrimitive } from "radix-ui";
3
+ import { cn } from "cn";
4
+ function Progress({ className, value, ...props }) {
5
+ return (_jsx(ProgressPrimitive.Root, { "data-slot": "progress", value: value, className: cn("relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-muted", className), ...props, children: _jsx(ProgressPrimitive.Indicator, { "data-slot": "progress-indicator", className: "size-full flex-1 bg-primary transition-all", style: { transform: `translateX(-${100 - (value || 0)}%)` } }) }));
6
+ }
7
+ export { Progress };
@@ -3,8 +3,10 @@ import { Select as SelectPrimitive } from "radix-ui";
3
3
  declare function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>): React.JSX.Element;
4
4
  declare function SelectGroup({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Group>): React.JSX.Element;
5
5
  declare function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>): React.JSX.Element;
6
- declare function SelectTrigger({ className, size, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
6
+ declare function SelectTrigger({ className, size, multiline, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
7
7
  size?: "sm" | "default";
8
+ /** Let the closed trigger grow to its value instead of clamping it to one line. */
9
+ multiline?: boolean;
8
10
  }): React.JSX.Element;
9
11
  declare function SelectContent({ className, children, position, align, ...props }: React.ComponentProps<typeof SelectPrimitive.Content>): React.JSX.Element;
10
12
  declare function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>): React.JSX.Element;
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Select as SelectPrimitive } from "radix-ui";
4
- import { cn } from "../lib/cn.js";
4
+ import { cn } from "cn";
5
5
  import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
6
6
  function Select({ ...props }) {
7
7
  return _jsx(SelectPrimitive.Root, { "data-slot": "select", ...props });
@@ -12,11 +12,13 @@ function SelectGroup({ className, ...props }) {
12
12
  function SelectValue({ ...props }) {
13
13
  return _jsx(SelectPrimitive.Value, { "data-slot": "select-value", ...props });
14
14
  }
15
- function SelectTrigger({ className, size = "default", children, ...props }) {
16
- return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: cn("flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx(ChevronDownIcon, { className: "pointer-events-none size-4 text-muted-foreground" }) })] }));
15
+ function SelectTrigger({ className, size = "default", multiline = false, children, ...props }) {
16
+ return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: cn("flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", multiline &&
17
+ "data-[size=default]:h-auto min-h-8 *:data-[slot=select-value]:line-clamp-none", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx(ChevronDownIcon, { className: "pointer-events-none size-4 text-muted-foreground" }) })] }));
17
18
  }
18
19
  function SelectContent({ className, children, position = "item-aligned", align = "center", ...props }) {
19
- return (_jsx(SelectPrimitive.Portal, { children: _jsxs(SelectPrimitive.Content, { "data-slot": "select-content", "data-align-trigger": position === "item-aligned", className: cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className), position: position, align: align, ...props, children: [_jsx(SelectScrollUpButton, {}), _jsx(SelectPrimitive.Viewport, { "data-position": position, className: cn("data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)", position === "popper" && ""), children: children }), _jsx(SelectScrollDownButton, {})] }) }));
20
+ return (_jsx(SelectPrimitive.Portal, { children: _jsxs(SelectPrimitive.Content, { "data-slot": "select-content", "data-align-trigger": position === "item-aligned", className: cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position === "popper" &&
21
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className), position: position, align: align, ...props, children: [_jsx(SelectScrollUpButton, {}), _jsx(SelectPrimitive.Viewport, { "data-position": position, className: cn("data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)", position === "popper" && ""), children: children }), _jsx(SelectScrollDownButton, {})] }) }));
20
22
  }
21
23
  function SelectLabel({ className, ...props }) {
22
24
  return (_jsx(SelectPrimitive.Label, { "data-slot": "select-label", className: cn("px-1.5 py-1 text-xs text-muted-foreground", className), ...props }));
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { Separator as SeparatorPrimitive } from "radix-ui";
4
- import { cn } from "../lib/cn.js";
4
+ import { cn } from "cn";
5
5
  function Separator({ className, orientation = "horizontal", decorative = true, ...props }) {
6
6
  return (_jsx(SeparatorPrimitive.Root, { "data-slot": "separator", decorative: decorative, orientation: orientation, className: cn("shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch", className), ...props }));
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Dialog as SheetPrimitive } from "radix-ui";
3
- import { cn } from "../lib/cn.js";
3
+ import { cn } from "cn";
4
4
  import { Button } from "./button.js";
5
5
  import { XIcon } from "lucide-react";
6
6
  function Sheet({ ...props }) {
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cn } from "../lib/cn.js";
2
+ import { cn } from "cn";
3
3
  function Skeleton({ className, ...props }) {
4
4
  return (_jsx("div", { "data-slot": "skeleton", className: cn("animate-pulse rounded-md bg-muted", className), ...props }));
5
5
  }
@@ -2,14 +2,19 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Toaster as Sonner } from "sonner";
5
- import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react";
6
- // SHADCN FRAMEWORK ADAPTER: shadcn's sonner reads next-themes (a Next.js
7
- // integration). VisionSet is Vite with one theme source, `.dark` on <html>
8
- // (styles.css `@custom-variant dark`). This hook is the only difference from
9
- // frontend/ui-core/shadcn/sonner.tsx; drop it if the app ever adopts a
10
- // provider that shadcn's file can read.
5
+ import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon, } from "lucide-react";
6
+ // The theme this package runs on has one source: the `dark` class on <html>,
7
+ // which is what `styles.css`'s `@custom-variant dark` keys off and therefore
8
+ // what every other component in here already follows. A toast is portalled
9
+ // outside the tree but reads the same page, so it has to answer to the same
10
+ // source hence this hook rather than a `next-themes` `useTheme`, which asks
11
+ // for a React provider none of this runs under. Sonner needs the answer as a
12
+ // value, not a class, which is the only reason the class has to be observed
13
+ // at all.
11
14
  function useTheme() {
12
- const read = () => (typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "light");
15
+ const read = () => typeof document !== "undefined" && document.documentElement.classList.contains("dark")
16
+ ? "dark"
17
+ : "light";
13
18
  const [theme, setTheme] = React.useState(read);
14
19
  React.useEffect(() => {
15
20
  const observer = new MutationObserver(() => setTheme(read()));
@@ -21,11 +26,11 @@ function useTheme() {
21
26
  const Toaster = ({ ...props }) => {
22
27
  const { theme = "system" } = useTheme();
23
28
  return (_jsx(Sonner, { theme: theme, className: "toaster group", icons: {
24
- success: (_jsx(CircleCheckIcon, { className: "size-4" })),
25
- info: (_jsx(InfoIcon, { className: "size-4" })),
26
- warning: (_jsx(TriangleAlertIcon, { className: "size-4" })),
27
- error: (_jsx(OctagonXIcon, { className: "size-4" })),
28
- loading: (_jsx(Loader2Icon, { className: "size-4 animate-spin" })),
29
+ success: _jsx(CircleCheckIcon, { className: "size-4" }),
30
+ info: _jsx(InfoIcon, { className: "size-4" }),
31
+ warning: _jsx(TriangleAlertIcon, { className: "size-4" }),
32
+ error: _jsx(OctagonXIcon, { className: "size-4" }),
33
+ loading: _jsx(Loader2Icon, { className: "size-4 animate-spin" }),
29
34
  }, style: {
30
35
  "--normal-bg": "var(--popover)",
31
36
  "--normal-text": "var(--popover-foreground)",
@@ -7,4 +7,4 @@ declare function TableRow({ className, ...props }: React.ComponentProps<"tr">):
7
7
  declare function TableHead({ className, ...props }: React.ComponentProps<"th">): React.JSX.Element;
8
8
  declare function TableCell({ className, ...props }: React.ComponentProps<"td">): React.JSX.Element;
9
9
  declare function TableCaption({ className, ...props }: React.ComponentProps<"caption">): React.JSX.Element;
10
- export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, };
10
+ export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cn } from "../lib/cn.js";
2
+ import { cn } from "cn";
3
3
  function Table({ className, ...props }) {
4
4
  return (_jsx("div", { "data-slot": "table-container", className: "relative w-full overflow-x-auto", children: _jsx("table", { "data-slot": "table", className: cn("w-full caption-bottom text-sm", className), ...props }) }));
5
5
  }
6
6
  function TableHeader({ className, ...props }) {
7
- return (_jsx("thead", { "data-slot": "table-header", className: cn("[&_tr]:border-b", className), ...props }));
7
+ return _jsx("thead", { "data-slot": "table-header", className: cn("[&_tr]:border-b", className), ...props });
8
8
  }
9
9
  function TableBody({ className, ...props }) {
10
10
  return (_jsx("tbody", { "data-slot": "table-body", className: cn("[&_tr:last-child]:border-0", className), ...props }));
@@ -24,4 +24,4 @@ function TableCell({ className, ...props }) {
24
24
  function TableCaption({ className, ...props }) {
25
25
  return (_jsx("caption", { "data-slot": "table-caption", className: cn("mt-4 text-sm text-muted-foreground", className), ...props }));
26
26
  }
27
- export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, };
27
+ export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cva } from "class-variance-authority";
3
3
  import { Tabs as TabsPrimitive } from "radix-ui";
4
- import { cn } from "../lib/cn.js";
4
+ import { cn } from "cn";
5
5
  function Tabs({ className, orientation = "horizontal", ...props }) {
6
6
  return (_jsx(TabsPrimitive.Root, { "data-slot": "tabs", "data-orientation": orientation, className: cn("group/tabs flex gap-2 data-horizontal:flex-col", className), ...props }));
7
7
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cn } from "../lib/cn.js";
2
+ import { cn } from "cn";
3
3
  function Textarea({ className, ...props }) {
4
4
  return (_jsx("textarea", { "data-slot": "textarea", className: cn("flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className), ...props }));
5
5
  }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Tooltip as TooltipPrimitive } from "radix-ui";
4
- import { cn } from "../lib/cn.js";
4
+ import { cn } from "cn";
5
5
  function TooltipProvider({ delayDuration = 0, ...props }) {
6
6
  return (_jsx(TooltipPrimitive.Provider, { "data-slot": "tooltip-provider", delayDuration: delayDuration, ...props }));
7
7
  }
@@ -0,0 +1,28 @@
1
+ /** Every `file:line` in `text` painting with the status palette, outside a comment. */
2
+ export declare function statusPaletteIn(file: string, text: string): string[];
3
+ /** Every `file:line` in `text` reaching for a colour family that competes with the status palette. */
4
+ export declare function competingStatusPaletteIn(file: string, text: string): string[];
5
+ /** Every `file:line` in `text` that puts a colour inside a Tailwind class. */
6
+ export declare function colouredClassesIn(file: string, text: string): string[];
7
+ /** Every line in `text` that paints with the brand colour. */
8
+ export declare function brandUsagesIn(file: string, text: string): {
9
+ file: string;
10
+ at: number;
11
+ text: string;
12
+ }[];
13
+ /**
14
+ * The `{ … }` body belonging to the block whose header (e.g. `:root {` or
15
+ * `.dark {`) appears first in the file, matched by brace depth rather than by
16
+ * the next `\n}` so a nested `calc(...)` or `color-mix(...)` cannot fool it.
17
+ */
18
+ export declare function blockBody(css: string, header: string): string;
19
+ /** Every `--name: value;` declaration in a block, keyed WITH the leading `--`. */
20
+ export declare function rawDeclarations(block: string): Map<string, string>;
21
+ /** The same, keyed WITHOUT the leading `--` — the shape `LIGHT_THEME` uses. */
22
+ export declare function declarations(block: string): Map<string, string>;
23
+ /**
24
+ * The foundation token names, read off the shipped stylesheet's `:root` so
25
+ * they can never drift from what actually runs. `radius` is a geometry
26
+ * setting, not a colour token, and is excluded — it matches LIGHT_THEME's keys.
27
+ */
28
+ export declare function foundationTokenNames(): string[];
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @robomous/ui-core/gates — the pure helpers and token facts that keep the
3
+ * design system honest, published so every consumer repo can run the same
4
+ * gates over its own sources with its own extensions registry.
5
+ *
6
+ * Eight exports: four scanners over one file's text, three readers for a
7
+ * stylesheet's blocks and declarations, and the token names read off the
8
+ * stylesheet that ships. DESIGN.md names the rule each one holds.
9
+ */
10
+ import { readFileSync } from "node:fs";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ const PKG = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
14
+ const COMMENT = /^\s*(?:\/\/|\/\*|\*|#)/;
15
+ // ---- status palette discipline ----
16
+ /**
17
+ * The status palette's one Tailwind family: emerald/amber/sky, across every
18
+ * prefix that can carry a colour. `Badge` and `statusTone.ts` are its one
19
+ * home — see `statusTone.ts`'s own docstring — so a third place naming the
20
+ * family is a fork of the palette, not a use of it.
21
+ */
22
+ const STATUS_PALETTE = /\b(?:bg|text|border|ring|fill|stroke|from|to|via|outline|decoration|shadow)-(?:emerald|amber|sky)-\d/;
23
+ /** Every `file:line` in `text` painting with the status palette, outside a comment. */
24
+ export function statusPaletteIn(file, text) {
25
+ return text
26
+ .split("\n")
27
+ .map((line, index) => ({ line, at: index + 1 }))
28
+ .filter(({ line }) => !COMMENT.test(line) && STATUS_PALETTE.test(line))
29
+ .map(({ line, at }) => `${file}:${at}: ${line.trim()}`);
30
+ }
31
+ /**
32
+ * A colour family that competes with the status palette for the same job —
33
+ * "a warning", "a success" — and so could stand in for it undetected. Unlike
34
+ * the palette itself these have no allowed home anywhere in a consumer.
35
+ */
36
+ const COMPETING_PALETTE = /\b(?:bg|text|border)-(?:green|lime|teal|yellow|orange|blue|cyan|red)-\d/;
37
+ /** Every `file:line` in `text` reaching for a colour family that competes with the status palette. */
38
+ export function competingStatusPaletteIn(file, text) {
39
+ return text
40
+ .split("\n")
41
+ .map((line, index) => ({ line, at: index + 1 }))
42
+ .filter(({ line }) => !COMMENT.test(line) && COMPETING_PALETTE.test(line))
43
+ .map(({ line, at }) => `${file}:${at}: ${line.trim()}`);
44
+ }
45
+ // ---- colour discipline (from tests/scripts/design_tokens.test.mjs) ----
46
+ /**
47
+ * A Tailwind arbitrary value whose content is a colour.
48
+ *
49
+ * Assembled from fragments so this file does not match itself. The `-` before
50
+ * the bracket is what makes it a *utility* rather than an array index or a
51
+ * TypeScript tuple type.
52
+ */
53
+ const HEX = ["#", "[0-9a-fA-F]{3,8}"].join("");
54
+ const LITERAL = String.raw `(?:${HEX}|rgba?\(|hsla?\(|oklch\()`;
55
+ // A raw colour right inside the bracket…
56
+ const ARBITRARY_COLOUR = new RegExp(String.raw `-\[\s*(?:${LITERAL}|var\(\s*--)`);
57
+ // The one colour-mix the preset writes mixes tokens only:
58
+ // `color-mix(in_oklch,var(--secondary),var(--foreground)_5%)`. Any other
59
+ // argument shape — a literal, a named colour, a bare number — is a colour.
60
+ const TOKEN_MIX = String.raw `color-mix\(in_[a-z0-9-]+(?:,var\(--[a-z0-9-]+\)(?:_\d+(?:\.\d+)?%)?)+\)`;
61
+ const BRACKET_MIX = new RegExp(String.raw `-\[\s*color-mix\([^\]]*\]`);
62
+ const ALLOWED_MIX = new RegExp(String.raw `-\[\s*${TOKEN_MIX}\]`);
63
+ /** Every `file:line` in `text` that puts a colour inside a Tailwind class. */
64
+ export function colouredClassesIn(file, text) {
65
+ return text
66
+ .split("\n")
67
+ .map((line, index) => ({ line, at: index + 1 }))
68
+ .filter(({ line }) => !COMMENT.test(line) &&
69
+ (ARBITRARY_COLOUR.test(line) || (BRACKET_MIX.test(line) && !ALLOWED_MIX.test(line))))
70
+ .map(({ line, at }) => `${file}:${at}: ${line.trim()}`);
71
+ }
72
+ /**
73
+ * `DESIGN.md` "Where the brand is": the brand is identity, not a functional-UI
74
+ * colour — the wordmark and the styleguide swatch that shows it off, nothing
75
+ * a person acts on. This is not a headcount: the gate does not exist to hold
76
+ * a count of sites, it exists so brand can never migrate onto a control (a
77
+ * button, a progress fill, anything with a function) instead of staying the
78
+ * one place it is allowed to just be seen. A pure function over one file's
79
+ * text, so the gate is provable with fabricated input, and `COMMENT` keeps
80
+ * the styles.css line that *states* the rule from counting as a usage of it.
81
+ */
82
+ const BRAND_UTILITY = /\b(?:bg|text|border|ring|fill|stroke)-brand\b/;
83
+ /** Every line in `text` that paints with the brand colour. */
84
+ export function brandUsagesIn(file, text) {
85
+ return text
86
+ .split("\n")
87
+ .map((line, index) => ({ line, at: index + 1 }))
88
+ .filter(({ line }) => !COMMENT.test(line) && BRAND_UTILITY.test(line))
89
+ .map(({ line, at }) => ({ file, at, text: line.trim() }));
90
+ }
91
+ // ---- stylesheet parsing (from src/theme/tokens.test.ts, translated TS→JS) ----
92
+ /** Whitespace is presentation; a value that wraps is the same value — used internally by `rawDeclarations`. */
93
+ function normalize(value) {
94
+ return value.replace(/\s+/g, " ").trim();
95
+ }
96
+ /**
97
+ * The `{ … }` body belonging to the block whose header (e.g. `:root {` or
98
+ * `.dark {`) appears first in the file, matched by brace depth rather than by
99
+ * the next `\n}` so a nested `calc(...)` or `color-mix(...)` cannot fool it.
100
+ */
101
+ export function blockBody(css, header) {
102
+ const headerAt = css.indexOf(header);
103
+ if (headerAt === -1)
104
+ throw new Error(`stylesheet has no ${JSON.stringify(header)} block`);
105
+ const braceAt = css.indexOf("{", headerAt);
106
+ let depth = 1;
107
+ let i = braceAt + 1;
108
+ while (depth > 0 && i < css.length) {
109
+ if (css[i] === "{")
110
+ depth++;
111
+ else if (css[i] === "}")
112
+ depth--;
113
+ i++;
114
+ }
115
+ return css.slice(braceAt + 1, i - 1);
116
+ }
117
+ /** Every `--name: value;` declaration in a block, keyed WITH the leading `--`. */
118
+ export function rawDeclarations(block) {
119
+ const map = new Map();
120
+ for (const match of block.matchAll(/(--[\w-]+):\s*([^;]+);/g)) {
121
+ map.set(match[1], normalize(match[2]));
122
+ }
123
+ return map;
124
+ }
125
+ /** The same, keyed WITHOUT the leading `--` — the shape `LIGHT_THEME` uses. */
126
+ export function declarations(block) {
127
+ const map = new Map();
128
+ for (const [name, value] of rawDeclarations(block)) {
129
+ map.set(name.slice(2), value);
130
+ }
131
+ return map;
132
+ }
133
+ // ---- foundation facts ----
134
+ /**
135
+ * The foundation token names, read off the shipped stylesheet's `:root` so
136
+ * they can never drift from what actually runs. `radius` is a geometry
137
+ * setting, not a colour token, and is excluded — it matches LIGHT_THEME's keys.
138
+ */
139
+ export function foundationTokenNames() {
140
+ const css = readFileSync(path.join(PKG, "src/theme/styles.css"), "utf8");
141
+ return [...declarations(blockBody(css, ":root {")).keys()].filter((n) => n !== "radius");
142
+ }