@crossworks/share-ui 0.230.43
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/LICENSE.md +135 -0
- package/package.json +68 -0
- package/src/app-bridge-protocol.ts +115 -0
- package/src/app-presenter.tsx +25 -0
- package/src/app-sandbox.tsx +552 -0
- package/src/appearance.ts +192 -0
- package/src/avatar.test.ts +229 -0
- package/src/avatar.ts +731 -0
- package/src/backgrounds.test.ts +119 -0
- package/src/backgrounds.ts +118 -0
- package/src/draw-presenter.tsx +39 -0
- package/src/event-presenter.tsx +62 -0
- package/src/file-presenter.tsx +76 -0
- package/src/formula-calculator.tsx +209 -0
- package/src/formula-presenter.test.ts +128 -0
- package/src/formula-presenter.tsx +301 -0
- package/src/help-topics.ts +104 -0
- package/src/lib/ink-audit.test.ts +314 -0
- package/src/lib/theme-css-blocks.ts +26 -0
- package/src/lib/theme-generator.test.ts +179 -0
- package/src/lib/theme-registry.gen.ts +352 -0
- package/src/lib/themes.test.ts +308 -0
- package/src/lib/themes.ts +75 -0
- package/src/lib/utils.ts +6 -0
- package/src/nav-items.ts +225 -0
- package/src/note-presenter.tsx +14 -0
- package/src/page-outline.tsx +127 -0
- package/src/table-presenter.tsx +226 -0
- package/src/task-presenter.tsx +60 -0
- package/src/ui/button.tsx +50 -0
- package/src/ui/input.tsx +18 -0
- package/src/ui/label.tsx +20 -0
- package/src/view-payload.ts +82 -0
- package/styles/app.css +1098 -0
- package/styles/themes.css +6198 -0
- package/themes/generate.d.mts +11 -0
- package/themes/generate.mjs +618 -0
- package/themes/model.d.mts +24 -0
- package/themes/model.mjs +213 -0
- package/themes/preview.html +145 -0
- package/themes/seeds.d.mts +16 -0
- package/themes/seeds.mjs +3694 -0
- package/tsconfig.json +15 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createElement } from 'react';
|
|
3
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
4
|
+
import { checkDimensions } from '@mantle/content-core/formula-dimensions';
|
|
5
|
+
import { checkLookupCoverage, parseFormulaSpec } from '@mantle/content-core/formula-spec';
|
|
6
|
+
import { FormulaPresenter } from './formula-presenter';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The public page is server-rendered on purpose: a shared engineering
|
|
10
|
+
* calculation that appears without its caveats is worse than one that does not
|
|
11
|
+
* appear at all. These tests hold the static HTML — no island, no JavaScript —
|
|
12
|
+
* to carrying everything a reader needs in order to distrust the number.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const spec = (() => {
|
|
16
|
+
const parsed = parseFormulaSpec({
|
|
17
|
+
id: 'shared-model',
|
|
18
|
+
name: 'Shared model',
|
|
19
|
+
source: { standard: 'Example standard', part: '3', edition: '2024', sections: ['5.3'] },
|
|
20
|
+
unitSystem: 'USC',
|
|
21
|
+
variables: [
|
|
22
|
+
{ symbol: 'Ps', name: 'Storage pressure', unit: 'lbf/in2 (abs)', role: 'input' },
|
|
23
|
+
{ symbol: 'k', role: 'constant', value: 1.5 },
|
|
24
|
+
],
|
|
25
|
+
expressions: [
|
|
26
|
+
{
|
|
27
|
+
id: 'sound',
|
|
28
|
+
equation: '3.6',
|
|
29
|
+
resultSymbol: 'W',
|
|
30
|
+
unit: 'lb/sec',
|
|
31
|
+
expression: '{Ps} * {k}',
|
|
32
|
+
latex: 'W = P_s k',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'guessed',
|
|
36
|
+
equation: '3.7',
|
|
37
|
+
expression: '{Ps} / {k}',
|
|
38
|
+
unverified: 'Supplied from memory; the source never defines it.',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
piecewise: [],
|
|
42
|
+
lookups: [
|
|
43
|
+
{
|
|
44
|
+
id: 'fact_di',
|
|
45
|
+
name: 'Reduction factor',
|
|
46
|
+
keys: ['detection'],
|
|
47
|
+
result: 'fact_di',
|
|
48
|
+
domains: { detection: ['A', 'B', 'C'] },
|
|
49
|
+
rows: [
|
|
50
|
+
{ detection: 'A', fact_di: 0.25 },
|
|
51
|
+
{ detection: 'B', fact_di: 0.1 },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
classifications: [
|
|
56
|
+
{
|
|
57
|
+
id: 'detection-rating',
|
|
58
|
+
domain: ['A', 'B', 'C'],
|
|
59
|
+
criteria: {
|
|
60
|
+
A: 'Instrumentation designed specifically to detect material losses.',
|
|
61
|
+
B: 'Suitably located detectors.',
|
|
62
|
+
C: 'Visual detection only.',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
notes: { transition: 'The source branches on a threshold it never defines.' },
|
|
67
|
+
});
|
|
68
|
+
if (!parsed.ok) throw new Error(parsed.errors.join('; '));
|
|
69
|
+
return parsed.spec;
|
|
70
|
+
})();
|
|
71
|
+
|
|
72
|
+
const html = renderToStaticMarkup(
|
|
73
|
+
createElement(FormulaPresenter, {
|
|
74
|
+
view: {
|
|
75
|
+
title: 'Shared model',
|
|
76
|
+
spec,
|
|
77
|
+
coverageGaps: checkLookupCoverage(spec),
|
|
78
|
+
dimensionIssues: checkDimensions(spec),
|
|
79
|
+
},
|
|
80
|
+
// The /s surface's shape: pre-rendered island markup behind its own
|
|
81
|
+
// dangerouslySetInnerHTML wrapper (the /team reader mounts the live
|
|
82
|
+
// component instead — the prop is a ReactNode either way).
|
|
83
|
+
calculator: createElement('div', {
|
|
84
|
+
dangerouslySetInnerHTML: { __html: '<div data-island="formula-calculator"></div>' },
|
|
85
|
+
}),
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
describe('FormulaPresenter', () => {
|
|
90
|
+
it('renders the title and a full citation', () => {
|
|
91
|
+
expect(html).toContain('Shared model');
|
|
92
|
+
expect(html).toContain('Example standard Part 3 (2024)');
|
|
93
|
+
expect(html).toContain('§5.3');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('warns about an unverified equation WITHOUT any JavaScript', () => {
|
|
97
|
+
// The whole reason the static half exists. If this ever moves into the
|
|
98
|
+
// island, a visitor with a blocked bundle reads a fabricated equation
|
|
99
|
+
// number as though it were transcribed.
|
|
100
|
+
expect(html).toContain('not read from the source');
|
|
101
|
+
expect(html).toContain('Supplied from memory');
|
|
102
|
+
expect(html).toContain('guessed');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('names the key combinations the source leaves unspecified', () => {
|
|
106
|
+
// detection=C is declared legal but has no row.
|
|
107
|
+
expect(html).toContain('unspecified');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('renders the equations, the lookup rows and the rating criteria', () => {
|
|
111
|
+
expect(html).toContain('Eq 3.6');
|
|
112
|
+
expect(html).toContain('0.25');
|
|
113
|
+
expect(html).toContain('Instrumentation designed specifically');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('keeps the transcription notes — what the source got wrong travels too', () => {
|
|
117
|
+
expect(html).toContain('branches on a threshold it never defines');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('mounts the calculator island', () => {
|
|
121
|
+
expect(html).toContain('data-island="formula-calculator"');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('renders LaTeX server-side rather than shipping the source string raw', () => {
|
|
125
|
+
// KaTeX output, not the `latex` field verbatim.
|
|
126
|
+
expect(html).toContain('katex');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import katex from 'katex';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type {
|
|
4
|
+
CoverageGap,
|
|
5
|
+
FormulaSpec,
|
|
6
|
+
FormulaValue,
|
|
7
|
+
SpecLookup,
|
|
8
|
+
} from '@mantle/content-core/formula-spec';
|
|
9
|
+
import type { DimensionIssue } from '@mantle/content-core/formula-dimensions';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Public render of a shared formula: the calculation model, its citation, and
|
|
13
|
+
* every caveat that travels with it.
|
|
14
|
+
*
|
|
15
|
+
* The static half lives here (server-rendered to HTML, no hydration); the live
|
|
16
|
+
* calculator mounts separately as a client island. That split is deliberate —
|
|
17
|
+
* the equations, the tables and above all the WARNINGS must be readable with
|
|
18
|
+
* JavaScript off or broken, because a shared engineering calculation that
|
|
19
|
+
* silently renders without its `unverified` notice is worse than one that does
|
|
20
|
+
* not render at all.
|
|
21
|
+
*
|
|
22
|
+
* KaTeX runs server-side with `trust: false` (its default, pinned because the
|
|
23
|
+
* output goes through dangerouslySetInnerHTML): a spec transcribed by an agent
|
|
24
|
+
* from a pasted document must not be able to inject a link or fetch a remote
|
|
25
|
+
* asset onto a public page.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
function cell(v: FormulaValue | undefined): string {
|
|
29
|
+
if (v === null || v === undefined) return '';
|
|
30
|
+
return String(v);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function citation(spec: FormulaSpec): string | null {
|
|
34
|
+
const s = spec.source;
|
|
35
|
+
if (!s) return null;
|
|
36
|
+
const head = [s.standard, s.part ? `Part ${s.part}` : '', s.edition ? `(${s.edition})` : '']
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join(' ');
|
|
39
|
+
const sections = s.sections?.length ? `, §${s.sections.join(', §')}` : '';
|
|
40
|
+
const tables = s.tables?.length ? `, Tables ${s.tables.join(', ')}` : '';
|
|
41
|
+
return `${head}${sections}${tables}`.trim() || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Equation({ latex, expression }: { latex?: string; expression: string }) {
|
|
45
|
+
if (latex) {
|
|
46
|
+
let html: string | null;
|
|
47
|
+
try {
|
|
48
|
+
html = katex.renderToString(latex, { displayMode: true, throwOnError: false, trust: false });
|
|
49
|
+
} catch {
|
|
50
|
+
// Unrenderable display string — fall through to the literal expression,
|
|
51
|
+
// which is what is actually computed anyway.
|
|
52
|
+
html = null;
|
|
53
|
+
}
|
|
54
|
+
if (html) {
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
className="overflow-x-auto py-2"
|
|
58
|
+
// KaTeX output from the spec's own display string, trust:false.
|
|
59
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return (
|
|
65
|
+
<pre className="overflow-x-auto rounded-md bg-muted px-3 py-2 text-xs text-muted-foreground">
|
|
66
|
+
<code>{expression}</code>
|
|
67
|
+
</pre>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
72
|
+
return (
|
|
73
|
+
<section className="mt-10 space-y-3">
|
|
74
|
+
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
|
|
75
|
+
{children}
|
|
76
|
+
</section>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function LookupTable({ lookup }: { lookup: SpecLookup }) {
|
|
81
|
+
const columns = [...lookup.keys, lookup.result];
|
|
82
|
+
return (
|
|
83
|
+
<div className="overflow-x-auto">
|
|
84
|
+
<table className="w-full text-sm">
|
|
85
|
+
<thead>
|
|
86
|
+
<tr className="border-b border-border">
|
|
87
|
+
{columns.map((c) => (
|
|
88
|
+
<th key={c} className="px-2 py-1.5 text-left font-mono text-xs text-muted-foreground">
|
|
89
|
+
{c}
|
|
90
|
+
</th>
|
|
91
|
+
))}
|
|
92
|
+
</tr>
|
|
93
|
+
</thead>
|
|
94
|
+
<tbody>
|
|
95
|
+
{lookup.rows.map((row, i) => (
|
|
96
|
+
<tr key={i} className="border-b border-border/50">
|
|
97
|
+
{columns.map((c) => (
|
|
98
|
+
<td key={c} className="px-2 py-1.5 font-mono text-xs">
|
|
99
|
+
{cell(row[c])}
|
|
100
|
+
</td>
|
|
101
|
+
))}
|
|
102
|
+
</tr>
|
|
103
|
+
))}
|
|
104
|
+
</tbody>
|
|
105
|
+
</table>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type FormulaShareView = {
|
|
111
|
+
title: string;
|
|
112
|
+
spec: FormulaSpec;
|
|
113
|
+
coverageGaps: CoverageGap[];
|
|
114
|
+
dimensionIssues: DimensionIssue[];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export function FormulaPresenter({
|
|
118
|
+
view,
|
|
119
|
+
calculator,
|
|
120
|
+
}: {
|
|
121
|
+
view: FormulaShareView;
|
|
122
|
+
/** The live calculator: the /s surface passes pre-rendered island markup
|
|
123
|
+
* (wrapped in its own dangerouslySetInnerHTML div); the /team inline
|
|
124
|
+
* reader mounts <FormulaCalculator> directly. */
|
|
125
|
+
calculator: ReactNode;
|
|
126
|
+
}) {
|
|
127
|
+
const { spec } = view;
|
|
128
|
+
const cite = citation(spec);
|
|
129
|
+
const unverified = spec.expressions.filter((e) => e.unverified);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<article className="mx-auto max-w-3xl px-6 py-12 md:py-16">
|
|
133
|
+
<h1 className="text-3xl font-bold tracking-tight text-balance">{view.title}</h1>
|
|
134
|
+
{cite ? <p className="mt-2 text-sm text-muted-foreground">{cite}</p> : null}
|
|
135
|
+
{spec.unitSystem ? (
|
|
136
|
+
<p className="mt-1 text-xs text-muted-foreground">Unit system: {spec.unitSystem}</p>
|
|
137
|
+
) : null}
|
|
138
|
+
|
|
139
|
+
{/* Caveats first. Someone who reads nothing else must still see these. */}
|
|
140
|
+
{unverified.length > 0 ? (
|
|
141
|
+
<div className="mt-6 rounded-md border border-destructive/40 bg-destructive/5 p-4">
|
|
142
|
+
<p className="text-sm font-medium text-foreground">
|
|
143
|
+
{unverified.length} equation{unverified.length === 1 ? '' : 's'} here{' '}
|
|
144
|
+
{unverified.length === 1 ? 'was' : 'were'} not read from the source
|
|
145
|
+
</p>
|
|
146
|
+
<ul className="mt-2 space-y-1 text-xs text-muted-foreground">
|
|
147
|
+
{unverified.map((e) => (
|
|
148
|
+
<li key={e.id}>
|
|
149
|
+
<code>{e.id}</code> — {e.unverified}
|
|
150
|
+
</li>
|
|
151
|
+
))}
|
|
152
|
+
</ul>
|
|
153
|
+
</div>
|
|
154
|
+
) : null}
|
|
155
|
+
|
|
156
|
+
{view.dimensionIssues.length > 0 ? (
|
|
157
|
+
<div className="mt-4 rounded-md border border-destructive/40 bg-destructive/5 p-4">
|
|
158
|
+
<p className="text-sm font-medium text-foreground">
|
|
159
|
+
The arithmetic disagrees with a declared unit
|
|
160
|
+
</p>
|
|
161
|
+
<ul className="mt-2 space-y-1 text-xs text-muted-foreground">
|
|
162
|
+
{view.dimensionIssues.map((d, i) => (
|
|
163
|
+
<li key={i}>
|
|
164
|
+
<code>{d.id}</code> — {d.detail}
|
|
165
|
+
</li>
|
|
166
|
+
))}
|
|
167
|
+
</ul>
|
|
168
|
+
</div>
|
|
169
|
+
) : null}
|
|
170
|
+
|
|
171
|
+
{view.coverageGaps.length > 0 ? (
|
|
172
|
+
<div className="mt-4 rounded-md border border-border bg-muted/50 p-4">
|
|
173
|
+
<p className="text-sm font-medium text-foreground">
|
|
174
|
+
The source leaves {view.coverageGaps.length} key combination
|
|
175
|
+
{view.coverageGaps.length === 1 ? '' : 's'} unspecified
|
|
176
|
+
</p>
|
|
177
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
178
|
+
Evaluating one of these is an error rather than a zero.
|
|
179
|
+
</p>
|
|
180
|
+
</div>
|
|
181
|
+
) : null}
|
|
182
|
+
|
|
183
|
+
{/* The calculator: the reason a link exists rather than a screenshot. */}
|
|
184
|
+
<div className="mt-10">{calculator}</div>
|
|
185
|
+
|
|
186
|
+
{spec.expressions.length > 0 ? (
|
|
187
|
+
<Section title="Equations">
|
|
188
|
+
<div className="space-y-5">
|
|
189
|
+
{spec.expressions.map((e) => (
|
|
190
|
+
<div key={e.id} className="space-y-1">
|
|
191
|
+
<div className="flex flex-wrap items-baseline gap-2 text-xs text-muted-foreground">
|
|
192
|
+
<code className="font-medium text-foreground">{e.id}</code>
|
|
193
|
+
{e.equation ? <span>Eq {e.equation}</span> : null}
|
|
194
|
+
{e.resultSymbol ? (
|
|
195
|
+
<span>
|
|
196
|
+
→ {e.resultSymbol}
|
|
197
|
+
{e.unit ? ` [${e.unit}]` : ''}
|
|
198
|
+
</span>
|
|
199
|
+
) : null}
|
|
200
|
+
</div>
|
|
201
|
+
<Equation latex={e.latex} expression={e.expression} />
|
|
202
|
+
{e.unverified ? (
|
|
203
|
+
<p className="text-xs text-destructive-ink">Unverified — {e.unverified}</p>
|
|
204
|
+
) : null}
|
|
205
|
+
{e.note ? <p className="text-xs text-muted-foreground">{e.note}</p> : null}
|
|
206
|
+
</div>
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
209
|
+
</Section>
|
|
210
|
+
) : null}
|
|
211
|
+
|
|
212
|
+
{spec.piecewise.length > 0 ? (
|
|
213
|
+
<Section title="Conditional selection">
|
|
214
|
+
{spec.piecewise.map((p) => (
|
|
215
|
+
<div key={p.id} className="space-y-1 text-sm">
|
|
216
|
+
<code className="text-xs font-medium text-foreground">{p.id}</code>
|
|
217
|
+
<ul className="space-y-1 text-xs text-muted-foreground">
|
|
218
|
+
{p.cases.map((c, i) => (
|
|
219
|
+
<li key={i}>
|
|
220
|
+
{c.label ? <span className="text-foreground">{c.label}</span> : null} when{' '}
|
|
221
|
+
<code>{c.when}</code> use <code>{c.use}</code>
|
|
222
|
+
</li>
|
|
223
|
+
))}
|
|
224
|
+
{p.otherwise ? (
|
|
225
|
+
<li>
|
|
226
|
+
otherwise use <code>{p.otherwise}</code>
|
|
227
|
+
</li>
|
|
228
|
+
) : null}
|
|
229
|
+
</ul>
|
|
230
|
+
</div>
|
|
231
|
+
))}
|
|
232
|
+
</Section>
|
|
233
|
+
) : null}
|
|
234
|
+
|
|
235
|
+
{spec.variables.length > 0 ? (
|
|
236
|
+
<Section title="Variables">
|
|
237
|
+
<div className="overflow-x-auto">
|
|
238
|
+
<table className="w-full text-sm">
|
|
239
|
+
<thead>
|
|
240
|
+
<tr className="border-b border-border">
|
|
241
|
+
{['Symbol', 'Description', 'Value', 'Unit', 'Role'].map((h) => (
|
|
242
|
+
<th key={h} className="px-2 py-1.5 text-left text-xs text-muted-foreground">
|
|
243
|
+
{h}
|
|
244
|
+
</th>
|
|
245
|
+
))}
|
|
246
|
+
</tr>
|
|
247
|
+
</thead>
|
|
248
|
+
<tbody>
|
|
249
|
+
{spec.variables.map((v) => (
|
|
250
|
+
<tr key={v.symbol} className="border-b border-border/50">
|
|
251
|
+
<td className="px-2 py-1.5 font-mono text-xs">{v.symbol}</td>
|
|
252
|
+
<td className="px-2 py-1.5 text-xs">{v.name ?? ''}</td>
|
|
253
|
+
<td className="px-2 py-1.5 font-mono text-xs">
|
|
254
|
+
{v.expression ?? (v.value !== undefined ? String(v.value) : '')}
|
|
255
|
+
</td>
|
|
256
|
+
<td className="px-2 py-1.5 text-xs text-muted-foreground">{v.unit ?? ''}</td>
|
|
257
|
+
<td className="px-2 py-1.5 text-xs text-muted-foreground">{v.role}</td>
|
|
258
|
+
</tr>
|
|
259
|
+
))}
|
|
260
|
+
</tbody>
|
|
261
|
+
</table>
|
|
262
|
+
</div>
|
|
263
|
+
</Section>
|
|
264
|
+
) : null}
|
|
265
|
+
|
|
266
|
+
{spec.lookups.map((lookup) => (
|
|
267
|
+
<Section key={lookup.id} title={lookup.name ?? lookup.id}>
|
|
268
|
+
<LookupTable lookup={lookup} />
|
|
269
|
+
</Section>
|
|
270
|
+
))}
|
|
271
|
+
|
|
272
|
+
{spec.classifications.map((c) => (
|
|
273
|
+
<Section key={c.id} title={c.id}>
|
|
274
|
+
<dl className="space-y-2 text-sm">
|
|
275
|
+
{c.domain.map((value) => (
|
|
276
|
+
<div key={value} className="flex gap-3">
|
|
277
|
+
<dt className="w-8 shrink-0 font-mono text-xs font-medium text-foreground">
|
|
278
|
+
{value}
|
|
279
|
+
</dt>
|
|
280
|
+
<dd className="text-xs text-muted-foreground">{c.criteria[value]}</dd>
|
|
281
|
+
</div>
|
|
282
|
+
))}
|
|
283
|
+
</dl>
|
|
284
|
+
</Section>
|
|
285
|
+
))}
|
|
286
|
+
|
|
287
|
+
{spec.notes && Object.keys(spec.notes).length > 0 ? (
|
|
288
|
+
<Section title="Transcription notes">
|
|
289
|
+
<dl className="space-y-2">
|
|
290
|
+
{Object.entries(spec.notes).map(([key, text]) => (
|
|
291
|
+
<div key={key}>
|
|
292
|
+
<dt className="font-mono text-xs font-medium text-foreground">{key}</dt>
|
|
293
|
+
<dd className="text-xs text-muted-foreground">{text}</dd>
|
|
294
|
+
</div>
|
|
295
|
+
))}
|
|
296
|
+
</dl>
|
|
297
|
+
</Section>
|
|
298
|
+
) : null}
|
|
299
|
+
</article>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route → help-topic map. The header's "?" button reads this to decide which
|
|
3
|
+
* topic to open; a route with no topic renders no button at all, so coverage
|
|
4
|
+
* can grow screen by screen without anything breaking in between.
|
|
5
|
+
*
|
|
6
|
+
* Matching is LONGEST-PREFIX, so `/settings/agents` beats `/settings`, and a
|
|
7
|
+
* detail route (`/tables/abc-123`) inherits its list route's topic. Keeping the
|
|
8
|
+
* map here — rather than a <SetPageHelp> tag in 80 page files — means the set is
|
|
9
|
+
* exhaustive by construction and checkable in CI (help-topics.test.ts asserts
|
|
10
|
+
* every topic named here has a content file, and vice versa).
|
|
11
|
+
*
|
|
12
|
+
* Topic slugs are file names under `docs/guide/06-help/<topic>.md`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Ordered longest-first at module load, so lookup is a plain scan. */
|
|
16
|
+
const ROUTE_TOPICS: ReadonlyArray<readonly [route: string, topic: string]> = [
|
|
17
|
+
// ── Workspace ──────────────────────────────────────────────────────────
|
|
18
|
+
['/tables', 'tables'],
|
|
19
|
+
['/pages', 'pages'],
|
|
20
|
+
['/draw', 'draw'],
|
|
21
|
+
['/notes', 'notes'],
|
|
22
|
+
['/files', 'files'],
|
|
23
|
+
['/assistant', 'assistant'],
|
|
24
|
+
['/journal', 'journal'],
|
|
25
|
+
['/tasks', 'tasks'],
|
|
26
|
+
['/events', 'events'],
|
|
27
|
+
['/contacts', 'contacts'],
|
|
28
|
+
['/secrets', 'secrets'],
|
|
29
|
+
// The mail screen lives at /inbox; the topic keeps the user-facing name.
|
|
30
|
+
['/inbox', 'inbox'],
|
|
31
|
+
['/formulas', 'formulas'],
|
|
32
|
+
['/apps', 'apps'],
|
|
33
|
+
['/docs', 'docs'],
|
|
34
|
+
|
|
35
|
+
// ── Review ─────────────────────────────────────────────────────────────
|
|
36
|
+
['/models', 'models'],
|
|
37
|
+
['/settings/discover', 'discover'],
|
|
38
|
+
['/team-admin', 'team-admin'],
|
|
39
|
+
['/pending', 'pending'],
|
|
40
|
+
|
|
41
|
+
// ── External ───────────────────────────────────────────────────────────
|
|
42
|
+
['/team-portal', 'team-portal'],
|
|
43
|
+
|
|
44
|
+
// ── Settings ───────────────────────────────────────────────────────────
|
|
45
|
+
['/settings/agents', 'agents'],
|
|
46
|
+
['/settings/ai-workers', 'ai-workers'],
|
|
47
|
+
['/settings/tools', 'tools'],
|
|
48
|
+
['/settings/tool-groups', 'tool-groups'],
|
|
49
|
+
['/settings/skills', 'skills'],
|
|
50
|
+
['/settings/keys', 'keys'],
|
|
51
|
+
['/settings/embedding', 'embedding'],
|
|
52
|
+
['/settings/heartbeats', 'heartbeats'],
|
|
53
|
+
['/settings/worker-groups', 'worker-groups'],
|
|
54
|
+
['/settings/appearance', 'appearance'],
|
|
55
|
+
['/settings/accounts', 'accounts'],
|
|
56
|
+
['/settings/microsoft', 'microsoft'],
|
|
57
|
+
['/settings/calendar', 'calendar'],
|
|
58
|
+
['/settings/profile', 'profile'],
|
|
59
|
+
['/settings/mcp', 'mcp'],
|
|
60
|
+
['/settings/network', 'network'],
|
|
61
|
+
['/settings/config', 'config'],
|
|
62
|
+
['/settings/entities', 'entities'],
|
|
63
|
+
['/settings/peers', 'peers'],
|
|
64
|
+
['/settings/pdf-passwords', 'pdf-passwords'],
|
|
65
|
+
['/settings/backups', 'backups'],
|
|
66
|
+
['/settings/updates', 'updates'],
|
|
67
|
+
['/settings/users', 'users'],
|
|
68
|
+
['/settings/audit', 'audit'],
|
|
69
|
+
|
|
70
|
+
// ── System ─────────────────────────────────────────────────────────────
|
|
71
|
+
['/studio', 'studio'],
|
|
72
|
+
['/dev-tools', 'dev-tools'],
|
|
73
|
+
['/runners', 'runners'],
|
|
74
|
+
['/runs', 'runs'],
|
|
75
|
+
['/sandboxes', 'sandboxes'],
|
|
76
|
+
['/traces', 'traces'],
|
|
77
|
+
['/debug', 'debug'],
|
|
78
|
+
|
|
79
|
+
// Dashboard. Matched exact-only by the '/' special case below.
|
|
80
|
+
['/', 'dashboard'],
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const SORTED = [...ROUTE_TOPICS].sort((a, b) => b[0].length - a[0].length);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The help topic for a pathname, or null when the screen has none yet.
|
|
87
|
+
* `/` matches only itself — a bare prefix test would hand it every route.
|
|
88
|
+
*/
|
|
89
|
+
export function helpTopicForPath(pathname: string): string | null {
|
|
90
|
+
const path = pathname.replace(/\/+$/, '') || '/';
|
|
91
|
+
for (const [route, topic] of SORTED) {
|
|
92
|
+
if (route === '/') {
|
|
93
|
+
if (path === '/') return topic;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (path === route || path.startsWith(`${route}/`)) return topic;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Every topic slug the map references — the CI drift test's left-hand side. */
|
|
102
|
+
export function allHelpTopics(): string[] {
|
|
103
|
+
return [...new Set(ROUTE_TOPICS.map(([, topic]) => topic))].sort();
|
|
104
|
+
}
|