@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.
- package/CHANGELOG.md +99 -0
- package/bin/cli.mjs +2 -0
- package/bin/lib/check.mjs +241 -42
- package/bin/lib/copy.mjs +16 -34
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
- package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
- package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
- package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
- package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
- package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
- package/docs/adr/0049-inline-navigation-groups-with-the-heading.md +95 -0
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
- package/registry/templates/app/src/App.tsx +24 -12
- package/src/ui/appearance.ts +149 -0
- package/src/ui/components/patterns/action-form-card.tsx +46 -33
- package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
- package/src/ui/components/patterns/confirm.tsx +3 -1
- package/src/ui/components/patterns/fill-region.ts +8 -7
- package/src/ui/components/patterns/form-dialog.tsx +1 -3
- package/src/ui/components/patterns/form.tsx +8 -7
- package/src/ui/components/patterns/list.tsx +15 -19
- package/src/ui/components/patterns/page.tsx +15 -11
- package/src/ui/components/patterns/pane.tsx +9 -240
- package/src/ui/components/patterns/presentation.tsx +14 -14
- package/src/ui/components/patterns/sidebar.tsx +7 -17
- package/src/ui/components/patterns/split.tsx +111 -59
- package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +90 -27
- package/src/ui/components/patterns/structural-media.tsx +26 -0
- package/src/ui/components/patterns/surface-anatomy.ts +71 -61
- package/src/ui/components/patterns/surface-assistant.tsx +2 -4
- package/src/ui/components/patterns/surface.tsx +620 -0
- package/src/ui/components/patterns/trigger.tsx +11 -9
- package/src/ui/components/primitives/ask.tsx +155 -83
- package/src/ui/components/primitives/button.tsx +11 -9
- package/src/ui/components/primitives/checkbox.tsx +8 -8
- package/src/ui/components/primitives/command.tsx +1 -1
- package/src/ui/components/primitives/composer.tsx +47 -45
- package/src/ui/components/primitives/detail.tsx +9 -2
- package/src/ui/components/primitives/dialog.tsx +60 -26
- package/src/ui/components/primitives/drawer.tsx +58 -12
- package/src/ui/components/primitives/icon-picker.tsx +90 -73
- package/src/ui/components/primitives/input-group.tsx +35 -28
- package/src/ui/components/primitives/input-otp.tsx +16 -16
- package/src/ui/components/primitives/input.tsx +20 -10
- package/src/ui/components/primitives/item.tsx +9 -12
- package/src/ui/components/primitives/menu.tsx +44 -65
- package/src/ui/components/primitives/metric-card.tsx +75 -59
- package/src/ui/components/primitives/radio-group.tsx +9 -9
- package/src/ui/components/primitives/select.tsx +344 -209
- package/src/ui/components/primitives/switch.tsx +10 -10
- package/src/ui/components/primitives/table.tsx +29 -24
- package/src/ui/components/primitives/tabs.tsx +4 -2
- package/src/ui/components/primitives/textarea.tsx +6 -6
- package/src/ui/components/primitives/toggle-group-context.ts +24 -0
- package/src/ui/components/primitives/toggle-group.tsx +116 -74
- package/src/ui/components/primitives/toggle.tsx +82 -16
- package/src/ui/docs/DocBrowser.tsx +77 -69
- package/src/ui/docs/content/action-form-card.md +9 -9
- package/src/ui/docs/content/action-list.md +76 -70
- package/src/ui/docs/content/action-view.md +33 -18
- package/src/ui/docs/content/button.md +1 -1
- package/src/ui/docs/content/communication.md +1 -1
- package/src/ui/docs/content/compositions.md +479 -0
- package/src/ui/docs/content/customization.md +64 -11
- package/src/ui/docs/content/detail.md +8 -6
- package/src/ui/docs/content/dialog.md +105 -60
- package/src/ui/docs/content/drawer.md +68 -22
- package/src/ui/docs/content/field.md +14 -15
- package/src/ui/docs/content/icon-tile.md +14 -13
- package/src/ui/docs/content/page.md +44 -34
- package/src/ui/docs/content/pane.md +42 -290
- package/src/ui/docs/content/presentation.md +3 -3
- package/src/ui/docs/content/separator.md +6 -6
- package/src/ui/docs/content/sidebar.md +68 -266
- package/src/ui/docs/content/skeleton.md +20 -18
- package/src/ui/docs/content/split.md +14 -14
- package/src/ui/docs/content/surface.md +513 -0
- package/src/ui/docs/content/tabs.md +20 -20
- package/src/ui/docs/content/toggle.md +117 -65
- package/src/ui/docs/content/tokens.md +124 -40
- package/src/ui/docs/registry.tsx +10 -6
- package/src/ui/index.ts +30 -27
- package/src/ui/meta.ts +15 -27
- package/src/ui/react.tsx +40 -43
- package/src/ui/theme.css +311 -64
- package/src/ui/components/patterns/content-header.tsx +0 -528
- package/src/ui/components/patterns/surface-media.tsx +0 -69
- package/src/ui/components/primitives/card.tsx +0 -191
- package/src/ui/docs/content/card.md +0 -292
- 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 {
|
|
16
|
+
import { StructuralHeader } from "../patterns/structural-header.tsx";
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
type
|
|
20
|
-
} from "../patterns/
|
|
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
|
-
<
|
|
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
|
-
</
|
|
346
|
+
</StructuralHeader>
|
|
345
347
|
);
|
|
346
348
|
}
|
|
347
349
|
|
|
@@ -373,31 +375,55 @@ export function DialogActions({
|
|
|
373
375
|
);
|
|
374
376
|
}
|
|
375
377
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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"
|
|
459
|
-
|
|
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 {
|
|
15
|
+
import { StructuralHeader } from "../patterns/structural-header.tsx";
|
|
16
16
|
import {
|
|
17
|
-
|
|
18
|
-
type
|
|
19
|
-
} from "../patterns/
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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:
|
|
185
|
-
return <
|
|
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
|
-
|
|
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
|
|
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
|
|
51
|
-
import { cn } from
|
|
52
|
-
import { controlHeight, focusRing } from
|
|
53
|
-
import {
|
|
54
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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 =
|
|
137
|
+
placeholder = "Selecione um ícone…",
|
|
131
138
|
disabled,
|
|
132
139
|
className,
|
|
133
140
|
id,
|
|
134
|
-
|
|
135
|
-
|
|
141
|
+
"aria-invalid": ariaInvalid,
|
|
142
|
+
"aria-describedby": ariaDescribedBy,
|
|
136
143
|
}: IconPickerProps): React.ReactElement {
|
|
137
|
-
const [open, setOpen] = React.useState(false)
|
|
138
|
-
const Current = value !==
|
|
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
|
-
|
|
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
|
-
|
|
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">
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
5
|
-
import type { ControlShape } from
|
|
6
|
-
import { Button, type ButtonProps } from
|
|
7
|
-
import { Input } from
|
|
8
|
-
import { Textarea } from
|
|
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({
|
|
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-
|
|
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<
|
|
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,
|
|
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
|
+
};
|