@xemahq/ui-kernel 0.11.1 → 0.11.2
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/package.json +1 -2
- package/src/index.ts +0 -17
- package/src/lib/biome-host/agent-validation.ts +0 -239
- package/src/lib/biome-host/biome-builders.ts +0 -112
- package/src/lib/biome-host/biome-mode.ts +0 -6
- package/src/lib/biome-host/biome-navigation.ts +0 -37
- package/src/lib/biome-host/biome-registry.ts +0 -299
- package/src/lib/biome-host/biome-scope-hooks.ts +0 -41
- package/src/lib/biome-host/biome-scope.tsx +0 -52
- package/src/lib/biome-host/biome-scoped-query.ts +0 -134
- package/src/lib/biome-host/create-biome-orval-config.ts +0 -82
- package/src/lib/biome-host/define-web-biome.spec.ts +0 -221
- package/src/lib/biome-host/define-web-biome.ts +0 -222
- package/src/lib/biome-host/errors.ts +0 -220
- package/src/lib/biome-host/frontend-biome.ts +0 -256
- package/src/lib/biome-host/host-bridge.ts +0 -281
- package/src/lib/biome-host/host-sources.ts +0 -54
- package/src/lib/biome-host/i18n-hooks.ts +0 -51
- package/src/lib/biome-host/i18n-port.test.ts +0 -50
- package/src/lib/biome-host/i18n-port.ts +0 -96
- package/src/lib/biome-host/index.ts +0 -35
- package/src/lib/biome-host/realtime-hooks.ts +0 -75
- package/src/lib/biome-host/realtime-port.ts +0 -115
- package/src/lib/biome-host/session-contributions.ts +0 -296
- package/src/lib/biome-host/use-mutation-with-error-toast.ts +0 -90
- package/src/lib/biome-host/use-page-state.ts +0 -248
- package/src/lib/capabilities/capability-context.ts +0 -25
- package/src/lib/capabilities/capability-provider.tsx +0 -73
- package/src/lib/capabilities/index.ts +0 -17
- package/src/lib/capabilities/types.ts +0 -69
- package/src/lib/capabilities/use-capability.ts +0 -79
- package/src/lib/contribution-registry/contribution-registry.spec.ts +0 -141
- package/src/lib/contribution-registry/contribution-registry.ts +0 -204
- package/src/lib/contribution-registry/index.ts +0 -9
- package/src/lib/contribution-registry/renderable-registry.ts +0 -142
- package/src/lib/system-bus/capability-invoke.ts +0 -92
- package/src/lib/system-bus/deeplink.ts +0 -210
- package/src/lib/system-bus/enums.ts +0 -86
- package/src/lib/system-bus/host-ports.ts +0 -93
- package/src/lib/system-bus/index.ts +0 -16
- package/src/lib/system-bus/intent-registry.ts +0 -109
- package/src/lib/system-bus/intents.ts +0 -113
- package/src/lib/system-bus/palette.ts +0 -78
- package/src/lib/system-bus/system-bus-builder.ts +0 -159
- package/src/lib/system-bus/system-bus.ts +0 -37
- package/src/lib/system-bus/windows.ts +0 -51
- package/src/offline/id.ts +0 -16
- package/src/offline/index.ts +0 -54
- package/src/offline/mutation.ts +0 -75
- package/src/offline/offline-status-badge.tsx +0 -51
- package/src/offline/online-controller.ts +0 -105
- package/src/offline/outbox.test.ts +0 -172
- package/src/offline/outbox.ts +0 -348
- package/src/offline/react-hooks.ts +0 -40
- package/src/offline/react.tsx +0 -32
- package/src/offline/storage.ts +0 -112
- package/src/offline/types.ts +0 -77
- package/src/org-db/components/DbResultTable.tsx +0 -176
- package/src/org-db/index.ts +0 -1
- package/src/pdf/index.ts +0 -11
- package/src/registry/index.ts +0 -9
- package/src/registry/lib/agent-validation-host.ts +0 -36
- package/src/registry/lib/biome-slot.tsx +0 -50
- package/src/registry/lib/biomes-enabled-context.ts +0 -20
- package/src/registry/lib/extension-points.ts +0 -134
- package/src/registry/lib/primitives/SessionEventCard.tsx +0 -164
- package/src/registry/lib/primitives/SessionEventTimeline.tsx +0 -95
- package/src/registry/lib/primitives/SessionMutationBar.tsx +0 -76
- package/src/registry/lib/primitives/SessionTabbedDrawer.tsx +0 -165
- package/src/registry/lib/primitives/index.ts +0 -18
- package/src/registry/lib/session-context-builder.ts +0 -84
- package/src/registry/lib/session-context-provider.tsx +0 -33
- package/src/registry/lib/session-context.ts +0 -21
- package/src/registry/lib/session-selectors.ts +0 -272
- package/src/session/comments/CommentRail.tsx +0 -164
- package/src/session/index.ts +0 -3
- package/src/session/lib/cn.ts +0 -11
- package/src/session/shell/SessionWorkspaceShell.tsx +0 -147
- package/src/session-kit/approvals/ApprovalButton.tsx +0 -89
- package/src/session-kit/approvals/ApprovalCard.tsx +0 -341
- package/src/session-kit/approvals/ApprovalsCenter.tsx +0 -336
- package/src/session-kit/approvals/CapabilityApprovalStickyBar.tsx +0 -118
- package/src/session-kit/approvals/Obligation.tsx +0 -111
- package/src/session-kit/approvals/ScopedBody.tsx +0 -392
- package/src/session-kit/approvals/approval-icons.ts +0 -31
- package/src/session-kit/approvals/approval-model.ts +0 -204
- package/src/session-kit/approvals/index.ts +0 -22
- package/src/session-kit/approvals/obligation-display.ts +0 -100
- package/src/session-kit/approvals/risk-accent.ts +0 -47
- package/src/session-kit/approvals/scope-icons.ts +0 -19
- package/src/session-kit/combobox/Combobox.tsx +0 -358
- package/src/session-kit/combobox/use-click-outside.ts +0 -21
- package/src/session-kit/display/ContextHeader.tsx +0 -148
- package/src/session-kit/display/FileDiffCard.tsx +0 -140
- package/src/session-kit/display/MD.tsx +0 -162
- package/src/session-kit/display/MessageTurn.tsx +0 -157
- package/src/session-kit/display/ThinkingPanel.tsx +0 -81
- package/src/session-kit/display/TodoChecklist.tsx +0 -120
- package/src/session-kit/display/TokenMeter.tsx +0 -89
- package/src/session-kit/display/ToolStrip.tsx +0 -278
- package/src/session-kit/display/TypingDots.tsx +0 -24
- package/src/session-kit/index.ts +0 -44
- package/src/session-kit/lib/enums.ts +0 -66
- package/src/session-kit/lib/portal-accent.ts +0 -30
- package/src/session-kit/lib/status-dot.ts +0 -68
- package/src/session-kit/primitives/Avatar.tsx +0 -44
- package/src/session-kit/primitives/PortalGlyph.tsx +0 -51
- package/src/session-kit/primitives/RiskPill.tsx +0 -95
- package/src/session-kit/primitives/ScopeDot.tsx +0 -47
- package/src/session-kit/primitives/Segmented.tsx +0 -71
- package/src/ui/branding/app-branding-context.ts +0 -20
- package/src/ui/branding/app-branding-provider.tsx +0 -123
- package/src/ui/branding/sanitize.ts +0 -89
- package/src/ui/breakpoints.ts +0 -14
- package/src/ui/capture/document-preview.tsx +0 -166
- package/src/ui/capture/field-spec-to-zod.test.ts +0 -81
- package/src/ui/capture/field-spec-to-zod.ts +0 -174
- package/src/ui/capture/field-spec-validate.ts +0 -236
- package/src/ui/capture/field-spec.test.ts +0 -49
- package/src/ui/capture/field-spec.ts +0 -274
- package/src/ui/capture/index.ts +0 -51
- package/src/ui/capture/schema-field-input.tsx +0 -620
- package/src/ui/capture/schema-form.tsx +0 -149
- package/src/ui/capture/submit-values.test.ts +0 -70
- package/src/ui/capture/submit-values.ts +0 -63
- package/src/ui/capture/use-prefill.ts +0 -186
- package/src/ui/capture/use-schema-validation.ts +0 -145
- package/src/ui/capture/use-wizard.ts +0 -180
- package/src/ui/capture/value-format.ts +0 -32
- package/src/ui/capture/wizard.tsx +0 -105
- package/src/ui/charts/bar-chart.tsx +0 -139
- package/src/ui/charts/chart-frame.tsx +0 -125
- package/src/ui/charts/chart-types.ts +0 -52
- package/src/ui/charts/chart-utils.ts +0 -190
- package/src/ui/charts/charts.test.tsx +0 -204
- package/src/ui/charts/donut-chart.tsx +0 -129
- package/src/ui/charts/index.ts +0 -27
- package/src/ui/charts/line-chart.tsx +0 -202
- package/src/ui/charts/sparkline.tsx +0 -109
- package/src/ui/chrome/AsyncBoundary.tsx +0 -74
- package/src/ui/chrome/EmptyState.tsx +0 -257
- package/src/ui/chrome/ErrorCard.tsx +0 -63
- package/src/ui/chrome/LoadingState.tsx +0 -61
- package/src/ui/chrome/PageHeader.tsx +0 -137
- package/src/ui/chrome/RecordCardList.tsx +0 -98
- package/src/ui/chrome/StateCard.tsx +0 -150
- package/src/ui/chrome/confirm-delete-dialog.tsx +0 -59
- package/src/ui/chrome/page-shell.spec.tsx +0 -117
- package/src/ui/chrome/page-shell.tsx +0 -221
- package/src/ui/chrome/scope-badge.tsx +0 -40
- package/src/ui/chrome/status-badge.tsx +0 -79
- package/src/ui/cn.ts +0 -32
- package/src/ui/design-tokens.ts +0 -372
- package/src/ui/hooks/use-debounced-value.ts +0 -16
- package/src/ui/hooks/use-is-mobile.ts +0 -28
- package/src/ui/index.ts +0 -110
- package/src/ui/pdf/pdf-preview.tsx +0 -122
- package/src/ui/primitives/alert-dialog.tsx +0 -104
- package/src/ui/primitives/async-combobox.tsx +0 -182
- package/src/ui/primitives/badge-variants.ts +0 -21
- package/src/ui/primitives/badge.tsx +0 -25
- package/src/ui/primitives/button-variants.ts +0 -31
- package/src/ui/primitives/button.tsx +0 -29
- package/src/ui/primitives/card.tsx +0 -43
- package/src/ui/primitives/checkbox.tsx +0 -26
- package/src/ui/primitives/collapsible.tsx +0 -9
- package/src/ui/primitives/dialog.tsx +0 -103
- package/src/ui/primitives/dropdown-menu.tsx +0 -179
- package/src/ui/primitives/form-stepper.tsx +0 -94
- package/src/ui/primitives/form-stepper.types.ts +0 -15
- package/src/ui/primitives/input.tsx +0 -22
- package/src/ui/primitives/label.tsx +0 -17
- package/src/ui/primitives/overflow-tabs.tsx +0 -331
- package/src/ui/primitives/popover.tsx +0 -33
- package/src/ui/primitives/preview-device-frame.tsx +0 -213
- package/src/ui/primitives/radio-group.tsx +0 -36
- package/src/ui/primitives/resizable.tsx +0 -67
- package/src/ui/primitives/scroll-area.tsx +0 -38
- package/src/ui/primitives/select.tsx +0 -143
- package/src/ui/primitives/separator.tsx +0 -20
- package/src/ui/primitives/sheet.tsx +0 -107
- package/src/ui/primitives/skeleton.tsx +0 -99
- package/src/ui/primitives/switch.tsx +0 -27
- package/src/ui/primitives/table.tsx +0 -72
- package/src/ui/primitives/tabs.tsx +0 -53
- package/src/ui/primitives/tag-multi-select.tsx +0 -261
- package/src/ui/primitives/textarea.tsx +0 -21
- package/src/ui/primitives/tooltip.tsx +0 -30
- package/src/ui/primitives/url-preview-chrome.tsx +0 -103
- package/src/vista/GenericCatalogueSurface.tsx +0 -179
- package/src/vista/SurfaceFallbackCard.tsx +0 -109
- package/src/vista/VistaPanel.tsx +0 -163
- package/src/vista/form-payload.spec.ts +0 -139
- package/src/vista/html/CustomUiFrame.spec.tsx +0 -127
- package/src/vista/html/CustomUiFrame.tsx +0 -206
- package/src/vista/html/custom-ui-frame-bridge.spec.ts +0 -191
- package/src/vista/html/custom-ui-frame-bridge.ts +0 -98
- package/src/vista/html/custom-ui-frame-contract.ts +0 -88
- package/src/vista/html/custom-ui-intents.spec.ts +0 -135
- package/src/vista/html/custom-ui-intents.ts +0 -155
- package/src/vista/html/custom-ui-srcdoc.spec.ts +0 -151
- package/src/vista/html/custom-ui-srcdoc.ts +0 -193
- package/src/vista/html/index.ts +0 -13
- package/src/vista/index.ts +0 -52
- package/src/vista/surface-catalogue.spec.ts +0 -140
- package/src/vista/surface-catalogue.ts +0 -90
- package/src/vista/surface-icons.ts +0 -57
- package/src/vista/surface-registry.ts +0 -56
- package/src/vista/surfaces/ChartSurface.tsx +0 -34
- package/src/vista/surfaces/FilesSurface.tsx +0 -202
- package/src/vista/surfaces/FormSurface.tsx +0 -380
- package/src/vista/surfaces/HtmlSurface.tsx +0 -43
- package/src/vista/surfaces/TableSurface.tsx +0 -100
- package/src/vista/surfaces/UrlSurface.tsx +0 -40
- package/src/vista/surfaces/blob-surfaces.tsx +0 -139
- package/src/vista/surfaces/elicitation-transport.spec.ts +0 -369
- package/src/vista/surfaces/elicitation-transport.ts +0 -367
- package/src/vista/surfaces/file-surface-utils.ts +0 -69
- package/src/vista/surfaces/index.ts +0 -24
- package/src/vista/surfaces/register-surfaces.tsx +0 -139
- package/src/vista/surfaces/text-surfaces.tsx +0 -132
- package/src/vista/use-vista-tabs.ts +0 -196
- package/src/vista/vista-code-renderer-context.ts +0 -16
- package/src/vista/vista-code-renderer.tsx +0 -35
- package/src/vista/vista-file-context.ts +0 -14
- package/src/vista/vista-file-source.tsx +0 -141
- package/src/vista/vista-form-context.ts +0 -20
- package/src/vista/vista-form-host.tsx +0 -48
- package/src/vista/vista-model.ts +0 -137
- package/src/vista/vista-open-context.tsx +0 -27
- package/src/vista/vista-open-state.ts +0 -11
- package/src/vista/vista-payloads.ts +0 -635
- package/src/vista/vista-tabs-manifest.ts +0 -138
- package/src/vista/vista-tools.ts +0 -232
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { Loader2 } from 'lucide-react';
|
|
2
|
-
|
|
3
|
-
import { cn } from '../cn';
|
|
4
|
-
import { Skeleton } from '../primitives/skeleton';
|
|
5
|
-
|
|
6
|
-
export type LoadingStateVariant = 'spinner' | 'skeleton-list' | 'skeleton-card';
|
|
7
|
-
|
|
8
|
-
interface LoadingStateProps {
|
|
9
|
-
/**
|
|
10
|
-
* `spinner` (default): centred spinner + optional label. `skeleton-list`:
|
|
11
|
-
* N skeleton rows. `skeleton-card`: a single card-shaped skeleton block.
|
|
12
|
-
*/
|
|
13
|
-
variant?: LoadingStateVariant;
|
|
14
|
-
/** Accessible label / caption (spinner variant). */
|
|
15
|
-
label?: string;
|
|
16
|
-
/** Row count for `skeleton-list` (default 4). */
|
|
17
|
-
rows?: number;
|
|
18
|
-
className?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Canonical loading affordance shared across biomes. Replaces the per-biome
|
|
23
|
-
* ad-hoc spinners and inline skeleton stacks.
|
|
24
|
-
*/
|
|
25
|
-
export default function LoadingState({
|
|
26
|
-
variant = 'spinner',
|
|
27
|
-
label = 'Loading…',
|
|
28
|
-
rows = 4,
|
|
29
|
-
className,
|
|
30
|
-
}: Readonly<LoadingStateProps>) {
|
|
31
|
-
if (variant === 'skeleton-list') {
|
|
32
|
-
return (
|
|
33
|
-
<div className={cn('space-y-3', className)} role="status" aria-label={label}>
|
|
34
|
-
{Array.from({ length: rows }).map((_, i) => (
|
|
35
|
-
<Skeleton key={i} className="h-12 w-full rounded-xl" />
|
|
36
|
-
))}
|
|
37
|
-
</div>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (variant === 'skeleton-card') {
|
|
42
|
-
return (
|
|
43
|
-
<div className={cn('space-y-3', className)} role="status" aria-label={label}>
|
|
44
|
-
<Skeleton className="h-40 w-full rounded-2xl" />
|
|
45
|
-
<Skeleton className="h-4 w-2/3 rounded" />
|
|
46
|
-
<Skeleton className="h-4 w-1/2 rounded" />
|
|
47
|
-
</div>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<div
|
|
53
|
-
className={cn('flex flex-col items-center justify-center py-14 text-center', className)}
|
|
54
|
-
role="status"
|
|
55
|
-
aria-label={label}
|
|
56
|
-
>
|
|
57
|
-
<Loader2 className="h-6 w-6 animate-spin text-ink-3" />
|
|
58
|
-
{label && <p className="mt-3 text-body-2 text-ink-3">{label}</p>}
|
|
59
|
-
</div>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// ── PageHeader — INVISIBLE topbar registration ──
|
|
3
|
-
//
|
|
4
|
-
// PageHeader does NOT render a visible header bar. It registers the page's
|
|
5
|
-
// title, description, eyebrow, help-actions, status pill, count, and
|
|
6
|
-
// primary action buttons into the shared page-meta store. The global
|
|
7
|
-
// `AppTopbar` reads from that store and renders them in the topbar
|
|
8
|
-
// (mac-style — the topbar IS the page chrome).
|
|
9
|
-
//
|
|
10
|
-
// Why invisible: stacking a second header row beneath the topbar wastes
|
|
11
|
-
// vertical space and competes with the topbar for attention. Pages get
|
|
12
|
-
// the canvas back; the topbar carries context.
|
|
13
|
-
//
|
|
14
|
-
// When you need a thin in-page row (filter dropdowns, search input,
|
|
15
|
-
// selection-mode toolbar), use `<PageToolbar>` from this same file —
|
|
16
|
-
// that's a real visible row but carries NO title (the topbar still owns
|
|
17
|
-
// it), so there's no visual duplication.
|
|
18
|
-
//
|
|
19
|
-
// Host-agnostic: reaches the page-meta store through the kernel `HostBridge`
|
|
20
|
-
// singleton (NOT a host import), so it bundles cleanly into any biome remote.
|
|
21
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
-
|
|
23
|
-
import { useMemo, type ReactNode } from 'react';
|
|
24
|
-
|
|
25
|
-
import { useHostBridge, type PageBackTarget } from '../../lib/biome-host';
|
|
26
|
-
import { cn } from '../cn';
|
|
27
|
-
|
|
28
|
-
interface PageHeaderProps {
|
|
29
|
-
readonly title: string;
|
|
30
|
-
/**
|
|
31
|
-
* Short secondary line. Renders in the topbar info popover (long-form
|
|
32
|
-
* `topbarDescription` wins for the popover if both are set) AND
|
|
33
|
-
* inline next to the title as `topbarMeta` when it's a string.
|
|
34
|
-
*/
|
|
35
|
-
readonly description?: string;
|
|
36
|
-
/**
|
|
37
|
-
* Long-form text rendered ONLY in the topbar's info popover (the
|
|
38
|
-
* (i) button). Use when `description` is a counts line and you still
|
|
39
|
-
* want a richer popover blurb.
|
|
40
|
-
*/
|
|
41
|
-
readonly topbarDescription?: string;
|
|
42
|
-
/** Mono caption rendered before the title in the topbar info popover. */
|
|
43
|
-
readonly eyebrow?: string;
|
|
44
|
-
/** Bullet list of "what you can do here" shown in the topbar info popover. */
|
|
45
|
-
readonly helpActions?: readonly string[];
|
|
46
|
-
/**
|
|
47
|
-
* Right-aligned topbar action cluster. Use small h-7 buttons / icon
|
|
48
|
-
* buttons. Anything that needs more space (multi-input filter rows,
|
|
49
|
-
* search inputs) belongs in a `<PageToolbar>` inside the page body
|
|
50
|
-
* — not here.
|
|
51
|
-
*/
|
|
52
|
-
readonly actions?: ReactNode;
|
|
53
|
-
/**
|
|
54
|
-
* Leftmost back affordance. Renders as an icon-only button before
|
|
55
|
-
* the title in the topbar. Pages with a parent context (a detail
|
|
56
|
-
* page under a list, etc.) set this so the user can return to where
|
|
57
|
-
* they came from. See `app/page-meta.ts` for the full layout
|
|
58
|
-
* contract — back always lives on the left, never inside the page
|
|
59
|
-
* body.
|
|
60
|
-
*/
|
|
61
|
-
readonly backTo?: PageBackTarget;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Invisible primitive — registers page meta with the topbar. Renders
|
|
66
|
-
* nothing visible. Mount once at the top of a page component.
|
|
67
|
-
*/
|
|
68
|
-
export default function PageHeader({
|
|
69
|
-
title,
|
|
70
|
-
description,
|
|
71
|
-
topbarDescription,
|
|
72
|
-
eyebrow,
|
|
73
|
-
helpActions,
|
|
74
|
-
actions,
|
|
75
|
-
backTo,
|
|
76
|
-
}: Readonly<PageHeaderProps>) {
|
|
77
|
-
// `topbarActions` is a ReactNode — wrap in useMemo so React doesn't see
|
|
78
|
-
// a new identity every render and re-notify the store needlessly. Same
|
|
79
|
-
// for the meta line.
|
|
80
|
-
const topbarActions = useMemo(() => actions, [actions]);
|
|
81
|
-
const topbarMeta = useMemo<ReactNode>(
|
|
82
|
-
() => (description ? <span>{description}</span> : null),
|
|
83
|
-
[description],
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
// Reach the host page-meta store through the shared bridge singleton — NOT
|
|
87
|
-
// a module-level import — so this component bundles cleanly into an MF
|
|
88
|
-
// remote and still drives the one host topbar store.
|
|
89
|
-
useHostBridge().pageMeta.usePageMeta({
|
|
90
|
-
title,
|
|
91
|
-
eyebrow,
|
|
92
|
-
description: topbarDescription ?? description,
|
|
93
|
-
actions: helpActions,
|
|
94
|
-
topbarActions,
|
|
95
|
-
topbarMeta,
|
|
96
|
-
backTo,
|
|
97
|
-
});
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
102
|
-
// ── PageToolbar — thin in-page row for filters / search / sub-tabs ──
|
|
103
|
-
//
|
|
104
|
-
// Use when the page needs a horizontal row of controls that don't fit
|
|
105
|
-
// in the topbar (multi-select filter, search input, bulk-selection
|
|
106
|
-
// toolbar). NEVER include the page title — that's the topbar's job.
|
|
107
|
-
//
|
|
108
|
-
// Default height is `h-9` to match the topbar visual rhythm without
|
|
109
|
-
// looking like a duplicated header. Background is the same paper tone
|
|
110
|
-
// as the page so it reads as a tool strip, not a banner.
|
|
111
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
112
|
-
|
|
113
|
-
interface PageToolbarProps {
|
|
114
|
-
/** Left cluster — filters, search, etc. */
|
|
115
|
-
readonly children?: ReactNode;
|
|
116
|
-
/** Right cluster — usually `null`; the primary action lives in the topbar. */
|
|
117
|
-
readonly trailing?: ReactNode;
|
|
118
|
-
readonly className?: string;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function PageToolbar({
|
|
122
|
-
children,
|
|
123
|
-
trailing,
|
|
124
|
-
className,
|
|
125
|
-
}: Readonly<PageToolbarProps>) {
|
|
126
|
-
return (
|
|
127
|
-
<div
|
|
128
|
-
className={cn(
|
|
129
|
-
'flex shrink-0 items-center gap-2 border-b border-rule/50 bg-paper px-4 py-1.5 sm:px-6',
|
|
130
|
-
className,
|
|
131
|
-
)}
|
|
132
|
-
>
|
|
133
|
-
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">{children}</div>
|
|
134
|
-
{trailing && <div className="flex shrink-0 items-center gap-1.5">{trailing}</div>}
|
|
135
|
-
</div>
|
|
136
|
-
);
|
|
137
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { cn } from '../cn';
|
|
2
|
-
|
|
3
|
-
import type { ReactNode } from 'react';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* One label/value row inside a record card — mirrors one table column.
|
|
7
|
-
*
|
|
8
|
-
* `key` exists because a card's rows are keyed by the COLUMN, and a column's
|
|
9
|
-
* identity is its field key, not its user-facing label (two columns may carry
|
|
10
|
-
* the same label). It defaults to `label` for the common case where the label
|
|
11
|
-
* is already unique.
|
|
12
|
-
*/
|
|
13
|
-
export interface RecordCardField {
|
|
14
|
-
readonly key?: string;
|
|
15
|
-
readonly label: string;
|
|
16
|
-
readonly value: ReactNode;
|
|
17
|
-
/**
|
|
18
|
-
* `data-*` attributes for the row element — e.g. the same hook the equivalent
|
|
19
|
-
* table cell carries, so a click-to-context resolver lands on the identical
|
|
20
|
-
* node at every viewport width. Deliberately `data-*` only: styling stays in
|
|
21
|
-
* this primitive, so a caller cannot re-open the drift it closed.
|
|
22
|
-
*/
|
|
23
|
-
readonly rowAttributes?: Readonly<Record<`data-${string}`, string>>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface RecordCardListProps<T> {
|
|
27
|
-
readonly items: readonly T[];
|
|
28
|
-
readonly getKey: (item: T) => string;
|
|
29
|
-
/** Card header rendered above the field list (title, status, row toggle). */
|
|
30
|
-
readonly renderHeader?: (item: T) => ReactNode;
|
|
31
|
-
readonly getFields: (item: T) => readonly RecordCardField[];
|
|
32
|
-
/** Row actions, rendered right-aligned below a divider. */
|
|
33
|
-
readonly renderActions?: (item: T) => ReactNode;
|
|
34
|
-
/** Free block appended after the actions (expanded detail). */
|
|
35
|
-
readonly renderDetail?: (item: T) => ReactNode;
|
|
36
|
-
/** Extra classes for the list element. */
|
|
37
|
-
readonly className?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Small-viewport (below `md`) layout for a record list: one stacked card per
|
|
42
|
-
* record with the table's columns rendered as label/value rows, instead of a
|
|
43
|
-
* horizontal-scroll-only table.
|
|
44
|
-
*
|
|
45
|
-
* This is the ONE definition of that card chrome — spacing, border, divider,
|
|
46
|
-
* label/value alignment. Callers name their columns and their actions; they do
|
|
47
|
-
* not restate the chrome. Deliberately NOT parameterised by card class: a
|
|
48
|
-
* per-caller class hook is how the three copies this replaced drifted apart in
|
|
49
|
-
* the first place.
|
|
50
|
-
*/
|
|
51
|
-
export function RecordCardList<T>(props: Readonly<RecordCardListProps<T>>) {
|
|
52
|
-
const {
|
|
53
|
-
items,
|
|
54
|
-
getKey,
|
|
55
|
-
renderHeader,
|
|
56
|
-
getFields,
|
|
57
|
-
renderActions,
|
|
58
|
-
renderDetail,
|
|
59
|
-
className,
|
|
60
|
-
} = props;
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<ul className={cn('space-y-2', className)}>
|
|
64
|
-
{items.map((item) => {
|
|
65
|
-
const actions = renderActions?.(item);
|
|
66
|
-
const detail = renderDetail?.(item);
|
|
67
|
-
return (
|
|
68
|
-
<li
|
|
69
|
-
key={getKey(item)}
|
|
70
|
-
className="space-y-2 rounded-lg border border-rule bg-paper-elev p-3"
|
|
71
|
-
>
|
|
72
|
-
{renderHeader?.(item)}
|
|
73
|
-
<dl className="space-y-1.5">
|
|
74
|
-
{getFields(item).map((field) => (
|
|
75
|
-
<div
|
|
76
|
-
key={field.key ?? field.label}
|
|
77
|
-
{...field.rowAttributes}
|
|
78
|
-
className="flex justify-between gap-3 text-sm"
|
|
79
|
-
>
|
|
80
|
-
<dt className="shrink-0 text-ink-3">{field.label}</dt>
|
|
81
|
-
<dd className="text-right text-ink-2">{field.value}</dd>
|
|
82
|
-
</div>
|
|
83
|
-
))}
|
|
84
|
-
</dl>
|
|
85
|
-
{actions ? (
|
|
86
|
-
<div className="flex items-center justify-end gap-2 border-t border-rule/50 pt-2">
|
|
87
|
-
{actions}
|
|
88
|
-
</div>
|
|
89
|
-
) : null}
|
|
90
|
-
{detail}
|
|
91
|
-
</li>
|
|
92
|
-
);
|
|
93
|
-
})}
|
|
94
|
-
</ul>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export default RecordCardList;
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified state display card for empty, error-simple, and error-detailed states.
|
|
3
|
-
* Replaces ErrorCard, ErrorDisplay, and EmptyState components.
|
|
4
|
-
* Supports collapsible error details and structured field extraction.
|
|
5
|
-
*/
|
|
6
|
-
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
7
|
-
import React, { useState } from 'react';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { cn } from '../cn';
|
|
11
|
-
|
|
12
|
-
interface FieldEntry {
|
|
13
|
-
label: string;
|
|
14
|
-
value: string | React.ReactNode;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface StateCardProps {
|
|
18
|
-
/** State variant: 'empty' | 'error-simple' | 'error-detailed' */
|
|
19
|
-
variant: 'empty' | 'error-simple' | 'error-detailed';
|
|
20
|
-
/** Icon React element */
|
|
21
|
-
icon: React.ReactNode;
|
|
22
|
-
/** Primary title */
|
|
23
|
-
title: string;
|
|
24
|
-
/** Description or error message */
|
|
25
|
-
description?: string;
|
|
26
|
-
/** Optional action button */
|
|
27
|
-
action?: {
|
|
28
|
-
label: string;
|
|
29
|
-
onClick: () => void;
|
|
30
|
-
};
|
|
31
|
-
/** Optional error details (for error-detailed variant) */
|
|
32
|
-
errorDetails?: {
|
|
33
|
-
fields?: FieldEntry[];
|
|
34
|
-
stackTrace?: string;
|
|
35
|
-
requestId?: string;
|
|
36
|
-
};
|
|
37
|
-
/** Optional CSS class */
|
|
38
|
-
className?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Renders a state card (empty/error) with consistent appearance.
|
|
43
|
-
* Supports collapsible error details for debugging.
|
|
44
|
-
*/
|
|
45
|
-
export const StateCard: React.FC<StateCardProps> = ({
|
|
46
|
-
variant,
|
|
47
|
-
icon,
|
|
48
|
-
title,
|
|
49
|
-
description,
|
|
50
|
-
action,
|
|
51
|
-
errorDetails,
|
|
52
|
-
className = '',
|
|
53
|
-
}) => {
|
|
54
|
-
const [detailsExpanded, setDetailsExpanded] = useState(false);
|
|
55
|
-
|
|
56
|
-
const isError = variant.startsWith('error');
|
|
57
|
-
const isDetailed = variant === 'error-detailed';
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<div className={cn(
|
|
61
|
-
'flex flex-col items-center justify-center py-14 px-6 rounded-xl border-2',
|
|
62
|
-
isError ? 'border-destructive/20 bg-destructive/[0.03]' : 'border-dashed border-border/50 bg-paper-elev/60',
|
|
63
|
-
className,
|
|
64
|
-
)}>
|
|
65
|
-
<div className={cn(
|
|
66
|
-
'h-14 w-14 rounded-2xl flex items-center justify-center mb-5',
|
|
67
|
-
isError ? 'bg-destructive/10' : 'bg-primary/10',
|
|
68
|
-
)}>
|
|
69
|
-
<span className={isError ? 'text-destructive' : 'text-primary'}>{icon}</span>
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
<h2 className="text-subtitle font-semibold text-ink mb-2 text-center">
|
|
73
|
-
{title}
|
|
74
|
-
</h2>
|
|
75
|
-
|
|
76
|
-
{description && (
|
|
77
|
-
<p className="text-body-1 text-ink-3 max-w-md text-center mb-4 leading-relaxed">
|
|
78
|
-
{description}
|
|
79
|
-
</p>
|
|
80
|
-
)}
|
|
81
|
-
|
|
82
|
-
{/* Error details section */}
|
|
83
|
-
{isDetailed && errorDetails && (
|
|
84
|
-
<div className="mt-4 w-full max-w-md">
|
|
85
|
-
<button
|
|
86
|
-
type="button"
|
|
87
|
-
onClick={() => setDetailsExpanded(!detailsExpanded)}
|
|
88
|
-
className="text-body-1 font-medium text-destructive hover:text-destructive flex items-center gap-1.5 mb-2"
|
|
89
|
-
>
|
|
90
|
-
{detailsExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
|
91
|
-
Error Details
|
|
92
|
-
</button>
|
|
93
|
-
|
|
94
|
-
{detailsExpanded && (
|
|
95
|
-
<div className="p-4 bg-card border border-destructive/20 rounded-xl space-y-3">
|
|
96
|
-
{/* Structured fields */}
|
|
97
|
-
{errorDetails.fields && errorDetails.fields.length > 0 && (
|
|
98
|
-
<div className="space-y-1.5">
|
|
99
|
-
{errorDetails.fields.map((field, idx) => (
|
|
100
|
-
<div key={idx} className="text-body-1">
|
|
101
|
-
<span className="font-semibold text-destructive">{field.label}</span>
|
|
102
|
-
<span className="text-ink/70">: {field.value}</span>
|
|
103
|
-
</div>
|
|
104
|
-
))}
|
|
105
|
-
</div>
|
|
106
|
-
)}
|
|
107
|
-
|
|
108
|
-
{/* Stack trace */}
|
|
109
|
-
{errorDetails.stackTrace && (
|
|
110
|
-
<details className="text-body-1">
|
|
111
|
-
<summary className="font-medium text-destructive cursor-pointer mb-1">
|
|
112
|
-
Stack Trace
|
|
113
|
-
</summary>
|
|
114
|
-
<pre className="bg-destructive/5 p-3 rounded-lg overflow-auto max-h-32 text-destructive whitespace-pre-wrap break-words text-body-1 font-mono">
|
|
115
|
-
{errorDetails.stackTrace}
|
|
116
|
-
</pre>
|
|
117
|
-
</details>
|
|
118
|
-
)}
|
|
119
|
-
|
|
120
|
-
{/* Request ID */}
|
|
121
|
-
{errorDetails.requestId && (
|
|
122
|
-
<div className="text-body-1 pt-2 border-t border-destructive/15">
|
|
123
|
-
<span className="font-medium text-ink-3">Request ID</span>
|
|
124
|
-
<br />
|
|
125
|
-
<code className="text-body-1 text-ink/70 font-mono break-all">{errorDetails.requestId}</code>
|
|
126
|
-
</div>
|
|
127
|
-
)}
|
|
128
|
-
</div>
|
|
129
|
-
)}
|
|
130
|
-
</div>
|
|
131
|
-
)}
|
|
132
|
-
|
|
133
|
-
{/* Action button */}
|
|
134
|
-
{action && (
|
|
135
|
-
<button
|
|
136
|
-
type="button"
|
|
137
|
-
onClick={action.onClick}
|
|
138
|
-
className={cn(
|
|
139
|
-
'mt-6 px-5 py-2.5 rounded-lg font-medium transition-colors h-10',
|
|
140
|
-
isError
|
|
141
|
-
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
|
142
|
-
: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
143
|
-
)}
|
|
144
|
-
>
|
|
145
|
-
{action.label}
|
|
146
|
-
</button>
|
|
147
|
-
)}
|
|
148
|
-
</div>
|
|
149
|
-
);
|
|
150
|
-
};
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { Loader2 } from 'lucide-react';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
AlertDialog,
|
|
5
|
-
AlertDialogAction,
|
|
6
|
-
AlertDialogCancel,
|
|
7
|
-
AlertDialogContent,
|
|
8
|
-
AlertDialogDescription,
|
|
9
|
-
AlertDialogFooter,
|
|
10
|
-
AlertDialogHeader,
|
|
11
|
-
AlertDialogTitle,
|
|
12
|
-
} from '../primitives/alert-dialog';
|
|
13
|
-
|
|
14
|
-
export interface ConfirmDeleteDialogProps {
|
|
15
|
-
open: boolean;
|
|
16
|
-
onOpenChange: (open: boolean) => void;
|
|
17
|
-
onConfirm: () => void;
|
|
18
|
-
title?: string;
|
|
19
|
-
description?: string;
|
|
20
|
-
isPending?: boolean;
|
|
21
|
-
/** Confirm-button label. Defaults to 'Delete' for the destructive case. */
|
|
22
|
-
confirmLabel?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Reusable delete confirmation dialog.
|
|
27
|
-
* Used across CRUD surfaces to prevent accidental destructive actions.
|
|
28
|
-
*/
|
|
29
|
-
export default function ConfirmDeleteDialog({
|
|
30
|
-
open,
|
|
31
|
-
onOpenChange,
|
|
32
|
-
onConfirm,
|
|
33
|
-
title = 'Delete item',
|
|
34
|
-
description = 'This action cannot be undone. Are you sure you want to continue?',
|
|
35
|
-
isPending,
|
|
36
|
-
confirmLabel = 'Delete',
|
|
37
|
-
}: ConfirmDeleteDialogProps) {
|
|
38
|
-
return (
|
|
39
|
-
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
40
|
-
<AlertDialogContent>
|
|
41
|
-
<AlertDialogHeader>
|
|
42
|
-
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
43
|
-
<AlertDialogDescription>{description}</AlertDialogDescription>
|
|
44
|
-
</AlertDialogHeader>
|
|
45
|
-
<AlertDialogFooter>
|
|
46
|
-
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
|
47
|
-
<AlertDialogAction
|
|
48
|
-
onClick={onConfirm}
|
|
49
|
-
disabled={isPending}
|
|
50
|
-
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
51
|
-
>
|
|
52
|
-
{isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
|
53
|
-
{confirmLabel}
|
|
54
|
-
</AlertDialogAction>
|
|
55
|
-
</AlertDialogFooter>
|
|
56
|
-
</AlertDialogContent>
|
|
57
|
-
</AlertDialog>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An EMBEDDED PageShell must not touch the topbar.
|
|
3
|
-
*
|
|
4
|
-
* ── THE DEFECT ────────────────────────────────────────────────────────────
|
|
5
|
-
*
|
|
6
|
-
* `embedded` means "the host page already owns the topbar — render only my
|
|
7
|
-
* body". But `usePageMeta` was called UNCONDITIONALLY, above the `embedded`
|
|
8
|
-
* check, and the page-meta store is a STACK: "nested pages can push their own
|
|
9
|
-
* meta; on unmount the outer meta is restored".
|
|
10
|
-
*
|
|
11
|
-
* So an embedded shell pushed its own `title`, `description`, `eyebrow`,
|
|
12
|
-
* `backTo` and `topbarActions` OVER its host's — the precise opposite of what
|
|
13
|
-
* embedding means. The host's title and back button silently became the
|
|
14
|
-
* child's for as long as the child was mounted, and the page-meta contract
|
|
15
|
-
* ("never render a back button inside the page body… move it into
|
|
16
|
-
* usePageMeta") made that the ONLY back affordance on screen, so it pointed at
|
|
17
|
-
* the wrong parent.
|
|
18
|
-
*
|
|
19
|
-
* A hook cannot be called conditionally, so the fix is structural: `PageShell`
|
|
20
|
-
* calls no hooks and only chooses between two components. These tests pin the
|
|
21
|
-
* OBSERVABLE property — whether meta is registered — not the file layout, so
|
|
22
|
-
* any future refactor that reintroduces the push fails them.
|
|
23
|
-
*
|
|
24
|
-
* Runs under the host-web vitest mirror (this package carries no vitest dep).
|
|
25
|
-
*/
|
|
26
|
-
import { FileQuestion } from 'lucide-react';
|
|
27
|
-
import { renderToStaticMarkup } from 'react-dom/server';
|
|
28
|
-
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const registered: unknown[] = [];
|
|
32
|
-
|
|
33
|
-
// The kernel reaches the page-meta store through the HostBridge singleton,
|
|
34
|
-
// never a host import — so the bridge is the seam to observe.
|
|
35
|
-
vi.mock('../../lib/biome-host/host-bridge', async () => {
|
|
36
|
-
const actual = await vi.importActual<Record<string, unknown>>(
|
|
37
|
-
'../../lib/biome-host/host-bridge',
|
|
38
|
-
);
|
|
39
|
-
return {
|
|
40
|
-
...actual,
|
|
41
|
-
useHostBridge: () => ({
|
|
42
|
-
pageMeta: {
|
|
43
|
-
usePageMeta: (meta: unknown) => {
|
|
44
|
-
registered.push(meta);
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
}),
|
|
48
|
-
};
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const { default: PageShell } = await import('./page-shell');
|
|
52
|
-
|
|
53
|
-
beforeEach(() => {
|
|
54
|
-
registered.length = 0;
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const META = {
|
|
58
|
-
icon: FileQuestion,
|
|
59
|
-
title: 'Child Title',
|
|
60
|
-
description: 'Child description',
|
|
61
|
-
eyebrow: 'Child eyebrow',
|
|
62
|
-
backTo: { to: '/child-parent', label: 'Child parent' },
|
|
63
|
-
} as const;
|
|
64
|
-
|
|
65
|
-
describe('PageShell page-meta ownership', () => {
|
|
66
|
-
it('registers NOTHING when embedded — the host owns the topbar', () => {
|
|
67
|
-
// THE BUG, directly. Before the fix this registered the child's meta,
|
|
68
|
-
// replacing the host's title and back button on a stack-based store.
|
|
69
|
-
renderToStaticMarkup(
|
|
70
|
-
<PageShell {...META} embedded>
|
|
71
|
-
<p>body</p>
|
|
72
|
-
</PageShell>,
|
|
73
|
-
);
|
|
74
|
-
expect(registered).toEqual([]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('registers its meta when NOT embedded — a full page owns its topbar', () => {
|
|
78
|
-
// The other half: the fix must not silence the normal case.
|
|
79
|
-
renderToStaticMarkup(
|
|
80
|
-
<PageShell {...META}>
|
|
81
|
-
<p>body</p>
|
|
82
|
-
</PageShell>,
|
|
83
|
-
);
|
|
84
|
-
expect(registered).toHaveLength(1);
|
|
85
|
-
expect(registered[0]).toMatchObject({
|
|
86
|
-
title: 'Child Title',
|
|
87
|
-
description: 'Child description',
|
|
88
|
-
eyebrow: 'Child eyebrow',
|
|
89
|
-
backTo: { to: '/child-parent' },
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('still renders the embedded body — not registering is not "render nothing"', () => {
|
|
94
|
-
const html = renderToStaticMarkup(
|
|
95
|
-
<PageShell {...META} embedded>
|
|
96
|
-
<p>embedded-body-marker</p>
|
|
97
|
-
</PageShell>,
|
|
98
|
-
);
|
|
99
|
-
expect(html).toContain('embedded-body-marker');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('renders the embedded sub-tab strip, which is the child’s own chrome', () => {
|
|
103
|
-
// Embedding suppresses the TOPBAR meta, not the child's in-body tabs.
|
|
104
|
-
const html = renderToStaticMarkup(
|
|
105
|
-
<PageShell
|
|
106
|
-
{...META}
|
|
107
|
-
embedded
|
|
108
|
-
tabs={[{ value: 'a', label: 'Alpha' }]}
|
|
109
|
-
activeTab="a"
|
|
110
|
-
>
|
|
111
|
-
<p>body</p>
|
|
112
|
-
</PageShell>,
|
|
113
|
-
);
|
|
114
|
-
expect(html).toContain('Alpha');
|
|
115
|
-
expect(registered).toEqual([]);
|
|
116
|
-
});
|
|
117
|
-
});
|