@flamingo-stack/openframe-frontend-core 0.0.739 → 0.0.741
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/export-map.json +1 -1
- package/dist/{chunk-ENCAIAMY.cjs → chunk-4XL4YEZM.cjs} +3 -3
- package/dist/{chunk-FL6UO7IR.cjs → chunk-5SVOGLNK.cjs} +2 -2
- package/dist/{chunk-WFPCCBOM.cjs → chunk-7AM3AFM4.cjs} +10 -10
- package/dist/{chunk-S2CA5WE4.cjs → chunk-CWRVTQUO.cjs} +9 -1
- package/dist/{chunk-KHXZFLIN.cjs → chunk-DKOLQPE7.cjs} +8 -2
- package/dist/{chunk-UXFJXIUT.cjs → chunk-EKGL5K5K.cjs} +2 -2
- package/dist/{chunk-DTLHY2ZF.cjs → chunk-FQN6STIM.cjs} +12 -155
- package/dist/{chunk-V2ZFH42N.cjs → chunk-HYBMADYO.cjs} +3 -3
- package/dist/{chunk-NX6JQT25.cjs → chunk-I6UE6YVM.cjs} +4 -4
- package/dist/{chunk-SB4VWA5V.cjs → chunk-IK2RJUEA.cjs} +2118 -268
- package/dist/{chunk-CUDTMCXK.cjs → chunk-ITPEFCQ2.cjs} +13 -13
- package/dist/{chunk-WMKNZI4G.cjs → chunk-JFS6TZDX.cjs} +22 -22
- package/dist/{chunk-RGFT3P3L.cjs → chunk-LZ6NOVXX.cjs} +55 -55
- package/dist/{chunk-TF7J6VXG.cjs → chunk-QEUGYHFB.cjs} +9 -9
- package/dist/{chunk-II4QMH26.cjs → chunk-STEJZPIO.cjs} +3 -3
- package/dist/{chunk-UI3HROSI.cjs → chunk-XJ36TEGE.cjs} +33 -33
- package/dist/{chunk-UYP3G6PB.cjs → chunk-YQ37ZCD2.cjs} +2 -2
- package/dist/{chunk-Q7GG3WC5.cjs → chunk-ZJPDGVA6.cjs} +5 -5
- package/dist/{chunk-EGN4I5Q7.cjs → chunk-ZYOS7LVQ.cjs} +134 -46
- package/dist/{cloud-tenants-QSFSO3PA.cjs → cloud-tenants-MVJK52I2.cjs} +13 -13
- package/dist/components/case-studies/index.cjs +11 -11
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/features/capability-explorer.d.ts +9 -2
- package/dist/components/features/capability-explorer.d.ts.map +1 -1
- package/dist/components/features/capability-explorer.js +113 -6
- package/dist/components/features/index.cjs +141 -3
- package/dist/components/features/index.d.ts +1 -0
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +1 -0
- package/dist/components/features/scripts/execution-cells.d.ts +65 -0
- package/dist/components/features/scripts/execution-cells.d.ts.map +1 -0
- package/dist/components/features/scripts/execution-cells.js +75 -0
- package/dist/components/features/scripts/execution-status.d.ts +48 -0
- package/dist/components/features/scripts/execution-status.d.ts.map +1 -0
- package/dist/components/features/scripts/execution-status.js +15 -0
- package/dist/components/features/scripts/index.d.ts +20 -0
- package/dist/components/features/scripts/index.d.ts.map +1 -0
- package/dist/components/features/scripts/index.js +59 -0
- package/dist/components/features/scripts/schedule-form/index.d.ts +18 -0
- package/dist/components/features/scripts/schedule-form/index.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/index.js +84 -0
- package/dist/components/features/scripts/schedule-form/schedule-enums.d.ts +24 -0
- package/dist/components/features/scripts/schedule-form/schedule-enums.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-enums.js +18 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-fields.d.ts +57 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-fields.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-fields.js +169 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-types.d.ts +138 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-types.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-types.js +72 -0
- package/dist/components/features/scripts/schedule-form/schedule-interval-input.d.ts +54 -0
- package/dist/components/features/scripts/schedule-form/schedule-interval-input.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-interval-input.js +60 -0
- package/dist/components/features/scripts/schedule-form/schedule-offline-fields.d.ts +29 -0
- package/dist/components/features/scripts/schedule-form/schedule-offline-fields.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-offline-fields.js +124 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.d.ts +52 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.js +244 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-search-field.d.ts +35 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-search-field.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-search-field.js +58 -0
- package/dist/components/features/scripts/schedule-form/schedule-scripts-field.d.ts +25 -0
- package/dist/components/features/scripts/schedule-form/schedule-scripts-field.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-scripts-field.js +66 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing-fields.d.ts +77 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing-fields.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing-fields.js +262 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing.d.ts +288 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing.js +165 -0
- package/dist/components/features/scripts/script-cells.d.ts +11 -0
- package/dist/components/features/scripts/script-cells.d.ts.map +1 -0
- package/dist/components/features/scripts/script-cells.js +19 -0
- package/dist/components/features/scripts/script-executions-table.d.ts +58 -0
- package/dist/components/features/scripts/script-executions-table.d.ts.map +1 -0
- package/dist/components/features/scripts/script-executions-table.js +88 -0
- package/dist/components/features/scripts/script-schedules-table.d.ts +38 -0
- package/dist/components/features/scripts/script-schedules-table.d.ts.map +1 -0
- package/dist/components/features/scripts/script-schedules-table.js +71 -0
- package/dist/components/features/scripts/script-shell-badge.d.ts +9 -0
- package/dist/components/features/scripts/script-shell-badge.d.ts.map +1 -0
- package/dist/components/features/scripts/script-shell-badge.js +15 -0
- package/dist/components/features/scripts/scripts-table-columns.d.ts +162 -0
- package/dist/components/features/scripts/scripts-table-columns.d.ts.map +1 -0
- package/dist/components/features/scripts/scripts-table-columns.js +69 -0
- package/dist/components/features/scripts/scripts-table-shared.d.ts +19 -0
- package/dist/components/features/scripts/scripts-table-shared.d.ts.map +1 -0
- package/dist/components/features/scripts/scripts-table-shared.js +35 -0
- package/dist/components/features/scripts/scripts-table.d.ts +40 -0
- package/dist/components/features/scripts/scripts-table.d.ts.map +1 -0
- package/dist/components/features/scripts/scripts-table.js +84 -0
- package/dist/components/features/scripts/scripts-tabs.d.ts +5 -0
- package/dist/components/features/scripts/scripts-tabs.d.ts.map +1 -0
- package/dist/components/features/scripts/scripts-tabs.js +10 -0
- package/dist/components/features/scripts/shell-types.d.ts +13 -0
- package/dist/components/features/scripts/shell-types.d.ts.map +1 -0
- package/dist/components/features/scripts/shell-types.js +21 -0
- package/dist/components/help-center-pages/index.cjs +53 -53
- package/dist/components/index.cjs +178 -34
- package/dist/components/meeting-scheduler/index.cjs +17 -17
- package/dist/components/navigation/index.cjs +6 -6
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/product-demo/fixtures/scripts.d.ts +34 -0
- package/dist/components/product-demo/fixtures/scripts.d.ts.map +1 -0
- package/dist/components/product-demo/fixtures/scripts.js +123 -0
- package/dist/components/product-demo/fixtures/shared.d.ts +8 -0
- package/dist/components/product-demo/fixtures/shared.d.ts.map +1 -1
- package/dist/components/product-demo/fixtures/shared.js +7 -1
- package/dist/components/product-demo/fixtures/software-update.d.ts.map +1 -1
- package/dist/components/product-demo/fixtures/software-update.js +1 -5
- package/dist/components/product-demo/index.cjs +16 -13
- package/dist/components/product-demo/product-screen.d.ts.map +1 -1
- package/dist/components/product-demo/product-screen.js +2 -1
- package/dist/components/product-demo/registry.d.ts.map +1 -1
- package/dist/components/product-demo/registry.js +2 -1
- package/dist/components/product-demo/screen-keys.cjs +2 -1
- package/dist/components/product-demo/screen-keys.d.ts +1 -0
- package/dist/components/product-demo/screen-keys.d.ts.map +1 -1
- package/dist/components/product-demo/screen-keys.js +2 -1
- package/dist/components/product-demo/screens/scripts.d.ts +11 -0
- package/dist/components/product-demo/screens/scripts.d.ts.map +1 -0
- package/dist/components/product-demo/screens/scripts.js +130 -0
- package/dist/components/shared/legal-document/index.d.ts +2 -1
- package/dist/components/shared/legal-document/index.d.ts.map +1 -1
- package/dist/components/shared/legal-document/index.js +10 -2
- package/dist/components/shared/legal-document/legal-document-page.d.ts +35 -23
- package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
- package/dist/components/shared/legal-document/legal-document-page.js +27 -10
- package/dist/components/shared/legal-document/use-legal-docs.d.ts +7 -3
- package/dist/components/shared/legal-document/use-legal-docs.d.ts.map +1 -1
- package/dist/components/shared/legal-document/use-legal-docs.js +7 -0
- package/dist/components/shared/legal-document/use-legal-documents.d.ts +31 -0
- package/dist/components/shared/legal-document/use-legal-documents.d.ts.map +1 -0
- package/dist/components/shared/legal-document/use-legal-documents.js +67 -0
- package/dist/components/tickets/index.cjs +8 -8
- package/dist/components/ui/index.cjs +3 -3
- package/dist/{devices-C7XLWJFC.cjs → devices-W3S7Q2PG.cjs} +9 -9
- package/dist/index.cjs +150 -8
- package/dist/{knowledge-3QKWTOE7.cjs → knowledge-AIYQ4FC3.cjs} +7 -7
- package/dist/{logs-JXD75NR4.cjs → logs-7SHJ2ARE.cjs} +25 -25
- package/dist/{policies-ZPN53PFQ.cjs → policies-EIZJ5SUP.cjs} +10 -10
- package/dist/{remote-session-DNFZ5OLY.cjs → remote-session-BZMNE6D2.cjs} +5 -5
- package/dist/scripts-J2FWC7E4.cjs +2112 -0
- package/dist/{software-update-CRSIBS3K.cjs → software-update-QAMNASVF.cjs} +14 -18
- package/dist/{tickets-board-CKZRB6YV.cjs → tickets-board-CEDHD3KB.cjs} +17 -17
- package/dist/utils/index.cjs +8 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/legal-documents.d.ts +18 -0
- package/dist/utils/legal-documents.d.ts.map +1 -0
- package/dist/utils/legal-documents.js +8 -0
- package/package.json +1 -1
- package/src/components/features/capability-explorer.tsx +134 -7
- package/src/components/features/index.ts +1 -0
- package/src/components/features/scripts/execution-cells.tsx +149 -0
- package/src/components/features/scripts/execution-status.ts +42 -0
- package/src/components/features/scripts/index.ts +42 -0
- package/src/components/features/scripts/schedule-form/index.ts +59 -0
- package/src/components/features/scripts/schedule-form/schedule-enums.ts +26 -0
- package/src/components/features/scripts/schedule-form/schedule-form-fields.tsx +249 -0
- package/src/components/features/scripts/schedule-form/schedule-form-types.ts +189 -0
- package/src/components/features/scripts/schedule-form/schedule-interval-input.tsx +121 -0
- package/src/components/features/scripts/schedule-form/schedule-offline-fields.tsx +218 -0
- package/src/components/features/scripts/schedule-form/schedule-script-picker-card.tsx +321 -0
- package/src/components/features/scripts/schedule-form/schedule-script-search-field.tsx +115 -0
- package/src/components/features/scripts/schedule-form/schedule-scripts-field.tsx +97 -0
- package/src/components/features/scripts/schedule-form/schedule-timing-fields.tsx +482 -0
- package/src/components/features/scripts/schedule-form/schedule-timing.ts +484 -0
- package/src/components/features/scripts/script-cells.tsx +29 -0
- package/src/components/features/scripts/script-executions-table.tsx +145 -0
- package/src/components/features/scripts/script-schedules-table.tsx +113 -0
- package/src/components/features/scripts/script-shell-badge.tsx +25 -0
- package/src/components/features/scripts/scripts-table-columns.ts +95 -0
- package/src/components/features/scripts/scripts-table-shared.tsx +43 -0
- package/src/components/features/scripts/scripts-table.tsx +124 -0
- package/src/components/features/scripts/scripts-tabs.ts +11 -0
- package/src/components/features/scripts/shell-types.ts +26 -0
- package/src/components/product-demo/fixtures/scripts.ts +186 -0
- package/src/components/product-demo/fixtures/shared.ts +7 -0
- package/src/components/product-demo/fixtures/software-update.ts +1 -7
- package/src/components/product-demo/product-screen.tsx +3 -1
- package/src/components/product-demo/registry.ts +1 -0
- package/src/components/product-demo/screen-keys.ts +1 -0
- package/src/components/product-demo/screens/scripts.tsx +192 -0
- package/src/components/shared/legal-document/index.ts +8 -0
- package/src/components/shared/legal-document/legal-document-page.tsx +83 -44
- package/src/components/shared/legal-document/use-legal-docs.ts +18 -3
- package/src/components/shared/legal-document/use-legal-documents.ts +113 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/legal-documents.ts +21 -0
|
@@ -21,13 +21,15 @@ export const PRODUCT_SCREEN_COMPACT_HEIGHT = 300;
|
|
|
21
21
|
* host's readable width: the device list's customer column breaks a name
|
|
22
22
|
* mid-word below it, and the logs table beside its open drawer has no room
|
|
23
23
|
* for the message; the software form is laid out wide so the package, the
|
|
24
|
-
* schedule and the picked devices all fit the frame's height
|
|
24
|
+
* schedule and the picked devices all fit the frame's height; the scripts
|
|
25
|
+
* screen puts a schedule's name, its timing and its repeat on one row. A frame never
|
|
25
26
|
* lays such a screen out narrower.
|
|
26
27
|
*/
|
|
27
28
|
const MIN_LAYOUT_WIDTH: Partial<Record<ProductScreenKey, number>> = {
|
|
28
29
|
devices: 1120,
|
|
29
30
|
logs: 1120,
|
|
30
31
|
'software-update': 1400,
|
|
32
|
+
scripts: 1400,
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
/**
|
|
@@ -17,4 +17,5 @@ export const PRODUCT_SCREEN_LOADERS: Record<ProductScreenKey, ProductScreenLoade
|
|
|
17
17
|
'tickets-board': () => import('./screens/tickets-board'),
|
|
18
18
|
logs: () => import('./screens/logs'),
|
|
19
19
|
knowledge: () => import('./screens/knowledge'),
|
|
20
|
+
scripts: () => import('./screens/scripts'),
|
|
20
21
|
};
|
|
@@ -15,6 +15,7 @@ export const PRODUCT_SCREEN_LABELS = {
|
|
|
15
15
|
'tickets-board': 'Ticket board',
|
|
16
16
|
logs: 'Logs with a log open',
|
|
17
17
|
knowledge: 'Knowledge base',
|
|
18
|
+
scripts: 'Script library and schedules',
|
|
18
19
|
} as const;
|
|
19
20
|
|
|
20
21
|
export type ProductScreenKey = keyof typeof PRODUCT_SCREEN_LABELS;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
EXECUTION_COLUMNS,
|
|
6
|
+
ScriptExecutionResult,
|
|
7
|
+
ScriptExecutionsTable,
|
|
8
|
+
type ScriptExecutionTableRow,
|
|
9
|
+
ScriptShellBadge,
|
|
10
|
+
type ScriptsTableColumnLayout,
|
|
11
|
+
} from '../../features/scripts';
|
|
12
|
+
import { TimerIcon } from '../../icons-v2-generated/date-and-time/timer-icon';
|
|
13
|
+
import { TruncateText } from '../../ui/truncate-text';
|
|
14
|
+
import { useProductDemoCast } from '../cast';
|
|
15
|
+
import { buildScriptsFixture, type DemoScript } from '../fixtures/scripts';
|
|
16
|
+
import type { ProductScreenViewProps } from '../types';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The picture has nowhere to open a run and no row menu, and the Result
|
|
20
|
+
* column would repeat the output opened under its row.
|
|
21
|
+
*/
|
|
22
|
+
const WIDE_HIDDEN_COLUMNS = ['result', 'open'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A card's width hides the device column (the product's page drops it below
|
|
26
|
+
* `lg`), and the device is what the narrow picture is about: here it takes
|
|
27
|
+
* the room, with the status beside it.
|
|
28
|
+
*/
|
|
29
|
+
const COMPACT_COLUMNS: readonly ScriptsTableColumnLayout[] = [
|
|
30
|
+
{ ...EXECUTION_COLUMNS.machineId, width: 'flex-1 min-w-0', hideAt: undefined },
|
|
31
|
+
EXECUTION_COLUMNS.status,
|
|
32
|
+
];
|
|
33
|
+
const COMPACT_ROWS = 4;
|
|
34
|
+
|
|
35
|
+
type TokenKind = 'comment' | 'keyword' | 'string' | 'variable' | 'command' | 'parameter' | 'number' | 'plain';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* PowerShell token colours: the product editor's `ods-dark` highlight style
|
|
39
|
+
* (`script-editor-view.tsx`) in ODS tokens. Keywords yellow, strings and
|
|
40
|
+
* variables cyan, commands green, numbers amber, parameters grey, comments
|
|
41
|
+
* muted.
|
|
42
|
+
*/
|
|
43
|
+
const TOKEN_CLASS: Record<TokenKind, string> = {
|
|
44
|
+
comment: 'italic text-ods-text-tertiary',
|
|
45
|
+
keyword: 'text-ods-warning',
|
|
46
|
+
string: 'text-ods-flamingo-cyan',
|
|
47
|
+
variable: 'text-ods-flamingo-cyan',
|
|
48
|
+
command: 'text-ods-success',
|
|
49
|
+
parameter: 'text-ods-text-secondary',
|
|
50
|
+
number: 'text-ods-open-yellow',
|
|
51
|
+
plain: 'text-ods-text-primary',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** One group per kind, in {@link TOKEN_KINDS} order; a comment or a string swallows what it contains. */
|
|
55
|
+
const TOKEN_PATTERN =
|
|
56
|
+
/(#.*$)|("(?:[^"\\]|\\.)*"|'[^']*')|(\$[\w:]+)|\b(foreach|in|if|else|exit|return|param)\b|\b([A-Z][a-z]+-[A-Z][A-Za-z]+)\b|(-[A-Za-z]+)\b|\b(\d+(?:GB|MB|KB)?)\b/g;
|
|
57
|
+
const TOKEN_KINDS: readonly TokenKind[] = [
|
|
58
|
+
'comment',
|
|
59
|
+
'string',
|
|
60
|
+
'variable',
|
|
61
|
+
'keyword',
|
|
62
|
+
'command',
|
|
63
|
+
'parameter',
|
|
64
|
+
'number',
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
interface Token {
|
|
68
|
+
text: string;
|
|
69
|
+
kind: TokenKind;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One line of PowerShell as coloured runs. Only the picture's own script is read, so the rules are few. */
|
|
73
|
+
function tokenize(line: string): Token[] {
|
|
74
|
+
const tokens: Token[] = [];
|
|
75
|
+
let last = 0;
|
|
76
|
+
for (const match of line.matchAll(TOKEN_PATTERN)) {
|
|
77
|
+
const start = match.index ?? 0;
|
|
78
|
+
if (start > last) tokens.push({ text: line.slice(last, start), kind: 'plain' });
|
|
79
|
+
const group = match.slice(1).findIndex(Boolean);
|
|
80
|
+
tokens.push({ text: match[0], kind: TOKEN_KINDS[group] ?? 'plain' });
|
|
81
|
+
last = start + match[0].length;
|
|
82
|
+
}
|
|
83
|
+
if (last < line.length) tokens.push({ text: line.slice(last), kind: 'plain' });
|
|
84
|
+
return tokens;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The script as the product's details page shows it: its name and shell over
|
|
89
|
+
* the editor's read-only view. The product's editor is CodeMirror, which the
|
|
90
|
+
* library does not carry, so this draws the editor's geometry (a 46px number
|
|
91
|
+
* gutter, the code 32px past it, 22px rows of 14px mono) in its colours. The
|
|
92
|
+
* header adds the schedule that runs the script and who wrote and approved
|
|
93
|
+
* it: the two things the conversation beside the picture settled.
|
|
94
|
+
*/
|
|
95
|
+
function ScriptCodePicture({ script }: { script: DemoScript }) {
|
|
96
|
+
// Keyed by line number: the script never changes under the picture.
|
|
97
|
+
const lines = useMemo(
|
|
98
|
+
() => script.code.split('\n').map((line, index) => ({ number: index + 1, tokens: tokenize(line) })),
|
|
99
|
+
[script.code],
|
|
100
|
+
);
|
|
101
|
+
const { schedule, provenance } = script;
|
|
102
|
+
return (
|
|
103
|
+
<section
|
|
104
|
+
aria-label={script.name}
|
|
105
|
+
className="flex h-full min-w-0 flex-col overflow-hidden rounded-[8px] border border-ods-border bg-ods-card"
|
|
106
|
+
>
|
|
107
|
+
<div className="flex flex-col gap-[var(--spacing-system-xxs)] border-b border-ods-border p-[var(--spacing-system-m)]">
|
|
108
|
+
<div className="flex min-w-0 items-center justify-between gap-[var(--spacing-system-m)]">
|
|
109
|
+
<TruncateText variant="h4">{script.name}</TruncateText>
|
|
110
|
+
<span className="flex shrink-0 items-center gap-[var(--spacing-system-xxs)] text-ods-text-primary text-h6">
|
|
111
|
+
<TimerIcon className="size-4 text-ods-text-secondary" />
|
|
112
|
+
{`${schedule.repeat} ${schedule.time} · ${schedule.deviceCount} devices`}
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="flex min-w-0 items-center justify-between gap-[var(--spacing-system-m)]">
|
|
116
|
+
<TruncateText variant="h6" tone="secondary">
|
|
117
|
+
{`Written by ${provenance.author} · approved by ${provenance.approver}`}
|
|
118
|
+
</TruncateText>
|
|
119
|
+
<ScriptShellBadge
|
|
120
|
+
value={script.shellType}
|
|
121
|
+
className="shrink-0 text-ods-text-secondary text-h6"
|
|
122
|
+
iconClassName="size-4"
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<pre className="m-0 min-h-0 flex-1 overflow-hidden bg-ods-bg py-3 font-mono text-[14px] leading-[22px]">
|
|
127
|
+
{lines.map(line => (
|
|
128
|
+
<div key={line.number} className="flex">
|
|
129
|
+
<span className="w-[46px] shrink-0 select-none pr-[4px] text-right text-ods-text-tertiary">
|
|
130
|
+
{line.number}
|
|
131
|
+
</span>
|
|
132
|
+
<code className="whitespace-pre pl-[32px]">
|
|
133
|
+
{line.tokens.map((token, at) => (
|
|
134
|
+
<span key={`${at}-${token.text}`} className={TOKEN_CLASS[token.kind]}>
|
|
135
|
+
{token.text}
|
|
136
|
+
</span>
|
|
137
|
+
))}
|
|
138
|
+
</code>
|
|
139
|
+
</div>
|
|
140
|
+
))}
|
|
141
|
+
</pre>
|
|
142
|
+
</section>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Where the page's Mingo conversation leaves off: the script it wrote, on the
|
|
148
|
+
* left, and the first night its schedule ran, on the right, as the product's
|
|
149
|
+
* Execution History (a run still going, one failed, one finished with its
|
|
150
|
+
* output open under it: what it cleaned and what it freed). The schedule form
|
|
151
|
+
* and the library are the conversation's part, so the picture leaves them
|
|
152
|
+
* out. The narrow rendering is the run alone: which devices, and how each went.
|
|
153
|
+
*/
|
|
154
|
+
export default function ScriptsScreen({ compact = false }: ProductScreenViewProps) {
|
|
155
|
+
const cast = useProductDemoCast();
|
|
156
|
+
const fixture = useMemo(() => buildScriptsFixture(cast), [cast]);
|
|
157
|
+
|
|
158
|
+
if (compact) {
|
|
159
|
+
return (
|
|
160
|
+
// Four runs of the card's row height fill the frame; the padding is what is left round them.
|
|
161
|
+
<div className="h-full overflow-hidden bg-ods-bg p-[var(--spacing-system-xsf)]">
|
|
162
|
+
<ScriptExecutionsTable
|
|
163
|
+
rows={fixture.executions.slice(0, COMPACT_ROWS)}
|
|
164
|
+
columns={COMPACT_COLUMNS}
|
|
165
|
+
showHeader={false}
|
|
166
|
+
compact
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const renderOutput = (row: ScriptExecutionTableRow) =>
|
|
173
|
+
row.id === fixture.openExecutionId ? <ScriptExecutionResult result={row.result} empty="—" /> : null;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
// The frame's tighter padding and no header row: the output opened under a run needs the height.
|
|
177
|
+
<div className="flex h-full gap-[var(--spacing-system-m)] overflow-hidden bg-ods-bg p-[var(--spacing-system-m)]">
|
|
178
|
+
<div className="w-[39%] shrink-0">
|
|
179
|
+
<ScriptCodePicture script={fixture.script} />
|
|
180
|
+
</div>
|
|
181
|
+
<div className="min-w-0 flex-1 overflow-hidden">
|
|
182
|
+
<ScriptExecutionsTable
|
|
183
|
+
rows={fixture.executions}
|
|
184
|
+
hideColumns={WIDE_HIDDEN_COLUMNS}
|
|
185
|
+
renderSubRow={renderOutput}
|
|
186
|
+
showHeader={false}
|
|
187
|
+
compact
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
export {
|
|
10
10
|
LegalDocumentPage,
|
|
11
|
+
legalDocumentStampLabel,
|
|
11
12
|
type LegalDocumentPageProps,
|
|
12
13
|
type LegalDocumentMarkdownRendererProps,
|
|
13
14
|
} from './legal-document-page';
|
|
14
15
|
export { useLegalDocs, type LegalDocument, type UseLegalDocsOptions, type UseLegalDocsReturn } from './use-legal-docs';
|
|
16
|
+
export {
|
|
17
|
+
useLegalDocuments,
|
|
18
|
+
toLegalDocumentSummaries,
|
|
19
|
+
type LegalDocumentSummary,
|
|
20
|
+
type UseLegalDocumentsOptions,
|
|
21
|
+
type UseLegalDocumentsReturn,
|
|
22
|
+
} from './use-legal-documents';
|
|
@@ -25,6 +25,7 @@ import { FaqSection, pageFaqProps, type PageFaqSlot } from '../../faq/faq-sectio
|
|
|
25
25
|
import { PageShell, PageLayout } from '../../ui';
|
|
26
26
|
import { RichMarkdownRenderer } from '../../ui/markdown';
|
|
27
27
|
import { useLegalDocs, type LegalDocument } from './use-legal-docs';
|
|
28
|
+
import { useLegalDocuments } from './use-legal-documents';
|
|
28
29
|
|
|
29
30
|
export interface LegalDocumentMarkdownRendererProps {
|
|
30
31
|
content: string;
|
|
@@ -34,25 +35,24 @@ export interface LegalDocumentMarkdownRendererProps {
|
|
|
34
35
|
|
|
35
36
|
export interface LegalDocumentPageProps {
|
|
36
37
|
/** Document type identifier — drives the default API endpoint
|
|
37
|
-
* `/api/legal/<docType>` AND the error-log prefix.
|
|
38
|
-
*
|
|
38
|
+
* `/api/legal/<docType>` AND the error-log prefix. One of the types the
|
|
39
|
+
* host's list answers (`useLegalDocuments`). */
|
|
39
40
|
docType: string;
|
|
40
|
-
/** Heading text
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
fallbackDescription
|
|
45
|
-
/** Email shown in the error + empty-state copy
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
emptyStateMessage: string;
|
|
41
|
+
/** Heading text. Default: the title the served document carries, so a host
|
|
42
|
+
* that shows whatever the list holds states no per-document copy. */
|
|
43
|
+
title?: string;
|
|
44
|
+
/** Subtitle shown when the document states neither a version nor a date. */
|
|
45
|
+
fallbackDescription?: string;
|
|
46
|
+
/** Email shown in the error + empty-state copy. Absent: no contact line. */
|
|
47
|
+
contactEmail?: string;
|
|
48
|
+
/** Prompt shown above the contact link in the error state.
|
|
49
|
+
* Default "For questions about this document, please contact:". */
|
|
50
|
+
errorContactPrompt?: string;
|
|
51
|
+
/** Title for the error block. Default "Unable to load <title>". */
|
|
52
|
+
errorTitle?: string;
|
|
53
|
+
/** Sentence shown when the API returns no document.
|
|
54
|
+
* Default "<title> is not available at this time.". */
|
|
55
|
+
emptyStateMessage?: string;
|
|
56
56
|
/** SSR-prepared document, if available. */
|
|
57
57
|
initialData?: LegalDocument | null;
|
|
58
58
|
/** SSR-prepared formatted "Last Updated" label. Stable across hydration. */
|
|
@@ -66,29 +66,52 @@ export interface LegalDocumentPageProps {
|
|
|
66
66
|
* to hide. Default `{ label: 'Back to home', href: '/' }`. */
|
|
67
67
|
backButton?: { label?: string; href?: string } | false;
|
|
68
68
|
/**
|
|
69
|
-
* The
|
|
70
|
-
*
|
|
71
|
-
*
|
|
69
|
+
* The hub's list of legal documents (`GET /api/legal`). Given it, the page
|
|
70
|
+
* takes the document's title and FAQ topic from the list, so a host passes
|
|
71
|
+
* only the type and its endpoints and states nothing per document; the title
|
|
72
|
+
* then names the document even when its text fails to load.
|
|
72
73
|
*/
|
|
73
|
-
|
|
74
|
+
documentsEndpoint?: string;
|
|
75
|
+
/**
|
|
76
|
+
* The page's own FAQ block: the topic the page is about and where the host
|
|
77
|
+
* serves the FAQs. Rendered after the page's content, inside its container.
|
|
78
|
+
* Without a `topic`, the topic is the one the list (`documentsEndpoint`)
|
|
79
|
+
* gives the document. Absent, or no topic known: no FAQ block.
|
|
80
|
+
*/
|
|
81
|
+
faq?: PageFaqSlot | Omit<PageFaqSlot, 'topic'>;
|
|
74
82
|
/** Render the standalone `<PageShell>`. Default true. Pass false when the host
|
|
75
83
|
* layout already provides the page container — only the padding box renders,
|
|
76
84
|
* avoiding a nested `<main>`. */
|
|
77
85
|
shell?: boolean;
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
/**
|
|
89
|
+
* "Version 1.0 · Effective October 14, 2026" for a document that states them;
|
|
90
|
+
* null for one that states neither. One wording for every legal surface.
|
|
91
|
+
*/
|
|
92
|
+
export function legalDocumentStampLabel(
|
|
93
|
+
doc: Pick<LegalDocument, 'version' | 'effectiveDate'> | null | undefined,
|
|
94
|
+
): string | null {
|
|
95
|
+
const parts = [
|
|
96
|
+
doc?.version ? `Version ${doc.version}` : null,
|
|
97
|
+
doc?.effectiveDate ? `Effective ${doc.effectiveDate}` : null,
|
|
98
|
+
].filter((part): part is string => part !== null);
|
|
99
|
+
return parts.length > 0 ? parts.join(' \u00b7 ') : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
80
102
|
export function LegalDocumentPage({
|
|
81
103
|
docType,
|
|
82
|
-
title,
|
|
83
|
-
fallbackDescription,
|
|
104
|
+
title: titleProp,
|
|
105
|
+
fallbackDescription = '',
|
|
84
106
|
contactEmail,
|
|
85
|
-
errorContactPrompt,
|
|
86
|
-
errorTitle,
|
|
87
|
-
emptyStateMessage,
|
|
107
|
+
errorContactPrompt = 'For questions about this document, please contact:',
|
|
108
|
+
errorTitle: errorTitleProp,
|
|
109
|
+
emptyStateMessage: emptyStateMessageProp,
|
|
88
110
|
initialData = null,
|
|
89
111
|
initialLastUpdatedLabel = null,
|
|
90
112
|
apiEndpoint,
|
|
91
113
|
MarkdownRenderer = RichMarkdownRenderer,
|
|
114
|
+
documentsEndpoint,
|
|
92
115
|
backButton,
|
|
93
116
|
faq,
|
|
94
117
|
shell = true,
|
|
@@ -107,13 +130,25 @@ export function LegalDocumentPage({
|
|
|
107
130
|
onClick: () => router.push(backButton?.href ?? '/'),
|
|
108
131
|
};
|
|
109
132
|
|
|
133
|
+
const { documents } = useLegalDocuments({ apiEndpoint: documentsEndpoint, enabled: Boolean(documentsEndpoint) });
|
|
134
|
+
const listed = documents.find(doc => doc.docType === docType);
|
|
135
|
+
const title = titleProp ?? data?.title ?? listed?.title ?? 'Legal document';
|
|
136
|
+
const faqTopic = faq && 'topic' in faq ? faq.topic : listed?.faqTopic;
|
|
137
|
+
const faqSlot = faq && faqTopic ? { ...faq, topic: faqTopic } : null;
|
|
138
|
+
const errorTitle = errorTitleProp ?? `Unable to load ${title}`;
|
|
139
|
+
const emptyStateMessage = emptyStateMessageProp ?? `${title} is not available at this time.`;
|
|
140
|
+
|
|
110
141
|
const fallbackLastUpdatedLabel = data?.lastSynced != null ? formatLegalDate(data.lastSynced) : null;
|
|
111
142
|
const effectiveLastUpdatedLabel = initialLastUpdatedLabel ?? fallbackLastUpdatedLabel;
|
|
112
143
|
|
|
113
144
|
// Subtitle routes through the frozen `PageLayout` `TitleBlock` (text-h2 title
|
|
114
|
-
// + subtitle) — unified header across all help-center pages.
|
|
115
|
-
//
|
|
116
|
-
|
|
145
|
+
// + subtitle) — unified header across all help-center pages. A document that
|
|
146
|
+
// states its own version and effective date shows those (they are what a
|
|
147
|
+
// reader agreed to); one that states neither shows when it was last read
|
|
148
|
+
// from its source, else the fallback description.
|
|
149
|
+
const subtitle =
|
|
150
|
+
legalDocumentStampLabel(data) ??
|
|
151
|
+
(effectiveLastUpdatedLabel ? `Last Updated: ${effectiveLastUpdatedLabel}` : fallbackDescription);
|
|
117
152
|
|
|
118
153
|
const inner = (
|
|
119
154
|
<PageLayout title={title} subtitle={subtitle} backButton={backCfg} titleSize="h1" titleWrap>
|
|
@@ -144,32 +179,36 @@ export function LegalDocumentPage({
|
|
|
144
179
|
<p className="mb-2 text-ods-error">{errorTitle}</p>
|
|
145
180
|
<p className="text-ods-error text-h6">{error}</p>
|
|
146
181
|
</div>
|
|
147
|
-
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
{contactEmail}
|
|
151
|
-
|
|
152
|
-
|
|
182
|
+
{contactEmail && (
|
|
183
|
+
<div className="text-ods-text-secondary">
|
|
184
|
+
<p>{errorContactPrompt}</p>
|
|
185
|
+
<Link href={`mailto:${contactEmail}`} className="text-ods-accent hover:underline">
|
|
186
|
+
{contactEmail}
|
|
187
|
+
</Link>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
153
190
|
</div>
|
|
154
191
|
) : data ? (
|
|
155
192
|
<MarkdownRenderer content={data.content} sectionIds={data.sections || []} demoteMarkdownH1ToH2 />
|
|
156
193
|
) : (
|
|
157
194
|
<div className="py-16 text-center text-ods-text-secondary">
|
|
158
195
|
<p className="text-h4">{emptyStateMessage}</p>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
{contactEmail}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
{contactEmail && (
|
|
197
|
+
<p className="mt-2">
|
|
198
|
+
Please contact{' '}
|
|
199
|
+
<Link href={`mailto:${contactEmail}`} className="text-ods-accent hover:underline">
|
|
200
|
+
{contactEmail}
|
|
201
|
+
</Link>{' '}
|
|
202
|
+
for more information.
|
|
203
|
+
</p>
|
|
204
|
+
)}
|
|
166
205
|
</div>
|
|
167
206
|
)}
|
|
168
207
|
</article>
|
|
169
208
|
</div>
|
|
170
209
|
</div>
|
|
171
210
|
</div>
|
|
172
|
-
{
|
|
211
|
+
{faqSlot && <FaqSection {...pageFaqProps(faqSlot)} />}
|
|
173
212
|
</PageLayout>
|
|
174
213
|
);
|
|
175
214
|
|
|
@@ -24,6 +24,10 @@ export interface LegalDocument {
|
|
|
24
24
|
githubSha: string | null;
|
|
25
25
|
sections: Array<{ id: string; title: string; level: number }>;
|
|
26
26
|
docType: string;
|
|
27
|
+
/** The version the document states for itself ("1.0"); null when it states none. */
|
|
28
|
+
version: string | null;
|
|
29
|
+
/** The day that version takes effect, as the document words it; null when it states none. */
|
|
30
|
+
effectiveDate: string | null;
|
|
27
31
|
meta: {
|
|
28
32
|
sectionsCount: number;
|
|
29
33
|
contentLength: number;
|
|
@@ -111,6 +115,8 @@ function toLegalDocument(payload: unknown, docType: string): LegalDocument | nul
|
|
|
111
115
|
githubSha: typeof githubSha === 'string' ? githubSha : null,
|
|
112
116
|
sections,
|
|
113
117
|
docType: readString(payload, 'docType') ?? docType,
|
|
118
|
+
version: readString(payload, 'version') ?? null,
|
|
119
|
+
effectiveDate: readString(payload, 'effectiveDate') ?? null,
|
|
114
120
|
meta: {
|
|
115
121
|
sectionsCount: typeof sectionsCount === 'number' ? sectionsCount : sections.length,
|
|
116
122
|
contentLength: typeof contentLength === 'number' ? contentLength : content.length,
|
|
@@ -122,9 +128,9 @@ function toLegalDocument(payload: unknown, docType: string): LegalDocument | nul
|
|
|
122
128
|
/**
|
|
123
129
|
* Hook to fetch a legal document.
|
|
124
130
|
* @param docType — short identifier for the document (drives the
|
|
125
|
-
* default endpoint path AND the error-log prefix).
|
|
126
|
-
*
|
|
127
|
-
*
|
|
131
|
+
* default endpoint path AND the error-log prefix). The valid values are
|
|
132
|
+
* the ones the host's list endpoint answers (`useLegalDocuments`); the
|
|
133
|
+
* hook treats it as opaque.
|
|
128
134
|
*/
|
|
129
135
|
export function useLegalDocs(docType: string, options: UseLegalDocsOptions = {}): UseLegalDocsReturn {
|
|
130
136
|
const { initialData = null, apiEndpoint } = options;
|
|
@@ -183,6 +189,15 @@ export function useLegalDocs(docType: string, options: UseLegalDocsOptions = {})
|
|
|
183
189
|
try {
|
|
184
190
|
const response = await contentFetch(effectiveEndpoint);
|
|
185
191
|
|
|
192
|
+
// 404: the hub has no published text for this document yet (a legal
|
|
193
|
+
// file is published only once it states its version and effective
|
|
194
|
+
// date). That is "not available", not a failure: no data, no error.
|
|
195
|
+
if (response.status === 404) {
|
|
196
|
+
if (!isCurrent()) return;
|
|
197
|
+
setData(null);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
186
201
|
if (!response.ok) {
|
|
187
202
|
throw new Error(`Failed to fetch ${docType} document: ${response.status} ${response.statusText}`);
|
|
188
203
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useLegalDocuments — the list of legal documents a hub publishes (privacy
|
|
5
|
+
* policy, terms of service, payment terms, license, security policy, and
|
|
6
|
+
* whatever it adds later), each with its page and the version it states.
|
|
7
|
+
*
|
|
8
|
+
* A host that lists legal pages reads this list, shows one entry per published
|
|
9
|
+
* document, each leading to that document's own page, and names no document
|
|
10
|
+
* itself: a document the hub adds appears with no change here.
|
|
11
|
+
*
|
|
12
|
+
* Endpoint: default `/api/legal`; an embedder passes its proxied path.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useEffect, useState } from 'react';
|
|
16
|
+
import { contentFetch } from '../../../utils/embed-content-fetch';
|
|
17
|
+
|
|
18
|
+
export interface LegalDocumentSummary {
|
|
19
|
+
/** The identifier `useLegalDocs` and `<LegalDocumentPage>` take. */
|
|
20
|
+
docType: string;
|
|
21
|
+
title: string;
|
|
22
|
+
/** One line saying what the document is, for a card that lists it. */
|
|
23
|
+
summary: string;
|
|
24
|
+
/** The document's page on the hub's own site (a path). */
|
|
25
|
+
route: string;
|
|
26
|
+
sourceFile: string;
|
|
27
|
+
/** The topic the document's FAQs are written for, when the hub has one. */
|
|
28
|
+
faqTopic: string | null;
|
|
29
|
+
version: string | null;
|
|
30
|
+
effectiveDate: string | null;
|
|
31
|
+
/** False while the hub has no content for it yet. */
|
|
32
|
+
published: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UseLegalDocumentsOptions {
|
|
36
|
+
/** Full GET endpoint URL. Default `/api/legal`. */
|
|
37
|
+
apiEndpoint?: string;
|
|
38
|
+
/** False: read nothing and answer an empty list. Default true. */
|
|
39
|
+
enabled?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UseLegalDocumentsReturn {
|
|
43
|
+
documents: LegalDocumentSummary[];
|
|
44
|
+
isLoading: boolean;
|
|
45
|
+
error: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
49
|
+
return typeof value === 'object' && value !== null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function text(source: Record<string, unknown>, key: string): string | null {
|
|
53
|
+
const value = source[key];
|
|
54
|
+
return typeof value === 'string' && value ? value : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Decode the endpoint's answer; a row without a type and a title is left out. */
|
|
58
|
+
export function toLegalDocumentSummaries(payload: unknown): LegalDocumentSummary[] {
|
|
59
|
+
const rows: unknown = isRecord(payload) ? payload.documents : null;
|
|
60
|
+
if (!Array.isArray(rows)) return [];
|
|
61
|
+
const documents: LegalDocumentSummary[] = [];
|
|
62
|
+
for (const row of rows as readonly unknown[]) {
|
|
63
|
+
if (!isRecord(row)) continue;
|
|
64
|
+
const docType = text(row, 'docType');
|
|
65
|
+
const title = text(row, 'title');
|
|
66
|
+
if (!docType || !title) continue;
|
|
67
|
+
documents.push({
|
|
68
|
+
docType,
|
|
69
|
+
title,
|
|
70
|
+
summary: text(row, 'summary') ?? '',
|
|
71
|
+
route: text(row, 'route') ?? '',
|
|
72
|
+
sourceFile: text(row, 'sourceFile') ?? '',
|
|
73
|
+
faqTopic: text(row, 'faqTopic'),
|
|
74
|
+
version: text(row, 'version'),
|
|
75
|
+
effectiveDate: text(row, 'effectiveDate'),
|
|
76
|
+
published: row.published === true,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return documents;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function useLegalDocuments(options: UseLegalDocumentsOptions = {}): UseLegalDocumentsReturn {
|
|
83
|
+
const endpoint = options.apiEndpoint ?? '/api/legal';
|
|
84
|
+
const enabled = options.enabled ?? true;
|
|
85
|
+
const [state, setState] = useState<UseLegalDocumentsReturn>({ documents: [], isLoading: enabled, error: null });
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
let current = enabled;
|
|
89
|
+
const load = async () => {
|
|
90
|
+
try {
|
|
91
|
+
const response = await contentFetch(endpoint);
|
|
92
|
+
if (!response.ok) throw new Error(`Failed to fetch legal documents: ${response.status} ${response.statusText}`);
|
|
93
|
+
const payload: unknown = await response.json();
|
|
94
|
+
if (current) setState({ documents: toLegalDocumentSummaries(payload), isLoading: false, error: null });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error('Error fetching legal documents:', err);
|
|
97
|
+
if (current) {
|
|
98
|
+
setState({
|
|
99
|
+
documents: [],
|
|
100
|
+
isLoading: false,
|
|
101
|
+
error: err instanceof Error ? err.message : 'Unknown error occurred',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
if (enabled) void load();
|
|
107
|
+
return () => {
|
|
108
|
+
current = false;
|
|
109
|
+
};
|
|
110
|
+
}, [endpoint, enabled]);
|
|
111
|
+
|
|
112
|
+
return state;
|
|
113
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -415,6 +415,7 @@ export * from './humanity-signals';
|
|
|
415
415
|
// so the host's draft endpoint re-applies the same filter the browser hook runs.
|
|
416
416
|
// Also exported via the granular subpath `./utils/form-rescue`.
|
|
417
417
|
export * from './form-rescue';
|
|
418
|
+
export * from './legal-documents';
|
|
418
419
|
|
|
419
420
|
// Doc-source viewer utilities (path parsing, tree building, section extraction,
|
|
420
421
|
// embed-URL conversion) — single home for all doc-viewer pure helpers across
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The types of legal document a hub can publish, each with a page of its own
|
|
3
|
+
* (`/api/legal/<type>`, `<LegalDocumentPage docType>`).
|
|
4
|
+
*
|
|
5
|
+
* WHICH of them are published, and each one's title, version and effective
|
|
6
|
+
* date, is the hub's answer at run time (`GET /api/legal`, `useLegalDocuments`).
|
|
7
|
+
* This list exists for a host that must know every possible page before it
|
|
8
|
+
* runs: a statically exported app prerenders one legal page per type here and
|
|
9
|
+
* links only the ones the hub's list says are published.
|
|
10
|
+
*
|
|
11
|
+
* Pure and server-safe. The hub's own list of legal documents carries exactly
|
|
12
|
+
* these types.
|
|
13
|
+
*/
|
|
14
|
+
export const LEGAL_DOCUMENT_TYPES = ['privacy', 'terms', 'payment', 'license', 'security'] as const;
|
|
15
|
+
|
|
16
|
+
export type LegalDocumentType = (typeof LEGAL_DOCUMENT_TYPES)[number];
|
|
17
|
+
|
|
18
|
+
/** True for a type a host has a page for (one of {@link LEGAL_DOCUMENT_TYPES}). */
|
|
19
|
+
export function isLegalDocumentType(value: string): value is LegalDocumentType {
|
|
20
|
+
return (LEGAL_DOCUMENT_TYPES as readonly string[]).includes(value);
|
|
21
|
+
}
|