@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
@@ -1,7 +1,14 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
  import { describe, expect, it } from "vitest";
4
- import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from "./combobox";
4
+ import {
5
+ Combobox,
6
+ ComboboxContent,
7
+ ComboboxEmpty,
8
+ ComboboxInput,
9
+ ComboboxItem,
10
+ ComboboxList,
11
+ } from "./combobox";
5
12
 
6
13
  const FRUIT = ["apple", "banana", "cherry"];
7
14
 
@@ -11,7 +18,13 @@ function Demo() {
11
18
  <ComboboxInput placeholder="Pick" aria-label="Fruit" />
12
19
  <ComboboxContent>
13
20
  <ComboboxEmpty>No matches</ComboboxEmpty>
14
- <ComboboxList>{(item: string) => <ComboboxItem key={item} value={item}>{item}</ComboboxItem>}</ComboboxList>
21
+ <ComboboxList>
22
+ {(item: string) => (
23
+ <ComboboxItem key={item} value={item}>
24
+ {item}
25
+ </ComboboxItem>
26
+ )}
27
+ </ComboboxList>
15
28
  </ComboboxContent>
16
29
  </Combobox>
17
30
  );
@@ -1,29 +1,20 @@
1
- "use client"
1
+ "use client";
2
2
 
3
- import * as React from "react"
4
- import { Combobox as ComboboxPrimitive } from "@base-ui/react"
3
+ import * as React from "react";
4
+ import { Combobox as ComboboxPrimitive } from "@base-ui/react";
5
5
 
6
- import { cn } from "../lib/cn.js"
7
- import { Button } from "./button.js"
8
- import {
9
- InputGroup,
10
- InputGroupAddon,
11
- InputGroupButton,
12
- InputGroupInput,
13
- } from "./input-group.js"
14
- import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"
6
+ import { cn } from "cn";
7
+ import { Button } from "./button.js";
8
+ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "./input-group.js";
9
+ import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react";
15
10
 
16
- const Combobox = ComboboxPrimitive.Root
11
+ const Combobox = ComboboxPrimitive.Root;
17
12
 
18
13
  function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
19
- return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
14
+ return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
20
15
  }
21
16
 
22
- function ComboboxTrigger({
23
- className,
24
- children,
25
- ...props
26
- }: ComboboxPrimitive.Trigger.Props) {
17
+ function ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Trigger.Props) {
27
18
  return (
28
19
  <ComboboxPrimitive.Trigger
29
20
  data-slot="combobox-trigger"
@@ -33,7 +24,7 @@ function ComboboxTrigger({
33
24
  {children}
34
25
  <ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
35
26
  </ComboboxPrimitive.Trigger>
36
- )
27
+ );
37
28
  }
38
29
 
39
30
  function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
@@ -46,7 +37,7 @@ function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
46
37
  >
47
38
  <XIcon className="pointer-events-none" />
48
39
  </ComboboxPrimitive.Clear>
49
- )
40
+ );
50
41
  }
51
42
 
52
43
  function ComboboxInput({
@@ -57,15 +48,12 @@ function ComboboxInput({
57
48
  showClear = false,
58
49
  ...props
59
50
  }: ComboboxPrimitive.Input.Props & {
60
- showTrigger?: boolean
61
- showClear?: boolean
51
+ showTrigger?: boolean;
52
+ showClear?: boolean;
62
53
  }) {
63
54
  return (
64
55
  <InputGroup className={cn("w-auto", className)}>
65
- <ComboboxPrimitive.Input
66
- render={<InputGroupInput disabled={disabled} />}
67
- {...props}
68
- />
56
+ <ComboboxPrimitive.Input render={<InputGroupInput disabled={disabled} />} {...props} />
69
57
  <InputGroupAddon align="inline-end">
70
58
  {showTrigger && (
71
59
  <InputGroupButton
@@ -83,7 +71,7 @@ function ComboboxInput({
83
71
  </InputGroupAddon>
84
72
  {children}
85
73
  </InputGroup>
86
- )
74
+ );
87
75
  }
88
76
 
89
77
  function ComboboxContent({
@@ -112,12 +100,15 @@ function ComboboxContent({
112
100
  <ComboboxPrimitive.Popup
113
101
  data-slot="combobox-content"
114
102
  data-chips={!!anchor}
115
- className={cn("group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none 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 )}
103
+ className={cn(
104
+ "group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none 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",
105
+ className,
106
+ )}
116
107
  {...props}
117
108
  />
118
109
  </ComboboxPrimitive.Positioner>
119
110
  </ComboboxPrimitive.Portal>
120
- )
111
+ );
121
112
  }
122
113
 
123
114
  function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
@@ -126,24 +117,20 @@ function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
126
117
  data-slot="combobox-list"
127
118
  className={cn(
128
119
  "no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0",
129
- className
120
+ className,
130
121
  )}
131
122
  {...props}
132
123
  />
133
- )
124
+ );
134
125
  }
135
126
 
136
- function ComboboxItem({
137
- className,
138
- children,
139
- ...props
140
- }: ComboboxPrimitive.Item.Props) {
127
+ function ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) {
141
128
  return (
142
129
  <ComboboxPrimitive.Item
143
130
  data-slot="combobox-item"
144
131
  className={cn(
145
132
  "relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
146
- className
133
+ className,
147
134
  )}
148
135
  {...props}
149
136
  >
@@ -156,36 +143,27 @@ function ComboboxItem({
156
143
  <CheckIcon className="pointer-events-none" />
157
144
  </ComboboxPrimitive.ItemIndicator>
158
145
  </ComboboxPrimitive.Item>
159
- )
146
+ );
160
147
  }
161
148
 
162
149
  function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
163
150
  return (
164
- <ComboboxPrimitive.Group
165
- data-slot="combobox-group"
166
- className={cn(className)}
167
- {...props}
168
- />
169
- )
151
+ <ComboboxPrimitive.Group data-slot="combobox-group" className={cn(className)} {...props} />
152
+ );
170
153
  }
171
154
 
172
- function ComboboxLabel({
173
- className,
174
- ...props
175
- }: ComboboxPrimitive.GroupLabel.Props) {
155
+ function ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) {
176
156
  return (
177
157
  <ComboboxPrimitive.GroupLabel
178
158
  data-slot="combobox-label"
179
159
  className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
180
160
  {...props}
181
161
  />
182
- )
162
+ );
183
163
  }
184
164
 
185
165
  function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
186
- return (
187
- <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
188
- )
166
+ return <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />;
189
167
  }
190
168
 
191
169
  function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
@@ -194,41 +172,37 @@ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
194
172
  data-slot="combobox-empty"
195
173
  className={cn(
196
174
  "hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex",
197
- className
175
+ className,
198
176
  )}
199
177
  {...props}
200
178
  />
201
- )
179
+ );
202
180
  }
203
181
 
204
- function ComboboxSeparator({
205
- className,
206
- ...props
207
- }: ComboboxPrimitive.Separator.Props) {
182
+ function ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) {
208
183
  return (
209
184
  <ComboboxPrimitive.Separator
210
185
  data-slot="combobox-separator"
211
186
  className={cn("-mx-1 my-1 h-px bg-border", className)}
212
187
  {...props}
213
188
  />
214
- )
189
+ );
215
190
  }
216
191
 
217
192
  function ComboboxChips({
218
193
  className,
219
194
  ...props
220
- }: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
221
- ComboboxPrimitive.Chips.Props) {
195
+ }: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> & ComboboxPrimitive.Chips.Props) {
222
196
  return (
223
197
  <ComboboxPrimitive.Chips
224
198
  data-slot="combobox-chips"
225
199
  className={cn(
226
200
  "flex min-h-8 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",
227
- className
201
+ className,
228
202
  )}
229
203
  {...props}
230
204
  />
231
- )
205
+ );
232
206
  }
233
207
 
234
208
  function ComboboxChip({
@@ -237,14 +211,14 @@ function ComboboxChip({
237
211
  showRemove = true,
238
212
  ...props
239
213
  }: ComboboxPrimitive.Chip.Props & {
240
- showRemove?: boolean
214
+ showRemove?: boolean;
241
215
  }) {
242
216
  return (
243
217
  <ComboboxPrimitive.Chip
244
218
  data-slot="combobox-chip"
245
219
  className={cn(
246
220
  "flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
247
- className
221
+ className,
248
222
  )}
249
223
  {...props}
250
224
  >
@@ -259,24 +233,21 @@ function ComboboxChip({
259
233
  </ComboboxPrimitive.ChipRemove>
260
234
  )}
261
235
  </ComboboxPrimitive.Chip>
262
- )
236
+ );
263
237
  }
264
238
 
265
- function ComboboxChipsInput({
266
- className,
267
- ...props
268
- }: ComboboxPrimitive.Input.Props) {
239
+ function ComboboxChipsInput({ className, ...props }: ComboboxPrimitive.Input.Props) {
269
240
  return (
270
241
  <ComboboxPrimitive.Input
271
242
  data-slot="combobox-chip-input"
272
243
  className={cn("min-w-16 flex-1 outline-none", className)}
273
244
  {...props}
274
245
  />
275
- )
246
+ );
276
247
  }
277
248
 
278
249
  function useComboboxAnchor() {
279
- return React.useRef<HTMLDivElement | null>(null)
250
+ return React.useRef<HTMLDivElement | null>(null);
280
251
  }
281
252
 
282
253
  export {
@@ -296,4 +267,4 @@ export {
296
267
  ComboboxTrigger,
297
268
  ComboboxValue,
298
269
  useComboboxAnchor,
299
- }
270
+ };
@@ -1,20 +1,16 @@
1
1
  /**
2
- * The component harness, proved on the primitives that carry a decision.
2
+ * The component harness, proved on the components that carry a decision.
3
3
  *
4
- * Not a snapshot of every class string that would pin the design system to
5
- * whatever it happened to be on the day. What is asserted here is the handful of behaviours a screen
6
- * would silently lose: the merge that makes `className` a real override and the
7
- * two geometry overrides that ride on it, `inlineLink` and `menuSurface`, whose
8
- * whole job is to beat a canonical utility the `asChild` that keeps a link a
9
- * link, and the role an error is announced with.
4
+ * Deliberately not an echo of every class string: pinning the design system to
5
+ * whatever it looked like on the day is the mistake this repository just spent
6
+ * a restructure undoing, and a restyle is coming. What is asserted here is the
7
+ * handful of behaviours a screen would silently lose the merge that makes
8
+ * `className` a real override, the `asChild` that keeps a link a link, the role
9
+ * an error is announced with, and the value a `Progress` reports without being
10
+ * asked.
10
11
  *
11
- * The button no longer defaults `type`, so nothing here stops a "Cancel"
12
- * submitting a form; that is a call-site property now, and
13
- * `tests/scripts/form_buttons.test.mjs` is what holds it.
14
- *
15
- * This file is also the reason the jsdom harness exists at all: standing the
16
- * environment up once here is cheaper than the first screen that needs it doing so
17
- * under deadline.
12
+ * The button does not default `type`, so nothing here stops a "Cancel"
13
+ * submitting a form; that is a call-site property, gated in the consumer.
18
14
  */
19
15
 
20
16
  import { render, screen } from "@testing-library/react";
@@ -22,10 +18,6 @@ import userEvent from "@testing-library/user-event";
22
18
  import type { JSX } from "react";
23
19
  import { describe, expect, it } from "vitest";
24
20
 
25
- import { inlineLink } from "../lib/button";
26
- import { menuSurface } from "../lib/menu";
27
- import { progressAria } from "../lib/progress";
28
- import { twoLineTrigger } from "../lib/select";
29
21
  import { Alert, AlertDescription, AlertTitle } from "./alert";
30
22
  import { Badge } from "./badge";
31
23
  import { Button } from "./button";
@@ -41,13 +33,7 @@ import { Progress } from "./progress";
41
33
  import { FieldError } from "./field";
42
34
  import { Input } from "./input";
43
35
  import { Label } from "./label";
44
- import {
45
- Select,
46
- SelectContent,
47
- SelectItem,
48
- SelectTrigger,
49
- SelectValue,
50
- } from "./select";
36
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
51
37
  import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "./sheet";
52
38
  import { Table, TableBody, TableHead, TableHeader, TableRow } from "./table";
53
39
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
@@ -59,14 +45,14 @@ describe("Button", () => {
59
45
  });
60
46
 
61
47
  it("lets a caller override a conflicting utility rather than emitting both", () => {
62
- // Without `tailwind-merge` both `px-4` and `px-6` survive and which one wins is
48
+ // Without `tailwind-merge` both `px-2.5` and `px-6` survive and which one wins is
63
49
  // decided by the order Tailwind wrote them into the stylesheet — a rule nobody
64
50
  // can see from the call site. This is what makes `className` an extension
65
51
  // point rather than a suggestion.
66
52
  render(<Button className="px-6">Wide</Button>);
67
53
  const className = screen.getByRole("button", { name: "Wide" }).className;
68
54
  expect(className).toContain("px-6");
69
- expect(className).not.toContain("px-4");
55
+ expect(className).not.toContain("px-2.5");
70
56
  });
71
57
 
72
58
  it("renders the child element with asChild, so a link stays a link", () => {
@@ -95,21 +81,31 @@ describe("Button", () => {
95
81
  expect(classes).not.toMatch(/(^|\s)underline(\s|$)/);
96
82
  });
97
83
 
98
- it("hands back the height and padding a link button in prose cannot keep", () => {
84
+ it("lets a caller's className outrank the inline size", () => {
99
85
  render(
100
- <Button variant="link" className={inlineLink}>
86
+ <Button variant="link" size="inline" className="h-8">
101
87
  More
102
88
  </Button>,
103
89
  );
104
90
 
105
- // `Button` merges `className` over `buttonVariants` with tailwind-merge, so
106
- // this is the merge itself: canonical's `h-8 px-2.5` is gone from the
107
- // rendered attribute rather than merely outranked by a later rule.
91
+ // `h-auto` and `h-8` are the same utility group, so tailwind-merge has to
92
+ // drop the one the variant supplied. Without the merge both survive and
93
+ // which wins is decided by stylesheet order a rule the call site cannot see.
108
94
  const classes = screen.getByRole("button").className.split(" ");
109
- expect(classes).toContain("h-auto");
110
- expect(classes).toContain("p-0");
111
- expect(classes).not.toContain("h-8");
112
- expect(classes).not.toContain("px-2.5");
95
+ expect(classes).toContain("h-8");
96
+ expect(classes).not.toContain("h-auto");
97
+ });
98
+
99
+ it("has an inline size that keeps a link button inside a sentence", () => {
100
+ render(
101
+ <Button variant="link" size="inline">
102
+ Read the docs
103
+ </Button>,
104
+ );
105
+ const button = screen.getByRole("button", { name: "Read the docs" });
106
+ expect(button.getAttribute("data-size")).toBe("inline");
107
+ expect(button.className).toContain("h-auto");
108
+ expect(button.className).toContain("p-0");
113
109
  });
114
110
  });
115
111
 
@@ -132,13 +128,16 @@ describe("Alert and Badge", () => {
132
128
  expect(screen.getByText("done").getAttribute("data-slot")).toBe("badge");
133
129
  });
134
130
 
135
- it.each(["success", "warning", "info", "quiet"] as const)("%s is a Badge variant keyed on data", (variant) => {
136
- render(<Badge variant={variant}>x</Badge>);
137
- const el = screen.getByText("x");
138
- expect(el.getAttribute("data-variant")).toBe(variant);
139
- expect(el.className).toContain("border-transparent");
140
- expect(el.className).not.toMatch(/border-(emerald|amber|sky)/);
141
- });
131
+ it.each(["success", "warning", "info", "quiet"] as const)(
132
+ "%s is a Badge variant keyed on data",
133
+ (variant) => {
134
+ render(<Badge variant={variant}>x</Badge>);
135
+ const el = screen.getByText("x");
136
+ expect(el.getAttribute("data-variant")).toBe(variant);
137
+ expect(el.className).toContain("border-transparent");
138
+ expect(el.className).not.toMatch(/border-(emerald|amber|sky)/);
139
+ },
140
+ );
142
141
  });
143
142
 
144
143
  describe("fields", () => {
@@ -159,9 +158,10 @@ describe("fields", () => {
159
158
  });
160
159
 
161
160
  /**
162
- * The two-line option — composed at the call site rather than a primitive prop,
163
- * which is why what is asserted here is the trigger's own behaviour and not a
164
- * layout this file would otherwise have to keep in step with a call site.
161
+ * The two-line option — a `multiline` prop on the trigger itself rather than a
162
+ * class string composed at the call site, which is why what is asserted here
163
+ * is the trigger's own behaviour and not a layout this file would otherwise
164
+ * have to keep in step with a call site.
165
165
  *
166
166
  * The claim worth a test is the one that is easy to lose: the trigger shows the
167
167
  * *same* two lines the list does, because Radix renders the selected item's own
@@ -172,7 +172,7 @@ describe("Select", () => {
172
172
  function pickOne(): JSX.Element {
173
173
  return (
174
174
  <Select defaultValue="a">
175
- <SelectTrigger data-testid="model" className={twoLineTrigger}>
175
+ <SelectTrigger data-testid="model" multiline>
176
176
  <SelectValue />
177
177
  </SelectTrigger>
178
178
  <SelectContent>
@@ -202,15 +202,16 @@ describe("Select", () => {
202
202
 
203
203
  it("grows rather than clipping, and leaves a one-line option where it was", () => {
204
204
  render(pickOne());
205
- // `h-8` would fix the height and squash the second line; `min-h-8` keeps the
206
- // one-line control on Nova's contract height and lets a two-line one grow.
205
+ // `h-8` would fix the height and squash the second line; `min-h-8` keeps a
206
+ // one-line control at the trigger's usual height and lets a two-line one grow.
207
207
  const trigger = screen.getByTestId("model");
208
208
  expect(trigger.className).toContain("min-h-8");
209
209
  // Nothing truncates: half a model id is not a model id.
210
210
  expect(trigger.className).not.toContain("truncate");
211
- // `twoLineTrigger` names canonical's own modifier chains, so `cn`'s merge
212
- // replaces the fixed height and the value clamp rather than stacking beside
213
- // them a real check of the merge at render, not an echo of the constant.
211
+ // `multiline` is a prop, not a class string arriving from outside, so it
212
+ // does not need to outrank anything but `cn`'s merge still has to replace
213
+ // the fixed height and the value clamp rather than stack beside them, which
214
+ // is what this checks at render, not an echo of the prop's own classes.
214
215
  expect(trigger.className).toContain("data-[size=default]:h-auto");
215
216
  expect(trigger.className).toContain("line-clamp-none");
216
217
  expect(trigger.className).not.toContain("data-[size=default]:h-8");
@@ -226,6 +227,21 @@ describe("Select", () => {
226
227
  expect(className).toContain("w-full");
227
228
  expect(className).toContain("min-w-(--radix-select-trigger-width)");
228
229
  });
230
+
231
+ it("grows and stops clamping its value when multiline", () => {
232
+ render(
233
+ <Select>
234
+ <SelectTrigger multiline aria-label="Export target">
235
+ <SelectValue placeholder="Pick one" />
236
+ </SelectTrigger>
237
+ </Select>,
238
+ );
239
+ const trigger = screen.getByRole("combobox", { name: "Export target" });
240
+ expect(trigger.className).toContain("data-[size=default]:h-auto");
241
+ expect(trigger.className).not.toContain("data-[size=default]:h-8");
242
+ expect(trigger.className).toContain("line-clamp-none");
243
+ expect(trigger.className).not.toContain("line-clamp-1");
244
+ });
229
245
  });
230
246
 
231
247
  describe("Card and Table", () => {
@@ -323,18 +339,15 @@ describe("Tabs", () => {
323
339
  });
324
340
 
325
341
  describe("Progress", () => {
326
- it("reports its value to assistive technology, not only as a width", () => {
327
- // Canonical `Progress` reads `value` only to size the indicator and never
328
- // forwards it to Radix's `Root`, so `progressAria` says it again — every
329
- // caller in the product spreads it beside `value` for exactly this reason.
330
- render(<Progress value={42} {...progressAria(42)} aria-label="Ingest" />);
331
- expect(screen.getByRole("progressbar", { name: "Ingest" }).getAttribute("aria-valuenow")).toBe(
332
- "42",
333
- );
342
+ it("reports its value to assistive technology without help from the caller", () => {
343
+ render(<Progress value={42} aria-label="Ingest" />);
344
+ const bar = screen.getByRole("progressbar", { name: "Ingest" });
345
+ expect(bar.getAttribute("aria-valuenow")).toBe("42");
346
+ expect(bar.getAttribute("data-state")).not.toBe("indeterminate");
334
347
  });
335
348
 
336
349
  it("fills with the functional colour, and carries the data-slot a caller can restyle from", () => {
337
- // Canonical `Progress` has no `variant` prop and no notion of status — a
350
+ // `Progress` has no `variant` prop and no notion of status — a
338
351
  // batch's completion is an amount, not a polarity, so the indicator is
339
352
  // always `bg-primary`. A caller who needs to reach it targets the
340
353
  // `data-slot` it renders with, from its own `className` on `Root`.
@@ -402,7 +415,16 @@ describe("Dialog", () => {
402
415
 
403
416
  describe("sheet", () => {
404
417
  it("renders a dialog with its title and description wired by Radix", async () => {
405
- render(<Sheet open><SheetContent><SheetHeader><SheetTitle>Filters</SheetTitle><SheetDescription>Narrow the list</SheetDescription></SheetHeader></SheetContent></Sheet>);
418
+ render(
419
+ <Sheet open>
420
+ <SheetContent>
421
+ <SheetHeader>
422
+ <SheetTitle>Filters</SheetTitle>
423
+ <SheetDescription>Narrow the list</SheetDescription>
424
+ </SheetHeader>
425
+ </SheetContent>
426
+ </Sheet>,
427
+ );
406
428
  const dialog = await screen.findByRole("dialog");
407
429
  expect(dialog.getAttribute("data-slot")).toBe("sheet-content");
408
430
  expect(dialog.getAttribute("data-side")).toBe("right");
@@ -418,23 +440,38 @@ describe("DropdownMenu", () => {
418
440
  <DropdownMenuTrigger asChild>
419
441
  <Button variant="ghost" size="icon" aria-label="Actions" />
420
442
  </DropdownMenuTrigger>
421
- <DropdownMenuContent className={menuSurface}>
443
+ <DropdownMenuContent>
422
444
  <DropdownMenuItem>Check integrity of this connection</DropdownMenuItem>
423
445
  </DropdownMenuContent>
424
446
  </DropdownMenu>,
425
447
  );
426
448
  await user.click(screen.getByRole("button", { name: "Actions" }));
427
449
  const classes = (await screen.findByRole("menu")).className.split(" ");
428
- // Canonical pins the surface to the trigger with
429
- // `w-(--radix-dropdown-menu-trigger-width)`, which behind this icon-sized
430
- // button is the 128px floor — long items wrap. `menuSurface` is in the same
431
- // utility group, so tailwind-merge drops canonical's at render and the
432
- // rendered class list is the assertion: this is the merge, not a hope about
433
- // cascade order.
450
+ // The default sizes to items, not to the trigger: behind this icon-sized
451
+ // button, pinning to the trigger width would be a 128px ceiling that wraps
452
+ // every longer item. `min-w-32` is what is asserted below, surviving as
453
+ // the floor it was meant to be.
434
454
  expect(classes).toContain("w-auto");
435
455
  expect(classes).not.toContain("w-(--radix-dropdown-menu-trigger-width)");
436
456
  // The floor is a different group and survives, which is what keeps a
437
457
  // one-word menu from collapsing to its widest item.
438
458
  expect(classes).toContain("min-w-32");
439
459
  });
460
+
461
+ it("sizes to its items and leaves on the frame it is dismissed", async () => {
462
+ const user = userEvent.setup();
463
+ render(
464
+ <DropdownMenu>
465
+ <DropdownMenuTrigger>Actions</DropdownMenuTrigger>
466
+ <DropdownMenuContent>
467
+ <DropdownMenuItem>Duplicate this model</DropdownMenuItem>
468
+ </DropdownMenuContent>
469
+ </DropdownMenu>,
470
+ );
471
+ await user.click(screen.getByRole("button", { name: "Actions" }));
472
+ const menu = await screen.findByRole("menu");
473
+ expect(menu.className).toContain("w-auto");
474
+ expect(menu.className).not.toContain("--radix-dropdown-menu-trigger-width");
475
+ expect(menu.className).not.toContain("data-closed:animate-out");
476
+ });
440
477
  });