@motion-proto/live-tokens 0.74.0 → 0.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/live-tokens-check-compliance/SKILL.md +33 -38
- package/.claude/skills/live-tokens-create-component/SKILL copy.md +196 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +198 -146
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +95 -52
- package/.claude/skills/live-tokens-create-component/references/intrinsics.md +2 -2
- package/.claude/skills/live-tokens-create-component/references/linked-siblings.md +2 -2
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +12 -12
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +2 -1
- package/.claude/skills/live-tokens-create-page/SKILL.md +187 -0
- package/.claude/skills/live-tokens-create-page/references/interaction-sources.md +66 -0
- package/.claude/skills/live-tokens-create-page/references/layout-sources.md +87 -0
- package/.claude/skills/live-tokens-create-theme/SKILL.md +52 -48
- package/.claude/skills/live-tokens-create-theme/references/design-directions.md +1 -1
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +69 -60
- package/.claude/skills/live-tokens-pick-component/SKILL.md +64 -79
- package/.claude/skills/live-tokens-set-colors/SKILL.md +47 -38
- package/.claude/skills/live-tokens-set-geometry/SKILL.md +58 -35
- package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +5 -3
- package/.claude/skills/live-tokens-set-type/SKILL.md +30 -30
- package/CHANGELOG.md +138 -0
- package/README.md +17 -7
- package/bin/check-component.mjs +91 -19
- package/bin/check-page.mjs +102 -21
- package/bin/cli.mjs +87 -113
- package/bin/contractRunner.mjs +945 -0
- package/bin/create.mjs +1 -1
- package/bin/lib/catalogue.mjs +37 -30
- package/bin/lib/findings.mjs +46 -15
- package/bin/lib/report.mjs +3 -3
- package/bin/lib/tokenVocabulary.mjs +4 -4
- package/bin/migrate-routes.mjs +5 -5
- package/bin/migrate.mjs +4 -4
- package/bin/save-theme.mjs +8 -9
- package/bin/set-colors.mjs +9 -11
- package/bin/set-geometry.mjs +7 -7
- package/bin/set-type.mjs +5 -7
- package/bin/setup-claude.mjs +110 -0
- package/dist-plugin/{chunk-W6Y4BWFB.js → chunk-6WGFOJXO.js} +22 -0
- package/dist-plugin/{chunk-7VRTBGJT.js → chunk-PDNL4NC5.js} +9 -2
- package/dist-plugin/{chunk-V3YF6CGT.js → chunk-SWXRVZKT.js} +43 -1
- package/dist-plugin/{dataPaths-BhWzd5cL.d.cts → dataPaths-BpIK_Obx.d.cts} +1 -0
- package/dist-plugin/{dataPaths-BhWzd5cL.d.ts → dataPaths-BpIK_Obx.d.ts} +1 -0
- package/dist-plugin/index.cjs +310 -147
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +218 -122
- package/dist-plugin/migrateData/index.cjs +66 -2
- package/dist-plugin/migrateData/index.d.cts +1 -1
- package/dist-plugin/migrateData/index.d.ts +1 -1
- package/dist-plugin/migrateData/index.js +3 -3
- package/dist-plugin/setColors/index.cjs +61 -4
- package/dist-plugin/setColors/index.d.cts +1 -1
- package/dist-plugin/setColors/index.d.ts +1 -1
- package/dist-plugin/setColors/index.js +5 -5
- package/dist-plugin/setGeometry/index.cjs +107 -44
- package/dist-plugin/setGeometry/index.d.cts +5 -5
- package/dist-plugin/setGeometry/index.d.ts +5 -5
- package/dist-plugin/setGeometry/index.js +51 -45
- package/dist-plugin/setType/index.cjs +15 -0
- package/dist-plugin/setType/index.d.cts +1 -1
- package/dist-plugin/setType/index.d.ts +1 -1
- package/dist-plugin/setType/index.js +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +35 -8
- package/src/app/site.css +19 -9
- package/src/editor/bootstrap.ts +2 -0
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +4 -4
- package/src/editor/component-editor/DialogEditor.svelte +4 -4
- package/src/editor/component-editor/MenuSelectEditor.svelte +4 -1
- package/src/editor/component-editor/SegmentedControlEditor.svelte +6 -1
- package/src/editor/component-editor/TabBarEditor.svelte +1 -1
- package/src/editor/component-editor/TableEditor.svelte +2 -2
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +6 -3
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +41 -20
- package/src/editor/core/components/adjustAliases.ts +59 -45
- package/src/editor/core/components/aliasKinds.ts +9 -5
- package/src/editor/core/preview/themePreview.ts +9 -2
- package/src/editor/core/sketch/sketchLayer.ts +22 -0
- package/src/editor/core/store/editorStore.ts +10 -1
- package/src/editor/core/themes/buildColors.ts +3 -3
- package/src/editor/core/themes/liveStateStream.ts +26 -0
- package/src/editor/core/themes/migrations/2026-09-07-stroke-role-renames.ts +52 -0
- package/src/editor/core/themes/migrations/index.ts +2 -0
- package/src/editor/core/themes/themeDocumentSync.ts +22 -11
- package/src/editor/core/themes/themeService.ts +9 -2
- package/src/editor/pages/ComponentEditorPage.svelte +17 -1
- package/src/editor/pages/liveTokensEditorHandle.ts +23 -0
- package/src/editor/skill-atlas/SkillAtlas.svelte +75 -573
- package/src/editor/skill-atlas/TreeCanvas.svelte +263 -0
- package/src/editor/skill-atlas/TreeNodeCard.svelte +136 -53
- package/src/editor/skill-atlas/edges.ts +31 -0
- package/src/editor/skill-atlas/skillSources.generated.ts +20 -19
- package/src/editor/skill-atlas/skillTrees.ts +19 -3840
- package/src/editor/skill-atlas/trees/check-compliance.ts +183 -0
- package/src/editor/skill-atlas/trees/create-component.ts +275 -0
- package/src/editor/skill-atlas/trees/create-page.ts +320 -0
- package/src/editor/skill-atlas/trees/create-theme.ts +255 -0
- package/src/editor/skill-atlas/trees/fix-findings.ts +469 -0
- package/src/editor/skill-atlas/trees/pick-component.ts +300 -0
- package/src/editor/skill-atlas/trees/set-colors.ts +148 -0
- package/src/editor/skill-atlas/trees/set-geometry.ts +136 -0
- package/src/editor/skill-atlas/trees/set-type.ts +142 -0
- package/src/editor/skill-atlas/types.ts +3 -4
- package/src/editor/skill-atlas/wireLayout.ts +287 -0
- package/src/live-tokens/data/themes/autumn.json +15 -15
- package/src/live-tokens/data/themes/halloween.json +15 -15
- package/src/live-tokens/data/themes/midnight-study.json +15 -15
- package/src/live-tokens/data/themes/ocean.json +15 -15
- package/src/live-tokens/data/themes/royal-velvet.json +15 -15
- package/src/live-tokens/data/themes/sketchy.json +15 -15
- package/src/live-tokens/data/themes/spring-meadow.json +15 -15
- package/src/live-tokens/data/themes/sunset.json +15 -15
- package/src/system/components/Badge.svelte +7 -0
- package/src/system/components/Button.svelte +7 -0
- package/src/system/components/Callout.svelte +10 -6
- package/src/system/components/Card.svelte +23 -4
- package/src/system/components/CodeSnippet.svelte +4 -3
- package/src/system/components/CollapsibleSection.svelte +23 -8
- package/src/system/components/CornerBadge.svelte +6 -0
- package/src/system/components/Dialog.svelte +13 -6
- package/src/system/components/IconButton.svelte +9 -0
- package/src/system/components/Image.svelte +8 -0
- package/src/system/components/ImageLightbox.svelte +6 -0
- package/src/system/components/InlineEditActions.svelte +7 -0
- package/src/system/components/Input.svelte +7 -0
- package/src/system/components/MenuSelect.svelte +7 -0
- package/src/system/components/Notification.svelte +7 -0
- package/src/system/components/Panel.svelte +6 -0
- package/src/system/components/ProgressBar.svelte +5 -0
- package/src/system/components/RadioButton.svelte +11 -5
- package/src/system/components/SectionDivider.svelte +8 -0
- package/src/system/components/SegmentedControl.svelte +6 -0
- package/src/system/components/SideNavigation.svelte +6 -0
- package/src/system/components/Slider.svelte +7 -4
- package/src/system/components/TabBar.svelte +15 -9
- package/src/system/components/Table.svelte +8 -3
- package/src/system/components/Toggle.svelte +4 -4
- package/src/system/components/Tooltip.svelte +6 -0
- package/src/testing-js/chunk-AO7EZHYV.js +776 -0
- package/src/testing-js/chunk-AO7EZHYV.js.map +1 -0
- package/src/testing-js/chunk-FAFOAWYL.js +39 -0
- package/src/testing-js/chunk-FAFOAWYL.js.map +1 -0
- package/src/testing-js/chunk-L73N4NSO.js +23 -0
- package/src/testing-js/chunk-L73N4NSO.js.map +1 -0
- package/src/testing-js/chunk-LXR3MN6N.js +3063 -0
- package/src/testing-js/chunk-LXR3MN6N.js.map +1 -0
- package/src/testing-js/chunk-ZMSX6CXR.js +53 -0
- package/src/testing-js/chunk-ZMSX6CXR.js.map +1 -0
- package/src/testing-js/component-alias.contract.js +81 -0
- package/src/testing-js/component-alias.contract.js.map +1 -0
- package/src/testing-js/component-editor.contract.js +62 -0
- package/src/testing-js/component-editor.contract.js.map +1 -0
- package/src/testing-js/component-render.contract.js +568 -0
- package/src/testing-js/component-render.contract.js.map +1 -0
- package/src/testing-js/index.d.ts +293 -0
- package/src/testing-js/index.js +222 -0
- package/src/testing-js/index.js.map +1 -0
- package/src/testing-js/registry.contract.js +39 -0
- package/src/testing-js/registry.contract.js.map +1 -0
- package/src/testing-js/vitest-BE6uGF31.d.ts +73 -0
- package/src/testing-js/vitest.d.ts +3 -0
- package/src/testing-js/vitest.js +13 -0
- package/src/testing-js/vitest.js.map +1 -0
- package/template/README.md +13 -0
- package/template/_gitignore +6 -0
- package/template/package.json +3 -1
- package/template/src/pages/Home.svelte +4 -18
- package/.claude/skills/live-tokens-build-page/SKILL.md +0 -103
- package/.claude/skills/live-tokens-build-page/references/layout-sources.md +0 -48
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../testing/playwright.ts","../testing/isolation.ts","../testing/port.ts"],"sourcesContent":["import path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n defineConfig,\n devices,\n type PlaywrightTestConfig,\n type PlaywrightTestProject,\n} from '@playwright/test';\nimport {\n COMPONENT_ENV,\n COMPONENTS_PATH_ENV,\n devServerCommand,\n resolveTestingConfig,\n type LiveTokensTestingConfig,\n} from './config';\nimport { allContracts, CONTRACTS_MODULE_ENV, selectedContracts } from './contracts';\nimport { DATA_DIR_ENV, TEST_DATA_DIR_ENV, isolateDataDir } from './isolation';\nimport { resolvePort } from './port';\n\nconst HOST = '127.0.0.1';\n\n/** The shipped suites, wherever the package is installed. */\nconst CONTRACT_TEST_DIR = path.dirname(fileURLToPath(import.meta.url));\n\nexport interface PlaywrightConfigOptions extends LiveTokensTestingConfig {\n /** Project root the settings resolve against. Default `process.cwd()`. */\n root?: string;\n /** Projects that run beside the contract project. */\n extraProjects?: PlaywrightTestProject[];\n /** Where projects that declare no `testDir` of their own look. */\n testDir?: string;\n}\n\n/**\n * The contract project, its dev server, and the data isolation the two share.\n *\n * Isolation runs here rather than in a global setup because the config is the\n * first thing every process in the run evaluates: the environment it leaves\n * behind reaches the workers and the dev server, and nothing in the run can\n * observe the state before it.\n */\nexport async function createPlaywrightConfig(\n options: PlaywrightConfigOptions = {},\n): Promise<PlaywrightTestConfig> {\n const settings = resolveTestingConfig(options, options.root);\n const { dataDir } = isolateDataDir(settings.dataDir);\n const port = resolvePort(settings.port);\n const baseURL = `http://${HOST}:${port}`;\n\n process.env[COMPONENTS_PATH_ENV] = settings.componentsPath;\n if (settings.contractsModule) process.env[CONTRACTS_MODULE_ENV] = settings.contractsModule;\n\n const requested = process.env[COMPONENT_ENV];\n if (requested) {\n const matches = await selectedContracts();\n if (matches.length === 0) {\n const all = await allContracts();\n throw new Error(\n `${COMPONENT_ENV}=${requested} names a component with no contract. `\n + `Declared: ${all.map((contract) => contract.id).sort().join(', ')}`,\n );\n }\n }\n\n return defineConfig({\n testDir: options.testDir ?? CONTRACT_TEST_DIR,\n // The component suites carry their serial and parallel intent per file:\n // `component-editor.contract.ts` runs each component's save and reset\n // cycles in order against one data tree. Turning this on would interleave\n // them.\n fullyParallel: false,\n // One worker is the safe default: the specs share a dev server and a data\n // directory. A run that can afford more passes a worker count on the\n // command line.\n workers: 1,\n timeout: 30_000,\n expect: { timeout: 5_000 },\n // A shared-server suite times out under CI load in ways it never does\n // locally, and without a retry budget one wobble aborts a tagged release.\n retries: process.env.CI ? 2 : 0,\n // Absolute and rooted at the consumer project, not the config file's own\n // directory: `check-component --tests`'s generated config lives in a\n // temporary directory that gets removed on completion, and a relative\n // `outputDir` would put every trace and screenshot in there too.\n outputDir: path.join(settings.root, 'test-results/playwright'),\n reporter: process.env.CI ? [['line'], ['html', { open: 'never' }]] : 'list',\n use: {\n baseURL,\n // Unset, both of these are unbounded: a locator that never resolves waits\n // out the whole test timeout and reports nothing about where it stopped.\n actionTimeout: 10_000,\n navigationTimeout: 30_000,\n trace: 'on-first-retry',\n screenshot: 'only-on-failure',\n // `retain-on-failure` still records every passing test and throws the\n // file away, which costs ~28% CPU. Match the trace policy instead.\n video: 'on-first-retry',\n },\n projects: [\n ...(options.extraProjects ?? []),\n {\n name: 'contract',\n testDir: CONTRACT_TEST_DIR,\n // `.ts` in this repo, `.js` once tsup compiles the shipped build into\n // `src/testing-js`; matching both here needs no build-time swap.\n testMatch: '**/component-*.contract.{ts,js}',\n // `.tabs-preview` now caps its sticky band at 50vh, which is enough for\n // image, panel, card and sidenavigation to pass alone at 1280x720. A\n // full parallel run still fails panel's gradient radio there, so the\n // band can cover a control at some scroll positions. Until that is\n // fixed the suite runs taller. See docs/contract-test-defects.md.\n use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 900 } },\n },\n ],\n webServer: {\n command: devServerCommand(settings.devCommand, HOST, port),\n // Otherwise defaults to the config file's own directory. A generated\n // config that lives outside the consumer root (`check-component\n // --tests`'s temporary config) would run `npm run dev` from wherever\n // that happens to be instead.\n cwd: settings.root,\n url: baseURL,\n // A server this run did not start carries none of these variables, so it\n // would write the project's own data tree.\n reuseExistingServer: false,\n timeout: 120_000,\n env: {\n ...process.env,\n [TEST_DATA_DIR_ENV]: dataDir,\n [DATA_DIR_ENV]: dataDir,\n },\n },\n });\n}\n","import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\n/**\n * The plugin reads this name ahead of its own options and\n * `live-tokens.config.json`, so setting it redirects every server-side write —\n * the data tree, `tokens.generated.css`, and `fonts.css` — into the copy.\n */\nexport const TEST_DATA_DIR_ENV = 'LIVE_TOKENS_TEST_DATA_DIR';\n\n/** What the suites and the harness read component configs from in the runner's\n * own process. It names the same directory and is a separate variable because\n * it answers a different question: where a reader looks, not where the server\n * is confined. */\nexport const DATA_DIR_ENV = 'LIVE_TOKENS_DATA_DIR';\n\n/**\n * A developer's current session rather than the document under test. Each one\n * is gitignored or dev-written, so leaving it in the copy would make the run\n * pass or fail on whichever theme the maintainer has open.\n *\n * _working.json an unsaved buffer, which reads as a dirty document and\n * can raise a destructive confirmation dialog\n * _active.json the theme the editor has open. One carrying a sketchstyle\n * boots the suite with the sketch layer on, which paints\n * every fill onto a pseudo-element and leaves the real\n * background transparent\n * _production.json the theme baked into tokens.generated.css\n *\n * A missing pointer resolves to \"default\" at runtime, so removing them is the\n * whole reset.\n */\nconst SESSION_FILES = new Set(['_working.json', '_active.json', '_production.json']);\n\nexport interface IsolatedData {\n dataDir: string;\n /** True when this process made the copy and owns removing it. */\n created: boolean;\n}\n\nlet removed = false;\n\nfunction registerCleanup(dataDir: string): void {\n const remove = () => {\n if (removed) return;\n removed = true;\n fs.rmSync(dataDir, { recursive: true, force: true });\n };\n process.once('exit', remove);\n // Playwright owns the exit path on a signal: it stops the workers and kills\n // the dev server's process group, then exits, which reaches `exit` above.\n // These two only make sure the copy is gone before that teardown starts.\n process.once('SIGINT', remove);\n process.once('SIGTERM', remove);\n}\n\n/**\n * Copy `sourceDataDir` into a unique temporary directory and point both the\n * server and the runner at it. Idempotent across processes: Playwright loads\n * the config again in every worker, and each inherits the parent's environment,\n * so only the first call copies anything.\n */\nexport function isolateDataDir(sourceDataDir: string): IsolatedData {\n const existing = process.env[TEST_DATA_DIR_ENV];\n if (existing) {\n const dataDir = path.resolve(existing);\n if (!fs.existsSync(dataDir)) {\n throw new Error(`${TEST_DATA_DIR_ENV} names ${dataDir}, which does not exist`);\n }\n process.env[DATA_DIR_ENV] = dataDir;\n return { dataDir, created: false };\n }\n\n const source = path.resolve(sourceDataDir);\n if (!fs.existsSync(source)) {\n throw new Error(\n `No data directory at ${source}. Set \\`dataDir\\` in live-tokens.testing.ts `\n + 'when the project keeps its live-tokens data somewhere else.',\n );\n }\n\n // realpath: macOS resolves os.tmpdir() through a symlink, and the plugin\n // compares resolved paths when it decides whether a write is in scope.\n const dataDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'live-tokens-contract-')));\n fs.cpSync(source, dataDir, { recursive: true });\n for (const entry of fs.readdirSync(dataDir, { recursive: true }) as string[]) {\n if (SESSION_FILES.has(path.basename(entry))) {\n fs.rmSync(path.join(dataDir, entry), { force: true });\n }\n }\n\n process.env[TEST_DATA_DIR_ENV] = dataDir;\n process.env[DATA_DIR_ENV] = dataDir;\n registerCleanup(dataDir);\n return { dataDir, created: true };\n}\n","import { execFileSync } from 'node:child_process';\n\nexport const PORT_ENV = 'LIVE_TOKENS_TEST_PORT';\n\nconst PREFERRED_PORT = 4173;\n\n/** Bind the preferred port, fall back to whatever the OS hands out, print the\n * answer. A Playwright config is evaluated synchronously, so the check runs in\n * a child rather than on a listener callback. */\nconst PROBE = `\nconst net = require('net');\nconst listen = (port) => new Promise((resolve, reject) => {\n const server = net.createServer();\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => {\n const { port: bound } = server.address();\n server.close(() => resolve(bound));\n });\n});\n// process.stdout.write, not console.log: the parent's FORCE_COLOR makes\n// console.log wrap a number in ANSI escapes.\nlisten(Number(process.argv[1])).catch(() => listen(0)).then((port) => process.stdout.write(String(port)));\n`;\n\n/**\n * The port the dev server and `baseURL` share. Cached in the environment so\n * every Playwright worker, which loads the config again in its own process,\n * reads the port the main process settled on.\n */\nexport function resolvePort(preferred?: number): number {\n const cached = process.env[PORT_ENV];\n if (cached) return Number(cached);\n\n const port = preferred\n ?? Number(execFileSync(process.execPath, ['-e', PROBE, String(PREFERRED_PORT)], {\n encoding: 'utf-8',\n }).trim());\n if (!Number.isInteger(port) || port <= 0) {\n throw new Error(`Could not settle on a dev-server port (read ${port})`);\n }\n process.env[PORT_ENV] = String(port);\n return port;\n}\n"],"mappings":"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","names":["path","dataDir","path"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/testing/registry.contract.ts
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { checkRegistryEntry } from "../editor/component-editor/contract";
|
|
6
|
+
import { getComponentRegistryEntries } from "../editor/component-editor/registry";
|
|
7
|
+
import { discoverComponents } from "../../bin/check-component.mjs";
|
|
8
|
+
var REGISTRY_SETUP_ENV = "LIVE_TOKENS_REGISTRY_SETUP";
|
|
9
|
+
var setupModule = process.env[REGISTRY_SETUP_ENV];
|
|
10
|
+
if (setupModule) await import(pathToFileURL(path.resolve(setupModule)).href);
|
|
11
|
+
var packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
12
|
+
var consumerRoot = process.cwd();
|
|
13
|
+
var componentConfigsDir = path.resolve(
|
|
14
|
+
process.env.LIVE_TOKENS_DATA_DIR ?? "src/live-tokens/data",
|
|
15
|
+
"component-configs"
|
|
16
|
+
);
|
|
17
|
+
var entries = getComponentRegistryEntries();
|
|
18
|
+
var requested = process.env.LIVE_TOKENS_COMPONENT;
|
|
19
|
+
var targets = requested ? entries.filter((entry) => entry.id === requested) : entries;
|
|
20
|
+
var authored = discoverComponents(consumerRoot);
|
|
21
|
+
describe("component registry contract", () => {
|
|
22
|
+
it("selects at least one component", () => {
|
|
23
|
+
expect(
|
|
24
|
+
targets.map((entry) => entry.id),
|
|
25
|
+
requested ? `no component is registered under the id "${requested}"` : "no component is registered"
|
|
26
|
+
).not.toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
it("registers every component authored in the project", () => {
|
|
29
|
+
const ids = new Set(entries.map((entry) => entry.id));
|
|
30
|
+
expect(authored.filter((id) => !ids.has(id))).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
describe.each(targets.map((entry) => [entry.id, entry]))("%s", (_id, entry) => {
|
|
33
|
+
it("meets the registry contract", () => {
|
|
34
|
+
const projectRoot = entry.origin === "system" ? packageRoot : consumerRoot;
|
|
35
|
+
expect(checkRegistryEntry(entry, { projectRoot, componentConfigsDir })).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=registry.contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../testing/registry.contract.ts"],"sourcesContent":["// @vitest-environment happy-dom\n/**\n * The registry contract, run against the project's own registrations.\n *\n * The setup module is what puts a consumer's components in the registry.\n * Importing an editor does not register it, and importing the app's entry point\n * would mount the app, so the project names a module that registers and stops\n * there. Without one, the run covers the components the package registers\n * itself.\n */\nimport path from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport { describe, expect, it } from 'vitest';\nimport { checkRegistryEntry } from '../editor/component-editor/contract';\nimport { getComponentRegistryEntries } from '../editor/component-editor/registry';\n// @ts-expect-error — plain .mjs module, no types\nimport { discoverComponents } from '../../bin/check-component.mjs';\n\nconst REGISTRY_SETUP_ENV = 'LIVE_TOKENS_REGISTRY_SETUP';\n\nconst setupModule = process.env[REGISTRY_SETUP_ENV];\nif (setupModule) await import(pathToFileURL(path.resolve(setupModule)).href);\n\n/** `<package>/src/testing/` is this file's directory in the repository and in\n * an installed copy alike, so the package root is two levels up. */\nconst packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');\nconst consumerRoot = process.cwd();\nconst componentConfigsDir = path.resolve(\n process.env.LIVE_TOKENS_DATA_DIR ?? 'src/live-tokens/data',\n 'component-configs',\n);\n\nconst entries = getComponentRegistryEntries();\nconst requested = process.env.LIVE_TOKENS_COMPONENT;\nconst targets = requested ? entries.filter((entry) => entry.id === requested) : entries;\n\n/** Ids authored in this project. `discoverComponents` reports the runtime and\n * editor pair on disk, which is a different question from what the registry\n * holds: a component can exist as files and never be registered. */\nconst authored: string[] = discoverComponents(consumerRoot);\n\ndescribe('component registry contract', () => {\n it('selects at least one component', () => {\n expect(\n targets.map((entry) => entry.id),\n requested\n ? `no component is registered under the id \"${requested}\"`\n : 'no component is registered',\n ).not.toEqual([]);\n });\n\n it('registers every component authored in the project', () => {\n const ids = new Set(entries.map((entry) => entry.id));\n expect(authored.filter((id) => !ids.has(id))).toEqual([]);\n });\n\n describe.each(targets.map((entry) => [entry.id, entry] as const))('%s', (_id, entry) => {\n it('meets the registry contract', () => {\n // A shipped component's `sourceFile` is relative to the package; a\n // consumer's is relative to their own project.\n const projectRoot = entry.origin === 'system' ? packageRoot : consumerRoot;\n expect(checkRegistryEntry(entry, { projectRoot, componentConfigsDir })).toEqual([]);\n });\n });\n});\n"],"mappings":";AAUA,OAAO,UAAU;AACjB,SAAS,eAAe,qBAAqB;AAC7C,SAAS,UAAU,QAAQ,UAAU;AACrC,SAAS,0BAA0B;AACnC,SAAS,mCAAmC;AAE5C,SAAS,0BAA0B;AAEnC,IAAM,qBAAqB;AAE3B,IAAM,cAAc,QAAQ,IAAI,kBAAkB;AAClD,IAAI,YAAa,OAAM,OAAO,cAAc,KAAK,QAAQ,WAAW,CAAC,EAAE;AAIvE,IAAM,cAAc,KAAK,QAAQ,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,MAAM,IAAI;AACzF,IAAM,eAAe,QAAQ,IAAI;AACjC,IAAM,sBAAsB,KAAK;AAAA,EAC/B,QAAQ,IAAI,wBAAwB;AAAA,EACpC;AACF;AAEA,IAAM,UAAU,4BAA4B;AAC5C,IAAM,YAAY,QAAQ,IAAI;AAC9B,IAAM,UAAU,YAAY,QAAQ,OAAO,CAAC,UAAU,MAAM,OAAO,SAAS,IAAI;AAKhF,IAAM,WAAqB,mBAAmB,YAAY;AAE1D,SAAS,+BAA+B,MAAM;AAC5C,KAAG,kCAAkC,MAAM;AACzC;AAAA,MACE,QAAQ,IAAI,CAAC,UAAU,MAAM,EAAE;AAAA,MAC/B,YACI,4CAA4C,SAAS,MACrD;AAAA,IACN,EAAE,IAAI,QAAQ,CAAC,CAAC;AAAA,EAClB,CAAC;AAED,KAAG,qDAAqD,MAAM;AAC5D,UAAM,MAAM,IAAI,IAAI,QAAQ,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC;AACpD,WAAO,SAAS,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,EAC1D,CAAC;AAED,WAAS,KAAK,QAAQ,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,KAAK,CAAU,CAAC,EAAE,MAAM,CAAC,KAAK,UAAU;AACtF,OAAG,+BAA+B,MAAM;AAGtC,YAAM,cAAc,MAAM,WAAW,WAAW,cAAc;AAC9D,aAAO,mBAAmB,OAAO,EAAE,aAAa,oBAAoB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,IACpF,CAAC;AAAA,EACH,CAAC;AACH,CAAC;","names":[]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { UserConfig } from 'vite';
|
|
2
|
+
import { TestUserConfig } from 'vitest/config';
|
|
3
|
+
|
|
4
|
+
/** The file a project puts its shared test settings in, at the project root. */
|
|
5
|
+
declare const TESTING_CONFIG_FILE = "live-tokens.testing.ts";
|
|
6
|
+
/** The components route the run drives, published from the settings so the
|
|
7
|
+
* suites need no argument. */
|
|
8
|
+
declare const COMPONENTS_PATH_ENV = "LIVE_TOKENS_COMPONENTS_PATH";
|
|
9
|
+
/** Narrows a run to one component. */
|
|
10
|
+
declare const COMPONENT_ENV = "LIVE_TOKENS_COMPONENT";
|
|
11
|
+
interface LiveTokensTestingConfig {
|
|
12
|
+
/** Vite config a generated Vitest config merges. Default `vite.config.ts`. */
|
|
13
|
+
viteConfig?: string;
|
|
14
|
+
/** Command that starts the dev server. `{host}` and `{port}` are substituted.
|
|
15
|
+
* Default `npm run dev -- --host {host} --port {port}`. */
|
|
16
|
+
devCommand?: string;
|
|
17
|
+
/** Fixed port for the dev server. Omitted, the run takes a free one. */
|
|
18
|
+
port?: number;
|
|
19
|
+
/** The components route, when `editorRoutes` moved it off the owned default. */
|
|
20
|
+
componentsPath?: string;
|
|
21
|
+
/** The plugin's data directory. Default: `dataDir` from
|
|
22
|
+
* `live-tokens.config.json`, else `src/live-tokens/data`. The run never uses
|
|
23
|
+
* it directly; it is the tree the isolated copy is made from. */
|
|
24
|
+
dataDir?: string;
|
|
25
|
+
/** Module that registers the project's components without mounting the app.
|
|
26
|
+
* The registry contract imports it before it selects entries. */
|
|
27
|
+
registrySetup?: string;
|
|
28
|
+
/** Module exporting the `ComponentContract[]` a custom component adds to the
|
|
29
|
+
* shipped list (default export or a named `contracts` export). The suite
|
|
30
|
+
* files are static Playwright entry points, so an env var naming this path
|
|
31
|
+
* plus a dynamic import inside `selectedContracts()` is the only way a
|
|
32
|
+
* custom contract reaches them. */
|
|
33
|
+
contractsModule?: string;
|
|
34
|
+
}
|
|
35
|
+
interface ResolvedTestingConfig {
|
|
36
|
+
root: string;
|
|
37
|
+
viteConfig: string;
|
|
38
|
+
devCommand: string;
|
|
39
|
+
port?: number;
|
|
40
|
+
componentsPath: string;
|
|
41
|
+
dataDir: string;
|
|
42
|
+
registrySetup?: string;
|
|
43
|
+
contractsModule?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Types the settings file without importing the interface by hand. */
|
|
46
|
+
declare function defineTestingConfig(config: LiveTokensTestingConfig): LiveTokensTestingConfig;
|
|
47
|
+
declare function resolveTestingConfig(config?: LiveTokensTestingConfig, root?: string): ResolvedTestingConfig;
|
|
48
|
+
declare function devServerCommand(command: string, host: string, port: number): string;
|
|
49
|
+
|
|
50
|
+
interface VitestConfigOptions {
|
|
51
|
+
/** Files the run collects. Default: the shipped registry contract, matched
|
|
52
|
+
* both in this package and in an installed copy of it. */
|
|
53
|
+
include?: string[];
|
|
54
|
+
exclude?: string[];
|
|
55
|
+
setupFiles?: string[];
|
|
56
|
+
/** Default `happy-dom`, which the registry contract needs to mount a store. */
|
|
57
|
+
environment?: TestUserConfig['environment'];
|
|
58
|
+
/** Module the registry contract imports to register the project's
|
|
59
|
+
* components. Reaches the run as an environment variable because the
|
|
60
|
+
* contract file resolves the path itself. */
|
|
61
|
+
registrySetup?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The project's Vite config plus what the registry contract needs on top.
|
|
65
|
+
*
|
|
66
|
+
* The package ships Svelte and TypeScript source and imports a FontAwesome
|
|
67
|
+
* stylesheet. Left external, Node meets that `.css` and stops with
|
|
68
|
+
* `Unknown file extension ".css"` before a single test runs, so both are
|
|
69
|
+
* inlined for Vite to transform.
|
|
70
|
+
*/
|
|
71
|
+
declare function createVitestConfig(viteConfig: UserConfig, options?: VitestConfigOptions): UserConfig;
|
|
72
|
+
|
|
73
|
+
export { COMPONENTS_PATH_ENV as C, type LiveTokensTestingConfig as L, type ResolvedTestingConfig as R, TESTING_CONFIG_FILE as T, type VitestConfigOptions as V, COMPONENT_ENV as a, devServerCommand as b, createVitestConfig as c, defineTestingConfig as d, resolveTestingConfig as r };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createVitestConfig
|
|
3
|
+
} from "./chunk-FAFOAWYL.js";
|
|
4
|
+
import {
|
|
5
|
+
defineTestingConfig,
|
|
6
|
+
resolveTestingConfig
|
|
7
|
+
} from "./chunk-ZMSX6CXR.js";
|
|
8
|
+
export {
|
|
9
|
+
createVitestConfig,
|
|
10
|
+
defineTestingConfig,
|
|
11
|
+
resolveTestingConfig
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=vitest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/template/README.md
CHANGED
|
@@ -40,3 +40,16 @@ your own editable component, install the Claude Code skills:
|
|
|
40
40
|
```bash
|
|
41
41
|
npx @motion-proto/live-tokens setup-claude
|
|
42
42
|
```
|
|
43
|
+
|
|
44
|
+
## Testing a component
|
|
45
|
+
|
|
46
|
+
`npm run test:design` runs `check-component --tests`: the static checks plus
|
|
47
|
+
the registry contract and the component contract suites, in a browser, against
|
|
48
|
+
this project. It needs three devDependencies the template does not install:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install -D @playwright/test vitest happy-dom
|
|
52
|
+
npx playwright install chromium
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A failing run leaves its report under `test-results/`, gitignored.
|
package/template/_gitignore
CHANGED
|
@@ -4,6 +4,12 @@ dist-ssr
|
|
|
4
4
|
*.local
|
|
5
5
|
*.log
|
|
6
6
|
.npmrc
|
|
7
|
+
playwright-report/
|
|
8
|
+
test-results/
|
|
9
|
+
|
|
10
|
+
# The live-tokens skills are a copy of what the package ships; postinstall
|
|
11
|
+
# refreshes them, so they are the package's to version, not this project's.
|
|
12
|
+
.claude/skills/live-tokens-*/
|
|
7
13
|
|
|
8
14
|
.DS_Store
|
|
9
15
|
.vscode/*
|
package/template/package.json
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"build": "npm run check:design && vite build",
|
|
9
9
|
"preview": "vite preview",
|
|
10
10
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
11
|
-
"check:design": "live-tokens check-page && live-tokens check-component"
|
|
11
|
+
"check:design": "live-tokens check-page && live-tokens check-component",
|
|
12
|
+
"test:design": "live-tokens check-component --tests",
|
|
13
|
+
"postinstall": "live-tokens setup-claude --force || exit 0"
|
|
12
14
|
},
|
|
13
15
|
"dependencies": {
|
|
14
16
|
"@motion-proto/live-tokens": "__LT_VERSION__"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// site.css carries themed page typography (bare h1/p/a rules that consume
|
|
3
3
|
// theme tokens). Imported per-page, not globally, so the editor routes stay
|
|
4
|
-
// theme-immune. It's yours to edit
|
|
4
|
+
// theme-immune. It's yours to edit; see src/styles/site.css.
|
|
5
5
|
import '../styles/site.css';
|
|
6
6
|
import Card from '@motion-proto/live-tokens/components/Card.svelte';
|
|
7
7
|
import Button from '@motion-proto/live-tokens/components/Button.svelte';
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
</p>
|
|
22
22
|
{#if isDev}
|
|
23
23
|
<div class="actions">
|
|
24
|
-
<Button
|
|
25
|
-
<Button variant="secondary"
|
|
24
|
+
<Button onclick={() => navigate('/live-tokens/editor')}>Open Token Editor</Button>
|
|
25
|
+
<Button variant="secondary" onclick={() => navigate('/live-tokens/components')}>Components</Button>
|
|
26
26
|
</div>
|
|
27
27
|
{/if}
|
|
28
28
|
</Card>
|
|
@@ -45,29 +45,15 @@
|
|
|
45
45
|
grid-column: 4 / span 6;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
h1 {
|
|
49
|
-
font-family: var(--font-display);
|
|
50
|
-
font-size: var(--font-size-4xl);
|
|
51
|
-
color: var(--text-primary);
|
|
52
|
-
margin: 0 0 var(--space-12);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
p {
|
|
56
|
-
color: var(--text-secondary);
|
|
57
|
-
line-height: 1.6;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
48
|
code {
|
|
61
49
|
background: var(--surface-neutral-high);
|
|
62
50
|
padding: var(--space-2) var(--space-6);
|
|
63
51
|
border-radius: var(--radius-sm);
|
|
64
|
-
font-family: var(--font-mono, monospace);
|
|
65
|
-
font-size: 0.9em;
|
|
66
52
|
}
|
|
67
53
|
|
|
68
54
|
.actions {
|
|
69
55
|
display: flex;
|
|
70
|
-
gap: var(--space-
|
|
56
|
+
gap: var(--space-8);
|
|
71
57
|
flex-wrap: wrap;
|
|
72
58
|
margin-top: var(--space-20);
|
|
73
59
|
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: live-tokens-build-page
|
|
3
|
-
description: Apply the @motion-proto/live-tokens project conventions when building a page: shipped components, theme tokens over hex/pixel literals, dynamic route mounting, per-page site.css. Use when the user asks to build, create, lay out, or rearrange a page, route, hero, landing page, dashboard, settings screen, pricing page, or a tool screen with a stage and controls; add a route; place an existing component on a page; assemble a screen from the catalogue; or says the layout, label sizes, or control sizes of a page are off. For component choice see live-tokens-pick-component; for a brand-new component, live-tokens-create-component; for look and feel mid-build, live-tokens-create-theme or live-tokens-set-geometry.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Building pages in a live-tokens project
|
|
7
|
-
|
|
8
|
-
Two rules above all else:
|
|
9
|
-
|
|
10
|
-
1. **Use a shipped component if one fits.** Import from `@motion-proto/live-tokens/components/<Name>.svelte`. See **live-tokens-pick-component** for the catalogue and the confusing-pair decisions. Pass only the props it declares, with variant and size values from its union: `npx live-tokens components <id>` prints them (`--json` for data), and the list includes the project's own components beside the shipped ones. A prop a component does not declare is dropped silently at runtime, and the checker reports it. Author custom markup only when nothing fits, and then consider **live-tokens-create-component** so the new piece is editable too.
|
|
11
|
-
2. **Use theme tokens for every value.** Every color, spacing, radius, font-size, and font-family in page CSS is a `var(--token-*)`, whether it sits in the `<style>` block, an inline `style=` attribute, or a `style:` directive. No colour literals in any notation, `white` and `rgb()` included. No px or rem in spacing, stroke, radius, or shadow: that is the geometry the theme owns and `set-geometry` moves. Sizing is layout, not theme: a hero's height, a max content width, or a column's minimum width stays a literal. A change in `/live-tokens/editor` should repaint your page.
|
|
12
|
-
|
|
13
|
-
For text, reach for a whole text style rather than assembling one: `--heading-xl` through `--heading-sm`, `--body-md`, `--body-sm`, `--editorial-xl` through `--editorial-sm`, `--eyebrow`, and `--code` each carry a `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. A heading set from `--heading-lg-*` retypes when the theme's fonts change; one set from a raw `font-size` does not.
|
|
14
|
-
|
|
15
|
-
Text inside a `Card` or a `CollapsibleSection` is typed by that container, not by the page: the slot pins the axes the container owns onto nested `p`, `ul`, `ol`, and `li`, so a consumer's global element rules cannot break a card's body. Pass `prose={false}` when the page should own the type instead, which is also what full-bleed media wants.
|
|
16
|
-
|
|
17
|
-
## Layout
|
|
18
|
-
|
|
19
|
-
**The purpose of a layout.** The page shows one thing. All other content must stay out of its way. Each mark that is not content costs attention: a rule, a border, a header bar, a shadow. Each mark must do a job that no other mark does.
|
|
20
|
-
|
|
21
|
-
Separate elements with the smallest difference that separates them. Use space first. If space is not sufficient, add a hairline rule. If a rule is not sufficient, use a second surface. Do not stack these separators. Two heavy edges side by side make a third shape between them. A band of boxes with borders and header bars looks like a set of posters.
|
|
22
|
-
|
|
23
|
-
Put each element in one of three layers, and type it from that layer. Content is `--text-primary`. Labels are `--eyebrow-*` or `--text-secondary`. Scaffolding is `--border-neutral`.
|
|
24
|
-
|
|
25
|
-
Show related items side by side when the width permits. Do not put them behind a toggle.
|
|
26
|
-
|
|
27
|
-
On a tool page, the stage is the content. Each control is administration. Give the space to the stage. Give the controls the smallest size that still works.
|
|
28
|
-
|
|
29
|
-
`references/layout-sources.md` names the sources for these laws.
|
|
30
|
-
|
|
31
|
-
Decide the bands before the columns. Read the page top to bottom and name each band by its job: what the user looks at, what they type into, what they press. A content page runs hero, sections, footer. A tool page runs the stage on top (the canvas, player, or strip the work is about), the inputs under it, and one toolbar of actions along the bottom edge. Each band is a row of the page grid; a band that needs columns of its own spans the grid and redeclares it, as below.
|
|
32
|
-
|
|
33
|
-
Separate bands with space and a rule, `padding-top: var(--space-16)` and `border-top: var(--border-width-1) solid var(--border-neutral)`, and stretch a band's boxes to one height (`align-items: stretch`) so their bottom edges make one line. Card chrome does not separate bands.
|
|
34
|
-
|
|
35
|
-
Pages sit inside the column grid via `--columns-count`, `--columns-gutter`, `--columns-max-width`. The columns button in the overlay's header (the vertical-lines icon) draws the grid over the page while you place content.
|
|
36
|
-
|
|
37
|
-
To place children at specific page-column positions, span the parent grid (`grid-column: 1 / -1`), redeclare `repeat(var(--columns-count), 1fr)` with `--columns-gutter`, then refer to children by real page-column numbers. Never fabricate a local `repeat(N, 1fr)` with a hardcoded count: the widths drift from the page grid and the numbers stop matching `ColumnsOverlay`.
|
|
38
|
-
|
|
39
|
-
### Containers by job
|
|
40
|
-
|
|
41
|
-
- `Panel` is a stage: a canvas, a player, a preview. It pins its height so the page holds still while what it shows changes.
|
|
42
|
-
- `Card` is a titled block of content. Its header is typed by the card's own tokens, `--card-default-title-*` at `--font-size-2xl` with a body at `--font-size-xl` by default; `size="compact"` drops the title to md, the body to sm, and tightens the padding. That is a content card's voice, and the theme editor retunes it for the whole project.
|
|
43
|
-
- A box in a tool UI labels itself. Use `variant="bare" size="compact"` and put your own label in the body, typed from a text style: the `.eyebrow` class from `site.css` for a quiet section label, `.heading-sm` for one that leads. Leave the shipped header alone rather than shrinking it with a page rule.
|
|
44
|
-
- A toolbar is a flex row of small buttons on the band's bottom edge, grouped left and right with `justify-content: space-between`. No card around it.
|
|
45
|
-
|
|
46
|
-
### Density
|
|
47
|
-
|
|
48
|
-
- `Button` and `IconButton` take `size="small"` in toolbars, compose rows, and any band that holds more than a couple of actions; the default size is for the page's primary action. `fullWidth` belongs to a stacked rail and comes off in a row.
|
|
49
|
-
- A project component that wraps shipped buttons forwards a `size` prop to them, so a page sets density the same way for shipped and custom pieces.
|
|
50
|
-
- Text in your own elements inside a `Card` inherits the card's body size unless you type it. A label, count, or status line inside a card sets a text style of its own (`--body-sm-*`, `--code-*`).
|
|
51
|
-
- `MenuSelect` renders its list open. For a picker, toggle it from a small `Button` with a trailing chevron (`icon="fa-solid fa-chevron-down" iconPosition="right"`) and position the list absolutely under the button, `top: 100%` with a `--space-*` margin.
|
|
52
|
-
|
|
53
|
-
## Wiring
|
|
54
|
-
|
|
55
|
-
- Add the route the way `App.svelte` already wires routes:
|
|
56
|
-
- **`<LiveTokensRouter pages={...}>`** (the usual case): add a `pages` entry as `lazy: () => import('./YourPage.svelte')` with a `source: 'src/...'` (and a `label`/`icon` to show it in the nav rail). For a route you can't enumerate (a `/:id`, a path prefix, a gated page), add a `resolve(path) => RouteEntry | null` instead of a `pages` key; same entry shape, so `props` and `source` (hence "Page Source") work identically.
|
|
57
|
-
- **Manual `<LiveEditorOverlay>`**: dispatch with `$derived.by(() => import(...))` and register the route's source in `pageSources={...}`.
|
|
58
|
-
Either way use `lazy`, not a static top-level import: static imports evaluate every page module at boot and leak page CSS into the editor routes.
|
|
59
|
-
- Import `site.css` from each page's `<script>` block, never from `main.ts` (would leak into editor routes).
|
|
60
|
-
|
|
61
|
-
The entry shape, for a project whose `App.svelte` has moved on from the template:
|
|
62
|
-
|
|
63
|
-
```svelte
|
|
64
|
-
const pages = {
|
|
65
|
-
'/pricing': {
|
|
66
|
-
lazy: () => import('./pages/Pricing.svelte'),
|
|
67
|
-
source: 'src/pages/Pricing.svelte',
|
|
68
|
-
label: 'Pricing',
|
|
69
|
-
icon: 'fa-tag',
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
`source` is what makes Page Source work; drop `label` to keep a route reachable by URL but off the nav rail.
|
|
75
|
-
|
|
76
|
-
## Avoid
|
|
77
|
-
|
|
78
|
-
- Colour literals, and px or rem in spacing, stroke, radius, or shadow.
|
|
79
|
-
- Hardcoded page-grid counts (`repeat(10, 1fr)`). Use `repeat(var(--columns-count), 1fr)`, or `calc(var(--columns-count) - 2)` for a sub-grid that spans fewer page columns. A local two-up or three-up is a layout and is fine.
|
|
80
|
-
- Utility classes overriding shipped components. Extend via the `/live-tokens/components` editor instead.
|
|
81
|
-
- A card header as a section label in a tool UI, and a page rule that shrinks it. Label the box yourself with a text style.
|
|
82
|
-
- Deep imports from `node_modules/@motion-proto/live-tokens/src/...`. Use public entry points only.
|
|
83
|
-
- Mounting `Editor` or `ComponentEditorPage` outside their dedicated routes.
|
|
84
|
-
- A page route under `/live-tokens/*`. That namespace is reserved for the package's own dev surfaces so they can never shadow your routes; the rest of the URL space is yours.
|
|
85
|
-
|
|
86
|
-
## Verify
|
|
87
|
-
|
|
88
|
-
Run the checker and fix what it reports. Repeat until it exits 0:
|
|
89
|
-
|
|
90
|
-
```sh
|
|
91
|
-
npx live-tokens check-page src/pages/YourPage.svelte
|
|
92
|
-
# or: npx @motion-proto/live-tokens check-page (every page under src/)
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
It fails on a component outside the catalogue, a prop or value the component does not declare, a deep import, a `var()` that resolves to nothing, a colour literal in any notation, a route under `/live-tokens/*`, and `site.css` imported from `main.ts`. It warns on a px or rem literal in the geometry the theme owns, a hardcoded page-column count, a raw type axis, and a route entry with no `source`. Inline `style=` attributes and `style:` directives are read the same way as the `<style>` block; a `var()` fallback is never a finding. The recipe for each rule is in **live-tokens-fix-findings**.
|
|
96
|
-
|
|
97
|
-
Warnings do not fail the run. `--strict` makes them fail, which is the setting to use when the page is meant to be fully tokenized. `--json` prints findings with a stable `rule` id, so you can work through one rule at a time and re-run. `--off=<rule>` silences a rule for a run; `"checks": { "rules": { ... } }` in `live-tokens.config.json` sets it for the project. A project scaffolded by `create` runs the checker, with `check-component`, as `npm run check:design` before every `vite build`, so the page has to pass before it can ship.
|
|
98
|
-
|
|
99
|
-
The checker cannot see a layout. Open the page at the width it is built for and read it band by band: the boxes in a band end on one line, no label is larger than the page's body copy, every control stays inside its box (a `width: 100%` field without `box-sizing: border-box` pushes past it by its padding), and the actions sit where the eye goes last. Fix what you see before you move on.
|
|
100
|
-
|
|
101
|
-
Then look at the page from a distance. The bands and their edges must be the only shapes that you see. Then look closely. For each border, header bar, and box, ask this question: does the page lose information if this element is removed? If the answer is no, remove the element. Find the element that a reader sees first, second, and third. Make sure that this is the reading order the page needs.
|
|
102
|
-
|
|
103
|
-
Then in dev: change a colour in `/live-tokens/editor` and confirm your page repaints (proves token usage). The overlay's "Page Source" button on the new route opens the page in VS Code (proves the route's `source`). The columns overlay shows content sitting inside `--columns-max-width`.
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# Layout sources
|
|
2
|
-
|
|
3
|
-
Read this when a layout decision in SKILL.md needs its reason. Each law in the
|
|
4
|
-
Layout section comes from one of these sources. The sources are for layout and
|
|
5
|
-
hierarchy only. Do not take color or type opinions from them: the theme owns
|
|
6
|
-
those.
|
|
7
|
-
|
|
8
|
-
## Edward Tufte
|
|
9
|
-
|
|
10
|
-
Tufte wrote about information graphics. His laws apply to a page because a page
|
|
11
|
-
is an information display with controls on it.
|
|
12
|
-
|
|
13
|
-
| Law | Statement | Rule in SKILL.md |
|
|
14
|
-
|---|---|---|
|
|
15
|
-
| Smallest effective difference | Make all visual distinctions as subtle as possible, but still clear and effective. | Separate with space first, then a hairline rule, then a second surface. |
|
|
16
|
-
| 1+1=3 | Two heavy marks side by side make a third mark: the space between them. | A band of boxes with borders and header bars looks like a set of posters. |
|
|
17
|
-
| Layering and separation | Put data on top, labels next, and scaffolding faintest. | Content, labels, and scaffolding each take their own token. |
|
|
18
|
-
| Administrative debris | The metaphor for the interface is the information. Remove the chrome that the tool adds for itself. | The stage takes the space. Controls take the smallest size that still works. |
|
|
19
|
-
| Spatial over temporal | Show information adjacent in space, not stacked in time behind controls. | Show related items side by side. Frames in a strip are small multiples. |
|
|
20
|
-
| Erase non-data ink | Remove each mark that carries no information. | The Verify question: does the page lose information if this element is removed? |
|
|
21
|
-
| Micro and macro readings | A good display reads at a distance and up close. | Verify from a distance, then closely. |
|
|
22
|
-
|
|
23
|
-
Sources:
|
|
24
|
-
|
|
25
|
-
- Envisioning Information (1990): layering and separation, small multiples, micro and macro. https://www.edwardtufte.com/book/envisioning-information/
|
|
26
|
-
- The Visual Display of Quantitative Information (1983): data-ink, chartjunk.
|
|
27
|
-
- iPhone interface design, edwardtufte.com notebook: administrative debris, spatial over temporal. https://www.edwardtufte.com/notebook/iphone-interface-design/
|
|
28
|
-
|
|
29
|
-
## Josef Müller-Brockmann
|
|
30
|
-
|
|
31
|
-
Grid Systems in Graphic Design (1981) is the discipline behind the page column
|
|
32
|
-
grid. The grid does the separating, so an element needs no border to show
|
|
33
|
-
where it sits. His stated aim is compact planning, intelligibility, and
|
|
34
|
-
clarity. That is Tufte's aim in a typographer's words.
|
|
35
|
-
|
|
36
|
-
## Refactoring UI
|
|
37
|
-
|
|
38
|
-
Adam Wathan and Steve Schoger, Refactoring UI (2018), turns both into working
|
|
39
|
-
rules for product screens:
|
|
40
|
-
|
|
41
|
-
- Put more space around a group than within it.
|
|
42
|
-
- Start with too much white space, then remove some.
|
|
43
|
-
- Use fewer borders. Separate with space, a shadow, or a second background.
|
|
44
|
-
- Emphasize by de-emphasizing the secondary content.
|
|
45
|
-
- Labels are a last resort.
|
|
46
|
-
- Keep a spacing scale where no two steps are closer than a quarter. The `--space-*` scale is that scale.
|
|
47
|
-
|
|
48
|
-
https://www.refactoringui.com/
|