mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Live picker: derive the command vocabulary from one canonical source
The verbs/labels/icons were copied three ways: live-browser.js (ICONS + ACTIONS), VISUAL_ACTIONS in live-event-validation.mjs, and the marketing demo. Collapse them to one source, skill/scripts/live-vocabulary.mjs (LIVE_COMMANDS + derived VISUAL_ACTIONS). - live-event-validation.mjs imports VISUAL_ACTIONS from it. - live-server.mjs serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ when it serves /live.js, next to the token/port. live-browser.js (served raw, can't import at runtime) builds its ICONS + ACTIONS from that injected vocab instead of an inline copy — byte-identical icons, zero behaviour change. - site/components/LiveDemoPalette.astro imports the same module at build time, so the demo and the real picker can no longer drift. Adds a /live.js test asserting the injected vocab deep-equals the canonical list. Harness skill dirs refreshed via build. (Pre-existing, unrelated: `bun run build:site` fails on an htmlparser2 import in the CLI detector.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1f975a69e4
commit
5fb30e03e6
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -1,27 +1,12 @@
|
||||
---
|
||||
// Command-palette switcher for the marketing live-demo loop. Renders the
|
||||
// design-command vocabulary as a 4-column icon grid; the demo loop in
|
||||
// live-demo.js opens it and lands on the `pick` verb.
|
||||
// Command-palette switcher for the marketing live-demo loop. Renders the design
|
||||
// vocabulary as a 4-column icon grid; the demo loop in live-demo.js opens it and
|
||||
// lands on the `pick` verb.
|
||||
//
|
||||
// The values/labels/icons mirror the real picker's ICONS + ACTIONS in
|
||||
// skill/scripts/live-browser.js (which is served raw and injected as an IIFE, so
|
||||
// it can't import shared modules at runtime — see the chat thread for the
|
||||
// proposed convergence). Keep this list in sync if a verb is added or reordered.
|
||||
const A = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"';
|
||||
const COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${A}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${A}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${A}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${A}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${A}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${A}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${A}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${A}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${A}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${A}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${A}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${A}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
// Values/labels/icons come from the same canonical source the real picker uses
|
||||
// (skill/scripts/live-vocabulary.mjs), imported at build time, so the demo and
|
||||
// the product never drift.
|
||||
import { LIVE_COMMANDS } from '../../skill/scripts/live-vocabulary.mjs';
|
||||
|
||||
interface Props {
|
||||
pick: string;
|
||||
@@ -31,7 +16,7 @@ const { pick } = Astro.props;
|
||||
|
||||
<div class="live-demo-ctx-palette" data-demo-palette data-demo-pick={pick} aria-hidden="true">
|
||||
<div class="live-demo-ctx-palette-grid">
|
||||
{COMMANDS.map((c) => (
|
||||
{LIVE_COMMANDS.map((c) => (
|
||||
<button type="button" class="live-demo-ctx-palette-chip" data-cmd={c.value}>
|
||||
<span class="live-demo-ctx-palette-ico" set:html={c.icon}></span>
|
||||
<span class="live-demo-ctx-palette-name">{c.label}</span>
|
||||
|
||||
@@ -81,39 +81,18 @@
|
||||
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
|
||||
]);
|
||||
|
||||
// SVG icons stack above each chip label. All strokes use currentColor so the
|
||||
// icon recolors to C.brand when its chip is selected. 20x20 render, 24-viewBox,
|
||||
// 1.5 stroke - visually consistent with the Foundation grid on the homepage.
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
const ICONS = {
|
||||
impeccable: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>`,
|
||||
bolder: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>`,
|
||||
quieter: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>`,
|
||||
distill: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>`,
|
||||
polish: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>`,
|
||||
typeset: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>`,
|
||||
colorize: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>`,
|
||||
layout: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>`,
|
||||
adapt: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>`,
|
||||
animate: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>`,
|
||||
delight: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>`,
|
||||
overdrive: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>`,
|
||||
};
|
||||
|
||||
const ACTIONS = [
|
||||
{ value: 'impeccable', label: 'Freeform' },
|
||||
{ value: 'bolder', label: 'Bolder' },
|
||||
{ value: 'quieter', label: 'Quieter' },
|
||||
{ value: 'distill', label: 'Distill' },
|
||||
{ value: 'polish', label: 'Polish' },
|
||||
{ value: 'typeset', label: 'Typeset' },
|
||||
{ value: 'colorize', label: 'Colorize' },
|
||||
{ value: 'layout', label: 'Layout' },
|
||||
{ value: 'adapt', label: 'Adapt' },
|
||||
{ value: 'animate', label: 'Animate' },
|
||||
{ value: 'delight', label: 'Delight' },
|
||||
{ value: 'overdrive', label: 'Overdrive' },
|
||||
];
|
||||
// Command vocabulary (values + labels + icons) comes from the canonical source,
|
||||
// skill/scripts/live-vocabulary.mjs, which live-server.mjs serializes into
|
||||
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
|
||||
// the token/port above, so it is always present here). The icons stack above
|
||||
// each chip label and recolor to C.brand when selected (strokes use
|
||||
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
|
||||
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
|
||||
const ICONS = {};
|
||||
const ACTIONS = VOCAB.map((c) => {
|
||||
ICONS[c.value] = c.icon;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { canCreateInsert } from './live-insert-ui.mjs';
|
||||
|
||||
export const VISUAL_ACTIONS = [
|
||||
'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset',
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
// The accepted visual action values come from the canonical vocabulary so the
|
||||
// validator, the picker UI, and the marketing demo never drift. Imported (not
|
||||
// just re-exported) so it is also in scope for the validators below.
|
||||
import { VISUAL_ACTIONS } from './live-vocabulary.mjs';
|
||||
export { VISUAL_ACTIONS };
|
||||
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { parseDesignMd } from './design-parser.mjs';
|
||||
import { resolveContextDir } from './context.mjs';
|
||||
import { createLiveSessionStore } from './live-session-store.mjs';
|
||||
import { validateEvent } from './live-event-validation.mjs';
|
||||
import { LIVE_COMMANDS } from './live-vocabulary.mjs';
|
||||
import {
|
||||
getDesignSidecarPath,
|
||||
getLiveDir,
|
||||
@@ -1272,6 +1273,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
||||
const body =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${state.port};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` +
|
||||
sessionScript + '\n' +
|
||||
liveScript;
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Canonical design-command vocabulary for Live Mode: each command's value, human
|
||||
* label, and SVG icon. Icons stack above the chip label; strokes use currentColor
|
||||
* so the icon recolors when its chip is selected.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live-event-validation.mjs — re-exports VISUAL_ACTIONS.
|
||||
* - skill/scripts/live-browser.js — the real picker. It is served raw and
|
||||
* injected as an IIFE, so it cannot import this at runtime; live-server.mjs
|
||||
* serializes LIVE_COMMANDS into window.__IMPECCABLE_VOCAB__ alongside the
|
||||
* token/port, and live-browser.js builds its ICONS + ACTIONS from that.
|
||||
* - site/components/LiveDemoPalette.astro — the marketing demo palette (imported
|
||||
* at build time).
|
||||
*
|
||||
* Add, rename, or reorder a verb here and all three follow.
|
||||
*/
|
||||
|
||||
const ICON_ATTRS = 'width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block"';
|
||||
|
||||
export const LIVE_COMMANDS = [
|
||||
{ value: 'impeccable', label: 'Freeform', icon: `<svg ${ICON_ATTRS}><path d="M4 20l4-1L18 9l-3-3L5 16z"/><path d="M14 7l3 3"/></svg>` },
|
||||
{ value: 'bolder', label: 'Bolder', icon: `<svg ${ICON_ATTRS}><rect x="6" y="12" width="4" height="7" rx="0.5"/><rect x="14" y="5" width="4" height="14" rx="0.5"/></svg>` },
|
||||
{ value: 'quieter', label: 'Quieter', icon: `<svg ${ICON_ATTRS}><rect x="6" y="5" width="4" height="14" rx="0.5"/><rect x="14" y="12" width="4" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'distill', label: 'Distill', icon: `<svg ${ICON_ATTRS}><path d="M4 5h16l-6 8v7l-4-2v-5z"/></svg>` },
|
||||
{ value: 'polish', label: 'Polish', icon: `<svg ${ICON_ATTRS}><path d="M15 3l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/><path d="M7 13l0.6 1.8 1.8 0.6-1.8 0.6-0.6 1.8-0.6-1.8-1.8-0.6 1.8-0.6z"/></svg>` },
|
||||
{ value: 'typeset', label: 'Typeset', icon: `<svg ${ICON_ATTRS}><path d="M5 6h14" stroke-width="2.6"/><path d="M5 12h9" stroke-width="1.9"/><path d="M5 18h5" stroke-width="1.3"/></svg>` },
|
||||
{ value: 'colorize', label: 'Colorize', icon: `<svg ${ICON_ATTRS}><circle cx="9" cy="10" r="5"/><circle cx="15" cy="10" r="5"/><circle cx="12" cy="15" r="5"/></svg>` },
|
||||
{ value: 'layout', label: 'Layout', icon: `<svg ${ICON_ATTRS}><rect x="3" y="4" width="8" height="16" rx="0.5"/><rect x="13" y="4" width="8" height="7" rx="0.5"/><rect x="13" y="13" width="8" height="7" rx="0.5"/></svg>` },
|
||||
{ value: 'adapt', label: 'Adapt', icon: `<svg ${ICON_ATTRS}><rect x="2.5" y="5" width="12" height="11" rx="1"/><line x1="2.5" y1="19" x2="14.5" y2="19"/><rect x="16.5" y="8" width="5" height="11" rx="1"/></svg>` },
|
||||
{ value: 'animate', label: 'Animate', icon: `<svg ${ICON_ATTRS}><path d="M3 18c4-4 6-10 10-10"/><path d="M13 8c3 0 5 5 8 10"/><circle cx="13" cy="8" r="1.6" fill="currentColor" stroke="none"/></svg>` },
|
||||
{ value: 'delight', label: 'Delight', icon: `<svg ${ICON_ATTRS}><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"/></svg>` },
|
||||
{ value: 'overdrive', label: 'Overdrive', icon: `<svg ${ICON_ATTRS}><path d="M13 3L5 13h5l-1 8 9-12h-6z"/></svg>` },
|
||||
];
|
||||
|
||||
// Action values accepted by the live event protocol, in palette order.
|
||||
export const VISUAL_ACTIONS = LIVE_COMMANDS.map((c) => c.value);
|
||||
@@ -158,6 +158,17 @@ describe('live-server integration', () => {
|
||||
assert.equal(data.connectedClients, 0);
|
||||
});
|
||||
|
||||
it('/live.js injects the canonical command vocabulary', async () => {
|
||||
// live-browser.js builds its action picker from window.__IMPECCABLE_VOCAB__
|
||||
// rather than an inline copy, so the server must serialize the canonical
|
||||
// vocabulary into /live.js (next to the token/port).
|
||||
const { LIVE_COMMANDS } = await import('../skill/scripts/live-vocabulary.mjs');
|
||||
const body = await (await fetch(`http://localhost:${server.port}/live.js`)).text();
|
||||
assert.match(body, /window\.__IMPECCABLE_VOCAB__\s*=/);
|
||||
const injected = JSON.parse(body.match(/window\.__IMPECCABLE_VOCAB__\s*=\s*(\[.*?\]);/s)[1]);
|
||||
assert.deepEqual(injected, LIVE_COMMANDS);
|
||||
});
|
||||
|
||||
it('/status returns durable recovery state', async () => {
|
||||
await drainPolls(server);
|
||||
const eventRes = await fetch(`http://localhost:${server.port}/events`, {
|
||||
|
||||
Reference in New Issue
Block a user