@calliopelabs/ui 0.7.0 → 0.11.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.
Files changed (120) hide show
  1. package/README.md +253 -3
  2. package/dist/appearance/AppearancePicker.js +4 -2
  3. package/dist/appearance/AppearanceProvider.d.ts +10 -2
  4. package/dist/appearance/AppearanceProvider.js +6 -2
  5. package/dist/appearance/AppearanceScript.d.ts +15 -4
  6. package/dist/appearance/AppearanceScript.js +11 -5
  7. package/dist/appearance/appearance.js +1 -1
  8. package/dist/appearance/boot.d.ts +13 -2
  9. package/dist/appearance/boot.js +35 -5
  10. package/dist/appearance/product.d.ts +13 -0
  11. package/dist/appearance/product.js +2 -2
  12. package/dist/appearance/runtime-brand.d.ts +106 -0
  13. package/dist/appearance/runtime-brand.js +420 -0
  14. package/dist/css/appearance.js +147 -0
  15. package/dist/css/calliope.css +1221 -0
  16. package/dist/css/components.css +729 -0
  17. package/dist/css/tokens.css +491 -0
  18. package/dist/data/DefinitionList.d.ts +16 -0
  19. package/dist/data/DefinitionList.js +9 -0
  20. package/dist/data/Stat.d.ts +25 -0
  21. package/dist/data/Stat.js +19 -0
  22. package/dist/data/Tag.d.ts +21 -0
  23. package/dist/data/Tag.js +18 -0
  24. package/dist/data/types.d.ts +3 -2
  25. package/dist/data/use-local-table.d.ts +36 -0
  26. package/dist/data/use-local-table.js +127 -0
  27. package/dist/index.d.ts +42 -3
  28. package/dist/index.js +39 -1
  29. package/dist/nav/Nav.d.ts +2 -1
  30. package/dist/nav/Nav.js +34 -15
  31. package/dist/nav/model.d.ts +21 -0
  32. package/dist/page/PageHeader.d.ts +5 -3
  33. package/dist/page/PageHeader.js +5 -4
  34. package/dist/page/Panel.d.ts +19 -0
  35. package/dist/page/Panel.js +12 -0
  36. package/dist/primitives/Avatar.d.ts +37 -0
  37. package/dist/primitives/Avatar.js +43 -0
  38. package/dist/primitives/Field.d.ts +33 -0
  39. package/dist/primitives/Field.js +30 -0
  40. package/dist/primitives/Segmented.d.ts +30 -0
  41. package/dist/primitives/Segmented.js +20 -0
  42. package/dist/primitives/Select.d.ts +40 -0
  43. package/dist/primitives/Select.js +47 -0
  44. package/dist/primitives/Switch.d.ts +7 -0
  45. package/dist/primitives/Switch.js +8 -0
  46. package/dist/primitives/Textarea.d.ts +3 -0
  47. package/dist/primitives/Textarea.js +6 -0
  48. package/dist/provider/messages.d.ts +412 -0
  49. package/dist/provider/messages.js +633 -0
  50. package/dist/shell/BrandMark.d.ts +25 -3
  51. package/dist/shell/BrandMark.js +34 -4
  52. package/dist/shell/RailPanel.js +2 -2
  53. package/dist/shell/RailTree.js +2 -2
  54. package/dist/shell/Sidebar.d.ts +9 -2
  55. package/dist/shell/Sidebar.js +10 -4
  56. package/dist/shell/UserMenu.d.ts +3 -1
  57. package/dist/shell/UserMenu.js +3 -11
  58. package/dist/shell/rail-boot.d.ts +28 -0
  59. package/dist/shell/rail-boot.js +45 -0
  60. package/dist/shell/rail-state.d.ts +6 -1
  61. package/dist/shell/rail-state.js +18 -2
  62. package/dist/styles/preset.css +4 -0
  63. package/dist/styles/theme.css +36 -13
  64. package/dist/theme/css-layer.d.ts +19 -0
  65. package/dist/theme/css-layer.js +171 -0
  66. package/dist/theme/css.d.ts +1 -0
  67. package/dist/theme/css.js +45 -9
  68. package/dist/workflow/ArtifactCard.d.ts +47 -0
  69. package/dist/workflow/ArtifactCard.js +45 -0
  70. package/dist/workflow/DocumentReview.d.ts +74 -0
  71. package/dist/workflow/DocumentReview.js +54 -0
  72. package/dist/workflow/FormReview.d.ts +53 -0
  73. package/dist/workflow/FormReview.js +83 -0
  74. package/dist/workflow/GateBadge.d.ts +11 -0
  75. package/dist/workflow/GateBadge.js +14 -0
  76. package/dist/workflow/MediaReview.d.ts +63 -0
  77. package/dist/workflow/MediaReview.js +166 -0
  78. package/dist/workflow/ReviewPanel.d.ts +41 -0
  79. package/dist/workflow/ReviewPanel.js +59 -0
  80. package/dist/workflow/RunTimeline.d.ts +25 -0
  81. package/dist/workflow/RunTimeline.js +56 -0
  82. package/dist/workflow/WorkflowCard.d.ts +39 -0
  83. package/dist/workflow/WorkflowCard.js +56 -0
  84. package/dist/workflow/WorkflowGallery.d.ts +43 -0
  85. package/dist/workflow/WorkflowGallery.js +69 -0
  86. package/dist/workflow/canvas/WorkflowCanvas.d.ts +39 -0
  87. package/dist/workflow/canvas/WorkflowCanvas.js +648 -0
  88. package/dist/workflow/definition.d.ts +408 -0
  89. package/dist/workflow/definition.js +411 -0
  90. package/dist/workflow/editor/WorkflowDiff.d.ts +17 -0
  91. package/dist/workflow/editor/WorkflowDiff.js +25 -0
  92. package/dist/workflow/editor/WorkflowImportPreview.d.ts +19 -0
  93. package/dist/workflow/editor/WorkflowImportPreview.js +50 -0
  94. package/dist/workflow/editor/WorkflowInspector.d.ts +40 -0
  95. package/dist/workflow/editor/WorkflowInspector.js +349 -0
  96. package/dist/workflow/editor/WorkflowJsonView.d.ts +16 -0
  97. package/dist/workflow/editor/WorkflowJsonView.js +40 -0
  98. package/dist/workflow/editor/WorkflowPalette.d.ts +16 -0
  99. package/dist/workflow/editor/WorkflowPalette.js +36 -0
  100. package/dist/workflow/editor/WorkflowProblems.d.ts +20 -0
  101. package/dist/workflow/editor/WorkflowProblems.js +51 -0
  102. package/dist/workflow/editor/WorkflowWorkspace.d.ts +49 -0
  103. package/dist/workflow/editor/WorkflowWorkspace.js +154 -0
  104. package/dist/workflow/gallery.d.ts +21 -0
  105. package/dist/workflow/gallery.js +29 -0
  106. package/dist/workflow/layout.d.ts +49 -0
  107. package/dist/workflow/layout.js +162 -0
  108. package/dist/workflow/model.d.ts +50 -0
  109. package/dist/workflow/model.js +7 -0
  110. package/dist/workflow/palette.d.ts +34 -0
  111. package/dist/workflow/palette.js +70 -0
  112. package/dist/workflow/portable.d.ts +162 -0
  113. package/dist/workflow/portable.js +547 -0
  114. package/dist/workflow/problems.d.ts +38 -0
  115. package/dist/workflow/problems.js +409 -0
  116. package/dist/workflow/run.d.ts +94 -0
  117. package/dist/workflow/run.js +31 -0
  118. package/dist/workflow/workspace.d.ts +33 -0
  119. package/dist/workflow/workspace.js +17 -0
  120. package/package.json +6 -1
@@ -0,0 +1,171 @@
1
+ import { BOOT_SOURCE, bootConfig } from "../appearance/boot.js";
2
+ import { astroliftBrand, chatStudioBrand, cykickBrand, loadrBrand, workbenchBrand, zentinelleBrand, } from "../appearance/brands.js";
3
+ import { CORNERS, defineProduct } from "../appearance/product.js";
4
+ import { accentDeclarations, block, renderThemeCss } from "./css.js";
5
+ /**
6
+ * The CSS layer: the same tokens and the same boot script, for pages that
7
+ * cannot render React (Django, Jinja, HTMX, Rails, Laravel, Phoenix). Both
8
+ * files are generated here from the token table and the boot source by
9
+ * `npm run theme`, and shipped as `@calliopelabs/ui/css/*`.
10
+ */
11
+ /** The products the CSS layer knows by name (`data-product`): the family's brands. */
12
+ export const CSS_LAYER_PRODUCTS = [
13
+ astroliftBrand,
14
+ zentinelleBrand,
15
+ workbenchBrand,
16
+ chatStudioBrand,
17
+ loadrBrand,
18
+ cykickBrand,
19
+ ].map((config) => defineProduct(config));
20
+ const TOKENS_HEADER = `/*
21
+ * @calliopelabs/ui CSS layer: tokens.
22
+ * Generated by \`npm run theme\` from src/theme/tokens.ts and the product
23
+ * brands. Do not edit by hand.
24
+ *
25
+ * The same values as the Tailwind preset (theme.css plus each product's brand
26
+ * tokens), for pages without React or Tailwind. Every axis is an attribute,
27
+ * on <html> or on any subtree:
28
+ * data-product the brand: its sidebar and its own accent
29
+ * data-mode dark | light (follows the ground)
30
+ * data-ground the surface ramp
31
+ * data-accent a family accent or the product's own; "custom" with an inline --brand-primary
32
+ * data-density compact | cards
33
+ * data-corners 0 | 2 | 4 | 10 (appearance.js sets --radius inline instead)
34
+ * appearance.js stamps all of them before first paint, or a server that
35
+ * keeps the preference renders them itself.
36
+ */`;
37
+ /** The whole of css/tokens.css. */
38
+ export function renderTokensCss() {
39
+ const theme = renderThemeCss().replace(/^\/\*[\s\S]*?\*\/\n\n/, "");
40
+ const blocks = [TOKENS_HEADER, theme.trimEnd()];
41
+ for (const corners of CORNERS) {
42
+ blocks.push(block(`[data-corners="${corners}"]`, [["--radius", `${corners}px`]], `Corners: ${corners}px`));
43
+ }
44
+ for (const product of CSS_LAYER_PRODUCTS) {
45
+ blocks.push(block(`[data-product="${product.id}"]`, [["--brand-sidebar", product.sidebar]], `Product: ${product.name}`));
46
+ const accent = product.brandAccent;
47
+ if (!accent)
48
+ continue;
49
+ for (const mode of ["dark", "light"]) {
50
+ blocks.push(block(`[data-mode="${mode}"][data-accent="${accent.key}"]`, accentDeclarations(mode, accent), `Accent: ${accent.label} (${product.name}, ${mode})`));
51
+ }
52
+ }
53
+ return `${blocks.join("\n\n")}\n`;
54
+ }
55
+ /** JSON that is still safe when the file is pasted into an inline <script>. */
56
+ function scriptJson(value) {
57
+ return JSON.stringify(value).replace(/</g, "\\u003c");
58
+ }
59
+ /**
60
+ * The whole of css/appearance.js: the boot script AppearanceScript renders
61
+ * (the same source text), the boot configs of the built-in products, and a
62
+ * small API over them. `set` stores the merged preference and runs the boot
63
+ * script again, so first paint and every later change resolve through one
64
+ * code path. Plain ES5, like the boot script.
65
+ */
66
+ export function renderAppearanceJs() {
67
+ const products = Object.fromEntries(CSS_LAYER_PRODUCTS.map((p) => [p.id, bootConfig(p)]));
68
+ return `/*
69
+ * @calliopelabs/ui CSS layer: appearance.
70
+ * Generated by \`npm run theme\` from src/appearance/boot.ts. Do not edit by hand.
71
+ *
72
+ * Load it in <head>, before the stylesheets, as a plain script element
73
+ * (no async, no defer) with the product on it, so it runs before first
74
+ * paint: src=".../appearance.js" data-product="astrolift".
75
+ *
76
+ * It stamps the person's ground, accent, density and corners on <html> from
77
+ * localStorage, exactly as AppearanceScript does, and leaves an API on
78
+ * window.CalliopeAppearance:
79
+ * get() the appearance on <html> now
80
+ * set(partial) store and apply { ground, accent, density, corners }
81
+ * reset() forget the choice; the product's defaults apply
82
+ * boot(config) use another product's boot config
83
+ * config the active product's grounds, accents and corners
84
+ * products the built-in products' boot configs, by id
85
+ * Every change dispatches "calliope:appearance" on document.
86
+ */
87
+ (function () {
88
+ var boot = ${BOOT_SOURCE};
89
+ var products = ${scriptJson(products)};
90
+ var d = document.documentElement;
91
+ var hex = /^#[0-9a-f]{6}$/;
92
+ var has = function (o, k) {
93
+ return typeof k === "string" && Object.prototype.hasOwnProperty.call(o, k);
94
+ };
95
+ var stored = function (c) {
96
+ try {
97
+ var parsed = JSON.parse(window.localStorage.getItem(c.key) || "null");
98
+ return parsed && typeof parsed === "object" ? parsed : {};
99
+ } catch (e) {
100
+ return {};
101
+ }
102
+ };
103
+ /* normalizePartial: only the axes that are valid for this product. A locked accent (22:1) takes no custom colour. */
104
+ var valid = function (c, v) {
105
+ var out = {};
106
+ if (has(c.grounds, v.ground)) out.ground = v.ground;
107
+ if (c.accents.indexOf(v.accent) > -1 || (hex.test(v.accent) && c.minContrast <= 21))
108
+ out.accent = v.accent;
109
+ if (v.density === "compact" || v.density === "cards") out.density = v.density;
110
+ if (c.corners.indexOf(v.corners) > -1) out.corners = v.corners;
111
+ return out;
112
+ };
113
+ var api = {
114
+ products: products,
115
+ config: null,
116
+ get: function () {
117
+ var custom = d.getAttribute("data-accent") === "custom";
118
+ return {
119
+ ground: d.getAttribute("data-ground"),
120
+ mode: d.getAttribute("data-mode"),
121
+ accent: custom ? d.style.getPropertyValue("--brand-primary") : d.getAttribute("data-accent"),
122
+ density: d.getAttribute("data-density"),
123
+ corners: parseInt(d.style.getPropertyValue("--radius"), 10)
124
+ };
125
+ },
126
+ set: function (next) {
127
+ var c = api.config;
128
+ if (!c || !next) return;
129
+ var merged = stored(c);
130
+ for (var k in next) if (has(next, k)) merged[k] = next[k];
131
+ if (typeof merged.accent === "string") merged.accent = merged.accent.toLowerCase();
132
+ try {
133
+ window.localStorage.setItem(c.key, JSON.stringify(valid(c, merged)));
134
+ } catch (e) {}
135
+ apply();
136
+ },
137
+ reset: function () {
138
+ if (!api.config) return;
139
+ try {
140
+ window.localStorage.removeItem(api.config.key);
141
+ } catch (e) {}
142
+ apply();
143
+ },
144
+ boot: function (config) {
145
+ api.config = config;
146
+ apply();
147
+ }
148
+ };
149
+ var apply = function () {
150
+ /* A custom accent is inline; a family accent comes from the stylesheet. */
151
+ d.style.removeProperty("--brand-primary");
152
+ d.style.removeProperty("--brand-on-primary");
153
+ try {
154
+ boot(api.config);
155
+ } catch (e) {}
156
+ var event;
157
+ try {
158
+ event = new CustomEvent("calliope:appearance", { detail: api.get() });
159
+ } catch (e) {
160
+ event = document.createEvent("CustomEvent");
161
+ event.initCustomEvent("calliope:appearance", false, false, api.get());
162
+ }
163
+ document.dispatchEvent(event);
164
+ };
165
+ window.CalliopeAppearance = api;
166
+ var me = document.currentScript;
167
+ var id = me && me.getAttribute("data-product");
168
+ if (has(products, id)) api.boot(products[id]);
169
+ })();
170
+ `;
171
+ }
@@ -1,5 +1,6 @@
1
1
  import { type Mode } from "./tokens.js";
2
2
  type Declarations = readonly (readonly [property: string, value: string])[];
3
+ export declare function block(selector: string, declarations: Declarations, comment?: string): string;
3
4
  /** The accent tokens one accent paints in one mode. */
4
5
  export declare function accentDeclarations(mode: Mode, accent: {
5
6
  dark: string;
package/dist/theme/css.js CHANGED
@@ -1,11 +1,51 @@
1
1
  import { ACCENTS, ACCENT_KEYS, BASE_SURFACES, DEFAULT_ACCENT, DENSITIES, GROUNDS, GROUND_KEYS, MODE_TOKENS, STATIC_TOKENS, SURFACES, } from "./tokens.js";
2
2
  const STATUS_NAMES = ["success", "warning", "info", "danger"];
3
- function block(selector, declarations, comment) {
3
+ export function block(selector, declarations, comment) {
4
4
  const head = comment ? `/* ${comment} */\n` : "";
5
5
  const body = declarations.map(([property, value]) => ` ${property}: ${value};`).join("\n");
6
6
  return `${head}${selector} {\n${body}\n}`;
7
7
  }
8
- function surfaceDeclarations(s) {
8
+ /**
9
+ * The sidebar of one ground. A runtime brand (`brandStyles`) reaches it only
10
+ * through `--brand-sidebar-*` variables, each with the ground's own value as
11
+ * the fallback, so a page without a brand paints exactly the token table:
12
+ * dark brand grounds --brand-sidebar, its foreground, secondary text and marker
13
+ * light grounds --brand-sidebar-light (opt-in), with its foreground,
14
+ * secondary text, marker, hover tint and hairline
15
+ * graphite neutral whatever the brand (Astrolift #2286)
16
+ */
17
+ function sidebarDeclarations(s, mode) {
18
+ const foreground = MODE_TOKENS[mode].sidebarForeground;
19
+ if (s.sidebar === "brand") {
20
+ return [
21
+ ["--sidebar", `var(--brand-sidebar, ${s.card})`],
22
+ ["--sidebar-foreground", `var(--brand-sidebar-foreground, ${foreground})`],
23
+ ["--sidebar-primary", "var(--brand-sidebar-primary, var(--brand-primary))"],
24
+ ["--sidebar-muted-foreground", `var(--brand-sidebar-muted-foreground, ${s.mutedForeground})`],
25
+ ["--sidebar-accent", s.sidebarAccent],
26
+ ["--sidebar-border", s.sidebarBorder],
27
+ ];
28
+ }
29
+ if (mode === "light") {
30
+ return [
31
+ ["--sidebar", `var(--brand-sidebar-light, ${s.sidebar})`],
32
+ ["--sidebar-foreground", `var(--brand-sidebar-foreground, ${foreground})`],
33
+ ["--sidebar-primary", "var(--brand-sidebar-primary, var(--brand-primary))"],
34
+ ["--sidebar-muted-foreground", `var(--brand-sidebar-muted-foreground, ${s.mutedForeground})`],
35
+ ["--sidebar-accent", `var(--brand-sidebar-accent, ${s.sidebarAccent})`],
36
+ ["--sidebar-border", `var(--brand-sidebar-border, ${s.sidebarBorder})`],
37
+ ];
38
+ }
39
+ return [
40
+ ["--sidebar", s.sidebar],
41
+ ["--sidebar-foreground", foreground],
42
+ ["--sidebar-primary", "var(--brand-primary)"],
43
+ ["--sidebar-muted-foreground", s.mutedForeground],
44
+ ["--sidebar-accent", s.sidebarAccent],
45
+ ["--sidebar-border", s.sidebarBorder],
46
+ ];
47
+ }
48
+ function surfaceDeclarations(s, mode) {
9
49
  return [
10
50
  ["--background", s.background],
11
51
  ["--card", s.card],
@@ -16,9 +56,7 @@ function surfaceDeclarations(s) {
16
56
  ["--muted-foreground", s.mutedForeground],
17
57
  ["--border", s.border],
18
58
  ["--input", s.input],
19
- ["--sidebar", s.sidebar === "brand" ? `var(--brand-sidebar, ${s.card})` : s.sidebar],
20
- ["--sidebar-accent", s.sidebarAccent],
21
- ["--sidebar-border", s.sidebarBorder],
59
+ ...sidebarDeclarations(s, mode),
22
60
  ];
23
61
  }
24
62
  function modeDeclarations(mode) {
@@ -26,7 +64,7 @@ function modeDeclarations(mode) {
26
64
  const accent = ACCENTS[DEFAULT_ACCENT];
27
65
  return [
28
66
  ["color-scheme", mode],
29
- ...surfaceDeclarations(BASE_SURFACES[mode]),
67
+ ...surfaceDeclarations(BASE_SURFACES[mode], mode),
30
68
  ["--foreground", m.foreground],
31
69
  ["--card-foreground", m.foreground],
32
70
  ["--popover-foreground", m.foreground],
@@ -40,8 +78,6 @@ function modeDeclarations(mode) {
40
78
  ["--ring", "var(--brand-primary)"],
41
79
  ["--chart-1", "var(--brand-primary)"],
42
80
  ...Object.entries(m.chart).map(([key, value]) => [`--${key}`, value]),
43
- ["--sidebar-foreground", m.sidebarForeground],
44
- ["--sidebar-primary", "var(--brand-primary)"],
45
81
  ["--sidebar-primary-foreground", "var(--brand-on-primary)"],
46
82
  ["--sidebar-accent-foreground", m.foreground],
47
83
  ["--sidebar-ring", "var(--brand-primary)"],
@@ -89,7 +125,7 @@ export function renderThemeCss() {
89
125
  blocks.push(block('[data-mode="light"]', modeDeclarations("light"), "Light: neutral ground, green accent"));
90
126
  for (const ground of GROUND_KEYS) {
91
127
  const { mode, label } = GROUNDS[ground];
92
- blocks.push(block(`[data-mode="${mode}"][data-ground="${ground}"]`, surfaceDeclarations(SURFACES[ground]), `Ground: ${label} (${mode})`));
128
+ blocks.push(block(`[data-mode="${mode}"][data-ground="${ground}"]`, surfaceDeclarations(SURFACES[ground], mode), `Ground: ${label} (${mode})`));
93
129
  }
94
130
  for (const key of ACCENT_KEYS) {
95
131
  const accent = ACCENTS[key];
@@ -0,0 +1,47 @@
1
+ import * as React from "react";
2
+ import { type Artifact } from "./run.js";
3
+ export interface ArtifactCardProps {
4
+ artifact: Artifact;
5
+ /**
6
+ * The host's view of the artifact: a thumbnail, a player, or one of the
7
+ * module review viewers. Rendered as given, so it can be interactive.
8
+ */
9
+ preview?: React.ReactNode;
10
+ /** Opens the artifact: the name becomes a link through the app's link component. */
11
+ href?: string;
12
+ /** Opens or selects the artifact without a route: the name becomes a button. Ignored with `href`. */
13
+ onOpen?: () => void;
14
+ /** Marks the artifact the host is showing beside the list. */
15
+ selected?: boolean;
16
+ /** The host's own controls (download, compare). */
17
+ actions?: React.ReactNode;
18
+ headingLevel?: "h2" | "h3" | "h4";
19
+ className?: string;
20
+ }
21
+ /**
22
+ * One typed output of a run: its type, name, status, size, version and the
23
+ * stage that made it, with a slot for the host's preview or a module viewer.
24
+ * Knows nothing of any module; the host decides what a kind looks like.
25
+ */
26
+ export declare function ArtifactCard({ artifact, preview, href, onOpen, selected, actions, headingLevel, className, }: ArtifactCardProps): React.JSX.Element;
27
+ export interface ArtifactListProps {
28
+ artifacts: readonly Artifact[];
29
+ /** Names the list. Defaults to "Artifacts". */
30
+ label?: string;
31
+ /** The preview or viewer for each artifact. */
32
+ renderPreview?: (artifact: Artifact) => React.ReactNode;
33
+ /** How each artifact opens: a route, a handler, the host's controls. */
34
+ actionsFor?: (artifact: Artifact) => Pick<ArtifactCardProps, "href" | "onOpen" | "actions">;
35
+ selectedId?: string;
36
+ /** One column, for a side panel; the default flows into columns as the width allows. */
37
+ columns?: "auto" | "one";
38
+ loading?: boolean;
39
+ error?: {
40
+ message: string;
41
+ } | null;
42
+ onRetry?: () => void;
43
+ headingLevel?: ArtifactCardProps["headingLevel"];
44
+ className?: string;
45
+ }
46
+ /** A run's artifacts as cards, with loading, error and empty states. */
47
+ export declare function ArtifactList({ artifacts, label, renderPreview, actionsFor, selectedId, columns, loading, error, onRetry, headingLevel, className, }: ArtifactListProps): React.JSX.Element;
@@ -0,0 +1,45 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { useUI } from "../provider/UIProvider.js";
6
+ import { EmptyState } from "../states/EmptyState.js";
7
+ import { ErrorState } from "../states/ErrorState.js";
8
+ import { StatusBadge } from "../status/StatusBadge.js";
9
+ import { ARTIFACT_TONE } from "./run.js";
10
+ import { WorkflowCardSkeleton } from "./WorkflowCard.js";
11
+ /**
12
+ * One typed output of a run: its type, name, status, size, version and the
13
+ * stage that made it, with a slot for the host's preview or a module viewer.
14
+ * Knows nothing of any module; the host decides what a kind looks like.
15
+ */
16
+ export function ArtifactCard({ artifact, preview, href, onOpen, selected = false, actions, headingLevel = "h3", className, }) {
17
+ const { Link, messages } = useUI();
18
+ const t = messages.run;
19
+ const titleId = React.useId();
20
+ const Heading = headingLevel;
21
+ const meta = [
22
+ artifact.size,
23
+ artifact.version && t.version(artifact.version),
24
+ artifact.producedBy && t.producedBy(artifact.producedBy),
25
+ ].filter(Boolean);
26
+ const nameClass = "rounded-sm text-left outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring";
27
+ return (_jsxs("article", { "aria-labelledby": titleId, "data-slot": "artifact-card", "data-kind": artifact.kind, "data-selected": selected || undefined, className: cn("flex min-w-0 flex-col overflow-hidden rounded-lg border bg-card text-card-foreground", selected && "border-primary ring-1 ring-primary", className), children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-1.5 p-(--panel-pad)", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", children: [_jsx("span", { className: "min-w-0 truncate font-mono text-xs text-muted-foreground", children: artifact.kindLabel }), artifact.status && (_jsx(StatusBadge, { tone: ARTIFACT_TONE[artifact.status], struck: artifact.status === "superseded", children: t.artifactStatus(artifact.status) })), actions && _jsx("span", { className: "ml-auto flex items-center gap-1", children: actions })] }), _jsx(Heading, { id: titleId, className: "text-sm font-medium [overflow-wrap:anywhere]", children: href ? (_jsx(Link, { href: href, "aria-current": selected || undefined, className: nameClass, children: artifact.name })) : onOpen ? (_jsx("button", { type: "button", onClick: onOpen, "aria-pressed": selected, className: nameClass, children: artifact.name })) : (artifact.name) }), artifact.summary && (_jsx("p", { className: "line-clamp-3 text-sm [overflow-wrap:anywhere] text-muted-foreground", children: artifact.summary })), meta.length > 0 && (_jsx("p", { className: "flex min-w-0 flex-wrap gap-x-3 font-mono text-xs text-muted-foreground", children: meta.map((item, i) => (_jsx("span", { className: "min-w-0 [overflow-wrap:anywhere]", children: item }, i))) }))] }), preview != null && (_jsx("div", { "data-slot": "artifact-preview", className: "mt-auto min-w-0 border-t", children: preview }))] }));
28
+ }
29
+ /** A run's artifacts as cards, with loading, error and empty states. */
30
+ export function ArtifactList({ artifacts, label, renderPreview, actionsFor, selectedId, columns = "auto", loading = false, error, onRetry, headingLevel, className, }) {
31
+ const { messages } = useUI();
32
+ const t = messages.run;
33
+ const name = label ?? t.artifacts;
34
+ const grid = cn("grid min-w-0 gap-3", columns === "auto" && "grid-cols-[repeat(auto-fill,minmax(min(100%,18rem),1fr))]", className);
35
+ if (error) {
36
+ return (_jsx(ErrorState, { title: t.loadFailed(name), error: error, onRetry: onRetry, className: className }));
37
+ }
38
+ if (loading) {
39
+ return (_jsxs("div", { "aria-busy": true, className: grid, children: [_jsx("p", { role: "status", className: "sr-only", children: messages.states.loading }), Array.from({ length: 3 }, (_, i) => (_jsx(WorkflowCardSkeleton, {}, i)))] }));
40
+ }
41
+ if (artifacts.length === 0) {
42
+ return (_jsx(EmptyState, { title: t.noArtifacts, description: t.noArtifactsDescription, className: className }));
43
+ }
44
+ return (_jsx("ul", { "aria-label": name, "data-slot": "artifact-list", className: grid, children: artifacts.map((artifact) => (_jsx("li", { className: "flex min-w-0", children: _jsx(ArtifactCard, { artifact: artifact, preview: renderPreview?.(artifact), selected: artifact.id === selectedId, headingLevel: headingLevel, className: "flex-1", ...actionsFor?.(artifact) }) }, artifact.id))) }));
45
+ }
@@ -0,0 +1,74 @@
1
+ import * as React from "react";
2
+ /** A run of text: plain, inserted or deleted (a redline). */
3
+ export interface DocumentSegment {
4
+ text: string;
5
+ mark?: "insert" | "delete";
6
+ }
7
+ /** A paragraph or clause. */
8
+ export interface DocumentBlock {
9
+ id: string;
10
+ /** The clause reference ("4.2", "Schedule 1, 3"), when the block is a clause. */
11
+ clause?: string;
12
+ heading?: string;
13
+ segments: readonly DocumentSegment[];
14
+ }
15
+ export interface DocumentPage {
16
+ /** From 1. */
17
+ number: number;
18
+ blocks: readonly DocumentBlock[];
19
+ }
20
+ /** A comment or note pinned to a block. */
21
+ export interface DocumentAnnotation {
22
+ id: string;
23
+ blockId: string;
24
+ author: string;
25
+ body: string;
26
+ resolved?: boolean;
27
+ }
28
+ export type SignOffStatus = "pending" | "signed" | "declined";
29
+ export interface DocumentSignOff {
30
+ id: string;
31
+ /** The role that signs ("Counsel", "Policy owner"). */
32
+ role: string;
33
+ name?: string;
34
+ status: SignOffStatus;
35
+ /** Formatted by the host. */
36
+ at?: string;
37
+ }
38
+ export interface DocumentReviewProps {
39
+ /** Names the document. */
40
+ title: string;
41
+ pages: readonly DocumentPage[];
42
+ /** The page shown, from 1. Uncontrolled without it. */
43
+ page?: number;
44
+ defaultPage?: number;
45
+ onPageChange?: (page: number) => void;
46
+ annotations?: readonly DocumentAnnotation[];
47
+ activeAnnotationId?: string;
48
+ /** An annotation was chosen in the margin: the host shows it (in ReviewPanel, say). */
49
+ onAnnotationSelect?: (annotation: DocumentAnnotation) => void;
50
+ /**
51
+ * The person chose a block to comment on. `label` is the anchor in words
52
+ * ("p. 3, 4.2"), ready for ReviewPanel's `commentAnchor`.
53
+ */
54
+ onAnchor?: (anchor: {
55
+ blockId: string;
56
+ page: number;
57
+ clause?: string;
58
+ label: string;
59
+ }) => void;
60
+ signOffs?: readonly DocumentSignOff[];
61
+ /** Signs off, after the confirm step. Shown when given and the person's sign-off is pending. */
62
+ onSignOff?: () => Promise<unknown> | unknown;
63
+ /** The role the person signs as, for the confirm step. */
64
+ signOffRole?: string;
65
+ className?: string;
66
+ }
67
+ /**
68
+ * Document review for legal and document modules: paged text with redline
69
+ * insertions and deletions, clause references, annotations in the margin and
70
+ * sign-off. Choosing a clause hands the host an anchor for its comment;
71
+ * choosing an annotation tells the host which one to show. The host supplies
72
+ * the text already segmented; the viewer renders, it does not diff.
73
+ */
74
+ export declare function DocumentReview({ title, pages, page: pageProp, defaultPage, onPageChange, annotations, activeAnnotationId, onAnnotationSelect, onAnchor, signOffs, onSignOff, signOffRole, className, }: DocumentReviewProps): React.JSX.Element;
@@ -0,0 +1,54 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronLeftIcon, ChevronRightIcon, MessageSquareIcon, SignatureIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { ConfirmDialog } from "../overlays/ConfirmDialog.js";
7
+ import { Button } from "../primitives/Button.js";
8
+ import { useUI } from "../provider/UIProvider.js";
9
+ import { StatusBadge } from "../status/StatusBadge.js";
10
+ const SIGN_OFF_TONE = {
11
+ pending: "muted",
12
+ signed: "ok",
13
+ declined: "error",
14
+ };
15
+ /**
16
+ * Document review for legal and document modules: paged text with redline
17
+ * insertions and deletions, clause references, annotations in the margin and
18
+ * sign-off. Choosing a clause hands the host an anchor for its comment;
19
+ * choosing an annotation tells the host which one to show. The host supplies
20
+ * the text already segmented; the viewer renders, it does not diff.
21
+ */
22
+ export function DocumentReview({ title, pages, page: pageProp, defaultPage = 1, onPageChange, annotations = [], activeAnnotationId, onAnnotationSelect, onAnchor, signOffs = [], onSignOff, signOffRole, className, }) {
23
+ const { messages } = useUI();
24
+ const t = messages.document;
25
+ const [ownPage, setOwnPage] = React.useState(defaultPage);
26
+ const [confirming, setConfirming] = React.useState(false);
27
+ const total = pages.length;
28
+ const current = Math.min(Math.max(pageProp ?? ownPage, 1), Math.max(total, 1));
29
+ const shown = pages.find((p) => p.number === current) ?? pages[current - 1];
30
+ const go = (next) => {
31
+ setOwnPage(next);
32
+ onPageChange?.(next);
33
+ };
34
+ const byBlock = new Map();
35
+ for (const a of annotations)
36
+ byBlock.set(a.blockId, [...(byBlock.get(a.blockId) ?? []), a]);
37
+ const canSign = Boolean(onSignOff) && signOffs.some((s) => s.status === "pending");
38
+ const changes = pages
39
+ .flatMap((p) => p.blocks)
40
+ .flatMap((b) => b.segments)
41
+ .filter((s) => s.mark).length;
42
+ return (_jsxs("section", { "aria-label": title, "data-slot": "document-review", className: cn("flex min-w-0 flex-col gap-3", className), children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-between gap-2", children: [_jsxs("p", { className: "flex min-w-0 flex-wrap gap-x-3 font-mono text-xs text-muted-foreground", children: [_jsx("span", { children: t.changes(changes) }), _jsx("span", { children: t.annotations(annotations.length) })] }), total > 1 && (_jsxs("nav", { "aria-label": t.pages, className: "flex items-center gap-1", children: [_jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": t.previousPage, disabled: current <= 1, onClick: () => go(current - 1), children: _jsx(ChevronLeftIcon, { "aria-hidden": true }) }), _jsx("span", { "aria-live": "polite", className: "font-mono text-xs tabular-nums", children: t.pageOf(current, total) }), _jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": t.nextPage, disabled: current >= total, onClick: () => go(current + 1), children: _jsx(ChevronRightIcon, { "aria-hidden": true }) })] }))] }), _jsx("div", { className: "min-w-0 rounded-lg border bg-card p-(--panel-pad) text-card-foreground", children: shown ? (_jsx("ol", { "aria-label": t.pageOf(current, total), className: "flex min-w-0 flex-col gap-3", children: shown.blocks.map((block) => {
43
+ const notes = byBlock.get(block.id) ?? [];
44
+ const active = notes.some((n) => n.id === activeAnnotationId);
45
+ const label = t.anchor(current, block.clause);
46
+ return (_jsxs("li", { className: cn("grid min-w-0 grid-cols-[minmax(0,1fr)_auto] gap-x-3 gap-y-1 rounded-md border-l-2 border-transparent pl-2", notes.length > 0 && "border-warning", active && "border-primary bg-primary/5"), children: [_jsxs("div", { className: "min-w-0", children: [(block.clause || block.heading) && (_jsxs("p", { className: "mb-0.5 flex min-w-0 flex-wrap items-baseline gap-2 text-sm font-medium", children: [block.clause &&
47
+ (onAnchor ? (_jsx("button", { type: "button", onClick: () => onAnchor({
48
+ blockId: block.id,
49
+ page: current,
50
+ clause: block.clause,
51
+ label,
52
+ }), "aria-label": t.commentOnClause(block.clause), className: "rounded-sm font-mono text-xs text-primary underline-offset-2 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring", children: block.clause })) : (_jsx("span", { className: "font-mono text-xs text-muted-foreground", children: block.clause }))), block.heading && _jsx("span", { children: block.heading })] })), _jsx("p", { className: "text-sm leading-relaxed [overflow-wrap:anywhere]", children: block.segments.map((segment, i) => segment.mark === "insert" ? (_jsxs("ins", { className: "rounded-sm bg-success/15 text-success-fg underline decoration-success", children: [_jsx("span", { className: "sr-only", children: t.inserted(segment.text) }), _jsx("span", { "aria-hidden": true, children: segment.text })] }, i)) : segment.mark === "delete" ? (_jsxs("del", { className: "rounded-sm bg-danger/15 text-danger-fg line-through", children: [_jsx("span", { className: "sr-only", children: t.deleted(segment.text) }), _jsx("span", { "aria-hidden": true, children: segment.text })] }, i)) : (_jsx(React.Fragment, { children: segment.text }, i))) })] }), _jsxs("div", { className: "flex flex-col items-end gap-1", children: [notes.map((note) => (_jsx("button", { type: "button", onClick: () => onAnnotationSelect?.(note), disabled: !onAnnotationSelect, "aria-current": note.id === activeAnnotationId || undefined, "aria-label": t.annotationBy(note.author, label), title: note.body, className: cn("inline-flex size-6 items-center justify-center rounded-md border text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default", note.id === activeAnnotationId && "border-primary text-primary", note.resolved && "opacity-60"), children: _jsx(MessageSquareIcon, { "aria-hidden": true, className: "size-3.5" }) }, note.id))), onAnchor && !block.clause && (_jsx(Button, { size: "icon-xs", variant: "ghost", "aria-label": t.commentOnBlock(label), onClick: () => onAnchor({ blockId: block.id, page: current, label }), children: _jsx(MessageSquareIcon, { "aria-hidden": true }) }))] })] }, block.id));
53
+ }) })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: t.empty })) }), signOffs.length > 0 && (_jsxs("div", { className: "flex min-w-0 flex-col gap-2 rounded-lg border p-3", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-between gap-2", children: [_jsx("h3", { className: "font-mono text-2xs font-medium tracking-wider text-muted-foreground uppercase", children: t.signOff }), canSign && (_jsxs(Button, { size: "sm", onClick: () => setConfirming(true), children: [_jsx(SignatureIcon, { "aria-hidden": true }), t.signOffAction] }))] }), _jsx("ul", { className: "flex min-w-0 flex-col gap-1.5", children: signOffs.map((s) => (_jsxs("li", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-sm", children: [_jsx("span", { className: "font-medium", children: s.role }), s.name && _jsx("span", { className: "text-muted-foreground", children: s.name }), _jsx(StatusBadge, { tone: SIGN_OFF_TONE[s.status], children: t.signOffStatus(s.status) }), s.at && _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: s.at })] }, s.id))) })] })), _jsx(ConfirmDialog, { open: confirming, onOpenChange: setConfirming, title: t.signOffTitle(title), description: t.signOffDescription(signOffRole), confirmLabel: t.signOffAction, onConfirm: () => onSignOff?.() })] }));
54
+ }
@@ -0,0 +1,53 @@
1
+ import * as React from "react";
2
+ /** A field's place on its source page, as fractions of the page's width and height (0 to 1). */
3
+ export interface FormFieldBox {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ export type FormFieldStatus = "pending" | "accepted" | "corrected";
10
+ /** One value extracted from a form. */
11
+ export interface FormField {
12
+ id: string;
13
+ label: string;
14
+ /** What extraction read. */
15
+ value: string;
16
+ /** Extraction's confidence, 0 to 1. */
17
+ confidence: number;
18
+ /** The source page it came from, from 1. */
19
+ page?: number;
20
+ box?: FormFieldBox;
21
+ status?: FormFieldStatus;
22
+ /** The person's correction, when `status` is corrected. */
23
+ corrected?: string;
24
+ }
25
+ /** A scanned or rendered source page. */
26
+ export interface FormSourcePage {
27
+ number: number;
28
+ src: string;
29
+ alt: string;
30
+ }
31
+ export interface FormReviewProps {
32
+ /** Names the form. */
33
+ title: string;
34
+ pages: readonly FormSourcePage[];
35
+ fields: readonly FormField[];
36
+ /** Below this confidence a field is flagged. Defaults to 0.85. */
37
+ threshold?: number;
38
+ activeFieldId?: string;
39
+ onFieldSelect?: (field: FormField) => void;
40
+ /** Accepts the extracted value. Without it and `onCorrect` the review is read-only. */
41
+ onAccept?: (field: FormField) => Promise<unknown> | unknown;
42
+ /** Saves a correction. A rejection keeps the editor open with the error. */
43
+ onCorrect?: (field: FormField, value: string) => Promise<unknown> | unknown;
44
+ className?: string;
45
+ }
46
+ /**
47
+ * Form review for the paperwork module: the extracted fields beside the
48
+ * source page, each with its confidence (flagged below the threshold), and
49
+ * accept or correct per field. Choosing a field shows its page and boxes its
50
+ * place; the boxes on the page choose fields too. The host extracts, stores
51
+ * and decides; the viewer reports intent.
52
+ */
53
+ export declare function FormReview({ title, pages, fields, threshold, activeFieldId, onFieldSelect, onAccept, onCorrect, className, }: FormReviewProps): React.JSX.Element;
@@ -0,0 +1,83 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { CheckIcon, PencilIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { Button } from "../primitives/Button.js";
7
+ import { Input } from "../primitives/Input.js";
8
+ import { Segmented } from "../primitives/Segmented.js";
9
+ import { useUI } from "../provider/UIProvider.js";
10
+ import { StatusBadge } from "../status/StatusBadge.js";
11
+ /**
12
+ * Form review for the paperwork module: the extracted fields beside the
13
+ * source page, each with its confidence (flagged below the threshold), and
14
+ * accept or correct per field. Choosing a field shows its page and boxes its
15
+ * place; the boxes on the page choose fields too. The host extracts, stores
16
+ * and decides; the viewer reports intent.
17
+ */
18
+ export function FormReview({ title, pages, fields, threshold = 0.85, activeFieldId, onFieldSelect, onAccept, onCorrect, className, }) {
19
+ const { messages } = useUI();
20
+ const t = messages.form;
21
+ const active = fields.find((f) => f.id === activeFieldId);
22
+ const [ownPage, setOwnPage] = React.useState(pages[0]?.number ?? 1);
23
+ const [lastActive, setLastActive] = React.useState(activeFieldId);
24
+ // Follow the chosen field to its page, until the person turns the page.
25
+ if (activeFieldId !== lastActive) {
26
+ setLastActive(activeFieldId);
27
+ if (active?.page)
28
+ setOwnPage(active.page);
29
+ }
30
+ const page = pages.find((p) => p.number === ownPage) ?? pages[0];
31
+ const onPage = fields.filter((f) => f.box && (f.page ?? 1) === page?.number);
32
+ const done = fields.filter((f) => f.status === "accepted" || f.status === "corrected").length;
33
+ const flagged = fields.filter((f) => f.confidence < threshold && f.status !== "accepted" && f.status !== "corrected").length;
34
+ return (_jsxs("section", { "aria-label": title, "data-slot": "form-review", className: cn("grid min-w-0 gap-4 md:grid-cols-2", className), children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-between gap-2", children: [_jsx("h3", { className: "font-mono text-2xs font-medium tracking-wider text-muted-foreground uppercase", children: t.source }), pages.length > 1 && (_jsx(Segmented, { label: t.sourcePage, size: "sm", value: String(page?.number), onValueChange: (v) => setOwnPage(Number(v)), options: pages.map((p) => ({ value: String(p.number), label: t.page(p.number) })) }))] }), page && (_jsxs("div", { className: "relative min-w-0 overflow-hidden rounded-lg border bg-muted/40", children: [_jsx("img", { src: page.src, alt: page.alt, className: "block h-auto w-full" }), onPage.map((f) => (_jsx("button", { type: "button", onClick: () => onFieldSelect?.(f), disabled: !onFieldSelect, "aria-label": t.fieldOnSource(f.label), "aria-current": f.id === activeFieldId || undefined, style: {
35
+ left: `${f.box.x * 100}%`,
36
+ top: `${f.box.y * 100}%`,
37
+ width: `${f.box.width * 100}%`,
38
+ height: `${f.box.height * 100}%`,
39
+ }, className: cn("absolute rounded-sm border-2 outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default", f.confidence < threshold && (f.status ?? "pending") === "pending"
40
+ ? "border-warning"
41
+ : "border-info/60", f.id === activeFieldId && "border-primary bg-primary/10") }, f.id)))] }))] }), _jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-between gap-2", children: [_jsx("h3", { className: "font-mono text-2xs font-medium tracking-wider text-muted-foreground uppercase", children: t.fields }), _jsx("p", { role: "status", className: "font-mono text-xs text-muted-foreground", children: t.progress(done, fields.length, flagged) })] }), _jsx("ul", { className: "flex min-w-0 flex-col gap-2", children: fields.map((field) => (_jsx(FieldRow, { field: field, low: field.confidence < threshold, active: field.id === activeFieldId, onSelect: onFieldSelect, onAccept: onAccept, onCorrect: onCorrect }, field.id))) })] })] }));
42
+ }
43
+ function FieldRow({ field, low, active, onSelect, onAccept, onCorrect, }) {
44
+ const { messages } = useUI();
45
+ const t = messages.form;
46
+ const id = React.useId();
47
+ const [editing, setEditing] = React.useState(false);
48
+ const [draft, setDraft] = React.useState("");
49
+ const [pending, setPending] = React.useState(false);
50
+ const [error, setError] = React.useState(null);
51
+ const input = React.useRef(null);
52
+ React.useEffect(() => {
53
+ if (editing)
54
+ input.current?.focus();
55
+ }, [editing]);
56
+ const status = field.status ?? "pending";
57
+ const shown = status === "corrected" && field.corrected != null ? field.corrected : field.value;
58
+ async function run(action, after) {
59
+ setPending(true);
60
+ setError(null);
61
+ try {
62
+ const result = await action();
63
+ if (result !== false)
64
+ after?.();
65
+ }
66
+ catch (err) {
67
+ setError(err instanceof Error && err.message ? err.message : messages.confirm.failed);
68
+ }
69
+ finally {
70
+ setPending(false);
71
+ }
72
+ }
73
+ return (_jsxs("li", { "aria-current": active || undefined, className: cn("flex min-w-0 flex-col gap-1.5 rounded-lg border bg-card p-2.5 text-card-foreground", active && "border-primary"), children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", children: [_jsx("span", { className: "min-w-0 text-xs font-medium [overflow-wrap:anywhere]", children: onSelect ? (_jsx("button", { type: "button", onClick: () => onSelect(field), className: "rounded-sm text-left outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring", children: field.label })) : (field.label) }), _jsx(StatusBadge, { tone: low ? "warn" : "muted", children: t.confidence(field.confidence) }), status !== "pending" && (_jsx(StatusBadge, { tone: status === "accepted" ? "ok" : "info", children: t.status(status) }))] }), editing ? (_jsxs("form", { className: "flex min-w-0 flex-col gap-1.5", onSubmit: (event) => {
74
+ event.preventDefault();
75
+ void run(() => onCorrect?.(field, draft.trim()), () => setEditing(false));
76
+ }, children: [_jsx("label", { htmlFor: `${id}-input`, className: "sr-only", children: t.correctLabel(field.label) }), _jsx(Input, { id: `${id}-input`, value: draft, disabled: pending, ref: input, "aria-invalid": error != null || undefined, "aria-describedby": `${id}-extracted${error ? ` ${id}-error` : ""}`, onChange: (event) => setDraft(event.target.value) }), _jsx("p", { id: `${id}-extracted`, className: "text-xs [overflow-wrap:anywhere] text-muted-foreground", children: t.extracted(field.value) }), error && (_jsx("p", { id: `${id}-error`, role: "alert", className: "text-xs text-danger-fg", children: error })), _jsxs("div", { className: "flex flex-wrap gap-1.5", children: [_jsx(Button, { type: "submit", size: "xs", disabled: pending || !draft.trim(), children: pending ? messages.confirm.working : t.save }), _jsx(Button, { type: "button", size: "xs", variant: "ghost", disabled: pending, onClick: () => {
77
+ setEditing(false);
78
+ setError(null);
79
+ }, children: t.cancel })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("p", { className: "font-mono text-sm [overflow-wrap:anywhere]", children: [shown || _jsx("span", { className: "text-muted-foreground", children: t.empty }), status === "corrected" && field.corrected != null && (_jsxs(_Fragment, { children: [" ", _jsxs("del", { className: "text-xs text-muted-foreground", children: [_jsx("span", { className: "sr-only", children: t.extracted(field.value) }), _jsx("span", { "aria-hidden": true, children: field.value })] })] }))] }), (onAccept || onCorrect) && (_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [onAccept && status === "pending" && (_jsxs(Button, { size: "xs", variant: "outline", disabled: pending, "aria-label": t.actionName(t.accept, field.label), onClick: () => void run(() => onAccept(field)), children: [_jsx(CheckIcon, { "aria-hidden": true }), t.accept] })), onCorrect && (_jsxs(Button, { size: "xs", variant: "ghost", disabled: pending, "aria-label": t.actionName(t.correct, field.label), onClick: () => {
80
+ setDraft(shown);
81
+ setEditing(true);
82
+ }, children: [_jsx(PencilIcon, { "aria-hidden": true }), t.correct] }))] })), error && (_jsx("p", { role: "alert", className: "text-xs text-danger-fg", children: error }))] }))] }));
83
+ }