@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.
- package/CHANGELOG.md +120 -1
- package/bin/cli.mjs +3 -9
- package/bin/lib/check.mjs +242 -49
- package/bin/lib/copy.mjs +17 -35
- package/bin/lib/gen-runner.mjs +2 -0
- package/bin/lib/gen-stubs.mjs +3 -1
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
- package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
- package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
- package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
- package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +59 -0
- 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/code-style.md +1 -1
- 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/skills/create-opus-action/references/contract-and-binding.md +3 -3
- package/registry/templates/app/src/App.tsx +24 -12
- package/src/core/contracts.ts +1 -1
- package/src/core/runtime.ts +4 -37
- package/src/core/types.ts +5 -23
- package/src/design/index.ts +14 -3
- package/src/testing/index.ts +6 -26
- 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 +64 -14
- 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} +50 -19
- 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 +93 -70
- 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 +58 -88
- 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 +85 -70
- package/src/ui/docs/content/action-view.md +33 -18
- package/src/ui/docs/content/actions.md +6 -7
- package/src/ui/docs/content/button.md +1 -1
- package/src/ui/docs/content/communication.md +1 -1
- 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 +100 -58
- package/src/ui/docs/content/drawer.md +63 -19
- 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 +34 -26
- package/src/ui/docs/content/pane.md +41 -290
- package/src/ui/docs/content/presentation.md +3 -3
- package/src/ui/docs/content/semantic-context.md +0 -1
- 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 +82 -83
- package/src/ui/docs/content/testing.md +2 -3
- package/src/ui/docs/content/toggle.md +120 -63
- package/src/ui/docs/content/tokens.md +124 -40
- package/src/ui/docs/registry.tsx +4 -6
- package/src/ui/drivers/react.tsx +4 -3
- package/src/ui/index.ts +30 -27
- package/src/ui/meta.ts +15 -27
- package/src/ui/react.tsx +42 -46
- package/src/ui/theme.css +311 -64
- package/src/vite/design.ts +23 -21
- package/src/ui/components/patterns/content-header.tsx +0 -532
- 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 -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 {
|
|
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,
|
|
@@ -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
|
|
48
|
-
import { cn } from
|
|
49
|
-
import { controlHeight, focusRing } from
|
|
50
|
-
import {
|
|
51
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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 =
|
|
137
|
+
placeholder = "Selecione um ícone…",
|
|
125
138
|
disabled,
|
|
126
139
|
className,
|
|
127
140
|
id,
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
"aria-invalid": ariaInvalid,
|
|
142
|
+
"aria-describedby": ariaDescribedBy,
|
|
130
143
|
}: IconPickerProps): React.ReactElement {
|
|
131
|
-
const [open, setOpen] = React.useState(false)
|
|
132
|
-
const Current = value !==
|
|
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
|
-
|
|
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
|
-
|
|
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">
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
}
|