@softize/opus 23.0.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 (95) hide show
  1. package/CHANGELOG.md +81 -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/package.json +5 -1
  16. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  17. package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
  18. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
  19. package/registry/templates/app/src/App.tsx +24 -12
  20. package/src/ui/appearance.ts +149 -0
  21. package/src/ui/components/patterns/action-form-card.tsx +46 -33
  22. package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
  23. package/src/ui/components/patterns/confirm.tsx +3 -1
  24. package/src/ui/components/patterns/fill-region.ts +8 -7
  25. package/src/ui/components/patterns/form-dialog.tsx +1 -3
  26. package/src/ui/components/patterns/form.tsx +8 -7
  27. package/src/ui/components/patterns/list.tsx +15 -19
  28. package/src/ui/components/patterns/page.tsx +15 -11
  29. package/src/ui/components/patterns/pane.tsx +9 -240
  30. package/src/ui/components/patterns/presentation.tsx +14 -14
  31. package/src/ui/components/patterns/sidebar.tsx +7 -17
  32. package/src/ui/components/patterns/split.tsx +111 -59
  33. package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
  34. package/src/ui/components/patterns/structural-media.tsx +26 -0
  35. package/src/ui/components/patterns/surface-anatomy.ts +71 -61
  36. package/src/ui/components/patterns/surface-assistant.tsx +2 -4
  37. package/src/ui/components/patterns/surface.tsx +620 -0
  38. package/src/ui/components/patterns/trigger.tsx +11 -9
  39. package/src/ui/components/primitives/ask.tsx +155 -83
  40. package/src/ui/components/primitives/button.tsx +11 -9
  41. package/src/ui/components/primitives/checkbox.tsx +8 -8
  42. package/src/ui/components/primitives/command.tsx +1 -1
  43. package/src/ui/components/primitives/composer.tsx +47 -45
  44. package/src/ui/components/primitives/detail.tsx +9 -2
  45. package/src/ui/components/primitives/dialog.tsx +60 -26
  46. package/src/ui/components/primitives/drawer.tsx +58 -12
  47. package/src/ui/components/primitives/icon-picker.tsx +90 -73
  48. package/src/ui/components/primitives/input-group.tsx +35 -28
  49. package/src/ui/components/primitives/input-otp.tsx +16 -16
  50. package/src/ui/components/primitives/input.tsx +20 -10
  51. package/src/ui/components/primitives/item.tsx +9 -12
  52. package/src/ui/components/primitives/menu.tsx +44 -65
  53. package/src/ui/components/primitives/metric-card.tsx +75 -59
  54. package/src/ui/components/primitives/radio-group.tsx +9 -9
  55. package/src/ui/components/primitives/select.tsx +344 -209
  56. package/src/ui/components/primitives/switch.tsx +10 -10
  57. package/src/ui/components/primitives/table.tsx +29 -24
  58. package/src/ui/components/primitives/tabs.tsx +4 -2
  59. package/src/ui/components/primitives/textarea.tsx +6 -6
  60. package/src/ui/components/primitives/toggle-group-context.ts +24 -0
  61. package/src/ui/components/primitives/toggle-group.tsx +116 -74
  62. package/src/ui/components/primitives/toggle.tsx +82 -16
  63. package/src/ui/docs/DocBrowser.tsx +77 -69
  64. package/src/ui/docs/content/action-form-card.md +9 -9
  65. package/src/ui/docs/content/action-list.md +76 -70
  66. package/src/ui/docs/content/action-view.md +33 -18
  67. package/src/ui/docs/content/button.md +1 -1
  68. package/src/ui/docs/content/communication.md +1 -1
  69. package/src/ui/docs/content/customization.md +64 -11
  70. package/src/ui/docs/content/detail.md +8 -6
  71. package/src/ui/docs/content/dialog.md +100 -58
  72. package/src/ui/docs/content/drawer.md +63 -19
  73. package/src/ui/docs/content/field.md +14 -15
  74. package/src/ui/docs/content/icon-tile.md +14 -13
  75. package/src/ui/docs/content/page.md +34 -26
  76. package/src/ui/docs/content/pane.md +41 -290
  77. package/src/ui/docs/content/presentation.md +3 -3
  78. package/src/ui/docs/content/separator.md +6 -6
  79. package/src/ui/docs/content/sidebar.md +68 -266
  80. package/src/ui/docs/content/skeleton.md +20 -18
  81. package/src/ui/docs/content/split.md +14 -14
  82. package/src/ui/docs/content/surface.md +513 -0
  83. package/src/ui/docs/content/tabs.md +20 -20
  84. package/src/ui/docs/content/toggle.md +117 -65
  85. package/src/ui/docs/content/tokens.md +124 -40
  86. package/src/ui/docs/registry.tsx +4 -6
  87. package/src/ui/index.ts +30 -27
  88. package/src/ui/meta.ts +15 -27
  89. package/src/ui/react.tsx +40 -43
  90. package/src/ui/theme.css +311 -64
  91. package/src/ui/components/patterns/content-header.tsx +0 -528
  92. package/src/ui/components/patterns/surface-media.tsx +0 -69
  93. package/src/ui/components/primitives/card.tsx +0 -191
  94. package/src/ui/docs/content/card.md +0 -292
  95. package/src/ui/docs/content/content.md +0 -275
@@ -1,528 +0,0 @@
1
- import {
2
- Children,
3
- createContext,
4
- isValidElement,
5
- useContext,
6
- useId,
7
- type ComponentProps,
8
- type HTMLAttributes,
9
- type ReactElement,
10
- type ReactNode,
11
- } from "react";
12
- import { cn } from "../../lib/cn.ts";
13
- import { IconTile, type IconTileProps } from "../primitives/icon-tile.tsx";
14
- import { fillRegionClassName, useFillRegion } from "./fill-region.ts";
15
- import { headingTags } from "./heading-level.ts";
16
- import { usePageHeadingPresence } from "./page-heading-context.tsx";
17
- import type { SurfaceLayout } from "./surface-anatomy.ts";
18
- import { SurfaceMedia, type SurfaceMediaProps } from "./surface-media.tsx";
19
- import {
20
- SurfaceHeader,
21
- surfaceHeaderClasses,
22
- } from "./surface-header.tsx";
23
-
24
- export type ContentHeaderLevel = 1 | 2 | 3 | 4 | 5 | 6;
25
- export type ContentElement = "section" | "fieldset";
26
- export type ContentFrameVariant = "default" | "legend";
27
-
28
- interface ContentContextValue {
29
- element: ContentElement;
30
- frameVariant: ContentFrameVariant;
31
- framed: boolean;
32
- layout: SurfaceLayout;
33
- level: ContentHeaderLevel;
34
- titleId: string;
35
- variant: "page" | "section";
36
- }
37
-
38
- const ContentContext = createContext<ContentContextValue | null>(null);
39
- const ContentHeaderContext = createContext<ContentContextValue | null>(null);
40
-
41
- function requiredContext<T>(
42
- value: T | null,
43
- component: string,
44
- parent: string,
45
- ): T {
46
- if (value === null)
47
- throw new Error(`${component} deve estar dentro de ${parent}.`);
48
- return value;
49
- }
50
-
51
- interface ContentSharedProps {
52
- /** Aplica a moldura estrutural da região sem transferi-la ao conteúdo interno. */
53
- framed?: boolean;
54
- /** Anatomia interna: contínua (default) ou com header separado do corpo. */
55
- layout?: SurfaceLayout;
56
- }
57
-
58
- interface ContentSectionBaseProps
59
- extends
60
- ContentSharedProps,
61
- Omit<ComponentProps<"section">, "children" | "title"> {
62
- /** Elemento semântico da região. */
63
- as?: "section";
64
- /** Tratamento visual da moldura: contínua (default) ou integrada ao header. */
65
- frameVariant?: ContentFrameVariant;
66
- /** Nível semântico do heading, independente de seu destaque visual. */
67
- level?: ContentHeaderLevel;
68
- }
69
-
70
- interface ContentFieldsetBaseProps
71
- extends
72
- ContentSharedProps,
73
- Omit<ComponentProps<"fieldset">, "children" | "title"> {
74
- /** Usa a semântica nativa de grupo de campos; o título vira `legend`. */
75
- as: "fieldset";
76
- /** Tratamento visual da moldura: contínua (default) ou recortada pela legenda. */
77
- frameVariant?: ContentFrameVariant;
78
- level?: never;
79
- }
80
-
81
- interface ContentSectionShorthandProps extends ContentSectionBaseProps {
82
- title: ReactNode;
83
- /** Ícone ou imagem que identifica a região, antes do título. */
84
- media?: ReactNode;
85
- /** Total de itens ao lado do título da seção. A página não carrega contador (ADR 0009). */
86
- count?: number;
87
- description?: ReactNode;
88
- actions?: ReactNode;
89
- children: ReactNode;
90
- }
91
-
92
- interface ContentFieldsetShorthandProps extends ContentFieldsetBaseProps {
93
- title: ReactNode;
94
- /** Ícone ou imagem decorativa que acompanha o título dentro da legenda. */
95
- media?: ReactNode;
96
- count?: never;
97
- description?: never;
98
- actions?: never;
99
- children: ReactNode;
100
- }
101
-
102
- interface ContentComposedProps {
103
- title?: never;
104
- media?: never;
105
- count?: never;
106
- description?: never;
107
- actions?: never;
108
- children: ReactNode;
109
- }
110
-
111
- export type ContentProps =
112
- | ContentSectionShorthandProps
113
- | ContentFieldsetShorthandProps
114
- | ((ContentSectionBaseProps | ContentFieldsetBaseProps) &
115
- ContentComposedProps);
116
-
117
- /** Região de conteúdo nomeada. A forma curta e a explícita produzem a mesma anatomia. */
118
- export function Content({
119
- as = "section",
120
- title,
121
- media,
122
- count,
123
- description,
124
- actions,
125
- level = 2,
126
- framed = false,
127
- layout = "continuous",
128
- frameVariant = "default",
129
- children,
130
- className,
131
- ...props
132
- }: ContentProps): ReactElement {
133
- const titleId = useId();
134
- const fill = useFillRegion();
135
- const variant = level === 1 && as === "section" ? "page" : "section";
136
- const context: ContentContextValue = {
137
- element: as,
138
- frameVariant,
139
- framed,
140
- layout,
141
- level: level as ContentHeaderLevel,
142
- titleId,
143
- variant,
144
- };
145
- const shorthand = title !== undefined;
146
- const nodes = Children.toArray(children);
147
-
148
- if (layout === "sectioned" && !framed) {
149
- throw new Error('Content layout="sectioned" exige framed.');
150
- }
151
- if (layout === "sectioned" && frameVariant === "legend") {
152
- throw new Error(
153
- 'Content não combina layout="sectioned" com frameVariant="legend".',
154
- );
155
- }
156
-
157
- if (
158
- shorthand &&
159
- nodes.some((node) => isValidElement(node) && node.type === ContentHeader)
160
- ) {
161
- throw new Error(
162
- "Content não permite misturar propriedades de shorthand com ContentHeader explícito.",
163
- );
164
- }
165
- if (!shorthand) {
166
- const headers = nodes.filter(
167
- (node) => isValidElement(node) && node.type === ContentHeader,
168
- );
169
- const bodies = nodes.filter(
170
- (node) => isValidElement(node) && node.type === ContentBody,
171
- );
172
- if (
173
- headers.length !== 1 ||
174
- bodies.length !== 1 ||
175
- headers.length + bodies.length !== nodes.length
176
- ) {
177
- throw new Error(
178
- "Content explícito exige exatamente um ContentHeader e um ContentBody como filhos diretos.",
179
- );
180
- }
181
- }
182
-
183
- const anatomy = shorthand ? (
184
- <>
185
- <ContentHeader>
186
- {hasContentMedia(media) && <ContentMedia>{media}</ContentMedia>}
187
- <ContentTitle>{title}</ContentTitle>
188
- {count !== undefined && <ContentMeta>{count}</ContentMeta>}
189
- {description !== undefined && (
190
- <ContentDescription>{description}</ContentDescription>
191
- )}
192
- {actions !== undefined && <ContentActions>{actions}</ContentActions>}
193
- </ContentHeader>
194
- <ContentBody>{children}</ContentBody>
195
- </>
196
- ) : (
197
- children
198
- );
199
- const ariaLabelledBy =
200
- props["aria-label"] === undefined ? titleId : undefined;
201
-
202
- return (
203
- <ContentContext.Provider value={context}>
204
- {as === "fieldset" ? (
205
- <fieldset
206
- data-slot="content"
207
- data-as="fieldset"
208
- data-framed={framed}
209
- data-frame-variant={frameVariant}
210
- data-layout={layout}
211
- {...fill.attributes}
212
- aria-labelledby={ariaLabelledBy}
213
- className={cn(
214
- !framed && layout === "continuous" && "space-y-3",
215
- framed &&
216
- layout === "continuous" &&
217
- frameVariant === "default" &&
218
- "rounded-xl border border-border p-4",
219
- framed &&
220
- layout === "continuous" &&
221
- frameVariant === "legend" &&
222
- "space-y-3 rounded-xl border border-border px-4 pb-4",
223
- framed &&
224
- layout === "sectioned" &&
225
- "overflow-hidden rounded-xl border border-border",
226
- fill.filling && fillRegionClassName,
227
- className,
228
- )}
229
- {...(props as ComponentProps<"fieldset">)}
230
- >
231
- {fill.provide(anatomy)}
232
- </fieldset>
233
- ) : (
234
- <section
235
- data-slot="content"
236
- data-as="section"
237
- data-framed={framed}
238
- data-frame-variant={frameVariant}
239
- data-layout={layout}
240
- {...fill.attributes}
241
- aria-labelledby={ariaLabelledBy}
242
- className={cn(
243
- layout === "continuous" &&
244
- (variant === "page" ? "space-y-6" : "space-y-3"),
245
- framed &&
246
- layout === "continuous" &&
247
- frameVariant === "default" &&
248
- "rounded-xl border border-border p-4",
249
- framed &&
250
- layout === "continuous" &&
251
- frameVariant === "legend" &&
252
- "rounded-xl border border-border px-4 pb-4",
253
- framed &&
254
- layout === "sectioned" &&
255
- "overflow-hidden rounded-xl border border-border",
256
- fill.filling && fillRegionClassName,
257
- className,
258
- )}
259
- {...(props as ComponentProps<"section">)}
260
- >
261
- {fill.provide(anatomy)}
262
- </section>
263
- )}
264
- </ContentContext.Provider>
265
- );
266
- }
267
-
268
- export type ContentHeaderProps = Omit<HTMLAttributes<HTMLDivElement>, "title">;
269
-
270
- /**
271
- * Header estrutural de Content; não é uma região autônoma. A anatomia é a de
272
- * `SurfaceHeader`, a mesma de `PageHeader`: só os nomes dos slots e a hierarquia mudam.
273
- */
274
- export function ContentHeader({
275
- className,
276
- children,
277
- ...props
278
- }: ContentHeaderProps): ReactElement {
279
- const content = requiredContext(
280
- useContext(ContentContext),
281
- "ContentHeader",
282
- "Content",
283
- );
284
- if (content.element === "fieldset") {
285
- return (
286
- <ContentHeaderContext.Provider value={content}>
287
- <SurfaceHeader
288
- as="legend"
289
- name="ContentHeader"
290
- slot="content"
291
- slots={{ media: ContentMedia, title: ContentTitle }}
292
- mediaPlacement="heading"
293
- invalidMessage="ContentHeader de fieldset aceita somente ContentMedia e ContentTitle; contador, descrição e ações ficam fora da legenda."
294
- className={cn(
295
- content.framed &&
296
- content.layout === "continuous" &&
297
- content.frameVariant === "default" &&
298
- "float-left w-full",
299
- content.framed &&
300
- content.layout === "continuous" &&
301
- content.frameVariant === "legend" &&
302
- "-ml-2 w-auto px-2",
303
- content.framed &&
304
- content.layout === "sectioned" &&
305
- "float-left w-full border-b border-border p-3",
306
- className,
307
- )}
308
- {...props}
309
- >
310
- {children}
311
- </SurfaceHeader>
312
- </ContentHeaderContext.Provider>
313
- );
314
- }
315
- return (
316
- <ContentHeaderContext.Provider value={content}>
317
- <SurfaceHeader
318
- name="ContentHeader"
319
- slot="content"
320
- slots={{
321
- media: ContentMedia,
322
- title: ContentTitle,
323
- count: ContentMeta,
324
- description: ContentDescription,
325
- actions: ContentActions,
326
- }}
327
- mediaPlacement="heading"
328
- className={cn(
329
- content.framed &&
330
- content.layout === "continuous" &&
331
- content.frameVariant === "legend" &&
332
- "flex items-center [&>[data-slot=content-actions]]:-mr-2 [&>[data-slot=content-actions]]:bg-[var(--surface-background,var(--background))] [&>[data-slot=content-actions]]:px-2 [&>[data-slot=content-heading]]:-ml-2 [&>[data-slot=content-heading]]:w-fit [&>[data-slot=content-heading]]:max-w-full [&>[data-slot=content-heading]]:bg-[var(--surface-background,var(--background))] [&>[data-slot=content-heading]]:px-2",
333
- content.framed &&
334
- content.layout === "continuous" &&
335
- content.frameVariant === "legend" &&
336
- content.variant === "section" &&
337
- "-mt-4 min-h-8",
338
- content.framed &&
339
- content.layout === "continuous" &&
340
- content.frameVariant === "legend" &&
341
- content.variant === "page" &&
342
- "-mt-5 min-h-10",
343
- content.framed &&
344
- content.layout === "sectioned" &&
345
- "border-b border-border p-3",
346
- content.variant === "page" &&
347
- "flex-row items-center justify-start gap-3",
348
- className,
349
- )}
350
- {...props}
351
- >
352
- {children}
353
- </SurfaceHeader>
354
- </ContentHeaderContext.Provider>
355
- );
356
- }
357
-
358
- /** `null`, `false` e `undefined` não desenham mídia: uma moldura vazia não identifica nada. */
359
- function hasContentMedia(media: ReactNode): boolean {
360
- return media !== undefined && media !== null && media !== false;
361
- }
362
-
363
- /**
364
- * Um `IconTile` recebido como filho não vira uma segunda moldura: o slot assume o lugar dele e
365
- * herda o que ele declara (`size`, `context`, `variant`, demais props), somando as classes.
366
- */
367
- function adoptIconTile(props: SurfaceMediaProps): SurfaceMediaProps {
368
- const { children, asChild, className } = props;
369
- if (asChild) return props;
370
- // `toArray`, não `only`: um array com um único item (de um `map`/`filter`) é válido aqui.
371
- const nodes = Children.toArray(children);
372
- if (nodes.length !== 1) return props;
373
- const child = nodes[0];
374
- if (!isValidElement<IconTileProps>(child) || child.type !== IconTile) {
375
- return props;
376
- }
377
- // `null` em size/context/variant significa "use o padrão" no IconTile: fica o do slot.
378
- const declared = Object.fromEntries(
379
- Object.entries(child.props).filter(([, value]) => value != null),
380
- ) as SurfaceMediaProps;
381
- return {
382
- ...props,
383
- ...declared,
384
- className: cn(className, declared.className),
385
- children: declared.children,
386
- };
387
- }
388
-
389
- /** Moldura de ícone ou imagem que identifica a região nomeada. */
390
- export function ContentMedia({
391
- size = "xs",
392
- ...rest
393
- }: SurfaceMediaProps): ReactElement {
394
- const context = requiredContext(
395
- useContext(ContentHeaderContext),
396
- "ContentMedia",
397
- "ContentHeader",
398
- );
399
- const props = adoptIconTile({ size, ...rest });
400
- return (
401
- <SurfaceMedia
402
- slot="content-media"
403
- {...props}
404
- aria-hidden={context.element === "fieldset" ? true : props["aria-hidden"]}
405
- />
406
- );
407
- }
408
-
409
- export function ContentTitle({
410
- className,
411
- id,
412
- ...props
413
- }: HTMLAttributes<HTMLElement>): ReactElement {
414
- const context = requiredContext(
415
- useContext(ContentHeaderContext),
416
- "ContentTitle",
417
- "ContentHeader",
418
- );
419
- const Heading =
420
- context.element === "fieldset" ? "span" : headingTags[context.level];
421
- usePageHeadingPresence(context.element !== "fieldset" && context.level === 1);
422
- return (
423
- <Heading
424
- id={id ?? context.titleId}
425
- data-slot="content-title"
426
- className={cn(surfaceHeaderClasses[context.variant].title, className)}
427
- {...props}
428
- />
429
- );
430
- }
431
-
432
- export function ContentDescription({
433
- className,
434
- ...props
435
- }: HTMLAttributes<HTMLParagraphElement>): ReactElement {
436
- const context = requiredContext(
437
- useContext(ContentHeaderContext),
438
- "ContentDescription",
439
- "ContentHeader",
440
- );
441
- return (
442
- <p
443
- data-slot="content-description"
444
- className={cn(
445
- surfaceHeaderClasses[context.variant].description,
446
- className,
447
- )}
448
- {...props}
449
- />
450
- );
451
- }
452
-
453
- /** O contador ao lado do título — o slot `count` do shorthand. */
454
- export function ContentMeta({
455
- className,
456
- ...props
457
- }: HTMLAttributes<HTMLSpanElement>): ReactElement {
458
- const context = requiredContext(
459
- useContext(ContentHeaderContext),
460
- "ContentMeta",
461
- "ContentHeader",
462
- );
463
- return (
464
- <span
465
- data-slot="content-meta"
466
- className={cn(surfaceHeaderClasses[context.variant].count, className)}
467
- {...props}
468
- />
469
- );
470
- }
471
-
472
- export function ContentActions({
473
- className,
474
- ...props
475
- }: HTMLAttributes<HTMLDivElement>): ReactElement {
476
- const context = requiredContext(
477
- useContext(ContentHeaderContext),
478
- "ContentActions",
479
- "ContentHeader",
480
- );
481
- return (
482
- <div
483
- data-slot="content-actions"
484
- className={cn(
485
- surfaceHeaderClasses[context.variant].actions,
486
- context.variant === "page" && "ml-auto w-auto",
487
- className,
488
- )}
489
- {...props}
490
- />
491
- );
492
- }
493
-
494
- export function ContentBody({
495
- className,
496
- children,
497
- ...props
498
- }: HTMLAttributes<HTMLDivElement>): ReactElement {
499
- const content = requiredContext(
500
- useContext(ContentContext),
501
- "ContentBody",
502
- "Content",
503
- );
504
- const fill = useFillRegion();
505
- return (
506
- <div
507
- data-slot="content-body"
508
- {...fill.attributes}
509
- className={cn(
510
- content.element === "fieldset" && "space-y-3",
511
- content.element === "fieldset" &&
512
- content.framed &&
513
- content.layout === "continuous" &&
514
- content.frameVariant === "default" &&
515
- "clear-left pt-3",
516
- content.element === "fieldset" &&
517
- content.framed &&
518
- content.layout === "sectioned" &&
519
- "clear-left",
520
- fill.filling && fillRegionClassName,
521
- className,
522
- )}
523
- {...props}
524
- >
525
- {fill.provide(children)}
526
- </div>
527
- );
528
- }
@@ -1,69 +0,0 @@
1
- /**
2
- * Mídia de superfície (interno): a moldura que abre a linha do header — ADR 0023.
3
- *
4
- * Reusa `IconTile`; nenhuma família cria outra escala de molduras. O slot público de
5
- * cada família (`CardMedia`, `DialogMedia`, …) delega para cá, escolhendo apenas o
6
- * `data-slot` e a escala default.
7
- */
8
- import type { ReactElement } from "react";
9
-
10
- import { Slot } from "@radix-ui/react-slot";
11
-
12
- import {
13
- IconTile,
14
- iconTileVariants,
15
- type IconTileContext,
16
- type IconTileSize,
17
- type IconTileVariant,
18
- } from "../primitives/icon-tile.tsx";
19
- import { cn } from "../../lib/cn.ts";
20
-
21
- export interface SurfaceMediaProps
22
- extends Omit<React.ComponentProps<"span">, "children"> {
23
- /** Escala da moldura; a família define o default. */
24
- size?: IconTileSize;
25
- context?: IconTileContext;
26
- variant?: IconTileVariant;
27
- /**
28
- * Renderiza como o filho: para a marca que já tem visual próprio — um orbe, um
29
- * logotipo — ocupar o slot sem ganhar a moldura por cima.
30
- */
31
- asChild?: boolean;
32
- children?: React.ReactNode;
33
- }
34
-
35
- export function SurfaceMedia({
36
- slot,
37
- size = "sm",
38
- context,
39
- variant,
40
- asChild = false,
41
- className,
42
- ...props
43
- }: SurfaceMediaProps & { slot: string }): ReactElement {
44
- if (asChild) {
45
- return (
46
- <Slot
47
- data-slot={slot}
48
- className={cn(
49
- iconTileVariants({ size, context: null, variant: null }),
50
- // Sem fundo nem borda: quem compõe traz o próprio visual.
51
- "bg-transparent",
52
- "shrink-0",
53
- className,
54
- )}
55
- {...props}
56
- />
57
- );
58
- }
59
- return (
60
- <IconTile
61
- data-slot={slot}
62
- size={size}
63
- {...(context !== undefined ? { context } : {})}
64
- {...(variant !== undefined ? { variant } : {})}
65
- className={cn("shrink-0", className)}
66
- {...props}
67
- />
68
- );
69
- }