@godxjp/ui 18.1.1 → 18.3.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/README.md +1 -1
- package/dist/components/data-display/credential-reveal.d.ts +2 -0
- package/dist/components/data-display/credential-reveal.js +5 -0
- package/dist/components/data-display/index.d.ts +5 -0
- package/dist/components/data-display/index.js +4 -0
- package/dist/components/data-display/qr-code.d.ts +8 -0
- package/dist/components/data-display/qr-code.js +34 -0
- package/dist/components/feedback/dialog.d.ts +9 -2
- package/dist/components/feedback/dialog.js +43 -11
- package/dist/components/general/button.js +1 -1
- package/dist/components/navigation/filter-bar.d.ts +1 -1
- package/dist/components/navigation/filter-bar.js +8 -2
- package/dist/components/ui/credential-reveal.d.ts +12 -0
- package/dist/components/ui/credential-reveal.js +160 -0
- package/dist/i18n/messages/en.json +14 -1
- package/dist/i18n/messages/ja.json +14 -1
- package/dist/i18n/messages/vi.json +14 -1
- package/dist/lib/permission-grid.d.ts +59 -0
- package/dist/lib/permission-grid.js +32 -0
- package/dist/props/components/data-display.prop.d.ts +60 -1
- package/dist/props/components/feedback.prop.d.ts +4 -0
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/navigation.prop.d.ts +8 -1
- package/dist/props/registry.d.ts +24 -1
- package/dist/props/registry.js +40 -1
- package/dist/props/vocabulary/data.prop.d.ts +6 -0
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/styles/control.css +9 -1
- package/dist/styles/data-display-layout.css +69 -0
- package/dist/styles/dialog-layout.css +4 -0
- package/dist/styles/layout.css +10 -0
- package/dist/tokens/components/data-display.css +9 -0
- package/dist/tokens/components/navigation.css +7 -0
- package/dist/tokens/foundation.css +8 -4
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Components emit `data-slot` / `data-*`; the look lives in `styles/*-layout.css`.
|
|
|
70
70
|
| **Layout** | `@godxjp/ui/layout` | `Flex`, `PageContainer`, `ResponsiveGrid`, `AppShell`, `Sidebar`, `Separator`, `AspectRatio`, `Resizable` |
|
|
71
71
|
| **General** | `@godxjp/ui/general` | `Button` |
|
|
72
72
|
| **Data Entry** | `@godxjp/ui/data-entry` | `Input`, `Select`, `FormField`, `Field`, `DatePicker`, `TimePicker`, `Combobox`, `Switch`, `Toggle`, `Upload`, `Cascader`, `TreeSelect`, `ColorPicker`, `Slider`, `PasswordInput`, `PasswordStrength`, `InputOTP`, `Rating`, `TagInput` |
|
|
73
|
-
| **Data Display** | `@godxjp/ui/data-display` | `Table`, `DataTable`, `Card`, `StatCard`, `Badge`, `Avatar`, `Descriptions`, `Timeline`, `EmptyState`, `Progress`, `Accordion`, `HoverCard`, `Carousel`, `Popover`, `Collapsible`
|
|
73
|
+
| **Data Display** | `@godxjp/ui/data-display` | `Table`, `DataTable`, `Card`, `StatCard`, `Badge`, `Avatar`, `Descriptions`, `Timeline`, `EmptyState`, `Progress`, `QrCode`, `CredentialReveal`, `Accordion`, `HoverCard`, `Carousel`, `Popover`, `Collapsible` |
|
|
74
74
|
| **Feedback** | `@godxjp/ui/feedback` | `Dialog`, `AlertDialog`, `Sheet` (side), `Drawer` (bottom-sheet), `Toast`, `Skeleton`, `Alert`, `Tooltip` |
|
|
75
75
|
| **Query** | `@godxjp/ui/query` | `DataState`, `InfiniteQueryState`, `PrefetchLink` (adapter subpath — pulls TanStack Query) |
|
|
76
76
|
| **Navigation** | `@godxjp/ui/navigation` | `Tabs`, `Toolbar`, `DropdownMenu`, `ContextMenu`, `Menubar`, `NavigationMenu`, `Steps`, `Pagination`, `Breadcrumb`, `AppSettingPicker` |
|
|
@@ -2,6 +2,11 @@ export { Badge } from "./badge.js";
|
|
|
2
2
|
export type { BadgeProps } from "./badge.js";
|
|
3
3
|
export { ListRow } from "./list-row.js";
|
|
4
4
|
export type { ListRowProps } from "./list-row.js";
|
|
5
|
+
export { CredentialReveal } from "./credential-reveal.js";
|
|
6
|
+
export type { CredentialRevealProp, CredentialRevealProps, CredentialRevealTone, } from "./credential-reveal.js";
|
|
7
|
+
export { QrCode } from "./qr-code.js";
|
|
8
|
+
export type { QrCodeProps } from "./qr-code.js";
|
|
9
|
+
export type { QrCodeProp } from "../../props/components/data-display.prop.js";
|
|
5
10
|
export { Avatar, AvatarImage, AvatarFallback } from "./avatar.js";
|
|
6
11
|
export { Card, CardBar, CardContent, CardCover, CardDescription, CardFooter, CardHeader, CardTitle, CardAction, StatCard, } from "./card.js";
|
|
7
12
|
export type { StatCardProps, CardBarProps } from "./card.js";
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Badge } from "./badge.js";
|
|
2
2
|
import { ListRow } from "./list-row.js";
|
|
3
|
+
import { CredentialReveal } from "./credential-reveal.js";
|
|
4
|
+
import { QrCode } from "./qr-code.js";
|
|
3
5
|
import { Avatar, AvatarImage, AvatarFallback } from "./avatar.js";
|
|
4
6
|
import {
|
|
5
7
|
Card,
|
|
@@ -69,6 +71,7 @@ export {
|
|
|
69
71
|
Collapsible,
|
|
70
72
|
CollapsibleContent,
|
|
71
73
|
CollapsibleTrigger,
|
|
74
|
+
CredentialReveal,
|
|
72
75
|
DataTable,
|
|
73
76
|
Descriptions,
|
|
74
77
|
EmptyState,
|
|
@@ -84,6 +87,7 @@ export {
|
|
|
84
87
|
PopoverTitle,
|
|
85
88
|
PopoverTrigger,
|
|
86
89
|
Progress,
|
|
90
|
+
QrCode,
|
|
87
91
|
ScrollArea,
|
|
88
92
|
ScrollBar,
|
|
89
93
|
StatCard,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { QrCodeProp } from "../../props/components/data-display.prop.js";
|
|
3
|
+
export type QrCodeProps = QrCodeProp & Omit<React.ComponentPropsWithoutRef<"svg">, keyof QrCodeProp | "children" | "role" | "style" | "type">;
|
|
4
|
+
/**
|
|
5
|
+
* Renders a QR code entirely in-process. The encoded value is never sent to a network service,
|
|
6
|
+
* written to an element attribute, or included in accessible text.
|
|
7
|
+
*/
|
|
8
|
+
export declare const QrCode: React.ForwardRefExoticComponent<QrCodeProp & Omit<Omit<React.SVGProps<SVGSVGElement>, "ref">, "style" | "children" | "role" | "type" | keyof QrCodeProp> & React.RefAttributes<SVGSVGElement>>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { QRCodeSVG } from "qrcode.react";
|
|
4
|
+
import { cn } from "../../lib/utils.js";
|
|
5
|
+
const QR_INTRINSIC_SIZE = 256;
|
|
6
|
+
const QrCode = React.forwardRef(
|
|
7
|
+
({ value, label, size = "md", className, ...props }, ref) => {
|
|
8
|
+
return /* @__PURE__ */ jsx(
|
|
9
|
+
QRCodeSVG,
|
|
10
|
+
{
|
|
11
|
+
ref,
|
|
12
|
+
...props,
|
|
13
|
+
value,
|
|
14
|
+
title: label,
|
|
15
|
+
size: QR_INTRINSIC_SIZE,
|
|
16
|
+
level: "M",
|
|
17
|
+
boostLevel: true,
|
|
18
|
+
marginSize: 4,
|
|
19
|
+
bgColor: "hsl(var(--qr-code-background))",
|
|
20
|
+
fgColor: "hsl(var(--qr-code-foreground))",
|
|
21
|
+
role: "img",
|
|
22
|
+
"aria-label": label,
|
|
23
|
+
focusable: "false",
|
|
24
|
+
"data-slot": "qr-code",
|
|
25
|
+
"data-size": size,
|
|
26
|
+
className: cn("ui-qr-code", className)
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
QrCode.displayName = "QrCode";
|
|
32
|
+
export {
|
|
33
|
+
QrCode
|
|
34
|
+
};
|
|
@@ -57,8 +57,15 @@ declare const DialogAction: React.ForwardRefExoticComponent<Omit<AlertDialogPrim
|
|
|
57
57
|
declare const DialogCancel: React.ForwardRefExoticComponent<Omit<AlertDialogPrimitive.AlertDialogCancelProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
|
|
58
58
|
declare const AlertDialogAction: React.ForwardRefExoticComponent<Omit<AlertDialogPrimitive.AlertDialogActionProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
|
|
59
59
|
declare const AlertDialogCancel: React.ForwardRefExoticComponent<Omit<AlertDialogPrimitive.AlertDialogCancelProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
|
|
60
|
-
/**
|
|
61
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Preset: confirm / destructive / typed-challenge / step-up without compound markup.
|
|
62
|
+
*
|
|
63
|
+
* High-stakes deletion recipe ("DangerConfirm", godxui#193): pass `challenge` (or `confirmPhrase`)
|
|
64
|
+
* to gate the confirm button behind an exact typed match (org slug / resource name), and `stepUp`
|
|
65
|
+
* for an async re-auth gate (passkey / 2FA) that must resolve truthy before `onConfirm` fires.
|
|
66
|
+
* Both flows force the destructive tone (button + soft header band).
|
|
67
|
+
*/
|
|
68
|
+
declare function AlertDialog({ open, onOpenChange, title, description, confirmLabel, cancelLabel, variant, confirmPhrase, challenge, onConfirm, stepUp, keepOpenOnConfirm, pending, }: AlertDialogProp): React.JSX.Element;
|
|
62
69
|
export declare const Dialog: typeof DialogRoot & {
|
|
63
70
|
Trigger: typeof DialogTrigger;
|
|
64
71
|
Portal: typeof DialogPortal;
|
|
@@ -241,25 +241,54 @@ function AlertDialog({
|
|
|
241
241
|
cancelLabel,
|
|
242
242
|
variant = "default",
|
|
243
243
|
confirmPhrase,
|
|
244
|
+
challenge,
|
|
244
245
|
onConfirm,
|
|
246
|
+
stepUp,
|
|
245
247
|
keepOpenOnConfirm = false,
|
|
246
248
|
pending = false
|
|
247
249
|
}) {
|
|
248
250
|
const { t } = useTranslation();
|
|
249
251
|
const [typed, setTyped] = React.useState("");
|
|
252
|
+
const [verifying, setVerifying] = React.useState(false);
|
|
253
|
+
const [stepUpFailed, setStepUpFailed] = React.useState(false);
|
|
250
254
|
const inputId = React.useId();
|
|
251
|
-
const
|
|
252
|
-
const
|
|
255
|
+
const stepErrorId = React.useId();
|
|
256
|
+
const phrase = confirmPhrase ?? challenge;
|
|
257
|
+
const needsPhrase = phrase != null && phrase.length > 0;
|
|
258
|
+
const phraseMatches = !needsPhrase || typed === phrase;
|
|
253
259
|
const effectiveVariant = needsPhrase ? "destructive" : variant;
|
|
260
|
+
const headerTone = effectiveVariant === "destructive" ? "destructive" : "default";
|
|
254
261
|
const resolvedConfirm = confirmLabel ?? (needsPhrase ? t("common.delete") : t("common.continue"));
|
|
255
262
|
const resolvedCancel = cancelLabel ?? t("common.cancel");
|
|
256
|
-
const
|
|
263
|
+
const busy = pending || verifying;
|
|
264
|
+
const confirmLabelText = verifying ? t("feedback.alert.verifying") : pending ? t("common.working") : resolvedConfirm;
|
|
265
|
+
const reset = () => {
|
|
257
266
|
setTyped("");
|
|
267
|
+
setVerifying(false);
|
|
268
|
+
setStepUpFailed(false);
|
|
269
|
+
};
|
|
270
|
+
const handleOpenChange = (next) => {
|
|
271
|
+
reset();
|
|
258
272
|
onOpenChange(next);
|
|
259
273
|
};
|
|
260
274
|
const handleConfirm = () => {
|
|
261
|
-
if (!phraseMatches) return;
|
|
275
|
+
if (!phraseMatches || busy) return;
|
|
262
276
|
void (async () => {
|
|
277
|
+
if (stepUp) {
|
|
278
|
+
setStepUpFailed(false);
|
|
279
|
+
setVerifying(true);
|
|
280
|
+
let ok = false;
|
|
281
|
+
try {
|
|
282
|
+
ok = await stepUp();
|
|
283
|
+
} catch {
|
|
284
|
+
ok = false;
|
|
285
|
+
}
|
|
286
|
+
setVerifying(false);
|
|
287
|
+
if (!ok) {
|
|
288
|
+
setStepUpFailed(true);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
263
292
|
await onConfirm();
|
|
264
293
|
if (!keepOpenOnConfirm) onOpenChange(false);
|
|
265
294
|
})();
|
|
@@ -278,12 +307,12 @@ function AlertDialog({
|
|
|
278
307
|
"data-slot": "dialog-content",
|
|
279
308
|
className: "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 duration-200 outline-none",
|
|
280
309
|
children: [
|
|
281
|
-
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
310
|
+
/* @__PURE__ */ jsxs(DialogHeader, { tone: headerTone, children: [
|
|
282
311
|
/* @__PURE__ */ jsx(AlertDialogPrimitive.Title, { "data-slot": "dialog-title", children: title }),
|
|
283
312
|
description ? /* @__PURE__ */ jsx(AlertDialogPrimitive.Description, { "data-slot": "dialog-description", children: description }) : null
|
|
284
313
|
] }),
|
|
285
314
|
needsPhrase && /* @__PURE__ */ jsxs("div", { className: "ui-stack-xs", children: [
|
|
286
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: inputId, className: "text-sm", children: t("common.typeToConfirm", { phrase
|
|
315
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: inputId, className: "text-sm", children: t("common.typeToConfirm", { phrase }) }),
|
|
287
316
|
/* @__PURE__ */ jsx(
|
|
288
317
|
Input,
|
|
289
318
|
{
|
|
@@ -294,20 +323,23 @@ function AlertDialog({
|
|
|
294
323
|
},
|
|
295
324
|
autoComplete: "off",
|
|
296
325
|
spellCheck: false,
|
|
297
|
-
placeholder:
|
|
298
|
-
"aria-required": "true"
|
|
326
|
+
placeholder: phrase,
|
|
327
|
+
"aria-required": "true",
|
|
328
|
+
disabled: busy
|
|
299
329
|
}
|
|
300
330
|
)
|
|
301
331
|
] }),
|
|
332
|
+
stepUpFailed && /* @__PURE__ */ jsx("p", { id: stepErrorId, role: "alert", className: "text-destructive text-sm", children: t("feedback.alert.stepUpFailed") }),
|
|
302
333
|
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
303
|
-
/* @__PURE__ */ jsx(DialogCancel, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", disabled:
|
|
334
|
+
/* @__PURE__ */ jsx(DialogCancel, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", disabled: busy, children: resolvedCancel }) }),
|
|
304
335
|
/* @__PURE__ */ jsx(
|
|
305
336
|
Button,
|
|
306
337
|
{
|
|
307
338
|
variant: effectiveVariant === "destructive" ? "destructive" : "default",
|
|
308
339
|
onClick: handleConfirm,
|
|
309
|
-
disabled:
|
|
310
|
-
|
|
340
|
+
disabled: busy || !phraseMatches,
|
|
341
|
+
"aria-describedby": stepUpFailed ? stepErrorId : void 0,
|
|
342
|
+
children: confirmLabelText
|
|
311
343
|
}
|
|
312
344
|
)
|
|
313
345
|
] })
|
|
@@ -19,7 +19,7 @@ const buttonVariants = cva("ui-button", {
|
|
|
19
19
|
variants: {
|
|
20
20
|
variant: {
|
|
21
21
|
default: "ui-button--default bg-primary text-primary-foreground hover:bg-primary/90",
|
|
22
|
-
destructive: "ui-button--destructive bg-destructive text-destructive-foreground
|
|
22
|
+
destructive: "ui-button--destructive bg-destructive text-destructive-foreground focus-visible:ring-destructive/20",
|
|
23
23
|
outline: "ui-button--outline border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
|
|
24
24
|
dashed: "ui-button--dashed border border-dashed bg-background hover:bg-accent hover:text-accent-foreground",
|
|
25
25
|
secondary: "ui-button--secondary bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ToolbarGroupProp, ToolbarProp } from "../../props/components/navigation.prop.js";
|
|
2
2
|
export type { ToolbarGroupProp, ToolbarGroupProp as ToolbarGroupProps, ToolbarProp, ToolbarProp as ToolbarProps, } from "../../props/components/navigation.prop.js";
|
|
3
|
-
export declare function Toolbar({ onClear, hasActiveFilters, className, children }: ToolbarProp): import("react").JSX.Element;
|
|
3
|
+
export declare function Toolbar({ onClear, hasActiveFilters, sticky, className, children, }: ToolbarProp): import("react").JSX.Element;
|
|
4
4
|
export declare function ToolbarGroup({ label, className, children }: ToolbarGroupProp): import("react").JSX.Element;
|
|
@@ -5,14 +5,20 @@ import { X } from "lucide-react";
|
|
|
5
5
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
6
|
import { Button } from "../general/button.js";
|
|
7
7
|
import { cn } from "../../lib/utils.js";
|
|
8
|
-
function Toolbar({
|
|
8
|
+
function Toolbar({
|
|
9
|
+
onClear,
|
|
10
|
+
hasActiveFilters = true,
|
|
11
|
+
sticky = false,
|
|
12
|
+
className,
|
|
13
|
+
children
|
|
14
|
+
}) {
|
|
9
15
|
const { t } = useTranslation();
|
|
10
16
|
return /* @__PURE__ */ jsxs(
|
|
11
17
|
"div",
|
|
12
18
|
{
|
|
13
19
|
role: "toolbar",
|
|
14
20
|
"aria-label": t("navigation.toolbar.ariaLabel"),
|
|
15
|
-
className: cn("ui-toolbar", className),
|
|
21
|
+
className: cn("ui-toolbar", sticky && "ui-toolbar-sticky", className),
|
|
16
22
|
children: [
|
|
17
23
|
children,
|
|
18
24
|
onClear && hasActiveFilters && /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: onClear, className: "ui-toolbar-clear", children: [
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { CredentialRevealProp } from "../../props/components/data-display.prop.js";
|
|
3
|
+
export type { CredentialRevealProp, CredentialRevealProp as CredentialRevealProps, CredentialRevealTone, } from "../../props/components/data-display.prop.js";
|
|
4
|
+
/**
|
|
5
|
+
* CredentialReveal — one-time secret surface (device credential / API key / service-account
|
|
6
|
+
* secret). Masked by default with a show/hide toggle, a copy button that confirms the copy, an
|
|
7
|
+
* optional download, and an optional acknowledge action to pair with a Dialog. Composed only from
|
|
8
|
+
* real @godxjp/ui primitives (Alert · Button · Text). Reveal is a controlled boolean triad
|
|
9
|
+
* (`revealed` / `defaultRevealed` / `onRevealedChange`); everything user-facing is routed through
|
|
10
|
+
* `t()`, and the copy confirmation is announced via an `aria-live` region.
|
|
11
|
+
*/
|
|
12
|
+
export declare const CredentialReveal: React.ForwardRefExoticComponent<CredentialRevealProp & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Check, Copy, Download, Eye, EyeOff } from "lucide-react";
|
|
5
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
|
+
import { cn } from "../../lib/utils.js";
|
|
7
|
+
import { Text } from "../general/typography.js";
|
|
8
|
+
import { Button } from "../general/button.js";
|
|
9
|
+
import { Alert, AlertDescription } from "../feedback/alert.js";
|
|
10
|
+
const MASK = "\u2022".repeat(24);
|
|
11
|
+
const COPIED_RESET_MS = 2e3;
|
|
12
|
+
const CredentialReveal = React.forwardRef(
|
|
13
|
+
({
|
|
14
|
+
secret,
|
|
15
|
+
label,
|
|
16
|
+
warning,
|
|
17
|
+
revealed,
|
|
18
|
+
defaultRevealed = false,
|
|
19
|
+
onRevealedChange,
|
|
20
|
+
onCopy,
|
|
21
|
+
onAcknowledge,
|
|
22
|
+
acknowledgeLabel,
|
|
23
|
+
downloadable = false,
|
|
24
|
+
downloadFileName = "credential.txt",
|
|
25
|
+
size = "md",
|
|
26
|
+
tone = "warning",
|
|
27
|
+
className,
|
|
28
|
+
id,
|
|
29
|
+
"aria-label": ariaLabel
|
|
30
|
+
}, ref) => {
|
|
31
|
+
const { t } = useTranslation();
|
|
32
|
+
const [internalRevealed, setInternalRevealed] = React.useState(defaultRevealed);
|
|
33
|
+
const isRevealed = revealed ?? internalRevealed;
|
|
34
|
+
const [copied, setCopied] = React.useState(false);
|
|
35
|
+
const copyTimer = React.useRef(null);
|
|
36
|
+
const labelId = React.useId();
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
if (revealed === void 0) setInternalRevealed(defaultRevealed);
|
|
39
|
+
setCopied(false);
|
|
40
|
+
}, [secret]);
|
|
41
|
+
React.useEffect(
|
|
42
|
+
() => () => {
|
|
43
|
+
if (copyTimer.current) clearTimeout(copyTimer.current);
|
|
44
|
+
},
|
|
45
|
+
[]
|
|
46
|
+
);
|
|
47
|
+
const setRevealed = (next) => {
|
|
48
|
+
if (revealed === void 0) setInternalRevealed(next);
|
|
49
|
+
onRevealedChange?.(next);
|
|
50
|
+
};
|
|
51
|
+
const copy = async () => {
|
|
52
|
+
try {
|
|
53
|
+
await navigator.clipboard.writeText(secret);
|
|
54
|
+
setCopied(true);
|
|
55
|
+
onCopy?.(secret);
|
|
56
|
+
if (copyTimer.current) clearTimeout(copyTimer.current);
|
|
57
|
+
copyTimer.current = setTimeout(() => setCopied(false), COPIED_RESET_MS);
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const download = () => {
|
|
62
|
+
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return;
|
|
63
|
+
const url = URL.createObjectURL(new Blob([secret], { type: "text/plain" }));
|
|
64
|
+
const anchor = document.createElement("a");
|
|
65
|
+
anchor.href = url;
|
|
66
|
+
anchor.download = downloadFileName;
|
|
67
|
+
document.body.appendChild(anchor);
|
|
68
|
+
anchor.click();
|
|
69
|
+
anchor.remove();
|
|
70
|
+
URL.revokeObjectURL(url);
|
|
71
|
+
};
|
|
72
|
+
const accessibleName = ariaLabel ?? (typeof label === "string" ? label : void 0) ?? t("ui.credentialReveal.secretLabel");
|
|
73
|
+
return /* @__PURE__ */ jsxs(
|
|
74
|
+
"div",
|
|
75
|
+
{
|
|
76
|
+
ref,
|
|
77
|
+
id,
|
|
78
|
+
"data-slot": "credential-reveal",
|
|
79
|
+
"data-size": size,
|
|
80
|
+
role: "group",
|
|
81
|
+
"aria-label": accessibleName,
|
|
82
|
+
className: cn("ui-credential-reveal", className),
|
|
83
|
+
children: [
|
|
84
|
+
warning !== null ? /* @__PURE__ */ jsx(Alert, { tone, className: "ui-credential-reveal-warning", children: /* @__PURE__ */ jsx(AlertDescription, { children: warning ?? t("ui.credentialReveal.warning") }) }) : null,
|
|
85
|
+
/* @__PURE__ */ jsxs("div", { className: "ui-credential-reveal-surface", children: [
|
|
86
|
+
label ? /* @__PURE__ */ jsx(
|
|
87
|
+
Text,
|
|
88
|
+
{
|
|
89
|
+
id: labelId,
|
|
90
|
+
size: "xs",
|
|
91
|
+
tone: "muted",
|
|
92
|
+
weight: "medium",
|
|
93
|
+
className: "ui-credential-reveal-label",
|
|
94
|
+
children: label
|
|
95
|
+
}
|
|
96
|
+
) : null,
|
|
97
|
+
/* @__PURE__ */ jsxs("div", { className: "ui-credential-reveal-row", children: [
|
|
98
|
+
/* @__PURE__ */ jsx(
|
|
99
|
+
Text,
|
|
100
|
+
{
|
|
101
|
+
mono: true,
|
|
102
|
+
size: "sm",
|
|
103
|
+
"data-slot": "credential-reveal-secret",
|
|
104
|
+
"data-state": isRevealed ? "revealed" : "masked",
|
|
105
|
+
"aria-labelledby": label ? labelId : void 0,
|
|
106
|
+
"aria-label": label ? void 0 : accessibleName,
|
|
107
|
+
className: "ui-credential-reveal-secret",
|
|
108
|
+
children: isRevealed ? secret : MASK
|
|
109
|
+
}
|
|
110
|
+
),
|
|
111
|
+
/* @__PURE__ */ jsxs("div", { className: "ui-credential-reveal-actions", children: [
|
|
112
|
+
/* @__PURE__ */ jsx(
|
|
113
|
+
Button,
|
|
114
|
+
{
|
|
115
|
+
type: "button",
|
|
116
|
+
variant: "ghost",
|
|
117
|
+
size,
|
|
118
|
+
onClick: () => setRevealed(!isRevealed),
|
|
119
|
+
"aria-pressed": isRevealed,
|
|
120
|
+
"aria-label": isRevealed ? t("ui.credentialReveal.hide") : t("ui.credentialReveal.show"),
|
|
121
|
+
children: isRevealed ? /* @__PURE__ */ jsx(EyeOff, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Eye, { "aria-hidden": "true" })
|
|
122
|
+
}
|
|
123
|
+
),
|
|
124
|
+
/* @__PURE__ */ jsx(
|
|
125
|
+
Button,
|
|
126
|
+
{
|
|
127
|
+
type: "button",
|
|
128
|
+
variant: "outline",
|
|
129
|
+
size,
|
|
130
|
+
onClick: copy,
|
|
131
|
+
"data-state": copied ? "copied" : "idle",
|
|
132
|
+
"aria-label": copied ? t("ui.credentialReveal.copied") : t("ui.credentialReveal.copy"),
|
|
133
|
+
children: copied ? /* @__PURE__ */ jsx(Check, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Copy, { "aria-hidden": "true" })
|
|
134
|
+
}
|
|
135
|
+
),
|
|
136
|
+
downloadable ? /* @__PURE__ */ jsx(
|
|
137
|
+
Button,
|
|
138
|
+
{
|
|
139
|
+
type: "button",
|
|
140
|
+
variant: "ghost",
|
|
141
|
+
size,
|
|
142
|
+
onClick: download,
|
|
143
|
+
"aria-label": t("ui.credentialReveal.download"),
|
|
144
|
+
children: /* @__PURE__ */ jsx(Download, { "aria-hidden": "true" })
|
|
145
|
+
}
|
|
146
|
+
) : null
|
|
147
|
+
] })
|
|
148
|
+
] })
|
|
149
|
+
] }),
|
|
150
|
+
onAcknowledge ? /* @__PURE__ */ jsx("div", { className: "ui-credential-reveal-footer", children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "default", size, onClick: onAcknowledge, children: acknowledgeLabel ?? t("ui.credentialReveal.acknowledge") }) }) : null,
|
|
151
|
+
/* @__PURE__ */ jsx("span", { "aria-live": "polite", className: "sr-only", "data-slot": "credential-reveal-status", children: copied ? t("ui.credentialReveal.copiedAnnounce") : "" })
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
CredentialReveal.displayName = "CredentialReveal";
|
|
158
|
+
export {
|
|
159
|
+
CredentialReveal
|
|
160
|
+
};
|
|
@@ -118,7 +118,9 @@
|
|
|
118
118
|
},
|
|
119
119
|
"genericError": "Something went wrong. Please retry.",
|
|
120
120
|
"alert": {
|
|
121
|
-
"dismiss": "Dismiss"
|
|
121
|
+
"dismiss": "Dismiss",
|
|
122
|
+
"verifying": "Verifying…",
|
|
123
|
+
"stepUpFailed": "Re-authentication failed. Please try again."
|
|
122
124
|
}
|
|
123
125
|
},
|
|
124
126
|
"layout": {
|
|
@@ -176,6 +178,17 @@
|
|
|
176
178
|
"show": "Show password",
|
|
177
179
|
"hide": "Hide password"
|
|
178
180
|
},
|
|
181
|
+
"credentialReveal": {
|
|
182
|
+
"secretLabel": "Secret",
|
|
183
|
+
"warning": "This secret is shown only once. Copy it now — you won't be able to see it again.",
|
|
184
|
+
"show": "Reveal secret",
|
|
185
|
+
"hide": "Hide secret",
|
|
186
|
+
"copy": "Copy secret",
|
|
187
|
+
"copied": "Copied",
|
|
188
|
+
"copiedAnnounce": "Secret copied to clipboard",
|
|
189
|
+
"download": "Download secret",
|
|
190
|
+
"acknowledge": "I've saved it"
|
|
191
|
+
},
|
|
179
192
|
"numberInput": {
|
|
180
193
|
"increment": "Increase",
|
|
181
194
|
"decrement": "Decrease"
|
|
@@ -118,7 +118,9 @@
|
|
|
118
118
|
},
|
|
119
119
|
"genericError": "問題が発生しました。もう一度お試しください。",
|
|
120
120
|
"alert": {
|
|
121
|
-
"dismiss": "閉じる"
|
|
121
|
+
"dismiss": "閉じる",
|
|
122
|
+
"verifying": "確認中…",
|
|
123
|
+
"stepUpFailed": "再認証に失敗しました。もう一度お試しください。"
|
|
122
124
|
}
|
|
123
125
|
},
|
|
124
126
|
"layout": {
|
|
@@ -176,6 +178,17 @@
|
|
|
176
178
|
"show": "パスワードを表示",
|
|
177
179
|
"hide": "パスワードを隠す"
|
|
178
180
|
},
|
|
181
|
+
"credentialReveal": {
|
|
182
|
+
"secretLabel": "シークレット",
|
|
183
|
+
"warning": "このシークレットは一度だけ表示されます。今すぐコピーしてください。再表示はできません。",
|
|
184
|
+
"show": "シークレットを表示",
|
|
185
|
+
"hide": "シークレットを隠す",
|
|
186
|
+
"copy": "シークレットをコピー",
|
|
187
|
+
"copied": "コピーしました",
|
|
188
|
+
"copiedAnnounce": "シークレットをクリップボードにコピーしました",
|
|
189
|
+
"download": "シークレットをダウンロード",
|
|
190
|
+
"acknowledge": "保存しました"
|
|
191
|
+
},
|
|
179
192
|
"numberInput": {
|
|
180
193
|
"increment": "増やす",
|
|
181
194
|
"decrement": "減らす"
|
|
@@ -118,7 +118,9 @@
|
|
|
118
118
|
},
|
|
119
119
|
"genericError": "Đã xảy ra lỗi. Vui lòng thử lại.",
|
|
120
120
|
"alert": {
|
|
121
|
-
"dismiss": "Đóng"
|
|
121
|
+
"dismiss": "Đóng",
|
|
122
|
+
"verifying": "Đang xác minh…",
|
|
123
|
+
"stepUpFailed": "Xác thực lại thất bại. Vui lòng thử lại."
|
|
122
124
|
}
|
|
123
125
|
},
|
|
124
126
|
"layout": {
|
|
@@ -176,6 +178,17 @@
|
|
|
176
178
|
"show": "Hiện mật khẩu",
|
|
177
179
|
"hide": "Ẩn mật khẩu"
|
|
178
180
|
},
|
|
181
|
+
"credentialReveal": {
|
|
182
|
+
"secretLabel": "Khóa bí mật",
|
|
183
|
+
"warning": "Khóa bí mật này chỉ hiển thị một lần. Hãy sao chép ngay — bạn sẽ không thể xem lại.",
|
|
184
|
+
"show": "Hiện khóa bí mật",
|
|
185
|
+
"hide": "Ẩn khóa bí mật",
|
|
186
|
+
"copy": "Sao chép khóa",
|
|
187
|
+
"copied": "Đã sao chép",
|
|
188
|
+
"copiedAnnounce": "Đã sao chép khóa bí mật vào bộ nhớ tạm",
|
|
189
|
+
"download": "Tải khóa bí mật",
|
|
190
|
+
"acknowledge": "Tôi đã lưu"
|
|
191
|
+
},
|
|
179
192
|
"numberInput": {
|
|
180
193
|
"increment": "Tăng",
|
|
181
194
|
"decrement": "Giảm"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* permission-grid — pure, render-neutral helpers for RBAC role × permission
|
|
3
|
+
* matrices.
|
|
4
|
+
*
|
|
5
|
+
* A permission matrix maps a set of PERMISSION rows against a set of ROLE
|
|
6
|
+
* columns; each (role, permission) pair is either granted or not. A "permission
|
|
7
|
+
* matrix" is NOT a framework component — it is a COMPOSITION of `Table`/
|
|
8
|
+
* `DataTable` + `Badge` + tokens (see `docs/COMPOSITION-VS-COMPONENT.md`, Gate 0:
|
|
9
|
+
* fails C2/C3/C7). What it DOES need is a single, tested source of truth for the
|
|
10
|
+
* non-trivial data logic every consumer would otherwise re-derive: grant lookup,
|
|
11
|
+
* the per-permission diff between two roles (compare mode), and the row set that
|
|
12
|
+
* survives a "differences only" (差分のみ) filter.
|
|
13
|
+
*
|
|
14
|
+
* These functions are framework-neutral (no React) and locale-neutral (no
|
|
15
|
+
* strings) so the showcase — and any RBAC admin — composes UI on top of them.
|
|
16
|
+
*
|
|
17
|
+
* Grants are modelled as a flat `Set` of composite keys for O(1) membership,
|
|
18
|
+
* which mirrors how an authz decision cache (`role:permission` tuples) is
|
|
19
|
+
* typically held in memory.
|
|
20
|
+
*/
|
|
21
|
+
/** Any object addressable by a stable string `id` (a role or a permission). */
|
|
22
|
+
export interface GridEntity {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
}
|
|
25
|
+
/** A pair of role ids being compared side by side in compare mode. */
|
|
26
|
+
export type ComparePair = readonly [roleA: string, roleB: string];
|
|
27
|
+
/** Options controlling which permission rows are visible. */
|
|
28
|
+
export interface VisibleRowsOptions {
|
|
29
|
+
/** The two roles under comparison; enables the diff-only filter. */
|
|
30
|
+
readonly compare?: ComparePair | null;
|
|
31
|
+
/**
|
|
32
|
+
* When true AND `compare` is set, keep only the permissions on which the two
|
|
33
|
+
* compared roles disagree. Without a `compare` pair this is a no-op (there is
|
|
34
|
+
* nothing to diff against), so every row is returned.
|
|
35
|
+
*/
|
|
36
|
+
readonly diffOnly?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the composite grant key for a (role, permission) pair. Use this both to
|
|
40
|
+
* seed the grant `Set` and to query it, so the encoding stays in one place.
|
|
41
|
+
*/
|
|
42
|
+
export declare function grantKey(roleId: string, permissionId: string): string;
|
|
43
|
+
/** Is `permissionId` granted to `roleId`? */
|
|
44
|
+
export declare function hasGrant(grants: ReadonlySet<string>, roleId: string, permissionId: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Do the two compared roles DISAGREE on `permissionId` (one grants it, the other
|
|
47
|
+
* does not)? Comparing a role with itself is always `false`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function rolesDifferOnPermission(grants: ReadonlySet<string>, compare: ComparePair, permissionId: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The permission rows to render: every row normally, or — when `diffOnly` is on
|
|
52
|
+
* and a `compare` pair is set — only the rows on which the two roles differ.
|
|
53
|
+
* Order is preserved; the input array is never mutated.
|
|
54
|
+
*/
|
|
55
|
+
export declare function visibleRows<P extends GridEntity>(permissions: readonly P[], grants: ReadonlySet<string>, { compare, diffOnly }?: VisibleRowsOptions): P[];
|
|
56
|
+
/** How many permissions the two compared roles disagree on (badge in the header). */
|
|
57
|
+
export declare function countDifferences<P extends GridEntity>(permissions: readonly P[], grants: ReadonlySet<string>, compare: ComparePair): number;
|
|
58
|
+
/** How many permissions a single role is granted (column subtotal). */
|
|
59
|
+
export declare function countGrants<P extends GridEntity>(permissions: readonly P[], grants: ReadonlySet<string>, roleId: string): number;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const KEY_SEP = "\0";
|
|
2
|
+
function grantKey(roleId, permissionId) {
|
|
3
|
+
return `${roleId}${KEY_SEP}${permissionId}`;
|
|
4
|
+
}
|
|
5
|
+
function hasGrant(grants, roleId, permissionId) {
|
|
6
|
+
return grants.has(grantKey(roleId, permissionId));
|
|
7
|
+
}
|
|
8
|
+
function rolesDifferOnPermission(grants, compare, permissionId) {
|
|
9
|
+
const [a, b] = compare;
|
|
10
|
+
return hasGrant(grants, a, permissionId) !== hasGrant(grants, b, permissionId);
|
|
11
|
+
}
|
|
12
|
+
function visibleRows(permissions, grants, { compare, diffOnly = false } = {}) {
|
|
13
|
+
if (!diffOnly || !compare) return permissions.slice();
|
|
14
|
+
return permissions.filter((p) => rolesDifferOnPermission(grants, compare, p.id));
|
|
15
|
+
}
|
|
16
|
+
function countDifferences(permissions, grants, compare) {
|
|
17
|
+
return permissions.reduce(
|
|
18
|
+
(n, p) => n + (rolesDifferOnPermission(grants, compare, p.id) ? 1 : 0),
|
|
19
|
+
0
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
function countGrants(permissions, grants, roleId) {
|
|
23
|
+
return permissions.reduce((n, p) => n + (hasGrant(grants, roleId, p.id) ? 1 : 0), 0);
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
countDifferences,
|
|
27
|
+
countGrants,
|
|
28
|
+
grantKey,
|
|
29
|
+
hasGrant,
|
|
30
|
+
rolesDifferOnPermission,
|
|
31
|
+
visibleRows
|
|
32
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Data Display component prop types — @see docs/COMPONENTS.md#data-display */
|
|
2
2
|
import type * as React from "react";
|
|
3
|
-
import type { ActionProp, ClassNameProp, DescriptionProp, IconProp, TitleProp, ColumnDefProp, GetRowIdProp, OnRowClickProp, OnSelectChangeProp, OnSortChangeProp, OnTableDensityChangeProp, SelectedIdsProp, SortStateProp, TableDensityProp, ChildrenProp, ToneProp, HeadingLevelProp } from "../vocabulary/index.js";
|
|
3
|
+
import type { ActionProp, ClassNameProp, DescriptionProp, IconProp, TitleProp, ColumnDefProp, GetRowIdProp, OnRowClickProp, OnSelectChangeProp, OnSortChangeProp, OnTableDensityChangeProp, SelectedIdsProp, SortStateProp, TableDensityProp, ChildrenProp, ToneProp, HeadingLevelProp, SizeProp, LabelProp, IdProp } from "../vocabulary/index.js";
|
|
4
4
|
/** @see EmptyState */
|
|
5
5
|
/**
|
|
6
6
|
* Semantic intent of the EmptyState icon medallion — a subset of the shared `ToneProp` vocabulary
|
|
@@ -62,6 +62,65 @@ export type BadgeProp = {
|
|
|
62
62
|
className?: ClassNameProp;
|
|
63
63
|
children?: ChildrenProp;
|
|
64
64
|
};
|
|
65
|
+
/** @see CredentialReveal */
|
|
66
|
+
/**
|
|
67
|
+
* Severity of the one-time-secret caution banner — a subset of the shared `ToneProp`
|
|
68
|
+
* vocabulary that carries a caution/danger/informational meaning (no `default`/`success`/
|
|
69
|
+
* `muted`/`neutral`, which would read as "safe"). Drives the composed `Alert` tone.
|
|
70
|
+
*/
|
|
71
|
+
export type CredentialRevealTone = Extract<ToneProp, "warning" | "destructive" | "info">;
|
|
72
|
+
/** @see CredentialReveal */
|
|
73
|
+
export type CredentialRevealProp = {
|
|
74
|
+
/** The one-time secret value shown masked by default and copied verbatim. */
|
|
75
|
+
secret: string;
|
|
76
|
+
/**
|
|
77
|
+
* Accessible name / caption for the secret (e.g. "API key", "デバイス資格情報"). When set it labels
|
|
78
|
+
* the secret region; when omitted a generic localized label is used.
|
|
79
|
+
*/
|
|
80
|
+
label?: LabelProp;
|
|
81
|
+
/**
|
|
82
|
+
* Caution banner copy. Defaults to the localized "shown only once" warning. Pass `null` to
|
|
83
|
+
* suppress the banner entirely (e.g. when the surrounding Dialog already carries the caution).
|
|
84
|
+
*/
|
|
85
|
+
warning?: React.ReactNode | null;
|
|
86
|
+
/** Controlled reveal state (masked ↔ shown). */
|
|
87
|
+
revealed?: boolean;
|
|
88
|
+
/** Uncontrolled initial reveal state. Default `false` (masked). */
|
|
89
|
+
defaultRevealed?: boolean;
|
|
90
|
+
/** Reveal-state change handler (fires on the show/hide toggle). */
|
|
91
|
+
onRevealedChange?: (revealed: boolean) => void;
|
|
92
|
+
/** Called after the secret is written to the clipboard. */
|
|
93
|
+
onCopy?: (secret: string) => void;
|
|
94
|
+
/**
|
|
95
|
+
* When provided, renders a confirm/acknowledge button that calls this — pair it with the
|
|
96
|
+
* surrounding Dialog's `onOpenChange(false)` so the secret re-blurs once dismissed.
|
|
97
|
+
*/
|
|
98
|
+
onAcknowledge?: () => void;
|
|
99
|
+
/** Label for the acknowledge button. Defaults to a localized "I've saved it". */
|
|
100
|
+
acknowledgeLabel?: React.ReactNode;
|
|
101
|
+
/** Offer a download-as-file button next to copy. Default `false`. */
|
|
102
|
+
downloadable?: boolean;
|
|
103
|
+
/** Filename for the downloaded secret. Default `"credential.txt"`. */
|
|
104
|
+
downloadFileName?: string;
|
|
105
|
+
/** Control size tier for the action buttons. Default `md`. */
|
|
106
|
+
size?: SizeProp;
|
|
107
|
+
/** Caution banner severity. Default `warning`. */
|
|
108
|
+
tone?: CredentialRevealTone;
|
|
109
|
+
className?: ClassNameProp;
|
|
110
|
+
id?: IdProp;
|
|
111
|
+
"aria-label"?: string;
|
|
112
|
+
};
|
|
113
|
+
/** @see QrCode */
|
|
114
|
+
export type QrCodeProp = {
|
|
115
|
+
/** Sensitive or public value encoded locally into the QR modules. */
|
|
116
|
+
value: string;
|
|
117
|
+
/** Purpose-specific accessible name. The encoded value is never used as accessible text. */
|
|
118
|
+
label: string;
|
|
119
|
+
/** Natural display size. Default `md`. */
|
|
120
|
+
size?: SizeProp;
|
|
121
|
+
className?: ClassNameProp;
|
|
122
|
+
id?: IdProp;
|
|
123
|
+
};
|
|
65
124
|
/** @see DataTable */
|
|
66
125
|
export type DataTableProp<T> = {
|
|
67
126
|
data: T[];
|
|
@@ -13,7 +13,11 @@ export type AlertDialogProp = {
|
|
|
13
13
|
variant?: ConfirmVariantProp;
|
|
14
14
|
/** Type-to-confirm friction — enables destructive flow (GitHub/Stripe style). */
|
|
15
15
|
confirmPhrase?: string;
|
|
16
|
+
/** Semantic alias of `confirmPhrase` — the exact token to type (e.g. an org slug) to arm confirm. */
|
|
17
|
+
challenge?: string;
|
|
16
18
|
onConfirm: HandlerProp;
|
|
19
|
+
/** Optional step-up re-auth gate (passkey/2FA); must resolve truthy before `onConfirm` fires. */
|
|
20
|
+
stepUp?: () => Promise<boolean> | boolean;
|
|
17
21
|
keepOpenOnConfirm?: boolean;
|
|
18
22
|
pending?: PendingProp;
|
|
19
23
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { AppShellProp, PageContainerProp, PageInsetProp, SidebarItemProp, SidebarProductProp, SidebarProp, SidebarSectionProp, TopbarProp, } from "./layout.prop.js";
|
|
2
2
|
export type { ButtonProp } from "./general.prop.js";
|
|
3
3
|
export type { InputProp, TextareaProp, FormFieldProp, SearchInputProp, CheckboxProp, CheckboxGroupProp, ChoiceOptionProp, RadioProp, RadioGroupProp, SwitchProp, SliderProp, CalendarProp, DatePickerProp, DateRangePickerProp, TimePickerProp, ColorPickerProp, UploadProp, UploadFileItemProp, UploadVariantProp, TreeOptionProp, TreeFieldNamesProp, CascaderProp, TreeSelectProp, ShowCheckedStrategyProp, TransferProp, TransferItemProp, } from "./data-entry.prop.js";
|
|
4
|
-
export type { EmptyStateProp, DescriptionsProp, DescriptionsItemProp, BadgeProp, DataTableProp, } from "./data-display.prop.js";
|
|
4
|
+
export type { EmptyStateProp, DescriptionsProp, DescriptionsItemProp, BadgeProp, DataTableProp, QrCodeProp, } from "./data-display.prop.js";
|
|
5
5
|
export type { ChartDatum, ChartSeriesProp, LineChartProp, BarChartProp, AreaChartProp, PieChartProp, } from "./charts.prop.js";
|
|
6
6
|
export type { AlertQueryErrorProp, AlertProp, AlertTitleProp, AlertContentProp, AlertDescriptionProp, AlertActionsProp, SkeletonRowsProp, } from "./feedback.prop.js";
|
|
7
7
|
export type { DataStateProp, InfiniteQueryStateProp, InfiniteQueryHelpers, PrefetchLinkProp, } from "./query.prop.js";
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/** Navigation component prop types — @see docs/COMPONENTS.md#navigation */
|
|
2
2
|
import type * as React from "react";
|
|
3
|
-
import type { ChildrenProp, ClassNameProp, DisabledProp, HasActiveFiltersProp, LabelProp, OnClearFiltersProp } from "../vocabulary/index.js";
|
|
3
|
+
import type { ChildrenProp, ClassNameProp, DisabledProp, HasActiveFiltersProp, LabelProp, OnClearFiltersProp, StickyProp } from "../vocabulary/index.js";
|
|
4
4
|
/** @see Toolbar */
|
|
5
5
|
export type ToolbarProp = {
|
|
6
6
|
onClear?: OnClearFiltersProp;
|
|
7
7
|
hasActiveFilters?: HasActiveFiltersProp;
|
|
8
|
+
/**
|
|
9
|
+
* Pin the strip to the top of its scroll container while the list scrolls beneath it
|
|
10
|
+
* (list-page filter bars, #197). Opt-in — default `false` keeps the toolbar quiet. Tune
|
|
11
|
+
* the pinned offset/background with the `--filter-bar-sticky-offset` /
|
|
12
|
+
* `--filter-bar-sticky-background` theme knobs.
|
|
13
|
+
*/
|
|
14
|
+
sticky?: StickyProp;
|
|
8
15
|
className?: ClassNameProp;
|
|
9
16
|
children: ChildrenProp;
|
|
10
17
|
};
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -384,6 +384,11 @@ export declare const VOCABULARY_REGISTRY: {
|
|
|
384
384
|
readonly category: "data";
|
|
385
385
|
readonly description: "Any filter active flag";
|
|
386
386
|
};
|
|
387
|
+
readonly StickyProp: {
|
|
388
|
+
readonly file: "vocabulary/data.prop.ts";
|
|
389
|
+
readonly category: "data";
|
|
390
|
+
readonly description: "Pin the filter strip while the list scrolls";
|
|
391
|
+
};
|
|
387
392
|
};
|
|
388
393
|
export type VocabularyPropName = keyof typeof VOCABULARY_REGISTRY;
|
|
389
394
|
export declare const COMPONENT_PROP_REGISTRY: {
|
|
@@ -724,6 +729,16 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
724
729
|
readonly file: "components/data-display.prop.ts";
|
|
725
730
|
readonly vocabulary: readonly ["ColumnDefProp", "DensityProp", "SortStateProp", "SelectedIdsProp"];
|
|
726
731
|
};
|
|
732
|
+
readonly CredentialRevealProp: {
|
|
733
|
+
readonly group: "data-display";
|
|
734
|
+
readonly file: "components/data-display.prop.ts";
|
|
735
|
+
readonly vocabulary: readonly ["LabelProp", "SizeProp", "ToneProp", "OnValueChangeProp", "HandlerProp", "ClassNameProp", "IdProp"];
|
|
736
|
+
};
|
|
737
|
+
readonly QrCodeProp: {
|
|
738
|
+
readonly group: "data-display";
|
|
739
|
+
readonly file: "components/data-display.prop.ts";
|
|
740
|
+
readonly vocabulary: readonly ["ValueProp", "LabelProp", "SizeProp", "ClassNameProp", "IdProp"];
|
|
741
|
+
};
|
|
727
742
|
readonly ChartSeriesProp: {
|
|
728
743
|
readonly group: "data-display";
|
|
729
744
|
readonly file: "components/charts.prop.ts";
|
|
@@ -900,7 +915,15 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
900
915
|
readonly field: "confirmPhrase";
|
|
901
916
|
readonly local: true;
|
|
902
917
|
readonly reason: "Destructive-dialog type-to-confirm friction phrase.";
|
|
918
|
+
}, {
|
|
919
|
+
readonly field: "challenge";
|
|
920
|
+
readonly local: true;
|
|
921
|
+
readonly reason: "Semantic alias of confirmPhrase — the typed challenge token (e.g. an org slug).";
|
|
903
922
|
}, "HandlerProp", {
|
|
923
|
+
readonly field: "stepUp";
|
|
924
|
+
readonly local: true;
|
|
925
|
+
readonly reason: "Step-up re-auth (passkey/2FA) gate resolved before confirm fires.";
|
|
926
|
+
}, {
|
|
904
927
|
readonly field: "keepOpenOnConfirm";
|
|
905
928
|
readonly local: true;
|
|
906
929
|
readonly reason: "AlertDialog-specific async completion behavior.";
|
|
@@ -985,7 +1008,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
985
1008
|
readonly ToolbarProp: {
|
|
986
1009
|
readonly group: "navigation";
|
|
987
1010
|
readonly file: "components/navigation.prop.ts";
|
|
988
|
-
readonly vocabulary: readonly ["OnClearFiltersProp", "HasActiveFiltersProp", "ClassNameProp", "ChildrenProp"];
|
|
1011
|
+
readonly vocabulary: readonly ["OnClearFiltersProp", "HasActiveFiltersProp", "StickyProp", "ClassNameProp", "ChildrenProp"];
|
|
989
1012
|
};
|
|
990
1013
|
readonly ToolbarGroupProp: {
|
|
991
1014
|
readonly group: "navigation";
|
package/dist/props/registry.js
CHANGED
|
@@ -384,6 +384,11 @@ const VOCABULARY_REGISTRY = {
|
|
|
384
384
|
file: "vocabulary/data.prop.ts",
|
|
385
385
|
category: "data",
|
|
386
386
|
description: "Any filter active flag"
|
|
387
|
+
},
|
|
388
|
+
StickyProp: {
|
|
389
|
+
file: "vocabulary/data.prop.ts",
|
|
390
|
+
category: "data",
|
|
391
|
+
description: "Pin the filter strip while the list scrolls"
|
|
387
392
|
}
|
|
388
393
|
};
|
|
389
394
|
const COMPONENT_PROP_REGISTRY = {
|
|
@@ -792,6 +797,24 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
792
797
|
file: "components/data-display.prop.ts",
|
|
793
798
|
vocabulary: ["ColumnDefProp", "DensityProp", "SortStateProp", "SelectedIdsProp"]
|
|
794
799
|
},
|
|
800
|
+
CredentialRevealProp: {
|
|
801
|
+
group: "data-display",
|
|
802
|
+
file: "components/data-display.prop.ts",
|
|
803
|
+
vocabulary: [
|
|
804
|
+
"LabelProp",
|
|
805
|
+
"SizeProp",
|
|
806
|
+
"ToneProp",
|
|
807
|
+
"OnValueChangeProp",
|
|
808
|
+
"HandlerProp",
|
|
809
|
+
"ClassNameProp",
|
|
810
|
+
"IdProp"
|
|
811
|
+
]
|
|
812
|
+
},
|
|
813
|
+
QrCodeProp: {
|
|
814
|
+
group: "data-display",
|
|
815
|
+
file: "components/data-display.prop.ts",
|
|
816
|
+
vocabulary: ["ValueProp", "LabelProp", "SizeProp", "ClassNameProp", "IdProp"]
|
|
817
|
+
},
|
|
795
818
|
ChartSeriesProp: {
|
|
796
819
|
group: "data-display",
|
|
797
820
|
file: "components/charts.prop.ts",
|
|
@@ -919,7 +942,17 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
919
942
|
local: true,
|
|
920
943
|
reason: "Destructive-dialog type-to-confirm friction phrase."
|
|
921
944
|
},
|
|
945
|
+
{
|
|
946
|
+
field: "challenge",
|
|
947
|
+
local: true,
|
|
948
|
+
reason: "Semantic alias of confirmPhrase \u2014 the typed challenge token (e.g. an org slug)."
|
|
949
|
+
},
|
|
922
950
|
"HandlerProp",
|
|
951
|
+
{
|
|
952
|
+
field: "stepUp",
|
|
953
|
+
local: true,
|
|
954
|
+
reason: "Step-up re-auth (passkey/2FA) gate resolved before confirm fires."
|
|
955
|
+
},
|
|
923
956
|
{
|
|
924
957
|
field: "keepOpenOnConfirm",
|
|
925
958
|
local: true,
|
|
@@ -1007,7 +1040,13 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
1007
1040
|
ToolbarProp: {
|
|
1008
1041
|
group: "navigation",
|
|
1009
1042
|
file: "components/navigation.prop.ts",
|
|
1010
|
-
vocabulary: [
|
|
1043
|
+
vocabulary: [
|
|
1044
|
+
"OnClearFiltersProp",
|
|
1045
|
+
"HasActiveFiltersProp",
|
|
1046
|
+
"StickyProp",
|
|
1047
|
+
"ClassNameProp",
|
|
1048
|
+
"ChildrenProp"
|
|
1049
|
+
]
|
|
1011
1050
|
},
|
|
1012
1051
|
ToolbarGroupProp: {
|
|
1013
1052
|
group: "navigation",
|
|
@@ -58,3 +58,9 @@ export type OnSearchChangeProp = (query: string) => void;
|
|
|
58
58
|
export type OnClearFiltersProp = () => void;
|
|
59
59
|
/** Whether any filter is active — shows "Clear all". */
|
|
60
60
|
export type HasActiveFiltersProp = boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Whether the filter strip pins to the top of its scroll container while the list
|
|
63
|
+
* scrolls beneath it. Opt-in (default `false`) so the toolbar stays quiet chrome;
|
|
64
|
+
* offset/background are tuned via the `--filter-bar-sticky-*` theme knobs.
|
|
65
|
+
*/
|
|
66
|
+
export type StickyProp = boolean;
|
|
@@ -4,4 +4,4 @@ export type { TitleProp, SubtitleProp, DescriptionProp, ExtraProp, FooterProp, A
|
|
|
4
4
|
export type { PageDensityProp, PageContainerVariantProp, CenteredShellWidthProp, TableDensityProp, DensityProp, GapProp, } from "./layout.prop.js";
|
|
5
5
|
export type { ButtonVariantProp, ButtonSizeProp, BadgeVariantProp, AppSettingPickerAppearanceProp, ShapeProp, TextSizeProp, TextToneProp, FontWeightProp, HeadingLevelProp, TextAlignProp, SizeProp, FormLayoutProp, BreakpointProp, ConfirmVariantProp, ToneProp, AlertVariantProp, SortDirectionProp, ColumnAlignProp, SortStateProp, RevealDelayProp, } from "./interaction.prop.js";
|
|
6
6
|
export type { BreadcrumbItemProp, BreadcrumbProp } from "./navigation.prop.js";
|
|
7
|
-
export type { GetRowIdProp, OnRowClickProp, ColumnDefProp, SelectedIdsProp, OnSelectChangeProp, OnTableDensityChangeProp, OnSortChangeProp, OnSearchChangeProp, OnClearFiltersProp, HasActiveFiltersProp, } from "./data.prop.js";
|
|
7
|
+
export type { GetRowIdProp, OnRowClickProp, ColumnDefProp, SelectedIdsProp, OnSelectChangeProp, OnTableDensityChangeProp, OnSortChangeProp, OnSearchChangeProp, OnClearFiltersProp, HasActiveFiltersProp, StickyProp, } from "./data.prop.js";
|
package/dist/styles/control.css
CHANGED
|
@@ -117,8 +117,16 @@
|
|
|
117
117
|
box-shadow: var(--shadow-sm);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/* SOLID hover/active tokens, never `--destructive / 0.9`: an alpha fill composites with whatever
|
|
121
|
+
* surface sits behind the button (a lighter Card/AlertDialog vs the page), so the effective colour
|
|
122
|
+
* — and its contrast with the label — drifts per context and can dip below AA (gh#199). The solid
|
|
123
|
+
* --destructive-hover/active are tuned to clear 4.5:1 on every surface. */
|
|
120
124
|
.ui-button--destructive:hover {
|
|
121
|
-
background: hsl(var(--destructive)
|
|
125
|
+
background: hsl(var(--destructive-hover));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.ui-button--destructive:active {
|
|
129
|
+
background: hsl(var(--destructive-active));
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
/* Outline + ghost set their text colour EXPLICITLY (read --foreground) — never rely on inherited
|
|
@@ -484,4 +484,73 @@
|
|
|
484
484
|
gap: var(--space-2);
|
|
485
485
|
margin-inline-start: auto;
|
|
486
486
|
}
|
|
487
|
+
|
|
488
|
+
/* CredentialReveal — one-time secret surface: caution banner, masked/revealed value row with
|
|
489
|
+
* copy/reveal/download actions, and an optional acknowledge footer. Density-aware via the
|
|
490
|
+
* --control-height tier; colours/spacing read semantic role tokens only. */
|
|
491
|
+
.ui-credential-reveal {
|
|
492
|
+
display: flex;
|
|
493
|
+
flex-direction: column;
|
|
494
|
+
gap: var(--space-stack-sm);
|
|
495
|
+
}
|
|
496
|
+
.ui-credential-reveal-warning {
|
|
497
|
+
margin-block-end: var(--space-1);
|
|
498
|
+
}
|
|
499
|
+
.ui-credential-reveal-surface {
|
|
500
|
+
display: flex;
|
|
501
|
+
flex-direction: column;
|
|
502
|
+
gap: var(--space-1);
|
|
503
|
+
}
|
|
504
|
+
.ui-credential-reveal-label {
|
|
505
|
+
display: inline-block;
|
|
506
|
+
}
|
|
507
|
+
.ui-credential-reveal-row {
|
|
508
|
+
display: flex;
|
|
509
|
+
align-items: center;
|
|
510
|
+
gap: var(--space-inline-sm);
|
|
511
|
+
min-height: var(--control-height);
|
|
512
|
+
padding-block: var(--space-1);
|
|
513
|
+
padding-inline: var(--space-3);
|
|
514
|
+
border: 1px solid hsl(var(--border));
|
|
515
|
+
border-radius: var(--control-radius);
|
|
516
|
+
background: hsl(var(--muted) / 0.4);
|
|
517
|
+
}
|
|
518
|
+
.ui-credential-reveal-secret {
|
|
519
|
+
flex: 1 1 auto;
|
|
520
|
+
min-width: 0;
|
|
521
|
+
letter-spacing: 0.02em;
|
|
522
|
+
word-break: break-all;
|
|
523
|
+
}
|
|
524
|
+
.ui-credential-reveal-secret[data-state="masked"] {
|
|
525
|
+
color: hsl(var(--muted-foreground));
|
|
526
|
+
}
|
|
527
|
+
.ui-credential-reveal-actions {
|
|
528
|
+
display: inline-flex;
|
|
529
|
+
flex-shrink: 0;
|
|
530
|
+
align-items: center;
|
|
531
|
+
gap: var(--space-inline-xs);
|
|
532
|
+
}
|
|
533
|
+
.ui-credential-reveal-footer {
|
|
534
|
+
display: flex;
|
|
535
|
+
justify-content: flex-end;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/* QrCode — a quiet, scanner-safe SVG surface with an ISO/IEC-style four-module quiet zone
|
|
539
|
+
* supplied by the encoder. Sizing is natural rather than stretched across its container. */
|
|
540
|
+
.ui-qr-code {
|
|
541
|
+
display: block;
|
|
542
|
+
inline-size: var(--qr-code-size-md);
|
|
543
|
+
block-size: auto;
|
|
544
|
+
max-inline-size: 100%;
|
|
545
|
+
aspect-ratio: 1;
|
|
546
|
+
}
|
|
547
|
+
.ui-qr-code[data-size="xs"] {
|
|
548
|
+
inline-size: var(--qr-code-size-xs);
|
|
549
|
+
}
|
|
550
|
+
.ui-qr-code[data-size="sm"] {
|
|
551
|
+
inline-size: var(--qr-code-size-sm);
|
|
552
|
+
}
|
|
553
|
+
.ui-qr-code[data-size="lg"] {
|
|
554
|
+
inline-size: var(--qr-code-size-lg);
|
|
555
|
+
}
|
|
487
556
|
}
|
package/dist/styles/layout.css
CHANGED
|
@@ -717,6 +717,16 @@
|
|
|
717
717
|
}
|
|
718
718
|
}
|
|
719
719
|
|
|
720
|
+
/* Toolbar `sticky` (#197): pin the filter strip while the list scrolls beneath it.
|
|
721
|
+
* `inset-block-start` (not `top`) keeps it RTL/vertical-writing-mode safe; the fill only
|
|
722
|
+
* paints in this pinned variant so a non-sticky toolbar stays transparent chrome. */
|
|
723
|
+
.ui-toolbar-sticky {
|
|
724
|
+
position: sticky;
|
|
725
|
+
inset-block-start: var(--filter-bar-sticky-offset);
|
|
726
|
+
z-index: 20;
|
|
727
|
+
background: hsl(var(--filter-bar-sticky-background, var(--background)));
|
|
728
|
+
}
|
|
729
|
+
|
|
720
730
|
.ui-toolbar-clear {
|
|
721
731
|
width: 100%;
|
|
722
732
|
}
|
|
@@ -31,4 +31,13 @@
|
|
|
31
31
|
re-resolves under a scoped theme. Defaults = hsl(var(--primary) / 0.3) border · 0.05 fill. */
|
|
32
32
|
--tree-item-active-border: initial;
|
|
33
33
|
--tree-item-active-background: initial;
|
|
34
|
+
|
|
35
|
+
/* QR codes stay scanner-safe in light and dark application themes. Consumers may override these
|
|
36
|
+
component tokens only when the resulting pair retains strong contrast. */
|
|
37
|
+
--qr-code-foreground: 0 0% 0%;
|
|
38
|
+
--qr-code-background: 0 0% 100%;
|
|
39
|
+
--qr-code-size-xs: 6rem;
|
|
40
|
+
--qr-code-size-sm: 8rem;
|
|
41
|
+
--qr-code-size-md: 10rem;
|
|
42
|
+
--qr-code-size-lg: 12.5rem;
|
|
34
43
|
}
|
|
@@ -20,4 +20,11 @@
|
|
|
20
20
|
* Defaults = hsl(var(--accent)) fill · hsl(var(--accent-foreground)) text. */
|
|
21
21
|
--menubar-item-hover-background: initial;
|
|
22
22
|
--menubar-item-hover-foreground: initial;
|
|
23
|
+
|
|
24
|
+
/* Sticky offset for the pinned filter strip (Toolbar `sticky`, #197). Default 0 (chrome,
|
|
25
|
+
* rule #44); raise it so the strip parks below a fixed topbar. */
|
|
26
|
+
--filter-bar-sticky-offset: 0px;
|
|
27
|
+
/* Fill painted only when the filter strip is pinned. Role-mirror knob (rule #45): `initial`
|
|
28
|
+
* so --background re-resolves at the call site under a scoped [data-tenant]/.dark theme. */
|
|
29
|
+
--filter-bar-sticky-background: initial;
|
|
23
30
|
}
|
|
@@ -359,8 +359,12 @@
|
|
|
359
359
|
--muted-foreground: 44 6% 64%;
|
|
360
360
|
--accent: 45 7% 22%;
|
|
361
361
|
--accent-foreground: 60 20% 96%;
|
|
362
|
-
|
|
363
|
-
|
|
362
|
+
/* Filled destructive surfaces (button/badge/step) must clear WCAG AA (4.5:1) with comfortable
|
|
363
|
+
* margin, not sit on the floor (gh#199 — dark default was 4.54:1). Base darkened 52%→48% and the
|
|
364
|
+
* on-fill text lifted to pure white → ~5.5:1; states go DARKER (never lighter, which cut contrast
|
|
365
|
+
* against the light label). Error TEXT on dark surfaces uses --text-error, not this fill token. */
|
|
366
|
+
--destructive: 357 55% 48%;
|
|
367
|
+
--destructive-foreground: 0 0% 100%;
|
|
364
368
|
--border: 45 6% 22%;
|
|
365
369
|
--input: 45 6% 22%;
|
|
366
370
|
--ring: 204 90% 60%;
|
|
@@ -388,6 +392,6 @@
|
|
|
388
392
|
--primary-active: 204 90% 72%;
|
|
389
393
|
--secondary-hover: 45 6% 22%;
|
|
390
394
|
--secondary-active: 45 6% 26%;
|
|
391
|
-
--destructive-hover: 357 55%
|
|
392
|
-
--destructive-active: 357 55%
|
|
395
|
+
--destructive-hover: 357 55% 43%;
|
|
396
|
+
--destructive-active: 357 55% 38%;
|
|
393
397
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/ui",
|
|
3
|
-
"version": "18.
|
|
4
|
-
"godxUiMcp": "18.
|
|
3
|
+
"version": "18.3.0",
|
|
4
|
+
"godxUiMcp": "18.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.29.1",
|
|
7
7
|
"pnpm": {
|
|
@@ -113,6 +113,10 @@
|
|
|
113
113
|
"types": "./dist/lib/utils.d.ts",
|
|
114
114
|
"import": "./dist/lib/utils.js"
|
|
115
115
|
},
|
|
116
|
+
"./lib/permission-grid": {
|
|
117
|
+
"types": "./dist/lib/permission-grid.d.ts",
|
|
118
|
+
"import": "./dist/lib/permission-grid.js"
|
|
119
|
+
},
|
|
116
120
|
"./hooks": {
|
|
117
121
|
"types": "./dist/lib/hooks.d.ts",
|
|
118
122
|
"import": "./dist/lib/hooks.js"
|
|
@@ -406,6 +410,7 @@
|
|
|
406
410
|
"embla-carousel-react": "^8.6.0",
|
|
407
411
|
"input-otp": "^1.4.2",
|
|
408
412
|
"lucide-react": "^1.25.0",
|
|
413
|
+
"qrcode.react": "4.2.0",
|
|
409
414
|
"react-day-picker": "^10.0.1",
|
|
410
415
|
"react-resizable-panels": "^4.12.2",
|
|
411
416
|
"sonner": "^2.0.7",
|
|
@@ -423,6 +428,7 @@
|
|
|
423
428
|
"@testing-library/react": "^16.3.2",
|
|
424
429
|
"@testing-library/user-event": "^14.6.1",
|
|
425
430
|
"@types/node": "^22.19.19",
|
|
431
|
+
"@types/pngjs": "^6.0.5",
|
|
426
432
|
"@types/react": "^19.2.17",
|
|
427
433
|
"@types/react-dom": "^19.2.3",
|
|
428
434
|
"@vitejs/plugin-react": "^6.0.3",
|
|
@@ -433,7 +439,9 @@
|
|
|
433
439
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
434
440
|
"globals": "^15.15.0",
|
|
435
441
|
"jsdom": "^29.1.1",
|
|
442
|
+
"jsqr": "^1.4.0",
|
|
436
443
|
"playwright": "^1.61.1",
|
|
444
|
+
"pngjs": "^7.0.0",
|
|
437
445
|
"prettier": "^3.9.5",
|
|
438
446
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
439
447
|
"react": "^19.2.7",
|