@softize/opus 23.0.0 → 24.0.1

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 (97) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/bin/cli.mjs +2 -0
  3. package/bin/lib/check.mjs +241 -42
  4. package/bin/lib/copy.mjs +16 -34
  5. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
  6. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
  7. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
  8. package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
  9. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
  10. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
  11. package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
  12. package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
  13. package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
  14. package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
  15. package/docs/adr/0049-inline-navigation-groups-with-the-heading.md +95 -0
  16. package/package.json +5 -1
  17. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  18. package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
  19. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
  20. package/registry/templates/app/src/App.tsx +24 -12
  21. package/src/ui/appearance.ts +149 -0
  22. package/src/ui/components/patterns/action-form-card.tsx +46 -33
  23. package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
  24. package/src/ui/components/patterns/confirm.tsx +3 -1
  25. package/src/ui/components/patterns/fill-region.ts +8 -7
  26. package/src/ui/components/patterns/form-dialog.tsx +1 -3
  27. package/src/ui/components/patterns/form.tsx +8 -7
  28. package/src/ui/components/patterns/list.tsx +15 -19
  29. package/src/ui/components/patterns/page.tsx +15 -11
  30. package/src/ui/components/patterns/pane.tsx +9 -240
  31. package/src/ui/components/patterns/presentation.tsx +14 -14
  32. package/src/ui/components/patterns/sidebar.tsx +7 -17
  33. package/src/ui/components/patterns/split.tsx +111 -59
  34. package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +90 -27
  35. package/src/ui/components/patterns/structural-media.tsx +26 -0
  36. package/src/ui/components/patterns/surface-anatomy.ts +71 -61
  37. package/src/ui/components/patterns/surface-assistant.tsx +2 -4
  38. package/src/ui/components/patterns/surface.tsx +620 -0
  39. package/src/ui/components/patterns/trigger.tsx +11 -9
  40. package/src/ui/components/primitives/ask.tsx +155 -83
  41. package/src/ui/components/primitives/button.tsx +11 -9
  42. package/src/ui/components/primitives/checkbox.tsx +8 -8
  43. package/src/ui/components/primitives/command.tsx +1 -1
  44. package/src/ui/components/primitives/composer.tsx +47 -45
  45. package/src/ui/components/primitives/detail.tsx +9 -2
  46. package/src/ui/components/primitives/dialog.tsx +60 -26
  47. package/src/ui/components/primitives/drawer.tsx +58 -12
  48. package/src/ui/components/primitives/icon-picker.tsx +90 -73
  49. package/src/ui/components/primitives/input-group.tsx +35 -28
  50. package/src/ui/components/primitives/input-otp.tsx +16 -16
  51. package/src/ui/components/primitives/input.tsx +20 -10
  52. package/src/ui/components/primitives/item.tsx +9 -12
  53. package/src/ui/components/primitives/menu.tsx +44 -65
  54. package/src/ui/components/primitives/metric-card.tsx +75 -59
  55. package/src/ui/components/primitives/radio-group.tsx +9 -9
  56. package/src/ui/components/primitives/select.tsx +344 -209
  57. package/src/ui/components/primitives/switch.tsx +10 -10
  58. package/src/ui/components/primitives/table.tsx +29 -24
  59. package/src/ui/components/primitives/tabs.tsx +4 -2
  60. package/src/ui/components/primitives/textarea.tsx +6 -6
  61. package/src/ui/components/primitives/toggle-group-context.ts +24 -0
  62. package/src/ui/components/primitives/toggle-group.tsx +116 -74
  63. package/src/ui/components/primitives/toggle.tsx +82 -16
  64. package/src/ui/docs/DocBrowser.tsx +77 -69
  65. package/src/ui/docs/content/action-form-card.md +9 -9
  66. package/src/ui/docs/content/action-list.md +76 -70
  67. package/src/ui/docs/content/action-view.md +33 -18
  68. package/src/ui/docs/content/button.md +1 -1
  69. package/src/ui/docs/content/communication.md +1 -1
  70. package/src/ui/docs/content/compositions.md +479 -0
  71. package/src/ui/docs/content/customization.md +64 -11
  72. package/src/ui/docs/content/detail.md +8 -6
  73. package/src/ui/docs/content/dialog.md +105 -60
  74. package/src/ui/docs/content/drawer.md +68 -22
  75. package/src/ui/docs/content/field.md +14 -15
  76. package/src/ui/docs/content/icon-tile.md +14 -13
  77. package/src/ui/docs/content/page.md +44 -34
  78. package/src/ui/docs/content/pane.md +42 -290
  79. package/src/ui/docs/content/presentation.md +3 -3
  80. package/src/ui/docs/content/separator.md +6 -6
  81. package/src/ui/docs/content/sidebar.md +68 -266
  82. package/src/ui/docs/content/skeleton.md +20 -18
  83. package/src/ui/docs/content/split.md +14 -14
  84. package/src/ui/docs/content/surface.md +513 -0
  85. package/src/ui/docs/content/tabs.md +20 -20
  86. package/src/ui/docs/content/toggle.md +117 -65
  87. package/src/ui/docs/content/tokens.md +124 -40
  88. package/src/ui/docs/registry.tsx +10 -6
  89. package/src/ui/index.ts +30 -27
  90. package/src/ui/meta.ts +15 -27
  91. package/src/ui/react.tsx +40 -43
  92. package/src/ui/theme.css +311 -64
  93. package/src/ui/components/patterns/content-header.tsx +0 -528
  94. package/src/ui/components/patterns/surface-media.tsx +0 -69
  95. package/src/ui/components/primitives/card.tsx +0 -191
  96. package/src/ui/docs/content/card.md +0 -292
  97. package/src/ui/docs/content/content.md +0 -275
@@ -13,11 +13,11 @@ import {
13
13
  } from "../patterns/surface-anatomy.ts";
14
14
  import { FillBoundary } from "../patterns/fill-region.ts";
15
15
  import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
16
- import { SurfaceHeader } from "../patterns/surface-header.tsx";
16
+ import { StructuralHeader } from "../patterns/structural-header.tsx";
17
17
  import {
18
- SurfaceMedia,
19
- type SurfaceMediaProps,
20
- } from "../patterns/surface-media.tsx";
18
+ StructuralMedia,
19
+ type StructuralMediaProps,
20
+ } from "../patterns/structural-media.tsx";
21
21
  import { CloseButton, type CloseButtonSize } from "./close-button.tsx";
22
22
 
23
23
  export type DialogMode = "default" | "alert";
@@ -43,6 +43,8 @@ const DialogContentContext = React.createContext<DialogContentContextValue>({
43
43
  closeSize: "sm",
44
44
  });
45
45
 
46
+ const DialogIntroContext = React.createContext(false);
47
+
46
48
  /**
47
49
  * Compõe o handler de dispensa do consumidor com o bloqueio de `closeDisabled`: o handler dele
48
50
  * roda primeiro e, com a superfície bloqueada, o evento é cancelado para que Esc, clique fora ou
@@ -316,7 +318,7 @@ export function DialogHeader({
316
318
  }
317
319
 
318
320
  return (
319
- <SurfaceHeader
321
+ <StructuralHeader
320
322
  name="DialogHeader"
321
323
  slot="dialog"
322
324
  slots={{
@@ -341,7 +343,7 @@ export function DialogHeader({
341
343
  {...props}
342
344
  >
343
345
  {children}
344
- </SurfaceHeader>
346
+ </StructuralHeader>
345
347
  );
346
348
  }
347
349
 
@@ -373,31 +375,55 @@ export function DialogActions({
373
375
  );
374
376
  }
375
377
 
376
- export function DialogMedia({ className, ...props }: SurfaceMediaProps) {
377
- const { mode } = useDialogContext();
378
- // No alerta a mídia é o elemento de reconhecimento da resposta e ocupa a moldura
379
- // maior — exceção declarada da ADR 0023, coberta por teste. Fora dele, a mesma
380
- // moldura das demais famílias.
381
- return mode === "alert" ? (
382
- <SurfaceMedia
383
- slot="dialog-media"
384
- size="lg"
385
- className={cn(
386
- 'mb-2 size-16 *:[svg:not([class*="size-"])]:size-8',
387
- className,
388
- )}
378
+ /** Título e ações da composição principal, como primeiro bloco de DialogBody. */
379
+ export function DialogIntro({
380
+ className,
381
+ children,
382
+ ...props
383
+ }: React.ComponentProps<"div">) {
384
+ return (
385
+ <DialogIntroContext.Provider value>
386
+ <StructuralHeader
387
+ name="DialogIntro"
388
+ slot="dialog-intro"
389
+ slots={{ title: DialogTitle, actions: DialogIntroActions }}
390
+ leadingPlacement="inline"
391
+ titleRequired
392
+ className={cn("text-left", className)}
393
+ {...props}
394
+ >
395
+ {children}
396
+ </StructuralHeader>
397
+ </DialogIntroContext.Provider>
398
+ );
399
+ }
400
+
401
+ /** Ações relacionadas ao título do conteúdo, separadas das ações de chrome do header. */
402
+ export function DialogIntroActions({
403
+ className,
404
+ ...props
405
+ }: React.ComponentProps<"div">) {
406
+ return (
407
+ <div
408
+ data-slot="dialog-intro-actions"
409
+ className={cn(surfaceActionsClasses, className)}
389
410
  {...props}
390
411
  />
391
- ) : (
392
- <SurfaceMedia slot="dialog-media" {...props} className={className} />
412
+ );
413
+ }
414
+
415
+ export function DialogMedia({ className, ...props }: StructuralMediaProps) {
416
+ return (
417
+ <StructuralMedia slot="dialog-media" {...props} className={className} />
393
418
  );
394
419
  }
395
420
 
396
421
  export function DialogBody({
422
+ flush = false,
397
423
  className,
398
424
  style,
399
425
  ...props
400
- }: React.ComponentProps<"div">) {
426
+ }: React.ComponentProps<"div"> & { flush?: boolean }) {
401
427
  const { mode } = useDialogContext();
402
428
  const layout = useSurfaceLayout("dialog");
403
429
  return (
@@ -405,14 +431,15 @@ export function DialogBody({
405
431
  data-slot="dialog-body"
406
432
  className={cn(
407
433
  mode === "alert"
408
- ? "min-w-0"
434
+ ? "flex min-w-0 flex-col gap-6"
409
435
  : cn(
410
436
  "min-h-0 flex-1 overflow-y-auto",
411
- surfaceRegionClasses("dialog", layout, "body"),
437
+ surfaceRegionClasses("dialog", layout, "body", { flush }),
412
438
  ),
413
439
  className,
414
440
  )}
415
441
  style={style}
442
+ data-flush={flush ? "true" : undefined}
416
443
  {...props}
417
444
  />
418
445
  );
@@ -451,12 +478,19 @@ export function DialogTitle({
451
478
  level?: HeadingLevel;
452
479
  }) {
453
480
  const { mode } = useDialogContext();
481
+ const intro = React.useContext(DialogIntroContext);
454
482
  const layout = useSurfaceLayout("dialog");
455
483
  const Title =
456
484
  mode === "alert" ? AlertDialogPrimitive.Title : DialogPrimitive.Title;
457
485
  const classes = cn(
458
- mode === "alert" ? undefined : "min-w-0 truncate",
459
- surfaceTitleClasses("dialog", layout),
486
+ mode === "alert"
487
+ ? undefined
488
+ : intro
489
+ ? "min-w-0 whitespace-normal"
490
+ : "min-w-0 truncate",
491
+ intro
492
+ ? "text-xl font-semibold leading-tight tracking-tight"
493
+ : surfaceTitleClasses("dialog", layout),
460
494
  className,
461
495
  );
462
496
  // O nível troca a TAG do mesmo nó, via `asChild` — nunca aninha um heading dentro
@@ -12,11 +12,11 @@ import {
12
12
  type SurfaceLayout,
13
13
  } from "../patterns/surface-anatomy.ts";
14
14
  import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
15
- import { SurfaceHeader } from "../patterns/surface-header.tsx";
15
+ import { StructuralHeader } from "../patterns/structural-header.tsx";
16
16
  import {
17
- SurfaceMedia,
18
- type SurfaceMediaProps,
19
- } from "../patterns/surface-media.tsx";
17
+ StructuralMedia,
18
+ type StructuralMediaProps,
19
+ } from "../patterns/structural-media.tsx";
20
20
  import { CloseButton, type CloseButtonSize } from "./close-button.tsx";
21
21
  import { overlayClasses, preventDismissWhen } from "./dialog.tsx";
22
22
 
@@ -32,6 +32,8 @@ const DrawerContentContext = React.createContext<DrawerContentContextValue>({
32
32
  closeSize: "sm",
33
33
  });
34
34
 
35
+ const DrawerIntroContext = React.createContext(false);
36
+
35
37
  function Drawer({
36
38
  ...props
37
39
  }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
@@ -151,7 +153,7 @@ function DrawerHeader({
151
153
  const { showCloseButton, closeDisabled, closeSize } =
152
154
  React.useContext(DrawerContentContext);
153
155
  return (
154
- <SurfaceHeader
156
+ <StructuralHeader
155
157
  name="DrawerHeader"
156
158
  slot="drawer"
157
159
  slots={{
@@ -176,13 +178,13 @@ function DrawerHeader({
176
178
  {...props}
177
179
  >
178
180
  {children}
179
- </SurfaceHeader>
181
+ </StructuralHeader>
180
182
  );
181
183
  }
182
184
 
183
185
  /** Moldura de ícone ou imagem que abre a linha do header. */
184
- function DrawerMedia(props: SurfaceMediaProps) {
185
- return <SurfaceMedia slot="drawer-media" {...props} />;
186
+ function DrawerMedia(props: StructuralMediaProps) {
187
+ return <StructuralMedia slot="drawer-media" {...props} />;
186
188
  }
187
189
 
188
190
  /** Região introdutória do header — o voltar de um fluxo modal encadeado. */
@@ -210,21 +212,60 @@ function DrawerActions({ className, ...props }: React.ComponentProps<"div">) {
210
212
  );
211
213
  }
212
214
 
215
+ /** Título e ações da composição principal, como primeiro bloco de DrawerBody. */
216
+ function DrawerIntro({
217
+ className,
218
+ children,
219
+ ...props
220
+ }: React.ComponentProps<"div">) {
221
+ return (
222
+ <DrawerIntroContext.Provider value>
223
+ <StructuralHeader
224
+ name="DrawerIntro"
225
+ slot="drawer-intro"
226
+ slots={{ title: DrawerTitle, actions: DrawerIntroActions }}
227
+ leadingPlacement="inline"
228
+ titleRequired
229
+ className={cn("text-left", className)}
230
+ {...props}
231
+ >
232
+ {children}
233
+ </StructuralHeader>
234
+ </DrawerIntroContext.Provider>
235
+ );
236
+ }
237
+
238
+ /** Ações relacionadas ao título do conteúdo, separadas das ações de chrome do header. */
239
+ function DrawerIntroActions({
240
+ className,
241
+ ...props
242
+ }: React.ComponentProps<"div">) {
243
+ return (
244
+ <div
245
+ data-slot="drawer-intro-actions"
246
+ className={cn(surfaceActionsClasses, className)}
247
+ {...props}
248
+ />
249
+ );
250
+ }
251
+
213
252
  function DrawerBody({
253
+ flush = false,
214
254
  className,
215
255
  style,
216
256
  ...props
217
- }: React.ComponentProps<"div">) {
257
+ }: React.ComponentProps<"div"> & { flush?: boolean }) {
218
258
  const layout = useSurfaceLayout("drawer");
219
259
  return (
220
260
  <div
221
261
  data-slot="drawer-body"
222
262
  className={cn(
223
263
  "min-h-0 flex-1 overflow-y-auto",
224
- surfaceRegionClasses("drawer", layout, "body"),
264
+ surfaceRegionClasses("drawer", layout, "body", { flush }),
225
265
  className,
226
266
  )}
227
267
  style={style}
268
+ data-flush={flush ? "true" : undefined}
228
269
  {...props}
229
270
  />
230
271
  );
@@ -257,10 +298,13 @@ function DrawerTitle({
257
298
  /** Hierarquia do documento. Sem ele vale o `h2` que a primitiva emite. */
258
299
  level?: HeadingLevel;
259
300
  }) {
301
+ const intro = React.useContext(DrawerIntroContext);
260
302
  const layout = useSurfaceLayout("drawer");
261
303
  const classes = cn(
262
- "min-w-0 truncate",
263
- surfaceTitleClasses("drawer", layout),
304
+ intro ? "min-w-0 whitespace-normal" : "min-w-0 truncate",
305
+ intro
306
+ ? "text-xl font-semibold leading-tight tracking-tight"
307
+ : surfaceTitleClasses("drawer", layout),
264
308
  className,
265
309
  );
266
310
  // O nível troca a TAG do mesmo nó, via `asChild` — a escala continua vindo do layout.
@@ -287,6 +331,8 @@ function DrawerTitle({
287
331
 
288
332
  export {
289
333
  DrawerActions,
334
+ DrawerIntro,
335
+ DrawerIntroActions,
290
336
  DrawerMedia,
291
337
  DrawerNavigation,
292
338
  Drawer,
@@ -1,4 +1,4 @@
1
- import * as React from 'react'
1
+ import * as React from "react";
2
2
  import {
3
3
  Activity,
4
4
  Banknote,
@@ -47,74 +47,81 @@ import {
47
47
  Wrench,
48
48
  Zap,
49
49
  type LucideIcon,
50
- } from 'lucide-react'
51
- import { cn } from '../../lib/cn.ts'
52
- import { controlHeight, focusRing } from './control.ts'
53
- import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './command.tsx'
54
- import { Popover, PopoverContent, PopoverTrigger } from './popover.tsx'
50
+ } from "lucide-react";
51
+ import { cn } from "../../lib/cn.ts";
52
+ import { controlHeight, focusRing } from "./control.ts";
53
+ import {
54
+ Command,
55
+ CommandEmpty,
56
+ CommandGroup,
57
+ CommandInput,
58
+ CommandItem,
59
+ CommandList,
60
+ } from "./command.tsx";
61
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover.tsx";
55
62
 
56
63
  /** Paleta default (curada): nomes kebab-case estáveis → componente lucide. Curta de
57
64
  * propósito — cada ícone importado entra no bundle; pra outro vocabulário, passe
58
65
  * `icons` (o value salvo continua sendo só o nome). */
59
66
  export const iconPickerIcons: Record<string, LucideIcon> = {
60
- 'activity': Activity,
61
- 'banknote': Banknote,
62
- 'bell': Bell,
63
- 'box': Box,
64
- 'briefcase': Briefcase,
65
- 'building': Building2,
66
- 'calendar': Calendar,
67
- 'chart-bar': ChartBar,
68
- 'chart-line': ChartLine,
69
- 'chart-pie': ChartPie,
70
- 'clipboard-list': ClipboardList,
71
- 'clock': Clock,
72
- 'credit-card': CreditCard,
73
- 'database': Database,
74
- 'file-text': FileText,
75
- 'flag': Flag,
76
- 'folder': Folder,
77
- 'gauge': Gauge,
78
- 'globe': Globe,
79
- 'heart': Heart,
80
- 'layers': Layers,
81
- 'layout-grid': LayoutGrid,
82
- 'lightbulb': Lightbulb,
83
- 'mail': Mail,
84
- 'map-pin': MapPin,
85
- 'message-circle': MessageCircle,
86
- 'message-square': MessageSquare,
87
- 'package': Package,
88
- 'phone': Phone,
89
- 'rocket': Rocket,
90
- 'settings': Settings,
91
- 'shopping-cart': ShoppingCart,
92
- 'star': Star,
93
- 'store': Store,
94
- 'table': Table,
95
- 'target': Target,
96
- 'trending-down': TrendingDown,
97
- 'trending-up': TrendingUp,
98
- 'truck': Truck,
99
- 'user': UserRound,
100
- 'users': UsersRound,
101
- 'wallet': Wallet,
102
- 'wrench': Wrench,
103
- 'zap': Zap,
104
- }
67
+ activity: Activity,
68
+ banknote: Banknote,
69
+ bell: Bell,
70
+ box: Box,
71
+ briefcase: Briefcase,
72
+ building: Building2,
73
+ calendar: Calendar,
74
+ "chart-bar": ChartBar,
75
+ "chart-line": ChartLine,
76
+ "chart-pie": ChartPie,
77
+ "clipboard-list": ClipboardList,
78
+ clock: Clock,
79
+ "credit-card": CreditCard,
80
+ database: Database,
81
+ "file-text": FileText,
82
+ flag: Flag,
83
+ folder: Folder,
84
+ gauge: Gauge,
85
+ globe: Globe,
86
+ heart: Heart,
87
+ layers: Layers,
88
+ "layout-grid": LayoutGrid,
89
+ lightbulb: Lightbulb,
90
+ mail: Mail,
91
+ "map-pin": MapPin,
92
+ "message-circle": MessageCircle,
93
+ "message-square": MessageSquare,
94
+ package: Package,
95
+ phone: Phone,
96
+ rocket: Rocket,
97
+ settings: Settings,
98
+ "shopping-cart": ShoppingCart,
99
+ star: Star,
100
+ store: Store,
101
+ table: Table,
102
+ target: Target,
103
+ "trending-down": TrendingDown,
104
+ "trending-up": TrendingUp,
105
+ truck: Truck,
106
+ user: UserRound,
107
+ users: UsersRound,
108
+ wallet: Wallet,
109
+ wrench: Wrench,
110
+ zap: Zap,
111
+ };
105
112
 
106
113
  export interface IconPickerProps {
107
114
  /** Nome do ícone selecionado (chave da paleta) — '' = nenhum. */
108
- value: string
109
- onChange: (name: string) => void
115
+ value: string;
116
+ onChange: (name: string) => void;
110
117
  /** Paleta nome → componente. Default: `iconPickerIcons` (curada). */
111
- icons?: Record<string, LucideIcon>
112
- placeholder?: string
113
- disabled?: boolean
114
- className?: string
115
- id?: string
116
- 'aria-invalid'?: boolean
117
- 'aria-describedby'?: string
118
+ icons?: Record<string, LucideIcon>;
119
+ placeholder?: string;
120
+ disabled?: boolean;
121
+ className?: string;
122
+ id?: string;
123
+ "aria-invalid"?: boolean;
124
+ "aria-describedby"?: string;
118
125
  }
119
126
 
120
127
  /**
@@ -127,15 +134,15 @@ export function IconPicker({
127
134
  value,
128
135
  onChange,
129
136
  icons = iconPickerIcons,
130
- placeholder = 'Selecione um ícone…',
137
+ placeholder = "Selecione um ícone…",
131
138
  disabled,
132
139
  className,
133
140
  id,
134
- 'aria-invalid': ariaInvalid,
135
- 'aria-describedby': ariaDescribedBy,
141
+ "aria-invalid": ariaInvalid,
142
+ "aria-describedby": ariaDescribedBy,
136
143
  }: IconPickerProps): React.ReactElement {
137
- const [open, setOpen] = React.useState(false)
138
- const Current = value !== '' ? icons[value] : undefined
144
+ const [open, setOpen] = React.useState(false);
145
+ const Current = value !== "" ? icons[value] : undefined;
139
146
 
140
147
  return (
141
148
  <Popover open={open} onOpenChange={setOpen}>
@@ -149,18 +156,20 @@ export function IconPicker({
149
156
  data-slot="icon-picker"
150
157
  className={cn(
151
158
  // Mesma altura do Input e do Select no tamanho default: lado a lado num form, alinham.
152
- 'flex w-full items-center gap-2 rounded-md border border-input bg-muted/30 px-3 text-sm outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30',
159
+ "flex w-full items-center gap-2 rounded-md border border-input bg-control px-3 text-sm outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50",
153
160
  controlHeight.default,
154
161
  focusRing,
155
162
  // As mesmas classes de inválido do Input e do Select: o erro aparece na moldura.
156
- 'aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:aria-invalid:ring-context-danger/40',
163
+ "aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:aria-invalid:ring-context-danger/40",
157
164
  className,
158
165
  )}
159
166
  >
160
167
  {Current !== undefined ? (
161
168
  <>
162
169
  <Current className="size-4 shrink-0" />
163
- <span data-slot="icon-picker-value" className="truncate">{value}</span>
170
+ <span data-slot="icon-picker-value" className="truncate">
171
+ {value}
172
+ </span>
164
173
  </>
165
174
  ) : (
166
175
  <span className="text-muted-foreground">{placeholder}</span>
@@ -168,7 +177,10 @@ export function IconPicker({
168
177
  <ChevronsUpDown className="ml-auto size-4 shrink-0 opacity-50" />
169
178
  </button>
170
179
  </PopoverTrigger>
171
- <PopoverContent align="start" className="pointer-events-auto w-(--radix-popover-trigger-width) p-0">
180
+ <PopoverContent
181
+ align="start"
182
+ className="pointer-events-auto w-(--radix-popover-trigger-width) p-0"
183
+ >
172
184
  <Command>
173
185
  <CommandInput placeholder="Buscar ícone…" />
174
186
  <CommandList data-slot="icon-picker-list" className="max-h-60">
@@ -180,13 +192,18 @@ export function IconPicker({
180
192
  data-slot="icon-picker-item"
181
193
  value={name}
182
194
  onSelect={() => {
183
- onChange(name === value ? '' : name)
184
- setOpen(false)
195
+ onChange(name === value ? "" : name);
196
+ setOpen(false);
185
197
  }}
186
198
  >
187
199
  <Icon className="size-4" />
188
200
  <span className="truncate">{name}</span>
189
- <Check className={cn('ml-auto size-4 shrink-0', value === name ? 'opacity-100' : 'opacity-0')} />
201
+ <Check
202
+ className={cn(
203
+ "ml-auto size-4 shrink-0",
204
+ value === name ? "opacity-100" : "opacity-0",
205
+ )}
206
+ />
190
207
  </CommandItem>
191
208
  ))}
192
209
  </CommandGroup>
@@ -194,5 +211,5 @@ export function IconPicker({
194
211
  </Command>
195
212
  </PopoverContent>
196
213
  </Popover>
197
- )
214
+ );
198
215
  }
@@ -1,20 +1,24 @@
1
- import * as React from "react"
2
- import { cva, type VariantProps } from "class-variance-authority"
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
3
 
4
- import { cn } from '../../lib/cn.ts'
5
- import type { ControlShape } from './control.ts'
6
- import { Button, type ButtonProps } from './button.tsx'
7
- import { Input } from './input.tsx'
8
- import { Textarea } from './textarea.tsx'
4
+ import { cn } from "../../lib/cn.ts";
5
+ import type { ControlShape } from "./control.ts";
6
+ import { Button, type ButtonProps } from "./button.tsx";
7
+ import { Input } from "./input.tsx";
8
+ import { Textarea } from "./textarea.tsx";
9
9
 
10
- function InputGroup({ className, shape = 'default', ...props }: React.ComponentProps<"div"> & { shape?: ControlShape }) {
10
+ function InputGroup({
11
+ className,
12
+ shape = "default",
13
+ ...props
14
+ }: React.ComponentProps<"div"> & { shape?: ControlShape }) {
11
15
  return (
12
16
  <div
13
17
  data-slot="input-group"
14
18
  data-shape={shape}
15
19
  role="group"
16
20
  className={cn(
17
- "group/input-group relative flex w-full items-center rounded-md border border-input bg-muted/30 transition-[color,box-shadow] outline-none data-[shape=pill]:rounded-full dark:bg-input/30",
21
+ "group/input-group relative flex w-full items-center rounded-md border border-input bg-control transition-[color,box-shadow] outline-none data-[shape=pill]:rounded-full",
18
22
  "h-9 min-w-0 has-[>textarea]:h-auto",
19
23
 
20
24
  // Variants based on alignment.
@@ -29,11 +33,11 @@ function InputGroup({ className, shape = 'default', ...props }: React.ComponentP
29
33
  // Error state.
30
34
  "has-[[data-slot][aria-invalid=true]]:border-context-danger has-[[data-slot][aria-invalid=true]]:ring-context-danger/20 dark:has-[[data-slot][aria-invalid=true]]:ring-context-danger/40",
31
35
 
32
- className
36
+ className,
33
37
  )}
34
38
  {...props}
35
39
  />
36
- )
40
+ );
37
41
  }
38
42
 
39
43
  const inputGroupAddonVariants = cva(
@@ -59,8 +63,8 @@ const inputGroupAddonVariants = cva(
59
63
  defaultVariants: {
60
64
  align: "inline-start",
61
65
  },
62
- }
63
- )
66
+ },
67
+ );
64
68
 
65
69
  function InputGroupAddon({
66
70
  className,
@@ -75,15 +79,15 @@ function InputGroupAddon({
75
79
  className={cn(inputGroupAddonVariants({ align }), className)}
76
80
  onClick={(e) => {
77
81
  if ((e.target as HTMLElement).closest("button")) {
78
- return
82
+ return;
79
83
  }
80
84
  e.currentTarget.parentElement
81
85
  ?.querySelector<HTMLElement>("input, textarea")
82
- ?.focus()
86
+ ?.focus();
83
87
  }}
84
88
  {...props}
85
89
  />
86
- )
90
+ );
87
91
  }
88
92
 
89
93
  /** Só o acabamento por tamanho: altura, quadrado e glifo vêm da escala de control.ts via Button. */
@@ -101,11 +105,14 @@ const inputGroupButtonVariants = cva(
101
105
  defaultVariants: {
102
106
  size: "xs",
103
107
  },
104
- }
105
- )
108
+ },
109
+ );
106
110
 
107
111
  /** Subconjunto da escala que cabe dentro de um campo: `xs`, `sm`, `icon-xs` e `icon-sm`. */
108
- type InputGroupButtonSize = Extract<NonNullable<ButtonProps['size']>, 'xs' | 'sm' | 'icon-xs' | 'icon-sm'>
112
+ type InputGroupButtonSize = Extract<
113
+ NonNullable<ButtonProps["size"]>,
114
+ "xs" | "sm" | "icon-xs" | "icon-sm"
115
+ >;
109
116
 
110
117
  function InputGroupButton({
111
118
  className,
@@ -113,7 +120,7 @@ function InputGroupButton({
113
120
  variant = "ghost",
114
121
  size = "xs",
115
122
  ...props
116
- }: Omit<ButtonProps, 'size'> & { size?: InputGroupButtonSize }) {
123
+ }: Omit<ButtonProps, "size"> & { size?: InputGroupButtonSize }) {
117
124
  return (
118
125
  <Button
119
126
  type={type}
@@ -123,7 +130,7 @@ function InputGroupButton({
123
130
  className={cn(inputGroupButtonVariants({ size }), className)}
124
131
  {...props}
125
132
  />
126
- )
133
+ );
127
134
  }
128
135
 
129
136
  function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
@@ -131,11 +138,11 @@ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
131
138
  <span
132
139
  className={cn(
133
140
  "flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
134
- className
141
+ className,
135
142
  )}
136
143
  {...props}
137
144
  />
138
- )
145
+ );
139
146
  }
140
147
 
141
148
  function InputGroupInput({
@@ -147,11 +154,11 @@ function InputGroupInput({
147
154
  data-slot="input-group-control"
148
155
  className={cn(
149
156
  "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
150
- className
157
+ className,
151
158
  )}
152
159
  {...props}
153
160
  />
154
- )
161
+ );
155
162
  }
156
163
 
157
164
  function InputGroupTextarea({
@@ -163,11 +170,11 @@ function InputGroupTextarea({
163
170
  data-slot="input-group-control"
164
171
  className={cn(
165
172
  "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
166
- className
173
+ className,
167
174
  )}
168
175
  {...props}
169
176
  />
170
- )
177
+ );
171
178
  }
172
179
 
173
180
  export {
@@ -177,4 +184,4 @@ export {
177
184
  InputGroupText,
178
185
  InputGroupInput,
179
186
  InputGroupTextarea,
180
- }
187
+ };