@softize/opus 22.2.0 → 24.0.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 (115) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/bin/cli.mjs +3 -9
  3. package/bin/lib/check.mjs +242 -49
  4. package/bin/lib/copy.mjs +17 -35
  5. package/bin/lib/gen-runner.mjs +2 -0
  6. package/bin/lib/gen-stubs.mjs +3 -1
  7. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
  8. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
  9. package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
  10. package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
  11. package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
  12. package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
  13. package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
  14. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
  15. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +59 -0
  16. package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
  17. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
  18. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
  19. package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
  20. package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
  21. package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
  22. package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
  23. package/docs/code-style.md +1 -1
  24. package/package.json +5 -1
  25. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  26. package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
  27. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
  28. package/registry/skills/create-opus-action/references/contract-and-binding.md +3 -3
  29. package/registry/templates/app/src/App.tsx +24 -12
  30. package/src/core/contracts.ts +1 -1
  31. package/src/core/runtime.ts +4 -37
  32. package/src/core/types.ts +5 -23
  33. package/src/design/index.ts +14 -3
  34. package/src/testing/index.ts +6 -26
  35. package/src/ui/appearance.ts +149 -0
  36. package/src/ui/components/patterns/action-form-card.tsx +46 -33
  37. package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
  38. package/src/ui/components/patterns/confirm.tsx +3 -1
  39. package/src/ui/components/patterns/fill-region.ts +8 -7
  40. package/src/ui/components/patterns/form-dialog.tsx +1 -3
  41. package/src/ui/components/patterns/form.tsx +8 -7
  42. package/src/ui/components/patterns/list.tsx +64 -14
  43. package/src/ui/components/patterns/page.tsx +15 -11
  44. package/src/ui/components/patterns/pane.tsx +9 -240
  45. package/src/ui/components/patterns/presentation.tsx +14 -14
  46. package/src/ui/components/patterns/sidebar.tsx +7 -17
  47. package/src/ui/components/patterns/split.tsx +111 -59
  48. package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
  49. package/src/ui/components/patterns/structural-media.tsx +26 -0
  50. package/src/ui/components/patterns/surface-anatomy.ts +71 -61
  51. package/src/ui/components/patterns/surface-assistant.tsx +2 -4
  52. package/src/ui/components/patterns/surface.tsx +620 -0
  53. package/src/ui/components/patterns/trigger.tsx +11 -9
  54. package/src/ui/components/primitives/ask.tsx +155 -83
  55. package/src/ui/components/primitives/button.tsx +11 -9
  56. package/src/ui/components/primitives/checkbox.tsx +8 -8
  57. package/src/ui/components/primitives/command.tsx +1 -1
  58. package/src/ui/components/primitives/composer.tsx +47 -45
  59. package/src/ui/components/primitives/detail.tsx +9 -2
  60. package/src/ui/components/primitives/dialog.tsx +60 -26
  61. package/src/ui/components/primitives/drawer.tsx +58 -12
  62. package/src/ui/components/primitives/icon-picker.tsx +93 -70
  63. package/src/ui/components/primitives/input-group.tsx +35 -28
  64. package/src/ui/components/primitives/input-otp.tsx +16 -16
  65. package/src/ui/components/primitives/input.tsx +20 -10
  66. package/src/ui/components/primitives/item.tsx +9 -12
  67. package/src/ui/components/primitives/menu.tsx +44 -65
  68. package/src/ui/components/primitives/metric-card.tsx +75 -59
  69. package/src/ui/components/primitives/radio-group.tsx +9 -9
  70. package/src/ui/components/primitives/select.tsx +344 -209
  71. package/src/ui/components/primitives/switch.tsx +10 -10
  72. package/src/ui/components/primitives/table.tsx +29 -24
  73. package/src/ui/components/primitives/tabs.tsx +58 -88
  74. package/src/ui/components/primitives/textarea.tsx +6 -6
  75. package/src/ui/components/primitives/toggle-group-context.ts +24 -0
  76. package/src/ui/components/primitives/toggle-group.tsx +116 -74
  77. package/src/ui/components/primitives/toggle.tsx +82 -16
  78. package/src/ui/docs/DocBrowser.tsx +77 -69
  79. package/src/ui/docs/content/action-form-card.md +9 -9
  80. package/src/ui/docs/content/action-list.md +85 -70
  81. package/src/ui/docs/content/action-view.md +33 -18
  82. package/src/ui/docs/content/actions.md +6 -7
  83. package/src/ui/docs/content/button.md +1 -1
  84. package/src/ui/docs/content/communication.md +1 -1
  85. package/src/ui/docs/content/customization.md +64 -11
  86. package/src/ui/docs/content/detail.md +8 -6
  87. package/src/ui/docs/content/dialog.md +100 -58
  88. package/src/ui/docs/content/drawer.md +63 -19
  89. package/src/ui/docs/content/field.md +14 -15
  90. package/src/ui/docs/content/icon-tile.md +14 -13
  91. package/src/ui/docs/content/page.md +34 -26
  92. package/src/ui/docs/content/pane.md +41 -290
  93. package/src/ui/docs/content/presentation.md +3 -3
  94. package/src/ui/docs/content/semantic-context.md +0 -1
  95. package/src/ui/docs/content/separator.md +6 -6
  96. package/src/ui/docs/content/sidebar.md +68 -266
  97. package/src/ui/docs/content/skeleton.md +20 -18
  98. package/src/ui/docs/content/split.md +14 -14
  99. package/src/ui/docs/content/surface.md +513 -0
  100. package/src/ui/docs/content/tabs.md +82 -83
  101. package/src/ui/docs/content/testing.md +2 -3
  102. package/src/ui/docs/content/toggle.md +120 -63
  103. package/src/ui/docs/content/tokens.md +124 -40
  104. package/src/ui/docs/registry.tsx +4 -6
  105. package/src/ui/drivers/react.tsx +4 -3
  106. package/src/ui/index.ts +30 -27
  107. package/src/ui/meta.ts +15 -27
  108. package/src/ui/react.tsx +42 -46
  109. package/src/ui/theme.css +311 -64
  110. package/src/vite/design.ts +23 -21
  111. package/src/ui/components/patterns/content-header.tsx +0 -532
  112. package/src/ui/components/patterns/surface-media.tsx +0 -69
  113. package/src/ui/components/primitives/card.tsx +0 -191
  114. package/src/ui/docs/content/card.md +0 -292
  115. package/src/ui/docs/content/content.md +0 -276
@@ -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,
@@ -25,9 +25,12 @@ import {
25
25
  Layers,
26
26
  LayoutGrid,
27
27
  Lightbulb,
28
+ Mail,
28
29
  MapPin,
30
+ MessageCircle,
29
31
  MessageSquare,
30
32
  Package,
33
+ Phone,
31
34
  Rocket,
32
35
  Settings,
33
36
  ShoppingCart,
@@ -44,71 +47,81 @@ import {
44
47
  Wrench,
45
48
  Zap,
46
49
  type LucideIcon,
47
- } from 'lucide-react'
48
- import { cn } from '../../lib/cn.ts'
49
- import { controlHeight, focusRing } from './control.ts'
50
- import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './command.tsx'
51
- 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";
52
62
 
53
63
  /** Paleta default (curada): nomes kebab-case estáveis → componente lucide. Curta de
54
64
  * propósito — cada ícone importado entra no bundle; pra outro vocabulário, passe
55
65
  * `icons` (o value salvo continua sendo só o nome). */
56
66
  export const iconPickerIcons: Record<string, LucideIcon> = {
57
- 'activity': Activity,
58
- 'banknote': Banknote,
59
- 'bell': Bell,
60
- 'box': Box,
61
- 'briefcase': Briefcase,
62
- 'building': Building2,
63
- 'calendar': Calendar,
64
- 'chart-bar': ChartBar,
65
- 'chart-line': ChartLine,
66
- 'chart-pie': ChartPie,
67
- 'clipboard-list': ClipboardList,
68
- 'clock': Clock,
69
- 'credit-card': CreditCard,
70
- 'database': Database,
71
- 'file-text': FileText,
72
- 'flag': Flag,
73
- 'folder': Folder,
74
- 'gauge': Gauge,
75
- 'globe': Globe,
76
- 'heart': Heart,
77
- 'layers': Layers,
78
- 'layout-grid': LayoutGrid,
79
- 'lightbulb': Lightbulb,
80
- 'map-pin': MapPin,
81
- 'message-square': MessageSquare,
82
- 'package': Package,
83
- 'rocket': Rocket,
84
- 'settings': Settings,
85
- 'shopping-cart': ShoppingCart,
86
- 'star': Star,
87
- 'store': Store,
88
- 'table': Table,
89
- 'target': Target,
90
- 'trending-down': TrendingDown,
91
- 'trending-up': TrendingUp,
92
- 'truck': Truck,
93
- 'user': UserRound,
94
- 'users': UsersRound,
95
- 'wallet': Wallet,
96
- 'wrench': Wrench,
97
- 'zap': Zap,
98
- }
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
+ };
99
112
 
100
113
  export interface IconPickerProps {
101
114
  /** Nome do ícone selecionado (chave da paleta) — '' = nenhum. */
102
- value: string
103
- onChange: (name: string) => void
115
+ value: string;
116
+ onChange: (name: string) => void;
104
117
  /** Paleta nome → componente. Default: `iconPickerIcons` (curada). */
105
- icons?: Record<string, LucideIcon>
106
- placeholder?: string
107
- disabled?: boolean
108
- className?: string
109
- id?: string
110
- 'aria-invalid'?: boolean
111
- '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;
112
125
  }
113
126
 
114
127
  /**
@@ -121,15 +134,15 @@ export function IconPicker({
121
134
  value,
122
135
  onChange,
123
136
  icons = iconPickerIcons,
124
- placeholder = 'Selecione um ícone…',
137
+ placeholder = "Selecione um ícone…",
125
138
  disabled,
126
139
  className,
127
140
  id,
128
- 'aria-invalid': ariaInvalid,
129
- 'aria-describedby': ariaDescribedBy,
141
+ "aria-invalid": ariaInvalid,
142
+ "aria-describedby": ariaDescribedBy,
130
143
  }: IconPickerProps): React.ReactElement {
131
- const [open, setOpen] = React.useState(false)
132
- const Current = value !== '' ? icons[value] : undefined
144
+ const [open, setOpen] = React.useState(false);
145
+ const Current = value !== "" ? icons[value] : undefined;
133
146
 
134
147
  return (
135
148
  <Popover open={open} onOpenChange={setOpen}>
@@ -143,18 +156,20 @@ export function IconPicker({
143
156
  data-slot="icon-picker"
144
157
  className={cn(
145
158
  // Mesma altura do Input e do Select no tamanho default: lado a lado num form, alinham.
146
- '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",
147
160
  controlHeight.default,
148
161
  focusRing,
149
162
  // As mesmas classes de inválido do Input e do Select: o erro aparece na moldura.
150
- '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",
151
164
  className,
152
165
  )}
153
166
  >
154
167
  {Current !== undefined ? (
155
168
  <>
156
169
  <Current className="size-4 shrink-0" />
157
- <span data-slot="icon-picker-value" className="truncate">{value}</span>
170
+ <span data-slot="icon-picker-value" className="truncate">
171
+ {value}
172
+ </span>
158
173
  </>
159
174
  ) : (
160
175
  <span className="text-muted-foreground">{placeholder}</span>
@@ -162,7 +177,10 @@ export function IconPicker({
162
177
  <ChevronsUpDown className="ml-auto size-4 shrink-0 opacity-50" />
163
178
  </button>
164
179
  </PopoverTrigger>
165
- <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
+ >
166
184
  <Command>
167
185
  <CommandInput placeholder="Buscar ícone…" />
168
186
  <CommandList data-slot="icon-picker-list" className="max-h-60">
@@ -174,13 +192,18 @@ export function IconPicker({
174
192
  data-slot="icon-picker-item"
175
193
  value={name}
176
194
  onSelect={() => {
177
- onChange(name === value ? '' : name)
178
- setOpen(false)
195
+ onChange(name === value ? "" : name);
196
+ setOpen(false);
179
197
  }}
180
198
  >
181
199
  <Icon className="size-4" />
182
200
  <span className="truncate">{name}</span>
183
- <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
+ />
184
207
  </CommandItem>
185
208
  ))}
186
209
  </CommandGroup>
@@ -188,5 +211,5 @@ export function IconPicker({
188
211
  </Command>
189
212
  </PopoverContent>
190
213
  </Popover>
191
- )
214
+ );
192
215
  }