@changfenhuang/dsh-genui 0.9.1

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 (106) hide show
  1. package/CHANGELOG.md +367 -0
  2. package/LICENSE +21 -0
  3. package/README.md +261 -0
  4. package/README.zh-CN.md +261 -0
  5. package/SKILL.md +115 -0
  6. package/cordis.patch.yml +9 -0
  7. package/demo-prompts.md +118 -0
  8. package/lib/assets/echarts.js +64 -0
  9. package/lib/assets/mermaid.js +3591 -0
  10. package/lib/assets/three.js +3844 -0
  11. package/lib/client.js +5 -0
  12. package/lib/index.js +1895 -0
  13. package/lib/invariant.js +19 -0
  14. package/lib/types/client/EChartNode.d.ts +4 -0
  15. package/lib/types/client/ErrorBoundary.d.ts +30 -0
  16. package/lib/types/client/GenuiBlock.d.ts +7 -0
  17. package/lib/types/client/PlotBlock.d.ts +44 -0
  18. package/lib/types/client/action-context.d.ts +7 -0
  19. package/lib/types/client/asset-echarts.d.ts +15 -0
  20. package/lib/types/client/asset-loader.d.ts +31 -0
  21. package/lib/types/client/asset-mermaid.d.ts +1 -0
  22. package/lib/types/client/asset-three.d.ts +1 -0
  23. package/lib/types/client/blocks/advanced.d.ts +104 -0
  24. package/lib/types/client/blocks/basic.d.ts +27 -0
  25. package/lib/types/client/blocks/charts.d.ts +38 -0
  26. package/lib/types/client/blocks/diagram/geometry.d.ts +73 -0
  27. package/lib/types/client/blocks/diagram/index.d.ts +5 -0
  28. package/lib/types/client/blocks/diagram/layout.d.ts +41 -0
  29. package/lib/types/client/blocks/diagram/theme.d.ts +35 -0
  30. package/lib/types/client/blocks/forms.d.ts +83 -0
  31. package/lib/types/client/blocks/render-node.d.ts +10 -0
  32. package/lib/types/client/blocks/state.d.ts +58 -0
  33. package/lib/types/client/dom-fence.d.ts +14 -0
  34. package/lib/types/client/echarts-lazy.d.ts +17 -0
  35. package/lib/types/client/fence-render.d.ts +58 -0
  36. package/lib/types/client/gallery.d.ts +9 -0
  37. package/lib/types/client/guard.d.ts +126 -0
  38. package/lib/types/client/index.d.ts +36 -0
  39. package/lib/types/client/interaction-store.d.ts +43 -0
  40. package/lib/types/client/mermaid-core.d.ts +8 -0
  41. package/lib/types/client/mermaid-lazy.d.ts +9 -0
  42. package/lib/types/client/mermaid-safe.d.ts +52 -0
  43. package/lib/types/client/panel-command.d.ts +29 -0
  44. package/lib/types/client/panel-store.d.ts +94 -0
  45. package/lib/types/client/panel.d.ts +15 -0
  46. package/lib/types/client/parse-partial.d.ts +37 -0
  47. package/lib/types/client/safe-math.d.ts +43 -0
  48. package/lib/types/client/scene3d-core.d.ts +21 -0
  49. package/lib/types/client/scene3d-lazy.d.ts +8 -0
  50. package/lib/types/client/spec.d.ts +610 -0
  51. package/lib/types/client/toolview.d.ts +7 -0
  52. package/lib/types/index.d.ts +1 -0
  53. package/lib/types/plugin/index.d.ts +25 -0
  54. package/lib/types/plugin/invariant.d.ts +15 -0
  55. package/lib/types/plugin/tool.d.ts +27 -0
  56. package/lib/types/shared/fence-repair.d.ts +67 -0
  57. package/package.json +121 -0
  58. package/src/client/EChartNode.tsx +242 -0
  59. package/src/client/ErrorBoundary.tsx +66 -0
  60. package/src/client/GenuiBlock.module.css +1263 -0
  61. package/src/client/GenuiBlock.tsx +166 -0
  62. package/src/client/PlotBlock.module.css +155 -0
  63. package/src/client/PlotBlock.tsx +431 -0
  64. package/src/client/action-context.ts +32 -0
  65. package/src/client/asset-echarts.ts +25 -0
  66. package/src/client/asset-loader.ts +79 -0
  67. package/src/client/asset-mermaid.ts +13 -0
  68. package/src/client/asset-three.ts +12 -0
  69. package/src/client/blocks/advanced.tsx +435 -0
  70. package/src/client/blocks/basic.tsx +116 -0
  71. package/src/client/blocks/charts.tsx +319 -0
  72. package/src/client/blocks/diagram/geometry.ts +151 -0
  73. package/src/client/blocks/diagram/index.tsx +284 -0
  74. package/src/client/blocks/diagram/layout.ts +199 -0
  75. package/src/client/blocks/diagram/theme.ts +108 -0
  76. package/src/client/blocks/forms.tsx +523 -0
  77. package/src/client/blocks/render-node.tsx +260 -0
  78. package/src/client/blocks/state.ts +61 -0
  79. package/src/client/dom-fence.tsx +641 -0
  80. package/src/client/echarts-lazy.ts +35 -0
  81. package/src/client/fence-render.tsx +230 -0
  82. package/src/client/gallery.ts +128 -0
  83. package/src/client/guard.ts +1450 -0
  84. package/src/client/index.tsx +195 -0
  85. package/src/client/interaction-store.ts +122 -0
  86. package/src/client/mermaid-core.ts +121 -0
  87. package/src/client/mermaid-lazy.ts +31 -0
  88. package/src/client/mermaid-safe.ts +122 -0
  89. package/src/client/panel-command.ts +119 -0
  90. package/src/client/panel-store.ts +471 -0
  91. package/src/client/panel.tsx +185 -0
  92. package/src/client/parse-partial.ts +162 -0
  93. package/src/client/safe-math.ts +270 -0
  94. package/src/client/scene3d-core.ts +192 -0
  95. package/src/client/scene3d-lazy.ts +26 -0
  96. package/src/client/spec.ts +767 -0
  97. package/src/client/toolview.tsx +66 -0
  98. package/src/css-modules.d.ts +5 -0
  99. package/src/index.ts +12 -0
  100. package/src/plugin/index.ts +173 -0
  101. package/src/plugin/invariant.ts +30 -0
  102. package/src/plugin/node-shims.d.ts +31 -0
  103. package/src/plugin/tool.ts +330 -0
  104. package/src/shared/fence-repair.ts +244 -0
  105. package/tsconfig.json +35 -0
  106. package/tsdown.config.ts +193 -0
@@ -0,0 +1,19 @@
1
+ //#region src/plugin/invariant.ts
2
+ const PACKAGE_NAME = "@changfenhuang/dsh-genui";
3
+ /** Cordis companion plugin name. */
4
+ const name = "genui-invariant";
5
+ /** Service required before the companion can reserve package ownership. */
6
+ const inject = ["invariants"];
7
+ /**
8
+ * No runtime invariant: this host plugin only registers a static prompt
9
+ * section; the section text is pinned by its own tests.
10
+ */
11
+ const install = () => {};
12
+ /**
13
+ * Register this package's invariant companion.
14
+ * @param ctx - Cordis context carrying the invariant service.
15
+ * @returns the installed registration's disposer after setup succeeds.
16
+ */
17
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
18
+ //#endregion
19
+ export { apply, inject, name };
@@ -0,0 +1,4 @@
1
+ import type { GenuiEChart } from './spec.ts';
2
+ export declare function EChartNode({ node }: {
3
+ node: GenuiEChart;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Rendering error boundary: a component render failure inside one GenUI
3
+ * block must never take down the whole chat surface (pre-2026-08-09 builds
4
+ * crashed the entire conversation tree on a missing API). Every fence,
5
+ * toolview card and panel body renders under this boundary; on error the
6
+ * block degrades to a compact inline alert instead of unmounting the tree.
7
+ * @module @changfenhuang/dsh-genui/client
8
+ */
9
+ import { Component, type ErrorInfo, type ReactNode } from 'react';
10
+ interface ErrorBoundaryProps {
11
+ /** What the block is, shown in the fallback alert (fence / tool / panel). */
12
+ label?: string;
13
+ children: ReactNode;
14
+ }
15
+ interface ErrorBoundaryState {
16
+ error: Error | null;
17
+ }
18
+ /**
19
+ * Class boundary because function components cannot catch their own subtree
20
+ * errors. `getDerivedStateFromError` flips to the fallback render; the error
21
+ * itself is logged (and its message surfaced in the alert) so the user sees
22
+ * a hint instead of a silent white region.
23
+ */
24
+ export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
25
+ state: ErrorBoundaryState;
26
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState;
27
+ componentDidCatch(error: Error, info: ErrorInfo): void;
28
+ render(): ReactNode;
29
+ }
30
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { GenuiBlockProps } from './blocks/state.ts';
2
+ export declare const GENUI_ACTION_DEBOUNCE_MS = 300;
3
+ /**
4
+ * Render a GenUI spec as an inline block. Falls back to nothing when the spec
5
+ * carries no items (the fence renderer already refused non-specs before us).
6
+ */
7
+ export declare const GenuiBlock: import("react").NamedExoticComponent<GenuiBlockProps>;
@@ -0,0 +1,44 @@
1
+ export interface PlotSeriesParam {
2
+ name: string;
3
+ value: number;
4
+ min?: number | undefined;
5
+ max?: number | undefined;
6
+ step?: number | undefined;
7
+ /** v1.5: animate this parameter from its initial value to this target. */
8
+ animateTo?: number | undefined;
9
+ /** Animation duration in ms (default 4000). */
10
+ durationMs?: number | undefined;
11
+ /** Loop the animation (default false). */
12
+ loop?: boolean | undefined;
13
+ }
14
+ export interface PlotSeries {
15
+ /** Math expression in x. */
16
+ expr: string;
17
+ /** Optional label shown in a legend row. */
18
+ label?: string | undefined;
19
+ /** Stroke color; defaults to the accent token (multi-series auto-assign). */
20
+ color?: string | undefined;
21
+ /** v2.9 draw shape: line (default), area (fill to baseline), scatter. */
22
+ kind?: 'line' | 'area' | 'scatter' | undefined;
23
+ /** v2: adjustable parameters, one slider each, live re-render. */
24
+ params?: PlotSeriesParam[] | undefined;
25
+ }
26
+ /** Categorical palette for multi-series plots: host static tokens only
27
+ * (design system v2 — same families as the chart/avatar palettes in
28
+ * GenuiBlock, no off-theme hexes). CSS custom properties resolve inside
29
+ * inline `style` on SVG strokes and legend swatches. */
30
+ export declare const PLOT_COLORS: string[];
31
+ export interface PlotBlockProps {
32
+ /** Functions to draw, in draw order. */
33
+ series: PlotSeries[];
34
+ /** Horizontal range, inclusive. */
35
+ xMin?: number | undefined;
36
+ xMax?: number | undefined;
37
+ /** Vertical bounds; omitted = auto-fit to the sampled data. */
38
+ yMin?: number | undefined;
39
+ yMax?: number | undefined;
40
+ /** Chart title. */
41
+ title?: string | undefined;
42
+ }
43
+ /** Render one plot: grid, axes, one polyline per series, legend, sliders. */
44
+ export declare const PlotBlock: import("react").NamedExoticComponent<PlotBlockProps>;
@@ -0,0 +1,7 @@
1
+ import type { Context } from 'react';
2
+ /** v2 action handler: component action + its collected data. */
3
+ export type GenuiActionHandler = (action: string, payload: Record<string, unknown>) => void;
4
+ /** Host-provided context when the deployment ships it, else the local one. */
5
+ export declare const GenuiActionContext: Context<GenuiActionHandler | undefined>;
6
+ /** Read the installed action handler, if any. */
7
+ export declare function useGenuiAction(): GenuiActionHandler | undefined;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * ECharts asset-bundle entry: registers the echarts engine on
3
+ * `window.__GenuiAssets__.echarts`. Built as a standalone IIFE into
4
+ * `lib/assets/echarts.js` and served by the plugin's node-half route; loaded
5
+ * on demand by echarts-lazy when a spec contains an `echart` node.
6
+ * @module @changfenhuang/dsh-genui/client/asset-echarts
7
+ */
8
+ import { type EChartsType, type EChartsCoreOption } from 'echarts';
9
+ /** The engine surface registered by the echarts asset bundle. */
10
+ export interface EChartsAssetApi {
11
+ /** Create an ECharts instance on `el`, apply `option`, return the instance. */
12
+ createChart: (el: HTMLElement, option: EChartsCoreOption, opts?: {
13
+ height?: number;
14
+ }) => EChartsType;
15
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Asset-bundle loader: fetches one of the plugin's lazy engine bundles
3
+ * (`lib/assets/mermaid.js` / `lib/assets/three.js`) by script injection and
4
+ * hands back the engine surface the bundle registered on
5
+ * `window.__GenuiAssets__`.
6
+ *
7
+ * The bundles are served by the plugin's OWN node-half HTTP route
8
+ * (`/plugins/@changfenhuang/dsh-genui/assets/*`), registered through the host
9
+ * webserver service — no host source change needed, and the longest-prefix
10
+ * rule lets this route win over the generic `/plugins` bundle route. Each
11
+ * file is loaded at most once per page (promise memoized); a rejection is
12
+ * final for the page (the node's fallback UI takes over).
13
+ *
14
+ * The rev query rides the boot graph: the plugin's own row in
15
+ * `window.__DSH_BOOT__` carries the client bundle rev, so a plugin rebuild
16
+ * busts the asset cache together with the main bundle. Absent graph = no
17
+ * query; the route serves no-cache anyway.
18
+ * @module @changfenhuang/dsh-genui/client/asset-loader
19
+ */
20
+ /** Resolve an asset URL, appending the bundle rev for cache busting when the
21
+ * boot graph exposes it. */
22
+ export declare function assetUrl(file: string): string;
23
+ /**
24
+ * Load one asset bundle and return the engines it registered. Memoized per
25
+ * file: repeated requests (several mermaid nodes, re-renders) share one
26
+ * script load; a failed load stays failed for the page (the component shows
27
+ * its fallback).
28
+ * @param name - 'mermaid', 'three', or 'echarts'.
29
+ * @returns the registered engine surface.
30
+ */
31
+ export declare function loadGenuiAsset<T>(name: 'mermaid' | 'three' | 'echarts'): Promise<T>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,104 @@
1
+ import type { AnswersState, GenuiBlockProps } from './state.ts';
2
+ import type { GenuiAccordion, GenuiBreadcrumb, GenuiCallout, GenuiCode, GenuiCopy, GenuiDiff, GenuiFileTree, GenuiJson, GenuiKeyValue, GenuiMermaid, GenuiPlot, GenuiQuiz, GenuiScene3D, GenuiSteps, GenuiTabs, GenuiTimeline } from '../spec.ts';
3
+ /** Callout: a tinted notice box with an optional heading. */
4
+ export declare const CalloutNode: import("react").NamedExoticComponent<{
5
+ node: GenuiCallout;
6
+ }>;
7
+ /** Steps: a vertical progress checklist with an optional current index. */
8
+ export declare const StepsNode: import("react").NamedExoticComponent<{
9
+ steps: GenuiSteps;
10
+ }>;
11
+ /** KeyValue: a definition list for configs and metadata. */
12
+ export declare const KeyValueNode: import("react").NamedExoticComponent<{
13
+ node: GenuiKeyValue;
14
+ }>;
15
+ /** Plot: SVG function plot over the SafeMath evaluator. The series mapping
16
+ * is memoized on the stable spec node so the memo boundary actually skips
17
+ * sibling-state re-renders (a fresh mapped array would defeat it). */
18
+ export declare const PlotNode: import("react").NamedExoticComponent<{
19
+ plot: GenuiPlot;
20
+ }>;
21
+ /** Diff: 收编 dsh DiffBlock (same path/oldText/newText shape as DiffHunk). */
22
+ export declare const DiffNode: import("react").NamedExoticComponent<{
23
+ node: GenuiDiff;
24
+ }>;
25
+ /** Json: 收编 dsh JsonTree. */
26
+ export declare const JsonNode: import("react").NamedExoticComponent<{
27
+ node: GenuiJson;
28
+ }>;
29
+ /** Code: 收编 dsh CodeBlock with explicit language. */
30
+ export declare const CodeNode: import("react").NamedExoticComponent<{
31
+ node: GenuiCode;
32
+ }>;
33
+ /**
34
+ * Table: LOCAL sorting (v2.9) — click a header to sort ascending, click
35
+ * again for descending, a third click restores the spec order. Zero model
36
+ * round trip. Numeric cells (numbers or numeric strings) compare numerically;
37
+ * everything else compares as text.
38
+ */
39
+ export declare function TabsNode({ tabs, onAction, depth, answers }: {
40
+ tabs: GenuiTabs;
41
+ onAction?: GenuiBlockProps['onAction'];
42
+ depth?: number;
43
+ answers?: AnswersState | undefined;
44
+ }): import("react").JSX.Element;
45
+ /** Radio: one option from a group; local selection state. The group name is
46
+ * useId-based so sibling groups never collide (deterministic per mount).
47
+ *
48
+ * v2.5 aggregation: when `group` is set, the selection is recorded into the
49
+ * block-wide answers registry instead of firing a per-click action — a
50
+ * sibling `submit` node then grades the paper IN PLACE (v2.6, questions
51
+ * carry `answer` data) or collects all groups in ONE action. Without
52
+ * `group`, the legacy per-click action fires. After a local grading the
53
+ * group locks until 重新作答 resets it. */
54
+ export declare function AccordionNode({ node, onAction, depth, answers }: {
55
+ node: GenuiAccordion;
56
+ onAction?: GenuiBlockProps['onAction'];
57
+ depth?: number;
58
+ answers?: AnswersState | undefined;
59
+ }): import("react").JSX.Element;
60
+ /** Copy: a one-click copy chip. The live region is a visually-hidden SIBLING
61
+ * (not inside the button) — button content is atomic to screen readers, so a
62
+ * live region inside it would never announce. */
63
+ export declare const CopyNode: import("react").NamedExoticComponent<{
64
+ node: GenuiCopy;
65
+ }>;
66
+ /** Mermaid: lazily loaded diagram renderer. */
67
+ export declare const MermaidNode: import("react").NamedExoticComponent<{
68
+ node: GenuiMermaid;
69
+ }>;
70
+ /** Scene3D: three.js WebGL canvas, lazily imported. */
71
+ export declare const Scene3DNode: import("react").NamedExoticComponent<{
72
+ node: GenuiScene3D;
73
+ }>;
74
+ /** Timeline: vertical event list with time markers. */
75
+ export declare const TimelineNode: import("react").NamedExoticComponent<{
76
+ node: GenuiTimeline;
77
+ }>;
78
+ /** FileTree: indented tree of files and folders. Directory rows are LOCAL
79
+ * collapsible (spec.ts promised "collapsible children"; this makes it true)
80
+ * — click a dir to fold/unfold, default fully open. Zero model round trip. */
81
+ export declare const FileTreeNode: import("react").NamedExoticComponent<{
82
+ node: GenuiFileTree;
83
+ }>;
84
+ /** Quiz: a self-contained teaching question. Selecting an option marks it
85
+ * correct/incorrect in place and reveals feedback + explanation. With
86
+ * `action`, the chosen answer is ALSO sent back to the model
87
+ * (`{type:'quiz', question, answer, correct}`) so the model can collect or
88
+ * grade it — the in-place judging stays local (no round trip needed). */
89
+ export declare const QuizNode: import("react").NamedExoticComponent<{
90
+ node: GenuiQuiz;
91
+ onAction?: GenuiBlockProps["onAction"];
92
+ }>;
93
+ /** Breadcrumb: path-style navigation trail. */
94
+ export declare const BreadcrumbNode: import("react").NamedExoticComponent<{
95
+ node: GenuiBreadcrumb;
96
+ }>;
97
+ /**
98
+ * Trailing debounce window (ms) for one `[genui-action]` name: rapid
99
+ * repeated interactions on one control (button mashing, switch flipping)
100
+ * collapse into a single action with the LAST payload. Different action
101
+ * names stay independent. The model round-trip takes seconds, so a few
102
+ * hundred ms of trailing delay is imperceptible — and it stops bursts of
103
+ * queued user turns.
104
+ */
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Basic display family: avatar palette, and the local click-feedback button
3
+ * (the actionable-button chip). Used by the render dispatcher.
4
+ * @module @changfenhuang/dsh-genui/client/blocks/basic
5
+ */
6
+ import { type ReactNode } from 'react';
7
+ import type { GenuiAudio, GenuiVideo } from '../spec.ts';
8
+ export declare function avatarColor(name: string): string;
9
+ /** Button with LOCAL click feedback: clicking an actionable button shows a
10
+ * brief "✓ 已触发" chip so the user sees the click registered even while the
11
+ * model round trip is in flight — no more "点了没反应" perception. The chip
12
+ * is purely cosmetic; the action fires through `onClick` as before. */
13
+ export declare function ClickFeedbackButton({ className, disabled, onClick, children }: {
14
+ className: string;
15
+ disabled?: boolean;
16
+ onClick?: (() => void) | undefined;
17
+ children: ReactNode;
18
+ }): import("react").JSX.Element;
19
+ /** Native controls intentionally own play/pause/seek/volume. Model-authored
20
+ * autoplay and controls hints are ignored: media starts only after the user
21
+ * asks for it. */
22
+ export declare const AudioNode: import("react").NamedExoticComponent<{
23
+ node: GenuiAudio;
24
+ }>;
25
+ export declare const VideoNode: import("react").NamedExoticComponent<{
26
+ node: GenuiVideo;
27
+ }>;
@@ -0,0 +1,38 @@
1
+ import type { GenuiChart, GenuiTable } from '../spec.ts';
2
+ export declare const CHART_COLORS: string[];
3
+ /**
4
+ * Sortable numeric value of a cell. Human-written table cells are rarely
5
+ * plain numbers, so the sort accepts the usual decorations:
6
+ * `1,234` / `1,234`(千分位)、`1.2k`/`3M`/`5b`、`3.5万`/`2亿`、`0.3%`、
7
+ * `¥99`/`$12`。A cell that cannot be read as a number returns NaN and the
8
+ * row falls back to the text comparison — mixed columns sort deterministically
9
+ * (numbers first, then text).
10
+ */
11
+ export declare function parseSortableNumber(v: unknown): number;
12
+ export declare const TableNode: import("react").NamedExoticComponent<{
13
+ node: GenuiTable;
14
+ }>;
15
+ /** Chart: bars (default), line (trend), or donut (share); multi-series bars via `series`.
16
+ * memoized: the spec node is a stable reference, so a keystroke in a sibling
17
+ * field no longer re-renders the whole chart. */
18
+ export declare const ChartNode: import("react").NamedExoticComponent<{
19
+ chart: GenuiChart;
20
+ }>;
21
+ /** Bars: one column per datum (grouped bars when `series` is present). */
22
+ export declare const BarsNode: import("react").NamedExoticComponent<{
23
+ chart: GenuiChart;
24
+ }>;
25
+ /** Line: polyline over a fixed-height plot area with a readable Y axis —
26
+ * four evenly spaced gridlines + tick labels (design system v2 skeleton). */
27
+ export declare const LineChartNode: import("react").NamedExoticComponent<{
28
+ chart: GenuiChart;
29
+ }>;
30
+ /** Donut: share of total with a center total. Negative values contribute
31
+ * zero arc (a negative dasharray segment used to produce an invalid
32
+ * stroke-dasharray and the browser drew the FULL circle instead). */
33
+ export declare const DonutNode: import("react").NamedExoticComponent<{
34
+ chart: GenuiChart;
35
+ }>;
36
+ /** Tab strip with local active-tab state. Keyboard: ArrowLeft/Right to move,
37
+ * Home/End to jump; ids wired via useId so `aria-controls` stays unique
38
+ * across fences and sessions. */
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Editorial diagram geometry: the mandatory orthogonal-connector system from
3
+ * diagram-design §6, ported to pure functions producing SVG path strings.
4
+ *
5
+ * Rules enforced here (the "non-negotiable" list):
6
+ * 1. Rounded right-angle (orthogonal) elbows with r=8 — never diagonal lines
7
+ * between off-axis nodes.
8
+ * 2. Edge labels sit 6–10px off the stroke behind an opaque mask.
9
+ * 3. No two connectors share a path — parallel connectors offset ≥12px.
10
+ * 4. Shared-edge attach points fan ≥12px apart.
11
+ * 5. A connector never passes behind a non-endpoint box (dashed-transit
12
+ * exception only when geometrically unavoidable).
13
+ * 6. Label masks never overlap nodes (nodes paint after labels).
14
+ * @module @changfenhuang/dsh-genui/client/blocks/diagram/geometry
15
+ */
16
+ /** Axis-aligned box. */
17
+ export interface Box {
18
+ x: number;
19
+ y: number;
20
+ w: number;
21
+ h: number;
22
+ }
23
+ export interface Point {
24
+ x: number;
25
+ y: number;
26
+ }
27
+ export type Port = 'left' | 'right' | 'top' | 'bottom';
28
+ /** Attachment on a box edge. */
29
+ export interface Attach {
30
+ x: number;
31
+ y: number;
32
+ port: Port;
33
+ }
34
+ /** Center of a box. */
35
+ export declare function center(b: Box): Point;
36
+ /**
37
+ * Pick the attach point on `from` toward `to`. Port choice follows the
38
+ * dominant axis: mostly-horizontal travel uses left/right ports, mostly-
39
+ * vertical uses top/bottom. An explicit `prefer` overrides the heuristic.
40
+ */
41
+ export declare function attachPoint(box: Box, toward: Box, prefer: Port | undefined): Attach;
42
+ /**
43
+ * Build an orthogonal path between two attach points. Same-axis endpoints use
44
+ * a straight segment (the one sanctioned case); every bend is a quarter arc
45
+ * of radius R.
46
+ */
47
+ export declare function orthogonalPath(a: Attach, b: Attach): string;
48
+ /**
49
+ * Route a connector between two boxes with default port selection, returning
50
+ * the path plus both attach points (for label placement and fan offsets).
51
+ */
52
+ export declare function routeEdge(from: Box, to: Box, opts?: {
53
+ fromPort?: Port;
54
+ toPort?: Port;
55
+ }): {
56
+ d: string;
57
+ a: Attach;
58
+ b: Attach;
59
+ };
60
+ /** Fan an attach point along a box edge (≥12px apart via the (k+1)/(n+1) rule). */
61
+ export declare function fanPoint(box: Box, edge: 'top' | 'bottom' | 'left' | 'right', index: number, count: number): Attach;
62
+ /**
63
+ * Edge-label geometry: centered on the dominant segment, offset 6–10px from
64
+ * the stroke so the connector stays visible.
65
+ */
66
+ export declare function labelGeometry(a: Attach, b: Attach): {
67
+ cx: number;
68
+ cy: number;
69
+ maskY: number;
70
+ vertical: boolean;
71
+ };
72
+ /** Hop / bridge arc for a crossing connector (applied to the less important edge). */
73
+ export declare function bridgePath(cx: number, cy: number, horizontal: boolean): string;
@@ -0,0 +1,5 @@
1
+ import type { GenuiDiagram } from '../../spec.ts';
2
+ /** The `diagram` node renderer. */
3
+ export declare function DiagramNode({ node }: {
4
+ node: GenuiDiagram;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Editorial diagram layout: resolves a `diagram` spec into positioned boxes
3
+ * and routed edges. Two layout modes mirror diagram-design's grammar:
4
+ *
5
+ * - Coordinate kinds (architecture, it-state, high-level, process,
6
+ * medallion, data-flow, dp-integration): the spec carries x/y/w/h per
7
+ * node; this module passes them through (4px-grid already enforced by the
8
+ * guard) and routes edges.
9
+ * - Rule kinds (flowchart, sequence, state, er, timeline, swimlane,
10
+ * quadrant, radar, loop, nested, tree, org-chart, layers, venn, pyramid,
11
+ * bar, line, gantt, scatter, dp-security-matrix): the spec carries data
12
+ * only; this module lays the nodes out on the editorial grid.
13
+ *
14
+ * The output is a plain layout object consumed by the SVG renderer — no
15
+ * React, no DOM, fully unit-testable.
16
+ * @module @changfenhuang/dsh-genui/client/blocks/diagram/layout
17
+ */
18
+ import type { GenuiDiagram, GenuiDiagramEdge, GenuiDiagramNode } from '../../spec.ts';
19
+ import { Box } from './geometry.ts';
20
+ export interface LayoutNode {
21
+ node: GenuiDiagramNode;
22
+ box: Box;
23
+ }
24
+ export interface LayoutEdge {
25
+ edge: GenuiDiagramEdge;
26
+ /** Resolved box references for routing. */
27
+ fromId: string;
28
+ toId: string;
29
+ }
30
+ export interface DiagramLayout {
31
+ nodes: LayoutNode[];
32
+ edges: LayoutEdge[];
33
+ /** Canvas size (width/height). */
34
+ width: number;
35
+ height: number;
36
+ }
37
+ /**
38
+ * Resolve a diagram spec into a positioned layout. `parentOf` lets tree-like
39
+ * kinds pass their hierarchy; other rule kinds use a generic grid.
40
+ */
41
+ export declare function resolveLayout(diagram: GenuiDiagram, parentOf?: (id: string) => string | undefined): DiagramLayout;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Editorial diagram theme: the diagram-design semantic-token system ported
3
+ * into the dsh-genui renderer. Every diagram draws from these roles — never
4
+ * from inline hex in the layout code — and a spec-level `theme` override
5
+ * merges over the defaults, mirroring diagram-design's style-guide.md.
6
+ *
7
+ * Two skins ship: light and dark (the dark palette is the jet-black
8
+ * inversion, same roles). The `editorial` variant is the full skin and
9
+ * defaults to light unless the host theme is dark.
10
+ * @module @changfenhuang/dsh-genui/client/blocks/diagram/theme
11
+ */
12
+ import type { GenuiDiagramTheme, GenuiDiagramVariant } from '../../spec.ts';
13
+ export interface DiagramPalette {
14
+ paper: string;
15
+ paper2: string;
16
+ ink: string;
17
+ muted: string;
18
+ soft: string;
19
+ rule: string;
20
+ accent: string;
21
+ accentTint: string;
22
+ link: string;
23
+ }
24
+ /** Resolve the active palette from variant + optional theme overrides. */
25
+ export declare function resolvePalette(variant: GenuiDiagramVariant | undefined, theme: GenuiDiagramTheme | undefined): DiagramPalette;
26
+ /** Node treatment → { fill, stroke, dashed } per diagram-design §5. */
27
+ export declare function nodeTreatment(type: string | undefined, p: DiagramPalette): {
28
+ fill: string;
29
+ stroke: string;
30
+ dashed?: boolean;
31
+ };
32
+ /** Edge stroke color per semantic kind. */
33
+ export declare function edgeStroke(kind: string | undefined, p: DiagramPalette): string;
34
+ /** Helper: a color at a given opacity (accepts #hex and rgba() strings). */
35
+ export declare function inkAt(color: string, opacity: number): string;
@@ -0,0 +1,83 @@
1
+ import type { AnswersState, GenuiBlockProps, QuestionMeta } from './state.ts';
2
+ import type { GenuiInput, GenuiRadio, GenuiSelect, GenuiSlider, GenuiSubmit, GenuiSwitch, GenuiTextarea } from '../spec.ts';
3
+ export declare function RadioNode({ node, onAction, answers }: {
4
+ node: GenuiRadio;
5
+ onAction?: GenuiBlockProps['onAction'];
6
+ answers?: AnswersState | undefined;
7
+ }): import("react").JSX.Element;
8
+ /** Resolve a question's correct label from its metadata. */
9
+ export declare function correctLabelOf(m: QuestionMeta): string | undefined;
10
+ /** Submit: the "交卷" control of a grouped-radio block. LOCAL-FIRST (v2.6):
11
+ * when at least one question carries `answer` data the click grades IN PLACE
12
+ * — score, per-question right/wrong, explanations — with zero model round
13
+ * trip, and locks the questions until 重新作答 resets them. Only when NO
14
+ * question has answers does it fall back to firing ONE action
15
+ * (`{type:'submit', answers, total, answered}`). Disabled until the
16
+ * selection criteria are met (all listed groups answered, or ≥1 answer
17
+ * without a group list); the hint shows the progress. */
18
+ export declare function SubmitNode({ node, onAction, answers }: {
19
+ node: GenuiSubmit;
20
+ onAction?: GenuiBlockProps['onAction'];
21
+ answers?: AnswersState | undefined;
22
+ }): import("react").JSX.Element;
23
+ /** Switch: toggle with local state. */
24
+ export declare function SwitchNode({ node, onAction }: {
25
+ node: GenuiSwitch;
26
+ onAction?: GenuiBlockProps['onAction'];
27
+ }): import("react").JSX.Element;
28
+ /** Slider: range input for numeric form values (v2.9). Field-aligned: with an
29
+ * `id` the value persists across refresh and joins the sibling submit's
30
+ * `fields` collection (stored as the numeric string); a model-provided
31
+ * default registers at mount; a restored durable value wins. Dragging fires
32
+ * the action (block-level debounce collapses the drag into one delivery). */
33
+ export declare function SliderNode({ node, onAction, answers }: {
34
+ node: GenuiSlider;
35
+ onAction?: GenuiBlockProps['onAction'];
36
+ answers?: AnswersState | undefined;
37
+ }): import("react").JSX.Element;
38
+ /** Reuse the DSH main input's three-layer IME protection (verified in the
39
+ * host InputBar): composition start arms a ref, composition end clears it
40
+ * 10ms later (Safari sends the closing keydown BEFORE compositionend), and
41
+ * every submit keydown re-checks the ref, the native `isComposing` flag,
42
+ * and `keyCode === 229`. A Chinese selection Enter must never submit. */
43
+ export declare function useImeComposing(): {
44
+ isComposing: () => boolean;
45
+ onCompositionStart: () => void;
46
+ onCompositionEnd: () => void;
47
+ };
48
+ export declare function isImeSubmitKeydown(e: React.KeyboardEvent): boolean;
49
+ /** Select: single choice from a dropdown, field-aligned (v2.8). With an `id`
50
+ * the chosen option persists across refresh and joins the sibling submit's
51
+ * `fields` collection; a model-provided `selected` default registers at
52
+ * mount; a restored durable value wins over both. Without any default a
53
+ * placeholder option shows — nothing is silently pre-registered (same
54
+ * philosophy as radio). */
55
+ export declare function SelectNode({ node, onAction, answers }: {
56
+ node: GenuiSelect;
57
+ onAction?: GenuiBlockProps['onAction'];
58
+ answers?: AnswersState | undefined;
59
+ }): import("react").JSX.Element;
60
+ /** Input: single-line field. Controlled (value tracked for persistence and
61
+ * submit collection when `id` is set). With `action`: Enter submits
62
+ * immediately (`{type:'input', value, submit:true}`), blur sends too —
63
+ * the user never has to click elsewhere for the value to reach the model.
64
+ * Enter during IME composition never submits. `inputType: 'password'`
65
+ * stays masked; its value is never persisted and never joins submit
66
+ * collection (secrets stay out of localStorage), while its own `action`
67
+ * still delivers on explicit user submit. */
68
+ export declare function InputNode({ node, onAction, answers }: {
69
+ node: GenuiInput;
70
+ onAction?: GenuiBlockProps['onAction'];
71
+ answers?: AnswersState | undefined;
72
+ }): import("react").JSX.Element;
73
+ /** Textarea: multi-line input; with `action`, blurring sends the value and
74
+ * Ctrl/Cmd+Enter submits immediately. Controlled when `id` is set (durable
75
+ * value + submit collection). Ctrl/Cmd+Enter during IME composition never
76
+ * submits. */
77
+ export declare function TextareaNode({ node, onAction, answers }: {
78
+ node: GenuiTextarea;
79
+ onAction?: GenuiBlockProps['onAction'];
80
+ answers?: AnswersState | undefined;
81
+ }): import("react").JSX.Element;
82
+ /** Accordion: collapsible sections with local open state. Headings and
83
+ * bodies are wired via useId (`aria-controls`/`aria-labelledby`). */
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The recursive render dispatcher: maps the white-listed GenuiNode union to
3
+ * concrete components. Leaf cases render inline; compound families live in
4
+ * the sibling block modules. Depth-guarded against pathological specs.
5
+ * @module @changfenhuang/dsh-genui/client/blocks/render-node
6
+ */
7
+ import { type ReactNode } from 'react';
8
+ import type { GenuiNode } from '../spec.ts';
9
+ import type { AnswersState, GenuiBlockProps } from './state.ts';
10
+ export declare function renderNode(node: GenuiNode, key: number, onAction: GenuiBlockProps['onAction'] | undefined, depth?: number, answers?: AnswersState): ReactNode;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared block-level state types: the block props contract and the answers
3
+ * registry (grouped radios → submit grading). Lives OUTSIDE GenuiBlock.tsx so
4
+ * the per-family block modules can import the types without a cycle back
5
+ * into the block shell.
6
+ * @module @changfenhuang/dsh-genui/client/blocks/state
7
+ */
8
+ import type { GenuiSpec } from '../spec.ts';
9
+ export interface GenuiBlockProps {
10
+ /** Parsed spec to render. */
11
+ spec: GenuiSpec;
12
+ /**
13
+ * v2: optional action callback. Interactive components carrying an
14
+ * `action` field fire it (button click, switch toggle, form submit);
15
+ * absent = components are display-only (v1 behavior).
16
+ */
17
+ onAction?: ((action: string, payload: Record<string, unknown>) => void) | undefined;
18
+ /**
19
+ * v2.7: durable-state key (session + slot + content fingerprint). When set,
20
+ * interaction state (radio answers, submit lock, field values) persists to
21
+ * localStorage and restores on refresh / re-render of the same content.
22
+ */
23
+ stateKey?: string | undefined;
24
+ }
25
+ /** Per-question metadata registered by grouped radios for local grading. */
26
+ export interface QuestionMeta {
27
+ label: string;
28
+ options: string[];
29
+ /** Correct option: index (number) or label (string); absent = no local grading. */
30
+ answer?: number | string | undefined;
31
+ /** Shown after local grading. */
32
+ explanation?: string | undefined;
33
+ }
34
+ /** Block-wide answers registry (v2.5/v2.6): grouped radios record selections
35
+ * and register their question metadata here; a sibling `submit` node either
36
+ * grades IN PLACE (questions carry `answer` data) or collects everything
37
+ * into ONE action. Lives in GenuiBlock state so re-renders keep the
38
+ * selections, threaded down through the recursive render walk. Answers are
39
+ * plain group → chosen-option-label strings (the display label lives in
40
+ * QuestionMeta; localStorage stores the same string table — no migration). */
41
+ export interface AnswersState {
42
+ answers: Record<string, string>;
43
+ /** Field values by id (input/textarea with an `id`), collected by submit. */
44
+ fields: Record<string, string>;
45
+ /** Field ids whose value must never be persisted or collected (secrets). */
46
+ secretFields: ReadonlySet<string>;
47
+ meta: Record<string, QuestionMeta>;
48
+ /** True after a local grading: questions are locked until 重新作答. */
49
+ locked: boolean;
50
+ /** Bumped by every reset; radios use it as their remount key. */
51
+ round: number;
52
+ setAnswer: (group: string, choice: string) => void;
53
+ setField: (id: string, value: string) => void;
54
+ registerSecretField: (id: string) => void;
55
+ registerMeta: (group: string, meta: QuestionMeta) => void;
56
+ clear: () => void;
57
+ setLocked: (locked: boolean) => void;
58
+ }