@cosmicdrift/kumiko-renderer 0.384.0 → 0.385.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/dist/app/action-form-body.d.ts +50 -0
- package/dist/app/action-form-body.js +275 -0
- package/dist/app/client-actions.js +2 -2
- package/dist/app/config-edit-body.d.ts +9 -0
- package/dist/app/config-edit-body.js +340 -0
- package/dist/app/entity-edit-prefill.d.ts +27 -0
- package/dist/app/entity-edit-prefill.js +377 -0
- package/dist/app/entity-edit-screen.d.ts +11 -0
- package/dist/app/entity-edit-screen.js +395 -0
- package/dist/app/entity-list-screen.d.ts +10 -0
- package/dist/app/entity-list-screen.js +547 -0
- package/dist/app/kumiko-screen.d.ts +1 -36
- package/dist/app/kumiko-screen.js +16 -2855
- package/dist/app/projection-detail-body.d.ts +12 -0
- package/dist/app/projection-detail-body.js +464 -0
- package/dist/app/projection-detail-shim.d.ts +1 -1
- package/dist/app/projection-detail-shim.js +5 -1
- package/dist/app/projection-list-body.d.ts +10 -0
- package/dist/app/projection-list-body.js +245 -0
- package/dist/app/projection-list-shim.d.ts +2 -1
- package/dist/app/projection-list-shim.js +4 -0
- package/dist/app/reference-facet-bridge.js +2 -2
- package/dist/app/row-actions.js +3 -4
- package/dist/app/screen-shared.d.ts +72 -0
- package/dist/app/screen-shared.js +325 -0
- package/dist/app/secret-mint-body.js +3 -3
- package/dist/components/edit-mounts.d.ts +33 -0
- package/dist/components/edit-mounts.js +53 -0
- package/dist/components/error-boundary.js +2 -2
- package/dist/components/reference-create-dialog.js +1 -1
- package/dist/components/related-list-section.js +2 -2
- package/dist/components/render-edit-helpers.d.ts +29 -0
- package/dist/components/render-edit-helpers.js +121 -0
- package/dist/components/render-edit.d.ts +1 -4
- package/dist/components/render-edit.js +41 -443
- package/dist/components/render-field.js +3 -4
- package/dist/components/render-list.js +2 -2
- package/dist/components/use-wizard-form-draft.d.ts +51 -0
- package/dist/components/use-wizard-form-draft.js +291 -0
- package/dist/dev-console.d.ts +3 -0
- package/dist/dev-console.js +15 -0
- package/dist/headless/contracts/primitives.js +14 -17
- package/dist/headless/format/index.js +3 -3
- package/dist/headless/view-model/edit.d.ts +1 -1
- package/dist/headless/view-model/edit.js +9 -16
- package/dist/headless/view-model/list.d.ts +1 -1
- package/dist/headless/view-model/list.js +3 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -5
- package/dist/live/error-mapping.js +3 -4
- package/dist/web/app/create-app.js +5 -8
- package/dist/web/app/dashboard-body.js +4 -599
- package/dist/web/app/dashboard-panels.d.ts +53 -0
- package/dist/web/app/dashboard-panels.js +373 -0
- package/dist/web/app/dashboard-shared.d.ts +90 -0
- package/dist/web/app/dashboard-shared.js +171 -0
- package/dist/web/app/dashboard-text.d.ts +6 -0
- package/dist/web/app/dashboard-text.js +65 -0
- package/dist/web/layout/nav-tree.js +3 -4
- package/dist/web/layout/target-resolver-stub.js +2 -2
- package/dist/web/primitives/banner.d.ts +4 -0
- package/dist/web/primitives/banner.js +28 -0
- package/dist/web/primitives/button.d.ts +7 -0
- package/dist/web/primitives/button.js +48 -0
- package/dist/web/primitives/card-styles.d.ts +8 -0
- package/dist/web/primitives/card-styles.js +18 -0
- package/dist/web/primitives/card.d.ts +4 -0
- package/dist/web/primitives/card.js +41 -0
- package/dist/web/primitives/contexts.d.ts +22 -0
- package/dist/web/primitives/contexts.js +50 -0
- package/dist/web/primitives/data-table-cell.d.ts +76 -0
- package/dist/web/primitives/data-table-cell.js +403 -0
- package/dist/web/primitives/data-table.d.ts +4 -0
- package/dist/web/primitives/data-table.js +279 -0
- package/dist/web/primitives/default-form.d.ts +4 -0
- package/dist/web/primitives/default-form.js +79 -0
- package/dist/web/primitives/facet-filter.d.ts +14 -0
- package/dist/web/primitives/facet-filter.js +39 -0
- package/dist/web/primitives/field.d.ts +63 -0
- package/dist/web/primitives/field.js +181 -0
- package/dist/web/primitives/form.d.ts +53 -0
- package/dist/web/primitives/form.js +252 -0
- package/dist/web/primitives/index.d.ts +7 -30
- package/dist/web/primitives/index.js +27 -2006
- package/dist/web/primitives/input.d.ts +4 -0
- package/dist/web/primitives/input.js +166 -0
- package/dist/web/primitives/layout-elements.d.ts +14 -0
- package/dist/web/primitives/layout-elements.js +194 -0
- package/dist/web/primitives/modal.js +1 -1
- package/dist/web/primitives/pager.d.ts +22 -0
- package/dist/web/primitives/pager.js +89 -0
- package/dist/web/primitives/row-actions.d.ts +13 -0
- package/dist/web/primitives/row-actions.js +178 -0
- package/dist/web/primitives/secret-reveal.d.ts +4 -0
- package/dist/web/primitives/secret-reveal.js +67 -0
- package/package.json +5 -5
- package/src/changes.json +6 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Loader2, MoreHorizontal } from "lucide-react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { needsActionConfirm, shouldRenderActionsIconOnly, useTranslation, WriteFailedError, } from "../../index.js";
|
|
5
|
+
import { Icon } from "../icons.js";
|
|
6
|
+
import { cn } from "../lib/cn.js";
|
|
7
|
+
import { Button as UiButton } from "../ui/button.js";
|
|
8
|
+
import { DefaultDialog } from "./dialog.js";
|
|
9
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "./dropdown-menu.js";
|
|
10
|
+
import { useToast } from "./toast.js";
|
|
11
|
+
import "../react-css-custom-properties.js";
|
|
12
|
+
import { actionIconFor, SECONDARY_BUTTON_EXTRA_CLASSES } from "./button.js";
|
|
13
|
+
// The primary row action is the one that always stays a visible text
|
|
14
|
+
// button — `edit` if declared, else the first visible action (fw
|
|
15
|
+
// bedienkonzept L3: "Bearbeiten steht in jeder Zeile, immer, als Text").
|
|
16
|
+
function primaryRowAction(actions) {
|
|
17
|
+
return actions.find((a) => a.id === "edit") ?? actions[0];
|
|
18
|
+
}
|
|
19
|
+
// RowActionsCell renders the row actions depending on `mode`:
|
|
20
|
+
// - "adaptive" (default): when the row itself is the link (`rowIsLink`)
|
|
21
|
+
// every action sits in the kebab; otherwise the primary action (see
|
|
22
|
+
// `primaryRowAction`) is a link-button and the rest goes into the kebab.
|
|
23
|
+
// A single action never gets a kebab.
|
|
24
|
+
// - "inline": ALWAYS inline buttons, left-aligned + full-width, even for
|
|
25
|
+
// >2 (no kebab). `w-full justify-start` pins the first button to the
|
|
26
|
+
// column's left edge so it sits at the same position across rows
|
|
27
|
+
// (otherwise it would drift with differently-sized labels).
|
|
28
|
+
// The isVisible filter runs here; an action hidden for a given row never
|
|
29
|
+
// reaches render. If every action is hidden, the cell stays empty (no
|
|
30
|
+
// phantom column).
|
|
31
|
+
export function RowActionsCell({ row, actions, mode = "adaptive", rowIsLink = false, compact = false, }) {
|
|
32
|
+
const visible = actions.filter((a) => a.isVisible === undefined || a.isVisible(row));
|
|
33
|
+
if (visible.length === 0)
|
|
34
|
+
return null;
|
|
35
|
+
const hasExplicitDisplay = visible.some((a) => a.display !== undefined);
|
|
36
|
+
if (mode === "inline") {
|
|
37
|
+
// Teil C: >2 actions all carrying an icon collapse to icon-only —
|
|
38
|
+
// otherwise "inline" is exactly the wall-to-wall text-button problem
|
|
39
|
+
// this feature exists to fix. An author-set `display` takes that choice over.
|
|
40
|
+
const iconOnly = !hasExplicitDisplay && shouldRenderActionsIconOnly(visible);
|
|
41
|
+
return (_jsx("div", { className: "flex w-full items-center gap-1 justify-start", children: visible.map((a) => (_jsx(RowActionButton, { row: row, action: a, iconOnly: iconOnly, compact: compact }, a.id))) }));
|
|
42
|
+
}
|
|
43
|
+
if (hasExplicitDisplay) {
|
|
44
|
+
const inline = visible.filter((a) => a.display !== undefined);
|
|
45
|
+
const rest = visible.filter((a) => a.display === undefined);
|
|
46
|
+
return (_jsxs("div", { className: "inline-flex flex-wrap items-center gap-1 justify-end", children: [inline.map((a) => (_jsx(RowActionButton, { row: row, action: a, compact: compact }, a.id))), rest.length > 0 && _jsx(RowActionsKebab, { row: row, actions: rest })] }));
|
|
47
|
+
}
|
|
48
|
+
if (rowIsLink) {
|
|
49
|
+
return (_jsx("div", { className: "inline-flex items-center justify-end", children: _jsx(RowActionsKebab, { row: row, actions: visible }) }));
|
|
50
|
+
}
|
|
51
|
+
const primary = primaryRowAction(visible);
|
|
52
|
+
const rest = visible.filter((a) => a.id !== primary?.id);
|
|
53
|
+
return (_jsxs("div", { className: "inline-flex flex-wrap items-center gap-1 justify-end", children: [primary !== undefined && _jsx(RowActionButton, { row: row, action: primary, asLink: true }), rest.length > 0 && _jsx(RowActionsKebab, { row: row, actions: rest })] }));
|
|
54
|
+
}
|
|
55
|
+
// Shared trigger state between inline button + kebab item: busy flag
|
|
56
|
+
// (while the async onTrigger runs) + confirm-pending action. Both
|
|
57
|
+
// sub-components had duplicated the same state block in parallel to the
|
|
58
|
+
// confirm dialog render logic; the hook consolidates that.
|
|
59
|
+
function useRowActionTrigger(row) {
|
|
60
|
+
const [busy, setBusy] = useState(false);
|
|
61
|
+
const { toast } = useToast();
|
|
62
|
+
const t = useTranslation();
|
|
63
|
+
const triggerNow = async (action) => {
|
|
64
|
+
setBusy(true);
|
|
65
|
+
try {
|
|
66
|
+
await action.onTrigger(row);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
// Surface instead of swallow: a swallowed write error looked like
|
|
70
|
+
// "nothing happened" to the user (prod bug 2026-06-07).
|
|
71
|
+
const dispatcherError = e instanceof WriteFailedError ? e.dispatcherError : undefined;
|
|
72
|
+
const docsUrl = dispatcherError?.docsUrl;
|
|
73
|
+
const requestId = dispatcherError?.requestId;
|
|
74
|
+
toast({
|
|
75
|
+
title: t("kumiko.rowAction.failed"),
|
|
76
|
+
description: e instanceof Error ? e.message : String(e),
|
|
77
|
+
variant: "bad",
|
|
78
|
+
...(docsUrl !== undefined && { docsUrl }),
|
|
79
|
+
...(requestId !== undefined && { requestId }),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
setBusy(false);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
return { busy, triggerNow };
|
|
87
|
+
}
|
|
88
|
+
// An icon-only form without a resolvable icon would render an empty button.
|
|
89
|
+
// `compact` is the narrow card layout, where "responsive" collapses to its icon.
|
|
90
|
+
function effectiveRowActionDisplay(display, hasIcon, compact) {
|
|
91
|
+
if (display === "responsive")
|
|
92
|
+
return hasIcon && compact ? "icon" : "button";
|
|
93
|
+
if (display === "icon" && !hasIcon)
|
|
94
|
+
return "link";
|
|
95
|
+
return display;
|
|
96
|
+
}
|
|
97
|
+
function RowActionButton({ row, action, iconOnly = false, asLink = false, compact = false, }) {
|
|
98
|
+
const { busy, triggerNow } = useRowActionTrigger(row);
|
|
99
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
100
|
+
const resolvedIcon = actionIconFor(action.icon);
|
|
101
|
+
const display = effectiveRowActionDisplay(action.display, resolvedIcon !== undefined, compact);
|
|
102
|
+
const isBorderedButton = display === "button";
|
|
103
|
+
const showIconOnly = display === "icon" || (display === undefined && iconOnly && resolvedIcon !== undefined);
|
|
104
|
+
const linkLike = display === "link" || (display === undefined && asLink);
|
|
105
|
+
const variantClass = isBorderedButton
|
|
106
|
+
? action.style === "primary"
|
|
107
|
+
? "border border-transparent bg-primary text-primary-foreground hover:bg-primary/90"
|
|
108
|
+
: action.style === "danger"
|
|
109
|
+
? "border border-destructive/40 bg-card text-destructive hover:bg-destructive/10"
|
|
110
|
+
: "border border-border-strong bg-card text-foreground hover:bg-accent"
|
|
111
|
+
: action.style === "danger"
|
|
112
|
+
? "text-destructive hover:bg-destructive/10"
|
|
113
|
+
: linkLike
|
|
114
|
+
? "font-medium text-primary hover:bg-muted"
|
|
115
|
+
: action.style === "primary"
|
|
116
|
+
? "text-primary hover:bg-primary/10"
|
|
117
|
+
: "text-foreground hover:bg-accent";
|
|
118
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", "data-testid": `row-${row.id}-action-${action.id}`, disabled: busy, ...(showIconOnly && { "aria-label": action.label, title: action.label }), onClick: (e) => {
|
|
119
|
+
e.stopPropagation();
|
|
120
|
+
if (needsActionConfirm(action)) {
|
|
121
|
+
setConfirmOpen(true);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
void triggerNow(action);
|
|
125
|
+
}
|
|
126
|
+
}, className: cn("inline-flex items-center justify-center gap-1.5 text-sm", isBorderedButton
|
|
127
|
+
? "h-8 rounded-md px-3 font-medium max-md:h-11"
|
|
128
|
+
: linkLike
|
|
129
|
+
? "h-7 rounded-md px-2.5 max-md:h-11"
|
|
130
|
+
: "h-8 rounded-sm max-md:h-11", showIconOnly ? "w-8 max-md:w-11" : !linkLike && !isBorderedButton && "px-2", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", "disabled:opacity-50 disabled:pointer-events-none", variantClass), children: busy ? (_jsx(Loader2, { className: "size-3.5 animate-spin", "aria-hidden": "true" })) : resolvedIcon === undefined || (linkLike && !showIconOnly) ? (action.label) : showIconOnly ? (_jsx(Icon, { name: resolvedIcon, className: "size-4" })) : (_jsxs(_Fragment, { children: [_jsx(Icon, { name: resolvedIcon, className: "size-4" }), action.label] })) }), _jsx(DefaultDialog, { open: confirmOpen, onOpenChange: setConfirmOpen, title: action.label, ...(action.confirm !== undefined && { description: action.confirm }), confirmLabel: action.confirmLabel ?? action.label, ...(action.style === "danger" && { variant: "danger" }), onConfirm: () => triggerNow(action), testId: `row-${row.id}-action-${action.id}-dialog` })] }));
|
|
131
|
+
}
|
|
132
|
+
// Kebab dropdown for >2 actions, one inline confirm dialog per item like
|
|
133
|
+
// the inline-button path. Menu is controlled so it closes explicitly on
|
|
134
|
+
// select, instead of relying on Radix's auto-close, which preventDefault
|
|
135
|
+
// below blocks.
|
|
136
|
+
function RowActionsKebab({ row, actions, }) {
|
|
137
|
+
const { triggerNow } = useRowActionTrigger(row);
|
|
138
|
+
const t = useTranslation();
|
|
139
|
+
const [pendingConfirm, setPendingConfirm] = useState(null);
|
|
140
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
141
|
+
return (_jsxs(_Fragment, { children: [_jsxs(DropdownMenu, { open: menuOpen, onOpenChange: setMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": t("kumiko.list.row-actions.more"), "data-testid": `row-${row.id}-actions-menu`, className: cn("inline-flex size-7 items-center justify-center rounded-md max-md:size-11", "hover:bg-accent text-muted-foreground hover:text-foreground", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"), children: _jsx(MoreHorizontal, { className: "size-4", "aria-hidden": "true" }) }) }), _jsx(DropdownMenuContent, { align: "end",
|
|
142
|
+
// Radix refocuses the trigger after the exit animation, which by then sits behind the confirm dialog's aria-hidden.
|
|
143
|
+
onCloseAutoFocus: (e) => {
|
|
144
|
+
if (pendingConfirm !== null)
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
}, children: actions.map((action) => (_jsx(DropdownMenuItem, { "data-testid": `row-${row.id}-action-${action.id}`, onSelect: (e) => {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
setMenuOpen(false);
|
|
149
|
+
if (needsActionConfirm(action)) {
|
|
150
|
+
setPendingConfirm(action);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
void triggerNow(action);
|
|
154
|
+
}
|
|
155
|
+
}, className: cn(action.style === "danger" && "text-destructive focus:text-destructive"), children: action.label }, action.id))) })] }), pendingConfirm !== null && (_jsx(DefaultDialog, { open: true, onOpenChange: (open) => {
|
|
156
|
+
if (!open)
|
|
157
|
+
setPendingConfirm(null);
|
|
158
|
+
}, title: pendingConfirm.label, ...(pendingConfirm.confirm !== undefined && { description: pendingConfirm.confirm }), confirmLabel: pendingConfirm.confirmLabel ?? pendingConfirm.label, ...(pendingConfirm.style === "danger" && { variant: "danger" }), onConfirm: async () => {
|
|
159
|
+
const action = pendingConfirm;
|
|
160
|
+
setPendingConfirm(null);
|
|
161
|
+
await triggerNow(action);
|
|
162
|
+
}, testId: `row-${row.id}-action-${pendingConfirm.id}-dialog` }))] }));
|
|
163
|
+
}
|
|
164
|
+
// Header/row-actions overflow menu (A7): the trigger carries the classes of a
|
|
165
|
+
// secondary icon Button so it matches neighbouring icon buttons, including
|
|
166
|
+
// the 44 px touch size on phones. Generic ActionMenuItemSpec items instead of
|
|
167
|
+
// the DataTableRowAction schema (callers own confirm/danger handling per item).
|
|
168
|
+
export function ActionOverflowMenu({ items, label, testId }) {
|
|
169
|
+
const [open, setOpen] = useState(false);
|
|
170
|
+
return (_jsxs(DropdownMenu, { open: open, onOpenChange: setOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(UiButton, { type: "button", variant: "outline", size: "icon", className: SECONDARY_BUTTON_EXTRA_CLASSES, "aria-label": label, "data-testid": testId ?? "action-overflow-menu-trigger", children: _jsx(MoreHorizontal, { className: "size-4", "aria-hidden": "true" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: items.map((item) => {
|
|
171
|
+
const resolvedIcon = actionIconFor(item.icon);
|
|
172
|
+
return (_jsxs(DropdownMenuItem, { "data-testid": item.testId ?? `${testId ?? "action-overflow-menu"}-item-${item.id}`, disabled: item.disabled === true, onSelect: (e) => {
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
setOpen(false);
|
|
175
|
+
item.onSelect();
|
|
176
|
+
}, className: cn(item.variant === "danger" && "text-destructive focus:text-destructive"), children: [resolvedIcon !== undefined && _jsx(Icon, { name: resolvedIcon, className: "size-4" }), item.label] }, item.id));
|
|
177
|
+
}) })] }));
|
|
178
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/// <reference path="../qrcode-browser.d.ts" />
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Button as UiButton } from "../ui/button.js";
|
|
5
|
+
import "../react-css-custom-properties.js";
|
|
6
|
+
// otpauth:// enrollment URI → scannable QR (errorCorrectionLevel "H", ~30%
|
|
7
|
+
// redundancy — more resilient to camera/lighting issues than the default, no
|
|
8
|
+
// downside for a code this short-lived). Own component (not inline in
|
|
9
|
+
// values.map) because QRCode.toString is async — a hook inside .map would
|
|
10
|
+
// violate the rules of hooks.
|
|
11
|
+
function QrSecretValue({ value }) {
|
|
12
|
+
const [svg, setSvg] = useState(null);
|
|
13
|
+
const [failed, setFailed] = useState(false);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
let cancelled = false;
|
|
16
|
+
setFailed(false);
|
|
17
|
+
// Loaded on first QR render, not with the primitives. The main entry pulls
|
|
18
|
+
// Node-only deps (yargs/pngjs) under bundlers that ignore package.json#browser
|
|
19
|
+
// (Metro), hence the explicit browser subpath.
|
|
20
|
+
import("qrcode/lib/browser.js")
|
|
21
|
+
.then(({ default: QRCode }) => QRCode.toString(value, { type: "svg", errorCorrectionLevel: "H" }))
|
|
22
|
+
.then((result) => {
|
|
23
|
+
if (!cancelled)
|
|
24
|
+
setSvg(result);
|
|
25
|
+
})
|
|
26
|
+
.catch(() => {
|
|
27
|
+
// A generic `qr` field may have no separate text display, so fall back to the plain value.
|
|
28
|
+
if (!cancelled)
|
|
29
|
+
setFailed(true);
|
|
30
|
+
});
|
|
31
|
+
return () => {
|
|
32
|
+
cancelled = true;
|
|
33
|
+
};
|
|
34
|
+
}, [value]);
|
|
35
|
+
if (failed) {
|
|
36
|
+
return (_jsx("code", { className: "flex-1 overflow-x-auto whitespace-pre-wrap break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: value }));
|
|
37
|
+
}
|
|
38
|
+
if (svg === null)
|
|
39
|
+
return null;
|
|
40
|
+
return (_jsx("div", { className: "h-40 w-40",
|
|
41
|
+
// qrcode's own SVG string output, not user input — safe to inline
|
|
42
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: qrcode-generated SVG, no user input
|
|
43
|
+
dangerouslySetInnerHTML: { __html: svg } }));
|
|
44
|
+
}
|
|
45
|
+
// One-time secret reveal (fw#2548, secretMint confirm phase) — monospaced
|
|
46
|
+
// per-value display with a copy button that flips to `copiedLabel` on
|
|
47
|
+
// success. Silent-catch on clipboard error mirrors the copy-link pattern in
|
|
48
|
+
// create-app.tsx.
|
|
49
|
+
export function DefaultSecretReveal({ values, copyLabel, copiedLabel, testId, }) {
|
|
50
|
+
const [copiedIndex, setCopiedIndex] = useState(null);
|
|
51
|
+
// Labels are not unique per reveal; suffix repeats so React keys stay distinct.
|
|
52
|
+
const seenLabels = new Map();
|
|
53
|
+
const rowKeys = values.map((v) => {
|
|
54
|
+
const occurrence = seenLabels.get(v.label) ?? 0;
|
|
55
|
+
seenLabels.set(v.label, occurrence + 1);
|
|
56
|
+
return `${v.label}#${occurrence}`;
|
|
57
|
+
});
|
|
58
|
+
return (_jsx("div", { "data-testid": testId, className: "flex flex-col gap-3", children: values.map((v, i) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx("span", { className: "text-sm font-medium text-muted-foreground", children: v.label }), _jsxs("div", { className: "flex items-start gap-2", children: [v.qr === true ? (_jsx(QrSecretValue, { value: v.value })) : v.multiline ? (_jsx("pre", { className: "flex-1 overflow-x-auto whitespace-pre-wrap break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: v.value })) : (_jsx("code", { className: "flex-1 overflow-x-auto whitespace-pre-wrap break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: v.value })), v.copyable && (_jsx(UiButton, { type: "button", variant: "outline", size: "sm", onClick: async () => {
|
|
59
|
+
try {
|
|
60
|
+
await navigator.clipboard.writeText(v.value);
|
|
61
|
+
setCopiedIndex(i);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// clipboard blocked (non-secure context) — no fallback UI needed here
|
|
65
|
+
}
|
|
66
|
+
}, children: copiedIndex === i ? copiedLabel : copyLabel }))] })] }, rowKeys[i]))) }));
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.385.0",
|
|
4
4
|
"description": "React renderer for Kumiko screens. The root export holds the platform-agnostic logic (primitives contract, hooks, KumikoScreen, navigation and SSE abstractions); ./headless the DOM-free UI logic, ./live the HTTP dispatcher, ./web the browser bindings (HTML primitives, history navigation, EventSource live events, createKumikoApp).",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
52
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
51
|
+
"@cosmicdrift/kumiko-framework": "0.385.0",
|
|
52
|
+
"@cosmicdrift/kumiko-types": "0.385.0",
|
|
53
53
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
54
54
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
55
55
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
96
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
95
|
+
"@cosmicdrift/kumiko-locale-de": "0.385.0",
|
|
96
|
+
"@cosmicdrift/kumiko-testing": "0.385.0",
|
|
97
97
|
"@tailwindcss/cli": "^4.3.0",
|
|
98
98
|
"@testing-library/react": "^16.3.2",
|
|
99
99
|
"@testing-library/user-event": "^14.6.1",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.385.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "View-model inputs take only the entity parts they read",
|
|
6
|
+
"detail": "ComputeEditViewModelInput.entity and ComputeListViewModelInput.entity are typed as the Pick of EntityDefinition they actually read, so synthesized entities need no cast. Passing a full EntityDefinition keeps working. Renderer console output goes through one dev-console module."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.384.0",
|
|
4
10
|
"type": "fix",
|