orbital-ds 0.1.0 → 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 (101) hide show
  1. package/contract.json +38 -0
  2. package/dist/styles.css +98 -18
  3. package/dist/themes/qratia.css +13 -13
  4. package/dist/themes/zyro.css +189 -0
  5. package/dist/ui/accordion.d.ts +9 -0
  6. package/dist/ui/accordion.js +79 -0
  7. package/dist/ui/alert-dialog.d.ts +23 -0
  8. package/dist/ui/alert-dialog.js +173 -0
  9. package/dist/ui/alert.d.ts +13 -0
  10. package/dist/ui/alert.js +77 -0
  11. package/dist/ui/aspect-ratio.d.ts +7 -0
  12. package/dist/ui/aspect-ratio.js +22 -0
  13. package/dist/ui/attachment.d.ts +26 -0
  14. package/dist/ui/attachment.js +197 -0
  15. package/dist/ui/avatar.js +1 -1
  16. package/dist/ui/badge.d.ts +1 -1
  17. package/dist/ui/badge.js +1 -1
  18. package/dist/ui/bubble.d.ts +19 -0
  19. package/dist/ui/bubble.js +115 -0
  20. package/dist/ui/button-group.d.ts +15 -0
  21. package/dist/ui/button-group.js +82 -0
  22. package/dist/ui/button.d.ts +5 -3
  23. package/dist/ui/button.js +9 -6
  24. package/dist/ui/calendar.d.ts +15 -0
  25. package/dist/ui/calendar.js +190 -0
  26. package/dist/ui/card.js +4 -4
  27. package/dist/ui/carousel.d.ts +33 -0
  28. package/dist/ui/carousel.js +199 -0
  29. package/dist/ui/checkbox.js +1 -1
  30. package/dist/ui/combobox.d.ts +26 -0
  31. package/dist/ui/combobox.js +281 -0
  32. package/dist/ui/command.d.ts +22 -0
  33. package/dist/ui/command.js +183 -0
  34. package/dist/ui/context-menu.d.ts +31 -0
  35. package/dist/ui/context-menu.js +242 -0
  36. package/dist/ui/dialog.js +2 -2
  37. package/dist/ui/direction.d.ts +1 -0
  38. package/dist/ui/direction.js +8 -0
  39. package/dist/ui/drawer.d.ts +18 -0
  40. package/dist/ui/drawer.js +208 -0
  41. package/dist/ui/dropdown-menu.d.ts +1 -1
  42. package/dist/ui/dropdown-menu.js +13 -9
  43. package/dist/ui/empty.d.ts +15 -0
  44. package/dist/ui/empty.js +102 -0
  45. package/dist/ui/field.d.ts +28 -0
  46. package/dist/ui/field.js +217 -0
  47. package/dist/ui/hover-card.d.ts +8 -0
  48. package/dist/ui/hover-card.js +45 -0
  49. package/dist/ui/input-group.d.ts +22 -0
  50. package/dist/ui/input-group.js +145 -0
  51. package/dist/ui/input-otp.d.ts +13 -0
  52. package/dist/ui/input-otp.js +82 -0
  53. package/dist/ui/input.js +1 -1
  54. package/dist/ui/item.d.ts +26 -0
  55. package/dist/ui/item.js +195 -0
  56. package/dist/ui/kbd.d.ts +6 -0
  57. package/dist/ui/kbd.js +29 -0
  58. package/dist/ui/marker.d.ts +13 -0
  59. package/dist/ui/marker.js +71 -0
  60. package/dist/ui/menubar.d.ts +31 -0
  61. package/dist/ui/menubar.js +259 -0
  62. package/dist/ui/message-scroller.d.ts +16 -0
  63. package/dist/ui/message-scroller.js +120 -0
  64. package/dist/ui/message.d.ts +12 -0
  65. package/dist/ui/message.js +90 -0
  66. package/dist/ui/native-select.d.ts +10 -0
  67. package/dist/ui/native-select.js +63 -0
  68. package/dist/ui/navigation-menu.d.ts +15 -0
  69. package/dist/ui/navigation-menu.js +162 -0
  70. package/dist/ui/pagination.d.ts +22 -0
  71. package/dist/ui/pagination.js +128 -0
  72. package/dist/ui/popover.js +2 -2
  73. package/dist/ui/questionnaire.d.ts +24 -0
  74. package/dist/ui/questionnaire.js +331 -0
  75. package/dist/ui/radio-group.d.ts +8 -0
  76. package/dist/ui/radio-group.js +39 -0
  77. package/dist/ui/resizable.d.ts +10 -0
  78. package/dist/ui/resizable.js +46 -0
  79. package/dist/ui/scroll-area.js +1 -1
  80. package/dist/ui/select.js +5 -5
  81. package/dist/ui/sheet.js +4 -4
  82. package/dist/ui/sidebar.js +4 -4
  83. package/dist/ui/slider.d.ts +6 -0
  84. package/dist/ui/slider.js +53 -0
  85. package/dist/ui/spinner.d.ts +5 -0
  86. package/dist/ui/spinner.js +9 -0
  87. package/dist/ui/switch.d.ts +8 -0
  88. package/dist/ui/switch.js +32 -0
  89. package/dist/ui/table.js +2 -2
  90. package/dist/ui/tabs.js +1 -1
  91. package/dist/ui/textarea.js +1 -1
  92. package/dist/ui/toast.d.ts +19 -0
  93. package/dist/ui/toast.js +193 -0
  94. package/dist/ui/toggle-group.d.ts +14 -0
  95. package/dist/ui/toggle-group.js +77 -0
  96. package/dist/ui/toggle.d.ts +12 -0
  97. package/dist/ui/toggle.js +43 -0
  98. package/dist/ui/tooltip.js +3 -3
  99. package/docs/CONTRACT.md +113 -0
  100. package/package.json +50 -12
  101. package/scripts/doctor.mjs +142 -0
@@ -0,0 +1,77 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
4
+ import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group";
5
+ import { cn } from "../lib/utils";
6
+ import { toggleVariants } from "./toggle";
7
+ const ToggleGroupContext = React.createContext({
8
+ size: "default",
9
+ variant: "default",
10
+ spacing: 2,
11
+ orientation: "horizontal"
12
+ });
13
+ function ToggleGroup({
14
+ className,
15
+ variant,
16
+ size,
17
+ spacing = 2,
18
+ orientation = "horizontal",
19
+ children,
20
+ ...props
21
+ }) {
22
+ return /* @__PURE__ */ jsx(
23
+ ToggleGroupPrimitive,
24
+ {
25
+ "data-slot": "toggle-group",
26
+ "data-variant": variant,
27
+ "data-size": size,
28
+ "data-spacing": spacing,
29
+ "data-orientation": orientation,
30
+ style: { "--gap": spacing },
31
+ className: cn(
32
+ "group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-control data-vertical:flex-col data-vertical:items-stretch",
33
+ className
34
+ ),
35
+ ...props,
36
+ children: /* @__PURE__ */ jsx(
37
+ ToggleGroupContext.Provider,
38
+ {
39
+ value: { variant, size, spacing, orientation },
40
+ children
41
+ }
42
+ )
43
+ }
44
+ );
45
+ }
46
+ function ToggleGroupItem({
47
+ className,
48
+ children,
49
+ variant = "default",
50
+ size = "default",
51
+ ...props
52
+ }) {
53
+ const context = React.useContext(ToggleGroupContext);
54
+ return /* @__PURE__ */ jsx(
55
+ TogglePrimitive,
56
+ {
57
+ "data-slot": "toggle-group-item",
58
+ "data-variant": context.variant || variant,
59
+ "data-size": context.size || size,
60
+ "data-spacing": context.spacing,
61
+ className: cn(
62
+ "shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-3 group-data-[spacing=0]/toggle-group:shadow-none focus:z-(--ds-z-raised) focus-visible:z-(--ds-z-raised) group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-2.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-2.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-3xl group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-3xl group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-3xl group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-3xl data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t",
63
+ toggleVariants({
64
+ variant: context.variant || variant,
65
+ size: context.size || size
66
+ }),
67
+ className
68
+ ),
69
+ ...props,
70
+ children
71
+ }
72
+ );
73
+ }
74
+ export {
75
+ ToggleGroup,
76
+ ToggleGroupItem
77
+ };
@@ -0,0 +1,12 @@
1
+ import * as React from 'react';
2
+ import * as class_variance_authority_types from 'class-variance-authority/types';
3
+ import { Toggle as Toggle$1 } from '@base-ui/react/toggle';
4
+ import { VariantProps } from 'class-variance-authority';
5
+
6
+ declare const toggleVariants: (props?: ({
7
+ variant?: "default" | "outline" | null | undefined;
8
+ size?: "default" | "sm" | "lg" | null | undefined;
9
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
10
+ declare function Toggle({ className, variant, size, ...props }: Toggle$1.Props & VariantProps<typeof toggleVariants>): React.JSX.Element;
11
+
12
+ export { Toggle, toggleVariants };
@@ -0,0 +1,43 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
3
+ import { cva } from "class-variance-authority";
4
+ import { cn } from "../lib/utils";
5
+ const toggleVariants = cva(
6
+ "group/toggle inline-flex items-center justify-center gap-1 rounded-control text-sm font-medium whitespace-nowrap transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-(length:--ds-ring-width) focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-transparent",
11
+ outline: "border border-input bg-transparent hover:bg-muted"
12
+ },
13
+ size: {
14
+ default: "h-9 min-w-9 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5",
15
+ sm: "h-8 min-w-8 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
16
+ lg: "h-10 min-w-10 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3"
17
+ }
18
+ },
19
+ defaultVariants: {
20
+ variant: "default",
21
+ size: "default"
22
+ }
23
+ }
24
+ );
25
+ function Toggle({
26
+ className,
27
+ variant = "default",
28
+ size = "default",
29
+ ...props
30
+ }) {
31
+ return /* @__PURE__ */ jsx(
32
+ TogglePrimitive,
33
+ {
34
+ "data-slot": "toggle",
35
+ className: cn(toggleVariants({ variant, size, className })),
36
+ ...props
37
+ }
38
+ );
39
+ }
40
+ export {
41
+ Toggle,
42
+ toggleVariants
43
+ };
@@ -36,19 +36,19 @@ function TooltipContent({
36
36
  alignOffset,
37
37
  side,
38
38
  sideOffset,
39
- className: "isolate z-(--z-overlay)",
39
+ className: "isolate z-(--ds-z-overlay)",
40
40
  children: /* @__PURE__ */ jsxs(
41
41
  TooltipPrimitive.Popup,
42
42
  {
43
43
  "data-slot": "tooltip-content",
44
44
  className: cn(
45
- "z-(--z-overlay) inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-2xl bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 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-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-(--z-overlay) **:data-[slot=kbd]:rounded-control data-starting-style:animate-in data-starting-style:fade-in-0 data-starting-style:zoom-in-95 data-ending-style:animate-out data-ending-style:fade-out-0 data-ending-style:zoom-out-95",
45
+ "z-(--ds-z-overlay) inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-2xl bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 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-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-(--ds-z-overlay) **:data-[slot=kbd]:rounded-control data-starting-style:animate-in data-starting-style:fade-in-0 data-starting-style:zoom-in-95 data-ending-style:animate-out data-ending-style:fade-out-0 data-ending-style:zoom-out-95",
46
46
  className
47
47
  ),
48
48
  ...props,
49
49
  children: [
50
50
  children,
51
- /* @__PURE__ */ jsx(TooltipPrimitive.Arrow, { className: "z-(--z-overlay) size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=left]:translate-x-[-1.5px] data-[side=right]:translate-x-[1.5px]" })
51
+ /* @__PURE__ */ jsx(TooltipPrimitive.Arrow, { className: "z-(--ds-z-overlay) size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=left]:translate-x-[-1.5px] data-[side=right]:translate-x-[1.5px]" })
52
52
  ]
53
53
  }
54
54
  )
@@ -0,0 +1,113 @@
1
+ # Contrato de tema de orbital-ds
2
+
3
+ Un **tema** es un fichero CSS (`themes/<nombre>.css`) que da valores a un conjunto
4
+ cerrado de variables. Los componentes **solo** leen esas variables: ninguno lleva
5
+ colores, medidas, sombras, capas ni duraciones escritos a mano. Por eso cambiar de
6
+ tema cambia todo el aspecto sin tocar un componente.
7
+
8
+ El contrato vive en [`contract.json`](../contract.json) y lo comprueba
9
+ `pnpm doctor` (que forma parte de `pnpm verify`): si a un tema le falta un token
10
+ obligatorio, el build falla.
11
+
12
+ ## Cómo se usa
13
+
14
+ ```css
15
+ @import 'tailwindcss';
16
+ @import 'tw-animate-css';
17
+
18
+ @import 'orbital-ds/styles.css'; /* 1. la ESTRUCTURA común */
19
+ @import 'orbital-ds/themes/qratia.css'; /* 2. un TEMA (después, para poder sobrescribir) */
20
+
21
+ @source '../node_modules/orbital-ds/dist'; /* que Tailwind vea las clases del paquete */
22
+ ```
23
+
24
+ El tema se importa **después** de la estructura: así puede sobrescribir también la
25
+ escala de radios y las sombras, que viven en `@theme`.
26
+
27
+ Para ajustar un valor sin crear un tema, se sobrescribe la variable al final:
28
+
29
+ ```css
30
+ :root { --primary: oklch(0.55 0.2 260); }
31
+ ```
32
+
33
+ ## Tokens obligatorios (todo tema, en `:root`)
34
+
35
+ | Grupo | Tokens |
36
+ |---|---|
37
+ | Forma | `--radius` |
38
+ | Superficies y texto | `--background`, `--foreground`, `--card`, `--card-foreground`, `--popover`, `--popover-foreground` |
39
+ | Acción | `--primary`, `--primary-foreground`, `--secondary`, `--secondary-foreground`, `--accent`, `--accent-foreground` |
40
+ | Atenuado | `--muted`, `--muted-foreground` |
41
+ | Destructivo | `--destructive`, `--destructive-foreground` |
42
+ | Líneas y foco | `--border`, `--input`, `--ring` |
43
+ | Gráficos | `--chart-1` … `--chart-5` |
44
+ | Sidebar | `--sidebar`, `--sidebar-foreground`, `--sidebar-primary`, `--sidebar-primary-foreground`, `--sidebar-accent`, `--sidebar-accent-foreground`, `--sidebar-border`, `--sidebar-ring` |
45
+ | Estados | `--danger`, `--warning`, `--success`, `--info`, `--neutral` y cada uno con su `-soft` |
46
+ | Texto acentuado | `--link`, `--link-hover` |
47
+ | Superficie hundida | `--inset`, `--inset-foreground` |
48
+ | Marca | `--brand`, `--brand-bright` |
49
+ | Tipografía | `--font-sans` (en `:root` o en `@theme`) |
50
+
51
+ En `.dark` se redefine lo que cambie; `doctor` avisa (no falla) de los tokens
52
+ principales que se hereden de la piel clara.
53
+
54
+ `--destructive` es el rojo de **acciones** (botón de borrar); `--danger` es el rojo
55
+ de **estado** (etiqueta de nivel crítico). Pueden coincidir, pero son huecos distintos.
56
+
57
+ ## Tokens con valor por defecto (el tema los puede cambiar)
58
+
59
+ Los define la estructura con un valor neutro; un tema los sobrescribe si quiere.
60
+
61
+ | Hueco | Variables |
62
+ |---|---|
63
+ | Radios semánticos | `--radius-control`, `--radius-overlay` (y la rampa `--radius-sm … 2xl`, derivada de `--radius`) |
64
+ | Sombras | `--shadow-card`, `--shadow-card-hover`, `--shadow-control`, `--shadow-raised`, `--shadow-overlay`, `--shadow-popover` |
65
+ | Altura de controles | `--ds-control-h-sm`, `-md`, `-lg` |
66
+ | Espaciado interno | `--ds-card-spacing`, `--ds-card-spacing-sm`, `--ds-field-px`, `--ds-cell-p`, `--ds-overlay-p`, `--ds-overlay-gap`, `--ds-popover-p`, `--ds-menu-p`, `--ds-menu-item-px`, `--ds-menu-item-py`, `--ds-button-px-sm/md/lg`, `--ds-badge-px` |
67
+ | Capas | `--ds-z-raised`, `--ds-z-sticky`, `--ds-z-overlay` |
68
+ | Movimiento | `--ds-motion-fast`, `--ds-motion-base`, `--ds-motion-slow`, `--ds-motion-blink` |
69
+ | Foco | `--ds-ring-width` |
70
+ | Velo | `--color-scrim` |
71
+ | **Materiales** | `--ds-card-backdrop`, `--ds-overlay-backdrop`, `--ds-field-backdrop`, `--ds-sidebar-backdrop` (un `backdrop-filter` completo; por defecto `none`) y `--ds-page-backdrop` (un `background-image` para el fondo de la página; por defecto `none`) |
72
+
73
+ Los **materiales** son lo que hace posible un tema de cristal como `zyro`: con valores
74
+ neutros no hay efecto ni coste; con `blur(28px) saturate(180%)` las superficies se
75
+ vuelven translúcidas.
76
+
77
+ ## Reglas de nombres
78
+
79
+ - Todo en **inglés**: tokens, props y valores de variantes.
80
+ - Los nombres que Tailwind convierte en utilidades (`--color-*`, `--radius-*`,
81
+ `--shadow-*`) siguen su convención y **no llevan prefijo**. El resto de variables
82
+ propias llevan el prefijo **`--ds-`** para no chocar con las de una app.
83
+ - Un componente que necesite un hueco nuevo **añade el token** con un valor que deje
84
+ el aspecto como estaba; nunca escribe el valor en el componente.
85
+
86
+ ## Qué comprueba `pnpm doctor`
87
+
88
+ 1. **Contrato**: cada tema define todos los tokens obligatorios.
89
+ 2. **Repetidos**: ninguna variable se define dos veces en el mismo bloque.
90
+ 3. **Valores fijos** en `src/ui/*.tsx`: colores literales (hex, `rgb()`, `oklch()`
91
+ salvo derivados de un token), `z-<n>`, `duration-<n>`, `bg-black`/`bg-white`,
92
+ sombras de la escala de Tailwind, `text-[<n>px]`, radios fuera de la rampa y
93
+ colores de la paleta de Tailwind.
94
+
95
+ También se puede pasar el CSS de una app: `orbital-ds-doctor --css src/styles.css`
96
+ (detecta variables repetidas).
97
+
98
+ ## Crear un tema nuevo
99
+
100
+ 1. Copia `themes/qratia.css` (o `zyro.css`) a `themes/<nombre>.css`.
101
+ 2. Cambia los valores; no quites ninguno.
102
+ 3. `pnpm doctor`: te dice qué falta.
103
+ 4. Opcional: usa los materiales para dar cristal, relieve o un fondo propio.
104
+
105
+ Se puede partir de un tema de shadcn (por ejemplo exportado desde tweakcn): usa los
106
+ mismos nombres estándar (`--background`, `--primary`…). Habrá que añadir los tokens
107
+ propios (`danger`, `warning`, `success`, `info`, `neutral`, `link`, `inset`, `brand`).
108
+
109
+ ## Cosas que un tema NO puede cambiar
110
+
111
+ La **anatomía** de un componente (otro diseño de sidebar o de tarjeta) no es un valor,
112
+ es estructura: se resuelve con variantes del componente. Un fondo animado (como la
113
+ malla WebGL de zyro) es código, no una variable: se ofrece como componente opcional.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "orbital-ds",
3
- "version": "0.1.0",
4
- "description": "Sistema de diseño genérico: estructura común (tokens semánticos, capa base) + temas intercambiables + primitivos sobre Base UI y Tailwind v4.",
3
+ "version": "0.2.0",
4
+ "description": "Sistema de diseño genérico: estructura común + temas intercambiables (qratia, zyro) + ~60 componentes sobre Base UI y Tailwind v4, con contrato de tema verificable.",
5
5
  "keywords": [
6
6
  "design-system",
7
7
  "design-tokens",
@@ -18,6 +18,9 @@
18
18
  ],
19
19
  "files": [
20
20
  "dist",
21
+ "scripts/doctor.mjs",
22
+ "contract.json",
23
+ "docs",
21
24
  "README.md",
22
25
  "LICENSE"
23
26
  ],
@@ -36,16 +39,18 @@
36
39
  "types": "./dist/hooks/*.d.ts",
37
40
  "import": "./dist/hooks/*.js"
38
41
  },
39
- "./package.json": "./package.json"
42
+ "./package.json": "./package.json",
43
+ "./contract.json": "./contract.json"
40
44
  },
41
45
  "scripts": {
42
46
  "build": "tsup && node scripts/copy-css.mjs",
43
47
  "dev": "tsup --watch --onSuccess \"node scripts/copy-css.mjs\"",
44
48
  "typecheck": "tsc --noEmit",
45
- "verify": "pnpm typecheck && pnpm build"
49
+ "verify": "pnpm doctor && pnpm typecheck && pnpm build",
50
+ "doctor": "node scripts/doctor.mjs"
46
51
  },
47
52
  "peerDependencies": {
48
- "@base-ui/react": ">=1.7",
53
+ "@base-ui/react": ">=1.9",
49
54
  "class-variance-authority": ">=0.7",
50
55
  "clsx": ">=2",
51
56
  "lucide-react": ">=0.400",
@@ -56,7 +61,14 @@
56
61
  "tw-animate-css": ">=1.3",
57
62
  "next-themes": ">=0.4",
58
63
  "recharts": ">=3",
59
- "sonner": ">=2"
64
+ "sonner": ">=2",
65
+ "@shadcn/react": ">=0.3",
66
+ "cmdk": ">=1.1",
67
+ "date-fns": ">=4",
68
+ "embla-carousel-react": ">=8.6",
69
+ "input-otp": ">=1.5",
70
+ "react-day-picker": ">=10",
71
+ "react-resizable-panels": ">=4"
60
72
  },
61
73
  "peerDependenciesMeta": {
62
74
  "next-themes": {
@@ -67,10 +79,31 @@
67
79
  },
68
80
  "sonner": {
69
81
  "optional": true
82
+ },
83
+ "@shadcn/react": {
84
+ "optional": true
85
+ },
86
+ "cmdk": {
87
+ "optional": true
88
+ },
89
+ "date-fns": {
90
+ "optional": true
91
+ },
92
+ "embla-carousel-react": {
93
+ "optional": true
94
+ },
95
+ "input-otp": {
96
+ "optional": true
97
+ },
98
+ "react-day-picker": {
99
+ "optional": true
100
+ },
101
+ "react-resizable-panels": {
102
+ "optional": true
70
103
  }
71
104
  },
72
105
  "devDependencies": {
73
- "@base-ui/react": "^1.7.0",
106
+ "@base-ui/react": "^1.9.0",
74
107
  "@types/react": "^19.2.0",
75
108
  "@types/react-dom": "^19.2.0",
76
109
  "class-variance-authority": "^0.7.1",
@@ -85,11 +118,16 @@
85
118
  "tailwindcss": "^4.1.18",
86
119
  "tsup": "^8.5.0",
87
120
  "tw-animate-css": "^1.3.6",
88
- "typescript": "^5.9.0"
121
+ "typescript": "^5.9.0",
122
+ "@shadcn/react": "^0.3.1",
123
+ "cmdk": "^1.1.1",
124
+ "date-fns": "^4.4.0",
125
+ "embla-carousel-react": "^8.6.0",
126
+ "input-otp": "^1.5.0",
127
+ "react-day-picker": "^10.0.2",
128
+ "react-resizable-panels": "^4.14.4"
89
129
  },
90
- "pnpm": {
91
- "onlyBuiltDependencies": [
92
- "esbuild"
93
- ]
130
+ "bin": {
131
+ "orbital-ds-doctor": "./scripts/doctor.mjs"
94
132
  }
95
133
  }
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * orbital-ds doctor — comprueba que el sistema se cumple.
4
+ *
5
+ * 1. CONTRATO: cada tema (themes/*.css) define en :root todos los tokens de
6
+ * contract.json (y la fuente `--font-sans`). En .dark solo avisa.
7
+ * 2. REPETIDOS: ninguna variable CSS se define dos veces en el mismo bloque.
8
+ * 3. VALORES FIJOS: ningún componente (ui/*.tsx) escribe colores, z-index,
9
+ * duraciones ni sombras a mano; todo sale de tokens.
10
+ *
11
+ * Uso:
12
+ * node scripts/doctor.mjs # temas y componentes del paquete
13
+ * node scripts/doctor.mjs --css app.css # además, un CSS de una app (contrato + repetidos)
14
+ *
15
+ * Sale con código 1 si hay errores (los avisos no fallan).
16
+ */
17
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
18
+ import { join, dirname, resolve } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
22
+ const contract = JSON.parse(readFileSync(join(ROOT, 'contract.json'), 'utf8'));
23
+
24
+ const errors = [];
25
+ const warnings = [];
26
+ const read = (p) => readFileSync(p, 'utf8');
27
+ const stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
28
+
29
+ /** Devuelve el cuerpo de cada bloque `selector { ... }` (respeta llaves anidadas). */
30
+ function blocks(css, selectorRe) {
31
+ const out = [];
32
+ const re = new RegExp(selectorRe.source + '\\s*\\{', 'g');
33
+ let m;
34
+ while ((m = re.exec(css))) {
35
+ let depth = 1;
36
+ let i = re.lastIndex;
37
+ const start = i;
38
+ while (i < css.length && depth > 0) {
39
+ if (css[i] === '{') depth++;
40
+ else if (css[i] === '}') depth--;
41
+ i++;
42
+ }
43
+ out.push(css.slice(start, i - 1));
44
+ re.lastIndex = i;
45
+ }
46
+ return out;
47
+ }
48
+
49
+ const definedVars = (body) => [...body.matchAll(/(^|[;{\s])(--[a-z0-9-]+)\s*:/gi)].map((m) => m[2]);
50
+
51
+ function checkTheme(file, label) {
52
+ const css = stripComments(read(file));
53
+ const roots = blocks(css, /(?:^|[\s}])\:root/);
54
+ const darks = blocks(css, /(?:^|[\s}])\.dark/);
55
+ const themeBlocks = blocks(css, /@theme\s+inline/);
56
+
57
+ const rootVars = new Set(roots.flatMap(definedVars));
58
+ const themeVars = new Set(themeBlocks.flatMap(definedVars));
59
+ const darkVars = new Set(darks.flatMap(definedVars));
60
+
61
+ for (const t of contract.root) {
62
+ if (!rootVars.has(`--${t}`)) errors.push(`${label}: falta el token obligatorio --${t} en :root`);
63
+ }
64
+ for (const f of contract.fonts) {
65
+ if (!rootVars.has(`--${f}`) && !themeVars.has(`--${f}`)) {
66
+ errors.push(`${label}: falta la fuente --${f} (en :root o @theme)`);
67
+ }
68
+ }
69
+ if (darks.length) {
70
+ for (const t of contract.dark.avisar_si_falta) {
71
+ if (!darkVars.has(`--${t}`)) warnings.push(`${label}: .dark no redefine --${t} (hereda el valor claro)`);
72
+ }
73
+ } else {
74
+ warnings.push(`${label}: no define piel .dark`);
75
+ }
76
+
77
+ // Repetidos dentro de un mismo bloque
78
+ for (const [name, list] of [['@theme', themeBlocks], [':root', roots], ['.dark', darks]]) {
79
+ list.forEach((body, idx) => {
80
+ const seen = new Map();
81
+ for (const v of definedVars(body)) seen.set(v, (seen.get(v) ?? 0) + 1);
82
+ for (const [v, n] of seen) {
83
+ if (n > 1) errors.push(`${label}: ${v} definido ${n} veces en el mismo bloque ${name} (#${idx + 1})`);
84
+ }
85
+ });
86
+ }
87
+ }
88
+
89
+ // 1+2) temas del paquete
90
+ const themesDir = join(ROOT, existsSync(join(ROOT, 'src/themes')) ? 'src/themes' : 'dist/themes');
91
+ for (const f of readdirSync(themesDir).filter((n) => n.endsWith('.css'))) {
92
+ checkTheme(join(themesDir, f), `tema ${f}`);
93
+ }
94
+
95
+ // CSS de una app (opcional)
96
+ const cssArgIdx = process.argv.indexOf('--css');
97
+ if (cssArgIdx > -1 && process.argv[cssArgIdx + 1]) {
98
+ const f = resolve(process.argv[cssArgIdx + 1]);
99
+ const css = stripComments(read(f));
100
+ const roots = blocks(css, /(?:^|[\s}])\:root/);
101
+ roots.forEach((body, idx) => {
102
+ const seen = new Map();
103
+ for (const v of definedVars(body)) seen.set(v, (seen.get(v) ?? 0) + 1);
104
+ for (const [v, n] of seen) if (n > 1) errors.push(`app css: ${v} definido ${n} veces en :root (#${idx + 1})`);
105
+ });
106
+ }
107
+
108
+ // 3) valores fijos en componentes
109
+ const NO_HARDCODE = [
110
+ { re: /#[0-9a-fA-F]{3,8}\b/g, why: 'color hexadecimal', skip: ['chart.tsx'] },
111
+ { re: /\b(?:rgb|rgba|hsl|hsla|oklch)\((?!from_)/g, why: 'color literal' },
112
+ { re: /(?<![\w-])z-(?!0(?![\w-]))\d+(?![\w-])/g, why: 'z-index numérico (usa --ds-z-*)' },
113
+ { re: /(?<![\w-])duration-(?!0(?![\w-]))\d+(?![\w-])/g, why: 'duración fija (usa --ds-motion-*)' },
114
+ { re: /(?<![\w-])(?:bg|text|border)-(?:black|white)(?:\/\d+)?(?![\w-])/g, why: 'color fijo (usa un token de color)' },
115
+ { re: /(?<![\w-])shadow-(?:xs|sm|md|lg|xl|2xl)(?![\w-])/g, why: 'sombra de la escala Tailwind (usa shadow-card/control/raised/overlay/popover)' },
116
+ { re: /(?<![\w-])text-\[\d+px\]/g, why: 'tamaño de texto arbitrario (usa la escala estándar)' },
117
+ { re: /(?<![\w-])rounded-(?:3xl|4xl)(?![\w-])/g, why: 'radio fuera de la rampa (usa rounded-control/overlay o la rampa sm…2xl)' },
118
+ { re: /(?<![\w-])(?:bg|text|border|ring|fill|stroke)-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone)-\d{2,3}(?![\w-])/g, why: 'color de la paleta Tailwind (usa tokens del tema)' },
119
+ ];
120
+
121
+ const uiDir = join(ROOT, 'src/ui');
122
+ if (existsSync(uiDir)) {
123
+ for (const f of readdirSync(uiDir).filter((n) => n.endsWith('.tsx'))) {
124
+ const lines = read(join(uiDir, f)).split(/\r?\n/);
125
+ lines.forEach((line, i) => {
126
+ const code = line.replace(/\/\/.*$/, '');
127
+ for (const rule of NO_HARDCODE) {
128
+ if (rule.skip?.includes(f)) continue;
129
+ const hits = code.match(rule.re);
130
+ if (hits) errors.push(`ui/${f}:${i + 1}: ${rule.why} → ${[...new Set(hits)].join(', ')}`);
131
+ }
132
+ });
133
+ }
134
+ }
135
+
136
+ for (const w of warnings) console.log(`AVISO ${w}`);
137
+ for (const e of errors) console.log(`ERROR ${e}`);
138
+ if (errors.length) {
139
+ console.log(`\ndoctor: ${errors.length} error(es), ${warnings.length} aviso(s)`);
140
+ process.exit(1);
141
+ }
142
+ console.log(`doctor: contrato, repetidos y valores fijos correctos (${warnings.length} aviso(s)).`);