@nurix/etna 0.19.11 → 0.19.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/agents/maintenance.md +1 -1
- package/data/harnesses/code/harness.json +1 -1
- package/data/harnesses/code/rules/frontend.md +5 -5
- package/data/harnesses/code/skills/design/SKILL.md +70 -50
- package/data/harnesses/code/skills/design/components/charts.md +76 -0
- package/data/harnesses/code/skills/design/components/collections.md +132 -0
- package/data/harnesses/code/skills/design/components/feedback.md +47 -0
- package/data/harnesses/code/skills/design/components/hooks.md +35 -0
- package/data/harnesses/code/skills/design/components/index.md +143 -0
- package/data/harnesses/code/skills/design/components/inputs.md +327 -0
- package/data/harnesses/code/skills/design/components/layout.md +52 -0
- package/data/harnesses/code/skills/design/components/media.md +66 -0
- package/data/harnesses/code/skills/design/components/motion.md +82 -0
- package/data/harnesses/code/skills/design/components/navigation.md +29 -0
- package/data/harnesses/code/skills/design/components/overlays.md +72 -0
- package/data/harnesses/code/skills/design/components/setup.md +40 -0
- package/data/harnesses/code/skills/design/components/typography.md +46 -0
- package/data/harnesses/code/skills/design/components/utils.md +37 -0
- package/data/harnesses/code/skills/design/data/blueprints/browse.md +38 -0
- package/data/harnesses/code/skills/design/data/blueprints/creation-flow.md +38 -0
- package/data/harnesses/code/skills/design/data/blueprints/dashboard.md +38 -0
- package/data/harnesses/code/skills/design/data/blueprints/grid.md +38 -0
- package/data/harnesses/code/skills/design/data/blueprints/queue.md +38 -0
- package/data/harnesses/code/skills/design/data/blueprints/record-detail.md +38 -0
- package/data/harnesses/code/skills/design/data/blueprints/settings.md +38 -0
- package/data/harnesses/code/skills/design/data/chart-styling/Readme.md +33 -0
- package/data/harnesses/code/skills/design/data/chart-styling/carbon-accessible-alert-semantic.md +19 -0
- package/data/harnesses/code/skills/design/data/chart-styling/carbon-monochrome-sequential-trend.md +19 -0
- package/data/harnesses/code/skills/design/data/chart-styling/datadog-classic-six-aligned.md +19 -0
- package/data/harnesses/code/skills/design/data/chart-styling/datawrapper-tiered-gray-hierarchy.md +19 -0
- package/data/harnesses/code/skills/design/data/chart-styling/econ-red-rule-horizontal.md +19 -0
- package/data/harnesses/code/skills/design/data/chart-styling/financial-diverging-threshold-overlay.md +19 -0
- package/data/harnesses/code/skills/design/data/chart-styling/ft-vocabulary-ranked-muted.md +19 -0
- package/data/harnesses/code/skills/design/data/chart-styling/grafana-dark-threshold-bands.md +19 -0
- package/data/harnesses/code/skills/design/data/chart-styling/material-rounded-explicit-label.md +19 -0
- package/data/harnesses/code/skills/design/data/chart-styling/nyt-ghost-grid-direct-label.md +19 -0
- package/data/harnesses/code/skills/design/data/chart-styling/plot-currentcolor-inherit.md +19 -0
- package/data/harnesses/code/skills/design/data/chart-styling/saas-gradient-glow-area.md +19 -0
- package/data/harnesses/code/skills/design/data/chart-styling/shadcn-composable-css-vars.md +19 -0
- package/data/harnesses/code/skills/design/data/charts/census.csv +58 -0
- package/data/harnesses/code/skills/design/data/checklists/browse.md +21 -0
- package/data/harnesses/code/skills/design/data/checklists/creation-flow.md +20 -0
- package/data/harnesses/code/skills/design/data/checklists/dashboard.md +21 -0
- package/data/harnesses/code/skills/design/data/checklists/grid.md +21 -0
- package/data/harnesses/code/skills/design/data/checklists/queue.md +21 -0
- package/data/harnesses/code/skills/design/data/checklists/record-detail.md +21 -0
- package/data/harnesses/code/skills/design/data/checklists/settings.md +21 -0
- package/data/harnesses/code/skills/design/data/chrome/board-column.md +68 -0
- package/data/harnesses/code/skills/design/data/chrome/data-table.md +63 -0
- package/data/harnesses/code/skills/design/data/chrome/side-panel.md +60 -0
- package/data/harnesses/code/skills/design/data/chrome/sidebar.md +78 -0
- package/data/harnesses/code/skills/design/data/chrome/stat-cards.md +62 -0
- package/data/harnesses/code/skills/design/data/chrome/topbar.md +65 -0
- package/data/harnesses/code/skills/design/data/color/accent-presets.md +90 -0
- package/data/harnesses/code/skills/design/data/color/aesop-earth-mute.md +91 -0
- package/data/harnesses/code/skills/design/data/color/arc-peach-play.md +90 -0
- package/data/harnesses/code/skills/design/data/color/attio-hunter-teal.md +90 -0
- package/data/harnesses/code/skills/design/data/color/carbon-cyan-signal.md +90 -0
- package/data/harnesses/code/skills/design/data/color/clay-parchment.md +90 -0
- package/data/harnesses/code/skills/design/data/color/duolingo-feather.md +90 -0
- package/data/harnesses/code/skills/design/data/color/geist-mono-signal.md +90 -0
- package/data/harnesses/code/skills/design/data/color/linear-graphite-violet.md +90 -0
- package/data/harnesses/code/skills/design/data/color/mercury-onyx-cobalt.md +90 -0
- package/data/harnesses/code/skills/design/data/color/notion-paper-ink.md +90 -0
- package/data/harnesses/code/skills/design/data/color/radix-slate-iris.md +90 -0
- package/data/harnesses/code/skills/design/data/color/raycast-void-spectrum.md +90 -0
- package/data/harnesses/code/skills/design/data/color/spectrum-celery-fuchsia.md +90 -0
- package/data/harnesses/code/skills/design/data/color/stripe-blurple-ruby.md +90 -0
- package/data/harnesses/code/skills/design/data/color/superhuman-cod-picton.md +89 -0
- package/data/harnesses/code/skills/design/data/corpora/operations-review/companies.csv +15 -0
- package/data/harnesses/code/skills/design/data/corpora/operations-review/corpus.md +87 -0
- package/data/harnesses/code/skills/design/data/corpora/operations-review/people.csv +25 -0
- package/data/harnesses/code/skills/design/data/corpora/procurement-approvals/companies.csv +15 -0
- package/data/harnesses/code/skills/design/data/corpora/procurement-approvals/corpus.md +83 -0
- package/data/harnesses/code/skills/design/data/corpora/procurement-approvals/people.csv +25 -0
- package/data/harnesses/code/skills/design/data/corpora/revenue-analytics/companies.csv +15 -0
- package/data/harnesses/code/skills/design/data/corpora/revenue-analytics/corpus.md +89 -0
- package/data/harnesses/code/skills/design/data/corpora/revenue-analytics/people.csv +25 -0
- package/data/harnesses/code/skills/design/data/elevation/ant-design-directional-shadow.md +19 -0
- package/data/harnesses/code/skills/design/data/elevation/apple-liquid-glass-material.md +18 -0
- package/data/harnesses/code/skills/design/data/elevation/atlassian-surface-shadow-pairing.md +20 -0
- package/data/harnesses/code/skills/design/data/elevation/fluent-2-key-ambient-ramp.md +20 -0
- package/data/harnesses/code/skills/design/data/elevation/github-primer-sparing-shadow.md +19 -0
- package/data/harnesses/code/skills/design/data/elevation/ibm-carbon-layer-and-border.md +19 -0
- package/data/harnesses/code/skills/design/data/elevation/linear-dark-surface-ladder.md +17 -0
- package/data/harnesses/code/skills/design/data/elevation/material-2-key-ambient-dp-ramp.md +19 -0
- package/data/harnesses/code/skills/design/data/elevation/material-3-tonal-elevation.md +21 -0
- package/data/harnesses/code/skills/design/data/elevation/neobrutalist-hard-offset.md +19 -0
- package/data/harnesses/code/skills/design/data/elevation/neumorphic-soft-dual-shadow.md +18 -0
- package/data/harnesses/code/skills/design/data/elevation/notion-warm-minimal-hairline.md +17 -0
- package/data/harnesses/code/skills/design/data/elevation/polaris-elevation-inset-bevel.md +21 -0
- package/data/harnesses/code/skills/design/data/elevation/radix-themes-alpha-ring-scale.md +19 -0
- package/data/harnesses/code/skills/design/data/elevation/shadcn-neutral-derived-radius.md +19 -0
- package/data/harnesses/code/skills/design/data/elevation/stripe-brand-tinted-dual-shadow.md +18 -0
- package/data/harnesses/code/skills/design/data/elevation/tailwind-default-utility-scale.md +19 -0
- package/data/harnesses/code/skills/design/data/elevation/vercel-geist-shadow-as-border.md +20 -0
- package/data/harnesses/code/skills/design/data/gradients/amber-attention-fill.md +16 -0
- package/data/harnesses/code/skills/design/data/gradients/cloud-shadow-wash.md +16 -0
- package/data/harnesses/code/skills/design/data/gradients/confident-lavender-wash.md +16 -0
- package/data/harnesses/code/skills/design/data/gradients/ember-ship-flare.md +18 -0
- package/data/harnesses/code/skills/design/data/gradients/full-metal-wash.md +16 -0
- package/data/harnesses/code/skills/design/data/gradients/indigo-ruby-cta-band.md +18 -0
- package/data/harnesses/code/skills/design/data/gradients/linear-graphite-wash.md +18 -0
- package/data/harnesses/code/skills/design/data/gradients/marble-quartz-wash.md +16 -0
- package/data/harnesses/code/skills/design/data/gradients/midnight-aurora-panel.md +16 -0
- package/data/harnesses/code/skills/design/data/gradients/neutral-volume-fill.md +16 -0
- package/data/harnesses/code/skills/design/data/gradients/revenue-uptrend-fill.md +16 -0
- package/data/harnesses/code/skills/design/data/gradients/risk-decline-fill.md +16 -0
- package/data/harnesses/code/skills/design/data/gradients/signal-blue-develop.md +18 -0
- package/data/harnesses/code/skills/design/data/gradients/slate-mist-wash.md +16 -0
- package/data/harnesses/code/skills/design/data/gradients/stripe-atmosphere-mesh.md +18 -0
- package/data/harnesses/code/skills/design/data/gradients/teal-signal-accent.md +16 -0
- package/data/harnesses/code/skills/design/data/gradients/violet-magenta-promo.md +16 -0
- package/data/harnesses/code/skills/design/data/iconography.md +199 -0
- package/data/harnesses/code/skills/design/data/motion/accordion-expand.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/destructive-hold-confirm.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/drag-dismiss-momentum.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/empty-state-arrive.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/hover-lift.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/modal-center-arrive.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/number-ticker-roll.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/onboarding-bounce.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/panel-slide.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/press-feedback.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/progress-linear-loop.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/route-crossfade.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/rubber-band-overscroll.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/skeleton-shimmer.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/stagger-arrive.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/toast-enter-stack.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/tooltip-chain-instant.md +18 -0
- package/data/harnesses/code/skills/design/data/motion/trigger-anchored-reveal.md +18 -0
- package/data/harnesses/code/skills/design/data/tells.csv +34 -0
- package/data/harnesses/code/skills/design/data/typography/anton-work-sans.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/archivo-black-archivo.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/bitter-karla.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/dm-serif-dm-sans.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/fraunces-inter.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/geist-geist-mono.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/hubot-sans-mona-sans.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/ibm-plex-sans-operator-single-family.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/instrument-serif-instrument-sans.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/inter-operator-single-family.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/libre-baskerville-libre-franklin.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/newsreader-inter.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/playfair-display-source-sans.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/public-sans-operator-single-family.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/sohne-inter-fintech.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/source-serif-source-sans.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/space-grotesk-work-sans-space-mono.md +17 -0
- package/data/harnesses/code/skills/design/data/typography/spectral-manrope.md +17 -0
- package/data/harnesses/code/skills/design/data/voices/bright-collaborator.md +29 -0
- package/data/harnesses/code/skills/design/data/voices/direct-operator.md +29 -0
- package/data/harnesses/code/skills/design/data/voices/earnest-helper.md +29 -0
- package/data/harnesses/code/skills/design/data/voices/editorial-composed.md +29 -0
- package/data/harnesses/code/skills/design/data/voices/quiet-concierge.md +29 -0
- package/data/harnesses/code/skills/design/data/voices/warm-guide.md +29 -0
- package/data/harnesses/code/skills/design/design-md/SCHEMA.md +179 -0
- package/data/harnesses/code/skills/design/design-md/index.csv +67 -0
- package/data/harnesses/code/skills/design/design-md/library/airbnb.md +207 -0
- package/data/harnesses/code/skills/design/design-md/library/airtable.md +205 -0
- package/data/harnesses/code/skills/design/design-md/library/anthropic-claude.md +203 -0
- package/data/harnesses/code/skills/design/design-md/library/apple.md +206 -0
- package/data/harnesses/code/skills/design/design-md/library/binance.md +210 -0
- package/data/harnesses/code/skills/design/design-md/library/bmw.md +210 -0
- package/data/harnesses/code/skills/design/design-md/library/cal.md +209 -0
- package/data/harnesses/code/skills/design/design-md/library/clay.md +204 -0
- package/data/harnesses/code/skills/design/design-md/library/clickhouse.md +205 -0
- package/data/harnesses/code/skills/design/design-md/library/cohere.md +205 -0
- package/data/harnesses/code/skills/design/design-md/library/coinbase.md +198 -0
- package/data/harnesses/code/skills/design/design-md/library/composio.md +198 -0
- package/data/harnesses/code/skills/design/design-md/library/cursor.md +197 -0
- package/data/harnesses/code/skills/design/design-md/library/elevenlabs.md +199 -0
- package/data/harnesses/code/skills/design/design-md/library/expo.md +200 -0
- package/data/harnesses/code/skills/design/design-md/library/figma.md +199 -0
- package/data/harnesses/code/skills/design/design-md/library/framer.md +202 -0
- package/data/harnesses/code/skills/design/design-md/library/hashicorp.md +205 -0
- package/data/harnesses/code/skills/design/design-md/library/ibm.md +205 -0
- package/data/harnesses/code/skills/design/design-md/library/intercom.md +204 -0
- package/data/harnesses/code/skills/design/design-md/library/kraken.md +200 -0
- package/data/harnesses/code/skills/design/design-md/library/linear-app.md +198 -0
- package/data/harnesses/code/skills/design/design-md/library/lovable.md +199 -0
- package/data/harnesses/code/skills/design/design-md/library/meta.md +204 -0
- package/data/harnesses/code/skills/design/design-md/library/minimax.md +204 -0
- package/data/harnesses/code/skills/design/design-md/library/mintlify.md +210 -0
- package/data/harnesses/code/skills/design/design-md/library/miro.md +209 -0
- package/data/harnesses/code/skills/design/design-md/library/mistral-ai.md +201 -0
- package/data/harnesses/code/skills/design/design-md/library/mongodb.md +201 -0
- package/data/harnesses/code/skills/design/design-md/library/notion.md +204 -0
- package/data/harnesses/code/skills/design/design-md/library/nvidia.md +215 -0
- package/data/harnesses/code/skills/design/design-md/library/ollama.md +213 -0
- package/data/harnesses/code/skills/design/design-md/library/opencode-ai.md +216 -0
- package/data/harnesses/code/skills/design/design-md/library/posthog.md +218 -0
- package/data/harnesses/code/skills/design/design-md/library/raycast.md +214 -0
- package/data/harnesses/code/skills/design/design-md/library/replicate.md +202 -0
- package/data/harnesses/code/skills/design/design-md/library/resend.md +203 -0
- package/data/harnesses/code/skills/design/design-md/library/revolut.md +203 -0
- package/data/harnesses/code/skills/design/design-md/library/runwayml.md +203 -0
- package/data/harnesses/code/skills/design/design-md/library/sanity.md +200 -0
- package/data/harnesses/code/skills/design/design-md/library/sentry.md +201 -0
- package/data/harnesses/code/skills/design/design-md/library/shopify.md +202 -0
- package/data/harnesses/code/skills/design/design-md/library/slack.md +202 -0
- package/data/harnesses/code/skills/design/design-md/library/spacex.md +206 -0
- package/data/harnesses/code/skills/design/design-md/library/spotify.md +206 -0
- package/data/harnesses/code/skills/design/design-md/library/stripe.md +196 -0
- package/data/harnesses/code/skills/design/design-md/library/supabase.md +196 -0
- package/data/harnesses/code/skills/design/design-md/library/superhuman.md +194 -0
- package/data/harnesses/code/skills/design/design-md/library/tesla.md +198 -0
- package/data/harnesses/code/skills/design/design-md/library/theverge.md +199 -0
- package/data/harnesses/code/skills/design/design-md/library/together-ai.md +193 -0
- package/data/harnesses/code/skills/design/design-md/library/uber.md +195 -0
- package/data/harnesses/code/skills/design/design-md/library/vercel.md +195 -0
- package/data/harnesses/code/skills/design/design-md/library/voltagent.md +197 -0
- package/data/harnesses/code/skills/design/design-md/library/warp.md +196 -0
- package/data/harnesses/code/skills/design/design-md/library/webflow.md +197 -0
- package/data/harnesses/code/skills/design/design-md/library/wired.md +197 -0
- package/data/harnesses/code/skills/design/design-md/library/wise.md +198 -0
- package/data/harnesses/code/skills/design/design-md/library/x-ai.md +199 -0
- package/data/harnesses/code/skills/design/design-md/library/zapier.md +198 -0
- package/data/harnesses/code/skills/design/design-md/nurix.md +214 -0
- package/data/harnesses/code/skills/design/design-md/variants/editorial-luxury.md +184 -0
- package/data/harnesses/code/skills/design/design-md/variants/expressive-saas.md +216 -0
- package/data/harnesses/code/skills/design/design-md/variants/quiet-premium.md +211 -0
- package/data/harnesses/code/skills/design/design-md/variants/terse-dense-operator.md +212 -0
- package/data/harnesses/code/skills/design/design-md/variants/warm-approachable.md +216 -0
- package/data/harnesses/code/skills/design/references/compose.md +23 -0
- package/data/harnesses/code/skills/design/references/content.md +28 -0
- package/data/harnesses/code/skills/design/references/emission.md +21 -0
- package/data/harnesses/code/skills/design/references/existing-apps.md +27 -0
- package/data/harnesses/code/skills/design/references/intake.md +36 -0
- package/data/harnesses/code/skills/design/references/object-pass.md +24 -0
- package/data/harnesses/code/skills/design/references/ship-gate.md +29 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/Readme.md +195 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/ai-layer.md +49 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/attributes-lifecycle.md +43 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/container-choice.md +44 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/creation-paths.md +46 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/dashboards.md +42 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/modes.md +42 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/object-model.md +46 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/page-archetype.md +48 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/promotion-demotion.md +37 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/settings.md +42 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/standard-pages.md +46 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/state-matrix.md +41 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/table-mechanics.md +48 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/verbs.md +45 -0
- package/data/harnesses/code/skills/design/references/ux-patterns/viewer-context.md +40 -0
- package/data/harnesses/code/skills/design/scripts/README-validator.md +27 -0
- package/data/harnesses/code/skills/design/scripts/select-chart.mjs +596 -0
- package/data/harnesses/code/skills/design/scripts/validate-design-md.mjs +1198 -0
- package/data/harnesses/discussion/agents/discussion-baseline.md +1 -1
- package/data/harnesses/discussion/harness.json +1 -1
- package/data/harnesses/etna/agents/maintenance.md +1 -1
- package/data/harnesses/etna/harness.json +1 -1
- package/data/harnesses/hr/CLAUDE.md +1 -1
- package/data/harnesses/hr/harness.json +1 -1
- package/package.json +1 -1
- package/data/harnesses/code/skills/design/data/catalog/rules.csv +0 -137
- package/data/harnesses/code/skills/design/data/combinations/Readme.md +0 -33
- package/data/harnesses/code/skills/design/data/combinations/accent-presets.md +0 -113
- package/data/harnesses/code/skills/design/data/combinations/charts.csv +0 -33
- package/data/harnesses/code/skills/design/data/combinations/fonts-known.json +0 -448
- package/data/harnesses/code/skills/design/data/combinations/icons.csv +0 -157
- package/data/harnesses/code/skills/design/data/combinations/landing.csv +0 -38
- package/data/harnesses/code/skills/design/data/combinations/lucide-names.json +0 -2001
- package/data/harnesses/code/skills/design/data/combinations/manifest.json +0 -330
- package/data/harnesses/code/skills/design/data/combinations/motion.csv +0 -10
- package/data/harnesses/code/skills/design/data/combinations/palettes.csv +0 -92
- package/data/harnesses/code/skills/design/data/combinations/product-router.csv +0 -80
- package/data/harnesses/code/skills/design/data/combinations/styles.csv +0 -67
- package/data/harnesses/code/skills/design/data/combinations/typography.csv +0 -64
- package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +0 -106
- package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +0 -154
- package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +0 -151
- package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +0 -139
- package/data/harnesses/code/skills/design/data/design-md/library/apple.md +0 -139
- package/data/harnesses/code/skills/design/data/design-md/library/binance.md +0 -140
- package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +0 -140
- package/data/harnesses/code/skills/design/data/design-md/library/cal.md +0 -140
- package/data/harnesses/code/skills/design/data/design-md/library/clay.md +0 -139
- package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +0 -139
- package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +0 -147
- package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +0 -156
- package/data/harnesses/code/skills/design/data/design-md/library/composio.md +0 -138
- package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +0 -138
- package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +0 -137
- package/data/harnesses/code/skills/design/data/design-md/library/expo.md +0 -148
- package/data/harnesses/code/skills/design/data/design-md/library/figma.md +0 -141
- package/data/harnesses/code/skills/design/data/design-md/library/framer.md +0 -156
- package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +0 -140
- package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +0 -153
- package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +0 -158
- package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +0 -156
- package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +0 -122
- package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +0 -140
- package/data/harnesses/code/skills/design/data/design-md/library/meta.md +0 -139
- package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +0 -139
- package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +0 -139
- package/data/harnesses/code/skills/design/data/design-md/library/miro.md +0 -153
- package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +0 -140
- package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +0 -155
- package/data/harnesses/code/skills/design/data/design-md/library/notion.md +0 -140
- package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +0 -141
- package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +0 -145
- package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +0 -142
- package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +0 -152
- package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +0 -137
- package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +0 -140
- package/data/harnesses/code/skills/design/data/design-md/library/resend.md +0 -170
- package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +0 -145
- package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +0 -137
- package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +0 -168
- package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +0 -163
- package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +0 -147
- package/data/harnesses/code/skills/design/data/design-md/library/slack.md +0 -158
- package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +0 -171
- package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +0 -166
- package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +0 -160
- package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +0 -173
- package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +0 -145
- package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +0 -153
- package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +0 -167
- package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +0 -140
- package/data/harnesses/code/skills/design/data/design-md/library/uber.md +0 -140
- package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +0 -140
- package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +0 -130
- package/data/harnesses/code/skills/design/data/design-md/library/warp.md +0 -140
- package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +0 -140
- package/data/harnesses/code/skills/design/data/design-md/library/wired.md +0 -139
- package/data/harnesses/code/skills/design/data/design-md/library/wise.md +0 -137
- package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +0 -138
- package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +0 -158
- package/data/harnesses/code/skills/design/data/design-md/nurix.md +0 -138
- package/data/harnesses/code/skills/design/data/design-tokens/SCHEMA.md +0 -695
- package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.tokens.json +0 -1272
- package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.tokens.json +0 -1268
- package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.tokens.json +0 -1273
- package/data/harnesses/code/skills/design/data/design-tokens/apple/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/apple/design.tokens.json +0 -1262
- package/data/harnesses/code/skills/design/data/design-tokens/binance/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/binance/design.tokens.json +0 -1257
- package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.tokens.json +0 -1270
- package/data/harnesses/code/skills/design/data/design-tokens/cal/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/cal/design.tokens.json +0 -1270
- package/data/harnesses/code/skills/design/data/design-tokens/clay/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/clay/design.tokens.json +0 -1261
- package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.tokens.json +0 -1268
- package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.tokens.json +0 -1281
- package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.tokens.json +0 -1262
- package/data/harnesses/code/skills/design/data/design-tokens/composio/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/composio/design.tokens.json +0 -1271
- package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.tokens.json +0 -1276
- package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.tokens.json +0 -1293
- package/data/harnesses/code/skills/design/data/design-tokens/expo/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/expo/design.tokens.json +0 -1270
- package/data/harnesses/code/skills/design/data/design-tokens/figma/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/figma/design.tokens.json +0 -1276
- package/data/harnesses/code/skills/design/data/design-tokens/framer/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/framer/design.tokens.json +0 -1267
- package/data/harnesses/code/skills/design/data/design-tokens/goldens.json +0 -251
- package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.tokens.json +0 -1273
- package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.tokens.json +0 -1286
- package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.tokens.json +0 -1290
- package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.tokens.json +0 -1270
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.resolver.json +0 -23
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.dark.json +0 -599
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.json +0 -1272
- package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.tokens.json +0 -1267
- package/data/harnesses/code/skills/design/data/design-tokens/meta/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/meta/design.tokens.json +0 -1289
- package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.tokens.json +0 -1258
- package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.tokens.json +0 -1284
- package/data/harnesses/code/skills/design/data/design-tokens/miro/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/miro/design.tokens.json +0 -1291
- package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.tokens.json +0 -1263
- package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.tokens.json +0 -1270
- package/data/harnesses/code/skills/design/data/design-tokens/notion/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/notion/design.tokens.json +0 -1261
- package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.tokens.json +0 -1209
- package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.tokens.json +0 -1265
- package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.tokens.json +0 -1253
- package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.tokens.json +0 -1268
- package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.tokens.json +0 -1261
- package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.tokens.json +0 -1263
- package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.tokens.json +0 -1266
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.resolver.json +0 -23
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.dark.json +0 -623
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.json +0 -1276
- package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.tokens.json +0 -1264
- package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.tokens.json +0 -1268
- package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.tokens.json +0 -1270
- package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.tokens.json +0 -1288
- package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.tokens.json +0 -1272
- package/data/harnesses/code/skills/design/data/design-tokens/slack/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/slack/design.tokens.json +0 -1265
- package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.tokens.json +0 -1287
- package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.tokens.json +0 -1286
- package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.tokens.json +0 -1285
- package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.tokens.json +0 -1287
- package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.tokens.json +0 -1278
- package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.tokens.json +0 -1269
- package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.tokens.json +0 -1285
- package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.tokens.json +0 -1303
- package/data/harnesses/code/skills/design/data/design-tokens/uber/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/uber/design.tokens.json +0 -1274
- package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.tokens.json +0 -1296
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.resolver.json +0 -23
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.dark.json +0 -612
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.json +0 -1282
- package/data/harnesses/code/skills/design/data/design-tokens/warp/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/warp/design.tokens.json +0 -1287
- package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.tokens.json +0 -1268
- package/data/harnesses/code/skills/design/data/design-tokens/wired/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/wired/design.tokens.json +0 -1276
- package/data/harnesses/code/skills/design/data/design-tokens/wise/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/wise/design.tokens.json +0 -1273
- package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.tokens.json +0 -1296
- package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.resolver.json +0 -20
- package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.tokens.json +0 -1286
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +0 -249
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +0 -249
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +0 -225
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +0 -249
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +0 -224
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +0 -249
- package/data/harnesses/code/skills/design/data/operator-pack/fixtures.md +0 -88
- package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +0 -132
- package/data/harnesses/code/skills/design/data/operator-pack/trust-rubric.md +0 -76
- package/data/harnesses/code/skills/design/references/create.md +0 -173
- package/data/harnesses/code/skills/design/references/diagnosis.md +0 -108
- package/data/harnesses/code/skills/design/references/hard-rules.md +0 -177
- package/data/harnesses/code/skills/design/references/improve.md +0 -116
- package/data/harnesses/code/skills/design/references/interaction.md +0 -77
- package/data/harnesses/code/skills/design/references/introspect.md +0 -100
- package/data/harnesses/code/skills/design/references/lifecycle.md +0 -73
- package/data/harnesses/code/skills/design/references/personality.md +0 -575
- package/data/harnesses/code/skills/design/references/structure.md +0 -159
- package/data/harnesses/code/skills/design/scripts/convert-design-md.mjs +0 -1312
- package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +0 -1498
- package/data/harnesses/code/skills/design/scripts/generate-fixtures.mjs +0 -551
- package/data/harnesses/code/skills/design/scripts/lib/checks.mjs +0 -1092
- package/data/harnesses/code/skills/design/scripts/lib/golden.mjs +0 -26
- package/data/harnesses/code/skills/design/scripts/lib/vocabulary.mjs +0 -65
- package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +0 -538
- package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +0 -312
- package/data/harnesses/code/skills/design/scripts/render-rung.mjs +0 -890
- package/data/harnesses/code/skills/design/scripts/scaffold.mjs +0 -769
- package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +0 -675
- package/data/harnesses/code/skills/design/scripts/snapshot-goldens.mjs +0 -164
- package/data/harnesses/code/skills/design/scripts/validate-combinations.mjs +0 -439
|
@@ -1,1498 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// skills/design/scripts/derive-personality.mjs — design-tokens artifact resolver, coverage gate (--check; SCHEMA.md rules 1–15), and CSS emitter (--emit) for the operator-UI personality system (D29, D6).
|
|
3
|
-
//
|
|
4
|
-
// The reference consumer of data/design-tokens/SCHEMA.md (schema design-tokens/v2): resolves an
|
|
5
|
-
// artifact directory (design.tokens.json + design.resolver.json [+ design.tokens.dark.json]) through
|
|
6
|
-
// the resolver's deterministic order — concatenate the set files, overlay the selected theme context,
|
|
7
|
-
// resolve aliases — then validates (rules 1–15) or emits personality.css / globals.css / fonts.css /
|
|
8
|
-
// the <html> attributes. --emit refuses while any token carries a hint; --provisional (golden/
|
|
9
|
-
// regression flows only, never shipped output) emits hinted tokens at their provisional $values
|
|
10
|
-
// instead. --emit also enforces two emission contracts: font binding (contract 2, cod-16 — every
|
|
11
|
-
// emitted stack family binds to a generated @fontsource import / installed package or a system
|
|
12
|
-
// generic, fail-closed) and emitter completeness (contract 3 — an authored token group with no
|
|
13
|
-
// emitter refuses to emit, naming the group).
|
|
14
|
-
//
|
|
15
|
-
// Usage: derive-personality.mjs <artifact-dir> [--check | --emit <outdir> [--provisional]] [--theme light|dark]
|
|
16
|
-
|
|
17
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
-
import { dirname, join } from "node:path";
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { SYSTEM_FONT_KEYWORDS, familySlug } from "./lib/checks.mjs";
|
|
21
|
-
|
|
22
|
-
const EXT = "com.nurix.design";
|
|
23
|
-
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
const COMBINATIONS_DIR = join(SCRIPT_DIR, "..", "data", "combinations");
|
|
25
|
-
|
|
26
|
-
// The 27 visual primitives and their DTCG $type, in emission order (leaf names are law; no leading --).
|
|
27
|
-
const PRIMITIVE_TYPES = new Map([
|
|
28
|
-
["radius", "dimension"], ["shadow-intensity", "number"], ["border-weight", "dimension"],
|
|
29
|
-
["density-gap", "number"], ["density-inset", "number"],
|
|
30
|
-
["font-sans", "fontFamily"], ["font-mono", "fontFamily"],
|
|
31
|
-
["font-weight-normal", "fontWeight"], ["font-weight-strong", "fontWeight"],
|
|
32
|
-
["heading-tracking", "number"],
|
|
33
|
-
["duration-unit", "duration"], ["duration-fast", "duration"], ["duration-normal", "duration"],
|
|
34
|
-
["duration-slow", "duration"], ["ease-default", "cubicBezier"], ["stagger-unit", "duration"],
|
|
35
|
-
["contrast-boost", "number"],
|
|
36
|
-
["icon-stroke", "number"], ["icon-size-base", "dimension"],
|
|
37
|
-
["sidebar-width-expanded", "dimension"], ["sidebar-width-collapsed", "dimension"],
|
|
38
|
-
["topbar-height", "dimension"], ["side-panel-width", "dimension"],
|
|
39
|
-
["side-panel-width-rich", "dimension"], ["modal-width-max", "dimension"],
|
|
40
|
-
["form-max-width", "dimension"], ["table-row-height", "dimension"],
|
|
41
|
-
]);
|
|
42
|
-
const PRIMITIVES = [...PRIMITIVE_TYPES.keys()];
|
|
43
|
-
|
|
44
|
-
// The 19 semantic color tokens, in emission order.
|
|
45
|
-
const SEMANTIC_TOKENS = [
|
|
46
|
-
"background", "foreground", "card", "card-foreground", "muted", "muted-foreground",
|
|
47
|
-
"border", "input", "ring", "primary", "primary-foreground", "accent", "accent-foreground",
|
|
48
|
-
"destructive", "destructive-foreground", "success", "warning",
|
|
49
|
-
"surface-raised", "surface-overlay",
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
// The 17 substrate color tokens (the D6 set), in emission order.
|
|
53
|
-
const SUBSTRATE_TOKENS = [
|
|
54
|
-
"popover", "popover-foreground", "secondary", "secondary-foreground",
|
|
55
|
-
"chart-1", "chart-2", "chart-3", "chart-4", "chart-5",
|
|
56
|
-
"sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground",
|
|
57
|
-
"sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring",
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
const ALL_TOKENS = [...SEMANTIC_TOKENS, ...SUBSTRATE_TOKENS];
|
|
61
|
-
|
|
62
|
-
const RAMP_STEPS = ["h1", "h2", "h3", "body", "small", "caption", "display"];
|
|
63
|
-
const ELEVATION_TOKENS = ["sm", "md", "lg", "overlay"];
|
|
64
|
-
const WASH_TOKENS = ["surface", "emphasis"];
|
|
65
|
-
const LEGAL_VERBOSITY = ["full", "default", "compact", "expert"];
|
|
66
|
-
const LEGAL_FEEDBACK = ["subtle", "minimal"];
|
|
67
|
-
const LEGAL_DENSITY = ["spacious", "standard", "dense"];
|
|
68
|
-
const LEGAL_PROVENANCE = ["project", "extracted", "library", "default"];
|
|
69
|
-
const LEGAL_TYPES = ["color", "dimension", "duration", "cubicBezier", "fontFamily", "fontWeight", "number", "typography", "shadow", "gradient"];
|
|
70
|
-
const LEGAL_THEMES = ["light", "dark"];
|
|
71
|
-
const MANIFEST_KEYS = ["spacing-scale", "radius-scale", "shadow-levels", "motion", "icons", "accent-count"];
|
|
72
|
-
|
|
73
|
-
// The optional persisted intake look block's enums — mirrored from phase-gate.mjs validateLookBlock
|
|
74
|
-
// (the intake authority) so a persisted payload.look cannot rot unchecked.
|
|
75
|
-
const LOOK_THEMES = ["light", "dark", "both"];
|
|
76
|
-
const LOOK_DELIGHTS = ["baseline", "expressive"];
|
|
77
|
-
const LOOK_RESOLUTION_MODES = ["confirmed", "recorded"];
|
|
78
|
-
const LOOK_RESOLUTION_RE = /^(project|default|library:.+|seed:.+\+.+)$/i;
|
|
79
|
-
|
|
80
|
-
// Contract 3 — the emitter registry: every authored token group's named consumer in the emitted
|
|
81
|
-
// output. --emit refuses an authored group absent from this map (a group with no emitter vanishes).
|
|
82
|
-
const EMITTED_GROUPS = new Map([
|
|
83
|
-
["primitive", "the [data-style] primitives block"],
|
|
84
|
-
["color", "the armored theme blocks (:root, :root[data-theme=\"light\"] / :root[data-theme=\"dark\"])"],
|
|
85
|
-
["typography", "the @theme ramp text utilities (text-h1 … text-display)"],
|
|
86
|
-
["elevation", "the --shadow-sm/md/lg/overlay composites + @theme shadow utilities"],
|
|
87
|
-
["wash", "--wash-surface / --wash-emphasis (optional presence)"],
|
|
88
|
-
]);
|
|
89
|
-
const TYPOGRAPHY_SUBGROUPS = ["family", "ramp"];
|
|
90
|
-
|
|
91
|
-
// Legal $description opening tags (rule 15): the provenance-layer vocabulary (source/brief/deviation,
|
|
92
|
-
// 00-layering.md Composition A) plus the established mechanical-annotation prefixes.
|
|
93
|
-
const LEGAL_DESCRIPTION_TAGS = ["source", "brief", "deviation", "derived", "corpus", "rationale", "note", "synthesized", "brand", "dark"];
|
|
94
|
-
|
|
95
|
-
// Shadow-level names ranked by increasing intensity, for the ordering check.
|
|
96
|
-
const SHADOW_RANK = { none: 0, "2xs": 1, xs: 2, sm: 3, md: 4, lg: 5, xl: 6, "2xl": 7, overlay: 8 };
|
|
97
|
-
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// CSS value parsers — the parse-back authority (rule 9); byte-compatible with convert-design-md.mjs
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
|
|
102
|
-
// Round to at most 5 decimals (the srgb component precision).
|
|
103
|
-
function round5(n) {
|
|
104
|
-
return Math.round(n * 1e5) / 1e5;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Strip one layer of matching surrounding quotes and unescape \" within double quotes.
|
|
108
|
-
function unquote(raw) {
|
|
109
|
-
const s = raw.trim();
|
|
110
|
-
if (s.length >= 2 && s[0] === '"' && s.at(-1) === '"') return s.slice(1, -1).replaceAll('\\"', '"');
|
|
111
|
-
if (s.length >= 2 && s[0] === "'" && s.at(-1) === "'") return s.slice(1, -1);
|
|
112
|
-
return s;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Split on a delimiter at bracket-depth 0 and outside quotes.
|
|
116
|
-
function splitTopLevel(str, delim) {
|
|
117
|
-
const parts = [];
|
|
118
|
-
let depth = 0, inDouble = false, inSingle = false, cur = "";
|
|
119
|
-
for (let i = 0; i < str.length; i++) {
|
|
120
|
-
const c = str[i];
|
|
121
|
-
if (c === '"' && !inSingle && str[i - 1] !== "\\") { inDouble = !inDouble; cur += c; }
|
|
122
|
-
else if (c === "'" && !inDouble) { inSingle = !inSingle; cur += c; }
|
|
123
|
-
else if (!inDouble && !inSingle && (c === "[" || c === "{" || c === "(")) { depth++; cur += c; }
|
|
124
|
-
else if (!inDouble && !inSingle && (c === "]" || c === "}" || c === ")")) { depth--; cur += c; }
|
|
125
|
-
else if (!inDouble && !inSingle && depth === 0 && c === delim) { parts.push(cur); cur = ""; }
|
|
126
|
-
else cur += c;
|
|
127
|
-
}
|
|
128
|
-
parts.push(cur);
|
|
129
|
-
return parts;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Parse any corpus color string (oklch / #rrggbb / #rrggbbaa / rgb() / rgba()) into a typed DTCG color $value.
|
|
133
|
-
function parseColorCss(css) {
|
|
134
|
-
const s = css.trim();
|
|
135
|
-
let m = /^oklch\(\s*([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s*(?:\/\s*([\d.]+%?)\s*)?\)$/.exec(s);
|
|
136
|
-
if (m) {
|
|
137
|
-
const value = { colorSpace: "oklch", components: [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3])] };
|
|
138
|
-
if (m[4] !== undefined) value.alpha = m[4].endsWith("%") ? round5(parseFloat(m[4]) / 100) : parseFloat(m[4]);
|
|
139
|
-
return value;
|
|
140
|
-
}
|
|
141
|
-
m = /^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})?$/.exec(s);
|
|
142
|
-
if (m) {
|
|
143
|
-
const bytes = [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16));
|
|
144
|
-
const value = { colorSpace: "srgb", components: bytes.map((b) => round5(b / 255)), hex: `#${m[1].toLowerCase()}` };
|
|
145
|
-
if (m[2] !== undefined) value.alpha = round5(parseInt(m[2], 16) / 255);
|
|
146
|
-
return value;
|
|
147
|
-
}
|
|
148
|
-
m = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)$/.exec(s);
|
|
149
|
-
if (!m) m = /^rgba?\(\s*(\d+)\s+(\d+)\s+(\d+)\s*(?:\/\s*([\d.]+%?)\s*)?\)$/.exec(s); // modern space syntax
|
|
150
|
-
if (m) {
|
|
151
|
-
const bytes = [m[1], m[2], m[3]].map(Number);
|
|
152
|
-
const hex = "#" + bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
153
|
-
const value = { colorSpace: "srgb", components: bytes.map((b) => round5(b / 255)), hex };
|
|
154
|
-
if (m[4] !== undefined) value.alpha = m[4].endsWith("%") ? round5(parseFloat(m[4]) / 100) : parseFloat(m[4]);
|
|
155
|
-
return value;
|
|
156
|
-
}
|
|
157
|
-
throw new Error(`unparseable color: ${JSON.stringify(css)}`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Parse one shadow-list string (the elevation byte form) into the DTCG layer array. A layer color's
|
|
161
|
-
// alpha may ride the master multiplier as calc(A * var(--shadow-intensity)) — A is the layer alpha.
|
|
162
|
-
function parseShadowCss(css) {
|
|
163
|
-
return splitTopLevel(css, ",").map((layerRaw) => {
|
|
164
|
-
const s = layerRaw.trim();
|
|
165
|
-
const colorStart = s.search(/rgba?\(|oklch\(|hsla?\(|#[0-9a-fA-F]/);
|
|
166
|
-
if (colorStart < 0) throw new Error(`shadow layer names no color: ${JSON.stringify(s)}`);
|
|
167
|
-
const colorCss = s.slice(colorStart).trim().replace(/calc\(\s*([\d.]+)\s*\*\s*var\(--shadow-intensity\)\s*\)/, "$1");
|
|
168
|
-
const dims = s.slice(0, colorStart).trim().split(/\s+/);
|
|
169
|
-
if (dims.length < 3 || dims.length > 4) throw new Error(`shadow layer wants 3-4 lengths (offsetX offsetY blur [spread]): ${JSON.stringify(s)}`);
|
|
170
|
-
const dim = (d) => (d === "0" ? { value: 0, unit: "px" } : parseDimensionCss(d));
|
|
171
|
-
return {
|
|
172
|
-
color: parseColorCss(colorCss),
|
|
173
|
-
offsetX: dim(dims[0]),
|
|
174
|
-
offsetY: dim(dims[1]),
|
|
175
|
-
blur: dim(dims[2]),
|
|
176
|
-
spread: dims[3] !== undefined ? dim(dims[3]) : { value: 0, unit: "px" },
|
|
177
|
-
};
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Parse a tracking string ("0" | "0em" | "-0.015em") into its em-multiplier number.
|
|
182
|
-
function parseTrackingCss(css) {
|
|
183
|
-
const m = /^([+-]?\d*\.?\d+)(em)?$/.exec(css.trim());
|
|
184
|
-
if (!m) throw new Error(`unparseable tracking: ${JSON.stringify(css)}`);
|
|
185
|
-
return parseFloat(m[1]);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Parse "cubic-bezier(x1,y1,x2,y2)" into its four numbers.
|
|
189
|
-
function parseCubicBezierCss(css) {
|
|
190
|
-
const m = /^cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)$/.exec(css.trim());
|
|
191
|
-
if (!m) throw new Error(`unparseable cubic-bezier: ${JSON.stringify(css)}`);
|
|
192
|
-
return [1, 2, 3, 4].map((i) => parseFloat(m[i]));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Parse a font stack string into its array of unquoted family names.
|
|
196
|
-
function parseFontStackCss(css) {
|
|
197
|
-
return splitTopLevel(css, ",").map((p) => unquote(p)).filter((p) => p !== "");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Parse a ramp letterSpacing string ("+0.02em" | "-0.28px" | "0") into an em multiplier, px scaled by fontSize.
|
|
201
|
-
function parseLetterSpacingCss(css, fontSizePx) {
|
|
202
|
-
const m = /^([+-]?\d*\.?\d+)(em|px)?$/.exec(css.trim());
|
|
203
|
-
if (!m) throw new Error(`unparseable letterSpacing: ${JSON.stringify(css)}`);
|
|
204
|
-
const n = parseFloat(m[1]);
|
|
205
|
-
return m[2] === "px" ? round5(n / fontSizePx) : n;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function parseDimensionCss(css) {
|
|
209
|
-
const m = /^(-?\d*\.?\d+)px$/.exec(css.trim());
|
|
210
|
-
if (!m) throw new Error(`unparseable dimension: ${JSON.stringify(css)}`);
|
|
211
|
-
return { value: parseFloat(m[1]), unit: "px" };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function parseDurationCss(css) {
|
|
215
|
-
const m = /^(\d*\.?\d+)ms$/.exec(css.trim());
|
|
216
|
-
if (!m) throw new Error(`unparseable duration: ${JSON.stringify(css)}`);
|
|
217
|
-
return { value: parseFloat(m[1]), unit: "ms" };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function parseNumberCss(css) {
|
|
221
|
-
const n = parseFloat(css.trim());
|
|
222
|
-
if (Number.isNaN(n) || !/^[+-]?\d*\.?\d+$/.test(css.trim())) throw new Error(`unparseable number: ${JSON.stringify(css)}`);
|
|
223
|
-
return n;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ---------------------------------------------------------------------------
|
|
227
|
-
// Serialization — the schema's deterministic byte forms for tokens without preserved .css
|
|
228
|
-
// ---------------------------------------------------------------------------
|
|
229
|
-
|
|
230
|
-
// Serialize a concrete $value to its deterministic CSS byte form (the fallback when .css is absent).
|
|
231
|
-
function serializeValue(type, value) {
|
|
232
|
-
if (type === "dimension" || type === "duration") return `${value.value}${value.unit}`;
|
|
233
|
-
if (type === "number" || type === "fontWeight") return String(value);
|
|
234
|
-
if (type === "cubicBezier") return `cubic-bezier(${value.join(", ")})`;
|
|
235
|
-
if (type === "fontFamily") {
|
|
236
|
-
return (Array.isArray(value) ? value : [value]).map((f) => (/\s/.test(f) ? `"${f}"` : f)).join(", ");
|
|
237
|
-
}
|
|
238
|
-
if (type === "color") {
|
|
239
|
-
if (value.colorSpace === "srgb" && value.hex) return value.hex;
|
|
240
|
-
return `oklch(${value.components.join(" ")}${value.alpha !== undefined ? ` / ${value.alpha}` : ""})`;
|
|
241
|
-
}
|
|
242
|
-
if (type === "shadow") {
|
|
243
|
-
// Deterministic fallback (the migration always preserves .css): zero lengths serialize bare "0";
|
|
244
|
-
// the layer alpha rides the master multiplier so --shadow-intensity keeps flattening shadows.
|
|
245
|
-
const dim = (d) => (d.value === 0 ? "0" : `${d.value}${d.unit}`);
|
|
246
|
-
const layerColor = (c) => {
|
|
247
|
-
const alpha = c.alpha ?? 1;
|
|
248
|
-
if (c.colorSpace === "srgb") {
|
|
249
|
-
const bytes = c.components.map((n) => Math.round(n * 255));
|
|
250
|
-
return `rgb(${bytes.join(" ")} / calc(${alpha} * var(--shadow-intensity)))`;
|
|
251
|
-
}
|
|
252
|
-
return `oklch(${c.components.join(" ")} / calc(${alpha} * var(--shadow-intensity)))`;
|
|
253
|
-
};
|
|
254
|
-
return value.map((l) => `${dim(l.offsetX)} ${dim(l.offsetY)} ${dim(l.blur)} ${dim(l.spread ?? { value: 0, unit: "px" })} ${layerColor(l.color)}`).join(", ");
|
|
255
|
-
}
|
|
256
|
-
if (type === "gradient") {
|
|
257
|
-
// Deterministic fallback (authored washes preserve .css): a top-down tint ramp from the stop list.
|
|
258
|
-
const stops = value.map((s) => `${serializeValue("color", s.color)} ${Math.round(s.position * 100)}%`).join(", ");
|
|
259
|
-
return `linear-gradient(180deg, ${stops})`;
|
|
260
|
-
}
|
|
261
|
-
return String(value);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// ---------------------------------------------------------------------------
|
|
265
|
-
// Artifact loading & resolution (SCHEMA.md resolver section — deterministic order)
|
|
266
|
-
// ---------------------------------------------------------------------------
|
|
267
|
-
|
|
268
|
-
// True when a $value is an alias reference string ("{path.to.token}").
|
|
269
|
-
function isAlias(v) {
|
|
270
|
-
return typeof v === "string" && /^\{[^}]+\}$/.test(v);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// True when a node is a token (an object carrying $type).
|
|
274
|
-
function isToken(node) {
|
|
275
|
-
return node !== null && typeof node === "object" && !Array.isArray(node) && "$type" in node;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Collect every token in a tree as a path → token map ($-prefixed keys are metadata, not groups).
|
|
279
|
-
function collectTokens(root) {
|
|
280
|
-
const tokens = new Map();
|
|
281
|
-
const walk = (node, path) => {
|
|
282
|
-
if (isToken(node)) { tokens.set(path.join("."), node); return; }
|
|
283
|
-
if (node !== null && typeof node === "object" && !Array.isArray(node)) {
|
|
284
|
-
for (const [k, v] of Object.entries(node)) if (!k.startsWith("$")) walk(v, [...path, k]);
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
walk(root, []);
|
|
288
|
-
return tokens;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Overlay one modifier file onto the base at token-path granularity; an overlay may only
|
|
292
|
-
// override paths that exist in the base, never introduce new ones.
|
|
293
|
-
function overlayTree(base, overlay, file, errors, path = []) {
|
|
294
|
-
for (const [key, value] of Object.entries(overlay)) {
|
|
295
|
-
if (key.startsWith("$")) continue;
|
|
296
|
-
const here = [...path, key];
|
|
297
|
-
if (!(key in base)) {
|
|
298
|
-
errors.push(`rule 1 — resolver overlay ${file}: introduces ${here.join(".")}, absent from the base set`);
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
if (isToken(value) || isToken(base[key]) || typeof value !== "object" || value === null) {
|
|
302
|
-
base[key] = value;
|
|
303
|
-
} else {
|
|
304
|
-
overlayTree(base[key], value, file, errors, here);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Concatenate a later set file onto the accumulated base (a later file overrides per token path; new paths are legal here).
|
|
310
|
-
function concatTree(base, add) {
|
|
311
|
-
for (const [key, value] of Object.entries(add)) {
|
|
312
|
-
if (key.startsWith("$")) continue;
|
|
313
|
-
if (!(key in base) || isToken(value) || isToken(base[key]) || typeof value !== "object" || value === null) {
|
|
314
|
-
base[key] = value;
|
|
315
|
-
} else {
|
|
316
|
-
concatTree(base[key], value);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Load and resolve one artifact directory: parse files (rule 1), concatenate sets, build one
|
|
322
|
-
// resolved view per theme context. Returns null when the artifact is unreadable.
|
|
323
|
-
function loadArtifact(dir, errors) {
|
|
324
|
-
const readJson = (name) => {
|
|
325
|
-
const path = join(dir, name);
|
|
326
|
-
if (!existsSync(path)) { errors.push(`rule 1 — ${name}: missing`); return null; }
|
|
327
|
-
try {
|
|
328
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
329
|
-
} catch (e) {
|
|
330
|
-
errors.push(`rule 1 — ${name}: unparseable JSON (${e.message})`);
|
|
331
|
-
return null;
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
const base = readJson("design.tokens.json");
|
|
336
|
-
const resolver = readJson("design.resolver.json");
|
|
337
|
-
if (base === null || resolver === null) return null;
|
|
338
|
-
|
|
339
|
-
if (resolver.schema !== "design-resolver/v1") {
|
|
340
|
-
errors.push(`rule 1 — design.resolver.json: schema must be "design-resolver/v1" (found ${JSON.stringify(resolver.schema)})`);
|
|
341
|
-
}
|
|
342
|
-
if (!Array.isArray(resolver.sets) || resolver.sets.length === 0) {
|
|
343
|
-
errors.push("rule 1 — design.resolver.json: sets must be a non-empty array");
|
|
344
|
-
return null;
|
|
345
|
-
}
|
|
346
|
-
const themeModifier = (resolver.modifiers ?? []).find((m) => m?.name === "theme");
|
|
347
|
-
for (const m of resolver.modifiers ?? []) {
|
|
348
|
-
if (m?.name !== "theme") errors.push(`rule 1 — design.resolver.json: unknown modifier ${JSON.stringify(m?.name)} (exactly one modifier today: theme)`);
|
|
349
|
-
}
|
|
350
|
-
if (!themeModifier) {
|
|
351
|
-
errors.push("rule 1 — design.resolver.json: missing the theme modifier");
|
|
352
|
-
return null;
|
|
353
|
-
}
|
|
354
|
-
if (!LEGAL_THEMES.includes(themeModifier.default)) errors.push(`rule 1 — design.resolver.json: theme default must be light|dark (found ${JSON.stringify(themeModifier.default)}); it mirrors the payload's theme-default`);
|
|
355
|
-
if (!Array.isArray(themeModifier.contexts?.light) || themeModifier.contexts.light.length !== 0) {
|
|
356
|
-
errors.push("rule 1 — design.resolver.json: the light context must be the empty overlay [] (the base set is the light rendering)");
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Every file the resolver references exists beside it — and parses.
|
|
360
|
-
const files = new Map([["design.tokens.json", base]]);
|
|
361
|
-
const referenced = new Set(["design.tokens.json"]);
|
|
362
|
-
const loadReferenced = (name, where) => {
|
|
363
|
-
referenced.add(name);
|
|
364
|
-
if (files.has(name)) return;
|
|
365
|
-
const doc = readJson(name);
|
|
366
|
-
if (doc === null) errors.push(`rule 1 — resolver ${where} references ${name}`);
|
|
367
|
-
else files.set(name, doc);
|
|
368
|
-
};
|
|
369
|
-
for (const set of resolver.sets) {
|
|
370
|
-
for (const name of set?.values ?? []) loadReferenced(name, `set "${set.name}"`);
|
|
371
|
-
}
|
|
372
|
-
for (const [context, names] of Object.entries(themeModifier.contexts ?? {})) {
|
|
373
|
-
for (const name of names ?? []) loadReferenced(name, `theme context "${context}"`);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// The dark file exists exactly when the resolver's dark context names it.
|
|
377
|
-
const darkNamed = (themeModifier.contexts?.dark ?? []).includes("design.tokens.dark.json");
|
|
378
|
-
const darkExists = existsSync(join(dir, "design.tokens.dark.json"));
|
|
379
|
-
if (darkNamed && !darkExists) errors.push("rule 1 — design.tokens.dark.json: named by the resolver's dark context but absent");
|
|
380
|
-
if (!darkNamed && darkExists) errors.push("rule 1 — design.tokens.dark.json: present but not named by a resolver dark context");
|
|
381
|
-
|
|
382
|
-
// Resolution: concatenate the set files in array order, then one view per theme context.
|
|
383
|
-
const merged = {};
|
|
384
|
-
for (const set of resolver.sets) {
|
|
385
|
-
for (const name of set?.values ?? []) {
|
|
386
|
-
if (files.has(name)) concatTree(merged, files.get(name));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
const views = new Map();
|
|
390
|
-
for (const [context, names] of Object.entries(themeModifier.contexts ?? {})) {
|
|
391
|
-
const view = structuredClone(merged);
|
|
392
|
-
for (const name of names ?? []) {
|
|
393
|
-
if (files.has(name)) overlayTree(view, files.get(name), name, errors);
|
|
394
|
-
}
|
|
395
|
-
views.set(context, view);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return { base, resolver, files, views, payload: base?.$extensions?.[EXT] };
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Follow an alias chain to its final concrete token; records rule-6 errors (dangling refs, cycles, type mismatches).
|
|
402
|
-
function resolveAlias(ref, tokens, expectedType, context, errors) {
|
|
403
|
-
const seen = new Set();
|
|
404
|
-
let cur = ref;
|
|
405
|
-
while (isAlias(cur)) {
|
|
406
|
-
const path = cur.slice(1, -1);
|
|
407
|
-
if (seen.has(path)) { errors.push(`rule 6 — ${context}: alias cycle through {${path}}`); return null; }
|
|
408
|
-
seen.add(path);
|
|
409
|
-
const target = tokens.get(path);
|
|
410
|
-
if (!target) { errors.push(`rule 6 — ${context}: alias {${path}} does not resolve`); return null; }
|
|
411
|
-
if (expectedType && target.$type !== expectedType) {
|
|
412
|
-
errors.push(`rule 6 — ${context}: alias {${path}} resolves to $type ${target.$type} (want ${expectedType})`);
|
|
413
|
-
return null;
|
|
414
|
-
}
|
|
415
|
-
if (!isAlias(target.$value)) return target;
|
|
416
|
-
cur = target.$value;
|
|
417
|
-
}
|
|
418
|
-
return null;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// The emitted CSS string for a token path: follow aliases, prefer the preserved .css, fall back
|
|
422
|
-
// to the schema's deterministic serialization.
|
|
423
|
-
function cssFor(path, tokens, errors) {
|
|
424
|
-
const token = tokens.get(path);
|
|
425
|
-
if (!token) { errors.push(`emit — ${path}: token not found`); return ""; }
|
|
426
|
-
let target = token;
|
|
427
|
-
if (isAlias(token.$value)) {
|
|
428
|
-
target = resolveAlias(token.$value, tokens, token.$type, path, errors);
|
|
429
|
-
if (!target) return "";
|
|
430
|
-
}
|
|
431
|
-
const css = target.$extensions?.[EXT]?.css;
|
|
432
|
-
if (typeof css === "string") return css;
|
|
433
|
-
return serializeValue(target.$type, target.$value);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// ---------------------------------------------------------------------------
|
|
437
|
-
// Validation — SCHEMA.md rules 1–12 (rule 12 is enforced at emit time)
|
|
438
|
-
// ---------------------------------------------------------------------------
|
|
439
|
-
|
|
440
|
-
// True when a token's $value is absent or vacuously empty.
|
|
441
|
-
function isEmptyValue(v) {
|
|
442
|
-
return v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Verify a preserved-css color token: parse-back equality plus the srgb hex round-trip.
|
|
446
|
-
function verifyColorParseBack(name, token, errors) {
|
|
447
|
-
const css = token.$extensions?.[EXT]?.css;
|
|
448
|
-
if (typeof css !== "string" || isAlias(token.$value)) return;
|
|
449
|
-
let reparsed;
|
|
450
|
-
try { reparsed = parseColorCss(css); } catch (e) { errors.push(`rule 9 — ${name}: ${e.message}`); return; }
|
|
451
|
-
if (JSON.stringify(reparsed) !== JSON.stringify(token.$value)) {
|
|
452
|
-
errors.push(`rule 9 — ${name}: parse-back mismatch — css ${JSON.stringify(css)} → ${JSON.stringify(reparsed)} != $value ${JSON.stringify(token.$value)}`);
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
if (token.$value.colorSpace === "srgb") {
|
|
456
|
-
const viaHex = parseColorCss(token.$value.hex);
|
|
457
|
-
if (JSON.stringify(viaHex.components) !== JSON.stringify(token.$value.components)) {
|
|
458
|
-
errors.push(`rule 9 — ${name}: srgb components do not round-trip through hex ${token.$value.hex}`);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Rule 9 for one non-color token carrying a preserved string .css: parsing it must reproduce $value.
|
|
464
|
-
function verifyScalarParseBack(name, token, css, errors) {
|
|
465
|
-
if (isAlias(token.$value)) return; // an aliased token takes the target's .css; rule 6 owns alias integrity
|
|
466
|
-
try {
|
|
467
|
-
if (token.$type === "cubicBezier") {
|
|
468
|
-
if (JSON.stringify(parseCubicBezierCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
|
|
469
|
-
} else if (token.$type === "fontFamily") {
|
|
470
|
-
if (JSON.stringify(parseFontStackCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
|
|
471
|
-
} else if (token.$type === "number") {
|
|
472
|
-
if (parseTrackingCss(css) !== token.$value) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value ${token.$value}`);
|
|
473
|
-
} else if (token.$type === "dimension") {
|
|
474
|
-
if (JSON.stringify(parseDimensionCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
|
|
475
|
-
} else if (token.$type === "duration") {
|
|
476
|
-
if (JSON.stringify(parseDurationCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
|
|
477
|
-
} else if (token.$type === "fontWeight") {
|
|
478
|
-
if (parseNumberCss(css) !== token.$value) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value ${token.$value}`);
|
|
479
|
-
}
|
|
480
|
-
} catch (e) {
|
|
481
|
-
errors.push(`rule 9 — ${name}: ${e.message}`);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Run the full coverage gate (rules 1–15) over one artifact directory; returns everything emit needs.
|
|
486
|
-
// requestedTheme null = boot theme follows the payload's theme-default (light when absent).
|
|
487
|
-
function runCheck(dir, requestedTheme) {
|
|
488
|
-
const errors = [];
|
|
489
|
-
const artifact = loadArtifact(dir, errors);
|
|
490
|
-
if (!artifact) return { errors };
|
|
491
|
-
const { base, resolver, files, views, payload } = artifact;
|
|
492
|
-
|
|
493
|
-
// Rule 2 — the file-level payload on the base root.
|
|
494
|
-
if (!payload || typeof payload !== "object") {
|
|
495
|
-
errors.push(`rule 2 — payload: missing $extensions["${EXT}"] on the base file's root group`);
|
|
496
|
-
return { errors };
|
|
497
|
-
}
|
|
498
|
-
if (payload.schema !== "design-tokens/v2") errors.push(`rule 2 — payload.schema: must be "design-tokens/v2" (found ${JSON.stringify(payload.schema)})`);
|
|
499
|
-
if (payload.dtcg !== "2025.10") errors.push(`rule 2 — payload.dtcg: must be "2025.10" (found ${JSON.stringify(payload.dtcg)})`);
|
|
500
|
-
if (typeof payload.product !== "string" || payload.product.trim() === "") errors.push("rule 2 — payload.product: missing or empty");
|
|
501
|
-
if (!LEGAL_PROVENANCE.includes(payload.provenance)) errors.push(`rule 2 — payload.provenance: must be one of ${LEGAL_PROVENANCE.join("|")} (found ${JSON.stringify(payload.provenance)})`);
|
|
502
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(payload.sourced ?? "")) errors.push(`rule 2 — payload.sourced: must be an ISO date (found ${JSON.stringify(payload.sourced)})`);
|
|
503
|
-
if (!LEGAL_VERBOSITY.includes(payload.verbosity)) errors.push(`rule 2 — payload.verbosity: must be one of ${LEGAL_VERBOSITY.join("|")} (found ${JSON.stringify(payload.verbosity)})`);
|
|
504
|
-
if (!LEGAL_FEEDBACK.includes(payload.feedback)) errors.push(`rule 2 — payload.feedback: must be one of ${LEGAL_FEEDBACK.join("|")} (found ${JSON.stringify(payload.feedback)})`);
|
|
505
|
-
if (!LEGAL_DENSITY.includes(payload["density-tier"])) errors.push(`rule 2 — payload.density-tier: must be one of ${LEGAL_DENSITY.join("|")} (found ${JSON.stringify(payload["density-tier"])})`);
|
|
506
|
-
if (!Array.isArray(payload["product-traits"]) || payload["product-traits"].length === 0) errors.push("rule 2 — payload.product-traits: must be a non-empty array");
|
|
507
|
-
if (payload["theme-default"] !== undefined && !LEGAL_THEMES.includes(payload["theme-default"])) {
|
|
508
|
-
errors.push(`rule 2 — payload.theme-default: must be light|dark when present (found ${JSON.stringify(payload["theme-default"])})`);
|
|
509
|
-
}
|
|
510
|
-
const themeDefault = LEGAL_THEMES.includes(payload["theme-default"]) ? payload["theme-default"] : "light";
|
|
511
|
-
const resolverDefault = (resolver.modifiers ?? []).find((m) => m?.name === "theme")?.default;
|
|
512
|
-
if (LEGAL_THEMES.includes(resolverDefault) && resolverDefault !== themeDefault) {
|
|
513
|
-
errors.push(`rule 2 — design.resolver.json: theme default ${JSON.stringify(resolverDefault)} does not mirror payload theme-default ${JSON.stringify(themeDefault)}`);
|
|
514
|
-
}
|
|
515
|
-
const anchors = payload.anchors;
|
|
516
|
-
const anchorsShapeOk = Array.isArray(anchors) && anchors.length >= 2 && anchors.length <= 3 && anchors.every((a) => typeof a === "string" && a.trim() !== "");
|
|
517
|
-
if (anchors !== undefined && !anchorsShapeOk) errors.push(`rule 2 — payload.anchors: must be an array of 2–3 non-empty strings when present (found ${JSON.stringify(anchors)})`);
|
|
518
|
-
if (payload.provenance === "project" && anchors === undefined) errors.push("rule 2 — payload.anchors: required on provenance: project artifacts (the named real-product anchors from intake)");
|
|
519
|
-
// Rule 2 (look) — the optional persisted intake look block (SCHEMA.md, the payload): enum-checked
|
|
520
|
-
// when present; theme-default and anchors stay the canonical top-level projections emission consumes.
|
|
521
|
-
const look = payload.look;
|
|
522
|
-
if (look !== undefined) {
|
|
523
|
-
if (look === null || typeof look !== "object" || Array.isArray(look)) {
|
|
524
|
-
errors.push("rule 2 — payload.look: must be an object when present (the persisted intake look block)");
|
|
525
|
-
} else {
|
|
526
|
-
if (!LOOK_THEMES.includes(look.theme)) errors.push(`rule 2 — payload.look.theme: must be one of ${LOOK_THEMES.join("|")} (found ${JSON.stringify(look.theme)})`);
|
|
527
|
-
if (!LOOK_DELIGHTS.includes(look.delight)) errors.push(`rule 2 — payload.look.delight: must be one of ${LOOK_DELIGHTS.join("|")} (found ${JSON.stringify(look.delight)})`);
|
|
528
|
-
if (!LOOK_RESOLUTION_MODES.includes(look["resolution-mode"])) errors.push(`rule 2 — payload.look.resolution-mode: must be one of ${LOOK_RESOLUTION_MODES.join("|")} (found ${JSON.stringify(look["resolution-mode"])})`);
|
|
529
|
-
const lookAnchors = look.anchors;
|
|
530
|
-
if (!Array.isArray(lookAnchors) || lookAnchors.length < 2 || lookAnchors.length > 3 || !lookAnchors.every((a) => typeof a === "string" && a.trim() !== "")) {
|
|
531
|
-
errors.push(`rule 2 — payload.look.anchors: must be an array of 2–3 non-empty named strings (found ${JSON.stringify(lookAnchors)})`);
|
|
532
|
-
}
|
|
533
|
-
if (typeof look.resolution !== "string" || !LOOK_RESOLUTION_RE.test(look.resolution.trim())) {
|
|
534
|
-
errors.push(`rule 2 — payload.look.resolution: must match project | library:<slug> | seed:<style>+<palette> | default (found ${JSON.stringify(look.resolution)})`);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
const lm = payload["lever-manifest"];
|
|
539
|
-
for (const key of MANIFEST_KEYS) {
|
|
540
|
-
if (lm?.[key] === undefined) errors.push(`rule 2 — payload.lever-manifest.${key}: missing`);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
const theme = requestedTheme ?? themeDefault;
|
|
544
|
-
if (!views.has(theme)) {
|
|
545
|
-
errors.push(`rule 1 — resolver: theme ${theme} selected but the resolver carries no ${theme} context`);
|
|
546
|
-
return { errors };
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const lightView = views.get("light");
|
|
550
|
-
const lightTokens = collectTokens(lightView);
|
|
551
|
-
|
|
552
|
-
// Rule 3 — all 27 primitives present with the exact $type, each assigned or hinted.
|
|
553
|
-
for (const [key, type] of PRIMITIVE_TYPES) {
|
|
554
|
-
const t = lightView.primitive?.[key];
|
|
555
|
-
if (!isToken(t)) { errors.push(`rule 3 — primitive.${key}: missing`); continue; }
|
|
556
|
-
if (t.$type !== type) errors.push(`rule 3 — primitive.${key}: $type ${JSON.stringify(t.$type)} (want ${type})`);
|
|
557
|
-
if (isEmptyValue(t.$value)) errors.push(`rule 3 — primitive.${key}: empty $value`);
|
|
558
|
-
if (key === "stagger-unit" && typeof t.$value?.value === "number" && (t.$value.value < 0 || t.$value.value > 60)) {
|
|
559
|
-
errors.push(`rule 3 — primitive.stagger-unit: ${t.$value.value}ms outside the legal 0–60ms range`);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Rule 4 — all 36 color roles present, typed color, legal colorSpace, .css present (via alias when aliased).
|
|
564
|
-
for (const role of ALL_TOKENS) {
|
|
565
|
-
const t = lightView.color?.[role];
|
|
566
|
-
if (!isToken(t)) { errors.push(`rule 4 — color.${role}: missing`); continue; }
|
|
567
|
-
if (t.$type !== "color") { errors.push(`rule 4 — color.${role}: $type ${JSON.stringify(t.$type)} (want color)`); continue; }
|
|
568
|
-
const target = isAlias(t.$value) ? resolveAlias(t.$value, lightTokens, "color", `color.${role}`, []) : t;
|
|
569
|
-
if (!target) continue; // dangling alias — rule 6 reports it
|
|
570
|
-
if (!["oklch", "srgb"].includes(target.$value?.colorSpace)) errors.push(`rule 4 — color.${role}: colorSpace ${JSON.stringify(target.$value?.colorSpace)} (want oklch|srgb)`);
|
|
571
|
-
if (typeof target.$extensions?.[EXT]?.css !== "string") errors.push(`rule 4 — color.${role}: missing preserved .css`);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Rule 5 — typography coverage: the three families (display optional) and the seven-step ramp with all five sub-values.
|
|
575
|
-
for (const fam of ["heading", "body", "mono"]) {
|
|
576
|
-
const t = lightView.typography?.family?.[fam];
|
|
577
|
-
if (!isToken(t) || t.$type !== "fontFamily") errors.push(`rule 5 — typography.family.${fam}: missing or not $type fontFamily`);
|
|
578
|
-
}
|
|
579
|
-
const famDisplay = lightView.typography?.family?.display;
|
|
580
|
-
if (famDisplay !== undefined && (!isToken(famDisplay) || famDisplay.$type !== "fontFamily")) {
|
|
581
|
-
errors.push("rule 5 — typography.family.display: not $type fontFamily (the slot is optional; its shape is not)");
|
|
582
|
-
}
|
|
583
|
-
for (const step of RAMP_STEPS) {
|
|
584
|
-
const t = lightView.typography?.ramp?.[step];
|
|
585
|
-
if (!isToken(t) || t.$type !== "typography") { errors.push(`rule 5 — typography.ramp.${step}: missing or not $type typography`); continue; }
|
|
586
|
-
for (const sub of ["fontFamily", "fontSize", "fontWeight", "letterSpacing", "lineHeight"]) {
|
|
587
|
-
if (t.$value?.[sub] === undefined) errors.push(`rule 5 — typography.ramp.${step}.${sub}: missing sub-value`);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Rule 6 — alias integrity over every resolved context view (aliases resolve after overlay).
|
|
592
|
-
const SUB_TYPES = { fontFamily: "fontFamily", fontWeight: "fontWeight", letterSpacing: "number", fontSize: "dimension", lineHeight: "number" };
|
|
593
|
-
for (const [context, view] of views) {
|
|
594
|
-
const tokens = collectTokens(view);
|
|
595
|
-
const label = context === "light" ? "" : ` [${context}]`;
|
|
596
|
-
for (const [path, token] of tokens) {
|
|
597
|
-
if (isAlias(token.$value)) resolveAlias(token.$value, tokens, token.$type, `${path}${label}`, errors);
|
|
598
|
-
if (token.$type === "typography" && token.$value && typeof token.$value === "object") {
|
|
599
|
-
for (const [sub, want] of Object.entries(SUB_TYPES)) {
|
|
600
|
-
const v = token.$value[sub];
|
|
601
|
-
if (isAlias(v)) resolveAlias(v, tokens, want, `${path}.${sub}${label}`, errors);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Rule 7 — manifest scales.
|
|
608
|
-
if (lm && typeof lm === "object") {
|
|
609
|
-
const ascending = (arr) => arr.every((v, i) => i === 0 || v > arr[i - 1]);
|
|
610
|
-
for (const key of ["spacing-scale", "radius-scale"]) {
|
|
611
|
-
if (!Array.isArray(lm[key]) || lm[key].length === 0 || !ascending(lm[key])) errors.push(`rule 7 — lever-manifest.${key}: must be non-empty ascending (found ${JSON.stringify(lm[key])})`);
|
|
612
|
-
}
|
|
613
|
-
if (!Array.isArray(lm.motion?.durations) || lm.motion.durations.length === 0 || !ascending(lm.motion.durations)) errors.push(`rule 7 — lever-manifest.motion.durations: must be non-empty ascending (found ${JSON.stringify(lm.motion?.durations)})`);
|
|
614
|
-
if (!Array.isArray(lm.motion?.easings) || lm.motion.easings.length === 0) errors.push("rule 7 — lever-manifest.motion.easings: must be non-empty");
|
|
615
|
-
const shadows = lm["shadow-levels"];
|
|
616
|
-
const ranks = (Array.isArray(shadows) ? shadows : []).map((s) => SHADOW_RANK[s]);
|
|
617
|
-
if (ranks.length === 0 || ranks.some((r) => r === undefined) || !ascending(ranks)) {
|
|
618
|
-
errors.push(`rule 7 — lever-manifest.shadow-levels: must be non-empty, drawn from ${Object.keys(SHADOW_RANK).join(" ")}, strictly increasing (found ${JSON.stringify(shadows)})`);
|
|
619
|
-
}
|
|
620
|
-
if (typeof lm.icons?.family !== "string" || lm.icons.family === "") errors.push("rule 7 — lever-manifest.icons.family: missing or empty");
|
|
621
|
-
if (!(lm["accent-count"] >= 1)) errors.push(`rule 7 — lever-manifest.accent-count: must be >= 1 (found ${JSON.stringify(lm?.["accent-count"])})`);
|
|
622
|
-
|
|
623
|
-
// Rule 8 — the look's own base values sit on its own allowlist. An off-manifest value is legal
|
|
624
|
-
// only as a recorded deviation: its $description must open with the deviation: layer tag (rule 15).
|
|
625
|
-
const deviationTagged = (t) => typeof t?.$description === "string" && t.$description.startsWith("deviation:");
|
|
626
|
-
const radiusToken = lightView.primitive?.radius;
|
|
627
|
-
const radius = radiusToken?.$value?.value;
|
|
628
|
-
if (Array.isArray(lm["radius-scale"]) && radius !== undefined && !lm["radius-scale"].includes(radius) && !deviationTagged(radiusToken)) {
|
|
629
|
-
errors.push(`rule 8 — primitive.radius: ${radius} not on lever-manifest.radius-scale (off-manifest values need a deviation: $description)`);
|
|
630
|
-
}
|
|
631
|
-
for (const key of ["duration-fast", "duration-normal", "duration-slow"]) {
|
|
632
|
-
const t = lightView.primitive?.[key];
|
|
633
|
-
const ms = t?.$value?.value;
|
|
634
|
-
if (Array.isArray(lm.motion?.durations) && ms !== undefined && !lm.motion.durations.includes(ms) && !deviationTagged(t)) {
|
|
635
|
-
errors.push(`rule 8 — primitive.${key}: ${ms} not in lever-manifest.motion.durations (off-manifest values need a deviation: $description)`);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
const ease = lightView.primitive?.["ease-default"];
|
|
639
|
-
if (isToken(ease) && Array.isArray(lm.motion?.easings)) {
|
|
640
|
-
const easeCss = typeof ease.$extensions?.[EXT]?.css === "string" ? ease.$extensions[EXT].css : serializeValue("cubicBezier", ease.$value);
|
|
641
|
-
if (!lm.motion.easings.includes(easeCss) && !deviationTagged(ease)) errors.push(`rule 8 — primitive.ease-default: ${JSON.stringify(easeCss)} not in lever-manifest.motion.easings (off-manifest values need a deviation: $description)`);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Rule 9 — parse-back for every token carrying a preserved .css, per file.
|
|
646
|
-
for (const [fileName, doc] of files) {
|
|
647
|
-
const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
|
|
648
|
-
for (const [path, token] of collectTokens(doc)) {
|
|
649
|
-
const css = token.$extensions?.[EXT]?.css;
|
|
650
|
-
if (css === undefined) continue;
|
|
651
|
-
const name = `${prefix}${path}`;
|
|
652
|
-
if (token.$type === "color") {
|
|
653
|
-
verifyColorParseBack(name, token, errors);
|
|
654
|
-
} else if (token.$type === "shadow" && typeof css === "string") {
|
|
655
|
-
try {
|
|
656
|
-
const reparsed = parseShadowCss(css);
|
|
657
|
-
if (JSON.stringify(reparsed) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: shadow css ${JSON.stringify(css)} does not parse back to $value`);
|
|
658
|
-
} catch (e) {
|
|
659
|
-
errors.push(`rule 9 — ${name}: ${e.message}`);
|
|
660
|
-
}
|
|
661
|
-
} else if (token.$type === "gradient" && typeof css === "string") {
|
|
662
|
-
// A gradient's .css carries geometry the stop list does not; verify each stop color's byte form appears in it.
|
|
663
|
-
for (const [i, stop] of (Array.isArray(token.$value) ? token.$value : []).entries()) {
|
|
664
|
-
if (stop?.color && typeof stop.color === "object" && !css.includes(serializeValue("color", stop.color))) {
|
|
665
|
-
errors.push(`rule 9 — ${name}: stop ${i} color ${serializeValue("color", stop.color)} not present in the preserved .css`);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
} else if (typeof css === "string") {
|
|
669
|
-
verifyScalarParseBack(name, token, css, errors);
|
|
670
|
-
} else if (token.$type === "typography" && typeof css === "object" && css !== null) {
|
|
671
|
-
if (typeof css.letterSpacing === "string" && typeof token.$value?.letterSpacing === "number" && typeof token.$value?.fontSize?.value === "number") {
|
|
672
|
-
try {
|
|
673
|
-
const reparsed = parseLetterSpacingCss(css.letterSpacing, token.$value.fontSize.value);
|
|
674
|
-
if (reparsed !== token.$value.letterSpacing) errors.push(`rule 9 — ${name}: letterSpacing css ${css.letterSpacing} → ${reparsed} != ${token.$value.letterSpacing}`);
|
|
675
|
-
} catch (e) {
|
|
676
|
-
errors.push(`rule 9 — ${name}: ${e.message}`);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// Rule 10 — the dark file, when present, carries exactly the 36-role color group and nothing else.
|
|
684
|
-
const darkDoc = files.get("design.tokens.dark.json");
|
|
685
|
-
if (darkDoc !== undefined) {
|
|
686
|
-
const rootKeys = Object.keys(darkDoc).filter((k) => !k.startsWith("$"));
|
|
687
|
-
if (rootKeys.length !== 1 || rootKeys[0] !== "color") errors.push(`rule 10 — design.tokens.dark.json: root groups ${JSON.stringify(rootKeys)} (want exactly ["color"])`);
|
|
688
|
-
if (darkDoc.$extensions?.[EXT] !== undefined) errors.push("rule 10 — design.tokens.dark.json: carries a payload (the base file is the payload's single carrier)");
|
|
689
|
-
for (const role of ALL_TOKENS) {
|
|
690
|
-
const t = darkDoc.color?.[role];
|
|
691
|
-
if (!isToken(t)) { errors.push(`rule 10 — dark color.${role}: missing`); continue; }
|
|
692
|
-
if (t.$type !== "color") { errors.push(`rule 10 — dark color.${role}: $type ${JSON.stringify(t.$type)} (want color)`); continue; }
|
|
693
|
-
if (isAlias(t.$value)) continue; // resolves post-overlay; rules 4/6 cover it on the dark view
|
|
694
|
-
if (!["oklch", "srgb"].includes(t.$value?.colorSpace)) errors.push(`rule 10 — dark color.${role}: colorSpace ${JSON.stringify(t.$value?.colorSpace)} (want oklch|srgb)`);
|
|
695
|
-
if (typeof t.$extensions?.[EXT]?.css !== "string") errors.push(`rule 10 — dark color.${role}: missing preserved .css`);
|
|
696
|
-
}
|
|
697
|
-
for (const role of Object.keys(darkDoc.color ?? {})) {
|
|
698
|
-
if (!ALL_TOKENS.includes(role)) errors.push(`rule 10 — dark color.${role}: not a legal role`);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Rule 11 — only the ten legal $type values appear, in any file.
|
|
703
|
-
for (const [fileName, doc] of files) {
|
|
704
|
-
const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
|
|
705
|
-
for (const [path, token] of collectTokens(doc)) {
|
|
706
|
-
if (!LEGAL_TYPES.includes(token.$type)) errors.push(`rule 11 — ${prefix}${path}: illegal $type ${JSON.stringify(token.$type)}`);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// True when a shadow/wash layer color is a legal DTCG color value; records rule errors under `where`.
|
|
711
|
-
const checkLayerColor = (color, where, rule) => {
|
|
712
|
-
if (!color || typeof color !== "object" || !["oklch", "srgb"].includes(color.colorSpace) || !Array.isArray(color.components)) {
|
|
713
|
-
errors.push(`${rule} — ${where}: color must be a typed oklch|srgb value (found ${JSON.stringify(color)})`);
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
const isPureBlack = color.colorSpace === "srgb"
|
|
717
|
-
? color.components.every((c) => c === 0)
|
|
718
|
-
: color.components[0] === 0 && color.components[1] === 0;
|
|
719
|
-
if (rule === "rule 13" && isPureBlack && (color.alpha ?? 1) >= 0.15) {
|
|
720
|
-
errors.push(`${rule} — ${where}: pure black at alpha ${color.alpha ?? 1} (≥ 0.15) — shadow colors are brand-tintable, never a hard black drop`);
|
|
721
|
-
}
|
|
722
|
-
};
|
|
723
|
-
|
|
724
|
-
// Rule 13 — elevation coverage: all four elevation.* present, $type shadow, ≥ 1 layer each,
|
|
725
|
-
// every layer shape-legal and rule-conformant, the byte form preserved; each assigned or hinted.
|
|
726
|
-
for (const level of ELEVATION_TOKENS) {
|
|
727
|
-
const t = lightView.elevation?.[level];
|
|
728
|
-
if (!isToken(t)) { errors.push(`rule 13 — elevation.${level}: missing`); continue; }
|
|
729
|
-
if (t.$type !== "shadow") { errors.push(`rule 13 — elevation.${level}: $type ${JSON.stringify(t.$type)} (want shadow)`); continue; }
|
|
730
|
-
if (!Array.isArray(t.$value) || t.$value.length === 0) { errors.push(`rule 13 — elevation.${level}: $value must be a non-empty array of layers`); continue; }
|
|
731
|
-
for (const [i, layer] of t.$value.entries()) {
|
|
732
|
-
checkLayerColor(layer?.color, `elevation.${level} layer ${i}`, "rule 13");
|
|
733
|
-
for (const dim of ["offsetX", "offsetY", "blur"]) {
|
|
734
|
-
if (typeof layer?.[dim]?.value !== "number") errors.push(`rule 13 — elevation.${level} layer ${i}: ${dim} must be a dimension`);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
if (typeof t.$extensions?.[EXT]?.css !== "string") errors.push(`rule 13 — elevation.${level}: missing preserved .css (the serialized shadow list)`);
|
|
738
|
-
}
|
|
739
|
-
for (const level of Object.keys(lightView.elevation ?? {})) {
|
|
740
|
-
if (!level.startsWith("$") && !ELEVATION_TOKENS.includes(level)) errors.push(`rule 13 — elevation.${level}: not a legal level (sm|md|lg|overlay)`);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// Rule 14 — wash shape: the group is optional; when present, ≤ 2 gradient tokens named
|
|
744
|
-
// surface/emphasis with rule-conformant stops. Presence is never required.
|
|
745
|
-
if (lightView.wash !== undefined) {
|
|
746
|
-
const washKeys = Object.keys(lightView.wash).filter((k) => !k.startsWith("$"));
|
|
747
|
-
if (washKeys.length > 2) errors.push(`rule 14 — wash: at most 2 tokens (found ${washKeys.length})`);
|
|
748
|
-
for (const key of washKeys) {
|
|
749
|
-
if (!WASH_TOKENS.includes(key)) { errors.push(`rule 14 — wash.${key}: not a legal wash slot (surface|emphasis)`); continue; }
|
|
750
|
-
const t = lightView.wash[key];
|
|
751
|
-
if (!isToken(t) || t.$type !== "gradient") { errors.push(`rule 14 — wash.${key}: must be $type gradient`); continue; }
|
|
752
|
-
if (!Array.isArray(t.$value) || t.$value.length < 2) { errors.push(`rule 14 — wash.${key}: $value must be an array of ≥ 2 stops`); continue; }
|
|
753
|
-
for (const [i, stop] of t.$value.entries()) {
|
|
754
|
-
checkLayerColor(stop?.color, `wash.${key} stop ${i}`, "rule 14");
|
|
755
|
-
if (typeof stop?.position !== "number" || stop.position < 0 || stop.position > 1) errors.push(`rule 14 — wash.${key} stop ${i}: position must be a number in 0–1`);
|
|
756
|
-
}
|
|
757
|
-
if (typeof t.$extensions?.[EXT]?.css !== "string") errors.push(`rule 14 — wash.${key}: missing preserved .css (the authored gradient)`);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
// Rule 15 — provenance vocabulary: a per-value $description opening with a tag must use a legal
|
|
762
|
-
// one (source:/brief:/deviation: are the layer tags; untagged = inherited). Rule 8 enforces the
|
|
763
|
-
// off-manifest side (every off-manifest value carries deviation:).
|
|
764
|
-
for (const [fileName, doc] of files) {
|
|
765
|
-
const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
|
|
766
|
-
for (const [path, token] of collectTokens(doc)) {
|
|
767
|
-
const m = typeof token.$description === "string" && /^([a-z][a-z-]*):/.exec(token.$description);
|
|
768
|
-
if (m && !LEGAL_DESCRIPTION_TAGS.includes(m[1])) {
|
|
769
|
-
errors.push(`rule 15 — ${prefix}${path}: $description opens with unknown tag "${m[1]}:" (layer tags: source:|brief:|deviation:; untagged = inherited)`);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// Rule 12 inventory — every token still carrying a hint (passes --check; --emit refuses).
|
|
775
|
-
const hints = [];
|
|
776
|
-
for (const [fileName, doc] of files) {
|
|
777
|
-
const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
|
|
778
|
-
for (const [path, token] of collectTokens(doc)) {
|
|
779
|
-
const hint = token.$extensions?.[EXT]?.hint;
|
|
780
|
-
if (hint !== undefined) hints.push({ key: `${prefix}${path}`, text: String(hint) });
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
return { errors, payload, views, hints, theme, hasDark: files.has("design.tokens.dark.json"), hasWash: lightView.wash !== undefined };
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// ---------------------------------------------------------------------------
|
|
788
|
-
// Emitter feed — flatten the resolved views into the value map the emitters consume
|
|
789
|
-
// ---------------------------------------------------------------------------
|
|
790
|
-
|
|
791
|
-
// Assemble the emitters' document: 27 primitive CSS strings + the payload dials, the 36-role CSS
|
|
792
|
-
// string map per theme, the elevation/wash value strings, and the ramp steps as emitter-ready data
|
|
793
|
-
// (preferring each token's preserved .css throughout).
|
|
794
|
-
function buildEmitterDoc(views, payload, errors) {
|
|
795
|
-
const lightView = views.get("light");
|
|
796
|
-
const lightTokens = collectTokens(lightView);
|
|
797
|
-
const primitives = {};
|
|
798
|
-
for (const key of PRIMITIVES) primitives[key] = cssFor(`primitive.${key}`, lightTokens, errors);
|
|
799
|
-
primitives.verbosity = payload.verbosity;
|
|
800
|
-
primitives.feedback = payload.feedback;
|
|
801
|
-
primitives["density-tier"] = payload["density-tier"];
|
|
802
|
-
|
|
803
|
-
const colorTokens = { light: {} };
|
|
804
|
-
for (const role of ALL_TOKENS) colorTokens.light[role] = cssFor(`color.${role}`, lightTokens, errors);
|
|
805
|
-
|
|
806
|
-
if (views.has("dark")) {
|
|
807
|
-
const darkTokens = collectTokens(views.get("dark"));
|
|
808
|
-
colorTokens.dark = {};
|
|
809
|
-
for (const role of ALL_TOKENS) colorTokens.dark[role] = cssFor(`color.${role}`, darkTokens, errors);
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
const elevation = {};
|
|
813
|
-
for (const level of ELEVATION_TOKENS) elevation[level] = cssFor(`elevation.${level}`, lightTokens, errors);
|
|
814
|
-
|
|
815
|
-
let wash = null;
|
|
816
|
-
if (lightView.wash !== undefined) {
|
|
817
|
-
wash = {};
|
|
818
|
-
for (const key of WASH_TOKENS) {
|
|
819
|
-
if (isToken(lightView.wash[key])) wash[key] = cssFor(`wash.${key}`, lightTokens, errors);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// Ramp steps as @theme text-utility data: size/line-height as literals; weight and tracking as
|
|
824
|
-
// var() references when they alias a personality primitive, so the utilities track the cascade.
|
|
825
|
-
const ramp = {};
|
|
826
|
-
for (const step of RAMP_STEPS) {
|
|
827
|
-
const t = lightView.typography?.ramp?.[step];
|
|
828
|
-
if (!isToken(t) || !t.$value || typeof t.$value !== "object") { errors.push(`emit — typography.ramp.${step}: missing composite`); continue; }
|
|
829
|
-
const v = t.$value;
|
|
830
|
-
const weightVar = { "{primitive.font-weight-strong}": "var(--font-weight-strong)", "{primitive.font-weight-normal}": "var(--font-weight-normal)" };
|
|
831
|
-
let fontWeight;
|
|
832
|
-
if (typeof v.fontWeight === "number") fontWeight = String(v.fontWeight);
|
|
833
|
-
else if (weightVar[v.fontWeight]) fontWeight = weightVar[v.fontWeight];
|
|
834
|
-
else {
|
|
835
|
-
const target = resolveAlias(v.fontWeight, lightTokens, "fontWeight", `typography.ramp.${step}.fontWeight`, errors);
|
|
836
|
-
fontWeight = target ? String(target.$value) : "400";
|
|
837
|
-
}
|
|
838
|
-
const preservedTracking = t.$extensions?.[EXT]?.css?.letterSpacing;
|
|
839
|
-
let letterSpacing;
|
|
840
|
-
if (v.letterSpacing === "{primitive.heading-tracking}") letterSpacing = "var(--heading-tracking)";
|
|
841
|
-
else if (typeof preservedTracking === "string") letterSpacing = preservedTracking;
|
|
842
|
-
else if (typeof v.letterSpacing === "number") letterSpacing = `${v.letterSpacing}em`;
|
|
843
|
-
else {
|
|
844
|
-
const target = resolveAlias(v.letterSpacing, lightTokens, "number", `typography.ramp.${step}.letterSpacing`, errors);
|
|
845
|
-
letterSpacing = target ? `${target.$value}em` : "0em";
|
|
846
|
-
}
|
|
847
|
-
ramp[step] = {
|
|
848
|
-
fontSize: `${v.fontSize?.value}${v.fontSize?.unit ?? "px"}`,
|
|
849
|
-
lineHeight: String(v.lineHeight),
|
|
850
|
-
fontWeight,
|
|
851
|
-
letterSpacing,
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
return { primitives, elevation, wash, ramp, manifest: { "color-tokens": colorTokens } };
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// ---------------------------------------------------------------------------
|
|
859
|
-
// Contract 3 — emitter completeness: every authored group reaches consumable output
|
|
860
|
-
// ---------------------------------------------------------------------------
|
|
861
|
-
|
|
862
|
-
// Refuse emission when any resolved view carries an authored token group (or typography subgroup)
|
|
863
|
-
// absent from the emitter registry — machinery that accepts data it never emits is a silent breach.
|
|
864
|
-
function checkEmitterCompleteness(views, emitErrors) {
|
|
865
|
-
for (const [context, view] of views) {
|
|
866
|
-
const label = context === "light" ? "" : ` [${context}]`;
|
|
867
|
-
for (const group of Object.keys(view)) {
|
|
868
|
-
if (group.startsWith("$")) continue;
|
|
869
|
-
if (!EMITTED_GROUPS.has(group)) {
|
|
870
|
-
emitErrors.push(`emitter completeness — authored token group "${group}"${label} has no emitter (contract 3): an authored group with no emitter simply vanishes; add its emitter to this script or remove the group. Emitters exist for: ${[...EMITTED_GROUPS.keys()].join(", ")}.`);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
for (const sub of Object.keys(view.typography ?? {})) {
|
|
874
|
-
if (!sub.startsWith("$") && !TYPOGRAPHY_SUBGROUPS.includes(sub)) {
|
|
875
|
-
emitErrors.push(`emitter completeness — authored token group "typography.${sub}"${label} has no emitter (contract 3): only typography.family and typography.ramp reach the emitted output.`);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// ---------------------------------------------------------------------------
|
|
882
|
-
// Contract 2 — font binding: fonts.css generation + the fail-closed cod-16 gate
|
|
883
|
-
// ---------------------------------------------------------------------------
|
|
884
|
-
|
|
885
|
-
// Parse a CSV text into row objects keyed by its header (RFC quoting: "" escapes a quote inside a quoted field).
|
|
886
|
-
function parseCsv(text) {
|
|
887
|
-
const records = [];
|
|
888
|
-
let field = "", record = [], inQuotes = false;
|
|
889
|
-
const endField = () => { record.push(field); field = ""; };
|
|
890
|
-
const endRecord = () => { endField(); if (record.some((f) => f !== "")) records.push(record); record = []; };
|
|
891
|
-
for (let i = 0; i < text.length; i++) {
|
|
892
|
-
const c = text[i];
|
|
893
|
-
if (inQuotes) {
|
|
894
|
-
if (c === '"' && text[i + 1] === '"') { field += '"'; i++; }
|
|
895
|
-
else if (c === '"') inQuotes = false;
|
|
896
|
-
else field += c;
|
|
897
|
-
} else if (c === '"') inQuotes = true;
|
|
898
|
-
else if (c === ",") endField();
|
|
899
|
-
else if (c === "\n") endRecord();
|
|
900
|
-
else if (c !== "\r") field += c;
|
|
901
|
-
}
|
|
902
|
-
endRecord();
|
|
903
|
-
const header = records.shift() ?? [];
|
|
904
|
-
return records.map((cells) => Object.fromEntries(header.map((h, i) => [h, cells[i] ?? ""])));
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
// Load the family → registering-import resolver: typography.csv css-import rows first (paste-ready
|
|
908
|
-
// static @fontsource imports, registering the plain family names), fonts-known.json as the fallback
|
|
909
|
-
// oracle (family → static weights). Returns resolve(family) → { family, pkg, imports } | null.
|
|
910
|
-
function loadFontResolver() {
|
|
911
|
-
const byFamily = new Map();
|
|
912
|
-
try {
|
|
913
|
-
for (const row of parseCsv(readFileSync(join(COMBINATIONS_DIR, "typography.csv"), "utf8"))) {
|
|
914
|
-
for (const col of ["heading_family", "body_family", "mono_family"]) {
|
|
915
|
-
const family = (row[col] ?? "").trim();
|
|
916
|
-
if (family === "" || byFamily.has(family.toLowerCase())) continue;
|
|
917
|
-
const slug = familySlug(family);
|
|
918
|
-
const imports = [...String(row["css-import"] ?? "").matchAll(/@import\s+'(@fontsource\/([a-z0-9-]+)\/[^']+)'/g)]
|
|
919
|
-
.filter((m) => m[2] === slug)
|
|
920
|
-
.map((m) => `@import '${m[1]}';`);
|
|
921
|
-
if (imports.length > 0) byFamily.set(family.toLowerCase(), { family, pkg: `@fontsource/${slug}`, imports });
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
} catch { /* typography.csv unreadable — fonts-known.json stays the oracle */ }
|
|
925
|
-
let knownFamilies = {};
|
|
926
|
-
try { knownFamilies = JSON.parse(readFileSync(join(COMBINATIONS_DIR, "fonts-known.json"), "utf8")).families ?? {}; } catch { /* absent oracle degrades to csv-only resolution */ }
|
|
927
|
-
const knownLower = new Map(Object.entries(knownFamilies).map(([name, weights]) => [name.toLowerCase(), weights]));
|
|
928
|
-
return (family) => {
|
|
929
|
-
const hit = byFamily.get(family.toLowerCase());
|
|
930
|
-
if (hit) return hit;
|
|
931
|
-
const weights = knownLower.get(family.toLowerCase());
|
|
932
|
-
if (!Array.isArray(weights) || weights.length === 0) return null;
|
|
933
|
-
const slug = familySlug(family);
|
|
934
|
-
return { family, pkg: `@fontsource/${slug}`, imports: weights.map((w) => `@import '@fontsource/${slug}/${w}.css';`) };
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// Collect the artifact's font surfaces: the two emitted stacks (the cod-16 gate's subjects) and the
|
|
939
|
-
// deduped named families across every fontFamily token (import generation covers all of them).
|
|
940
|
-
function collectFontSurfaces(views, doc, errors) {
|
|
941
|
-
const lightTokens = collectTokens(views.get("light"));
|
|
942
|
-
const stacks = [
|
|
943
|
-
{ name: "--font-sans", families: parseFontStackCss(String(doc.primitives["font-sans"])) },
|
|
944
|
-
{ name: "--font-mono", families: parseFontStackCss(String(doc.primitives["font-mono"])) },
|
|
945
|
-
];
|
|
946
|
-
const importFamilies = [];
|
|
947
|
-
const seen = new Set();
|
|
948
|
-
const push = (family) => {
|
|
949
|
-
const key = family.toLowerCase();
|
|
950
|
-
if (!seen.has(key) && !SYSTEM_FONT_KEYWORDS.has(key)) { seen.add(key); importFamilies.push(family); }
|
|
951
|
-
};
|
|
952
|
-
for (const stack of stacks) for (const family of stack.families) push(family);
|
|
953
|
-
for (const slot of ["heading", "body", "mono", "display"]) {
|
|
954
|
-
if (!lightTokens.has(`typography.family.${slot}`)) continue;
|
|
955
|
-
for (const family of parseFontStackCss(cssFor(`typography.family.${slot}`, lightTokens, errors))) push(family);
|
|
956
|
-
}
|
|
957
|
-
return { stacks, importFamilies };
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// Read the emit target's dependency map, or null when the target is standalone (no package.json) —
|
|
961
|
-
// the standalone case degrades the cod-16 gate to import-list validation.
|
|
962
|
-
function readTargetDeps(outdir) {
|
|
963
|
-
const pkgPath = join(outdir, "package.json");
|
|
964
|
-
if (!existsSync(pkgPath)) return null;
|
|
965
|
-
try {
|
|
966
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
967
|
-
return { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
968
|
-
} catch {
|
|
969
|
-
return null;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
// The emitted src/styles/fonts.css content: the generated registering-import list, one derivation.
|
|
974
|
-
function emitFontsCss(entries, unresolved) {
|
|
975
|
-
const lines = [
|
|
976
|
-
"/* src/styles/fonts.css — @fontsource font registrations (emitted; do not hand-edit).",
|
|
977
|
-
" *",
|
|
978
|
-
" * One derivation, not two authored artifacts: this import list is GENERATED from the",
|
|
979
|
-
" * artifact's fontFamily tokens (typography.csv css-import rows; fonts-known.json fallback),",
|
|
980
|
-
" * and the emitted stacks use the same registered family names — so the flagship font cannot",
|
|
981
|
-
" * silently fail to bind (cod-16 gates the pair at --emit). Loaded FIRST by globals.css",
|
|
982
|
-
" * (step 1 of the five-step import order): faces register before any stack consumes them.",
|
|
983
|
-
" * To change a family, edit design.tokens.json and re-run derive-personality.mjs --emit. */",
|
|
984
|
-
];
|
|
985
|
-
for (const entry of entries) lines.push("", `/* ${entry.family} — ${entry.pkg} */`, ...entry.imports);
|
|
986
|
-
for (const family of unresolved) lines.push("", `/* UNRESOLVED: "${family}" — no known @fontsource package registers it; register it via @font-face or swap the family (cod-16). */`);
|
|
987
|
-
if (entries.length === 0 && unresolved.length === 0) lines.push("", "/* every stack family is a system/generic face — nothing to register. */");
|
|
988
|
-
return lines.join("\n") + "\n";
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// Contract 2 in full: generate fonts.css from the artifact's fontFamily tokens and run the cod-16
|
|
992
|
-
// binding gate over the emitted stacks — (a) each named family rides a generated import whose
|
|
993
|
-
// package is installed (project targets; standalone targets degrade to import-list validation), or
|
|
994
|
-
// (b) is a recognized system/generic family. Returns { fontsCss, failures }.
|
|
995
|
-
function buildFontBinding(views, doc, outdir, errors) {
|
|
996
|
-
const resolveFamily = loadFontResolver();
|
|
997
|
-
const { stacks, importFamilies } = collectFontSurfaces(views, doc, errors);
|
|
998
|
-
const deps = readTargetDeps(outdir);
|
|
999
|
-
const entries = [];
|
|
1000
|
-
const unresolved = [];
|
|
1001
|
-
for (const family of importFamilies) {
|
|
1002
|
-
const hit = resolveFamily(family);
|
|
1003
|
-
if (hit) { entries.push(hit); continue; }
|
|
1004
|
-
const variablePkg = `@fontsource-variable/${familySlug(family)}`;
|
|
1005
|
-
if (deps !== null && variablePkg in deps) {
|
|
1006
|
-
// The consumer already installed the variable package (registers the "<Family> Variable" name).
|
|
1007
|
-
entries.push({ family, pkg: variablePkg, imports: [`@import '${variablePkg}';`] });
|
|
1008
|
-
continue;
|
|
1009
|
-
}
|
|
1010
|
-
unresolved.push(family);
|
|
1011
|
-
}
|
|
1012
|
-
const entryByFamily = new Map(entries.map((entry) => [entry.family.toLowerCase(), entry]));
|
|
1013
|
-
const failures = [];
|
|
1014
|
-
for (const stack of stacks) {
|
|
1015
|
-
for (const family of stack.families) {
|
|
1016
|
-
const key = family.toLowerCase();
|
|
1017
|
-
if (SYSTEM_FONT_KEYWORDS.has(key)) continue;
|
|
1018
|
-
const entry = entryByFamily.get(key);
|
|
1019
|
-
const slug = familySlug(family);
|
|
1020
|
-
if (!entry) {
|
|
1021
|
-
failures.push(`"${family}" (emitted ${stack.name} stack): no known @fontsource package registers it (typography.csv, fonts-known.json${deps !== null ? ", package.json" : ""}) — install and register one (npm install @fontsource/${slug}, then re-run --emit) or swap to a bindable family / system generic.`);
|
|
1022
|
-
} else if (deps !== null && !(entry.pkg in deps) && !(`@fontsource/${slug}` in deps) && !(`@fontsource-variable/${slug}` in deps)) {
|
|
1023
|
-
failures.push(`"${family}" (emitted ${stack.name} stack): its registering import needs ${entry.pkg}, absent from package.json — npm install ${entry.pkg}`);
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
return { fontsCss: emitFontsCss(entries, unresolved), failures };
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// ---------------------------------------------------------------------------
|
|
1031
|
-
// Emitters (values come from the token artifact; non-comment output is v1 byte-identical)
|
|
1032
|
-
// ---------------------------------------------------------------------------
|
|
1033
|
-
|
|
1034
|
-
// Derive a rosterless data-style slug from the product name.
|
|
1035
|
-
function slugify(name) {
|
|
1036
|
-
return String(name || "project").toLowerCase().trim()
|
|
1037
|
-
.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// Quote multi-word font family names so the emitted CSS font stack is valid.
|
|
1041
|
-
function quoteFontStack(stack) {
|
|
1042
|
-
return String(stack).split(",").map((f) => {
|
|
1043
|
-
const t = f.trim();
|
|
1044
|
-
if (t === "") return t;
|
|
1045
|
-
if (t.startsWith('"') || t.startsWith("'")) return t;
|
|
1046
|
-
return /\s/.test(t) ? `"${t}"` : t;
|
|
1047
|
-
}).filter((t) => t !== "").join(", ");
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// Derive --ease-enter: reuse ease-default if it is already a decelerate curve, else the emphasized-decelerate arrival curve.
|
|
1051
|
-
function deriveEaseEnter(easeDefault) {
|
|
1052
|
-
const m = /cubic-bezier\(\s*[-\d.]+\s*,\s*([-\d.]+)/.exec(String(easeDefault));
|
|
1053
|
-
if (m && parseFloat(m[1]) >= 0.9) return easeDefault;
|
|
1054
|
-
return "cubic-bezier(0.16, 1, 0.3, 1)";
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
// Build the [data-style] primitive lines (governed density refs + derived temporals inserted after
|
|
1058
|
-
// the temporal family), then the elevation composites and the optional wash pair.
|
|
1059
|
-
function buildStyleBlockLines(doc) {
|
|
1060
|
-
const primitives = doc.primitives;
|
|
1061
|
-
const easeEnter = deriveEaseEnter(primitives["ease-default"]);
|
|
1062
|
-
const lines = [];
|
|
1063
|
-
for (const key of PRIMITIVES) {
|
|
1064
|
-
if (key === "density-gap") lines.push(" --density-gap: var(--density-base-gap);");
|
|
1065
|
-
else if (key === "density-inset") lines.push(" --density-inset: var(--density-base-inset);");
|
|
1066
|
-
else if (key === "font-sans") lines.push(` --font-sans: ${quoteFontStack(primitives["font-sans"])};`);
|
|
1067
|
-
else if (key === "font-mono") lines.push(` --font-mono: ${quoteFontStack(primitives["font-mono"])};`);
|
|
1068
|
-
else lines.push(` --${key}: ${primitives[key]};`);
|
|
1069
|
-
if (key === "stagger-unit") {
|
|
1070
|
-
lines.push(` --ease-enter: ${easeEnter}; /* derived from ease-default */`);
|
|
1071
|
-
lines.push(" --ease-exit: var(--ease-default); /* derived: departure mirrors the neutral curve */");
|
|
1072
|
-
lines.push(" --stagger-delay: var(--stagger-unit); /* derived: the motion-stagger cascade base */");
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
lines.push(" /* elevation (4) — designed shadow composites; --shadow-intensity stays the master multiplier */");
|
|
1076
|
-
for (const level of ELEVATION_TOKENS) lines.push(` --shadow-${level}: ${doc.elevation[level]};`);
|
|
1077
|
-
if (doc.wash) {
|
|
1078
|
-
lines.push(" /* wash (optional, <= 2) — low-chroma tint ramps, consumed only on budget-granted surfaces */");
|
|
1079
|
-
for (const key of WASH_TOKENS) {
|
|
1080
|
-
if (doc.wash[key] !== undefined) lines.push(` --wash-${key}: ${doc.wash[key]};`);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
return lines.join("\n");
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
// Emit the full src/styles/personality.css content for a passing artifact.
|
|
1087
|
-
function emitPersonalityCss(doc, slug, meta) {
|
|
1088
|
-
const p = doc.primitives;
|
|
1089
|
-
const light = doc.manifest["color-tokens"].light;
|
|
1090
|
-
const density = p["density-tier"];
|
|
1091
|
-
const verbosity = p.verbosity;
|
|
1092
|
-
const feedback = p.feedback;
|
|
1093
|
-
|
|
1094
|
-
const dark = doc.manifest["color-tokens"].dark;
|
|
1095
|
-
|
|
1096
|
-
const semanticLines = SEMANTIC_TOKENS.map((t) => ` --${t}: ${light[t]};`).join("\n");
|
|
1097
|
-
const substrateLines = SUBSTRATE_TOKENS.map((t) => ` --${t}: ${light[t]};`).join("\n");
|
|
1098
|
-
const styleLines = buildStyleBlockLines(doc);
|
|
1099
|
-
|
|
1100
|
-
const darkSection = dark
|
|
1101
|
-
? `/* ── Dark theme — OPT-IN ONLY (this look ships a dark overlay). ──
|
|
1102
|
-
* CASCADE CONTRACT 1b: :root[data-theme="dark"] (0,2,0) — a lone [data-theme="dark"]
|
|
1103
|
-
* attribute selector (0,1,0) loses to the library's later bare :root block by source
|
|
1104
|
-
* order. design.tokens.dark.json remaps all 36 color tokens; <html data-theme="dark">
|
|
1105
|
-
* opts in (theme-default decides the boot attribute). */
|
|
1106
|
-
:root[data-theme="dark"] {
|
|
1107
|
-
/* semantic (19) */
|
|
1108
|
-
${SEMANTIC_TOKENS.map((t) => ` --${t}: ${dark[t]};`).join("\n")}
|
|
1109
|
-
/* substrate (17, the D6 set) — popover/secondary/chart/sidebar the library consumes */
|
|
1110
|
-
${SUBSTRATE_TOKENS.map((t) => ` --${t}: ${dark[t]};`).join("\n")}
|
|
1111
|
-
}`
|
|
1112
|
-
: `/* ── Dark theme — OPT-IN ONLY. ──
|
|
1113
|
-
* Emitted only when the artifact ships a design.tokens.dark.json dark overlay,
|
|
1114
|
-
* as :root[data-theme="dark"] (0,2,0 — armored against the library's later :root).
|
|
1115
|
-
* This block never appears for a light-only project.
|
|
1116
|
-
:root[data-theme="dark"] {
|
|
1117
|
-
--background: ...; --foreground: ...; ... all 36 tokens remapped ...
|
|
1118
|
-
} */`;
|
|
1119
|
-
|
|
1120
|
-
return `/* src/styles/personality.css — operator-UI personality tokens (emitted; do not hand-edit).
|
|
1121
|
-
*
|
|
1122
|
-
* Active personality — code is the cache. These values and the <html data-*>
|
|
1123
|
-
* attributes in index.html are emitted from the design-tokens artifact. To change
|
|
1124
|
-
* the look, edit design.tokens.json and re-run derive-personality.mjs --emit.
|
|
1125
|
-
* style: ${slug} (<html data-style="${slug}">) — the project's own personality (rosterless)
|
|
1126
|
-
* density: ${density} (<html data-density="${density}">)
|
|
1127
|
-
* verbosity: ${verbosity} (<html data-verbosity="${verbosity}">)
|
|
1128
|
-
* feedback: ${feedback} (<html data-feedback="${feedback}">)
|
|
1129
|
-
* theme: ${meta.theme} (<html data-theme="${meta.theme}">) — the payload's theme-default decides the boot theme
|
|
1130
|
-
* artifact: design.tokens.json (resolved via design.resolver.json) — provenance: ${meta.provenance}, sourced ${meta.sourced}
|
|
1131
|
-
*
|
|
1132
|
-
* Semantic-token doctrine: components name purpose (bg-card, text-foreground, border-border);
|
|
1133
|
-
* this file resolves those names to values. Components never use dark: prefixes or literal colors.
|
|
1134
|
-
*/
|
|
1135
|
-
|
|
1136
|
-
/* ── System baseline: the 27 primitives at neutral operator defaults. ──
|
|
1137
|
-
* The safety net if <html> ever carries no data-style. The project's real
|
|
1138
|
-
* values live in the single [data-style="${slug}"] block below, which
|
|
1139
|
-
* overrides these. Improve/extract rewrites that block, never this baseline. */
|
|
1140
|
-
:root {
|
|
1141
|
-
/* geometric */
|
|
1142
|
-
--radius: 6px;
|
|
1143
|
-
--shadow-intensity: 1;
|
|
1144
|
-
--border-weight: 1px;
|
|
1145
|
-
--density-base-gap: 1; /* the density tier writes this; see the density mechanism */
|
|
1146
|
-
--density-base-inset: 1;
|
|
1147
|
-
--density-gap: var(--density-base-gap);
|
|
1148
|
-
--density-inset: var(--density-base-inset);
|
|
1149
|
-
/* typographic — generics only: the baseline never names a webfont it cannot
|
|
1150
|
-
* guarantee is registered (cod-16); the [data-style] block carries the real stacks. */
|
|
1151
|
-
--font-sans: system-ui, sans-serif;
|
|
1152
|
-
--font-mono: ui-monospace, SFMono-Regular, monospace;
|
|
1153
|
-
--font-weight-normal: 400;
|
|
1154
|
-
--font-weight-strong: 600;
|
|
1155
|
-
--heading-tracking: -0.01em;
|
|
1156
|
-
/* temporal (+ derived) */
|
|
1157
|
-
--duration-unit: 200ms;
|
|
1158
|
-
--duration-fast: calc(var(--duration-unit) * 0.5);
|
|
1159
|
-
--duration-normal: var(--duration-unit);
|
|
1160
|
-
--duration-slow: calc(var(--duration-unit) * 2);
|
|
1161
|
-
--ease-default: cubic-bezier(0.4, 0, 0.2, 1);
|
|
1162
|
-
--stagger-unit: 30ms;
|
|
1163
|
-
--ease-enter: cubic-bezier(0.16, 1, 0.3, 1);
|
|
1164
|
-
--ease-exit: var(--ease-default);
|
|
1165
|
-
--stagger-delay: var(--stagger-unit);
|
|
1166
|
-
/* surface */
|
|
1167
|
-
--contrast-boost: 1;
|
|
1168
|
-
/* elevation (neutral slate; the [data-style] block below carries the designed composites) */
|
|
1169
|
-
--shadow-sm: 0 1px 3px 0 rgb(15 23 42 / calc(0.10 * var(--shadow-intensity))), 0 1px 2px -1px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity)));
|
|
1170
|
-
--shadow-md: 0 4px 6px -1px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity))), 0 2px 4px -2px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity)));
|
|
1171
|
-
--shadow-lg: 0 10px 15px -3px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity))), 0 4px 6px -4px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity)));
|
|
1172
|
-
--shadow-overlay: 0 20px 25px -5px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity))), 0 8px 10px -6px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity)));
|
|
1173
|
-
/* iconography */
|
|
1174
|
-
--icon-stroke: 1.75;
|
|
1175
|
-
--icon-size-base: 16px;
|
|
1176
|
-
/* operator layout */
|
|
1177
|
-
--sidebar-width-expanded: 240px;
|
|
1178
|
-
--sidebar-width-collapsed: 56px;
|
|
1179
|
-
--topbar-height: 52px;
|
|
1180
|
-
--side-panel-width: 480px;
|
|
1181
|
-
--side-panel-width-rich: 640px;
|
|
1182
|
-
--modal-width-max: 480px;
|
|
1183
|
-
--form-max-width: 720px;
|
|
1184
|
-
--table-row-height: 40px;
|
|
1185
|
-
/* behavioral fallbacks (match verbosity=default + feedback=subtle) */
|
|
1186
|
-
--label-display: inline-flex;
|
|
1187
|
-
--helper-text-display: block;
|
|
1188
|
-
--tooltip-delay: 500ms;
|
|
1189
|
-
--feedback-press-scale: 0.99;
|
|
1190
|
-
--focus-ring-width: 2px;
|
|
1191
|
-
--focus-ring-offset: 1px;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
/* ── Semantic + substrate color tokens — light theme. ──
|
|
1195
|
-
* CASCADE CONTRACT 1: both theme blocks carry the same 0,2,0 armor — light as
|
|
1196
|
-
* :root, :root[data-theme="light"] and dark as :root[data-theme="dark"] — because
|
|
1197
|
-
* a bare :root or a lone attribute selector loses to the library's later :root
|
|
1198
|
-
* block in @nurix/components/styles.css by source order. All 36 tokens are set
|
|
1199
|
-
* (D6): no consumed token falls back to a shadcn-gray default. */
|
|
1200
|
-
:root,
|
|
1201
|
-
:root[data-theme="light"] {
|
|
1202
|
-
/* semantic (19) */
|
|
1203
|
-
${semanticLines}
|
|
1204
|
-
/* substrate (17, the D6 set) — popover/secondary/chart/sidebar the library consumes */
|
|
1205
|
-
${substrateLines}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
/* ── The project personality: the single [data-style] block (D3, rosterless). ──
|
|
1209
|
-
* Named after the project — not a preset. This materializes the artifact's
|
|
1210
|
-
* primitive token group (all 27 primitives + the derived temporals), the four
|
|
1211
|
-
* elevation composites, and the optional wash pair. It is the one block
|
|
1212
|
-
* improve/extract rewrites. density-gap/inset are governed refs, so the global
|
|
1213
|
-
* tier and per-surface step still compose. */
|
|
1214
|
-
[data-style="${slug}"] {
|
|
1215
|
-
${styleLines}
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
/* ── Behavioral dials (D4) — orthogonal to the visual primitives. ── */
|
|
1219
|
-
[data-verbosity="full"] { --label-display: inline-flex; --helper-text-display: block; --tooltip-delay: 800ms; }
|
|
1220
|
-
[data-verbosity="default"] { --label-display: inline-flex; --helper-text-display: block; --tooltip-delay: 500ms; }
|
|
1221
|
-
[data-verbosity="compact"] { --label-display: none; --helper-text-display: none; --tooltip-delay: 200ms; }
|
|
1222
|
-
[data-verbosity="expert"] { --label-display: none; --helper-text-display: none; --tooltip-delay: 0ms; }
|
|
1223
|
-
|
|
1224
|
-
[data-feedback="subtle"] { --feedback-press-scale: 0.99; --focus-ring-width: 2px; --focus-ring-offset: 1px; }
|
|
1225
|
-
[data-feedback="minimal"] { --feedback-press-scale: 1; --focus-ring-width: 1px; --focus-ring-offset: 0px; }
|
|
1226
|
-
|
|
1227
|
-
/* ── Density: global tier → base multipliers (D31). ──
|
|
1228
|
-
* The tier writes ONLY the base; --density-gap/inset resolve from it (above),
|
|
1229
|
-
* so a per-surface step can re-derive relative to the tier. */
|
|
1230
|
-
[data-density="dense"] { --density-base-gap: 0.85; --density-base-inset: 0.85; }
|
|
1231
|
-
[data-density="standard"] { --density-base-gap: 1; --density-base-inset: 1; }
|
|
1232
|
-
[data-density="spacious"] { --density-base-gap: 1.15; --density-base-inset: 1.15; }
|
|
1233
|
-
|
|
1234
|
-
/* ── Per-archetype density modifier (D31) — relative step on the tier, pure cascade, no JS. ──
|
|
1235
|
-
* A surface element carries data-density-step; it re-derives one step off the
|
|
1236
|
-
* inherited base. Reads --density-base-* (never itself) so it composes with the
|
|
1237
|
-
* tier and never absolutely overrides it. The recomputed --density-gap flows into
|
|
1238
|
-
* the library's p-* and gap-* through the @theme --spacing remap. */
|
|
1239
|
-
[data-density-step="tighter"] { --density-gap: calc(var(--density-base-gap) * 0.85); --density-inset: calc(var(--density-base-inset) * 0.85); }
|
|
1240
|
-
[data-density-step="looser"] { --density-gap: calc(var(--density-base-gap) * 1.15); --density-inset: calc(var(--density-base-inset) * 1.15); }
|
|
1241
|
-
|
|
1242
|
-
${darkSection}
|
|
1243
|
-
|
|
1244
|
-
/* ── Motion moments — named classes consuming the temporal tokens. ──
|
|
1245
|
-
* motion-lift: hover lift (-1px + elevation.sm) at duration-fast/ease-default.
|
|
1246
|
-
* motion-stagger: children arrival cascade at --stagger-delay, capped at 8
|
|
1247
|
-
* children (mot-08); consumption is budget-gated by the surface stamp. Both are
|
|
1248
|
-
* inert under prefers-reduced-motion (mot-03). */
|
|
1249
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
1250
|
-
.motion-lift {
|
|
1251
|
-
transition: transform var(--duration-fast) var(--ease-default), box-shadow var(--duration-fast) var(--ease-default);
|
|
1252
|
-
}
|
|
1253
|
-
.motion-lift:hover {
|
|
1254
|
-
transform: translateY(-1px);
|
|
1255
|
-
box-shadow: var(--shadow-sm);
|
|
1256
|
-
}
|
|
1257
|
-
@keyframes motion-stagger-arrive {
|
|
1258
|
-
from { opacity: 0; transform: translateY(4px); }
|
|
1259
|
-
to { opacity: 1; transform: translateY(0); }
|
|
1260
|
-
}
|
|
1261
|
-
.motion-stagger > * {
|
|
1262
|
-
animation: motion-stagger-arrive var(--duration-normal) var(--ease-enter) backwards;
|
|
1263
|
-
animation-delay: calc(var(--stagger-delay) * 7); /* children past the 8-cap arrive with the 8th */
|
|
1264
|
-
}
|
|
1265
|
-
${Array.from({ length: 8 }, (_, i) => ` .motion-stagger > :nth-child(${i + 1}) { animation-delay: calc(var(--stagger-delay) * ${i}); }`).join("\n")}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
/* ── Accessibility ── */
|
|
1269
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1270
|
-
:root, [data-style] {
|
|
1271
|
-
--duration-unit: 0ms; --stagger-unit: 0ms; --stagger-delay: 0ms;
|
|
1272
|
-
--ease-default: linear; --ease-enter: linear; --ease-exit: linear;
|
|
1273
|
-
--feedback-press-scale: 1;
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
@media (prefers-contrast: more) {
|
|
1277
|
-
:root, [data-style] {
|
|
1278
|
-
--contrast-boost: 1.15; --border-weight: 2px; --shadow-intensity: 0; --icon-stroke: 2;
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
`;
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
// Emit the full src/styles/globals.css content (Tailwind v4 glue + @theme inline mappings).
|
|
1285
|
-
function emitGlobalsCss(doc) {
|
|
1286
|
-
const p = doc.primitives;
|
|
1287
|
-
const fontSans = quoteFontStack(p["font-sans"]);
|
|
1288
|
-
const fontMono = quoteFontStack(p["font-mono"]);
|
|
1289
|
-
const fontWeightNormal = p["font-weight-normal"];
|
|
1290
|
-
const colorMaps = ALL_TOKENS.map((t) => ` --color-${t}: var(--${t});`).join("\n");
|
|
1291
|
-
|
|
1292
|
-
return `/* src/styles/globals.css — Tailwind v4 glue + @theme inline mappings (emitted). */
|
|
1293
|
-
|
|
1294
|
-
/* CASCADE CONTRACT 2: this five-step order is LOAD-BEARING. fonts.css registers the
|
|
1295
|
-
* @fontsource faces before any stack consumes them; the library's styles.css ships
|
|
1296
|
-
* its own :root token defaults and its own @theme radius scale, so our @theme MUST
|
|
1297
|
-
* come AFTER the import to win the radius scale and own every mapping.
|
|
1298
|
-
* rules/frontend.md states the same order — co-authority. */
|
|
1299
|
-
@import "./fonts.css"; /* 1. font registration (faces before stacks) */
|
|
1300
|
-
@import "./personality.css"; /* 2. token source */
|
|
1301
|
-
@import "tailwindcss"; /* 3. framework */
|
|
1302
|
-
@import "@nurix/components/styles.css"; /* 4. library (before our @theme) */
|
|
1303
|
-
|
|
1304
|
-
@theme inline { /* 5. consumer theme — AFTER the library, so it wins */
|
|
1305
|
-
/* radius scale — shadcn offsets on --radius, re-asserted so the CONSUMER owns it.
|
|
1306
|
-
* A flat personality sets --radius: 0 → every step collapses to 0. */
|
|
1307
|
-
--radius-sm: calc(var(--radius) - 4px);
|
|
1308
|
-
--radius-md: calc(var(--radius) - 2px);
|
|
1309
|
-
--radius-lg: var(--radius);
|
|
1310
|
-
--radius-xl: calc(var(--radius) + 4px);
|
|
1311
|
-
|
|
1312
|
-
/* density remap — the single line that scales ALL library spacing. Every p-* and
|
|
1313
|
-
* gap-* is calc(var(--spacing) x N); driving --spacing off --density-gap flows the
|
|
1314
|
-
* tier AND the per-surface density step into the library's internals. */
|
|
1315
|
-
--spacing: calc(0.25rem * var(--density-gap));
|
|
1316
|
-
|
|
1317
|
-
/* shadow — the elevation.* composites become the shadow-* utilities; every alpha rides
|
|
1318
|
-
* --shadow-intensity (2xs/xs stay the neutral slate hairlines the library leans on) */
|
|
1319
|
-
--shadow-2xs: 0 1px 2px 0 rgb(15 23 42 / calc(0.04 * var(--shadow-intensity)));
|
|
1320
|
-
--shadow-xs: 0 1px 2px 0 rgb(15 23 42 / calc(0.06 * var(--shadow-intensity)));
|
|
1321
|
-
--shadow-sm: ${doc.elevation.sm};
|
|
1322
|
-
--shadow-md: ${doc.elevation.md};
|
|
1323
|
-
--shadow-lg: ${doc.elevation.lg};
|
|
1324
|
-
--shadow-overlay: ${doc.elevation.overlay};
|
|
1325
|
-
|
|
1326
|
-
/* font weight — two-weight discipline: medium/semibold/bold all resolve to the
|
|
1327
|
-
* single emphasis weight; operator UI carries no third weight. normal is emitted
|
|
1328
|
-
* as a literal because its theme key and the primitive share a name (no self-ref). */
|
|
1329
|
-
--font-weight-normal: ${fontWeightNormal};
|
|
1330
|
-
--font-weight-medium: var(--font-weight-strong);
|
|
1331
|
-
--font-weight-semibold: var(--font-weight-strong);
|
|
1332
|
-
--font-weight-bold: var(--font-weight-strong);
|
|
1333
|
-
|
|
1334
|
-
/* tracking — heading tightness reaches the library's tracking-* utilities */
|
|
1335
|
-
--tracking-tighter: var(--heading-tracking);
|
|
1336
|
-
--tracking-tight: var(--heading-tracking);
|
|
1337
|
-
|
|
1338
|
-
/* type ramp — the seven authored steps as text-* utilities (typ-09: the type scale IS the
|
|
1339
|
-
* declared ramp; text-display is the scale-drama step, brand-forward surfaces only, typ-20) */
|
|
1340
|
-
${RAMP_STEPS.map((step) => {
|
|
1341
|
-
const r = doc.ramp[step];
|
|
1342
|
-
return ` --text-${step}: ${r.fontSize};
|
|
1343
|
-
--text-${step}--line-height: ${r.lineHeight};
|
|
1344
|
-
--text-${step}--font-weight: ${r.fontWeight};
|
|
1345
|
-
--text-${step}--letter-spacing: ${r.letterSpacing};`;
|
|
1346
|
-
}).join("\n")}
|
|
1347
|
-
|
|
1348
|
-
/* fonts — emitted as the literal stack: a font is not a runtime-toggled token, and
|
|
1349
|
-
* the theme key shares a name with the personality primitive (no self-reference).
|
|
1350
|
-
* personality.css carries the same --font-sans/--font-mono for direct var() use. */
|
|
1351
|
-
--font-sans: ${fontSans};
|
|
1352
|
-
--font-mono: ${fontMono};
|
|
1353
|
-
|
|
1354
|
-
/* transitions */
|
|
1355
|
-
--transition-fast: var(--duration-fast) var(--ease-default);
|
|
1356
|
-
--transition-normal: var(--duration-normal) var(--ease-default);
|
|
1357
|
-
--transition-slow: var(--duration-slow) var(--ease-default);
|
|
1358
|
-
--transition-enter: var(--duration-normal) var(--ease-enter);
|
|
1359
|
-
--transition-exit: var(--duration-fast) var(--ease-exit);
|
|
1360
|
-
|
|
1361
|
-
/* semantic + substrate color maps — all 36, so bg-popover / bg-chart-1 / bg-sidebar and the
|
|
1362
|
-
* dark ladder's bg-surface-raised / bg-surface-overlay resolve.
|
|
1363
|
-
* --color-destructive-foreground is mapped explicitly (D6): the library leaves it unmapped. */
|
|
1364
|
-
${colorMaps}
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
/* dark variant — ALWAYS ACTIVE: redefines the library's class-based dark: variant to
|
|
1368
|
-
* follow the data-theme attribute, so library dark: utilities (legal inside the library,
|
|
1369
|
-
* banned in project JSX by the no-literals rule) track the same attribute the theme
|
|
1370
|
-
* blocks armor. Inert until <html data-theme="dark">; harmless for a light-only project. */
|
|
1371
|
-
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
|
|
1372
|
-
`;
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
// Emit the index.html <html> attribute snippet the shell must carry.
|
|
1376
|
-
function emitIndexHtmlAttributes(doc, slug, meta) {
|
|
1377
|
-
const p = doc.primitives;
|
|
1378
|
-
return `<!-- Personality state lives in these <html data-*> attributes — code is the cache.
|
|
1379
|
-
The design-tokens artifact is the source. To change the look, edit design.tokens.json
|
|
1380
|
-
and re-run derive-personality.mjs --emit; to nudge one axis, edit an attribute here. -->
|
|
1381
|
-
<html
|
|
1382
|
-
lang="en"
|
|
1383
|
-
data-style="${slug}"
|
|
1384
|
-
data-density="${p["density-tier"]}"
|
|
1385
|
-
data-verbosity="${p.verbosity}"
|
|
1386
|
-
data-feedback="${p.feedback}"
|
|
1387
|
-
data-theme="${meta.theme}"
|
|
1388
|
-
>
|
|
1389
|
-
`;
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
// Write a file, creating parent directories as needed.
|
|
1393
|
-
function writeFileEnsured(path, content) {
|
|
1394
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
1395
|
-
writeFileSync(path, content);
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
// ---------------------------------------------------------------------------
|
|
1399
|
-
// CLI
|
|
1400
|
-
// ---------------------------------------------------------------------------
|
|
1401
|
-
|
|
1402
|
-
// Print usage and exit non-zero.
|
|
1403
|
-
function usage() {
|
|
1404
|
-
process.stderr.write("usage: derive-personality.mjs <artifact-dir> [--check | --emit <outdir> [--provisional]] [--theme light|dark]\n");
|
|
1405
|
-
process.exit(2);
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
// Entry point: parse argv, run --check or --emit over the artifact directory.
|
|
1409
|
-
function main() {
|
|
1410
|
-
const argv = process.argv.slice(2);
|
|
1411
|
-
let artifactDir = null;
|
|
1412
|
-
let mode = "check";
|
|
1413
|
-
let outdir = null;
|
|
1414
|
-
let theme = null; // null = follow the payload's theme-default
|
|
1415
|
-
let provisional = false;
|
|
1416
|
-
for (let i = 0; i < argv.length; i++) {
|
|
1417
|
-
const a = argv[i];
|
|
1418
|
-
if (a === "--check") mode = "check";
|
|
1419
|
-
else if (a === "--emit") { mode = "emit"; outdir = argv[++i] ?? null; }
|
|
1420
|
-
else if (a === "--theme") { theme = argv[++i] ?? null; }
|
|
1421
|
-
else if (a === "--provisional") provisional = true;
|
|
1422
|
-
else if (!a.startsWith("-") && artifactDir === null) artifactDir = a;
|
|
1423
|
-
else usage();
|
|
1424
|
-
}
|
|
1425
|
-
if (!artifactDir) usage();
|
|
1426
|
-
if (mode === "emit" && !outdir) usage();
|
|
1427
|
-
if (provisional && mode !== "emit") usage();
|
|
1428
|
-
if (theme !== null && !LEGAL_THEMES.includes(theme)) usage();
|
|
1429
|
-
|
|
1430
|
-
const { errors, payload, views, hints, theme: bootTheme, hasDark, hasWash } = runCheck(artifactDir, theme);
|
|
1431
|
-
|
|
1432
|
-
if (errors.length > 0) {
|
|
1433
|
-
process.stderr.write(`check FAILED (${artifactDir}): ${errors.length} issue(s)\n`);
|
|
1434
|
-
for (const e of errors) process.stderr.write(` - ${e}\n`);
|
|
1435
|
-
process.exit(1);
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
const slug = slugify(payload.product);
|
|
1439
|
-
|
|
1440
|
-
if (mode === "check") {
|
|
1441
|
-
const hintNote = hints.length > 0 ? `, ${hints.length} hint(s) outstanding (emit will refuse)` : "";
|
|
1442
|
-
process.stdout.write(`check PASSED (${artifactDir}): rules 1–15 OK — 27 primitives, 36 color roles, typography 3+7, elevation 4${hasWash ? ", wash present" : ""}, payload/manifest/parse-back OK — provenance=${payload.provenance}, style=${slug}, theme=${bootTheme}${hasDark ? ", dark overlay present" : ""}${hintNote}\n`);
|
|
1443
|
-
process.exit(0);
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
// Rule 12 — emission precondition: refuse while any token carries a hint. --provisional (the
|
|
1447
|
-
// golden/regression flow) emits hinted tokens at their provisional $values instead — loudly.
|
|
1448
|
-
if (hints.length > 0 && !provisional) {
|
|
1449
|
-
process.stderr.write(`emit REFUSED (${artifactDir}): ${hints.length} unresolved hint(s) — rule 12: every gated token must be concrete before emission.\n`);
|
|
1450
|
-
for (const h of hints) process.stderr.write(` - ${h.key}: hint: ${h.text}\n`);
|
|
1451
|
-
process.stderr.write(" Resolve each with judgment: set the real $value, delete the hint key, record the rationale in $description — then re-run --emit.\n");
|
|
1452
|
-
process.exit(1);
|
|
1453
|
-
}
|
|
1454
|
-
if (hints.length > 0) {
|
|
1455
|
-
process.stdout.write(`emit PROVISIONAL (${artifactDir}): ${hints.length} hint(s) emitted at their provisional $values — regression/golden flow only, never shipped output.\n`);
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
const emitErrors = [];
|
|
1459
|
-
const doc = buildEmitterDoc(views, payload, emitErrors);
|
|
1460
|
-
checkEmitterCompleteness(views, emitErrors);
|
|
1461
|
-
const fontBinding = buildFontBinding(views, doc, outdir, emitErrors);
|
|
1462
|
-
if (emitErrors.length > 0) {
|
|
1463
|
-
process.stderr.write(`emit FAILED (${artifactDir}): ${emitErrors.length} issue(s)\n`);
|
|
1464
|
-
for (const e of emitErrors) process.stderr.write(` - ${e}\n`);
|
|
1465
|
-
process.exit(1);
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// Contract 2 — the cod-16 font-binding gate, fail-closed at --emit. --provisional (the golden/
|
|
1469
|
-
// regression flow over hint-carrying or brand-font library twins) demotes it to a loud warning.
|
|
1470
|
-
if (fontBinding.failures.length > 0) {
|
|
1471
|
-
if (!provisional) {
|
|
1472
|
-
process.stderr.write(`emit FAILED (${artifactDir}): font binding (cod-16) — ${fontBinding.failures.length} unbound famil${fontBinding.failures.length === 1 ? "y" : "ies"}\n`);
|
|
1473
|
-
for (const f of fontBinding.failures) process.stderr.write(` - ${f}\n`);
|
|
1474
|
-
process.exit(1);
|
|
1475
|
-
}
|
|
1476
|
-
process.stdout.write(`emit PROVISIONAL font-binding warning (${artifactDir}): ${fontBinding.failures.length} unbound famil${fontBinding.failures.length === 1 ? "y" : "ies"} — cod-16 fails closed at a real --emit\n`);
|
|
1477
|
-
for (const f of fontBinding.failures) process.stdout.write(` - ${f}\n`);
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
const meta = { provenance: payload.provenance, sourced: payload.sourced, theme: bootTheme };
|
|
1481
|
-
const personalityCss = emitPersonalityCss(doc, slug, meta);
|
|
1482
|
-
const globalsCss = emitGlobalsCss(doc);
|
|
1483
|
-
const htmlAttrs = emitIndexHtmlAttributes(doc, slug, meta);
|
|
1484
|
-
const personalityPath = join(outdir, "src/styles/personality.css");
|
|
1485
|
-
const globalsPath = join(outdir, "src/styles/globals.css");
|
|
1486
|
-
const fontsPath = join(outdir, "src/styles/fonts.css");
|
|
1487
|
-
const attrsPath = join(outdir, "index-html-attributes.txt");
|
|
1488
|
-
writeFileEnsured(personalityPath, personalityCss);
|
|
1489
|
-
writeFileEnsured(globalsPath, globalsCss);
|
|
1490
|
-
writeFileEnsured(fontsPath, fontBinding.fontsCss);
|
|
1491
|
-
writeFileEnsured(attrsPath, htmlAttrs);
|
|
1492
|
-
|
|
1493
|
-
process.stdout.write(`emitted personality system for style=${slug} → ${outdir}\n`);
|
|
1494
|
-
process.stdout.write(` ${personalityPath}\n ${globalsPath}\n ${fontsPath}\n ${attrsPath}\n`);
|
|
1495
|
-
process.exit(0);
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
main();
|