@softize/opus 19.0.0 → 20.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 +193 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/check.mjs +198 -10
- package/bin/lib/copy.mjs +29 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +5 -0
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/data-products.md +15 -9
- package/docs/protocol.md +3 -4
- package/docs/releasing.md +2 -2
- package/package.json +2 -2
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
- package/registry/templates/app/package.json +1 -1
- package/src/core/runtime.ts +96 -8
- package/src/core/types.ts +5 -2
- package/src/ui/components/patterns/action-form-card.tsx +11 -1
- package/src/ui/components/patterns/content-header.tsx +15 -3
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/list.tsx +89 -78
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/presentation.tsx +52 -37
- package/src/ui/components/patterns/split.tsx +44 -4
- package/src/ui/components/patterns/surface-anatomy.ts +35 -8
- package/src/ui/components/patterns/trigger.tsx +12 -2
- package/src/ui/components/primitives/alert.tsx +9 -1
- package/src/ui/components/primitives/avatar.tsx +7 -3
- package/src/ui/components/primitives/carousel.tsx +34 -5
- package/src/ui/components/primitives/checkbox.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +2 -1
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/dialog.tsx +41 -10
- package/src/ui/components/primitives/drawer.tsx +9 -1
- package/src/ui/components/primitives/empty.tsx +1 -1
- package/src/ui/components/primitives/icon-picker.tsx +6 -2
- package/src/ui/components/primitives/icon-tile.tsx +7 -0
- package/src/ui/components/primitives/input-group.tsx +9 -2
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/kbd.tsx +1 -1
- package/src/ui/components/primitives/label.tsx +3 -0
- package/src/ui/components/primitives/menu.tsx +5 -0
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/components/primitives/progress.tsx +9 -1
- package/src/ui/components/primitives/slider.tsx +62 -11
- package/src/ui/components/primitives/truncate.tsx +10 -1
- package/src/ui/docs/content/accordion.md +74 -27
- package/src/ui/docs/content/action-form-card.md +24 -15
- package/src/ui/docs/content/action-form-dialog.md +17 -7
- package/src/ui/docs/content/action-form.md +92 -77
- package/src/ui/docs/content/action-list-dialog.md +75 -38
- package/src/ui/docs/content/action-list.md +134 -52
- package/src/ui/docs/content/action-trigger.md +27 -10
- package/src/ui/docs/content/action-view.md +14 -14
- package/src/ui/docs/content/actions.md +161 -66
- package/src/ui/docs/content/ai.md +43 -44
- package/src/ui/docs/content/alert.md +83 -29
- package/src/ui/docs/content/ask.md +51 -26
- package/src/ui/docs/content/aspect-ratio.md +22 -24
- package/src/ui/docs/content/audit.md +23 -19
- package/src/ui/docs/content/auth.md +38 -19
- package/src/ui/docs/content/avatar.md +54 -5
- package/src/ui/docs/content/badge.md +29 -9
- package/src/ui/docs/content/breadcrumb.md +21 -8
- package/src/ui/docs/content/button.md +73 -43
- package/src/ui/docs/content/calendar.md +134 -26
- package/src/ui/docs/content/card.md +41 -15
- package/src/ui/docs/content/carousel.md +37 -6
- package/src/ui/docs/content/chat.md +118 -37
- package/src/ui/docs/content/checkbox.md +73 -7
- package/src/ui/docs/content/cli.md +94 -55
- package/src/ui/docs/content/collapsible.md +43 -19
- package/src/ui/docs/content/command.md +30 -8
- package/src/ui/docs/content/composer.md +23 -8
- package/src/ui/docs/content/content.md +31 -7
- package/src/ui/docs/content/copyable.md +39 -8
- package/src/ui/docs/content/customization.md +55 -46
- package/src/ui/docs/content/cycle.md +33 -18
- package/src/ui/docs/content/data-products.md +115 -0
- package/src/ui/docs/content/data-state.md +20 -6
- package/src/ui/docs/content/data.md +20 -19
- package/src/ui/docs/content/detail.md +12 -5
- package/src/ui/docs/content/dialog.md +107 -24
- package/src/ui/docs/content/dictionary-value.md +4 -4
- package/src/ui/docs/content/dock.md +54 -19
- package/src/ui/docs/content/dot.md +35 -5
- package/src/ui/docs/content/drawer.md +63 -19
- package/src/ui/docs/content/empty-value.md +20 -6
- package/src/ui/docs/content/empty.md +24 -3
- package/src/ui/docs/content/events.md +76 -16
- package/src/ui/docs/content/executions.md +9 -9
- package/src/ui/docs/content/field.md +21 -6
- package/src/ui/docs/content/getting-started.md +44 -34
- package/src/ui/docs/content/icon-picker.md +19 -13
- package/src/ui/docs/content/icon-tile.md +28 -5
- package/src/ui/docs/content/input-otp.md +58 -7
- package/src/ui/docs/content/input.md +22 -3
- package/src/ui/docs/content/item.md +53 -6
- package/src/ui/docs/content/kbd.md +5 -1
- package/src/ui/docs/content/label.md +13 -4
- package/src/ui/docs/content/log.md +5 -5
- package/src/ui/docs/content/markdown.md +5 -4
- package/src/ui/docs/content/mcp.md +49 -23
- package/src/ui/docs/content/menu.md +95 -10
- package/src/ui/docs/content/metric-card.md +12 -4
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +118 -12
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +55 -5
- package/src/ui/docs/content/popover.md +42 -5
- package/src/ui/docs/content/presentation.md +91 -47
- package/src/ui/docs/content/progress.md +27 -14
- package/src/ui/docs/content/queue.md +39 -21
- package/src/ui/docs/content/radio-group.md +26 -4
- package/src/ui/docs/content/router.md +29 -10
- package/src/ui/docs/content/runtime.md +90 -54
- package/src/ui/docs/content/scheduler.md +53 -21
- package/src/ui/docs/content/scroll-area.md +42 -35
- package/src/ui/docs/content/select.md +78 -33
- package/src/ui/docs/content/semantic-context.md +21 -2
- package/src/ui/docs/content/separator.md +15 -9
- package/src/ui/docs/content/sidebar.md +3 -2
- package/src/ui/docs/content/skeleton.md +6 -1
- package/src/ui/docs/content/slider.md +37 -9
- package/src/ui/docs/content/spinner.md +4 -3
- package/src/ui/docs/content/split.md +19 -0
- package/src/ui/docs/content/storage.md +8 -9
- package/src/ui/docs/content/switch.md +10 -7
- package/src/ui/docs/content/table.md +46 -8
- package/src/ui/docs/content/tabs.md +14 -4
- package/src/ui/docs/content/testing.md +76 -31
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +48 -7
- package/src/ui/docs/content/toggle.md +3 -3
- package/src/ui/docs/content/tokens.md +15 -26
- package/src/ui/docs/content/tooltip.md +18 -2
- package/src/ui/docs/content/truncate.md +36 -6
- package/src/ui/docs/content/ui.md +74 -22
- package/src/ui/docs/content/upgrading.md +45 -36
- package/src/ui/docs/plugin.ts +36 -2
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +31 -31
- package/src/vite/design.ts +16 -4
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* Dois layouts, e cada um é um pacote indivisível:
|
|
10
10
|
*
|
|
11
11
|
* - `continuous` — sem divisores. O ritmo separa: o header se aproxima do corpo e a
|
|
12
|
-
* última região
|
|
12
|
+
* última região — seja ela o header, o body ou o footer — fecha o espaço de baixo com
|
|
13
|
+
* o mesmo respiro do topo. É o card de leitura.
|
|
13
14
|
* - `sectioned` — regiões simétricas, com divisor entre as que forem adjacentes. É o
|
|
14
15
|
* modal, o drawer e o painel.
|
|
15
16
|
*
|
|
@@ -18,7 +19,14 @@
|
|
|
18
19
|
* é mais folgado que um painel de operação.
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
createContext,
|
|
24
|
+
createElement,
|
|
25
|
+
useContext,
|
|
26
|
+
type ReactElement,
|
|
27
|
+
type ReactNode,
|
|
28
|
+
} from "react";
|
|
29
|
+
import { FillBoundary } from "./fill-region.ts";
|
|
22
30
|
|
|
23
31
|
export type SurfaceFamily = "card" | "dialog" | "drawer" | "pane";
|
|
24
32
|
export type SurfaceLayout = "continuous" | "sectioned";
|
|
@@ -76,9 +84,11 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
76
84
|
card: {
|
|
77
85
|
regions: {
|
|
78
86
|
// O `pb-3` do header e do body encurtam a distância até a região seguinte; quando
|
|
79
|
-
//
|
|
87
|
+
// a região é a última, ela reassume o respiro de baixo (`[&:last-child]:pb-5`) —
|
|
88
|
+
// inclusive o header sozinho, que sem isso fecharia o card mais apertado embaixo
|
|
89
|
+
// do que em cima.
|
|
80
90
|
continuous: {
|
|
81
|
-
header: "px-5 pt-5 pb-3",
|
|
91
|
+
header: "px-5 pt-5 pb-3 [&:last-child]:pb-5",
|
|
82
92
|
body: "px-5 py-3 [&:last-child]:pb-5 [&:first-child]:pt-5",
|
|
83
93
|
footer: "px-5 pt-3 pb-5",
|
|
84
94
|
},
|
|
@@ -95,7 +105,7 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
95
105
|
dialog: {
|
|
96
106
|
regions: {
|
|
97
107
|
continuous: {
|
|
98
|
-
header: "p-4 pb-2",
|
|
108
|
+
header: "p-4 pb-2 [&:last-child]:pb-4",
|
|
99
109
|
body: "p-4 py-2 [&:last-child]:pb-4 [&:first-child]:pt-4",
|
|
100
110
|
footer: "p-4 pt-2",
|
|
101
111
|
},
|
|
@@ -112,7 +122,7 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
112
122
|
drawer: {
|
|
113
123
|
regions: {
|
|
114
124
|
continuous: {
|
|
115
|
-
header: "p-4 pb-2",
|
|
125
|
+
header: "p-4 pb-2 [&:last-child]:pb-4",
|
|
116
126
|
body: "p-4 py-2 [&:last-child]:pb-4 [&:first-child]:pt-4",
|
|
117
127
|
footer: "p-4 pt-2",
|
|
118
128
|
},
|
|
@@ -129,7 +139,7 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
129
139
|
pane: {
|
|
130
140
|
regions: {
|
|
131
141
|
continuous: {
|
|
132
|
-
header: "flex shrink-0 items-center px-3 pt-3 pb-2",
|
|
142
|
+
header: "flex shrink-0 items-center px-3 pt-3 pb-2 [&:last-child]:pb-3",
|
|
133
143
|
body: "p-3 [&:first-child]:pt-3",
|
|
134
144
|
footer: "px-3 pt-2 pb-3",
|
|
135
145
|
},
|
|
@@ -200,7 +210,24 @@ export const surfaceActionsClasses = "flex shrink-0 items-center gap-2";
|
|
|
200
210
|
*/
|
|
201
211
|
const SurfaceLayoutContext = createContext<SurfaceLayout | null>(null);
|
|
202
212
|
|
|
203
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Publica o layout da superfície para os slots. A superfície também interrompe a
|
|
215
|
+
* propagação de `layout="fill"` da página (ADR 0024): a altura do seu corpo é decisão dela,
|
|
216
|
+
* inclusive quando um portal a renderiza dentro de uma página `fill`.
|
|
217
|
+
*/
|
|
218
|
+
export function SurfaceLayoutProvider({
|
|
219
|
+
value,
|
|
220
|
+
children,
|
|
221
|
+
}: {
|
|
222
|
+
value: SurfaceLayout;
|
|
223
|
+
children?: ReactNode;
|
|
224
|
+
}): ReactElement {
|
|
225
|
+
return createElement(
|
|
226
|
+
SurfaceLayoutContext.Provider,
|
|
227
|
+
{ value },
|
|
228
|
+
createElement(FillBoundary, null, children),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
204
231
|
|
|
205
232
|
const familyDefaultLayout: Record<SurfaceFamily, SurfaceLayout> = {
|
|
206
233
|
card: "continuous",
|
|
@@ -66,6 +66,7 @@ export interface ActionTriggerProps<TInput, TData> {
|
|
|
66
66
|
title: string;
|
|
67
67
|
/** Conteúdo relevante apresentado antes das ações. */
|
|
68
68
|
body?: ReactNode;
|
|
69
|
+
/** Texto do botão de confirmar. Default: `action.label`; o `label` do gatilho só sem ele. */
|
|
69
70
|
actionLabel?: string;
|
|
70
71
|
cancelLabel?: string;
|
|
71
72
|
};
|
|
@@ -123,6 +124,15 @@ export function ActionTrigger<TInput, TData>({
|
|
|
123
124
|
|
|
124
125
|
const buttonLabel =
|
|
125
126
|
label ?? (typeof action.label === "string" ? action.label : action.name);
|
|
127
|
+
/**
|
|
128
|
+
* Botão de confirmar: `confirmLabel` do contrato, depois o `label` da action. O `label` do
|
|
129
|
+
* gatilho costuma ser um nome acessível que identifica o alvo ("Excluir Unidade Centro"); na
|
|
130
|
+
* pergunta o alvo já está no corpo, então ele só entra quando o contrato não tem rótulo.
|
|
131
|
+
*/
|
|
132
|
+
const confirmActionLabel =
|
|
133
|
+
action.label !== undefined
|
|
134
|
+
? msgText(action.label, buttonLabel)
|
|
135
|
+
: buttonLabel;
|
|
126
136
|
|
|
127
137
|
// Prop sobrepõe; sem prop, o ConfirmSpec DECLARADO no contrato (action.confirm) vale.
|
|
128
138
|
const spec = action.confirm;
|
|
@@ -135,7 +145,7 @@ export function ActionTrigger<TInput, TData>({
|
|
|
135
145
|
? { body: msgText(spec.message, "") }
|
|
136
146
|
: {}),
|
|
137
147
|
...(spec.confirmLabel !== undefined
|
|
138
|
-
? { actionLabel: msgText(spec.confirmLabel,
|
|
148
|
+
? { actionLabel: msgText(spec.confirmLabel, confirmActionLabel) }
|
|
139
149
|
: {}),
|
|
140
150
|
...(spec.cancelLabel !== undefined
|
|
141
151
|
? { cancelLabel: msgText(spec.cancelLabel, "Cancelar") }
|
|
@@ -248,7 +258,7 @@ export function ActionTrigger<TInput, TData>({
|
|
|
248
258
|
busy={isLoading}
|
|
249
259
|
onClick={fire}
|
|
250
260
|
>
|
|
251
|
-
{confirm.actionLabel ??
|
|
261
|
+
{confirm.actionLabel ?? confirmActionLabel}
|
|
252
262
|
</Button>
|
|
253
263
|
</ButtonGroup>
|
|
254
264
|
</DialogFooter>
|
|
@@ -71,6 +71,14 @@ function alertMediaClasses(context: AlertContext): string {
|
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* `role="alert"` interrompe o leitor de tela; só perigo e atenção justificam a interrupção.
|
|
76
|
+
* Os demais contextos são anunciados sem cortar a leitura, como `role="status"`.
|
|
77
|
+
*/
|
|
78
|
+
function alertRole(context: AlertContext): "alert" | "status" {
|
|
79
|
+
return context === "danger" || context === "warning" ? "alert" : "status";
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
const alertHeaderClasses =
|
|
75
83
|
"col-start-2 row-start-1 flex min-w-0 flex-col justify-center gap-1";
|
|
76
84
|
|
|
@@ -147,7 +155,7 @@ export function Alert({
|
|
|
147
155
|
data-slot="alert"
|
|
148
156
|
data-context={resolved.context}
|
|
149
157
|
data-variant={resolved.variant}
|
|
150
|
-
role=
|
|
158
|
+
role={alertRole(resolved.context)}
|
|
151
159
|
className={cn(alertVariants(resolved), className)}
|
|
152
160
|
{...props}
|
|
153
161
|
>
|
|
@@ -8,6 +8,10 @@ import type { ControlSize } from './control.ts'
|
|
|
8
8
|
* Ejetado (14.0.0): `size` segue a escala de control.ts — o avatar de uma linha `sm` mede o
|
|
9
9
|
* mesmo que o controle `sm` ao lado (2rem); `xs` (1.5rem) é a lista densa; `default` (2.25rem)
|
|
10
10
|
* e `lg` (2.5rem) pareiam com Button/Input. O fallback e o selo acompanham o tamanho.
|
|
11
|
+
*
|
|
12
|
+
* O root não recorta (20.0.0): imagem e fallback herdam o raio do root e se recortam sozinhos, e
|
|
13
|
+
* o AvatarBadge, que transborda o círculo no canto, aparece inteiro. A forma continua declarada
|
|
14
|
+
* no root (`rounded-full` por padrão, `rounded-lg` para logo) e o anel do AvatarGroup a segue.
|
|
11
15
|
*/
|
|
12
16
|
function Avatar({
|
|
13
17
|
className,
|
|
@@ -21,7 +25,7 @@ function Avatar({
|
|
|
21
25
|
data-slot="avatar"
|
|
22
26
|
data-size={size}
|
|
23
27
|
className={cn(
|
|
24
|
-
"group/avatar relative flex shrink-0
|
|
28
|
+
"group/avatar relative flex shrink-0 rounded-full select-none data-[size=xs]:size-6 data-[size=sm]:size-8 data-[size=default]:size-9 data-[size=lg]:size-10",
|
|
25
29
|
className
|
|
26
30
|
)}
|
|
27
31
|
{...props}
|
|
@@ -36,7 +40,7 @@ function AvatarImage({
|
|
|
36
40
|
return (
|
|
37
41
|
<AvatarPrimitive.Image
|
|
38
42
|
data-slot="avatar-image"
|
|
39
|
-
className={cn("aspect-square size-full", className)}
|
|
43
|
+
className={cn("aspect-square size-full rounded-[inherit] object-cover", className)}
|
|
40
44
|
{...props}
|
|
41
45
|
/>
|
|
42
46
|
)
|
|
@@ -50,7 +54,7 @@ function AvatarFallback({
|
|
|
50
54
|
<AvatarPrimitive.Fallback
|
|
51
55
|
data-slot="avatar-fallback"
|
|
52
56
|
className={cn(
|
|
53
|
-
"flex size-full items-center justify-center rounded-
|
|
57
|
+
"flex size-full items-center justify-center overflow-hidden rounded-[inherit] bg-muted text-sm text-muted-foreground group-data-[size=xs]/avatar:text-xs group-data-[size=sm]/avatar:text-xs",
|
|
54
58
|
className
|
|
55
59
|
)}
|
|
56
60
|
{...props}
|
|
@@ -34,12 +34,33 @@ function useCarousel() {
|
|
|
34
34
|
const context = React.useContext(CarouselContext)
|
|
35
35
|
|
|
36
36
|
if (!context) {
|
|
37
|
-
throw new Error(
|
|
37
|
+
throw new Error(
|
|
38
|
+
"useCarousel só funciona dentro de <Carousel />. Coloque CarouselContent, CarouselItem, CarouselPrevious e CarouselNext dentro de um <Carousel>."
|
|
39
|
+
)
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
return context
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
// Quem usa as setas por conta própria: campos editáveis e widgets compostos que navegam por dentro
|
|
46
|
+
// (Slider, RadioGroup, Tabs, listas, menus; ToggleGroup e Accordion pelos itens de coleção do
|
|
47
|
+
// Radix). A captura do carrossel roda antes deles e, com `preventDefault`, os handlers do Radix
|
|
48
|
+
// desistem do evento — por isso as setas vindas daqui ficam com o widget.
|
|
49
|
+
const ARROW_OWNERS =
|
|
50
|
+
"input, textarea, select, [contenteditable]:not([contenteditable=false]), " +
|
|
51
|
+
"[role=slider], [role=radiogroup], [role=tablist], [role=listbox], [role=menu], [role=menubar], " +
|
|
52
|
+
"[role=combobox], [role=grid], [role=tree], [role=spinbutton], [role=toolbar], " +
|
|
53
|
+
"[data-radix-collection-item]"
|
|
54
|
+
|
|
55
|
+
// Só conta o dono que está DENTRO do carrossel (ou num portal aberto a partir dele): um carrossel
|
|
56
|
+
// posto dentro de uma toolbar ou de uma célula de grid continua respondendo às setas.
|
|
57
|
+
function ownsArrowKeys(target: EventTarget, carousel: EventTarget): boolean {
|
|
58
|
+
if (!(target instanceof HTMLElement)) return false
|
|
59
|
+
if (target.isContentEditable) return true
|
|
60
|
+
const owner = target.closest(ARROW_OWNERS)
|
|
61
|
+
return owner !== null && !(carousel instanceof Node && owner.contains(carousel))
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
function Carousel({
|
|
44
65
|
orientation = "horizontal",
|
|
45
66
|
opts,
|
|
@@ -75,15 +96,22 @@ function Carousel({
|
|
|
75
96
|
|
|
76
97
|
const handleKeyDown = React.useCallback(
|
|
77
98
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
78
|
-
|
|
99
|
+
// As setas de um campo editável dentro do slide movem o cursor ou mudam a opção; o
|
|
100
|
+
// carrossel não as rouba.
|
|
101
|
+
if (ownsArrowKeys(event.target, event.currentTarget)) return
|
|
102
|
+
const [prevKey, nextKey] =
|
|
103
|
+
orientation === "vertical"
|
|
104
|
+
? ["ArrowUp", "ArrowDown"]
|
|
105
|
+
: ["ArrowLeft", "ArrowRight"]
|
|
106
|
+
if (event.key === prevKey) {
|
|
79
107
|
event.preventDefault()
|
|
80
108
|
scrollPrev()
|
|
81
|
-
} else if (event.key ===
|
|
109
|
+
} else if (event.key === nextKey) {
|
|
82
110
|
event.preventDefault()
|
|
83
111
|
scrollNext()
|
|
84
112
|
}
|
|
85
113
|
},
|
|
86
|
-
[scrollPrev, scrollNext]
|
|
114
|
+
[orientation, scrollPrev, scrollNext]
|
|
87
115
|
)
|
|
88
116
|
|
|
89
117
|
React.useEffect(() => {
|
|
@@ -98,7 +126,8 @@ function Carousel({
|
|
|
98
126
|
api.on("select", onSelect)
|
|
99
127
|
|
|
100
128
|
return () => {
|
|
101
|
-
api
|
|
129
|
+
api.off("reInit", onSelect)
|
|
130
|
+
api.off("select", onSelect)
|
|
102
131
|
}
|
|
103
132
|
}, [api, onSelect])
|
|
104
133
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
-
import { CheckIcon } from "lucide-react"
|
|
2
|
+
import { CheckIcon, MinusIcon } from "lucide-react"
|
|
3
3
|
import { Checkbox as CheckboxPrimitive } from "radix-ui"
|
|
4
4
|
|
|
5
5
|
import { cn } from '../../lib/cn.ts'
|
|
@@ -12,7 +12,7 @@ function Checkbox({
|
|
|
12
12
|
<CheckboxPrimitive.Root
|
|
13
13
|
data-slot="checkbox"
|
|
14
14
|
className={cn(
|
|
15
|
-
"peer size-4 shrink-0 rounded-[0.25rem] border border-input transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-context-danger/40 dark:data-[state=checked]:bg-primary",
|
|
15
|
+
"peer group/checkbox size-4 shrink-0 rounded-[0.25rem] border border-input transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-context-danger/40 dark:data-[state=checked]:bg-primary dark:data-[state=indeterminate]:bg-primary",
|
|
16
16
|
className
|
|
17
17
|
)}
|
|
18
18
|
{...props}
|
|
@@ -21,7 +21,15 @@ function Checkbox({
|
|
|
21
21
|
data-slot="checkbox-indicator"
|
|
22
22
|
className="grid place-content-center text-current transition-none"
|
|
23
23
|
>
|
|
24
|
-
|
|
24
|
+
{/* Indeterminado ("alguns marcados") tem glifo próprio: o traço não se confunde com o check. */}
|
|
25
|
+
<CheckIcon
|
|
26
|
+
data-slot="checkbox-check"
|
|
27
|
+
className="size-3.5 group-data-[state=indeterminate]/checkbox:hidden"
|
|
28
|
+
/>
|
|
29
|
+
<MinusIcon
|
|
30
|
+
data-slot="checkbox-indeterminate"
|
|
31
|
+
className="hidden size-3.5 group-data-[state=indeterminate]/checkbox:block"
|
|
32
|
+
/>
|
|
25
33
|
</CheckboxPrimitive.Indicator>
|
|
26
34
|
</CheckboxPrimitive.Root>
|
|
27
35
|
)
|
|
@@ -103,12 +103,13 @@ function CommandList({
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function CommandEmpty({
|
|
106
|
+
className,
|
|
106
107
|
...props
|
|
107
108
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
|
108
109
|
return (
|
|
109
110
|
<CommandPrimitive.Empty
|
|
110
111
|
data-slot="command-empty"
|
|
111
|
-
className="py-6 text-center text-sm"
|
|
112
|
+
className={cn("py-6 text-center text-sm", className)}
|
|
112
113
|
{...props}
|
|
113
114
|
/>
|
|
114
115
|
);
|
|
@@ -2,6 +2,76 @@ import * as React from 'react'
|
|
|
2
2
|
import { CheckIcon, CopyIcon } from 'lucide-react'
|
|
3
3
|
import { cn } from '../../lib/cn.ts'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* A região viva que anuncia "Copiado", uma por documento.
|
|
7
|
+
*
|
|
8
|
+
* Uma tela com dezenas de Copyable (uma tabela de IDs, por exemplo) montaria dezenas de regiões
|
|
9
|
+
* `role="status"` vazias no fim do `body`. Todas as instâncias compartilham a mesma: a primeira a
|
|
10
|
+
* montar cria o nó, a última a desmontar o remove. Quem escreve a confirmação vira dono do texto;
|
|
11
|
+
* só o dono o apaga, para que a expiração de uma cópia antiga não silencie a mais recente.
|
|
12
|
+
* Se algo fora do React tirar o nó do documento (um `body.innerHTML` de script legado, por
|
|
13
|
+
* exemplo), a próxima montagem ou cópia o recoloca, para que o anúncio não suma em silêncio.
|
|
14
|
+
*/
|
|
15
|
+
interface SharedStatus {
|
|
16
|
+
node: HTMLSpanElement
|
|
17
|
+
users: number
|
|
18
|
+
owner: symbol | undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const statusByDocument = new WeakMap<Document, SharedStatus>()
|
|
22
|
+
|
|
23
|
+
function acquireStatus(doc: Document): SharedStatus {
|
|
24
|
+
let shared = statusByDocument.get(doc)
|
|
25
|
+
if (shared === undefined) {
|
|
26
|
+
const node = doc.createElement('span')
|
|
27
|
+
node.setAttribute('role', 'status')
|
|
28
|
+
node.setAttribute('aria-live', 'polite')
|
|
29
|
+
node.setAttribute('data-slot', 'copyable-status')
|
|
30
|
+
node.className = 'sr-only'
|
|
31
|
+
doc.body.appendChild(node)
|
|
32
|
+
shared = { node, users: 0, owner: undefined }
|
|
33
|
+
statusByDocument.set(doc, shared)
|
|
34
|
+
}
|
|
35
|
+
ensureConnected(shared.node, doc)
|
|
36
|
+
shared.users += 1
|
|
37
|
+
return shared
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Devolve `true` quando precisou recolocar a região no `body`.
|
|
41
|
+
function ensureConnected(node: HTMLSpanElement, doc: Document): boolean {
|
|
42
|
+
if (node.isConnected) return false
|
|
43
|
+
doc.body.appendChild(node)
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function releaseStatus(doc: Document, shared: SharedStatus, id: symbol): void {
|
|
48
|
+
if (shared.owner === id) clearStatus(shared, id)
|
|
49
|
+
shared.users -= 1
|
|
50
|
+
if (shared.users > 0) return
|
|
51
|
+
shared.node.remove()
|
|
52
|
+
if (statusByDocument.get(doc) === shared) statusByDocument.delete(doc)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function announceCopied(shared: SharedStatus, id: symbol): void {
|
|
56
|
+
shared.owner = id
|
|
57
|
+
const doc = shared.node.ownerDocument
|
|
58
|
+
// Um nó de texto novo, em vez de manter o anterior: uma segunda cópia enquanto "Copiado" ainda
|
|
59
|
+
// está na região (da mesma instância ou de outra) continua sendo uma mudança a anunciar.
|
|
60
|
+
const write = (): void => {
|
|
61
|
+
if (shared.owner === id) shared.node.replaceChildren(doc.createTextNode('Copiado'))
|
|
62
|
+
}
|
|
63
|
+
// Uma região recém-inserida já com texto costuma não ser lida: depois de recolocá-la, o texto
|
|
64
|
+
// entra no tick seguinte, quando o leitor de tela já a conhece.
|
|
65
|
+
if (ensureConnected(shared.node, doc)) setTimeout(write, 0)
|
|
66
|
+
else write()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function clearStatus(shared: SharedStatus, id: symbol): void {
|
|
70
|
+
if (shared.owner !== id) return
|
|
71
|
+
shared.owner = undefined
|
|
72
|
+
shared.node.replaceChildren()
|
|
73
|
+
}
|
|
74
|
+
|
|
5
75
|
export interface CopyableProps extends Omit<React.ComponentProps<'button'>, 'value'> {
|
|
6
76
|
/** O texto que vai pro clipboard no clique. */
|
|
7
77
|
value: string
|
|
@@ -13,7 +83,16 @@ export interface CopyableProps extends Omit<React.ComponentProps<'button'>, 'val
|
|
|
13
83
|
* Clicar-pra-copiar com feedback: copia `value` pro clipboard e troca o ícone por um
|
|
14
84
|
* check por ~1.5s. Sem filhos é um botão-ícone (bom pra toolbar/célula); com filhos, o
|
|
15
85
|
* rótulo visível + o ícone à direita. Pra IDs, tokens, slugs, URLs. No-op silencioso se
|
|
16
|
-
* o clipboard não estiver disponível (contexto inseguro/SSR)
|
|
86
|
+
* o clipboard não estiver disponível (contexto inseguro/SSR) ou recusar a escrita (permissão
|
|
87
|
+
* negada, documento sem foco): o botão só confirma "copiado" depois que a escrita conclui.
|
|
88
|
+
*
|
|
89
|
+
* Nome acessível: com filhos, é o próprio texto visível (quem fala o que vê aciona o botão);
|
|
90
|
+
* sem filhos, "Copiar" e, na confirmação, "Copiado". Um `aria-label` do consumidor vence os
|
|
91
|
+
* dois. A confirmação é anunciada por uma região viva no fim de `document.body`, compartilhada
|
|
92
|
+
* por todas as instâncias do documento: dentro do botão, o texto entraria no nome calculado pelo
|
|
93
|
+
* conteúdo (e o botão não expõe descendentes como regiões); como irmã, mudaria a estrutura do
|
|
94
|
+
* contêiner e seletores como `:first-child` de um ButtonGroup. A região só é criada no cliente,
|
|
95
|
+
* depois da hidratação, e existe antes da primeira cópia para que a mudança seja anunciada.
|
|
17
96
|
*/
|
|
18
97
|
export function Copyable({
|
|
19
98
|
value,
|
|
@@ -25,26 +104,57 @@ export function Copyable({
|
|
|
25
104
|
}: CopyableProps): React.ReactElement {
|
|
26
105
|
const [copied, setCopied] = React.useState(false)
|
|
27
106
|
const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
28
|
-
React.
|
|
107
|
+
const [id] = React.useState(() => Symbol('copyable'))
|
|
108
|
+
const status = React.useRef<SharedStatus | undefined>(undefined)
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
const doc = document
|
|
111
|
+
const shared = acquireStatus(doc)
|
|
112
|
+
status.current = shared
|
|
113
|
+
return () => {
|
|
114
|
+
clearTimeout(timer.current)
|
|
115
|
+
status.current = undefined
|
|
116
|
+
releaseStatus(doc, shared, id)
|
|
117
|
+
}
|
|
118
|
+
}, [id])
|
|
119
|
+
const iconOnly = children === undefined || children === null || children === false || children === ''
|
|
29
120
|
return (
|
|
30
121
|
<button
|
|
31
122
|
type="button"
|
|
32
123
|
data-slot="copyable"
|
|
33
124
|
data-copied={copied || undefined}
|
|
34
|
-
aria-label={copied ? 'Copiado' : 'Copiar'}
|
|
35
|
-
onClick={(e) => {
|
|
125
|
+
aria-label={iconOnly ? (copied ? 'Copiado' : 'Copiar') : undefined}
|
|
126
|
+
onClick={async (e) => {
|
|
36
127
|
onClick?.(e)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
128
|
+
const clipboard = globalThis.navigator?.clipboard
|
|
129
|
+
if (clipboard === undefined) return
|
|
130
|
+
clearTimeout(timer.current)
|
|
131
|
+
try {
|
|
132
|
+
await clipboard.writeText(value)
|
|
133
|
+
} catch {
|
|
134
|
+
// Recusado: some com um "copiado" anterior para não afirmar o que não aconteceu.
|
|
135
|
+
setCopied(false)
|
|
136
|
+
if (status.current !== undefined) clearStatus(status.current, id)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
const shared = status.current
|
|
140
|
+
// Desmontou enquanto a escrita corria: não há botão nem região a atualizar.
|
|
141
|
+
if (shared === undefined) return
|
|
142
|
+
setCopied(true)
|
|
143
|
+
announceCopied(shared, id)
|
|
144
|
+
timer.current = setTimeout(() => {
|
|
145
|
+
setCopied(false)
|
|
146
|
+
clearStatus(shared, id)
|
|
147
|
+
}, feedbackMs)
|
|
42
148
|
}}
|
|
43
149
|
className={cn('inline-flex items-center gap-1.5', className)}
|
|
44
150
|
{...props}
|
|
45
151
|
>
|
|
46
152
|
{children}
|
|
47
|
-
{copied ?
|
|
153
|
+
{copied ? (
|
|
154
|
+
<CheckIcon aria-hidden="true" className="size-3.5 text-context-success-emphasis" />
|
|
155
|
+
) : (
|
|
156
|
+
<CopyIcon aria-hidden="true" className="size-3.5" />
|
|
157
|
+
)}
|
|
48
158
|
</button>
|
|
49
159
|
)
|
|
50
160
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
useSurfaceLayout,
|
|
12
12
|
type SurfaceLayout,
|
|
13
13
|
} from "../patterns/surface-anatomy.ts";
|
|
14
|
+
import { FillBoundary } from "../patterns/fill-region.ts";
|
|
14
15
|
import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
|
|
15
16
|
import { SurfaceHeader } from "../patterns/surface-header.tsx";
|
|
16
17
|
import {
|
|
@@ -45,6 +46,24 @@ const DialogContentContext = React.createContext<DialogContentContextValue>({
|
|
|
45
46
|
closeSize: "sm",
|
|
46
47
|
});
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Compõe o handler de dispensa do consumidor com o bloqueio de `closeDisabled`: o handler dele
|
|
51
|
+
* roda primeiro e, com a superfície bloqueada, o evento é cancelado para que Esc, clique fora ou
|
|
52
|
+
* foco fora não a fechem. Sem bloqueio, devolve o handler original.
|
|
53
|
+
*/
|
|
54
|
+
export function preventDismissWhen<
|
|
55
|
+
E extends { defaultPrevented: boolean; preventDefault(): void },
|
|
56
|
+
>(
|
|
57
|
+
blocked: boolean,
|
|
58
|
+
handler: ((event: E) => void) | undefined,
|
|
59
|
+
): ((event: E) => void) | undefined {
|
|
60
|
+
if (!blocked) return handler;
|
|
61
|
+
return (event) => {
|
|
62
|
+
handler?.(event);
|
|
63
|
+
if (!event.defaultPrevented) event.preventDefault();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
function useDialogContext(): DialogContextValue {
|
|
49
68
|
return React.useContext(DialogContext);
|
|
50
69
|
}
|
|
@@ -192,11 +211,16 @@ export function DialogContent({
|
|
|
192
211
|
closeSize = "sm",
|
|
193
212
|
layout = "sectioned",
|
|
194
213
|
"aria-describedby": ariaDescribedBy,
|
|
214
|
+
onEscapeKeyDown,
|
|
215
|
+
onInteractOutside,
|
|
195
216
|
...props
|
|
196
217
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
197
218
|
/** Mostra a ação “Fechar” no modo padrão. O modo `alert` exige uma resposta identificável. */
|
|
198
219
|
showCloseButton?: boolean;
|
|
199
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* Bloqueia o fechamento enquanto a superfície está ocupada: desabilita a ação “Fechar”, que
|
|
222
|
+
* continua visível, e cancela Esc e clique ou foco fora depois dos handlers do consumidor.
|
|
223
|
+
*/
|
|
200
224
|
closeDisabled?: boolean;
|
|
201
225
|
/** Tamanho da ação circular de fechar no cabeçalho. */
|
|
202
226
|
closeSize?: CloseButtonSize;
|
|
@@ -217,17 +241,22 @@ export function DialogContent({
|
|
|
217
241
|
data-mode="alert"
|
|
218
242
|
className={cn(contentClasses, "grid max-w-xs gap-4 p-6", className)}
|
|
219
243
|
aria-describedby={ariaDescribedBy}
|
|
244
|
+
onEscapeKeyDown={preventDismissWhen(closeDisabled, onEscapeKeyDown)}
|
|
220
245
|
{...props}
|
|
221
246
|
>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
247
|
+
{/* Sem anatomia seccionada (exceção da ADR 0023), mas a altura continua sendo
|
|
248
|
+
decisão do diálogo: a cadeia `fill` da página não atravessa o portal. */}
|
|
249
|
+
<FillBoundary>
|
|
250
|
+
<DialogContentContext.Provider
|
|
251
|
+
value={{
|
|
252
|
+
showCloseButton: false,
|
|
253
|
+
closeDisabled: true,
|
|
254
|
+
closeSize,
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
{children}
|
|
258
|
+
</DialogContentContext.Provider>
|
|
259
|
+
</FillBoundary>
|
|
231
260
|
</AlertDialogPrimitive.Content>
|
|
232
261
|
</DialogPortal>
|
|
233
262
|
);
|
|
@@ -246,6 +275,8 @@ export function DialogContent({
|
|
|
246
275
|
className,
|
|
247
276
|
)}
|
|
248
277
|
aria-describedby={ariaDescribedBy}
|
|
278
|
+
onEscapeKeyDown={preventDismissWhen(closeDisabled, onEscapeKeyDown)}
|
|
279
|
+
onInteractOutside={preventDismissWhen(closeDisabled, onInteractOutside)}
|
|
249
280
|
{...props}
|
|
250
281
|
>
|
|
251
282
|
<SurfaceLayoutProvider value={layout}>
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
CloseButton,
|
|
22
22
|
type CloseButtonSize,
|
|
23
23
|
} from "./close-button.tsx";
|
|
24
|
-
import { overlayClasses } from "./dialog.tsx";
|
|
24
|
+
import { overlayClasses, preventDismissWhen } from "./dialog.tsx";
|
|
25
25
|
|
|
26
26
|
interface DrawerContentContextValue {
|
|
27
27
|
showCloseButton: boolean;
|
|
@@ -95,10 +95,16 @@ function DrawerContent({
|
|
|
95
95
|
closeSize = "sm",
|
|
96
96
|
layout = "sectioned",
|
|
97
97
|
"aria-describedby": ariaDescribedBy,
|
|
98
|
+
onEscapeKeyDown,
|
|
99
|
+
onInteractOutside,
|
|
98
100
|
...props
|
|
99
101
|
}: React.ComponentProps<typeof DrawerPrimitive.Content> & {
|
|
100
102
|
side?: "top" | "right" | "bottom" | "left";
|
|
101
103
|
showCloseButton?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Bloqueia o fechamento enquanto a superfície está ocupada: desabilita a ação “Fechar”, que
|
|
106
|
+
* continua visível, e cancela Esc e clique ou foco fora depois dos handlers do consumidor.
|
|
107
|
+
*/
|
|
102
108
|
closeDisabled?: boolean;
|
|
103
109
|
closeSize?: CloseButtonSize;
|
|
104
110
|
/** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
|
|
@@ -123,6 +129,8 @@ function DrawerContent({
|
|
|
123
129
|
className,
|
|
124
130
|
)}
|
|
125
131
|
aria-describedby={ariaDescribedBy}
|
|
132
|
+
onEscapeKeyDown={preventDismissWhen(closeDisabled, onEscapeKeyDown)}
|
|
133
|
+
onInteractOutside={preventDismissWhen(closeDisabled, onInteractOutside)}
|
|
126
134
|
{...props}
|
|
127
135
|
>
|
|
128
136
|
<SurfaceLayoutProvider value={layout}>
|
|
@@ -69,7 +69,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
69
69
|
)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function EmptyDescription({ className, ...props }: React.ComponentProps<"
|
|
72
|
+
function EmptyDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
73
73
|
return (
|
|
74
74
|
<div
|
|
75
75
|
data-slot="empty-description"
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
type LucideIcon,
|
|
47
47
|
} from 'lucide-react'
|
|
48
48
|
import { cn } from '../../lib/cn.ts'
|
|
49
|
-
import { focusRing } from './control.ts'
|
|
49
|
+
import { controlHeight, focusRing } from './control.ts'
|
|
50
50
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './command.tsx'
|
|
51
51
|
import { Popover, PopoverContent, PopoverTrigger } from './popover.tsx'
|
|
52
52
|
|
|
@@ -142,8 +142,12 @@ export function IconPicker({
|
|
|
142
142
|
aria-describedby={ariaDescribedBy}
|
|
143
143
|
data-slot="icon-picker"
|
|
144
144
|
className={cn(
|
|
145
|
-
|
|
145
|
+
// 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',
|
|
147
|
+
controlHeight.default,
|
|
146
148
|
focusRing,
|
|
149
|
+
// 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',
|
|
147
151
|
className,
|
|
148
152
|
)}
|
|
149
153
|
>
|
|
@@ -41,6 +41,13 @@ export interface IconTileProps
|
|
|
41
41
|
extends React.ComponentProps<'span'>,
|
|
42
42
|
IconTileStyleOptions {}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Preenche os padrões de `context`, `variant` e `size` (`neutral`, `subtle`, `default`).
|
|
46
|
+
*
|
|
47
|
+
* @deprecated Não há decisão a tomar com os valores resolvidos fora do próprio IconTile. Para a
|
|
48
|
+
* moldura, renderize `IconTile`; para aplicá-la a outro elemento, use `iconTileVariants`, que já
|
|
49
|
+
* aplica os mesmos padrões. Sai do pacote numa próxima versão major.
|
|
50
|
+
*/
|
|
44
51
|
export function resolveIconTileStyle({
|
|
45
52
|
context,
|
|
46
53
|
variant,
|