diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index cc4bd59..61bd535 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -43,13 +43,36 @@ jobs: run: pnpm install --frozen-lockfile - name: Build packages - run: pnpm build + run: pnpm exec turbo build --filter='!@datav-kit/docs' - name: Build docs run: pnpm docs:build env: VITEPRESS_BASE: /${{ github.event.repository.name }}/ + - name: Check documentation and example sources + run: pnpm docs:check + env: + VITEPRESS_BASE: /${{ github.event.repository.name }}/ + + - name: Install browser + run: pnpm exec playwright install --with-deps chromium + + - name: Verify example previews + run: pnpm examples:test + env: + VITEPRESS_BASE: /${{ github.event.repository.name }}/ + + - name: Upload browser verification + uses: actions/upload-artifact@v4 + if: always() + with: + name: example-browser-verification + path: | + .cache/playwright-report + .cache/playwright-results + .cache/example-screenshots + - name: Upload Pages artifact uses: actions/upload-pages-artifact@v5 with: diff --git a/.github/workflows/skill-examples.yml b/.github/workflows/skill-examples.yml new file mode 100644 index 0000000..60ec46d --- /dev/null +++ b/.github/workflows/skill-examples.yml @@ -0,0 +1,37 @@ +name: Skill examples + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + env: + VITEPRESS_BASE: /datav-kit/ + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v6 + with: + version: 11.6.0 + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm exec playwright install --with-deps chromium + - run: pnpm exec turbo build --filter='!@datav-kit/docs' + - run: pnpm docs:build + - run: pnpm docs:check + - run: pnpm examples:test + - uses: actions/upload-artifact@v4 + if: always() + with: + name: example-browser-verification + path: | + .cache/playwright-report + .cache/playwright-results + .cache/example-screenshots diff --git a/.gitignore b/.gitignore index d790ca9..873b412 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules temp cache .eslintcache +/docs/public/examples/ # AI .agents diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index b618d01..d5348fa 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,14 +1,18 @@ import process from 'node:process' import { defineConfig } from 'vitepress' import llmstxt, { copyOrDownloadAsMarkdownButtons } from 'vitepress-plugin-llms' +import { copySkillExamples, skillExamples } from './skill-examples' + +const base = process.env.VITEPRESS_BASE || '/' export default defineConfig({ title: 'DataV Kit', description: 'Framework-agnostic Web Components for data dashboard decoration.', - base: process.env.VITEPRESS_BASE || '/', + base, + buildEnd: copySkillExamples, cleanUrls: true, vite: { - plugins: [llmstxt()], + plugins: [llmstxt(), skillExamples(base)], server: { host: '0.0.0.0', }, @@ -16,6 +20,7 @@ export default defineConfig({ themeConfig: { nav: [ { text: 'Guide', link: '/guide/introduction' }, + { text: 'Examples', link: '/guide/dashboard-examples' }, { text: 'Components', link: '/components/decorations/decoration-1' }, { text: 'Reference', link: '/reference/architecture-contracts' }, ], @@ -31,6 +36,7 @@ export default defineConfig({ { text: 'Installation', link: '/guide/installation' }, { text: 'Framework Integration', link: '/guide/framework-integration' }, { text: 'Theming', link: '/guide/theming' }, + { text: 'Dashboard Examples', link: '/guide/dashboard-examples' }, { text: 'Component Authoring', link: '/guide/component-authoring' }, ], }, @@ -101,15 +107,6 @@ export default defineConfig({ ], }, ], - '/technical-architecture': [ - { - text: 'Reference', - items: [ - { text: 'Architecture Contracts', link: '/reference/architecture-contracts' }, - { text: 'Technical Architecture', link: '/technical-architecture' }, - ], - }, - ], }, search: { provider: 'local', diff --git a/docs/.vitepress/skill-examples.ts b/docs/.vitepress/skill-examples.ts new file mode 100644 index 0000000..740b8aa --- /dev/null +++ b/docs/.vitepress/skill-examples.ts @@ -0,0 +1,62 @@ +import type { Plugin } from 'vite' +import type { SiteConfig } from 'vitepress' +import { copyFile, mkdir, readdir, readFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const assets = fileURLToPath(new URL('../../skills/datav-kit/assets/', import.meta.url)) +const examples = path.join(assets, 'examples') + +function sourceFile(name: string): string | undefined { + if (!/^[a-z-]+\.(?:html|png)$/.test(name)) + return undefined + if (name === 'minimal.html') + return path.join(assets, 'minimal-example.html') + return path.join(examples, name.endsWith('.png') ? 'previews' : '', name) +} + +export function skillExamples(base: string): Plugin { + return { + name: 'datav-skill-examples', + async configResolved() { + // Public files must exist before VitePress resolves image and HTML links. + await copyExamples(fileURLToPath(new URL('../public/examples/', import.meta.url))) + }, + configureServer(server) { + server.middlewares.use(async (request, response, next) => { + const pathname = new URL(request.url || '/', 'http://localhost').pathname + const prefix = `${base}examples/` + if (!pathname.startsWith(prefix)) + return next() + const name = pathname.slice(prefix.length) + const file = sourceFile(name) + if (!file) + return next() + try { + const content = await readFile(file) + response.setHeader('Content-Type', name.endsWith('.png') ? 'image/png' : 'text/html; charset=utf-8') + response.end(content) + } + catch { + response.statusCode = 404 + response.end('Example not found') + } + }) + }, + } +} + +export async function copySkillExamples(site: SiteConfig): Promise { + await copyExamples(path.join(site.outDir, 'examples')) +} + +async function copyExamples(destination: string): Promise { + await mkdir(destination, { recursive: true }) + const html = (await readdir(examples)).filter(name => name.endsWith('.html')) + const previews = (await readdir(path.join(examples, 'previews'))).filter(name => name.endsWith('.png')) + await Promise.all([...html, ...previews, 'minimal.html'].map(async (name) => { + const file = sourceFile(name) + if (file) + await copyFile(file, path.join(destination, name)) + })) +} diff --git a/docs/components/borders/border-box-1.md b/docs/components/borders/border-box-1.md index 234231d..96c2c3d 100644 --- a/docs/components/borders/border-box-1.md +++ b/docs/components/borders/border-box-1.md @@ -20,6 +20,12 @@ description: Animated rectangular SVG border component with moving highlight for ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-10.md b/docs/components/borders/border-box-10.md index 224b17f..fc6e4f1 100644 --- a/docs/components/borders/border-box-10.md +++ b/docs/components/borders/border-box-10.md @@ -20,6 +20,12 @@ description: Rounded outline panel with animated corner glows and responsive con ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-11.md b/docs/components/borders/border-box-11.md index 9066c6a..5ef2916 100644 --- a/docs/components/borders/border-box-11.md +++ b/docs/components/borders/border-box-11.md @@ -20,6 +20,12 @@ description: Enterprise data-platform frame with status rails, live nodes, and r ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-12.md b/docs/components/borders/border-box-12.md index be38574..a68a69e 100644 --- a/docs/components/borders/border-box-12.md +++ b/docs/components/borders/border-box-12.md @@ -20,6 +20,12 @@ description: Minimal electric-blue HUD frame with chamfered corners, title rail, ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-13.md b/docs/components/borders/border-box-13.md index c9b6ccb..cc8ba15 100644 --- a/docs/components/borders/border-box-13.md +++ b/docs/components/borders/border-box-13.md @@ -20,6 +20,12 @@ description: Sparse electric-blue split rail frame with corner modules, carrier ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-14.md b/docs/components/borders/border-box-14.md index 9a69d98..34f6719 100644 --- a/docs/components/borders/border-box-14.md +++ b/docs/components/borders/border-box-14.md @@ -20,6 +20,12 @@ description: Orthogonal signal-port corner frame with circuit traces, pin contac ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-15.md b/docs/components/borders/border-box-15.md index 7154771..fbbc19a 100644 --- a/docs/components/borders/border-box-15.md +++ b/docs/components/borders/border-box-15.md @@ -20,6 +20,12 @@ description: Lightweight responsive panel with corner dots, corner ticks, straig ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-16.md b/docs/components/borders/border-box-16.md index c5e9f89..5bb3b6e 100644 --- a/docs/components/borders/border-box-16.md +++ b/docs/components/borders/border-box-16.md @@ -20,6 +20,12 @@ description: Floating CPU-like thin border with broken outer rails, open inner h ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-2.md b/docs/components/borders/border-box-2.md index 4d6b5f3..7209eb6 100644 --- a/docs/components/borders/border-box-2.md +++ b/docs/components/borders/border-box-2.md @@ -20,6 +20,12 @@ description: Layered neon cyber frame with corners, energy bars, tick marks, and ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-3.md b/docs/components/borders/border-box-3.md index 5a778d2..3cb2a46 100644 --- a/docs/components/borders/border-box-3.md +++ b/docs/components/borders/border-box-3.md @@ -20,6 +20,12 @@ description: Restrained futuristic blue frame with fixed corners, center modules ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-4.md b/docs/components/borders/border-box-4.md index 82125fb..97ea60a 100644 --- a/docs/components/borders/border-box-4.md +++ b/docs/components/borders/border-box-4.md @@ -20,6 +20,12 @@ description: Dense neon HUD frame with ornate corners, detail modules, and sourc ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-5.md b/docs/components/borders/border-box-5.md index 6de367c..50eb0b9 100644 --- a/docs/components/borders/border-box-5.md +++ b/docs/components/borders/border-box-5.md @@ -33,6 +33,12 @@ description: Layered electric-blue HUD frame with five fixed frame layers and re ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-6.md b/docs/components/borders/border-box-6.md index 4f83fa9..48c7b7e 100644 --- a/docs/components/borders/border-box-6.md +++ b/docs/components/borders/border-box-6.md @@ -33,6 +33,12 @@ description: High-precision cyan HUD frame with traced source layers, marker sta ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-7.md b/docs/components/borders/border-box-7.md index 16bfac4..8711a1e 100644 --- a/docs/components/borders/border-box-7.md +++ b/docs/components/borders/border-box-7.md @@ -20,6 +20,12 @@ description: Chamfered glowing panel with fixed mirrored corner ornaments and op ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-8.md b/docs/components/borders/border-box-8.md index 31c9515..e838cfa 100644 --- a/docs/components/borders/border-box-8.md +++ b/docs/components/borders/border-box-8.md @@ -20,6 +20,12 @@ description: Dynamic polygon panel with four fixed mirrored animated corner orna ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/borders/border-box-9.md b/docs/components/borders/border-box-9.md index 60c18fa..fc6ab52 100644 --- a/docs/components/borders/border-box-9.md +++ b/docs/components/borders/border-box-9.md @@ -20,6 +20,12 @@ description: Glowing panel with host border, inset shadow, and rounded corner li ``` +## Content + +Place headings and charts in the default slot. CSS parts are styling hooks, not named +slots. Keep the automatic content inset; see the shared [content-area contract](/reference/architecture-contracts#authoring-slotted-content) +for wrapper padding and explicit overrides. + ## Props | Name | Type | Default | Notes | diff --git a/docs/components/other/count-to.md b/docs/components/other/count-to.md index 6055d05..09daa40 100644 --- a/docs/components/other/count-to.md +++ b/docs/components/other/count-to.md @@ -6,6 +6,10 @@ description: Animated numeric metric display with prefix, suffix, thousands sepa `dvk-count-to` renders an animated numeric metric with optional prefix, suffix, thousands separator, and decimal formatting. +Numeric attributes are converted from strings. Custom `prefix` and `suffix` slot content +takes precedence over the corresponding text properties. Use `disabled` to immediately +show the target value when reduced motion is requested. +
diff --git a/docs/guide/dashboard-examples.md b/docs/guide/dashboard-examples.md new file mode 100644 index 0000000..9dbc015 --- /dev/null +++ b/docs/guide/dashboard-examples.md @@ -0,0 +1,96 @@ +--- +description: Four standalone dashboard examples with downloadable HTML, screenshots, geographic data, interactive Three.js equipment and linked business metrics. +--- + +# Dashboard Examples + + + +Four complete screens demonstrate different compositions with datav-kit Web Components. +Each preview and download uses the same HTML maintained in the datav-kit skill. Open the +downloaded file in a browser while connected to the internet; pinned libraries load from +CDNs, while application code and scene data are embedded in the file. + +## City Operations + +A map-led Shanghai riverfront screen with linked regional metrics, pedestrian counts and +incident queues. Streets and buildings are derived from OpenStreetMap; operational figures +are deterministic demonstration data. + +City operations dashboard + +[Open preview](/examples/city.html) · Download HTML + +## Industrial Monitoring + +An original procedural Three.js plant with assembly buildings, machining, utilities and +logistics. Select equipment in the scene or the equipment list, reset the camera, and pause +the conveyor animation. A non-WebGL browser displays the same assets and metrics in 2D. + +Industrial monitoring dashboard + +[Open preview](/examples/industrial.html) · Download HTML + +## Business Performance + +A light analytical composition with period selection, reconciled revenue totals, target +comparisons, orders and channel contributions. The primary visual is the revenue trend. + +Business performance dashboard + +[Open preview](/examples/business.html) · Download HTML + +## Energy Dispatch + +A source-grid-load-storage composition. Generation and grid input reconcile with industrial, +building and transport demand plus battery charging. Select flow nodes or use the node controls. + +Energy dispatch dashboard + +[Open preview](/examples/energy.html) · Download HTML + +## Adapting an Example + +Read [Installation](/guide/installation), [Theming](/guide/theming) and the detail pages for +the components you intend to use. Check the installed package API before adapting an example; +the examples' pinned CDN versions are not a requirement for your project. + +The [minimal HTML starter](/examples/minimal.html) shows registration, proportional scaling, +a border and a numeric metric. Layout and screen-token defaults are application conventions, +not additional component API. + +For state verification, append `?state=loading`, `?state=empty`, `?state=failed` or +`?state=stale` to a preview. Normal operation defaults to ready. Empty and failed states +provide a retry using the deterministic local dataset. The industrial example also accepts +`?webgl=off` to exercise its 2D fallback. A mobile viewport shows a proportional preview of +the 1920 x 1080 canvas. + +## Map Provenance + +The city example embeds a filtered GeoJSON derivative of +[OpenStreetMap API map data](https://api.openstreetmap.org/api/0.6/map?bbox=121.486,31.228,121.509,31.246) +for the Shanghai riverfront. The embedded GeoJSON is available in the HTML's `geography` +JSON block, with its source URL, ODbL license identifier and attribution. Geometry is +filtered by feature type and polygon winding is adjusted for the renderer. + +Map data © [OpenStreetMap contributors](https://www.openstreetmap.org/copyright), available +under the [Open Database License](https://opendatacommons.org/licenses/odbl/1-0/). +The plant geometry is authored in the industrial example itself. All operational datasets +are illustrative and do not represent a live installation. + +## Maintaining the Examples + +Edit `skills/datav-kit/assets/examples/*.html`. The development server and documentation +build copy those sources automatically; `docs/public/examples/` is generated and ignored +by Git. Keep previews in `assets/examples/previews/` as actual browser screenshots. + +From the repository root, build the packages and docs, then run `pnpm docs:check` and +`pnpm examples:test`. Install Chromium once with `pnpm exec playwright install chromium`. +Set `VITEPRESS_BASE=/datav-kit/` consistently when checking a GitHub Pages build. + +After changing a visual, run `pnpm examples:previews` against the updated docs build, +inspect the screenshots and rebuild the docs to include them. Browser reports and all +four viewport captures are saved in `.cache/`; CI uploads them for review. An existing +Chrome installation can be used locally with `PLAYWRIGHT_CHANNEL=chrome`. diff --git a/docs/guide/installation.md b/docs/guide/installation.md index eff9aa4..dfc004c 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -32,4 +32,36 @@ defineBorderBox3() Registration is guarded for SSR. Importing modules is allowed on the server, but defining custom elements only happens when browser APIs are available. +## Documentation and Package Versions + +Start component discovery with [llms.txt](/llms.txt), then read the returned detail links. +The documentation site describes the source revision used for its build. A page existing on +the site does not guarantee that an older installed package exports that component or API. +Inspect the project's dependency version and verify registration in the browser: + +```ts +import { register } from '@datav-kit/elements' + +register() +const available = customElements.get('dvk-border-box-15') !== undefined +``` + +Import alone does not register the elements. Perform feature detection after `register()` +or the selected `define*()` calls. Verify attributes and methods against that package version +as well; registration only proves that the tag exists. + +When a chart reads inherited theme variables inside `dvk-fit-screen`, wait for the container's +first Lit update before measuring or reading computed styles. Its slotted content may not yet +participate in the rendered tree immediately after registration: + +```js +await document.querySelector('dvk-fit-screen').updateComplete +await new Promise(requestAnimationFrame) +// Now measure the chart host and read its inherited project-theme values. +``` + +If a page is unavailable, use its `docs/` source at a verified GitHub tag or commit matching +the dependency. Material read from `main` may describe unreleased APIs. Offline, use matching +local docs and source. There is no separately maintained component availability list in the skill. + For Vue, React, Vite, and webpack setup examples, continue to [Framework Integration](/guide/framework-integration). diff --git a/docs/guide/theming.md b/docs/guide/theming.md index 9df736b..ebc1303 100644 --- a/docs/guide/theming.md +++ b/docs/guide/theming.md @@ -1,5 +1,5 @@ --- -description: Apply CSS variable themes to datav-kit components with cyber-blue, neon-magenta, and custom color presets. +description: Coordinate dashboard backgrounds, text, status, data-series colors and dvk component accents through scoped dark or light project themes. --- # Theming @@ -40,3 +40,57 @@ Or inherit variables from a theme scope:
+ +## Complete Project Themes + +Library presets primarily supply decorative component colors, surfaces and motion values. +The `ice-white` preset is a bright accent palette, not a complete light application theme. +A screen also needs readable text, page backgrounds, status colors and data-series roles. + +Define those roles once in a project-scoped class, then map them to the existing component +variables. The `--app-*` names below are application conventions, not new datav-kit APIs: + +```css +.dvk-theme-project { + --app-background: #f4f6f8; + --app-surface: #ffffff; + --app-text: #17252d; + --app-muted: #52646e; + --app-rule: #d9e1e5; + --app-selected: #126c94; + --app-positive: #197448; + --app-warning: #9a5300; + --app-danger: #be3545; + --app-series-1: #167ba5; + --app-series-2: #168268; + --app-series-3: #b66019; + --app-series-4: #7a54a4; + + --dvk-color-primary: var(--app-selected); + --dvk-color-secondary: var(--app-series-2); + --dvk-color-accent: var(--app-warning); + --dvk-color-surface: var(--app-surface); + --dvk-glow-soft: 0 0 0 transparent; + --dvk-glow-strong: 0 0 0 transparent; + --dvk-line-width: 1px; + --dvk-motion-duration: 2400ms; + --dvk-count-to-color: var(--app-text); +} +``` + +Apply the class to the screen root. Set its background and text color from these roles; +use the same roles for status labels and charts. For a dark theme, change the project +role values together and check contrast against the resulting surfaces. Warning color +should not double as an unrelated data category without an additional distinguishing cue. + +Keep spacing, type sizes and layout in the screen layer rather than the color theme. +Existing presets also declare `:root` values, so loading several preset stylesheets can +change the surrounding page. A custom project class avoids that global coupling. + +Chart and 3D libraries usually need concrete color values. Read inherited custom +properties with `getComputedStyle(screen)`. Resolve expressions such as `color-mix()` +through a computed `color` property before passing them to a library that does not parse +CSS expressions. Reapply colors when the project theme changes. + +See [Dashboard Examples](/guide/dashboard-examples) for complete dark, light, map and +3D compositions using this approach. diff --git a/docs/reference/architecture-contracts.md b/docs/reference/architecture-contracts.md index cb3f5ec..3078a85 100644 --- a/docs/reference/architecture-contracts.md +++ b/docs/reference/architecture-contracts.md @@ -100,6 +100,23 @@ CSS variable precedence for border-box content inset is: The computed value may be stored in an internal CSS variable such as `--dvk-border-box-auto-padding`, but it is not a public authoring contract. +### Authoring Slotted Content + +Border boxes expose a default content slot. `frame`, `graphic` and `content` are CSS parts, +not named slots; put headings and chart wrappers in the default slot rather than assigning +them to a `header` or `title` slot. + +Keep the computed inset unless the rendered result demonstrates an obstruction or overflow. +For additional breathing room, first adjust the layout or add padding to an inner content +wrapper. An explicit inset override affects every child and must still keep content clear +of the frame. Insets depend on the element and its measured size; do not infer a component +variant from padding values or copy a fixed inset across variants. + +An independently filled surface requires a component whose detail page documents +`background-color`. Transparent frames can instead inherit a project-owned surface. +Likewise, motion controls and automatic-height support belong to individual component APIs; +verify the selected element's detail page instead of assuming all border boxes share props. + ## Fullscreen Fullscreen must be requested from a user gesture. Components may expose methods such as `requestFullscreenMode()`, but they must not automatically call `requestFullscreen()` on mount. diff --git a/package.json b/package.json index a0c17dd..15c4131 100644 --- a/package.json +++ b/package.json @@ -24,18 +24,24 @@ "typecheck": "turbo typecheck", "prepare": "simple-git-hooks", "open:packages": "npx open-packages-on-npm", - "catalog": "pnpx codemod pnpm/catalog" + "catalog": "pnpx codemod pnpm/catalog", + "docs:check": "tsx scripts/check-docs.mjs", + "examples:test": "playwright test", + "examples:previews": "UPDATE_EXAMPLE_PREVIEWS=1 playwright test -g \"target and preview sizes\"" }, "devDependencies": { "@antfu/eslint-config": "catalog:cli", "@antfu/ni": "catalog:cli", "@antfu/utils": "catalog:inlined", + "@playwright/test": "catalog:", "@tsdown/css": "catalog:cli", "@types/node": "catalog:types", "bumpp": "catalog:cli", "eslint": "catalog:cli", "happy-dom": "catalog:testing", "lint-staged": "catalog:cli", + "markdown-it": "catalog:", + "pngjs": "catalog:", "publint": "catalog:cli", "simple-git-hooks": "catalog:cli", "tinyexec": "catalog:testing", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..2d57105 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,24 @@ +import process from 'node:process' +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './tests/examples', + timeout: 90_000, + expect: { timeout: 15_000 }, + workers: 1, + reporter: [['list'], ['html', { outputFolder: '.cache/playwright-report', open: 'never' }]], + outputDir: '.cache/playwright-results', + use: { + browserName: 'chromium', + channel: process.env.PLAYWRIGHT_CHANNEL || undefined, + viewport: { width: 1920, height: 1080 }, + launchOptions: { args: ['--enable-unsafe-swiftshader'] }, + screenshot: 'only-on-failure', + trace: 'retain-on-failure', + }, + webServer: { + command: 'node tests/examples/serve.mjs', + url: 'http://127.0.0.1:4173', + reuseExistingServer: !process.env.CI, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d23a938..43bc4c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,16 @@ catalogs: vitepress-plugin-llms: specifier: ^1.13.2 version: 1.13.2 + default: + '@playwright/test': + specifier: 1.58.2 + version: 1.58.2 + markdown-it: + specifier: 14.1.1 + version: 14.1.1 + pngjs: + specifier: 7.0.0 + version: 7.0.0 docs: echarts: specifier: ^6.1.0 @@ -100,6 +110,9 @@ importers: '@antfu/utils': specifier: catalog:inlined version: 9.3.0 + '@playwright/test': + specifier: 'catalog:' + version: 1.58.2 '@tsdown/css': specifier: catalog:cli version: 0.22.3(jiti@2.6.1)(postcss@8.5.15)(tsdown@0.22.3)(tsx@4.21.0)(yaml@2.8.3) @@ -118,6 +131,12 @@ importers: lint-staged: specifier: catalog:cli version: 16.4.0 + markdown-it: + specifier: 'catalog:' + version: 14.1.1 + pngjs: + specifier: 'catalog:' + version: 7.0.0 publint: specifier: catalog:cli version: 0.3.18 @@ -613,12 +632,6 @@ packages: '@lit/reactive-element@2.1.2': resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - '@napi-rs/wasm-runtime@1.1.6': resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: @@ -635,6 +648,11 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@publint/pack@0.1.4': resolution: {integrity: sha512-HDVTWq3H0uTXiU0eeSQntcVUTPP3GamzeXI41+x7uU9J65JgWQh3qWZHblR1i0npXfFtF+mxBiU2nJH8znxWnQ==} engines: {node: '>=18'} @@ -1065,9 +1083,6 @@ packages: cpu: [arm64] os: [win32] - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -1861,6 +1876,7 @@ packages: eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -1971,6 +1987,11 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2297,6 +2318,10 @@ packages: mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + markdown-it@14.2.0: resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==} hasBin: true @@ -2570,10 +2595,24 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + pnpm-workspace-yaml@1.6.0: resolution: {integrity: sha512-uUy4dK3E11sp7nK+hnT7uAWfkBMe00KaUw8OG3NuNlYQoTk4sc9pcdIy1+XIP85v9Tvr02mK3JPaNNrP0QyRaw==} @@ -3605,11 +3644,11 @@ snapshots: dependencies: '@lit-labs/ssr-dom-shim': 1.6.0 - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.1 + '@tybys/wasm-util': 0.10.3 optional: true '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': @@ -3626,6 +3665,10 @@ snapshots: '@pkgr/core@0.2.9': {} + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@publint/pack@0.1.4': {} '@quansync/fs@1.0.0': @@ -3708,7 +3751,7 @@ snapshots: dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@rolldown/binding-wasm32-wasi@1.1.4': @@ -3893,11 +3936,6 @@ snapshots: '@turbo/windows-arm64@2.9.18': optional: true - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -4003,7 +4041,7 @@ snapshots: eslint: 9.39.4(jiti@2.6.1) json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 - semver: 7.7.4 + semver: 7.8.5 transitivePeerDependencies: - supports-color - typescript @@ -4866,6 +4904,9 @@ snapshots: format@0.2.2: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -5158,6 +5199,15 @@ snapshots: mark.js@8.11.1: {} + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.1 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-it@14.2.0: dependencies: argparse: 2.0.1 @@ -5614,8 +5664,18 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} + pngjs@7.0.0: {} + pnpm-workspace-yaml@1.6.0: dependencies: yaml: 2.8.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c84a59b..f5015f2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,10 @@ trustPolicy: no-downgrade packages: - docs - packages/* +catalog: + '@playwright/test': 1.58.2 + markdown-it: 14.1.1 + pngjs: 7.0.0 catalogs: cli: '@antfu/eslint-config': ^6.7.3 diff --git a/scripts/check-docs.mjs b/scripts/check-docs.mjs new file mode 100644 index 0000000..68a1a17 --- /dev/null +++ b/scripts/check-docs.mjs @@ -0,0 +1,88 @@ +import assert from 'node:assert/strict' +import { readdir, readFile } from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' +import { Window } from 'happy-dom' +import MarkdownIt from 'markdown-it' +import { elementMetadata } from '../packages/elements/src/metadata.ts' +import { datavElementRegistrations } from '../packages/elements/src/register.ts' + +const docs = path.resolve('docs') +const output = path.join(docs, '.vitepress/dist') +const base = process.env.VITEPRESS_BASE || '/' +const parser = new MarkdownIt() + +assert.deepEqual( + elementMetadata.map(meta => meta.tagName).sort(), + datavElementRegistrations.map(registration => registration.tagName).sort(), + 'Component metadata and registrations differ', +) + +async function walk(directory) { + const entries = await readdir(directory, { withFileTypes: true }) + return (await Promise.all(entries.map(entry => entry.isDirectory() + ? walk(path.join(directory, entry.name)) + : path.join(directory, entry.name)))).flat() +} + +function links(markdown) { + return parser.parse(markdown, {}).flatMap(token => token.children || []).filter(token => token.type === 'link_open').map(token => token.attrGet('href')) +} + +function docPath(href) { + const url = new URL(href, `https://example.test${base}llms.txt`) + assert.equal(url.origin, 'https://example.test', `Unexpected external documentation link: ${href}`) + assert.ok(url.pathname.startsWith(base), `Link lost deployment base: ${href}`) + return decodeURIComponent(url.pathname.slice(base.length)) +} + +const indexLinks = links(await readFile(path.join(output, 'llms.txt'), 'utf8')).map(docPath) +assert.equal(new Set(indexLinks).size, indexLinks.length, 'llms.txt contains duplicate pages') +for (const link of indexLinks) + await readFile(path.join(output, link)) + +const componentPages = (await walk(path.join(docs, 'components'))).filter(file => file.endsWith('.md')) +const pageByTag = new Map() +for (const file of componentPages) { + const text = await readFile(file, 'utf8') + assert.match(text, /^---\s*\ndescription: .+/m, `Missing discoverable description: ${file}`) + const tag = text.match(/^`(dvk-[a-z\d-]+)`/m)?.[1] + assert.ok(tag, `Missing introductory component tag: ${file}`) + assert.ok(!pageByTag.has(tag), `Duplicate component documentation: ${tag}`) + pageByTag.set(tag, path.relative(docs, file)) +} + +for (const metadata of elementMetadata) { + const page = pageByTag.get(metadata.tagName) + assert.ok(page, `Registered component has no documentation: ${metadata.tagName}`) + assert.ok(indexLinks.includes(page), `Component missing from llms.txt: ${page}`) +} +for (const tag of pageByTag.keys()) + assert.ok(elementMetadata.some(meta => meta.tagName === tag), `Documentation describes an unknown component: ${tag}`) + +const known = new Set(elementMetadata.map(meta => meta.tagName)) +const exampleDirectory = path.resolve('skills/datav-kit/assets/examples') +const examples = (await readdir(exampleDirectory)).filter(name => name.endsWith('.html')) +const window = new Window() +for (const name of examples) { + const source = await readFile(path.join(exampleDirectory, name), 'utf8') + const html = new window.DOMParser().parseFromString(source, 'text/html') + // Includes tags in inline templates; the browser suite also examines the rendered DOM. + const tags = new Set([...source.matchAll(/<\/?(dvk-[a-z\d-]+)/g)].map(match => match[1])) + for (const tag of tags) { + assert.ok(known.has(tag), `Unknown example tag ${tag} in ${name}`) + assert.ok(pageByTag.has(tag), `Undocumented example tag ${tag} in ${name}`) + } + const imports = JSON.parse(html.querySelector('script[type="importmap"]').textContent).imports + for (const url of Object.values(imports)) + assert.match(url, /^https:\/\/cdn\.jsdelivr\.net\/npm\/(?:@[\w-]+\/)?[\w-]+@\d+\.\d+\.\d+\//, `Unpinned dependency: ${url}`) + assert.equal(await readFile(path.join(output, 'examples', name), 'utf8'), source, `Preview differs from source: ${name}`) + await readFile(path.join(output, 'examples', name.replace('.html', '.png'))) +} + +const guide = 'guide/dashboard-examples.md' +assert.ok(indexLinks.includes(guide), 'Examples guide missing from llms.txt') +const guideText = await readFile(path.join(docs, guide), 'utf8') +for (const name of examples) + assert.ok(guideText.includes(`/examples/${name}`), `Example missing from gallery: ${name}`) +console.log(`Verified ${elementMetadata.length} components, ${indexLinks.length} index links and ${examples.length} standalone previews.`) diff --git a/skills/datav-kit/SKILL.md b/skills/datav-kit/SKILL.md index 852dc73..ae5d362 100644 --- a/skills/datav-kit/SKILL.md +++ b/skills/datav-kit/SKILL.md @@ -1,145 +1,94 @@ --- name: datav-kit -description: Design and build large-screen data dashboards with datav-kit Web Components (`dvk-*`). Use when the user wants a 数据大屏 / dashboard screen, needs to choose or compose datav-kit elements, or wants dashboard output reviewed against a design spec. +description: Build and refine large-screen data dashboards with datav-kit Web Components. Use for data walls, command screens, and business cockpits using dvk-* elements, including visual composition and browser verification. Not for ordinary administration interfaces. --- -# datav-kit — Large-Screen Dashboards +# DataV Kit Large-Screen Development -Route a 大屏 brief through one contract: clarify → prototype → select → implement → self-check → -review. The skill carries the design rules, the composition patterns, the component availability -model, and the screen token set. It does not wrap the components and does not bundle a chart -library. +Use official documentation to discover component capabilities. This skill supplies a +development workflow and design guidance; it is not a component catalog. -## 1. When this skill applies +## 1. Read documentation first -**Use it when the work is a large screen** — a 数据大屏, dashboard wall, or control-room display -built from datav-kit `dvk-*` Web Components, including: +Start with https://hackycy.github.io/datav-kit/llms.txt. Resolve its returned links against +that URL, including the `/datav-kit/` base path. Read installation and theming for a new +project, framework integration when applicable, and details for the intended components. +Read props, events, CSS variables, slots and parts from those pages before authoring them. +Reuse pages already read in this session; fetch additional pages only as needed. -- choosing and composing `dvk-*` elements into a screen; -- laying a screen out against the design contract; -- reviewing existing dashboard output against that contract. +Documentation coverage is not installed-package availability. Inspect the project's +dependency version. Follow its documented registration API, complete registration, then +check `customElements.get(tag)` for the components used in the screen. Resolve missing +dependencies explicitly; a replacement needs its own verified API and suitable appearance. -**Do not use it for:** +If the site is unavailable or a page is missing: -- ordinary back-office pages or any non-large-screen UI — the density, type, and layout rules here - are calibrated for viewing distance, not for a desk monitor; -- projects that do not use datav-kit components. +1. Read the corresponding `docs/` page at a verified GitHub tag or commit matching the + dependency: `https://raw.githubusercontent.com/hackycy/datav-kit//docs/`. + Verify the ref exists rather than assuming a version has a same-named Git tag. +2. If only `main` has the material, label it as unreleased-source documentation and verify + compatibility against the actual dependency before using it. +3. Offline, inspect matching local documentation and package source. State remaining + uncertainty; do not invent an API to complete the screen. -This skill adds no wrapper components and no second color system. Every color comes from the -active theme's `--dvk-color-*` values; screen tokens (`--dvk-screen-*`) are not part of the theme -and are declared on `.dvk-screen`, never on `:root`. +Discover the official examples page through the same index. Its previews and downloads +come from this skill's `assets/examples/` HTML files, not a separate implementation. -## 2. Coverage and preflight +## 2. Establish the brief -Establish what is actually available before naming a component in a plan or a prototype. +Extract what is already known: business question, core metrics, data source and refresh, +physical screen size and resolution and viewing distance, visual identity, interactions, +and delivery framework. Ask only for missing information that materially changes the work. -| Status | Count | Components | -| --- | --- | --- | -| Published (`@datav-kit/elements@0.0.5`) | 30 | `dvk-border-box-1`…`15`, `dvk-decoration-1`…`11`, `dvk-count-to`, `dvk-fit-screen`, `dvk-loading-energy`, `dvk-loading-orbit` | -| `main` branch only | 5 | `dvk-title-1`…`3`, `dvk-border-box-16`, `dvk-performance-monitor` | -| Nonexistent | — | `dvk-title-4` — an empty directory; never reference it | +For drafts with unspecified hardware, use a 1920 x 1080 canvas and state that physical +viewing-distance calibration is pending. Production delivery requires checking readability +on the actual installation. -This list is a planning aid. **Runtime availability is the authority**, checked after the element -package has finished registering: +## 3. Choose a visual direction -```js -await import('@datav-kit/elements@0.0.5') -customElements.get('dvk-border-box-10') // truthy → registered -``` +Read [composition guidance](references/patterns.md) for a new composition. Choose the +primary visual from the business question, then arrange supporting information and select +documented components. Examples are design references, not mandatory layouts. -- Never pass a prop to a component that is not registered. -- If every candidate in a fallback chain is unavailable, stop and report the missing package. -- The header follows the same rule: `customElements.get('dvk-title-1')` decides between - `dvk-title-*` (Title 1 enterprise/industrial, Title 2 command centre, Title 3 city operations) - and the hand-built P3 title bar. `dvk-title-4` does not exist. +- Existing design or local change: preserve its direction and implement the change. +- New screen with a clear brief or reference: state the direction and implement it. +- New screen with materially ambiguous styling: offer a small set of different directions + and resolve the choice before investing in the full composition. -**Detail routing is online-first.** Use the live `llms.txt` index to decide *whether* a component -fits; fetch its detail page before writing props, events, CSS variables, or `::part()`. Do not -fetch the same page twice in one session. If the index omits the component or the page 404s, fall -back to the repository raw URL and label the result "from `main` — may not be published yet". +Directions should differ in layout, data visual, typography and decoration density, as +well as palette. Use [design checks](references/design-rules.md) to evaluate the result. +Follow the project's existing decision-record convention; create `design/` artifacts only +when persistent design history benefits the task. -Availability list, capability fields, and the full fetch protocol: `references/components.md`. +## 4. Implement -**One theme per screen.** A screen uses exactly one `.dvk-theme-*` class. To emphasise a region, -use the accent color role — never a second theme. +Coordinate backgrounds, text, status, data series and component accents through one scoped +project theme. The official theming guide owns CSS contracts. The editable +[project theme starter](assets/themes/theme-template.css) demonstrates a light palette. -## 3. The six-step workflow +Use [screen tokens](assets/tokens.css) as adjustable defaults, with +[calibration guidance](references/tokens.md). Read [chart guidance](references/charts.md) +when adding charts; `assets/charts/` contains optional ECharts starting points. Reuse an +existing project chart library when available. -Artifacts land in the project's `design/` directory so the process stays traceable and reviewable. +For standalone delivery, adapt a single `assets/examples/*.html`: inline application +styles, scripts and required scene data, pin CDN imports, and test opening the file directly. +`assets/minimal-example.html` is the small registration-and-scaling starting point. +Verify example APIs against project dependencies before adapting them. -| # | Step | Artifact | Exit condition | -| --- | --- | --- | --- | -| ① | Clarify | `design/brief.md` | All seven brief questions answered (§5) | -| ② | Prototype | `design/prototype-*.html` (2–3) | Each opens by double-click with no console errors | -| ③ | Select | `design/decision.md` | The user has explicitly chosen one; deviations registered | -| ④ | Implement | project code | The screen runs at the target resolution | -| ⑤ | Self-check | `design/self-check.md` | **Every redline passes** | -| ⑥ | Review | `design/review.md` | Review record plus the skill revision items it produced | +Represent loading, empty, failed and stale data deliberately. Retained previous values +need an update time and stale status. Keep units, totals and time ranges consistent. -Step ② rewrites 2–3 of the four templates T1–T4 instead of starting from a blank page. Step ⑤ -derives its checklist 1:1 from `references/design-rules.md` — redlines binary, advisory values -three-level — and must be able to detect hard-coded values where a token was required. Step ⑥ -turns every review finding into a skill revision item; without that the review is not complete. +## 5. Verify in a browser -## 4. Five hard gates +Test the real delivery path at target resolution and a smaller preview viewport. Check +registration, dependency errors, chart sizes, text fit, safe content areas, interactions, +keyboard focus, data states and reduced motion. Inspect an actual screenshot for hierarchy; +passing DOM checks alone does not establish visual quality. -1. **No implementation before a prototype is selected.** -2. **No prototype before the brief is clarified** — viewing distance and resolution are what make - font-size calibration possible. -3. **No review before every redline passes.** A failure is fixed first, or the screen goes back to - a prototype. -4. **Touching a redline or changing the skeleton → go back and re-select a prototype.** Skeleton - means row count, column count, primary-view position, header form, or adding/removing a block. -5. **An unregistered advisory-value deviation is a defect.** +For 3D, inspect rendered pixels and framing, exercise selection and camera controls, +verify pause/reduced motion, and test the non-WebGL presentation with the same business data. -## 5. The brief — seven questions - -Ask all seven before step ②; an unanswered question blocks the prototype. - -| # | Question | Decides | -| --- | --- | --- | -| 1 | Business domain / scenario | Template choice (T1–T4) | -| 2 | Core metrics (3–9) | KPI strip and block content | -| 3 | Data source and refresh rate | The four exception states, especially "stale" | -| 4 | **Target screen: size, resolution, viewing distance** | **Font-size calibration** | -| 5 | Theme | One library theme, or a custom project theme | -| 6 | Interaction (none, click, drill-down) | Static display vs. interactive screen | -| 7 | Delivery form (static HTML / Vue / React / other) | How it is implemented and charts are wired | - -## 6. Redline quick reference - -Four classes, plus the one-theme rule. `references/design-rules.md` owns the entries, thresholds, -and sources — this is the index, not a second copy. - -| Class | The redline, in one line | -| --- | --- | -| Readability | Contrast floor (body ≥ 4.5:1, large text ≥ 3:1, non-text ≥ 3:1, **never rounded**); minimum font size = viewing distance / 200; key data is not hover-only | -| Geometry | Proportional scaling only — no non-uniform stretch; content stays inside the safe area and must not overflow or clip | -| Semantics | Every glow, motion, and decoration needs a named data or interaction mapping — if it is only "pretty", delete it | -| Accessibility | `prefers-reduced-motion` is honoured; flashing ≤ 3/s; autoplay over 5s is pausable; keyboard reachable with a visible focus ring | - -Advisory values may be deviated from, but each deviation is registered with a one-line reason; -an unregistered one is a defect (gate 5). **One screen, one theme.** - -## 7. Reference index - -Resident — read them as part of the workflow: - -| File | Owns | -| --- | --- | -| `references/design-rules.md` | Six rule groups, tiers, thresholds, sources, rubric derivation | -| `references/patterns.md` | Routing path, T1–T4 templates, P1–P19 patterns, the P6 border-box matrix | -| `references/components.md` | Availability, border-box capability fields, online-first detail routing | -| `references/tokens.md` | `--dvk-screen-*` values, scope, precedence, calibration | -| `assets/tokens.css` | Reference implementation of the token set under `.dvk-screen` | - -On demand: - -| File | Read it when | -| --- | --- | -| `references/charts.md` | Choosing a chart form, or bridging tokens into a chart library | -| `assets/charts/*.js` | You need a runnable chart starting point | -| `assets/prototypes/t*.html` | You are writing step ② | -| `assets/themes/theme-template.css` | The project needs its own theme | -| `assets/tools/contrast-check.js` | You are checking the contrast redline | -| `assets/minimal-example.html` | You want the smallest working screen | +Report the runnable artifact, verification performed and remaining constraints. Fix delivery +defects in the project. Revise this skill only when a finding demonstrates a reusable problem +in the guidance, rather than requiring skill changes on every task. diff --git a/skills/datav-kit/assets/charts/bar-rank.js b/skills/datav-kit/assets/charts/bar-rank.js index 65875dc..cd4547f 100644 --- a/skills/datav-kit/assets/charts/bar-rank.js +++ b/skills/datav-kit/assets/charts/bar-rank.js @@ -21,7 +21,7 @@ import * as echarts from 'echarts' const CHART_MOTION = 300 -const MIN_WIDTH = 160 // charts.md §5: below this the chart degrades to a value card +const MIN_WIDTH = 160 // Starter size guard: below this, render a value card const MIN_HEIGHT = 100 const LARGE_THRESHOLD = 400 // large mode drops per-item styles and labels @@ -131,7 +131,7 @@ function buildOption(data, t) { // Explicit grid: the right gutter holds the direct value label. grid: { left: 8, right: 56, top: 8, bottom: 8, outerBoundsMode: 'same', outerBoundsContain: 'axisLabel' }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, - // Ranking bars start at 0; a truncated axis misleads (design-rules 6.4). + // Ranking bars start at 0; a truncated axis misleads. xAxis: { type: 'value', min: 0 }, yAxis: { type: 'category', inverse: true, data: items.map(item => item.name), axisTick: { show: false } }, series: [{ @@ -141,7 +141,7 @@ function buildOption(data, t) { barWidth: 12, large: true, largeThreshold: LARGE_THRESHOLD, - // Direct labels beat a detached legend (design-rules 6.1). + // Direct labels beat a detached legend. label: { show: true, position: 'right', @@ -298,7 +298,7 @@ export function createBarRank(el, data, tokens = readDatavTokens(el)) { applyState() } - /* Below the guard the chart is replaced by a value card (charts.md §5). */ + /* Below the guard the chart is replaced by a value card. */ function degrade() { if (degraded) return diff --git a/skills/datav-kit/assets/charts/gauge.js b/skills/datav-kit/assets/charts/gauge.js index ac5d986..7bfbad3 100644 --- a/skills/datav-kit/assets/charts/gauge.js +++ b/skills/datav-kit/assets/charts/gauge.js @@ -21,7 +21,7 @@ import * as echarts from 'echarts' const CHART_MOTION = 300 -const MIN_WIDTH = 160 // charts.md §5: below this the chart degrades to a value card +const MIN_WIDTH = 160 // Starter size guard: below this, render a value card const MIN_HEIGHT = 100 /* ---------- 1. token injection: --dvk-* -> ECharts theme object ---------- */ @@ -143,7 +143,7 @@ function buildOption(data, t) { startAngle: 210, endAngle: -30, splitNumber: 4, - // One pointer only; the design cap is 3 (design-rules 6.2). + // One pointer only; this starter uses one pointer. progress: { show: true, width: 8, @@ -312,7 +312,7 @@ export function createGauge(el, data, tokens = readDatavTokens(el)) { applyState() } - /* Below the guard the chart is replaced by a value card (charts.md §5). */ + /* Below the guard the chart is replaced by a value card. */ function degrade() { if (degraded) return diff --git a/skills/datav-kit/assets/charts/heatmap.js b/skills/datav-kit/assets/charts/heatmap.js index b8340a7..50ef37e 100644 --- a/skills/datav-kit/assets/charts/heatmap.js +++ b/skills/datav-kit/assets/charts/heatmap.js @@ -21,7 +21,7 @@ import * as echarts from 'echarts' const CHART_MOTION = 300 -const MIN_WIDTH = 160 // charts.md §5: below this the chart degrades to a value card +const MIN_WIDTH = 160 // Starter size guard: below this, render a value card const MIN_HEIGHT = 100 const PROGRESSIVE = 4000 // render in chunks above this many cells @@ -146,7 +146,7 @@ function buildOption(data, t) { bottom: 0, itemWidth: 12, itemHeight: 80, - // Single-hue ramp, monotonic in alpha: never a rainbow scale (design-rules 4.6). + // Single-hue ramp, monotonic in alpha: never a rainbow scale. inRange: { color: [withAlpha(t.primary, 0.12), withAlpha(t.primary, 0.35), withAlpha(t.primary, 0.65), t.primary] }, }, series: [{ @@ -304,7 +304,7 @@ export function createHeatmap(el, data, tokens = readDatavTokens(el)) { applyState() } - /* Below the guard the chart is replaced by a value card (charts.md §5). */ + /* Below the guard the chart is replaced by a value card. */ function degrade() { if (degraded) return diff --git a/skills/datav-kit/assets/charts/line-area.js b/skills/datav-kit/assets/charts/line-area.js index 816ce17..518d095 100644 --- a/skills/datav-kit/assets/charts/line-area.js +++ b/skills/datav-kit/assets/charts/line-area.js @@ -21,9 +21,9 @@ import * as echarts from 'echarts' const CHART_MOTION = 300 -const MIN_WIDTH = 160 // charts.md §5: below this the chart degrades to a value card +const MIN_WIDTH = 160 // Starter size guard: below this, render a value card const MIN_HEIGHT = 100 -const MAX_SERIES = 4 // design-rules 6.2 +const MAX_SERIES = 4 // Reference presentation limit; adapt to the dataset. /* ---------- 1. token injection: --dvk-* -> ECharts theme object ---------- */ @@ -123,7 +123,7 @@ function buildOption(data, t) { const all = data.series || [] const series = all.slice(0, MAX_SERIES) if (all.length > MAX_SERIES) - console.warn(`[datav-kit] line-area: ${all.length} series exceeds the 4-line cap (design-rules 6.2); only the first ${MAX_SERIES} are drawn.`) + console.warn(`[datav-kit] line-area: ${all.length} series exceeds the 4-line cap; only the first ${MAX_SERIES} are drawn.`) return { animation: !prefersReducedMotion(), @@ -141,17 +141,17 @@ function buildOption(data, t) { legend: { show: series.length > 1, top: 0, right: 0, itemWidth: 12, itemHeight: 8 }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', boundaryGap: false, data: data.labels || [], axisTick: { show: false } }, - // A line chart need not start at 0 (design-rules 6.4). + // A line chart need not start at 0. yAxis: { type: 'value', scale: true }, series: series.map(item => ({ type: 'line', name: item.name, data: item.data, - smooth: false, // no over-smoothing (design-rules 6.5) + smooth: false, // no over-smoothing showSymbol: false, connectNulls: false, // null means "no data here"; never invent a line across it sampling: 'lttb', // downsample when points far exceed pixels - // Data lines sit in the 1.5-2.25px band; axes use --dvk-line-width (charts.md §5). + // Data lines sit in the 1.5-2.25px band; axes use --dvk-line-width. lineStyle: { width: Math.min(2.25, Math.max(1.5, t.lineWidth * 2)) }, areaStyle: { opacity: 0.16 }, })), @@ -303,7 +303,7 @@ export function createLineArea(el, data, tokens = readDatavTokens(el)) { applyState() } - /* Below the guard the chart is replaced by a value card (charts.md §5). */ + /* Below the guard the chart is replaced by a value card. */ function degrade() { if (degraded) return diff --git a/skills/datav-kit/assets/charts/pie-doughnut.js b/skills/datav-kit/assets/charts/pie-doughnut.js index 3defad2..d483500 100644 --- a/skills/datav-kit/assets/charts/pie-doughnut.js +++ b/skills/datav-kit/assets/charts/pie-doughnut.js @@ -21,9 +21,9 @@ import * as echarts from 'echarts' const CHART_MOTION = 300 -const MIN_WIDTH = 160 // charts.md §5: below this the chart degrades to a value card +const MIN_WIDTH = 160 // Starter size guard: below this, render a value card const MIN_HEIGHT = 100 -const MAX_CATEGORIES = 5 // design-rules 6.2 / ECharts handbook +const MAX_CATEGORIES = 5 // Reference presentation limit; adapt to the dataset. /* ---------- 1. token injection: --dvk-* -> ECharts theme object ---------- */ @@ -127,7 +127,7 @@ function prefersReducedMotion() { function buildOption(data, t) { const items = data.items || [] if (items.length > MAX_CATEGORIES) - console.warn(`[datav-kit] pie-doughnut: ${items.length} categories exceeds the 5-slice cap (design-rules 6.2); a stacked bar compares them better.`) + console.warn(`[datav-kit] pie-doughnut: ${items.length} categories exceeds the 5-slice cap; a stacked bar compares them better.`) return { animation: !prefersReducedMotion(), @@ -142,11 +142,11 @@ function buildOption(data, t) { avoidLabelOverlap: true, minAngle: 6, percentPrecision: 2, - showEmptyCircle: false, // datav-kit owns the empty state (charts.md §6) - // Direct labels beat a detached legend (design-rules 6.1). + showEmptyCircle: false, // datav-kit owns the empty state + // Direct labels beat a detached legend. label: { show: true, formatter: '{b} {d}%' }, labelLine: { length: 8, length2: 8 }, - // A hairline in the surface colour separates adjacent slices (design-rules 4.8). + // A hairline in the surface colour separates adjacent slices. itemStyle: { borderColor: t.surface, borderWidth: t.lineWidth }, emphasis: { scale: false }, data: items, @@ -299,7 +299,7 @@ export function createPieDoughnut(el, data, tokens = readDatavTokens(el)) { applyState() } - /* Below the guard the chart is replaced by a value card (charts.md §5). */ + /* Below the guard the chart is replaced by a value card. */ function degrade() { if (degraded) return diff --git a/skills/datav-kit/assets/charts/radar.js b/skills/datav-kit/assets/charts/radar.js index 10a9840..f916e9d 100644 --- a/skills/datav-kit/assets/charts/radar.js +++ b/skills/datav-kit/assets/charts/radar.js @@ -21,9 +21,9 @@ import * as echarts from 'echarts' const CHART_MOTION = 300 -const MIN_WIDTH = 160 // charts.md §5: below this the chart degrades to a value card +const MIN_WIDTH = 160 // Starter size guard: below this, render a value card const MIN_HEIGHT = 100 -const MAX_INDICATORS = 5 // design-rules 6.2 / ECharts handbook +const MAX_INDICATORS = 5 // Reference presentation limit; adapt to the dataset. /* ---------- 1. token injection: --dvk-* -> ECharts theme object ---------- */ @@ -129,7 +129,7 @@ function prefersReducedMotion() { function buildOption(data, t) { const indicators = data.indicators || [] if (indicators.length > MAX_INDICATORS) - console.warn(`[datav-kit] radar: ${indicators.length} indicators exceeds the 5-axis cap (design-rules 6.2); a line chart reads exact values better.`) + console.warn(`[datav-kit] radar: ${indicators.length} indicators exceeds the 5-axis cap; a line chart reads exact values better.`) const series = data.series || [] return { @@ -148,7 +148,7 @@ function buildOption(data, t) { series: [{ type: 'radar', symbol: 'none', - // Data lines sit in the 1.5-2.25px band; axes use --dvk-line-width (charts.md §5). + // Data lines sit in the 1.5-2.25px band; axes use --dvk-line-width. lineStyle: { width: Math.min(2.25, Math.max(1.5, t.lineWidth * 2)) }, areaStyle: { opacity: 0.16 }, data: series.map(item => ({ name: item.name, value: item.values })), @@ -301,7 +301,7 @@ export function createRadar(el, data, tokens = readDatavTokens(el)) { applyState() } - /* Below the guard the chart is replaced by a value card (charts.md §5). */ + /* Below the guard the chart is replaced by a value card. */ function degrade() { if (degraded) return diff --git a/skills/datav-kit/assets/charts/scatter.js b/skills/datav-kit/assets/charts/scatter.js index 8efb663..08889b8 100644 --- a/skills/datav-kit/assets/charts/scatter.js +++ b/skills/datav-kit/assets/charts/scatter.js @@ -21,7 +21,7 @@ import * as echarts from 'echarts' const CHART_MOTION = 300 -const MIN_WIDTH = 160 // charts.md §5: below this the chart degrades to a value card +const MIN_WIDTH = 160 // Starter size guard: below this, render a value card const MIN_HEIGHT = 100 const LARGE_THRESHOLD = 2000 // scatter default; above it per-point symbol sizes are dropped @@ -83,7 +83,7 @@ function parseGlow(value) { function typeTheme(t) { return { scatter: { - // A hairline rim lifts each mark off the dark ground (design-rules 4.8). + // A hairline rim lifts each mark off the dark ground. itemStyle: { opacity: 0.72, borderColor: withAlpha(t.primary, 0.35), borderWidth: t.lineWidth }, }, } @@ -148,7 +148,7 @@ function buildOption(data, t) { symbolSize: point => (point?.[2] == null ? 8 : 6 + Math.sqrt(point[2]) * 2), large: true, largeThreshold: LARGE_THRESHOLD, - // The accent role marks the point under the pointer (charts.md §3). + // The accent role marks the point under the pointer. emphasis: { itemStyle: { opacity: 1, borderColor: withAlpha(t.accent, 0.9), borderWidth: 2 } }, })), } @@ -299,7 +299,7 @@ export function createScatter(el, data, tokens = readDatavTokens(el)) { applyState() } - /* Below the guard the chart is replaced by a value card (charts.md §5). */ + /* Below the guard the chart is replaced by a value card. */ function degrade() { if (degraded) return diff --git a/skills/datav-kit/assets/examples/business.html b/skills/datav-kit/assets/examples/business.html new file mode 100644 index 0000000..860780d --- /dev/null +++ b/skills/datav-kit/assets/examples/business.html @@ -0,0 +1,771 @@ + + + + + + + 远川商业 · 经营分析 + + + + + +
+
+
+
+ +
+
+

远川商业 · 经营分析

+

YUANCHUAN COMMERCE / PERFORMANCE

+
+
+
+
+ +
+ 数据加载中 +
+
+
+
+
+

营业收入

+
+ 万元 +
+

+
+
+

目标完成率

+
+ % +
+

+
+
+

成交订单

+
+ 笔 +
+

有效支付已扣除退款订单

+
+
+

平均客单价

+
+ 元 +
+

收入 / 订单同一统计周期

+
+
+
+
+
+
+

收入表现

+

+
+
+ 实际收入目标收入 +
+
+
+
+
单日收入峰值
+
日均营业收入
+
领先渠道
+
+
+
+

渠道贡献

+
+
+ 距周期目标 +

+
+
+
+
+ 示例数据 · 财务口径:已支付净收入 · 人民币最后更新 2026.09.09 14:30远川商业 / BUSINESS INTELLIGENCE +
+
+ +

正在汇总经营数据

+

2026.09.09

+ +
+
+
+ + + diff --git a/skills/datav-kit/assets/examples/city.html b/skills/datav-kit/assets/examples/city.html new file mode 100644 index 0000000..81f13aa --- /dev/null +++ b/skills/datav-kit/assets/examples/city.html @@ -0,0 +1,884 @@ + + + + + + + + + 上海滨江 · 城市运行 + + + + + +
+
+
+ +
+

上海滨江 · 城市运行

+

SHANGHAI RIVERFRONT / URBAN OPERATIONS

+
+
+
+
+ +
+
+ 14:30:00 +

2026.09.09 / 星期三

+
+
+
+
+
+
+
+

区域感知设备

+
+ 台 +
+
+
+

在线率

+
+ % +
+
+
+

待处理事件

+
+ 项 +
+
+
+

当日累计客流

+
+ 人 +
+
+
+
+ +
+ 黄浦江 · 核心滨水区31.237° N / 121.498° E +
+
N
+
黄 浦 江
+
+ 感知节点当前区域 +
+ © OpenStreetMap contributors · ODbL +
+
+
+

小时客流

+

07:00 - 14:00 · 人 / 小时

+ +
+
+
+
+ +
+
+ 运行指标为示例数据 · 地图为 OpenStreetMap 实际地理数据数据加载中最后更新 2026.09.09 14:30 +
+
+

正在加载区域态势

+

上海滨江

+ +
+
+
+ + + + + + diff --git a/skills/datav-kit/assets/examples/energy.html b/skills/datav-kit/assets/examples/energy.html new file mode 100644 index 0000000..80e6738 --- /dev/null +++ b/skills/datav-kit/assets/examples/energy.html @@ -0,0 +1,850 @@ + + + + + + + 青禾能源 · 综合调度 + + + + + +
+
+
+ +
+

青禾能源 · 综合调度

+

QINGHE ENERGY / INTEGRATED DISPATCH

+
+
+
+
临港零碳园区源 · 网 · 荷 · 储
+
数据加载中
+
14:30:002026.09.09
+
+
+
+
+
+

总供给功率

+
+ MW +
+ 光伏 + 风电 + 电网 +
+
+

可再生能源占比

+
+ % +
+ 园区自发绿色电力 +
+
+

用电负荷

+
+ MW +
+ 工业 + 园区 + 充电站 +
+
+

储能充电

+
+ MW +
+ 消纳富余发电功率 +
+
+

供需偏差

+
+ MW +
+ 供给 − 用电 − 储能 +
+
+
+
+
+

实时能量流向

+ 当前调度快照 / MW +
+
+ +
+
+
+
+

SELECTED NODE

+

+
+ MW +
+
+
+
节点类型
+
+
+
+
占总供给
+
+
+
+
调度状态
+
正常运行
+
+
+

供需平衡

+
+
+
+
+
+

园区负荷曲线

+ 00:00 - 14:30 / MW +
+
+
+
+
+

储能系统

+ +
+
+ 64.0%32 / 50 MWh +
+
+

充电中 · 8.0 MW · 剩余容量 18 MWh

+
+
+
+
+ 示例数据 · 功率口径:交流侧调度快照 · 未计变换损耗最后更新 2026.09.09 14:30QINGHE / ENERGY MANAGEMENT +
+
+

正在同步调度数据

+

临港零碳园区

+ +
+
+
+ + + diff --git a/skills/datav-kit/assets/examples/industrial.html b/skills/datav-kit/assets/examples/industrial.html new file mode 100644 index 0000000..546362b --- /dev/null +++ b/skills/datav-kit/assets/examples/industrial.html @@ -0,0 +1,1326 @@ + + + + + + + 东川智造 · 生产监控 + + + + + +
+
+
+ +
+

东川智造 · 生产监控

+

DONGCHUAN MANUFACTURING / PLANT OPERATIONS

+
+
+
+
一号工厂智能制造园区 / 东区
+
+ 日班 08:00 - 16:002026.09.09 / 14:30:00 +
+
正在加载
+
+
+
+
+
+
+

本班累计产量

+
+ 件 +
+
+
+

目标完成率

+
+ % +
+
+
+

待巡检设备

+
+ 台 +
+
+
+

设备开机率

+
+ % +
+
+
+
+ +
+

厂区运行态势

+

PLANT 01 / EAST CAMPUS

+
+
+ +
+ + +
+ 3D / ISOMETRIC设备状态 14:30 快照 +
+
+
+
+

分时产量

+

08:00 - 14:00 / 件

+ +
+
+
+
+ +
+
+ 示范厂区 · 原创程序化场景 · 运行指标为示例数据最后更新 2026.09.09 14:30DONGCHUAN / MANUFACTURING INTELLIGENCE +
+
+

正在加载厂区态势

+

一号工厂

+ +
+
+
+ + + diff --git a/skills/datav-kit/assets/examples/previews/business.png b/skills/datav-kit/assets/examples/previews/business.png new file mode 100644 index 0000000..e395878 Binary files /dev/null and b/skills/datav-kit/assets/examples/previews/business.png differ diff --git a/skills/datav-kit/assets/examples/previews/city.png b/skills/datav-kit/assets/examples/previews/city.png new file mode 100644 index 0000000..fd9623d Binary files /dev/null and b/skills/datav-kit/assets/examples/previews/city.png differ diff --git a/skills/datav-kit/assets/examples/previews/energy.png b/skills/datav-kit/assets/examples/previews/energy.png new file mode 100644 index 0000000..eaccefe Binary files /dev/null and b/skills/datav-kit/assets/examples/previews/energy.png differ diff --git a/skills/datav-kit/assets/examples/previews/industrial.png b/skills/datav-kit/assets/examples/previews/industrial.png new file mode 100644 index 0000000..e7d06be Binary files /dev/null and b/skills/datav-kit/assets/examples/previews/industrial.png differ diff --git a/skills/datav-kit/assets/minimal-example.html b/skills/datav-kit/assets/minimal-example.html index 4616747..39b7961 100644 --- a/skills/datav-kit/assets/minimal-example.html +++ b/skills/datav-kit/assets/minimal-example.html @@ -1,606 +1,98 @@ - - - - datav-kit 最小示例 · minimal example - - - - - - - - - - - - - - - - -
- THEME - - - -
- - -
- -
-
- SCOPE - 单屏 · 3 面板 · 1 图表 -
-
-
- UPDATED - -
-
- -
-
- -
-
-
-

TREND

-

近 8 小时趋势

-
- 实时 -
-
-
-
-
- -
- -
-
-
-

PROGRESS

-

任务完成率

-
-
-
-
-
- - -
-
-
-

SECONDARY

-

次级指标

-
-
-
-
-
-
-
-
-
- - - - + + + + +
+

生产运行总览

+ +
+

本班累计产量 / 件

+ +
+
+

+
+
+ + diff --git a/skills/datav-kit/assets/prototypes/t1-three-column.html b/skills/datav-kit/assets/prototypes/t1-three-column.html deleted file mode 100644 index 2d85a57..0000000 --- a/skills/datav-kit/assets/prototypes/t1-three-column.html +++ /dev/null @@ -1,948 +0,0 @@ - - - - - - datav-kit prototype · T1 三栏运维 - - - - - - - - - - - - - - - - - - - - - - - - - - -
- THEME - - - - - -
- - -
- -
-
- REGION - -
-
-
- SHIFT / CLOCK - -
-
- -
- -
-
- -
-
-
-

SHARE

-

链路占用

-
- 实时 -
-
-
-
- - -
-
-
-

PRESSURE

-

枢纽压力

-
- TOP 3 -
-
-
-
-
- -
- -
-
-
-

TOPOLOGY

-

区域节点拓扑

-
- 在线 1284 -
-
-
- -
-
-
-
- - -
-
-
-

时段强度

-
- 08:00 – 15:00 -
-
-
-
-
- -
- -
-
-
-

调度队列

-
- 3 项 -
-
-
-
- - -
-
-
-

同步率

-
-
-
-
- - - -
- - -
-
-
-
-
-
-
-
-
- - - - - - diff --git a/skills/datav-kit/assets/prototypes/t2-two-column.html b/skills/datav-kit/assets/prototypes/t2-two-column.html deleted file mode 100644 index 0718abe..0000000 --- a/skills/datav-kit/assets/prototypes/t2-two-column.html +++ /dev/null @@ -1,796 +0,0 @@ - - - - - - datav-kit prototype · T2 两栏分析 - - - - - - - - - - - - - - - - - - - - - - - - - - -
- THEME - - - - - -
- - -
- -
-
- REGION - -
-
-
- RANGE - -
-
- -
- -
-
- -
-
-
-

TREND

-

时段趋势

-
- 近 8 小时 -
-
-
-
- - -
-
-
-

COMPARISON

-

区域对比

-
- TOP 5 -
-
-
-
-
- -
- -
-
-
-

PRESSURE

-

枢纽压力

-
- TOP 3 -
-
-
-
- - -
-
-
-

SECONDARY

-

次级指标

-
-
-
-
-
-
-
-
-
- - - - - - diff --git a/skills/datav-kit/assets/prototypes/t3-single-column.html b/skills/datav-kit/assets/prototypes/t3-single-column.html deleted file mode 100644 index 0989299..0000000 --- a/skills/datav-kit/assets/prototypes/t3-single-column.html +++ /dev/null @@ -1,804 +0,0 @@ - - - - - - datav-kit prototype · T3 单栏叙事 - - - - - - - - - - - - - - - - - - - - - - - - - - -
- THEME - - - - - -
- - -
- -
-
- REGION - -
-
-
- PHASE - -
-
- -
- -
-
-
-

-

-
- -
-
-
- -
-
-
-
- -
- -
-
-
-

任务完成度

-
- 今日 -
-
-
-
- - -
-
-
-

-
-
-
-
- - - -

-
-
-
-
- - -
-
-
-

-
-
-
-
- - - - -
-
-
-
-
-
- - -
-
-
-
- - - - - - diff --git a/skills/datav-kit/assets/prototypes/t4-kpi-led.html b/skills/datav-kit/assets/prototypes/t4-kpi-led.html deleted file mode 100644 index ddb282b..0000000 --- a/skills/datav-kit/assets/prototypes/t4-kpi-led.html +++ /dev/null @@ -1,634 +0,0 @@ - - - - - - datav-kit prototype · T4 KPI 主导 - - - - - - - - - - - - - - - - - - - - - - - - - - -
- THEME - - - - - -
- - -
- -
-
- REGION - -
-
-
- PERIOD - -
-
- -
- -
-
- -
-
-
-

TARGET

-

指标完成度

-
- 本月 -
-
-
-
-
- -
- -
-
-
-

PRESSURE

-

区域负载

-
- TOP 5 -
-
-
-
-
-
-
-
- - - - - - diff --git a/skills/datav-kit/assets/themes/theme-template.css b/skills/datav-kit/assets/themes/theme-template.css index 13e59d1..46ea26b 100644 --- a/skills/datav-kit/assets/themes/theme-template.css +++ b/skills/datav-kit/assets/themes/theme-template.css @@ -1,94 +1,28 @@ -/* ========================================================================== - datav-kit project theme — template - -------------------------------------------------------------------------- - Scope: `.dvk-theme-` only. Never `:root` — the host page - outside the screen must not inherit the theme. - Lands at: `design/theme.css` in the project (the `design/` convention). - Companion: `references/tokens.md` (token boundary), `references/design-rules.md` - group 4 (colour roles + contrast), `assets/tools/contrast-check.js`. - Source: issue 15 (project theme generation), issue 06 (customization - boundary), issue 16 (one theme per screen), spec.md section 3.12. - - What a theme owns: colour, glow, line width, decorative animation period. - What it does NOT own: spacing, type, layering, interaction motion, layout — - those are the `--dvk-screen-*` tokens in `assets/tokens.css`, declared on - `.dvk-screen`. Never put a screen token in a theme. - - Copy the block below, replace every value with your brand-derived result, - and keep all eight declarations. Nothing else belongs in this file: no - chart code, no prototype code, no component styles. - ========================================================================== */ - -/* -------------------------------------------------------------------------- - Derivation — starting points, not destinations - - Every value must clear the contrast redline before it is used. Run - `node assets/tools/contrast-check.js` against the four object groups; do not - hand-wave the ratio. The text, graphic, and decorative-line floors are - redlines — adjust until they PASS. The adjacent-marks group is advisory and - conditional (design-rules 4.8): a FAIL there means two marks are not told - apart by colour alone, so assign colour roles so adjacent series differ, or - add a direct label or shape cue. On a dark surface at most two marks can be - mutually 3:1 apart, so do not chase an all-pairs PASS by weakening the - palette. - - | variable | derive from the brand colour | - | ------------------------- | ----------------------------------------------------- | - | --dvk-color-primary | the brand colour; if it misses AA, lighten it or | - | | desaturate it until it passes (do not keep a failing | - | | brand colour "because it is the brand") | - | --dvk-color-secondary | primary hue +/- 30-40 deg, or primary lightness -20% | - | --dvk-color-accent | primary complement (hue +180 deg) or a highly | - | | saturated contrasting hue; dark scenes MUST desaturate | - | --dvk-color-surface | a very dark primary (lightness 5-10%) + alpha 0.72; | - | | never pure black (#000) — pure black kills depth and | - | | makes glow read as a halo floating on a void | - | --dvk-glow-soft | primary + alpha 0.55, blur radius 12px | - | --dvk-glow-strong | primary + alpha 0.85, blur radius 24px | - | --dvk-line-width | 1px | - | --dvk-motion-duration | 2200-2600ms (decorative period; interaction motion | - | | keeps the screen tokens 150-300ms) | - - The eight declarations are a complete contract, not a set of overrides. - Do not "inherit a library theme and change two values": a partial theme - makes it impossible to tell at debug time which value came from where. - Coexist with the library themes instead — a project theme is a sixth, - complete theme class, and a screen applies exactly one theme class. - - One theme per screen (issue 16): never mix `.dvk-theme-a` and - `.dvk-theme-b` on one screen. To emphasise a region, use - `--dvk-color-accent`, not a second theme. - -------------------------------------------------------------------------- */ - -/* Replace `` with the project slug, e.g. `.dvk-theme-acme-ops`. - - The values below are one worked example of the derivation above, from the - brand seed `#1f6fd0`: primary is the seed lightened to hsl(209, 100%, 65%) - so it clears AA on the surface (7.68:1); secondary is primary at lightness - -20%; accent is the complement (hue +180 deg) desaturated for the dark - scene; surface is the seed's dark version, lightness 6%, alpha 0.72. - At these values primary clears the body-text floor (7.68:1) and the accent - clears the non-text floor (10.92:1); secondary clears the non-text floor - (4.39:1) but not the body-text floor, so use it for graphics, lines and - secondary emphasis — raise its lightness before using it for body text. - Replace all eight with your own result. */ -.dvk-theme- { - /* Brand-derived colour roles. These four are the only colour source: - no hard-coded palette, no separate chart palette, no component-local - colour. Charts follow through the token bridge, prototypes through the - root container class — neither needs its own colour code. */ - --dvk-color-primary: #4ea8ff; - --dvk-color-secondary: #0077e6; - --dvk-color-accent: #ffb454; - /* Dark surface: lightness 5-10% of primary, alpha 0.72, never pure black. */ - --dvk-color-surface: rgba(4, 15, 28, 0.72); - - /* Glow: primary + alpha 0.55 / 0.85, blur radius 12px / 24px. - Glow is not a substitute for elevation — see design-rules 5.6. */ - --dvk-glow-soft: 0 0 12px rgba(78, 168, 255, 0.55); - --dvk-glow-strong: 0 0 24px rgba(78, 168, 255, 0.85); - - /* Structure and decorative motion. */ +/* Project theme starter. Rename the class and adapt roles to the brief. + Official contract: https://hackycy.github.io/datav-kit/guide/theming.md + --app-* variables belong to the application, not the datav-kit API. */ +.dvk-theme-project { + --app-background: #f4f6f8; + --app-surface: #ffffff; + --app-text: #17252d; + --app-muted: #52646e; + --app-rule: #d9e1e5; + --app-selected: #126c94; + --app-positive: #197448; + --app-warning: #9a5300; + --app-danger: #be3545; + --app-series-1: #167ba5; + --app-series-2: #168268; + --app-series-3: #b66019; + --app-series-4: #7a54a4; + + --dvk-color-primary: var(--app-selected); + --dvk-color-secondary: var(--app-series-2); + --dvk-color-accent: var(--app-warning); + --dvk-color-surface: var(--app-surface); + --dvk-glow-soft: 0 0 0 transparent; + --dvk-glow-strong: 0 0 0 transparent; --dvk-line-width: 1px; --dvk-motion-duration: 2400ms; + --dvk-count-to-color: var(--app-text); } diff --git a/skills/datav-kit/assets/tokens.css b/skills/datav-kit/assets/tokens.css index eff1494..5a62770 100644 --- a/skills/datav-kit/assets/tokens.css +++ b/skills/datav-kit/assets/tokens.css @@ -2,17 +2,13 @@ datav-kit screen design tokens — reference implementation -------------------------------------------------------------------------- Scope: `.dvk-screen` (the large-screen root container). Never `:root`. - Companion: `references/tokens.md` (full table + calibration contract). - Source: issue 05 (token set), issue 07 (grid and layout values), - spec.md section 5 (locked values). + Companion: `references/tokens.md` (scope and physical calibration). + This file is the maintained source of adjustable preview defaults. Rules enforced here: - - No color tokens. Colors come only from the theme's `--dvk-color-*` values. + - Colors belong to the scoped project theme, including application roles. - Precedence: project override > these skill defaults > component fallback. - - Font sizes are calibrated for a 4m x 2.25m screen at 6m viewing distance. - Recalibrate `--dvk-screen-font-size-xs` per project with - `min size = (viewing distance / 200) x (1080 / screen height)` - and raise the whole scale when the result exceeds 14px. + - Recalibrate type and spacing for the actual display and viewing distance. ========================================================================== */ .dvk-screen { diff --git a/skills/datav-kit/assets/tools/contrast-check.js b/skills/datav-kit/assets/tools/contrast-check.js index 4c831a7..90b2da6 100644 --- a/skills/datav-kit/assets/tools/contrast-check.js +++ b/skills/datav-kit/assets/tools/contrast-check.js @@ -1,34 +1,9 @@ /** - * datav-kit contrast check — zero-dependency, pure functions. - * - * Four object groups (spec.md 3.13 / issue 15): text / surface, graphic / - * surface, decorative line / surface, and adjacent data marks. Each group is - * checked against its non-rounded lower threshold (design-rules 4.1, a redline) - * and against the advisory comfort band 7:1-15:1 (design-rules 4.5). The floor - * decides PASS / FAIL; the band only adds an advisory note and never fails. - * - * Never rounds. WCAG 2.2 SC 1.4.3 is explicit that 4.499:1 is not 4.5:1, so a - * pair at 4.4951:1 must FAIL the body-text floor even though a two-decimal - * display reads 4.50. `ratio` is returned raw and compared raw. - * - * Reading a FAIL: the text / graphic / decorative-line floors are design-rules - * 4.1 redlines. The adjacent-marks floor comes from 4.8, which is advisory and - * conditional ("when a boundary itself carries meaning"), so a FAIL there means - * the two marks are not told apart by colour alone — fix it by assigning colour - * roles so adjacent series differ, or by adding a direct label / shape cue, not - * necessarily by changing the palette. On a dark surface (`--dvk-color-surface` - * is a 5-10% lightness of the brand colour) at most two marks can be mutually - * 3:1 apart, so "every pair passes" is not a reachable target for a three-colour - * theme. - * - * No palette lives here. Every colour is an input; in a project they come only - * from the active theme's `--dvk-color-*` values (one colour source). A - * translucent colour is composited over its background first — `--dvk-color-surface` - * is rgba with alpha 0.72, so pass the screen ground as `ground`. - * - * Node-only, standard library only, no dependencies: - * node assets/tools/contrast-check.js - * runs the four groups with representative passing and failing inputs. + * Numerical contrast helpers. Colors are caller-supplied application/theme roles. + * Alpha colors are composited over the supplied ground before comparison. + * Ratios are compared without rounding. Text and meaningful graphic thresholds + * follow the caller's usage; decorative-only colors do not need a data threshold. + * Run this file with Node for representative passing and failing examples. */ import process from 'node:process' @@ -133,35 +108,35 @@ export function contrastRatio(foreground, background, options = {}) { /* ---------- the four object groups ---------- */ -/** design-rules 4.5, advisory only: body text sits in this comfort band. */ +/** Optional historical comfort band; not a WCAG requirement. */ const ADVISORY_BAND = Object.freeze([7, 15]) export const GROUPS = Object.freeze({ 'text': Object.freeze({ label: 'text / surface', floor: 4.5, - floorSource: 'design-rules 4.1 body text (WCAG 2.2 SC 1.4.3)', + floorSource: 'WCAG 2.2 SC 1.4.3 normal text', }), 'graphic': Object.freeze({ label: 'graphic / surface', floor: 3, - floorSource: 'design-rules 4.1 non-text and state indicators (WCAG 2.2 SC 1.4.11)', + floorSource: 'WCAG 2.2 SC 1.4.11 meaningful graphics', }), 'decorative-line': Object.freeze({ label: 'decorative line / surface', floor: 3, - floorSource: 'design-rules 4.1 non-text', + floorSource: 'Optional visibility target for decorative lines; not a conformance requirement', }), 'adjacent-marks': Object.freeze({ label: 'adjacent data marks', floor: 3, - floorSource: 'design-rules 4.1 non-text applied to 4.8 (advisory, only when the boundary carries meaning)', + floorSource: 'WCAG 2.2 SC 1.4.11, only when the boundary carries meaning', }), }) -/** design-rules 4.1 also floors large text at 3:1 — same text group, lower floor. */ +/** WCAG large text uses a 3:1 floor. */ const LARGE_TEXT_FLOOR = 3 -const LARGE_TEXT_SOURCE = 'design-rules 4.1 large text (>= 18pt / 14pt bold / ~24px)' +const LARGE_TEXT_SOURCE = 'WCAG 2.2 SC 1.4.3 large text (>= 18pt / 14pt bold)' function advisoryFor(ratio) { const [low, high] = ADVISORY_BAND diff --git a/skills/datav-kit/references/charts.md b/skills/datav-kit/references/charts.md index cea1f86..9ce8186 100644 --- a/skills/datav-kit/references/charts.md +++ b/skills/datav-kit/references/charts.md @@ -1,186 +1,56 @@ -# Chart Guidance — Large-Screen Dashboards +# Charts and Data Visuals -Owns chart selection, the token bridge, and ECharts usage. **The library-neutral guidance is -normative for every project**; the seven modules in `assets/charts/` are an ECharts reference -implementation (used by `assets/prototypes/`, available as a project default) and are never a -datav-kit component abstraction. +Use the project's existing chart library. ECharts is the default for these standalone +examples. `assets/charts/` modules are optional starting points, not component contracts +or code that must be copied verbatim. Verify APIs against the actual library version. -| Layer | Content | Status | -| --- | --- | --- | -| Library-neutral | selection matrix, colour roles, four exception states, padding, type sizes, stroke tiers, anti-patterns, performance guards | design rule — holds for any chart library | -| ECharts-specific | token injection, `setTheme`, resize, DPR compensation, explicit `grid` | reference implementation — copy it or replace it | +## Choose an encoding -Colour comes only from the theme's `--dvk-color-*` values (`design-rules.md` §4). There is **no -second palette** — no chart colour file, no hard-coded hex, no per-chart theme. One screen, one theme. - -## 1. Selection matrix - -| Data shape | Preferred | Also acceptable | Anti-patterns (source) | Hard cap | -| --- | --- | --- | --- | --- | -| Time series — a metric over time | `line`, `area` | `bar` for a few discrete periods | More than 4 lines in one chart — lines entangle and lose contrast (ECharts handbook, basic-line); flattening or exaggerating the trend; `smooth: true` over-smoothing (design-rules 6.5) | 4 series | -| Part-to-whole — composition | `doughnut`, `radius: ['58%', '75%']` | stacked bar when the parts must be compared | Pie for close values — "people is less sensitive to the minor radian difference" than to small length differences (series-pie); 3D pie distorts the ratio (handbook) | 5 categories | -| Ranking — discrete comparison | horizontal `bar`, sorted, `yAxis.inverse: true` | lollipop for long lists | Bar axis not starting at 0 — "it will mislead the user" (handbook); a second colour when length already encodes the value; 3D bar | virtualise beyond ~20 rows | -| Distribution — spread and correlation | `scatter` (+ symbol size for a third dimension) | heatmap when points overlap densely | Scatter with no visible correlation; implying causality; a handful of unrelated points (handbook) | 1 series per relation | -| Density — two-dimensional | `heatmap` on two **category** axes | `scatter` with opacity when both axes are continuous | Rainbow sequential scale (design-rules 4.6); heatmap on two value axes — ECharts requires two categories | progressive rendering | -| Geographic | `map` / `geo` — **documentation only, no template** (§2) | a region list plus bars when geography adds nothing | Shipping boundary data without a licence check; a geographic layout for non-geographic data | see §2 | -| Relationship | `graph` with `layout: 'none'` (fixed coordinates) or `'circular'` | an adjacency matrix | `layout: 'force'` above ~100 nodes — the handbook warns the browser can hang; per-frame force jitter reads as noise on a wall screen | 100 nodes for force | -| Single value — progress | `gauge`, at most 3 pointers | KPI card with a sparkline | A gauge carpeted across several variables — "not suitable for carpeting different variables or trends" (handbook); more than 3 pointers; a gauge whose only job is one number — a big number is cheaper and more legible | 3 pointers | -| Multidimensional — profile | `radar`, at most 5 axes | parallel coordinates for an expert audience | More than 5 axes — "both the outline and color block will be too confusing to read" (handbook); reading exact values off radial distance — the handbook recommends a line chart for that | 5 indicators | -| Hierarchy — levels | `treemap` | `sunburst` with drill-down off | `tree` for a forest — "Forests are not currently supported directly in a single series"; `sunburst` left with its default `nodeClick` drill-down on an unattended screen | — | - -Two forms are forbidden outright (design-rules 6.5): 3D pie/bar, and dual axis. A second y-axis -hides the fact that two series share no scale — split the panel instead. - -## 2. Geographic is documentation-only - -No map template is shipped. ECharts removed the built-in geoJSON files in v5 — "These geoJSON files -were always sourced from third parties" (v5 upgrade guide) — so a map is a data-and-licence decision, -not a template decision. To add one: - -1. Obtain boundary data and **record its licence**. The ECharts FAQ points at third-party sources; - each project verifies its own terms. -2. `echarts.registerMap(name, geoJSON)` before the first `setOption`. -3. `series: [{ type: 'map', map: name }]` plus a `visualMap` for the value scale (same single-hue - ramp as §5). -4. Everything else still applies: the min-size guard, the four states, and the token bridge. - -`themeRiver`, `candlestick`, and the `tree` family are the same kind of decision — heavy, narrow, or -licence-bound — so they are not templates either. - -## 3. Token bridge contract - -The same eight tokens drive **any** chart library. ECharts cannot read CSS variables (the -maintainers declined the feature), so the bridge reads computed values and hands them over. - -| Token | Maps to | +| Question | Encoding | | --- | --- | -| `--dvk-color-primary` | palette position 1 / primary focus colour | -| `--dvk-color-secondary` | palette position 2 / contrast colour | -| `--dvk-color-accent` | accent — selection or alert emphasis | -| `--dvk-color-surface` | tooltip / overlay background | -| `--dvk-glow-soft` | shadow (blur radius + colour) | -| `--dvk-line-width` | axis and split-line width | -| `--dvk-motion-duration` | **not mapped** — decoration cycle (2200–2600ms), not a chart transition | -| `--dvk-screen-font-size-*` | in-chart type size | - -Rules that come with the bridge: - -- Read the tokens from the **nearest themed ancestor** of the chart container (or the container - itself, which inherits them) — never assume `document.documentElement` carries the screen's theme. -- Read only self-contained values (hex / rgb / rgba / px). `var()` is substituted by the browser, - but `rem` is not converted and `color-mix()` is not evaluated. -- ECharts has no alpha syntax: derive translucent roles in JS (`withAlpha`), do not bake them into - the theme file. -- Do **not** map `--dvk-motion-duration` into chart animation. Chart transitions stay in the - 200–400ms band; the theme token is a decoration cycle and makes a chart crawl. -- Do not map `--dvk-color-surface` to the chart background. The panel already paints a surface; - a second one muddies it. It belongs on tooltips and overlays. - -## 4. Copy vs adjust - -Each module in `assets/charts/` is self-contained and copy-and-run: the bridge, the option skeleton, -the resize wiring, the state layer, and the guards are all inside the one file. Copy the six pieces -verbatim; change only what the data demands. - -| Piece | What it is | Copy? | -| --- | --- | --- | -| Token injection | `readDatavTokens()` → `buildTheme()` → `init(el, themeObject)` | **verbatim** | -| Option skeleton | explicit `grid` / `center` + `radius`, explicit guards, no library defaults | skeleton verbatim, values per chart | -| Resize | `ResizeObserver` + `requestAnimationFrame` → `chart.resize()` | **verbatim** | -| Theme switching | `chart.setTheme(themeObject)` — never `dispose()` + re-init | **verbatim** | -| Four states | loading / empty / failed / stale, exposed as `setState()` | **verbatim** | -| Performance guard | `sampling`, `large` + `largeThreshold`, `progressive`, `animationThreshold` | **verbatim** | - -Must be adjusted for every real dataset — copying these is the defect: - -- the series data and its shape; -- the axis categories, ranges, and sort order; -- which colour role each series carries (primary focus vs secondary contrast vs accent); -- **the chart type itself**, when the data shape is not the one the template assumes. - -## 5. Space, size, type, strokes - -- **Fill the panel's content area; add no DOM padding.** `dvk-border-box-*` already computes an - 8–44px inset from its `contentRect` and scales it with the panel. Set the chart container to - `width: 100%; height: 100%` and let it fill `::part(content)`. -- **Override `grid` explicitly.** The library defaults (`left/right: '10%'`, `top/bottom: 60`) - waste most of a small panel. Use px values plus `outerBoundsMode: 'same'` / - `outerBoundsContain: 'axisLabel'` (the v6 replacement for `containLabel`). Non-cartesian types - (pie, gauge, radar) have no `grid` — give them an explicit `center` and `radius` instead. -- **To change the chart's own breathing room, override the token** — not by adding margin. - Cascade: `--dvk-border-box--padding` → `--dvk-border-box-padding` → the computed safe area. - The computed inset is the safe distance and the default; an override is a registered deviation - for a measured problem, never a way to dodge a decoration or to buy space. -- **Minimum size guard: content area < 160 x 100** → degrade to a `dvk-count-to` value card or a - mini sparkline (no axes, no labels). Decide from the container size, not from the rendered chart. -- **Type sizes**: axis labels and legend use `--dvk-screen-font-size-xs` (14); data labels and - tooltips use `--dvk-screen-font-size-sm` (18). **Panel titles never go inside the chart** — they - are DOM (`.panel-heading`), so they inherit CSS variables and stay readable to assistive tech. -- **Stroke tiers**: non-data 0.5–1px; data lines 1.5–2.25px; emphasis lines 2.5–3px. Axis and - split lines take `--dvk-line-width`; a series line is heavier than an axis by design. -- **Legend** sits next to the chart, never detached, and stays ≤ 30% of the chart height - (design-rules 6.3). Prefer direct labels — a legend is the fallback. - -## 6. Four exception states - -Every chart implements all four (design-rules 6.6), and prefers **stale but visible** over a blank -panel (design-rules 6.7). +| Change over time | Line or area with explicit units and time range | +| Rank or compare magnitude | Sorted bar, zero magnitude origin | +| Contribution to a whole | Stacked bar; doughnut for a few distinct categories | +| Distribution or correlation | Scatter or heatmap with meaningful axes | +| Location | Map with traceable data and attribution | +| Connectivity or flow | Stable graph or Sankey with direction and units | +| Progress against target | Value plus comparison, bullet or gauge | -| State | Chart side | DOM side | -| --- | --- | --- | -| Loading | `showLoading('default', { maskColor: 'transparent', color, textColor })` — **always override the default mask**, which is `rgba(255,255,255,0.8)` and paints a white sheet over a dark screen | optional `dvk-loading-*` component for a themed spinner | -| No data | `graphic` group (circle + text), cleared with `replaceMerge: ['graphic']`; `data: []` and `data: [0, 0]` are different — the latter still draws | — | -| Failed | `chart.clear()` — the chart holds no frame | `role="alert"` overlay with the message and, if available, a retry affordance | -| Stale | keep the last frame; mark missing spans with `null` (breaks the line) or `markArea` | a `最后更新` badge plus a dashed outline | +Prefer direct labels. Separate incompatible units rather than using an ambiguous second +axis. Reduce crowded series and labels. Avoid 3D magnitude charts and rainbow continuous scales. -The failed and stale layers live in the DOM on purpose: they can be announced, themed, and -repositioned without touching the canvas. Empty-state text is `silent: true` so it never swallows -pointer events. +## Theme and lifecycle -## 7. Anti-patterns +Read resolved colors from the screen root rather than `document.documentElement`. Separate +text and grid from series roles. Pass concrete CSS colors to the library; use the browser +to resolve CSS expressions when necessary. Extend the project theme instead of distributing +color literals through chart options. -- More than 4 lines, 5 pie categories, or 3 gauge pointers (`design-rules.md` 6.2). -- 3D anything, dual axes, rainbow sequential scales, over-smoothing (6.5). -- A chart smaller than 160 x 100 with axes and labels still drawn. -- A legend parked in a corner, detached from the marks it names (6.1). -- Axis lines or labels rendered in a data colour — the data layer must outrank the frame. -- A chart that re-inits on theme change (`dispose()` + `init()`), which flashes and loses state. -- Chart animation driven by `--dvk-motion-duration`. -- Decoration inside the plot area with no data mapping (5.1) — including a gradient that pretends - to be a value scale. +Keep charts transparent when a parent supplies the surface, and headings in the DOM. +Measure actual content space and set plot bounds. Official component documentation owns +the safe-area contract; do not assume uniform padding across frames. -## 8. Performance guards +Use ResizeObserver with animation-frame scheduling. Dispose charts, observers and pending +frames on teardown. SVG is a useful default for modest ECharts datasets under CSS scaling; +inspect pixel density when choosing canvas. -| Guard | Setting | Why | -| --- | --- | --- | -| Renderer | SVG under `dvk-fit-screen` scaling; canvas only above ~1k points | a CSS `transform: scale()` blur is not repairable by `resize()` in echarts 6.1.0 | -| Downsampling | `sampling: 'lttb'` on line series | keeps trend and extrema when points far exceed pixels; off by default | -| Large mode | `large: true` + `largeThreshold` (bar 400, scatter 2000) | only activates at the threshold; above it per-item styles and labels are dropped, so it is a last resort | -| Progressive | `progressive` / `progressiveThreshold` (default 3000) | renders in chunks instead of blocking the frame | -| Animation | `animationThreshold: 2000`; `animation: false` under `prefers-reduced-motion` | large scenes skip animation automatically | -| Resize | `ResizeObserver` + `requestAnimationFrame`, `disconnect()` on teardown | `window.resize` never fires for panel or fit-screen changes | +Update existing charts while preserving user selection. The optional modules target +ECharts 6.1.0; their `setTheme` usage is not a compatibility promise for older versions. +Disable transitions for reduced motion. -## 9. Theme switching +## Data and state -- Switch with `chart.setTheme(themeObject)`. ECharts does not observe CSS variable changes, so the - class change alone repaints nothing — re-read the tokens and call `setTheme`. -- Never `dispose()` + `init()` to change theme: it flashes, re-runs the entry animation, and a - repeated `init()` on the same DOM returns the old instance without a word. -- Under `dvk-fit-screen` scaling prefer the SVG renderer. echarts 6.1.0 `resize()` does not refresh - device pixel ratio, so a canvas chart that changes scale needs a full re-init. -- One screen, one theme (`design-rules.md`). Chart colour comes from that theme only. +Derive totals, percentages and comparisons from the same dataset. Unknown values are not +zero. Show update time and implement loading, empty, failed and stale states. Retained old +data needs explicit stale labeling; retries must perform an actual state transition. -## 10. Reference templates +For standalone HTML, embed scene data and pin external libraries. The four screen examples +support `?state=loading|empty|failed|stale` for verification; normal operation defaults to ready. +This is an example convention, not a new datav-kit API. -All seven export `createXxx(el, data, tokens)` and return -`{ chart, update, setState, setTheme, dispose }`. `tokens` defaults to a read off `el`. +## Performance and validation -| File | Factory | Data shape | Guard | -| --- | --- | --- | --- | -| `assets/charts/line-area.js` | `createLineArea` | `{ labels, series: [{ name, data }] }` | ≤ 4 series; `sampling: 'lttb'`; `null` breaks the line | -| `assets/charts/bar-rank.js` | `createBarRank` | `{ items: [{ name, value }], unit }` | value axis from 0; `large` at 400 items | -| `assets/charts/pie-doughnut.js` | `createPieDoughnut` | `{ items: [{ name, value }] }` | ≤ 5 categories; `minAngle`, `avoidLabelOverlap` | -| `assets/charts/scatter.js` | `createScatter` | `{ series: [{ name, points: [[x, y, size?]] }] }` | `large` at 2000 points | -| `assets/charts/gauge.js` | `createGauge` | `{ value, min, max, unit, label }` | one pointer; never more than 3 | -| `assets/charts/radar.js` | `createRadar` | `{ indicators: [{ name, max }], series: [{ name, values }] }` | ≤ 5 indicators | -| `assets/charts/heatmap.js` | `createHeatmap` | `{ xLabels, yLabels, cells: [[x, y, value]] }` | two category axes; single-hue ramp; progressive | +Verify visible chart content, text fit and data selection at delivery dimensions. Apply +sampling or progressive rendering when dataset size requires it. Use stable positions for +monitoring topologies. Record performance with browser and viewport context, and verify +pause, visibility changes and reduced motion for continuously animated visuals. diff --git a/skills/datav-kit/references/components.md b/skills/datav-kit/references/components.md deleted file mode 100644 index 5b3bbe0..0000000 --- a/skills/datav-kit/references/components.md +++ /dev/null @@ -1,175 +0,0 @@ -# Components — Availability, Capabilities, and Detail Routing - -The component layer of the datav-kit knowledge base. It answers **"is this component here, what -can it do, and where is its detail page"**. - -**This file owns** - -- the publication status list and the runtime availability check; -- border-box capability fields (which variant supports what); -- the online-first fetch protocol and the `main`-branch fallback. - -**This file does not own** - -- the border-box **role** matrix — that lives in `patterns.md` §3 (P6) and must not be duplicated - here; -- generic redlines and thresholds → `design-rules.md`; -- token values → `tokens.md` / `assets/tokens.css`. - -## 1. Runtime availability is the authority - -The status list below is a planning aid. At runtime, availability is decided by feature -detection **after** the element package has finished registering: - -```js -await import('@datav-kit/elements@0.0.5') // or the CDN import map entry -customElements.get('dvk-border-box-10') // truthy → registered -``` - -- Never pass a prop to a component that is not registered. -- Never decide availability from the static list when the runtime check is possible. -- If every candidate in a fallback chain is unavailable, **stop and report the missing package** - — do not substitute an arbitrary element. - -The static list exists because the planning phase has no runtime: before the screen is running, -an agent cannot call `customElements.get`, yet it still has to pick components. - -## 2. Publication status - -| Status | Count | Components | -| --- | --- | --- | -| Published (`@datav-kit/elements@0.0.5`) | 30 | `dvk-border-box-1` … `dvk-border-box-15`, `dvk-decoration-1` … `dvk-decoration-11`, `dvk-count-to`, `dvk-fit-screen`, `dvk-loading-energy`, `dvk-loading-orbit` | -| `main` branch only | 5 | `dvk-title-1`, `dvk-title-2`, `dvk-title-3`, `dvk-border-box-16`, `dvk-performance-monitor` | -| Nonexistent | — | `dvk-title-4` — an empty directory in the source tree; it does not exist and must never be referenced | - -`main`-only components are absent from the published package and from the live documentation -site. The live `llms.txt` index does not list them either. - -`dvk-performance-monitor` is a development-time diagnostics overlay (FPS plus a 0–100 pressure -score). It is not a screen-region component; do not place it in a large-screen layout. - -## 3. Border-box capability fields - -All 16 variants share the same content-area contract (§4) and expose **only the default slot** — -`frame`, `graphic`, and `content` are parts, not slots. Role selection is in `patterns.md` §3. - -| Variant | `background-color` | `animated` / `paused` | `auto-height` | `glow-intensity` default | Content-area source | Publication | -| --- | --- | --- | --- | --- | --- | --- | -| `dvk-border-box-1` | no | yes / yes | **yes** (only variant) | — | `contentRect` | published | -| `dvk-border-box-2` | no | no | no | `1` | `contentRect` | published | -| `dvk-border-box-3` | no | no | no | `1` | `contentRect` | published | -| `dvk-border-box-4` | no | no | no | `1` | `contentRect` | published | -| `dvk-border-box-5` | no | no | no | `1` | `contentRect` | published | -| `dvk-border-box-6` | no | no | no | `1` | `contentRect` | published | -| `dvk-border-box-7` | yes (default `transparent`) | no | no | — | `contentRect` | published | -| `dvk-border-box-8` | yes (default `transparent`) | yes / yes | no | — | `contentRect` | published | -| `dvk-border-box-9` | yes (default `transparent`) | no | no | — | `contentRect` | published | -| `dvk-border-box-10` | yes (default `transparent`) | yes / yes | no | — | `contentRect` | published | -| `dvk-border-box-11` | no | yes / yes | no | `1` | `contentRect` | published | -| `dvk-border-box-12` | no | yes / yes | no | `1` | `contentRect` | published | -| `dvk-border-box-13` | no | yes / yes | no | `1` | `contentRect` | published | -| `dvk-border-box-14` | no | yes / yes | no | `1` | `contentRect` | published | -| `dvk-border-box-15` | yes (default `transparent`) | no | no | — | `contentRect` | published | -| `dvk-border-box-16` | no | yes / yes | no | `0.7` | `contentRect` | `main` only | - -Reading the table: - -- **`background-color`** — the surface capability. Only `7/8/9/10/15` expose it; the rest are - transparent HUD, status, or compact frames that take their background from the host or screen. -- **`animated` / `paused`** — motion control. `1/8/10/11/12/13/14/16` support both; `2/3/4/5/6/7/9/15` - are static. `border-box-1` also takes `animated=false`. -- **`auto-height`** — `border-box-1` only. Every other variant stretches to its parent height. -- **`glow-intensity`** — a multiplier on the glow, default `1` where present and `0.7` on - `border-box-16`. Keep the default unless a visual-hierarchy deviation is registered. -- **`accent-color`** — present on `2/3/4/5/6/11/12/13/14/16` (and as the third entry of - `colors`). Not a selection criterion on its own. -- **Publication** — `border-box-16` is a `main`-only optional enhancement; the chain in - `patterns.md` §3 already accounts for it. - -`colors` is a comma-separated string in the order primary, secondary[, accent] — the same for all -16 variants. - -## 4. Content-area source - -Every border box derives its content inset from its `contentRect` — the safe rectangle declared -in the component's own SVG coordinate system, mapped to the measured host size. This is the -default model; fixed padding values are not. - -Precedence for the content inset: - -```txt ---dvk-border-box-N-padding -> --dvk-border-box-padding -> computed contentRect padding -``` - -- The computed inset **is** the safe distance between content and frame. It differs per variant - and per host size, so its numbers are an implementation detail, not an authoring contract. -- Keep it. When content needs more room than the inset gives — the small variants bottom out at - 10px — add padding on an **inner wrapper** (`.panel-inner`, `.kpi-card`, a marker's label - plate), never on the component. Prefer horizontal padding: a fixed-height panel rarely has - vertical slack. An inset override moves every child, including the ones that were already - clear, and can pull content closer to the frame than the safe distance. -- An override is justified only by an observed obstruction, overflow, or readability problem, - and is recorded as a deviation. -- Padding numbers must never be used to infer which variant is in play. - -## 5. Detail routing — online first - -The knowledge base is online-first; there is no offline bundle. - -| Need | Action | -| --- | --- | -| "Should I use this component", its tag name, its doc URL | the index is enough | -| Writing `props` / `events` / CSS variables / `::part()` | **fetch the detail page — mandatory** | -| The same component again in the same session | do not re-fetch | -| The index lacks the component, or the page 404s | use the fallback source (§6) and label it | - -Sources: - -```txt -index: https://hackycy.github.io/datav-kit/llms.txt -detail: https://hackycy.github.io/datav-kit/components//.md -fallback: https://raw.githubusercontent.com/hackycy/datav-kit/main/docs/components//.md -``` - -`` is `borders`, `decorations`, `titles`, or `other`. - -The index carries the tag, a one-line purpose, and the doc URL. It does **not** carry prop names — -never write a prop from memory or from a pattern's example; fetch the detail page. - -## 6. `main`-branch fallback - -Trigger: the component is absent from `llms.txt`, or its detail page returns 404. - -```txt -https://raw.githubusercontent.com/hackycy/datav-kit/main/docs/ -``` - -The fetched content comes from the `main` branch and may not be published. When a skill or -project uses it, the source must be stated explicitly: - -> from `main` — the published npm package may not include this component yet - -Apply the label wherever the component is recommended, so nobody is guided to a component they -cannot install. This is the documented route for `dvk-title-1/2/3`, `dvk-border-box-16`, and -`dvk-performance-monitor`. - -## 7. Capability notes for non-border-box components - -These are capability facts, not placement rules. - -- **`dvk-title-1/2/3`** (`main` only): isomorphic props — `color`, `secondary-color`, - `accent-color`, `colors`, `title-text`. The default slot and `title-text` are mutually - exclusive; parts are always `content` / `title` / `title-text`. Absent from 0.0.5, so a screen - must feature-detect and fall back (see `patterns.md` P3). -- **`dvk-decoration-4`** and **`dvk-decoration-8`**: the only slot-bearing decorations — they can - frame compact content. -- **`dvk-decoration-5/6/7/9`**: support `reverse`, documented for symmetric title or divider - pairings. -- **`dvk-count-to`**: the only count-up primitive. Numeric props are attributes and are coerced - from strings; `prefix` / `suffix` slots win over the same-named props. -- **`dvk-fit-screen`**: the only scaling element. Defaults `width=1920 height=1080 - mode=contain align=center center fit-target=viewport`. -- **`dvk-loading-energy`** / **`dvk-loading-orbit`**: loading states for panels and empty - regions (`size` defaults 72 and 50). diff --git a/skills/datav-kit/references/design-rules.md b/skills/datav-kit/references/design-rules.md index 233f870..264d580 100644 --- a/skills/datav-kit/references/design-rules.md +++ b/skills/datav-kit/references/design-rules.md @@ -1,144 +1,44 @@ -# Design Rules — Large-Screen Dashboards - -The single authority for design redlines and advisory values. Every rule below is derived -1:1 into a review rubric item (see [Rubric derivation](#rubric-derivation)). - -**Tiers** - -- `[redline]` — must not be violated. Binary pass/fail; any failure fails the case. -- `[advisory]` — may be deviated from, but the deviation must be registered in the project's - deviation ledger. An unregistered deviation is a defect. - -🅰 marks an accessibility cross-rule. Accessibility has no separate group; it constrains the -groups where it applies and is summarised in [Accessibility cross-rules](#accessibility-cross-rules). - -**Source labels** (recorded per entry; cited thresholds and self-defined thresholds are -distinguished): - -| Label | Meaning | -| --- | --- | -| `[standard]` | normative standard, primary text verified | -| `[standard-2nd]` | standard clause obtained via third-party transcription, not the official text | -| `[guide]` | official guidance from a vendor or organisation | -| `[research]` | academic study or measured result | -| `[repo]` | fact measured in this repository | -| `[custom]` | self-defined threshold — no external standard exists | -| `[derived]` | derived from another entry in this file | - -**Checkability**: only quantified thresholds are listed. Entries marked `mech` can be checked -mechanically (a script or a count); `review` needs a human or model judgement. Purely stylistic -descriptions are stated as prose advice, not as thresholds. - ---- - -## 1. Canvas and Grid - -| # | Tier | Rule | Threshold / criterion | Source | Check | -| --- | --- | --- | --- | --- | --- | -| 1.1 | `[redline]` | Proportional scaling | No non-uniform stretch; a screen with text or line work must not use `fill` | `[derived]` geometry class | review | -| 1.2 | `[redline]` | Content safe area | Content sits inside `contentRect`; it must not cover decorative line work | `[repo]` architecture contracts | mech | -| 1.3 | `[redline]` | No overflow or clipping | No block content truncated by `overflow` at the design canvas | `[repo]` 26px overflow found in an existing demo | mech | -| 1.4 | `[advisory]` | Safe margin | `48px` default; video wall or overscan risk tightens to 5% (`96px` horizontal / `54px` vertical) | `[custom]` — no standard gives px values | mech | -| 1.5 | `[advisory]` | Grid | 12 columns / gutter `24px` / margin `48px`, on the 8pt grid | `[custom]` | mech | -| 1.6 | `[advisory]` | Fluid row height | Main region uses `minmax(0, 1fr)`, never fixed px rows | `[repo]` | mech | -| 1.7 | `[advisory]` | Sub-pixel line width | In scaled scenes use `vector-effect: non-scaling-stroke`; do not rely on 1px CSS borders | `[standard]` CSS Transforms — `scale()` is post-layout | mech | -| 1.8 | `[advisory]` | Canvas | 1920 x 1080, `mode="contain"` + `align="center center"`; outside canvas space is filled with an ambience layer, not dead black | `[derived]` | mech | -| 1.9 | `[advisory]` | Degradation | Ultra-wide 21:9/32:9: keep the 16:9 content area centred, fill the sides with an auxiliary band or ambience — never stretch or crop. Video wall: fixed height 1080, width = units x 1920, fine elements (<2px line, <16px text) avoid the seams. Portrait: **not supported**. Screen <=1440: preview only, never a delivery target | `[custom]` — no standard for 21:9 | review | - -## 2. Spacing and Rhythm - -| # | Tier | Rule | Threshold / criterion | Source | Check | -| --- | --- | --- | --- | --- | --- | -| 2.1 | `[advisory]` | Spacing scale | 8pt grid: `4/8/12/16/24/32/48/64/96` (token set in `tokens.md`) | `[guide]` Carbon 8px unit; Ant Design `sizeUnit` 4 | mech | -| 2.2 | `[advisory]` | Screen rhythm | Screen margin `48` / block gap `24` / panel gap `16` / panel padding `24` / header `104` / KPI strip `104` | `[custom]` | mech | -| 2.3 | `[advisory]` | Modules per screen | 5–9 modules | `[guide]` Ant Design | mech | -| 2.4 | `[advisory]` | Whitespace rhythm | The gap between adjacent blocks is at least the smallest spacing step used inside either block | `[derived]` | mech | - -## 3. Typography and Hierarchy - -| # | Tier | Rule | Threshold / criterion | Source | Check | -| --- | --- | --- | --- | --- | --- | -| 3.1 | `[redline]` 🅰 | Minimum font size | Physical character height = viewing distance / 200; coloured characters >= 21 arcmin (30 arcmin recommended); shortcut: max viewing distance = 215 x character height | `[standard-2nd]` T/CIDADS 00011-2022; `[standard]` ISO 9241-3 §6.4 | review | -| 3.2 | `[redline]` 🅰 | Key data is not hover-only | Primary metrics are statically visible | `[guide]` OpenAI quality gate | review | -| 3.3 | `[advisory]` | Font families | <= 2 families on one screen | `[standard-2nd]` T/CIDADS | mech | -| 3.4 | `[advisory]` | Type levels | 3–5 levels | `[guide]` Ant Design | mech | -| 3.5 | `[advisory]` | Weights | 400 / 500 for most text; 600 for Latin bold | `[guide]` Ant Design | mech | -| 3.6 | `[advisory]` 🅰 | Line length | <= 40 CJK characters per line; no justified alignment | `[standard]` WCAG 1.4.8 | mech | -| 3.7 | `[advisory]` 🅰 | Line height | >= 1.5x font size; paragraph spacing >= 2x | `[standard]` WCAG 1.4.12 | mech | - -## 4. Color Roles - -Colors come only from the theme's `--dvk-color-*` values. **No second color source** — no -hard-coded palette, no separate chart palette file. - -| # | Tier | Rule | Threshold / criterion | Source | Check | -| --- | --- | --- | --- | --- | --- | -| 4.1 | `[redline]` 🅰 | Contrast floor | Body text >= 4.5:1; large text >= 3:1 (large = >= 18pt / 14pt bold / CJK equivalent, approx 24px / 18.5px); non-text and state indicators >= 3:1. **Never round**: 4.499:1 is not 4.5:1 | `[standard]` WCAG 2.2 SC 1.4.3 / 1.4.11 | mech | -| 4.2 | `[redline]` | No role overloading | One hue must not carry several unrelated meanings | `[guide]` OpenAI | review | -| 4.3 | `[advisory]` | Colour role ledger | 10 roles: neutral background / primary focus / secondary contrast / ordered magnitude / positive-negative change / alert-error / selected / hover-focus / missing-uncertain / disabled or expired | `[guide]` OpenAI | review | -| 4.4 | `[advisory]` | Colour count | <= 5 categorical data colours; <= 2 interface accent colours; semantic colours counted separately | `[custom]` | mech | -| 4.5 | `[advisory]` | Contrast ceiling | Body text sits in 7:1–15:1; dark scenes avoid pure white text on pure black | `[custom]`; `[standard]` MIL-STD-1472H 6:1–10:1, FAA/NATS > 15:1 discomfort | mech | -| 4.6 | `[advisory]` | Colour scale | Sequential/diverging scales must be perceptually uniform; **no rainbow scale** | `[research]` Moreland 2009 | mech | -| 4.7 | `[advisory]` | White area | <= 40% of the screen area | `[standard-2nd]` T/CIDADS | mech | -| 4.8 | `[advisory]` | Adjacent mark contrast | When a boundary itself carries meaning, also check contrast between adjacent data marks | `[guide]` OpenAI | mech | - -## 5. Decoration and Motion Budget - -| # | Tier | Rule | Threshold / criterion | Source | Check | -| --- | --- | --- | --- | --- | --- | -| 5.1 | `[redline]` | Decoration must be semantic | Every glow / pulse / halo / blur / particle / shimmer / animation must have a **named data or interaction mapping**. If it is only "pretty", delete it | `[guide]` OpenAI | review | -| 5.2 | `[redline]` 🅰 | Motion is controllable | Autoplay motion longer than 5s must be pausable/stoppable/hideable; flashing <= 3 times per second; `prefers-reduced-motion` is honoured | `[standard]` WCAG 2.2.2 / 2.3.1 / 2.3.3 | mech | -| 5.3 | `[advisory]` | Duration tiers | Interaction 150–300ms (`--dvk-screen-duration-*`); decoration follows the theme's `--dvk-motion-duration` (2200–2600ms); charts 200–400ms | `[guide]` Carbon / Ant Design / NN/g; `[repo]` theme token range | mech | -| 5.4 | `[advisory]` | Decoration budget | <= 1 decoration container + 1 decoration track per block; decoration visual weight stays below the data layer of the same block. Defined by hierarchy, not by element count | `[custom]` | review | -| 5.5 | `[advisory]` | Anti-AI ambience | Refuse broad brush strokes, wispy ribbons, bokeh/orbs, cinematic wallpaper, one-hue drama, decorative gradients, unmotivated particles | `[guide]` OpenAI | review | -| 5.6 | `[advisory]` | Gradient and glow | No gradient as a substitute for a sequential palette; no glow as a substitute for shadow/elevation | `[guide]` Carbon / Material | review | - -## 6. Data Presentation and Exception States - -| # | Tier | Rule | Threshold / criterion | Source | Check | -| --- | --- | --- | --- | --- | --- | -| 6.1 | `[advisory]` | Label directly | Direct labels beat a detached legend; a legend that is used must sit next to the chart | `[guide]` OpenAI / Carbon / Datawrapper | review | -| 6.2 | `[advisory]` | Mark count | <= 4 lines; <= 5 pie categories; <= 3 gauge needles | `[guide]` ECharts Handbook | mech | -| 6.3 | `[advisory]` | Legend height | <= 30% of chart height | `[guide]` Carbon | mech | -| 6.4 | `[advisory]` | Axis origin | Bar charts start the y-axis at 0; line charts need not | `[guide]` Datawrapper | mech | -| 6.5 | `[advisory]` | Forbidden chart forms | No 3D pie/bar, no dual axis, no rainbow scale, no over-smoothing | `[research]` Wilke; `[guide]` Datawrapper; `[research]` Moreland | mech | -| 6.6 | `[advisory]` | Four exception states | No data / loading / failed / stale — every state has an explicit presentation | `[guide]` OpenAI | mech | -| 6.7 | `[advisory]` | Real-time data | Prefer stale-but-visible over a blank chart; show the last update time and a live/stale/offline/partial state | `[guide]` OpenAI | review | - ---- - -## Accessibility cross-rules - -These are the 🅰 entries above, collected. They are redlines wherever the group marks them -`[redline]`; the remaining ones are advisory with a registered-deviation path. - -| Rule | Threshold | Source | -| --- | --- | --- | -| Contrast (4.1) | body >= 4.5:1, large text >= 3:1, non-text >= 3:1, no rounding | WCAG 2.2 SC 1.4.3 / 1.4.11 | -| Minimum font size (3.1) | viewing distance / 200; coloured characters >= 21 arcmin | T/CIDADS 00011-2022; ISO 9241-3 §6.4 | -| Key data not hover-only (3.2) | primary metrics statically visible | OpenAI quality gate | -| Motion controllable (5.2) | > 5s pausable, flashing <= 3/s, reduced motion honoured | WCAG 2.2.2 / 2.3.1 / 2.3.3 | -| Keyboard reachable | all interactive elements reachable by keyboard | WCAG 2.1.1 | -| Focus appearance | >= 2 CSS px perimeter and >= 3:1 contrast | WCAG 2.4.13 | -| Text resizing | text scales to 200% without loss of content or function | WCAG 1.4.4 | -| Line length / line height (3.6, 3.7) | <= 40 CJK; >= 1.5x | WCAG 1.4.8 / 1.4.12 | - -## One theme per screen - -A screen uses exactly **one** theme class (`.dvk-theme-*`). Mixing several theme classes on one -screen is forbidden, including per-block themes. To emphasise a region, use the accent colour -role from the ledger — not a second theme. `[custom]` - -## Rubric derivation - -- **1:1 derivation**: every rule above becomes exactly one rubric item, carrying its tier. The - free zone (business copy, chart type and option inside a block, pattern substitution inside a - block) is not scored. -- **Redline item = binary**: pass / fail. **Any failed redline fails the case.** -- **Advisory item = three levels**: meets / registered deviation / unregistered deviation. An - unregistered deviation is a defect. -- **No total score**: averaging hides a failed redline. -- **Writeback rule**: every failed redline and every unmet advisory value must be turned into a - skill revision item, otherwise the review is not complete. -- The self-check list is derived 1:1 from these entries and must be able to detect hard-coded - values in the implementation. +# Screen Design Checks + +Use these checks on the delivered screen. Layout counts and stylistic preferences are +not universal correctness rules; adjust them to the business and viewing conditions. + +## Required delivery checks + +- Scale the canvas proportionally. Keep text and data inside the viewport and documented + component content areas; inspect the rendered result for clipping. +- Make primary metrics visible without hover. Calibrate text for the intended hardware + and viewing distance; check long labels, large values and alternate datasets. +- Check contrast against the composited background: WCAG AA uses 4.5:1 for normal text, + 3:1 for large text and 3:1 for meaningful non-text indicators, unless an equivalent + non-color cue supplies the information. Decorative linework is not a data indicator. +- Provide accessible names, keyboard access and visible focus. Honor reduced motion, + offer pause for ongoing animation, and avoid flashing. +- Distinguish loading, empty, failed and stale data. Show units and update time; reconcile + aggregate metrics with plotted datasets. +- Keep stable dimensions during updates. Verify chart/scene pixels, not just the existence + of a canvas or SVG node. + +`assets/tools/contrast-check.js` can check numerical contrast. Screenshots are still needed +to evaluate layering and text over geographic or 3D content. + +## Design review + +- Establish a primary visual and reading order. Supporting regions should answer the same + business question rather than occupy arbitrary grid cells. +- Use one project theme with distinct roles for neutral text, surfaces, data, selection + and warnings. Add labels or shape cues to status colors. Official theming docs own CSS API. +- Match decoration to the scene. Static rails can establish hierarchy; motion should + explain updates or selection without competing with the information. +- Choose module count, spacing and type hierarchy by content and viewing conditions. + Avoid excessive framing, small legends and repeated oversized headings. +- Reveal the subject through maps, images and models. Record external asset provenance. +- Use accurate chart encodings: zero origin for magnitude bars, explicit units and ranges, + direct labels where practical, and perceptually ordered continuous scales. + +## Verification record + +Record viewport, delivery path, screenshots, tested interactions, data states and remaining +issues. Fix required-check failures before handoff. Explain material style tradeoffs briefly. +For unattended screens verify pause, visibility changes and cleanup. Mobile preview preserves +the large-screen composition; a mobile product redesign is a separate requirement. diff --git a/skills/datav-kit/references/patterns.md b/skills/datav-kit/references/patterns.md index f37f6c3..65a2d45 100644 --- a/skills/datav-kit/references/patterns.md +++ b/skills/datav-kit/references/patterns.md @@ -1,497 +1,40 @@ -# Composition Patterns — Large-Screen Dashboards +# Composition Guidance -The pattern layer of the datav-kit knowledge base. It answers **"how do I assemble this -block"** and is the routing main table: every screen block resolves here before a component is -chosen. +Start with the question the audience must answer at a glance. The primary data visual +determines composition; component selection follows through official documentation. -**This file owns** - -- the 19 patterns P1–P19 and their construction; -- the **P6 border-box selection matrix** — capability groups, role entries, and fixed fallback - chains. It is the single operative matrix. - -**This file does not own** - -- component capability fields, publication status, or the runtime availability check → - `components.md`; -- generic redlines and thresholds → `design-rules.md`; -- token values → `tokens.md` / `assets/tokens.css`. - -`spec.md` §3.4 carries a locked snapshot of the P6 matrix for implementation handoff only; it is -not a second, independently maintained rule set. - -## 0. Routing path - -``` -user brief - → template (T1–T4, §1) which screen layout - → pattern (P1–P19) which block structure, components, parameters - → component (components.md + the online llms.txt index) which tag, which props -``` - -| Hop | Decided by | Output | +| Business question | Primary visual | Supporting information | | --- | --- | --- | -| brief → template | the brief's job: monitoring / analysis / narrative / KPI-led | one of T1–T4 | -| template → pattern | the template's block list | the P-numbers for each block | -| pattern → component | the pattern's **Construction** field | tag + key attributes | -| component → detail | the routing protocol in `components.md` | props / events / CSS variables from the detail page | - -There is deliberately **no "task → component" mapping table**: it would duplicate this layer, -which already carries task → structure → component. Reach for the online `llms.txt` index only -for a component that no pattern covers. - -## 1. Template layer - -Shared skeleton: header `--dvk-screen-header-height` (104px), KPI strip -`--dvk-screen-kpi-height` (104px), main region `minmax(0, 1fr)`. All four templates use the -12-column grid (gutter 24, margin 48) and fluid rows. - -| # | Layout | Rows | Main region (12 col) | Blocks | Use for | -| --- | --- | --- | --- | --- | --- | -| T1 | Three-column operations | header 104 / KPI 104 / main 1fr | 3 / 6 / 3 | left P6+P7+P9 or P10 x2; centre P6+P17 + P12; right P11 / P15 / P13 x2–3 | monitoring, command, duty room | -| T2 | Two-column analysis | header 104 / KPI 104 / main 1fr | 8 / 4 | main P6 + chart slot x2; side P10 + P15 | trend, comparison, attribution | -| T3 | Single-column narrative | header 104 / main 1fr / rhythm strip 104 | full width | P6+P17 + three small panels (P9/P14/P16) | situation overview, briefing | -| T4 | KPI-led | header 104 / KPI 208 / main 1fr | 6 / 6 | top P4 enlarged KPI cards x6 (P14/P16); bottom P6+P7 panels x2 (P9/P10) | metric board, business cockpit | - -Changing the skeleton — row count, column count, primary-view position, header form, adding or -removing a block — is a redline: go back to the prototype and re-select. Substituting a pattern -inside a block is a free-zone change; each pattern lists its allowed replacements below. - -## 2. Route index - -| P | Pattern | Purpose | Primary components | Lands in | -| --- | --- | --- | --- | --- | -| P1 | Full-screen fit shell | Scale a fixed canvas into the page | `dvk-fit-screen` | all templates, outermost | -| P2 | Three-part header | Top information band | hand-written boxes + P3 | all templates | -| P3 | Title bar | Screen title as the visual focus | `dvk-title-*` or paired `dvk-decoration-9` | header centre | -| P4 | KPI strip | One row of 4–6 top-line metrics | `dvk-border-box-15` + `dvk-count-to` | T1–T4, row 2 | -| P5 | Three-column main grid | Left process / centre view / right status | `dvk-border-box-*` | T1 main region | -| P6 | Panel container | Frame + optional surface for one block | `dvk-border-box-N` (see §3) | every block | -| P7 | Panel head | "What is this panel, what state" | `h3` + `p` + P8 | panel first row | -| P8 | Panel head right zone | Count/state chip or tag group | hand-written chips | panel head right | -| P9 | Horizontal progress row | Share / completion | `dvk-count-to` + `--bar-value` bar | list rows | -| P10 | Rank / pressure row | Name + status + full-width bar | `dvk-count-to` + `--bar-value` bar | left/right column | -| P11 | Triage card | Risk / ticket / task queue | hand-written card + role colour | right column | -| P12 | Rhythm bars | Intensity by time slice | `--bar-value` columns | centre bottom / footer | -| P13 | Ring gauge | One percentage | `dvk-decoration-8` + `dvk-count-to` | narrow right panel | -| P14 | Hero metric | One block, one core number | surface panel + `dvk-count-to` | left column top | -| P15 | Metric stack | Two secondary metrics | `dvk-count-to` cards | middle rows | -| P16 | `dvk-count-to` usage | Every animating number | `dvk-count-to` | any metric slot | -| P17 | Map stage | The screen's visual lead | `dvk-border-box-*` + inline SVG | centre column | -| P18 | Map marker | A point on the map | absolute `div` + diamond | inside the map | -| P19 | Map overlay card | A conclusion over the map | absolute `section` | map bottom-right | - -## 3. P6 — border-box selection matrix (normative) - -P6 selects a border-box variant **capability first, role second**. `surface` means the component -exposes a `background-color` capability; omitting that attribute still permits a transparent -panel. `HUD` means a transparent decorative frame whose background is supplied by the host or -screen. A scene role is the container's semantic job after the capability branch — it is not a -colour theme. - -### Capability groups - -| Group | Variants | Capability boundary | -| --- | --- | --- | -| Generic rectangle | `1` | Animated rectangular frame; the only `auto-height` variant | -| Large HUD | `2/3/4/5/6` | Transparent large-format sliced/tiled HUD frames; no `background-color` | -| Surface panel | `7/8/9/10/15` | Optional independent surface through `background-color` | -| Status/precision frame | `11/12/13/14` | Transparent operational or technical rails; no `background-color` | -| Compact enhancement | `16` | Compact CPU-like KPI/topology/health frame; main-only; no `background-color` | - -### Role entries and fixed fallback chains - -Pick the preferred variant for the scene role, then take the first registered tag in the chain. -Import and registration must have completed before `customElements.get(tag)` is called. - -| Scene role | Preferred | Fixed fallback chain | -| --- | --- | --- | -| Focal primary view | `4` | `4 → 2 → 6 → 3 → 5 → 1` | -| Dense-data primary view | `3` | `3 → 6 → 5 → 2 → 1` | -| Transparent free-size HUD | `5` | `5 → 3 → 6 → 2 → 1` | -| Precision technology primary view | `6` | `6 → 3 → 5 → 2 → 1` | -| Cyber/HUD primary view | `2` | `2 → 4 → 3 → 5 → 1` | -| Generic rectangle or content-sized box | `1` | `1 → 15` | -| Chamfered surface panel | `7` | `7 → 10 → 9 → 15` | -| Animated polygon surface panel | `8` | `8 → 10 → 7 → 15` | -| Restrained static surface panel | `9` | `9 → 15 → 7 → 10` | -| Rounded glow surface panel | `10` | `10 → 9 → 15 → 7` | -| Repeated lightweight card | `15` | `15 → 9 → 10 → 7` | -| Operational status rail | `11` | `11 → 13 → 12 → 14 → 1` | -| Top rail structural frame | `12` | `12 → 13 → 14 → 11 → 1` | -| Bottom carrier-spine frame | `13` | `13 → 12 → 14 → 11 → 1` | -| Signal-port technical area | `14` | `14 → 13 → 12 → 11 → 1` | -| Compact KPI, topology, or device health | `16` | `16 → 15 → 9` | - -### Fallback rules - -- Surface and HUD families must not silently cross during fallback. `1 → 15` is the explicit - generic-rectangle fallback; omit `background-color` when the original role did not require a - surface, and record the resulting visual change. -- `16 → 15 → 9` is the explicit compact-role exception: omit `background-color` on the fallback - so the panel stays transparent, and record the loss of CPU-like geometry as a visual-contract - degradation. -- Unsupported props must not be passed to a fallback variant. -- If a chain has no registered candidate, **stop with a missing dependency** — do not invent a - component and do not silently substitute an arbitrary border. - -### Surface boundary - -When an independent surface is required, select only `7/8/9/10/15` and pass the theme's surface -value through the supported `background-color` prop. Transparent HUD and status variants take -their background from the host or the screen instead. - -### Motion rule - -Motion is not a default selection reason. Enable it only for a named data or interaction -mapping. Under `prefers-reduced-motion: reduce`, use `paused` where supported and -`animated=false` for `border-box-1`; otherwise prefer a static role variant. Keep the component -default `glow-intensity` unless a documented visual-hierarchy deviation is registered. - -### Content-area rule - -Every variant keeps the `contentRect` contract. Automatic padding is the default; only an -observed obstruction, overflow, or readability problem justifies -`--dvk-border-box-N-padding`, and that override is recorded as a deviation. Padding numbers are -never used to infer the variant. - -### Default-slot title rule - -All 16 border boxes expose **only the default slot**. `frame`, `graphic`, and `content` are -parts, not title slots — no variant has a `#header` or `#title` slot. The panel title therefore -lives inside the default slot (P7's `.panel-inner`). The left/right title rails on `-11` and the -top title rail on `-12` are decorative graphics, not text containers. - -## 4. Skeleton patterns - -### P1 — Full-screen fit shell - -- **Purpose**: scale a fixed design canvas into the page container; the only element that - handles scaling. -- **Construction**: `` wrapping the whole screen DOM. -- **Parameters**: `width="1920" height="1080" mode="contain" align="center center"`; - `fit-target="viewport"` for a full-page screen, `"host"` when embedded in a host container. - Inside the canvas, write absolute px — no responsive breakpoints. -- **Grid position**: outermost, fills the host; the host must have a resolved height or the - canvas collapses. -- **Replacements**: none — a screen has exactly one fit shell. A fixed-size kiosk output that - needs no scaling uses a plain container instead. -- **Cautions**: `auto-fullscreen` is a compatibility flag only; fullscreen must be requested - from a user gesture. `fit-target="host"` requires the host to set a height (a `clamp()` is a - common guard). Non-uniform `fill` is a redline violation. - -### P2 — Three-part header - -- **Purpose**: the top information band — left running context, centred title, right - clock/status. -- **Construction**: `header` grid of three columns → two hand-written info boxes (`span` label + - `strong` value) + the title group (P3). -- **Parameters**: height `--dvk-screen-header-height` (104px); columns - `minmax(0, 1fr)` centre with equal side columns; gap `--dvk-screen-space-xl` (24px). Info box - padding `--dvk-screen-space-lg` (16px) / `--dvk-screen-space-xl` (24px), 1px border or a 4px - accent left border — pick one per screen. -- **Grid position**: first row, fixed height, never part of the `1fr` distribution; side columns - must match the centre column height (`align-items: center`). -- **Replacements**: the left box may be dropped (title + right status only) on T3/T4; the centre - group is replaced per P3. -- **Cautions**: the 390px / 350px side widths in the existing demos are scene values, not a - rule. The eyebrow is English and the title Chinese — the shared convention of both demos, not - a redline. - -### P3 — Title bar - -- **Purpose**: the screen's main title; the highest-contrast text region on the screen. -- **Construction**: two paths, chosen by feature detection on `customElements.get('dvk-title-1')` - **after** registration. - - Path 1 — title components registered: `dvk-title-1` / `-2` / `-3`; content goes in the - default slot **or** `title-text` (mutually exclusive). - - Path 2 — not registered (the published 0.0.5 case): a hand-built pair of mirrored - `dvk-decoration-9` (standard track) plus a centred title block (`span` eyebrow + `h1`). -- **Parameters**: track pair — the left instance carries `reverse`; `colors` is a three-colour - theme string; height 58–64px; opacity <= 0.85. Title block — centred, padding - `--dvk-screen-space-lg` (16px) / `--dvk-screen-space-2xl` (32px), a 1px rule above and below - at low alpha; `h1` at `--dvk-screen-font-size-xl` (44px). -- **Grid position**: header centre column, horizontally centred; the two tracks occupy the - remaining space symmetrically. -- **Replacements**: Path 1 ↔ Path 2 by availability; `dvk-decoration-9` ↔ `dvk-decoration-6`; - a compact title with no tracks on T4. -- **Cautions**: `reverse` is documented for symmetric title/divider layouts, so the paired - mirror is evidence-based, not incidental. The 1px rules and translucent background in both - demos are hand-written, not a component capability. Absolutely positioning the tracks (one - demo does) overlaps the title on a narrow canvas — use grid columns. Title 1–3 are main-only - and absent from 0.0.5; `title-4` does not exist and must never be referenced. - -### P4 — KPI strip - -- **Purpose**: one row of 4–6 top-line metrics. -- **Construction**: `section` grid → one card per metric → `dvk-count-to` inside each card. Card - frame: `dvk-border-box-15` (the "repeated lightweight card" role), or `dvk-decoration-4` (the - only slot-bearing decoration) for the diamond KPI look. -- **Parameters**: `grid-template-columns: repeat(N, minmax(0, 1fr))`; column gap - `--dvk-screen-space-2xl` (32px); row height `--dvk-screen-kpi-height` (104px); value - `--dvk-count-to-font-size` = `--dvk-screen-font-size-lg` (32px); label - `--dvk-screen-font-size-xs` (14px); the card's inner wrapper carries - `padding: 0 --dvk-screen-space-lg` — the frame's inset bottoms out at 10px, which otherwise - puts the label and value too close to the frame edge. -- **Grid position**: second row, fixed height, equal cards, not part of the `1fr` distribution. -- **Replacements**: T4 enlarges the strip to 208px and uses P14/P16 for six cards; on an - analysis screen the strip may be replaced by P15. -- **Cautions**: both existing demos hand-write the card with no component — a cost tradeoff, - not a rule. Per-card colour must carry a role (design-rules 4.2), not decoration. - -### P5 — Three-column main grid - -- **Purpose**: the main region — left process/list, centre primary view, right status/queue. -- **Construction**: `main` grid → three column containers, each a grid holding `dvk-border-box-*` - panels. -- **Parameters**: `grid-template-columns: L minmax(0, 1fr) R; column-gap` - `--dvk-screen-space-2xl` (32px); rows `minmax(0, 1fr)`; every column and panel needs - `min-height: 0`. -- **Grid position**: fills the remaining height; the centre column is always `minmax(0, 1fr)`. -- **Replacements**: T2 uses `8 / 4`, T3 a single column, T4 `6 / 6`. -- **Cautions**: fixed px row heights overflow the canvas — one demo loses 26px to - `overflow: hidden`; always use fluid rows (design-rules 1.6). Missing `minmax(0, ...)` or - `min-height: 0` lets content blow the grid out. - -## 5. Panel patterns - -### P6 — Panel container - -- **Purpose**: carry one block of business content; provide frame, corners, and an optional - surface. -- **Construction**: `` wrapping `.panel-inner` in the default slot. The - variant comes from the matrix in §3 — never from taste or from a demo. -- **Parameters**: `colors` is a comma-separated string ordered primary, secondary[, accent]; - surface variants take `background-color`; content inset is derived from `contentRect` by - default. `.panel-inner` is `grid-template-rows: auto minmax(0, 1fr)` with gap - `--dvk-screen-space-lg` (16px). -- **Grid position**: any grid cell; the element is `height: 100%` / `width: 100%` by default. A - content-sized box uses `dvk-border-box-1` with `auto-height`. -- **Replacements**: only through the fixed fallback chain of the selected role (§3). -- **Cautions**: all 16 variants expose only the default slot, so the panel title goes inside it - (P7). Never pass a prop the fallback variant does not support. Padding overrides require an - observed obstruction, overflow, or readability problem plus a registered deviation; never - infer the variant from padding numbers. - -### P7 — Panel head - -- **Purpose**: the panel's top information band — "what is this panel, what state is it in". -- **Construction**: `header.panel-heading` (`display: flex; justify-content: space-between`) → - left `div` with a `p` eyebrow and an `h3` title, plus the optional right zone (P8). -- **Parameters**: gap `--dvk-screen-space-md` (12px)–`--dvk-screen-space-lg` (16px); eyebrow - `--dvk-screen-font-size-sm` (18px) at reduced alpha; title `--dvk-screen-font-size-md` - (24px) at weight 600; the gap to the body comes from `.panel-inner`. -- **Grid position**: the first row of the panel's inner grid (`auto`); the body takes - `minmax(0, 1fr)`. -- **Replacements**: the right zone may be dropped; the eyebrow may be dropped on compact cards; - a chart title may replace the heading when the panel holds a single chart. -- **Cautions**: one demo uses `` instead of `

` and omits the right zone — the - normative form is `p` + `h3`, with the right zone optional. The heading stays inside the - default slot: no border box has a `#header` or `#title` slot. The demos' 16px eyebrow is not a - token step; use 18px or 14px. - -### P8 — Panel head right zone - -- **Purpose**: a count/state or a group of category chips at the right of the panel head. -- **Construction**: a hand-written `span` (status chip) or a `div` with child `span`s (tag - group). -- **Parameters**: chip `padding: 5px 9px`, 1px border, `--dvk-screen-font-size-xs` (14px), - translucent background from a `--dvk-color-*` role; tag group `flex-wrap: wrap; - justify-content: flex-end; gap` `--dvk-screen-space-sm` (8px). -- **Grid position**: the right end of the panel head, `flex: 0 0 auto`. -- **Replacements**: chip ↔ tag group; either may be dropped on compact cards. -- **Cautions**: chip text is business copy or a state token, not part of the design contract. - Cap the chip count — overflow must scroll or be summarised. - -## 6. Metric patterns - -### P9 — Horizontal progress row - -- **Purpose**: one row of "label + value + bar" for share or completion. -- **Construction**: a hand-written `div.progress-line` with an `` fill; the value uses - `dvk-count-to`. -- **Parameters**: track `height: 8px; overflow: hidden` on a low-alpha `--dvk-color-*` - background; fill width from an inline CSS variable `--bar-value`; fill colour from the theme; - value `--dvk-count-to-font-size` = `--dvk-screen-font-size-sm` (18px). -- **Grid position**: a list row inside a panel, full row width (`grid-column: 1 / -1`). -- **Replacements**: P10 when the row needs a rank, name, or status; P12 when the same data is - time-sliced. -- **Cautions**: one demo mixes an inline `width` with `--bar-value` in the same file — use the - CSS variable only, so the bar stays themeable and animatable. Never bake the value into a - hard-coded gradient. - -### P10 — Rank / pressure row - -- **Purpose**: name + status word + full-width bar — hub pressure, area load. -- **Construction**: an `article` grid → name zone (`strong` code + `span` name) + `em` status + - a full-width `.progress-line`. -- **Parameters**: `grid-template-columns: minmax(0, 120px) minmax(0, 1fr)`; the bar spans - `1 / -1`; code at `--dvk-screen-font-size-md` (24px); row padding `--dvk-screen-space-md` - (12px) / `--dvk-screen-space-lg` (16px) with a 1px border and translucent background; list - `display: grid; gap` `--dvk-screen-space-md` (12px). -- **Grid position**: a left or right column panel body, stacked, `align-content: start`. -- **Replacements**: P9 for a plain progress row; P11 for a triage queue with time and severity. -- **Cautions**: the list must clip (`overflow: hidden`) or it overflows the panel. Row height is - content-driven, so cap the number of rows per panel instead of fixing the height. - -### P11 — Triage card - -- **Purpose**: a risk, ticket, or task queue coloured by severity. -- **Construction**: an `article` grid → severity badge `b` + text zone (`strong` title + `span` - meta) + an optional right `time`. -- **Parameters**: `grid-template-columns: 42px minmax(0, 1fr) 52px; gap` - `--dvk-screen-space-md` (12px); padding `--dvk-screen-space-md` (12px) / - `--dvk-screen-space-lg` (16px); `border-left: 3px solid `; the severity colour - comes from a `--dvk-color-*` role. -- **Grid position**: a right column panel body ("dispatch" / "queue"), stacked. -- **Replacements**: P10 when there is no severity dimension; P15 when the items are metrics - rather than events. -- **Cautions**: this is the closest structural match between the two demos — treat it as - verified. Badge and time column widths are scene values. Severity colour must map to a named - role (design-rules 4.2), not to decoration. - -### P12 — Rhythm bars - -- **Purpose**: intensity by time slice — next N hours, day-long traffic. -- **Construction**: a `div` grid of N equal slices → `article` - (`grid-template-rows: auto minmax(0, 1fr) auto`) → top `time`, bar slot `div > i`, bottom - label. -- **Parameters**: `repeat(N, minmax(0, 1fr)); gap` `--dvk-screen-space-md` (12px); bar slot - `min-height: 72px`; fill `height: var(--bar-value)` with a theme-derived gradient; bottom - label `--dvk-screen-font-size-xs` (14px). -- **Grid position**: the centre column's bottom row (T1, ~190px) or the footer strip (T3, - 104px). -- **Replacements**: P9 when the data is a share rather than a time series; the strip may be - dropped on T2/T4. -- **Cautions**: both demos implement it identically — a verified shared pattern. The bar slot - needs `min-height` or it collapses under `minmax(0, 1fr)`. Slice count and gradient direction - are scene values. - -### P13 — Ring gauge - -- **Purpose**: a single percentage — sync rate, health. -- **Construction**: `` with `` in its default slot. -- **Parameters**: `colors` from the theme; `dur` 4–6s; a near-square host (128 x 128px); the - inner `dvk-count-to` at `--dvk-count-to-font-size: 18px`, weight 600, affix 0.58em. Outer - layout `grid-template-columns: 128px minmax(0, 1fr)` with the caption on the right. -- **Grid position**: a narrow bottom panel of the right column. -- **Replacements**: `dvk-decoration-10` or `-11` for a larger circular visual; a `gauge` chart - when the value needs a scale. -- **Cautions**: `dvk-decoration-8` is the only slot-bearing ring — size the slotted content to - the hollow. It appears once in the demos, so verify it visually before reusing it at another - size. Under `prefers-reduced-motion`, set `paused`. - -### P14 — Hero metric - -- **Purpose**: one block carrying a single core number. -- **Construction**: a surface panel (P6 role "rounded glow surface panel") + `span` label + - `dvk-count-to` + `p` caption. -- **Parameters**: `dvk-count-to` `--dvk-count-to-font-size` = `--dvk-screen-font-size-hero` - (56px), weight 600, affix 0.42em in a role colour; panel padding - `--dvk-screen-space-xl` (24px). -- **Grid position**: a fixed-height top row of the left column (T1/T4, ~180px). -- **Replacements**: P4 when the number belongs to a strip; P16 for a bare value; P13 when the - number is a percentage that benefits from a ring. -- **Cautions**: the hero size is a **metric exception**, not a sixth text level — one hero - number per screen. The demos' 52px is superseded by the token value (56px). - -### P15 — Metric stack - -- **Purpose**: two secondary metrics side by side or stacked. -- **Construction**: a `section` grid → two `article` cards → `dvk-count-to` in each. -- **Parameters**: `grid-template-rows: repeat(2, minmax(0, 1fr)); gap` - `--dvk-screen-space-sm` (8px)–`--dvk-screen-space-md` (12px); card padding - `--dvk-screen-space-md` (12px) / `--dvk-screen-space-lg` (16px); value - `--dvk-count-to-font-size` = `--dvk-screen-font-size-md` (24px); affix at reduced alpha. -- **Grid position**: a middle row of the left or right column (T1). -- **Replacements**: P4 for a full-width strip; P16 for a single value; `dvk-border-box-15` when - the framed/unframed alternation is not wanted. -- **Cautions**: the unframed stack creates a framed/unframed alternation — allowed, but the - screen must stay consistent about it. A naked stack needs enough spacing to read as a group. - -### P16 — `dvk-count-to` usage - -- **Purpose**: every number that animates. -- **Construction**: ``, with optional `prefix` / `suffix` slots (a slot wins over - the same-named prop). -- **Parameters**: `start-val`, `end-val`, `duration`, `delay`, `decimals`, `decimal`, - `separator`, `prefix`, `suffix`, `disabled`, `transition`; CSS variables - `--dvk-count-to-color/font-family/font-size/font-weight/gap/affix-color/affix-font-size/decimal-color/decimal-font-size/decimal-font-weight`. -- **Grid position**: any metric slot; set `--dvk-count-to-font-size` on the host selector, not - per instance. -- **Replacements**: none — this is the only count-up primitive. A value that must not animate - renders statically. -- **Cautions**: in plain single-file HTML every prop is an attribute coerced from a string — - write the value, not a template expression. Use **one duration per screen**: the demos' 1300 / - 1400 / 1500 / 1600ms spread is unregularized, not a rule. Under `prefers-reduced-motion`, set - `disabled` so the end value renders immediately. Count-up is a data animation and needs a - named mapping (design-rules 5.1). - -## 7. Map patterns +| Where is the incident and what is affected? | Geographic map with traceable data | Regional metrics, incident queue, update status | +| Which asset needs attention? | Plant scene or stable equipment topology | Asset health, throughput, alarms | +| Are we on track and why? | Trend with target and comparison | Output, gap to target, contribution ranking | +| Where does the resource go? | Directed flow or process diagram | Supply, demand, loss, storage, time profile | +| What must an executive know now? | Prioritized metrics with context | Comparisons, exceptions, explanatory trend | -### P17 — Map stage +## Compose around the primary visual -- **Purpose**: the screen's visual lead, in the centre column, usually at least two thirds of - the screen height. -- **Construction**: a large HUD or surface panel (P6 role chosen by capability) + `.map-shell` - (`grid-template-rows: auto minmax(0, 1fr)`) + a map container (`position: relative; - overflow: hidden`, 1px border, grid texture) + an inline `` + absolutely - positioned markers + overlay cards. -- **Parameters**: SVG inset 24–32px; texture `background-size: 42px 42px`; paths through - ``, flow through `stroke-dasharray`; the panel variant follows §3. -- **Grid position**: the centre column's main row (`minmax(0, 1fr)`), panel head above, map - filling the rest. -- **Replacements**: a chart-based primary view (see `charts.md`) when the data has no geography; - `dvk-decoration-10` as a radar-style primary visual. -- **Cautions**: the two demos picked different variants for a capability reason (`-11` has no - `background-color`, `-10` does) — resolve through §3, never by copying a demo. Keep the map's - ambience layer behind the panel, not inside the frame. SVG inset values are scene values. +Give the main visual enough room to identify its subject before reading fine labels. +Supporting information can use side bands, a lower strip, an asymmetric split or a shared +axis. A KPI row and centered decorative title are choices, not mandatory regions. -### P18 — Map marker +Use stable grid tracks, `minmax(0, 1fr)` and explicit visual-container dimensions so dynamic +values cannot resize the layout. Keep the business title at screen scale and supporting +headings at panel scale. Repeated measurements usually read better as aligned rows than +as individually framed subsections. -- **Purpose**: a point on the map — hub, node, flight. -- **Construction**: an absolutely positioned `div` (percentage `left`/`top`) → diamond point `i` - (`transform: rotate(45deg)`) + a label plate `div` holding `strong` code + `span` name + an - optional `em` value. -- **Parameters**: `position: absolute; transform: translate(-50%, -50%); display: grid; - justify-items: center; gap` `--dvk-screen-space-xs` (4px); `min-width: 88px`; the point is - 14–16px with a 2px border and a theme glow; tone colour per role; `pointer-events: none`; the - label plate carries a `--dvk-color-surface` backing and `--dvk-screen-space-xs` inline padding - so the map's connection lines never cross the code or the name. -- **Grid position**: inside the map container; coordinates are percentages, decoupled from the - SVG `viewBox`. -- **Replacements**: a symbol layer once markers exceed ~20 (DOM markers stop scaling); P19 for a - summary anchored to a marker. -- **Cautions**: the two demos hand-write different names and sizes (88px vs 94px) — use this one - pattern. `pointer-events: none` is mandatory, or the markers block map interaction. Labels - collide before points do, so the marker count is a density decision. +Select a documented border or decoration for a framing or hierarchy need. Preserve its +documented content area. Verify capabilities and size rather than treating components as +interchangeable based on numeric suffixes. -### P19 — Map overlay card +## Make directions visibly different -- **Purpose**: a conclusion or annotation over the map without interrupting it. -- **Construction**: an absolutely positioned `section` (`right` / `bottom`) → `span` eyebrow + - `strong` conclusion + `p` caption. -- **Parameters**: width 300–360px; padding `--dvk-screen-space-lg` (16px); a 1px border from a - role colour; background alpha >= 0.8 for legibility over the map. -- **Grid position**: bottom-right of the map container. -- **Replacements**: a smaller region label for a single zone; a tooltip when the content is - per-marker. -- **Cautions**: offset values are scene values. An alpha below 0.8 makes the text unreadable - over map detail — a contrast problem, not a taste problem. +Develop spatial hierarchy, theme roles, typography, framing and motion together. Analytical +screens can use sparse rules; command screens can use selected technical frames. Keep +motion subordinate to the data and provide pause controls for persistent animation. -## 8. Cross-pattern rules +An industrial scene needs recognizable buildings and equipment. A map needs geographic +data and attribution. A financial trend needs totals and comparisons that reconcile. -- **Default slot only**: every border box exposes only the default slot; `frame`, `graphic`, and - `content` are parts. Panel titles go inside the default slot. -- **One colour source**: colours come only from the active theme's `--dvk-color-*`. No palette - extracted from the demos or from component-doc demos may be reused. -- **One theme per screen**: exactly one `.dvk-theme-*` class per screen (design-rules, "One - theme per screen"). -- **Fluid rows**: `minmax(0, 1fr)` for the main region, never fixed px row heights. -- **Named mapping**: every decoration and every motion must map to a named data or interaction - semantic. -- **Token vocabulary**: values in this file are token references; the authoritative numbers are - in `tokens.md` and `assets/tokens.css`. -- **Runtime availability**: check `customElements.get(tag)` after registration and follow - `components.md` for capability fields, publication status, and the fetch/fallback protocol. -- **Never reference `title-4`**: it does not exist. +The standalone examples in `assets/examples/` demonstrate different compositions. Inspect +their rendered previews, then adapt data and structure to the current brief. Their CDN +versions are executable examples, not a version policy for consuming projects. diff --git a/skills/datav-kit/references/tokens.md b/skills/datav-kit/references/tokens.md index ba1acee..6734e6e 100644 --- a/skills/datav-kit/references/tokens.md +++ b/skills/datav-kit/references/tokens.md @@ -1,130 +1,39 @@ -# Screen Design Tokens +# Screen Tokens and Calibration -The `--dvk-screen-*` token set is the design vocabulary for a large screen: spacing, type, -layering, interaction motion, and layout. Reference implementation: `assets/tokens.css` -(copy it into the project, then override only what the project needs). +`assets/tokens.css` is the single maintained source of default screen-token values. +Copy it when useful and override defaults to fit the composition. -## Scope and precedence +Declare `--dvk-screen-*` on `.dvk-screen`, not the surrounding page. They cover spacing, +typography, layering, interaction duration and layout. Project overrides win over starter +defaults. These are application conventions, not datav-kit component API. -- **Scope**: declare these tokens on the large-screen root container `.dvk-screen`. - Never `:root` — the host page outside the screen must not inherit them. -- **Precedence**: project override > skill default > component fallback. - A project value on `.dvk-screen` (or a narrower selector inside it) wins; otherwise the - `assets/tokens.css` default applies; otherwise the element's own `--dvk-*` fallback applies. -- **Colors are read-only**: this set declares **no** color token. Every color comes from the - theme's `--dvk-color-*` values. Do not introduce a second color source. -- Screen tokens are **not** part of a theme. A theme carries colors and glow only; screen - tokens stay in the screen layer so one theme can serve every screen. +Colors belong to the scoped project theme. Official theming documentation explains how +application colors coordinate with `--dvk-color-*`. See `charts.md` for the computed-value bridge. -## 1. Spacing — 8pt grid +## Viewing-distance calibration -| Token | Value | Use | -| --- | --- | --- | -| `--dvk-screen-space-xs` | `4px` | hairline separation, icon-to-label | -| `--dvk-screen-space-sm` | `8px` | inside a compact row | -| `--dvk-screen-space-md` | `12px` | label-to-value | -| `--dvk-screen-space-lg` | `16px` | panel spacing (adjacent panels) | -| `--dvk-screen-space-xl` | `24px` | block spacing, panel padding | -| `--dvk-screen-space-2xl` | `32px` | section spacing | -| `--dvk-screen-space-3xl` | `48px` | screen margin, major section gap | -| `--dvk-screen-space-4xl` | `64px` | hero block breathing room | -| `--dvk-screen-space-5xl` | `96px` | rare, full-width separation | +Starter values are preview defaults, not a claim of suitability for every display. Estimate +required character height, convert it to design pixels, then verify representative text at +the expected viewing distance: -Adjacent blocks must not use a step smaller than the smallest step inside either block. - -## 2. Typography - -| Token | Value | Use | -| --- | --- | --- | -| `--dvk-screen-font-family` | `Inter, "PingFang SC", "Microsoft YaHei", sans-serif` | single screen family | -| `--dvk-screen-font-size-xs` | `14px` | axis labels, legends, secondary text | -| `--dvk-screen-font-size-sm` | `18px` | data labels, tooltips, body | -| `--dvk-screen-font-size-md` | `24px` | panel titles, key values | -| `--dvk-screen-font-size-lg` | `32px` | KPI values | -| `--dvk-screen-font-size-xl` | `44px` | screen title, hero KPI | -| `--dvk-screen-font-size-hero` | `56px` | hero number only — not a 6th text level | -| `--dvk-screen-font-weight-regular` | `400` | default text | -| `--dvk-screen-font-weight-medium` | `500` | emphasis, values | -| `--dvk-screen-font-weight-bold` | `600` | Latin bold only | -| `--dvk-screen-line-height-tight` | `1.25` | single-line values | -| `--dvk-screen-line-height-base` | `1.5` | multi-line text | - -**Hero exception**: `--dvk-screen-font-size-hero` belongs to metrics, not to the text scale. -The text scale stays at five levels (`xs`–`xl`). - -### Calibration (required for every project) - -The defaults are calibrated for a **4m x 2.25m screen at 6m viewing distance**. A monitor and -a large screen differ by an order of magnitude in physical character height, so recalibrate: - -```txt -minimum font size (px) = (viewing distance / 200) x (1080 / screen height) +```text +minimum design pixels = required physical character height + * design canvas height / physical display height ``` -- `viewing distance` and `screen height` in the same unit (e.g. metres); the screen height is - the physical height of the display, not the CSS pixel height. -- Apply the result to `--dvk-screen-font-size-xs`. **If the computed value is larger than the - default, raise the whole scale proportionally** — never leave `xs` below the computed floor. -- **Target screens that are monitors or laptops must raise the whole type scale**, even when - the formula returns a small number. -- Source: T/CIDADS 00011-2022 `minimum font size = viewing distance / 200`, combined with the - `1080 / screen height` term for non-1080p canvases. Clause obtained via third-party - transcription — label it as a secondary source when quoting the standard. - -## 3. Layering - -| Token | Value | Use | -| --- | --- | --- | -| `--dvk-screen-z-base` | `0` | canvas, background ambience | -| `--dvk-screen-z-panel` | `10` | panels and blocks | -| `--dvk-screen-z-overlay` | `100` | drawers, full-screen overlays | -| `--dvk-screen-z-tooltip` | `1000` | tooltips, popovers | - -Use these four steps only; do not invent intermediate z-index values. - -## 4. Motion — interaction budget - -| Token | Value | Use | -| --- | --- | --- | -| `--dvk-screen-duration-fast` | `150ms` | hover, focus, small state change | -| `--dvk-screen-duration-base` | `250ms` | expand/collapse, panel state change | -| `--dvk-screen-duration-slow` | `300ms` | overlay and drawer transitions | -| `--dvk-screen-ease-standard` | `cubic-bezier(0.4, 0, 0.2, 1)` | default | -| `--dvk-screen-ease-decelerate` | `cubic-bezier(0, 0, 0.2, 1)` | entering | -| `--dvk-screen-ease-accelerate` | `cubic-bezier(0.4, 0, 1, 1)` | leaving | - -These tokens cover **interaction motion only** (150–300ms). They do not replace the theme's -decorative animation token: - -- **Decoration** keeps using `--dvk-motion-duration` from the active theme (2200–2600ms). - Do not override it with a screen duration token. -- **Charts** use 200–400ms transitions and must not read `--dvk-motion-duration` (see - `charts.md`). - -## 5. Layout — 1920 x 1080 canvas - -| Token | Value | Use | -| --- | --- | --- | -| `--dvk-screen-safe-margin` | `48px` | screen edge inset | -| `--dvk-screen-grid-columns` | `12` | main grid columns | -| `--dvk-screen-grid-gutter` | `24px` | gutter between columns | -| `--dvk-screen-header-height` | `104px` | top bar | -| `--dvk-screen-kpi-height` | `104px` | KPI strip | -| `--dvk-screen-panel-padding` | `24px` | panel content inset | +Use the same physical unit throughout. A preliminary character-height estimate of viewing +distance / 200 can inform a draft; it does not replace legibility testing or the installation's +applicable specification. Use the actual canvas height for non-1080p designs. -At 1920 with a 48px margin and 11 gutters, one column is -`(1920 - 96 - 11 x 24) / 12 = 130px`. +Raise the smallest labels and revise hierarchy and spacing together. A monitor preview +cannot establish readability on a distant wall display. -- The main region row height is **fluid** (`minmax(0, 1fr)`), not a token and not a fixed px - value. -- Video-wall or overscan-risk screens tighten the safe margin to 5% (96px horizontal / - 54px vertical, EBU R95 graphics safe). -- Canvas, scaling, and degradation policy live in `design-rules.md` group 1. +## Layout and motion -## Token boundary summary +Use fluid main tracks and explicit visual dimensions. Safe margins depend on hardware, +overscan and wall seams; the starter grid is adjustable. Ultra-wide delivery may need a +different composition. Smaller devices can show a proportional contained preview. -| Layer | Owns | Does not own | -| --- | --- | --- | -| Theme (`--dvk-color-*`, `--dvk-glow-*`, `--dvk-line-width`, `--dvk-motion-duration`) | color, glow, line width, decorative animation period | spacing, type, layout | -| Screen (`--dvk-screen-*`, this file) | spacing, type, layering, interaction motion, layout | any color | -| Component | `--dvk--*` fallbacks | the layers above | +Interaction transitions, chart updates and decorative loops serve different purposes. +Use short data transitions and reduced-motion support. Do not drive chart transitions with +a decorative loop duration. diff --git a/tests/examples/screens.spec.ts b/tests/examples/screens.spec.ts new file mode 100644 index 0000000..e8ced01 --- /dev/null +++ b/tests/examples/screens.spec.ts @@ -0,0 +1,301 @@ +import type { Page } from '@playwright/test' +import { Buffer } from 'node:buffer' +import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' +import { pathToFileURL } from 'node:url' +import { expect, test } from '@playwright/test' +import { PNG } from 'pngjs' + +const directory = path.resolve('skills/datav-kit/assets/examples') +const examples = (await readdir(directory)).filter(name => name.endsWith('.html')) +const base = process.env.VITEPRESS_BASE || '/' +const previewBase = `http://127.0.0.1:4173${base}examples/` +const chartLibrary = 'https://cdn.jsdelivr.net/npm/echarts@6.1.0/dist/echarts.esm.min.js' + +async function openExample(page: Page, url: string) { + const errors: string[] = [] + page.on('pageerror', error => errors.push(error.message)) + page.on('console', (message) => { + if (message.type() === 'error') + errors.push(message.text()) + }) + await page.goto(url) + await expect(page.locator('#screen')).toHaveAttribute('data-ready', 'true', { timeout: 75_000 }) + await page.evaluate(() => document.fonts.ready) + // Allow the explicitly bounded chart transition to complete before inspecting pixels. + await page.waitForTimeout(1200) + expect(errors).toEqual([]) + return errors +} + +async function expectRegistered(page: Page) { + const missing = await page.evaluate(() => [...document.querySelectorAll('*')] + .filter(element => element.localName.startsWith('dvk-') && !customElements.get(element.localName)) + .map(element => element.localName)) + expect(missing).toEqual([]) + const invalid = await page.evaluate(async () => { + const { elementMetadata } = await import('https://cdn.jsdelivr.net/npm/@datav-kit/elements@0.0.5/+esm') + const result: string[] = [] + for (const element of document.querySelectorAll('*')) { + if (!element.localName.startsWith('dvk-')) + continue + const meta = elementMetadata.find((item: { tagName: string }) => item.tagName === element.localName) + const allowed = Object.entries(meta.props).flatMap(([key, property]) => { + const attribute = (property as { attribute?: boolean | string }).attribute + return attribute === false ? [] : [typeof attribute === 'string' ? attribute : key.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`)] + }) + for (const attribute of element.getAttributeNames()) { + if (!allowed.includes(attribute) && !['class', 'id', 'style', 'title', 'role', 'tabindex', 'hidden', 'slot'].includes(attribute) && !/^(?:data|aria)-/.test(attribute)) + result.push(`${element.localName}[${attribute}]`) + } + } + return result + }) + expect(invalid).toEqual([]) +} + +async function expectCharts(page: Page) { + const chartResults = await page.evaluate(async (url) => { + const echarts = await import(/* @vite-ignore */ url) + return [...document.querySelectorAll('[data-chart]')].map((element) => { + const chart = echarts.getInstanceByDom(element) + const options = chart.getOption() + const roles = [...getComputedStyle(document.querySelector('#screen')!)] + .filter(name => name.startsWith('--app-')) + .map(name => getComputedStyle(document.querySelector('#screen')!).getPropertyValue(name).trim().toLowerCase()) + const marks = [...element.querySelectorAll('path,rect')].filter(mark => ['fill', 'stroke'] + .some(attribute => roles.includes((mark.getAttribute(attribute) || '').toLowerCase()))) + return { id: element.id, width: chart.getWidth(), height: chart.getHeight(), series: options.series.length, coloredMarks: marks.length } + }) + }, chartLibrary) + for (const result of chartResults) { + expect(result.width, result.id).toBeGreaterThan(100) + expect(result.height, result.id).toBeGreaterThan(80) + expect(result.series, result.id).toBeGreaterThan(0) + expect(result.coloredMarks, `Visible theme-colored marks in ${result.id}`).toBeGreaterThan(2) + } +} + +async function expectCanvasFits(page: Page) { + const geometry = await page.locator('#screen').evaluate((element) => { + const rect = element.getBoundingClientRect() + return { x: rect.x, y: rect.y, right: rect.right, bottom: rect.bottom, ratio: rect.width / rect.height, viewport: [innerWidth, innerHeight] } + }) + expect(geometry.ratio).toBeCloseTo(16 / 9, 3) + expect(geometry.x).toBeGreaterThanOrEqual(-1) + expect(geometry.y).toBeGreaterThanOrEqual(-1) + expect(geometry.right).toBeLessThanOrEqual(geometry.viewport[0] + 1) + expect(geometry.bottom).toBeLessThanOrEqual(geometry.viewport[1] + 1) + const overflow = await page.locator('#screen').evaluate((screen) => { + const result: string[] = [] + for (const element of screen.querySelectorAll('main,aside,section,.channels,.equipment-list,.metrics')) { + if (element.scrollHeight > element.clientHeight + 3 || element.scrollWidth > element.clientWidth + 3) + result.push(element.id || element.className || element.tagName) + } + return result + }) + expect(overflow).toEqual([]) +} + +for (const name of examples) { + for (const delivery of ['file', 'preview']) { + test(`${name}: ${delivery} registration, pixels, interaction and data`, async ({ page }) => { + const url = delivery === 'file' ? pathToFileURL(path.join(directory, name)).href : `${previewBase}${name}` + const errors = await openExample(page, url) + await expectRegistered(page) + await expectCharts(page) + await expectCanvasFits(page) + const screen = page.locator('#screen') + if (name === 'business.html') { + await page.getByRole('button', { name: '近 7 天' }).click() + await expect(screen).toHaveAttribute('data-period', 'week') + await expect(screen).toHaveAttribute('data-total', '603.00') + await expect(page.locator('#completion')).toHaveText('102.6') + await expect(page.locator('#channel-list')).not.toContainText('000000000') + } + else if (name === 'city.html') { + await page.getByRole('button', { name: '金融城', exact: true }).click() + await expect(page.locator('#area-name')).toHaveText('金融城') + await expect(page.locator('#open-events')).toHaveText('2') + await expect(screen).toHaveAttribute('data-total', '17010') + expect(await page.locator('#city-map path.road').count()).toBeGreaterThan(100) + await expect(page.getByRole('link', { name: /OpenStreetMap/ })).toBeVisible() + } + else if (name === 'energy.html') { + await page.getByRole('button', { name: '储能', exact: true }).click() + await expect(page.locator('#node-name')).toHaveText('储能充电') + await expect(page.locator('#node-power')).toHaveText('8.0') + await expect(page.locator('#deviation')).toHaveText('0.0') + } + else if (name === 'industrial.html') { + await expect(screen).toHaveAttribute('data-renderer', 'webgl') + await page.getByRole('button', { name: /精加工/ }).click() + await expect(page.locator('#asset-temperature')).toHaveText('58 °C') + await expect(page.locator('#asset-name')).toHaveText('精加工') + } + expect(errors).toEqual([]) + }) + } + + test(`${name}: all data states and retry`, async ({ page }) => { + for (const state of ['loading', 'empty', 'failed', 'stale']) { + await openExample(page, `${previewBase}${name}?state=${state}`) + await expect(page.locator('#screen')).toHaveAttribute('data-state', state) + if (state === 'stale') { + await expect(page.locator('main')).toBeVisible() + await expect(page.locator('#status')).toContainText('过期') + } + else { + await expect(page.locator('.state-layer')).toBeVisible() + await expect(page.locator('main')).toBeHidden() + } + if (state === 'failed' || state === 'empty') { + await page.getByRole('button', { name: '重新加载' }).click() + await expect(page.locator('#screen')).toHaveAttribute('data-state', 'ready') + } + } + }) + + test(`${name}: target and preview sizes, reduced motion`, async ({ page }) => { + await page.emulateMedia({ reducedMotion: 'reduce' }) + await openExample(page, `${previewBase}${name}`) + for (const viewport of [{ width: 1920, height: 1080 }, { width: 3840, height: 2160 }, { width: 1366, height: 768 }, { width: 390, height: 844 }]) { + await page.setViewportSize(viewport) + await page.waitForTimeout(150) + await expectCanvasFits(page) + await expectCharts(page) + const target = path.resolve('.cache/example-screenshots', `${name.replace('.html', '')}-${viewport.width}.png`) + await mkdir(path.dirname(target), { recursive: true }) + await page.screenshot({ path: target }) + if (process.env.UPDATE_EXAMPLE_PREVIEWS && viewport.width === 1920) { + await mkdir(path.join(directory, 'previews'), { recursive: true }) + await page.screenshot({ path: path.join(directory, 'previews', name.replace('.html', '.png')) }) + } + } + const animation = await page.evaluate(async (url) => { + const echarts = await import(/* @vite-ignore */ url) + return [...document.querySelectorAll('[data-chart]')].map(element => echarts.getInstanceByDom(element).getOption().animation) + }, chartLibrary) + expect(animation.every(value => value === false)).toBe(true) + if (name === 'industrial.html') { + await expect(page.locator('#screen')).toHaveAttribute('data-paused', 'true') + const frames = await page.locator('#screen').getAttribute('data-frames') + await page.waitForTimeout(300) + await expect(page.locator('#screen')).toHaveAttribute('data-frames', frames!) + } + }) +} + +test('industrial: nonempty scene, animation, picking, reset and frame timing', async ({ page }, testInfo) => { + await openExample(page, `${previewBase}industrial.html`) + const canvas = page.locator('#scene canvas') + const extent = await canvas.evaluate((element: HTMLCanvasElement) => { + const gl = element.getContext('webgl2')! + const pixels = new Uint8Array(element.width * element.height * 4) + gl.readPixels(0, 0, element.width, element.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels) + let count = 0 + let minX = element.width + let minY = element.height + let maxX = 0 + let maxY = 0 + for (let y = 0; y < element.height; y++) { + for (let x = 0; x < element.width; x++) { + if (pixels[(y * element.width + x) * 4 + 3] > 20) { + count++ + minX = Math.min(minX, x) + minY = Math.min(minY, y) + maxX = Math.max(maxX, x) + maxY = Math.max(maxY, y) + } + } + } + return { minX, minY, maxX, maxY, width: element.width, height: element.height, coverage: count / (element.width * element.height) } + }) + expect(extent.coverage).toBeGreaterThan(0.12) + expect(extent.minX).toBeGreaterThan(3) + expect(extent.minY).toBeGreaterThan(3) + expect(extent.maxX).toBeLessThan(extent.width - 3) + expect(extent.maxY).toBeLessThan(extent.height - 3) + const initial = PNG.sync.read(await canvas.screenshot()) + let pixels = 0 + for (let i = 3; i < initial.data.length; i += 4) { + if (initial.data[i] > 0) + pixels++ + } + // Canvas screenshot has an opaque page background: count varied RGB values as well. + const colors = new Set() + for (let i = 0; i < initial.data.length; i += 64) + colors.add(`${initial.data[i]},${initial.data[i + 1]},${initial.data[i + 2]}`) + expect(pixels).toBeGreaterThan(1000) + expect(colors.size).toBeGreaterThan(100) + const before = await canvas.screenshot() + await page.waitForTimeout(350) + expect(Buffer.compare(before, await canvas.screenshot())).not.toBe(0) + await page.getByRole('button', { name: '暂停动态' }).click() + await expect(page.locator('#screen')).toHaveAttribute('data-paused', 'true') + const paused = await canvas.screenshot() + await page.waitForTimeout(200) + expect(Buffer.compare(paused, await canvas.screenshot())).toBe(0) + await page.getByRole('button', { name: /精加工/ }).click() + const bounds = await canvas.boundingBox() + expect(bounds).not.toBeNull() + // The assembly roof is visibly located near this point in the initial fitted camera. + await page.mouse.click(bounds!.x + bounds!.width * 0.46, bounds!.y + bounds!.height * 0.32) + await expect(page.locator('#screen')).toHaveAttribute('data-asset', 'assembly') + await page.mouse.move(bounds!.x + bounds!.width / 2, bounds!.y + bounds!.height / 2) + await page.mouse.down() + await page.mouse.move(bounds!.x + bounds!.width / 2 + 100, bounds!.y + bounds!.height / 2 + 35, { steps: 8 }) + await page.mouse.up() + const rotated = await canvas.screenshot() + await page.getByRole('button', { name: '复位视角' }).click() + expect(Buffer.compare(rotated, await canvas.screenshot())).not.toBe(0) + const frameMs = Number(await page.locator('#screen').getAttribute('data-frame-ms')) + expect(frameMs).toBeGreaterThan(0) + const timing = { browser: await page.evaluate(() => navigator.userAgent), viewport: page.viewportSize(), meanFrameMs: frameMs, sceneCoverage: extent.coverage } + await testInfo.attach('frame-timing', { body: JSON.stringify(timing), contentType: 'application/json' }) + await writeFile('.cache/industrial-frame-timing.json', JSON.stringify(timing, null, 2)) + await canvas.evaluate((element: HTMLCanvasElement) => element.getContext('webgl2')!.getExtension('WEBGL_lose_context')!.loseContext()) + await expect(page.locator('#screen')).toHaveAttribute('data-renderer', '2d') + await page.locator('#fallback').getByRole('button', { name: /能源站/ }).click() + await expect(page.locator('#asset-online')).toHaveText('8 / 8 台') +}) + +test('industrial: unavailable WebGL keeps the same equipment data', async ({ page }) => { + await page.addInitScript(() => { + const original = HTMLCanvasElement.prototype.getContext + HTMLCanvasElement.prototype.getContext = function (...args) { + if (String(args[0]).includes('webgl')) + return null + return Reflect.apply(original, this, args) + } + }) + // Three.js emits a console diagnostic for an unavailable graphics context. + await page.goto(`${previewBase}industrial.html`) + await expect(page.locator('#screen')).toHaveAttribute('data-ready', 'true', { timeout: 75_000 }) + await expect(page.locator('#screen')).toHaveAttribute('data-renderer', '2d') + await expect(page.locator('#fallback')).toBeVisible() + await page.locator('#fallback').getByRole('button', { name: /能源站/ }).click() + await expect(page.locator('#asset-online')).toHaveText('8 / 8 台') + await expect(page.locator('#asset-temperature')).toHaveText('36 °C') +}) + +test('minimal starter registers and scales', async ({ page }) => { + await page.goto(`${previewBase}minimal.html`) + await expect(page.locator('#screen')).toHaveAttribute('data-ready', 'true') + await expectRegistered(page) + await expectCanvasFits(page) +}) + +test('gallery serves screenshots and downloads the maintained HTML', async ({ page }) => { + await page.goto(`http://127.0.0.1:4173${base}guide/dashboard-examples`) + for (const name of examples) { + const image = page.locator(`.vp-doc img[src$="/${name.replace('.html', '.png')}"]`) + await expect(image).toBeVisible() + await expect.poll(() => image.evaluate((element: HTMLImageElement) => element.naturalWidth)).toBe(1920) + const downloadEvent = page.waitForEvent('download') + await page.locator(`a[download="${name}"]`).click() + const download = await downloadEvent + expect(await readFile((await download.path())!, 'utf8')).toBe(await readFile(path.join(directory, name), 'utf8')) + } +}) diff --git a/tests/examples/serve.mjs b/tests/examples/serve.mjs new file mode 100644 index 0000000..271a758 --- /dev/null +++ b/tests/examples/serve.mjs @@ -0,0 +1,28 @@ +import { readFile } from 'node:fs/promises' +import http from 'node:http' +import path from 'node:path' + +const root = path.resolve('docs/.vitepress/dist') +const types = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png', '.svg': 'image/svg+xml' } +http.createServer(async (request, response) => { + const url = new URL(request.url, 'http://localhost') + const pathname = decodeURIComponent(url.pathname).replace(/^\/datav-kit(?=\/)/, '') + if (pathname === '/favicon.ico') { + response.writeHead(204).end() + return + } + const filename = pathname.endsWith('/') ? `${pathname}index.html` : path.extname(pathname) ? pathname : `${pathname}.html` + const file = path.resolve(root, `.${filename}`) + if (!file.startsWith(`${root}${path.sep}`)) { + response.writeHead(403).end() + return + } + try { + const content = await readFile(file) + response.writeHead(200, { 'Content-Type': types[path.extname(file)] || 'text/plain' }) + response.end(content) + } + catch { + response.writeHead(404).end('Not found') + } +}).listen(4173, '127.0.0.1')