@fias/arche-sdk 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Panels — the declarative content shape, and a validator for plugins that store
3
+ * panels as DATA.
4
+ *
5
+ * A panel is a bounded block of content (a title, ordered leaf elements, some
6
+ * buttons) rendered by `@fias/panel-kit`, which the platform serves to plugin
7
+ * iframes from its own CDN (declare the `sandbox:vendored-libraries` permission
8
+ * and list `"@fias/panel-kit"` in your manifest `dependencies`). See
9
+ * `templates/default/CLAUDE.md` § Panels.
10
+ *
11
+ * WHAT THIS MODULE IS FOR. Rendering needs no help from the SDK — panel-kit takes
12
+ * a plain object. What the SDK adds is the part a plugin cannot do for itself:
13
+ *
14
+ * 1. **The types**, so a plugin written against the published npm package has
15
+ * the same shape the platform validates, without depending on panel-kit's
16
+ * private workspace package for type information.
17
+ * 2. **`validatePanelCatalog`**, so a plugin that keeps its panels as data —
18
+ * in `useFiasDataStore`, in a config document, in an admin-edited blob —
19
+ * can gate a WRITE the way the platform gates its own server-authored
20
+ * catalogs, instead of discovering a malformed panel when a player sees a
21
+ * blank one.
22
+ *
23
+ * That second point is the whole design: **panels stored as data need no new
24
+ * platform op.** A plugin already has generic storage; what it lacked was the
25
+ * shared definition of "valid". Nothing here talks to the platform, nothing is
26
+ * gated, nothing is billed — it is a pure function over a value you already hold,
27
+ * which is why it can live in the SDK rather than behind a bridge call.
28
+ *
29
+ * MIRROR, NOT A FORK. The canonical schema is `panelThemeSchema` /
30
+ * `panelElementSchema` in `packages/db-types/src/panel-definition.ts` (Zod,
31
+ * server-side). This is a hand-written mirror for the same reason panel-kit's
32
+ * types are: `@fias/db-types` is a server package that would drag zod and a large
33
+ * unrelated surface into every plugin bundle. `tests/architecture/panel-schema-mirror.test.ts`
34
+ * runs the canonical corpora against BOTH mirrors, so a grammar cannot drift here
35
+ * without failing CI.
36
+ *
37
+ * A renderer older than a definition SKIPS element kinds it does not know. This
38
+ * validator is deliberately stricter than that: an unknown kind is an ERROR, so an
39
+ * authoring mistake fails at write time rather than rendering as silence.
40
+ */
41
+ /** Element list bounds. Mirrors `PANEL_*` in db-types. */
42
+ export declare const PANEL_MIN_ELEMENTS = 1;
43
+ export declare const PANEL_MAX_ELEMENTS = 24;
44
+ export declare const PANEL_TITLE_MAX_LENGTH = 80;
45
+ export declare const PANEL_TEXT_MAX_LENGTH = 500;
46
+ export declare const PANEL_IMAGE_ALT_MAX_LENGTH = 120;
47
+ export declare const PANEL_IMAGE_KEY_MAX_LENGTH = 500;
48
+ export declare const PANEL_IMAGE_MIN_HEIGHT = 24;
49
+ export declare const PANEL_IMAGE_MAX_HEIGHT = 512;
50
+ export declare const PANEL_BUTTON_LABEL_MAX_LENGTH = 40;
51
+ export declare const PANEL_ACTION_MAX_LENGTH = 2048;
52
+ export declare const PANEL_EMIT_TOKEN_MAX_LENGTH = 64;
53
+ export declare const PANEL_ITEMS_EMPTY_TEXT_MAX_LENGTH = 120;
54
+ export declare const PANEL_ITEMS_SLOT_MAX_LENGTH = 64;
55
+ export declare const PANEL_SHOW_IF_MAX_LENGTH = 64;
56
+ export declare const PANEL_PRIORITY_ABS_MAX = 1000;
57
+ export declare const PANEL_THEME_FONT_MAX_LENGTH = 200;
58
+ export declare const PANEL_THEME_AMOUNT_FORMAT_MAX_LENGTH = 24;
59
+ export declare const PANEL_THEME_Z_MAX = 1000000;
60
+ export declare const PANEL_THEME_LENGTH_MAX_INTEGER_DIGITS = 4;
61
+ /**
62
+ * Storage roots the platform will presign inside a panel image key. A key outside
63
+ * them is not merely unusual — it is one the platform will not resolve, so it
64
+ * would render as a permanently missing image.
65
+ */
66
+ export declare const PANEL_IMAGE_ROOTS: readonly ["dfe-icons", "dfe-avatars"];
67
+ /** Leaf kinds only — panels do not nest. Order matches the canonical union. */
68
+ export declare const PANEL_ELEMENT_KINDS: readonly ["text", "conditionalText", "image", "assetImage", "items", "slottedItems", "button", "spacer"];
69
+ export type PanelElementKind = (typeof PANEL_ELEMENT_KINDS)[number];
70
+ export type PanelTextStyle = 'title' | 'body' | 'caption';
71
+ export type PanelTextAlign = 'left' | 'center' | 'right';
72
+ export type PanelSize = 'small' | 'medium' | 'large';
73
+ interface PanelTextFields {
74
+ text: string;
75
+ style?: PanelTextStyle;
76
+ align?: PanelTextAlign;
77
+ /** Draw on the same line as the element that follows. Purely a line-break hint. */
78
+ joinNext?: boolean;
79
+ }
80
+ export interface PanelTextElement extends PanelTextFields {
81
+ kind: 'text';
82
+ }
83
+ /** Drawn only when `context.values[showIf]` is truthy. `showIf` is a value NAME, never an expression. */
84
+ export interface PanelConditionalTextElement extends PanelTextFields {
85
+ kind: 'conditionalText';
86
+ showIf: string;
87
+ }
88
+ export interface PanelImageElement {
89
+ kind: 'image';
90
+ /** Relative platform-storage key under {@link PANEL_IMAGE_ROOTS}; the platform presigns it. */
91
+ imageKey: string;
92
+ alt?: string;
93
+ maxHeight?: number;
94
+ }
95
+ export interface PanelAssetImageElement {
96
+ kind: 'assetImage';
97
+ /** `as_<32 hex>` — an arche asset-library id. */
98
+ assetId: string;
99
+ alt?: string;
100
+ maxHeight?: number;
101
+ }
102
+ export interface PanelItemsElement {
103
+ kind: 'items';
104
+ showIcons?: boolean;
105
+ iconSize?: PanelSize;
106
+ emptyText?: string;
107
+ }
108
+ /** Draws `context.itemsBySlot[slot]` — one panel, several independent lists. */
109
+ export interface PanelSlottedItemsElement {
110
+ kind: 'slottedItems';
111
+ slot: string;
112
+ showIcons?: boolean;
113
+ iconSize?: PanelSize;
114
+ emptyText?: string;
115
+ }
116
+ export interface PanelButtonElement {
117
+ kind: 'button';
118
+ label: string;
119
+ /** `close` | `emit:<token>` | `open_arche:arc_<32 hex>` | `open_url:https://…` */
120
+ action: string;
121
+ variant?: 'primary' | 'secondary';
122
+ }
123
+ export interface PanelSpacerElement {
124
+ kind: 'spacer';
125
+ size?: PanelSize;
126
+ }
127
+ export type PanelElement = PanelTextElement | PanelConditionalTextElement | PanelImageElement | PanelAssetImageElement | PanelItemsElement | PanelSlottedItemsElement | PanelButtonElement | PanelSpacerElement;
128
+ /** An authored hint about presentation; the call site may override any of it. */
129
+ export interface PanelPresentation {
130
+ size?: PanelSize;
131
+ dismissible?: boolean;
132
+ priority?: number;
133
+ }
134
+ export interface PanelDefinition {
135
+ /** 2–64 chars: lowercase letters, digits and hyphens, not starting with a hyphen. */
136
+ id: string;
137
+ title: string;
138
+ elements: PanelElement[];
139
+ presentation?: PanelPresentation;
140
+ }
141
+ /** Colours, font, radii, spacing and quantity format for a whole catalog. */
142
+ export interface PanelTheme {
143
+ bg?: string;
144
+ fg?: string;
145
+ border?: string;
146
+ accent?: string;
147
+ accentFg?: string;
148
+ scrim?: string;
149
+ font?: string;
150
+ radius?: string;
151
+ buttonRadius?: string;
152
+ gap?: string;
153
+ fontSize?: string;
154
+ widthSmall?: string;
155
+ widthMedium?: string;
156
+ widthLarge?: string;
157
+ z?: number;
158
+ /** Must contain `{n}` — `×{n}` renders 25 as `×25`. */
159
+ amountFormat?: string;
160
+ }
161
+ /** A plugin's own stored panels: the definitions plus one optional theme. */
162
+ export interface PanelCatalog {
163
+ panels: PanelDefinition[];
164
+ theme?: PanelTheme;
165
+ }
166
+ /** `ok: false` carries one human-readable issue per problem, each prefixed by its path. */
167
+ export type PanelValidationResult = {
168
+ ok: true;
169
+ issues: [];
170
+ } | {
171
+ ok: false;
172
+ issues: string[];
173
+ };
174
+ type Issues = string[];
175
+ /**
176
+ * The closed action namespace. `open_url` is checked by PARSING the URL, not by a
177
+ * regex, so `javascript:`, `data:`, protocol-relative and whitespace-obfuscated
178
+ * forms are rejected the same way the platform rejects them.
179
+ */
180
+ export declare function validatePanelAction(action: unknown, path: string, issues: Issues): void;
181
+ /**
182
+ * Validate an authored theme.
183
+ *
184
+ * The grammars are tight on purpose, and the reason is not style policing: a theme
185
+ * value becomes a CSS custom property, and a custom property can otherwise carry
186
+ * `url(…)` — an outbound request from every surface that draws one of your panels
187
+ * — or a `;` that escapes its declaration. Colours are hex or `rgb()/rgba()`,
188
+ * lengths are `px`/`rem`/`em`, and a font stack admits no parentheses at all.
189
+ */
190
+ export declare function validatePanelTheme(value: unknown, path?: string): PanelValidationResult;
191
+ /** Validate one panel definition. */
192
+ export declare function validatePanelDefinition(value: unknown, path?: string): PanelValidationResult;
193
+ /**
194
+ * Validate a whole catalog — `{ panels, theme? }` — before you STORE it.
195
+ *
196
+ * Call this on the write side (a DataStore document, a config blob, an admin form
197
+ * submit), not on the read side. A renderer skipping an element it cannot draw is
198
+ * a designed degrade; a panel that was never valid is an authoring bug, and the
199
+ * difference between the two is which side of the write you find out on.
200
+ *
201
+ * ```ts
202
+ * const result = validatePanelCatalog(draft);
203
+ * if (!result.ok) return setErrors(result.issues);
204
+ * await datastore.put('panels', draft);
205
+ * ```
206
+ *
207
+ * Panel ids must be unique within a catalog: they are used for dedupe, telemetry
208
+ * and render keys, so a duplicate is an authoring error rather than a
209
+ * last-one-wins.
210
+ */
211
+ export declare function validatePanelCatalog(value: unknown): PanelValidationResult;
212
+ export {};
213
+ //# sourceMappingURL=panels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panels.d.ts","sourceRoot":"","sources":["../src/panels.ts"],"names":[],"mappings":"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"}
package/dist/panels.js ADDED
@@ -0,0 +1,422 @@
1
+ "use strict";
2
+ /**
3
+ * Panels — the declarative content shape, and a validator for plugins that store
4
+ * panels as DATA.
5
+ *
6
+ * A panel is a bounded block of content (a title, ordered leaf elements, some
7
+ * buttons) rendered by `@fias/panel-kit`, which the platform serves to plugin
8
+ * iframes from its own CDN (declare the `sandbox:vendored-libraries` permission
9
+ * and list `"@fias/panel-kit"` in your manifest `dependencies`). See
10
+ * `templates/default/CLAUDE.md` § Panels.
11
+ *
12
+ * WHAT THIS MODULE IS FOR. Rendering needs no help from the SDK — panel-kit takes
13
+ * a plain object. What the SDK adds is the part a plugin cannot do for itself:
14
+ *
15
+ * 1. **The types**, so a plugin written against the published npm package has
16
+ * the same shape the platform validates, without depending on panel-kit's
17
+ * private workspace package for type information.
18
+ * 2. **`validatePanelCatalog`**, so a plugin that keeps its panels as data —
19
+ * in `useFiasDataStore`, in a config document, in an admin-edited blob —
20
+ * can gate a WRITE the way the platform gates its own server-authored
21
+ * catalogs, instead of discovering a malformed panel when a player sees a
22
+ * blank one.
23
+ *
24
+ * That second point is the whole design: **panels stored as data need no new
25
+ * platform op.** A plugin already has generic storage; what it lacked was the
26
+ * shared definition of "valid". Nothing here talks to the platform, nothing is
27
+ * gated, nothing is billed — it is a pure function over a value you already hold,
28
+ * which is why it can live in the SDK rather than behind a bridge call.
29
+ *
30
+ * MIRROR, NOT A FORK. The canonical schema is `panelThemeSchema` /
31
+ * `panelElementSchema` in `packages/db-types/src/panel-definition.ts` (Zod,
32
+ * server-side). This is a hand-written mirror for the same reason panel-kit's
33
+ * types are: `@fias/db-types` is a server package that would drag zod and a large
34
+ * unrelated surface into every plugin bundle. `tests/architecture/panel-schema-mirror.test.ts`
35
+ * runs the canonical corpora against BOTH mirrors, so a grammar cannot drift here
36
+ * without failing CI.
37
+ *
38
+ * A renderer older than a definition SKIPS element kinds it does not know. This
39
+ * validator is deliberately stricter than that: an unknown kind is an ERROR, so an
40
+ * authoring mistake fails at write time rather than rendering as silence.
41
+ */
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.PANEL_ELEMENT_KINDS = exports.PANEL_IMAGE_ROOTS = exports.PANEL_THEME_LENGTH_MAX_INTEGER_DIGITS = exports.PANEL_THEME_Z_MAX = exports.PANEL_THEME_AMOUNT_FORMAT_MAX_LENGTH = exports.PANEL_THEME_FONT_MAX_LENGTH = exports.PANEL_PRIORITY_ABS_MAX = exports.PANEL_SHOW_IF_MAX_LENGTH = exports.PANEL_ITEMS_SLOT_MAX_LENGTH = exports.PANEL_ITEMS_EMPTY_TEXT_MAX_LENGTH = exports.PANEL_EMIT_TOKEN_MAX_LENGTH = exports.PANEL_ACTION_MAX_LENGTH = exports.PANEL_BUTTON_LABEL_MAX_LENGTH = exports.PANEL_IMAGE_MAX_HEIGHT = exports.PANEL_IMAGE_MIN_HEIGHT = exports.PANEL_IMAGE_KEY_MAX_LENGTH = exports.PANEL_IMAGE_ALT_MAX_LENGTH = exports.PANEL_TEXT_MAX_LENGTH = exports.PANEL_TITLE_MAX_LENGTH = exports.PANEL_MAX_ELEMENTS = exports.PANEL_MIN_ELEMENTS = void 0;
44
+ exports.validatePanelAction = validatePanelAction;
45
+ exports.validatePanelTheme = validatePanelTheme;
46
+ exports.validatePanelDefinition = validatePanelDefinition;
47
+ exports.validatePanelCatalog = validatePanelCatalog;
48
+ /** Element list bounds. Mirrors `PANEL_*` in db-types. */
49
+ exports.PANEL_MIN_ELEMENTS = 1;
50
+ exports.PANEL_MAX_ELEMENTS = 24;
51
+ exports.PANEL_TITLE_MAX_LENGTH = 80;
52
+ exports.PANEL_TEXT_MAX_LENGTH = 500;
53
+ exports.PANEL_IMAGE_ALT_MAX_LENGTH = 120;
54
+ exports.PANEL_IMAGE_KEY_MAX_LENGTH = 500;
55
+ exports.PANEL_IMAGE_MIN_HEIGHT = 24;
56
+ exports.PANEL_IMAGE_MAX_HEIGHT = 512;
57
+ exports.PANEL_BUTTON_LABEL_MAX_LENGTH = 40;
58
+ exports.PANEL_ACTION_MAX_LENGTH = 2048;
59
+ exports.PANEL_EMIT_TOKEN_MAX_LENGTH = 64;
60
+ exports.PANEL_ITEMS_EMPTY_TEXT_MAX_LENGTH = 120;
61
+ exports.PANEL_ITEMS_SLOT_MAX_LENGTH = 64;
62
+ exports.PANEL_SHOW_IF_MAX_LENGTH = 64;
63
+ exports.PANEL_PRIORITY_ABS_MAX = 1000;
64
+ exports.PANEL_THEME_FONT_MAX_LENGTH = 200;
65
+ exports.PANEL_THEME_AMOUNT_FORMAT_MAX_LENGTH = 24;
66
+ exports.PANEL_THEME_Z_MAX = 1000000;
67
+ exports.PANEL_THEME_LENGTH_MAX_INTEGER_DIGITS = 4;
68
+ /**
69
+ * Storage roots the platform will presign inside a panel image key. A key outside
70
+ * them is not merely unusual — it is one the platform will not resolve, so it
71
+ * would render as a permanently missing image.
72
+ */
73
+ exports.PANEL_IMAGE_ROOTS = ['dfe-icons', 'dfe-avatars'];
74
+ /** Leaf kinds only — panels do not nest. Order matches the canonical union. */
75
+ exports.PANEL_ELEMENT_KINDS = [
76
+ 'text',
77
+ 'conditionalText',
78
+ 'image',
79
+ 'assetImage',
80
+ 'items',
81
+ 'slottedItems',
82
+ 'button',
83
+ 'spacer',
84
+ ];
85
+ const PANEL_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,63}$/;
86
+ const SLOT_PATTERN = new RegExp(`^[a-z0-9][a-z0-9_-]{0,${exports.PANEL_ITEMS_SLOT_MAX_LENGTH - 1}}$`);
87
+ const EMIT_TOKEN_PATTERN = new RegExp(`^[a-z0-9][a-z0-9_-]{0,${exports.PANEL_EMIT_TOKEN_MAX_LENGTH - 1}}$`);
88
+ const SHOW_IF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
89
+ const ASSET_ID_PATTERN = /^as_[a-f0-9]{32}$/;
90
+ const ARCHE_ID_PATTERN = /^arc_[a-f0-9]{32}$/;
91
+ const IMAGE_KEY_PATTERN = new RegExp(`^(?:${exports.PANEL_IMAGE_ROOTS.join('|')})/[A-Za-z0-9._/-]+\\.(?:png|jpe?g|webp|gif|svg)$`, 'i');
92
+ const THEME_COLOR_PATTERN = /^(?:#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})|rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(?:,\s*(?:0|1|0?\.\d{1,3})\s*)?\))$/i;
93
+ const THEME_LENGTH_PATTERN = new RegExp(`^\\d{1,${exports.PANEL_THEME_LENGTH_MAX_INTEGER_DIGITS}}(?:\\.\\d{1,3})?(?:px|rem|em)$`);
94
+ const THEME_FONT_PATTERN = /^[A-Za-z0-9 '",._-]+$/;
95
+ const TEXT_STYLES = ['title', 'body', 'caption'];
96
+ const TEXT_ALIGNS = ['left', 'center', 'right'];
97
+ const SIZES = ['small', 'medium', 'large'];
98
+ const BUTTON_VARIANTS = ['primary', 'secondary'];
99
+ function isRecord(value) {
100
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
101
+ }
102
+ /** `.strict()`, mirrored: an unrecognized field is a typo that would silently do nothing. */
103
+ function rejectUnknownKeys(value, allowed, path, issues) {
104
+ for (const key of Object.keys(value)) {
105
+ if (!allowed.includes(key))
106
+ issues.push(`${path}: unknown field "${key}"`);
107
+ }
108
+ }
109
+ function checkString(value, path, issues, opts) {
110
+ if (typeof value !== 'string') {
111
+ issues.push(`${path}: expected a string`);
112
+ return;
113
+ }
114
+ if (value.length < (opts.min ?? 1))
115
+ issues.push(`${path}: must not be empty`);
116
+ if (value.length > opts.max)
117
+ issues.push(`${path}: longer than ${opts.max} characters`);
118
+ if (opts.pattern && !opts.pattern.test(value)) {
119
+ issues.push(`${path}: ${opts.hint ?? 'does not match the required format'}`);
120
+ }
121
+ }
122
+ function checkOptionalEnum(value, path, issues, allowed) {
123
+ if (value === undefined)
124
+ return;
125
+ if (typeof value !== 'string' || !allowed.includes(value)) {
126
+ issues.push(`${path}: must be one of ${allowed.join(' | ')}`);
127
+ }
128
+ }
129
+ function checkOptionalBoolean(value, path, issues) {
130
+ if (value !== undefined && typeof value !== 'boolean')
131
+ issues.push(`${path}: expected a boolean`);
132
+ }
133
+ function checkOptionalIntRange(value, path, issues, min, max) {
134
+ if (value === undefined)
135
+ return;
136
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < min || value > max) {
137
+ issues.push(`${path}: must be an integer between ${min} and ${max}`);
138
+ }
139
+ }
140
+ /**
141
+ * The closed action namespace. `open_url` is checked by PARSING the URL, not by a
142
+ * regex, so `javascript:`, `data:`, protocol-relative and whitespace-obfuscated
143
+ * forms are rejected the same way the platform rejects them.
144
+ */
145
+ function validatePanelAction(action, path, issues) {
146
+ if (typeof action !== 'string' || action.length === 0) {
147
+ issues.push(`${path}: expected an action string`);
148
+ return;
149
+ }
150
+ if (action.length > exports.PANEL_ACTION_MAX_LENGTH) {
151
+ issues.push(`${path}: longer than ${exports.PANEL_ACTION_MAX_LENGTH} characters`);
152
+ return;
153
+ }
154
+ if (action === 'close')
155
+ return;
156
+ if (action.startsWith('emit:')) {
157
+ if (!EMIT_TOKEN_PATTERN.test(action.slice('emit:'.length))) {
158
+ issues.push(`${path}: emit token must be 1–${exports.PANEL_EMIT_TOKEN_MAX_LENGTH} chars — lowercase letters, digits, hyphens and underscores, not starting with a separator`);
159
+ }
160
+ return;
161
+ }
162
+ if (action.startsWith('open_arche:')) {
163
+ if (!ARCHE_ID_PATTERN.test(action.slice('open_arche:'.length))) {
164
+ issues.push(`${path}: open_arche target must be an arche id (arc_ + 32 hex chars)`);
165
+ }
166
+ return;
167
+ }
168
+ if (action.startsWith('open_url:')) {
169
+ if (/\s/.test(action)) {
170
+ issues.push(`${path}: action must not contain whitespace`);
171
+ return;
172
+ }
173
+ let parsed;
174
+ try {
175
+ parsed = new URL(action.slice('open_url:'.length));
176
+ }
177
+ catch {
178
+ issues.push(`${path}: open_url must be a valid absolute URL`);
179
+ return;
180
+ }
181
+ if (parsed.protocol !== 'https:')
182
+ issues.push(`${path}: open_url scheme must be https`);
183
+ return;
184
+ }
185
+ issues.push(`${path}: must be one of close | emit:<token> | open_arche:arc_<32 hex> | open_url:https://…`);
186
+ }
187
+ function validateTextFields(element, path, issues) {
188
+ checkString(element.text, `${path}.text`, issues, { max: exports.PANEL_TEXT_MAX_LENGTH });
189
+ checkOptionalEnum(element.style, `${path}.style`, issues, TEXT_STYLES);
190
+ checkOptionalEnum(element.align, `${path}.align`, issues, TEXT_ALIGNS);
191
+ checkOptionalBoolean(element.joinNext, `${path}.joinNext`, issues);
192
+ }
193
+ function validateItemsFields(element, path, issues) {
194
+ checkOptionalBoolean(element.showIcons, `${path}.showIcons`, issues);
195
+ checkOptionalEnum(element.iconSize, `${path}.iconSize`, issues, SIZES);
196
+ if (element.emptyText !== undefined) {
197
+ checkString(element.emptyText, `${path}.emptyText`, issues, {
198
+ min: 0,
199
+ max: exports.PANEL_ITEMS_EMPTY_TEXT_MAX_LENGTH,
200
+ });
201
+ }
202
+ }
203
+ function validateImageFields(element, path, issues) {
204
+ if (element.alt !== undefined) {
205
+ checkString(element.alt, `${path}.alt`, issues, { min: 0, max: exports.PANEL_IMAGE_ALT_MAX_LENGTH });
206
+ }
207
+ checkOptionalIntRange(element.maxHeight, `${path}.maxHeight`, issues, exports.PANEL_IMAGE_MIN_HEIGHT, exports.PANEL_IMAGE_MAX_HEIGHT);
208
+ }
209
+ function validateElement(value, path, issues) {
210
+ if (!isRecord(value)) {
211
+ issues.push(`${path}: expected an object`);
212
+ return;
213
+ }
214
+ const kind = value.kind;
215
+ if (typeof kind !== 'string' || !exports.PANEL_ELEMENT_KINDS.includes(kind)) {
216
+ issues.push(`${path}.kind: unknown element kind ${JSON.stringify(kind)} — expected one of ${exports.PANEL_ELEMENT_KINDS.join(' | ')}`);
217
+ return;
218
+ }
219
+ switch (kind) {
220
+ case 'text':
221
+ rejectUnknownKeys(value, ['kind', 'text', 'style', 'align', 'joinNext'], path, issues);
222
+ validateTextFields(value, path, issues);
223
+ return;
224
+ case 'conditionalText':
225
+ rejectUnknownKeys(value, ['kind', 'text', 'style', 'align', 'joinNext', 'showIf'], path, issues);
226
+ validateTextFields(value, path, issues);
227
+ checkString(value.showIf, `${path}.showIf`, issues, {
228
+ max: exports.PANEL_SHOW_IF_MAX_LENGTH,
229
+ pattern: SHOW_IF_PATTERN,
230
+ hint: 'must be a value NAME (letters, digits, underscore; not starting with a digit) — expressions and operators are not supported',
231
+ });
232
+ return;
233
+ case 'image':
234
+ rejectUnknownKeys(value, ['kind', 'imageKey', 'alt', 'maxHeight'], path, issues);
235
+ checkString(value.imageKey, `${path}.imageKey`, issues, {
236
+ max: exports.PANEL_IMAGE_KEY_MAX_LENGTH,
237
+ pattern: IMAGE_KEY_PATTERN,
238
+ hint: `must be a relative key under ${exports.PANEL_IMAGE_ROOTS.join(' or ')}/ ending in an image extension (no URL, no leading slash)`,
239
+ });
240
+ if (typeof value.imageKey === 'string' && value.imageKey.includes('..')) {
241
+ issues.push(`${path}.imageKey: must not contain ".."`);
242
+ }
243
+ validateImageFields(value, path, issues);
244
+ return;
245
+ case 'assetImage':
246
+ rejectUnknownKeys(value, ['kind', 'assetId', 'alt', 'maxHeight'], path, issues);
247
+ checkString(value.assetId, `${path}.assetId`, issues, {
248
+ max: 64,
249
+ pattern: ASSET_ID_PATTERN,
250
+ hint: 'must be an arche asset id (as_ + 32 hex chars)',
251
+ });
252
+ validateImageFields(value, path, issues);
253
+ return;
254
+ case 'items':
255
+ rejectUnknownKeys(value, ['kind', 'showIcons', 'iconSize', 'emptyText'], path, issues);
256
+ validateItemsFields(value, path, issues);
257
+ return;
258
+ case 'slottedItems':
259
+ rejectUnknownKeys(value, ['kind', 'slot', 'showIcons', 'iconSize', 'emptyText'], path, issues);
260
+ checkString(value.slot, `${path}.slot`, issues, {
261
+ max: exports.PANEL_ITEMS_SLOT_MAX_LENGTH,
262
+ pattern: SLOT_PATTERN,
263
+ hint: 'must be lowercase letters, digits, hyphens and underscores, not starting with a separator',
264
+ });
265
+ validateItemsFields(value, path, issues);
266
+ return;
267
+ case 'button':
268
+ rejectUnknownKeys(value, ['kind', 'label', 'action', 'variant'], path, issues);
269
+ checkString(value.label, `${path}.label`, issues, { max: exports.PANEL_BUTTON_LABEL_MAX_LENGTH });
270
+ validatePanelAction(value.action, `${path}.action`, issues);
271
+ checkOptionalEnum(value.variant, `${path}.variant`, issues, BUTTON_VARIANTS);
272
+ return;
273
+ case 'spacer':
274
+ rejectUnknownKeys(value, ['kind', 'size'], path, issues);
275
+ checkOptionalEnum(value.size, `${path}.size`, issues, SIZES);
276
+ return;
277
+ }
278
+ }
279
+ const THEME_COLOR_FIELDS = ['bg', 'fg', 'border', 'accent', 'accentFg', 'scrim'];
280
+ const THEME_LENGTH_FIELDS = [
281
+ 'radius',
282
+ 'buttonRadius',
283
+ 'gap',
284
+ 'fontSize',
285
+ 'widthSmall',
286
+ 'widthMedium',
287
+ 'widthLarge',
288
+ ];
289
+ /**
290
+ * Validate an authored theme.
291
+ *
292
+ * The grammars are tight on purpose, and the reason is not style policing: a theme
293
+ * value becomes a CSS custom property, and a custom property can otherwise carry
294
+ * `url(…)` — an outbound request from every surface that draws one of your panels
295
+ * — or a `;` that escapes its declaration. Colours are hex or `rgb()/rgba()`,
296
+ * lengths are `px`/`rem`/`em`, and a font stack admits no parentheses at all.
297
+ */
298
+ function validatePanelTheme(value, path = 'theme') {
299
+ const issues = [];
300
+ if (!isRecord(value))
301
+ return { ok: false, issues: [`${path}: expected an object`] };
302
+ rejectUnknownKeys(value, [...THEME_COLOR_FIELDS, ...THEME_LENGTH_FIELDS, 'font', 'z', 'amountFormat'], path, issues);
303
+ for (const field of THEME_COLOR_FIELDS) {
304
+ if (value[field] === undefined)
305
+ continue;
306
+ checkString(value[field], `${path}.${field}`, issues, {
307
+ max: 64,
308
+ pattern: THEME_COLOR_PATTERN,
309
+ hint: 'must be a hex colour (#rgb, #rgba, #rrggbb, #rrggbbaa) or rgb()/rgba() with numeric components',
310
+ });
311
+ }
312
+ for (const field of THEME_LENGTH_FIELDS) {
313
+ if (value[field] === undefined)
314
+ continue;
315
+ checkString(value[field], `${path}.${field}`, issues, {
316
+ max: 16,
317
+ pattern: THEME_LENGTH_PATTERN,
318
+ hint: `must be a non-negative number (max ${exports.PANEL_THEME_LENGTH_MAX_INTEGER_DIGITS} digits before the point) followed by px, rem or em`,
319
+ });
320
+ }
321
+ if (value.font !== undefined) {
322
+ checkString(value.font, `${path}.font`, issues, {
323
+ max: exports.PANEL_THEME_FONT_MAX_LENGTH,
324
+ pattern: THEME_FONT_PATTERN,
325
+ hint: 'must be a font-family stack (names, commas, quotes) — no parentheses, so no url() or var()',
326
+ });
327
+ }
328
+ checkOptionalIntRange(value.z, `${path}.z`, issues, 0, exports.PANEL_THEME_Z_MAX);
329
+ if (value.amountFormat !== undefined) {
330
+ checkString(value.amountFormat, `${path}.amountFormat`, issues, {
331
+ max: exports.PANEL_THEME_AMOUNT_FORMAT_MAX_LENGTH,
332
+ });
333
+ if (typeof value.amountFormat === 'string' && !value.amountFormat.includes('{n}')) {
334
+ issues.push(`${path}.amountFormat: must contain the placeholder {n}`);
335
+ }
336
+ }
337
+ return issues.length === 0 ? { ok: true, issues: [] } : { ok: false, issues };
338
+ }
339
+ /** Validate one panel definition. */
340
+ function validatePanelDefinition(value, path = 'panel') {
341
+ const issues = [];
342
+ if (!isRecord(value))
343
+ return { ok: false, issues: [`${path}: expected an object`] };
344
+ rejectUnknownKeys(value, ['id', 'title', 'elements', 'presentation'], path, issues);
345
+ checkString(value.id, `${path}.id`, issues, {
346
+ max: 64,
347
+ pattern: PANEL_ID_PATTERN,
348
+ hint: 'must be 2–64 chars: lowercase letters, digits and hyphens, not starting with a hyphen',
349
+ });
350
+ checkString(value.title, `${path}.title`, issues, { max: exports.PANEL_TITLE_MAX_LENGTH });
351
+ if (!Array.isArray(value.elements)) {
352
+ issues.push(`${path}.elements: expected an array`);
353
+ }
354
+ else {
355
+ if (value.elements.length < exports.PANEL_MIN_ELEMENTS) {
356
+ issues.push(`${path}.elements: needs at least ${exports.PANEL_MIN_ELEMENTS} element`);
357
+ }
358
+ if (value.elements.length > exports.PANEL_MAX_ELEMENTS) {
359
+ issues.push(`${path}.elements: more than ${exports.PANEL_MAX_ELEMENTS} elements`);
360
+ }
361
+ value.elements.forEach((element, index) => validateElement(element, `${path}.elements[${index}]`, issues));
362
+ }
363
+ if (value.presentation !== undefined) {
364
+ if (!isRecord(value.presentation)) {
365
+ issues.push(`${path}.presentation: expected an object`);
366
+ }
367
+ else {
368
+ rejectUnknownKeys(value.presentation, ['size', 'dismissible', 'priority'], `${path}.presentation`, issues);
369
+ checkOptionalEnum(value.presentation.size, `${path}.presentation.size`, issues, SIZES);
370
+ checkOptionalBoolean(value.presentation.dismissible, `${path}.presentation.dismissible`, issues);
371
+ checkOptionalIntRange(value.presentation.priority, `${path}.presentation.priority`, issues, -exports.PANEL_PRIORITY_ABS_MAX, exports.PANEL_PRIORITY_ABS_MAX);
372
+ }
373
+ }
374
+ return issues.length === 0 ? { ok: true, issues: [] } : { ok: false, issues };
375
+ }
376
+ /**
377
+ * Validate a whole catalog — `{ panels, theme? }` — before you STORE it.
378
+ *
379
+ * Call this on the write side (a DataStore document, a config blob, an admin form
380
+ * submit), not on the read side. A renderer skipping an element it cannot draw is
381
+ * a designed degrade; a panel that was never valid is an authoring bug, and the
382
+ * difference between the two is which side of the write you find out on.
383
+ *
384
+ * ```ts
385
+ * const result = validatePanelCatalog(draft);
386
+ * if (!result.ok) return setErrors(result.issues);
387
+ * await datastore.put('panels', draft);
388
+ * ```
389
+ *
390
+ * Panel ids must be unique within a catalog: they are used for dedupe, telemetry
391
+ * and render keys, so a duplicate is an authoring error rather than a
392
+ * last-one-wins.
393
+ */
394
+ function validatePanelCatalog(value) {
395
+ const issues = [];
396
+ if (!isRecord(value))
397
+ return { ok: false, issues: ['catalog: expected an object'] };
398
+ rejectUnknownKeys(value, ['panels', 'theme'], 'catalog', issues);
399
+ if (value.theme !== undefined) {
400
+ const theme = validatePanelTheme(value.theme, 'catalog.theme');
401
+ if (!theme.ok)
402
+ issues.push(...theme.issues);
403
+ }
404
+ if (!Array.isArray(value.panels)) {
405
+ issues.push('catalog.panels: expected an array');
406
+ return { ok: false, issues };
407
+ }
408
+ const seen = new Set();
409
+ value.panels.forEach((panel, index) => {
410
+ const path = `catalog.panels[${index}]`;
411
+ const result = validatePanelDefinition(panel, path);
412
+ if (!result.ok)
413
+ issues.push(...result.issues);
414
+ if (isRecord(panel) && typeof panel.id === 'string') {
415
+ if (seen.has(panel.id))
416
+ issues.push(`${path}.id: duplicate panel id "${panel.id}"`);
417
+ seen.add(panel.id);
418
+ }
419
+ });
420
+ return issues.length === 0 ? { ok: true, issues: [] } : { ok: false, issues };
421
+ }
422
+ //# sourceMappingURL=panels.js.map