Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b291f4217e | ||
|
|
6efe4a43c8 | ||
|
|
66632b050a | ||
|
|
93218c543a | ||
|
|
5b9974a50a | ||
|
|
07ebdfd5b1 | ||
|
|
35de8595b6 | ||
|
|
82b03772e4 | ||
|
|
f775f9e585 | ||
|
|
618dcae7bf | ||
|
|
065bd4f3ca | ||
|
|
6c2a79891c | ||
|
|
187e589ef7 | ||
|
|
0aec856f45 | ||
|
|
89c842e5f4 | ||
|
|
a5cf9266e6 | ||
|
|
bba73a2283 | ||
|
|
33ea98ae3e | ||
|
|
7e9a873499 | ||
|
|
4199ad4bc4 | ||
|
|
5400675585 | ||
|
|
3202a3aab5 | ||
|
|
4f069a39ef | ||
|
|
ba999bd799 | ||
|
|
6343ec689b | ||
|
|
8b6bbaef3d | ||
|
|
6cfeb7d374 | ||
|
|
bccbc98c9b | ||
|
|
07c8d43b5d | ||
|
|
2fd4c86794 | ||
|
|
f68c27a13f | ||
|
|
80e26259e1 | ||
|
|
40ff2b6eda | ||
|
|
be7d4d9242 | ||
|
|
cc8d992d8b | ||
|
|
fa5d7474b3 | ||
|
|
6d0dc0287d | ||
|
|
6fbf5b952f | ||
|
|
1537368b8c | ||
|
|
8b4507db53 | ||
|
|
a256b6e42b | ||
|
|
9fbda768e6 | ||
|
|
72fec2849c | ||
|
|
44aa29fb63 | ||
|
|
c582fd4699 | ||
|
|
5758e4c6bd | ||
|
|
b56ca30fcd | ||
|
|
601a7a8755 | ||
|
|
b4cfe626ff | ||
|
|
a975a8bcc1 | ||
|
|
da94340e9b | ||
|
|
457c2d877c | ||
|
|
470b655096 | ||
|
|
d886003879 | ||
|
|
55617ac5d8 | ||
|
|
60130bcea9 | ||
|
|
ae1c632594 | ||
|
|
6d684d2d6a | ||
|
|
adabd62011 | ||
|
|
0b867f0e25 | ||
|
|
b629daef01 | ||
|
|
80211f3364 | ||
|
|
5dffc569ff | ||
|
|
38b7029184 | ||
|
|
a5083e23e1 | ||
|
|
a34c9828aa | ||
|
|
05f228728e | ||
|
|
e2048614b5 | ||
|
|
7d4e1bff0c | ||
|
|
3214ead9c2 | ||
|
|
b12b67d344 | ||
|
|
cadb70a09e | ||
|
|
bf8e3ee0c2 | ||
|
|
c06bf3f558 | ||
|
|
ecadbc9556 | ||
|
|
4c1fbbc7c0 | ||
|
|
4b0c4a5e80 | ||
|
|
b4110667c9 | ||
|
|
4d919ec519 | ||
|
|
3e67293747 | ||
|
|
ca4f39e01d | ||
|
|
e0ddad3198 | ||
|
|
24cea3bd60 | ||
|
|
6b30ce5d02 | ||
|
|
cf80c3df1a | ||
|
|
29598150b4 | ||
|
|
2feb2bdb9d | ||
|
|
035e191ce5 | ||
|
|
2f491f54c3 | ||
|
|
1be3f1145b | ||
|
|
1d192d1f39 | ||
|
|
ba9b1cd8c3 | ||
|
|
8489467b99 | ||
|
|
0db6186617 | ||
|
|
154a84df56 | ||
|
|
336ee34b73 | ||
|
|
f3c26f1f42 | ||
|
|
5602cdfc7e | ||
|
|
7af6e831fb | ||
|
|
83b4fbcb5d | ||
|
|
e9f36ad9d8 | ||
|
|
28abe9a071 | ||
|
|
6444deb361 | ||
|
|
9af6fc1cb7 | ||
|
|
51ce043130 | ||
|
|
626bfbb0a8 | ||
|
|
f7751fad0c | ||
|
|
d64909decf | ||
|
|
c19c349fc6 | ||
|
|
ba7f3c8891 | ||
|
|
1f20997a6d | ||
|
|
72ba69e3a4 | ||
|
|
98ab04b94b | ||
|
|
c23c587ed5 | ||
|
|
a0f7fae4e2 | ||
|
|
af01134825 | ||
|
|
93e0571f20 | ||
|
|
f8981c61dd | ||
|
|
7593ec9d93 | ||
|
|
a84002d69b | ||
|
|
2ae67485fb | ||
|
|
6e4dcef80c | ||
|
|
0473c9f7e0 | ||
|
|
71f683ddb6 | ||
|
|
a6d818b31c | ||
|
|
4bf39fe09d | ||
|
|
e237df2b11 | ||
|
|
24aaabdd26 | ||
|
|
54a5cbd379 | ||
|
|
a0fd0475f5 | ||
|
|
ec83174eb2 | ||
|
|
f92adeab17 | ||
|
|
65ac2a5f13 | ||
|
|
840165463c | ||
|
|
94ed8d7d45 |
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||
"name": "impeccable",
|
||||
"metadata": {
|
||||
"description": "Design fluency for AI harnesses. 1 skill, 23 commands, and curated anti-patterns for impeccable frontend design."
|
||||
"description": "Design fluency for AI harnesses. 1 skill, 24 commands, and curated anti-patterns for impeccable frontend design."
|
||||
},
|
||||
"owner": {
|
||||
"name": "Paul Bakaus",
|
||||
@@ -11,7 +11,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "impeccable",
|
||||
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"description": "Design fluency for frontend development. 1 skill with 24 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"version": "4.1.2",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"description": "Design fluency for frontend development. 1 skill with 24 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"version": "4.1.2",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
|
||||
@@ -21,10 +21,12 @@ concurrency:
|
||||
|
||||
env:
|
||||
GENERATED_PATHS: >-
|
||||
.agent
|
||||
.agents
|
||||
.claude
|
||||
.cursor
|
||||
.gemini
|
||||
.github/agents
|
||||
.github/skills
|
||||
.grok
|
||||
.hermes
|
||||
@@ -35,6 +37,7 @@ env:
|
||||
.rovodev
|
||||
.trae
|
||||
.trae-cn
|
||||
.veto
|
||||
.vibe
|
||||
plugin
|
||||
|
||||
|
||||
@@ -61,6 +61,14 @@ Thumbs.db
|
||||
.impeccable/live/deferred-svelte-component-accepts.json
|
||||
.impeccable/history/
|
||||
.impeccable/config.local.json
|
||||
# API keys for optional image-generation cues (document seed mode).
|
||||
.impeccable/.env
|
||||
# Generated visual-cue images + cues.json (document seed Step 4).
|
||||
.impeccable/visual-cues/
|
||||
# The design-context store itself is the user's own record and is trackable;
|
||||
# only its session state and its generated exports stay local.
|
||||
.impeccable/design-context/runtime/
|
||||
.impeccable/design-context/exports/
|
||||
**/.impeccable/hook.pending.json
|
||||
.impeccable/provider-smoke/
|
||||
src/__impeccable_provider_smoke_*.html
|
||||
@@ -129,3 +137,9 @@ tmp/
|
||||
# PNGs, the old card backup). The canonical generator is `bun run og-image`
|
||||
# (scripts/generate-og-image.js); this dir is throwaway and safe to delete.
|
||||
.og-build/
|
||||
|
||||
|
||||
# Intermediate and generated output for the picker build. Release builds sync
|
||||
# the generated picker into the tracked provider harness directories.
|
||||
build-picker/
|
||||
skill/scripts/picker/
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"ignoreFiles": [
|
||||
"tests/fixtures/**",
|
||||
"tests/framework-fixtures/**",
|
||||
"tests/detect-antipatterns.test.js"
|
||||
"tests/detect-antipatterns.test.js",
|
||||
"picker/styles/design-context.css"
|
||||
],
|
||||
"ignoreValues": [
|
||||
{
|
||||
@@ -15,6 +16,41 @@
|
||||
],
|
||||
"createdAt": "2026-07-17T00:00:00.000Z",
|
||||
"reason": "Live overlay chrome is injected over arbitrary host pages and builds a self-contained UI with its own small type scale; DESIGN.md's ramp describes the impeccable website, not this widget"
|
||||
},
|
||||
{
|
||||
"rule": "broken-image",
|
||||
"value": "*",
|
||||
"files": [
|
||||
"picker/pages/index.astro"
|
||||
],
|
||||
"createdAt": "2026-07-23T14:51:18.241Z"
|
||||
},
|
||||
{
|
||||
"rule": "design-system-font-size",
|
||||
"value": "*",
|
||||
"files": [
|
||||
"picker/styles/picker.css"
|
||||
],
|
||||
"createdAt": "2026-07-23T21:00:00.000Z",
|
||||
"reason": "The picker is a self-contained questionnaire widget with its own kit-token type scale, and screen 04's cards render arbitrary Google Fonts at specimen sizes; DESIGN.md's ramp describes the impeccable website, not this tool (same footing as the live-browser.js entry)"
|
||||
},
|
||||
{
|
||||
"rule": "design-system-font",
|
||||
"value": "*",
|
||||
"files": [
|
||||
"picker/scripts/design-context.js"
|
||||
],
|
||||
"createdAt": "2026-07-30T13:11:54.730Z",
|
||||
"reason": "Runtime font loader: builds a Google Fonts URL from the family the agent/user chose in the questionnaire; no literal face is being introduced"
|
||||
},
|
||||
{
|
||||
"rule": "bounce-easing",
|
||||
"value": "cubic-bezier(.34, 1.56, .64, 1)",
|
||||
"files": [
|
||||
"picker/scripts/palette-picker.js"
|
||||
],
|
||||
"createdAt": "2026-08-20T02:53:58.453Z",
|
||||
"reason": "plan-18 step 10c: byte-exact port from tmp/motion-previews-premium.html installChoreographedPremium spring stagger"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -64,6 +64,8 @@ TDD order is non-negotiable:
|
||||
5. Add two adapters that wrap the pure check: `checkElementXxxDOM(el)` for the browser (`getComputedStyle` + `getBoundingClientRect`) and `checkElementXxx(el, tag, window)` for jsdom (`parseFloat(style.width)` instead of layout). Wire **both** adapters into **both** element loops in `cli/engine/detect-antipatterns.mjs` (browser loop ~line 1837, jsdom loop in `detectHtml` ~line 2058). Forgetting one is the most common mistake.
|
||||
6. Verify on a live page at `http://localhost:4321/fixtures/antipatterns/{rule-id}.html` and on the homepage. The two adapter paths can disagree.
|
||||
|
||||
To scan the browser questionnaire, use `node cli/bin/cli.js detect skill/scripts/picker/` after `bun run build:picker`. Name the directory, never `index.html` on its own: the page links its stylesheet, so a page-only scan reads none of the picker's CSS. The baseline and the reason each accepted finding stands are in CLAUDE.md's **Picker anti-pattern gate**.
|
||||
|
||||
Conventions: wrap the identifying heading text in straight double quotes inside snippets so the fixture test can extract it. jsdom-specific helpers `resolveBackground()`, `resolveGradientStops()`, and `parseGradientColors()` exist because `background:` shorthand isn't decomposed and computed colors aren't normalized in jsdom — use them. Reference rules to copy from: `side-tab` (border), `low-contrast` (color+gradient), `icon-tile-stack` (sibling relationship), `flat-type-hierarchy` (page-level).
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Architecture (v3.0+)
|
||||
|
||||
There is **one** user-invocable skill, `impeccable`, with **23 commands** underneath it. Users type `/impeccable polish`, `/impeccable audit`, etc. The skill is defined in `skill/`:
|
||||
There is **one** user-invocable skill, `impeccable`, with **24 commands** underneath it. Users type `/impeccable polish`, `/impeccable audit`, etc. The skill is defined in `skill/`:
|
||||
|
||||
- `SKILL.src.md` — frontmatter (with the auto-trigger-optimized description and the `allowed-tools` list), shared design laws, and the **Commands** router table. Provider `SKILL.md` files are generated from this source.
|
||||
- `reference/` — one `<command>.md` per command (`audit.md`, `polish.md`, `critique.md`, etc.), the shared playbooks the router loads outside the command table (`new-work.md`, `craft-floor.md`, `operate.md`, `routing.md`), and the native platform references (`ios.md`, `android.md`). When a sub-command is matched, the router loads its reference file.
|
||||
@@ -230,6 +230,31 @@ bun run build:browser
|
||||
|
||||
**IMPORTANT**: Always use `node` (not `bun`) to run the detect CLI. Bun's jsdom implementation is extremely slow and will cause scans with HTML files to hang for minutes.
|
||||
|
||||
### Picker anti-pattern gate
|
||||
|
||||
After `bun run build:picker`, scan the **built directory**:
|
||||
|
||||
```bash
|
||||
node cli/bin/cli.js detect skill/scripts/picker/
|
||||
```
|
||||
|
||||
**Name the directory, not `index.html`.** The built page only links its stylesheet and its script, and the rules that read CSS or JS text run per file, so scanning the page alone never reads a line of either. That is how three `design-system-color` findings vanished with nothing fixed: the mode-tile demo palette moved out of an inline `style` attribute and into `picker.css`, and the gate went quiet while the values stayed put. Naming a bundle by path is not a fix either, because Vite content-hashes the filenames and a stale path silently narrows the scan back to the page.
|
||||
|
||||
Baseline: **606 findings**: 556 on `index.html`, 47 on the bundled stylesheet, and 3 on the bundled script. The headline is dominated by per-element multiplication, not by distinct problems: the jsdom pass reports once per matching element, so one deliberate declaration can carry hundreds of hits, and a single one does: `picker.css`'s artboard morph, which eases `margin` and `padding` while the wireframe previews an answer, accounts for 527 of the `index.html` findings on its own (the morph is the preview mechanic; easing geometry on a miniature board is the point, not thrash). The distinct accepted groups below are the real ledger, each with a reason on record, so hold 606 rather than treating it as a backlog:
|
||||
|
||||
- 18 `cramped-padding` on the palette strips, nine for each of the two: the editable one on screen 02 and the reading copy under screen 03's choices. Each strip reports its group, its four items, and its four swatches. A swatch is a color that has to reach its own edges, so `.picker-band` carries no padding and places its hex readout and drag grip by inset instead, which the rule has no way to see. Adding padding here would put a frame around every color the visitor picked.
|
||||
- 3 `design-system-color` for the mode-tile demo palette (`#195856`, `#5f7c7a`, `#3a958b`). They are examples standing in for the palette the visitor is about to pick, which is the carve-out DESIGN.md's OKLCH-Only Rule already names. The argument is at the declaration in `picker/styles/picker.css`; read it before touching them.
|
||||
- 8 `design-system-font-size` from `picker/styles/picker.css`, seven of them already waived there in `.impeccable/config.json`. They reappear under the built path only because that waiver is scoped to the source path. The eighth is `1.05rem` on `.picker-finish-lede`, the review copy on the last screen, which sits on the questionnaire's own kit-token ramp for the same reason the other seven do.
|
||||
- 2 `border-accent-on-rounded` on `.ks-bento` in the vendored `kinpaku-kit.css`, which carries no `border-radius` at all. The rule wants a radius on the same line, and minification puts the whole stylesheet on one line, so every line-scoped regex rule reads this bundle with no line context. Check a snippet against the source before acting on it.
|
||||
- 11 from the design context document, the review UI the picker hands off to after the last question. Nine are `picker/styles/design-context.css`: 5 `design-system-font-size` and 1 `design-system-radius` (`22px`) for the ported prototype's own type ramp and shape vocabulary, 2 `design-system-color` (`#7ba98f`, `#c26d5a`) for the tray status dots, and 1 `layout-transition` on the tile expander, which animates `width` and `height` because the tile morphing into a panel is the geometry it is animating. One is `picker/scripts/design-context.js`, whose Google Fonts URL is built from whichever family the run chose, so the rule sees a template where it wants a declared face. The last is `all-caps-body` on the doc masthead's mono note. The user locked that UI; both files carry a waiver in `.impeccable/config.json` saying so, and like the picker.css group those waivers are source-scoped, so the values resurface under the built path.
|
||||
- 1 `undersized-ui-text` on the `Change` legend in the document's request-scope control, at 10.88px. Same slot and same argument as the micro-labels below.
|
||||
|
||||
The last 9 are the page findings the questionnaire has carried since those screens were built: 3 `cramped-padding` on the option panel and the icon sheet, both edge-to-edge grids whose rows hold their own padding, and 6 mono readouts and micro-labels under the text-size and tracking floors. They are the group to revisit first if the number is ever worth lowering.
|
||||
|
||||
**When the number moves, find out which file moved it before editing this section.** The distinct-group ledger read 39 until the design context document landed, and 15 findings arrived with it: 13 in its own two files (two later left with a dead type-specimen block), plus the finish-screen lede and the `Change` legend that ship in the picker's files but belong to that feature. The five other workstreams in the same batch added none. Building the tree at the previous commit in a `git worktree` and scanning both builds is how that was established, and it beats reasoning about which change is responsible.
|
||||
|
||||
For line numbers during triage, `node cli/bin/cli.js detect picker/styles/` scans the unminified sources. It is not the gate: the source scan's style-context heuristic skips custom-property declarations, so it misses the palette entirely. A served URL is worse than either, because the browser pass only measures the screen in front of it and the picker hides all but the active one.
|
||||
|
||||
## Versioning
|
||||
|
||||
**Feature PRs do not bump versions and do not add changelog entries.** Bumping is a release step, not part of the change that earns the release: a version in a feature branch conflicts with every other open branch, and a changelog entry describes a release that has not happened. Land the code first; the maintainer bumps and writes the changelog when cutting the release. This holds even though the "Bump when: ..." notes below name the source dirs — those say *which* component a change belongs to, not *when* to edit the manifest. The only PR that touches a manifest version is one whose purpose is the release itself.
|
||||
|
||||
@@ -9,3 +9,21 @@ The `skill/reference/ios.md` and `skill/reference/android.md` platform reference
|
||||
**Original work:** https://github.com/ehmo/platform-design-skills
|
||||
**Original license:** MIT
|
||||
**Author:** ehmo
|
||||
|
||||
## Icon specimens in the design picker
|
||||
|
||||
`picker/data/icon-packs.json`, and the copy of it built into `skill/scripts/picker/`, holds twenty-four glyphs from each of the icon families the picker offers, redistributed unmodified apart from the removal of attributes the specimen does not draw with. They are shown so a user can compare the families and choose one; the choice, not this file, is what a project then installs. `scripts/vendor-icons.mjs` records the exact version each set was taken from.
|
||||
|
||||
| Set | License | Source |
|
||||
|---|---|---|
|
||||
| Lucide | ISC | https://github.com/lucide-icons/lucide |
|
||||
| Feather | MIT | https://github.com/feathericons/feather |
|
||||
| Phosphor | MIT | https://github.com/phosphor-icons/core |
|
||||
| Heroicons | MIT | https://github.com/tailwindlabs/heroicons |
|
||||
| Tabler | MIT | https://github.com/tabler/tabler-icons |
|
||||
| Radix Icons | MIT | https://github.com/radix-ui/icons |
|
||||
| Bootstrap Icons | MIT | https://github.com/twbs/icons |
|
||||
| Iconoir | MIT | https://github.com/iconoir-icons/iconoir |
|
||||
| Octicons | MIT | https://github.com/primer/octicons |
|
||||
| Hugeicons (free core) | MIT | https://github.com/hugeicons/hugeicons-react |
|
||||
| Remix Icon | Apache-2.0 | https://github.com/Remix-Design/RemixIcon |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Impeccable
|
||||
|
||||
Design guidance for AI coding agents. 1 skill, 23 commands, live browser iteration, and 61 deterministic detector rules for AI-generated frontend design.
|
||||
Design guidance for AI coding agents. 1 skill, 24 commands, live browser iteration, and 61 deterministic detector rules for AI-generated frontend design.
|
||||
|
||||
> **Quick start:** From your project root, run `npx impeccable install`, then run `/impeccable init` inside your AI coding tool. Full docs: [impeccable.style](https://impeccable.style).
|
||||
|
||||
@@ -12,7 +12,7 @@ Every model trained on the same SaaS templates. Skip the guidance and you get th
|
||||
|
||||
Impeccable adds:
|
||||
- **One setup flow.** `/impeccable init` records durable product truth in `PRODUCT.md`, so later commands know the audience, purpose, operating context, constraints, voice, and evidence without confusing those facts with surface-level visual direction.
|
||||
- **23 commands.** A shared design vocabulary with your AI: `polish`, `audit`, `critique`, `distill`, `animate`, `bolder`, `quieter`, and more.
|
||||
- **24 commands.** A shared design vocabulary with your AI: `polish`, `audit`, `critique`, `distill`, `animate`, `bolder`, `quieter`, and more.
|
||||
- **61 deterministic detector rules** plus LLM-only critique checks. The CLI and browser extension run the deterministic rules with no LLM and no API key.
|
||||
|
||||
## What's Included
|
||||
@@ -33,7 +33,7 @@ Start every new project with:
|
||||
|
||||
`init` inspects the project, asks only for material gaps in durable product truth, and writes `PRODUCT.md`. Visitor mode and visual direction are chosen later for each surface; incumbent or newly built visual systems are recorded separately in `DESIGN.md`.
|
||||
|
||||
### 23 Commands
|
||||
### 24 Commands
|
||||
|
||||
All commands are accessed through `/impeccable`:
|
||||
|
||||
@@ -43,6 +43,7 @@ All commands are accessed through `/impeccable`:
|
||||
| `/impeccable init` | One-time setup: gather durable product context, write PRODUCT.md, configure live mode when applicable, recommend next steps |
|
||||
| `/impeccable document` | Generate root DESIGN.md from existing project code |
|
||||
| `/impeccable extract` | Pull reusable components and tokens into the design system |
|
||||
| `/impeccable design-context` | Reopen, revise, export, or import the design interview and its design context document |
|
||||
| `/impeccable shape` | Plan UX/UI before writing code |
|
||||
| `/impeccable critique` | UX design review: hierarchy, clarity, emotional resonance |
|
||||
| `/impeccable audit` | Run technical quality checks (a11y, performance, responsive) |
|
||||
|
||||
@@ -81,7 +81,7 @@ impeccable detect [options] [file-or-dir-or-url...]
|
||||
|
||||
## Part of Impeccable
|
||||
|
||||
This CLI is part of [Impeccable](https://impeccable.style), a cross-provider design skill pack for AI-powered development tools. The full suite includes 23 commands for Claude, Cursor, GitHub Copilot, Gemini, Codex, Hermes Agent, Veto, and more.
|
||||
This CLI is part of [Impeccable](https://impeccable.style), a cross-provider design skill pack for AI-powered development tools. The full suite includes 24 commands for Claude, Cursor, GitHub Copilot, Gemini, Codex, Hermes Agent, Veto, and more.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@babel/parser": "^8.0.4",
|
||||
"ai": "^7.0.14",
|
||||
"archiver": "^8.0.0",
|
||||
"astro": "^7.0.0",
|
||||
"playwright": "^1.59.1",
|
||||
"svelte": "^5",
|
||||
"zod": "^4.3.6",
|
||||
@@ -63,6 +64,36 @@
|
||||
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.122.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-GGPNftt0caaz9MDlmNQGHX8855Ojaduyy5pm9Sm1h7HalCn0cWNb5/bweadJF+4yzbal+QL6ztBa09WAAOzLmQ=="],
|
||||
|
||||
"@astrojs/compiler-binding": ["@astrojs/compiler-binding@0.4.0", "", { "optionalDependencies": { "@astrojs/compiler-binding-darwin-arm64": "0.4.0", "@astrojs/compiler-binding-darwin-x64": "0.4.0", "@astrojs/compiler-binding-linux-arm64-gnu": "0.4.0", "@astrojs/compiler-binding-linux-arm64-musl": "0.4.0", "@astrojs/compiler-binding-linux-x64-gnu": "0.4.0", "@astrojs/compiler-binding-linux-x64-musl": "0.4.0", "@astrojs/compiler-binding-wasm32-wasi": "0.4.0", "@astrojs/compiler-binding-win32-arm64-msvc": "0.4.0", "@astrojs/compiler-binding-win32-x64-msvc": "0.4.0" } }, "sha512-x2RjDUuWfwLNtc3mjAdSRInwqh/rqbLar9cm/5FOMbHvmYZB7yfKewzSclAxWjIZsypJDXv1lhaP2WG+P8TK3g=="],
|
||||
|
||||
"@astrojs/compiler-binding-darwin-arm64": ["@astrojs/compiler-binding-darwin-arm64@0.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZVUwHundaQyFNjE6uoa0usaC0WOCitDCLS/4mdb4rOiJXwVUuKJBMxI5WMzXLWmamsXtK/Z//ifLXvV5Yeh4Hw=="],
|
||||
|
||||
"@astrojs/compiler-binding-darwin-x64": ["@astrojs/compiler-binding-darwin-x64@0.4.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-FI6G8AY8u6fR1SI/QRR5yGMwtvZwP34CDmZpZ5HwJGa50UM1VISTLhqkhV4a476pmgd25X1Aur2dqw6hUnrlKA=="],
|
||||
|
||||
"@astrojs/compiler-binding-linux-arm64-gnu": ["@astrojs/compiler-binding-linux-arm64-gnu@0.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lB9gLFJK7m82EnjaU8nlRBEfcwGNeHidW3sSjODTUjMNaoewVuUz9fwwdY5M4jiSXIqWLH3yl6TX8FTDKA74Sw=="],
|
||||
|
||||
"@astrojs/compiler-binding-linux-arm64-musl": ["@astrojs/compiler-binding-linux-arm64-musl@0.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HPbvWqbxFxyaoQJhLxCaSjtYBx9KBo7JGVzEFZCmMl968a2PsSH0UfiODYgYPXofTOIsIH2aoCcrHXML0IA3ig=="],
|
||||
|
||||
"@astrojs/compiler-binding-linux-x64-gnu": ["@astrojs/compiler-binding-linux-x64-gnu@0.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-tQKolMxoJ/+0AmLWm1PmJ/i+z3i10ZU1bNuVjEDulCf48azEMtUNjTZgHJ5MPtpYRNc7dlETr8QujUfduzoC7Q=="],
|
||||
|
||||
"@astrojs/compiler-binding-linux-x64-musl": ["@astrojs/compiler-binding-linux-x64-musl@0.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5v5YymudsxMHp3NBLCS8BUlu5CRqeLtWD9cKS/4nIhIEHCbpz9okmVV6I0HWqmBAPhWYcDa3vw/vltYPrOQCTA=="],
|
||||
|
||||
"@astrojs/compiler-binding-wasm32-wasi": ["@astrojs/compiler-binding-wasm32-wasi@0.4.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.2.2" }, "cpu": "none" }, "sha512-m/phuH3x3PREvv1OnkM44NoPh4MatUadix1fB1u5SvMLCyDTUZykDJbKnWf1cjnYmHdlB8HcjTjl6JrCqAIXcw=="],
|
||||
|
||||
"@astrojs/compiler-binding-win32-arm64-msvc": ["@astrojs/compiler-binding-win32-arm64-msvc@0.4.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-B9zYf3okEY83kM8gydlpH2BHP00w4ifxPqlYlWrgTwuD6wnkrJDCwBlgy1q31cERjCJRXN1lrE2VmkLvFjv/6g=="],
|
||||
|
||||
"@astrojs/compiler-binding-win32-x64-msvc": ["@astrojs/compiler-binding-win32-x64-msvc@0.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-zB0Nrv0dGc0zZWPGDRmmETTPhDRqyZjAjk+gWMlVrJX5U89obpB3VUUE1ZiHxOCN5LQojeLK6O8L/dnoHolvNQ=="],
|
||||
|
||||
"@astrojs/compiler-rs": ["@astrojs/compiler-rs@0.4.0", "", { "dependencies": { "@astrojs/compiler-binding": "0.4.0" } }, "sha512-koVikeon1kreEy+/JzLQRy3vzHHQVOjycs4degg4vFufKApZOwMZvSSAEztYNhmcQVfNVsVZZI4cEge3cexAbQ=="],
|
||||
|
||||
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.11.0", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.3.0", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-3rzxJ+xbo0+8YyqOzLziIN32wmsHdCjEVz2sGOpRxJ+Ben/KiLph4ItxBy1abEL+E8fkRzqjg0rfXmaHJGw9JA=="],
|
||||
|
||||
"@astrojs/markdown-satteri": ["@astrojs/markdown-satteri@0.4.0", "", { "dependencies": { "@astrojs/internal-helpers": "0.11.0", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "satteri": "^0.10.3" } }, "sha512-wykOOW9KsUVcZweOpY/CeXpdKcCKZy6fQbdcteWFuI75+sQCiqxYM7VKsGa5b+aGl3cYQscFY37rsbbyal5MRw=="],
|
||||
|
||||
"@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="],
|
||||
|
||||
"@astrojs/telemetry": ["@astrojs/telemetry@3.3.3", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "package-manager-detector": "^1.6.0" } }, "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0", "", {}, "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.4", "", {}, "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg=="],
|
||||
@@ -73,8 +104,144 @@
|
||||
|
||||
"@babel/types": ["@babel/types@8.0.4", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0", "@babel/helper-validator-identifier": "^8.0.4" } }, "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g=="],
|
||||
|
||||
"@bruits/satteri-darwin-arm64": ["@bruits/satteri-darwin-arm64@0.10.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA=="],
|
||||
|
||||
"@bruits/satteri-darwin-x64": ["@bruits/satteri-darwin-x64@0.10.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q=="],
|
||||
|
||||
"@bruits/satteri-linux-arm64-gnu": ["@bruits/satteri-linux-arm64-gnu@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w=="],
|
||||
|
||||
"@bruits/satteri-linux-arm64-musl": ["@bruits/satteri-linux-arm64-musl@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA=="],
|
||||
|
||||
"@bruits/satteri-linux-x64-gnu": ["@bruits/satteri-linux-x64-gnu@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw=="],
|
||||
|
||||
"@bruits/satteri-linux-x64-musl": ["@bruits/satteri-linux-x64-musl@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg=="],
|
||||
|
||||
"@bruits/satteri-wasm32-wasi": ["@bruits/satteri-wasm32-wasi@0.10.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.2.3" }, "cpu": "none" }, "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw=="],
|
||||
|
||||
"@bruits/satteri-win32-arm64-msvc": ["@bruits/satteri-win32-arm64-msvc@0.10.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw=="],
|
||||
|
||||
"@bruits/satteri-win32-x64-msvc": ["@bruits/satteri-win32-x64-msvc@0.10.5", "", { "os": "win32", "cpu": "x64" }, "sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ=="],
|
||||
|
||||
"@capsizecss/unpack": ["@capsizecss/unpack@4.0.1", "", { "dependencies": { "fontkitten": "^1.0.3" } }, "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ=="],
|
||||
|
||||
"@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="],
|
||||
|
||||
"@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.3" }, "os": "darwin", "cpu": "x64" }, "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw=="],
|
||||
|
||||
"@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "os": "freebsd" }, "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w=="],
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.3", "", { "os": "linux", "cpu": "none" }, "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.3" }, "os": "linux", "cpu": "arm" }, "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ=="],
|
||||
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ=="],
|
||||
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.3" }, "os": "linux", "cpu": "none" }, "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.3" }, "os": "linux", "cpu": "s390x" }, "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.4", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA=="],
|
||||
|
||||
"@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "cpu": "none" }, "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.4", "", { "os": "win32", "cpu": "x64" }, "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
@@ -87,18 +254,86 @@
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
||||
|
||||
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.147.0", "", {}, "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg=="],
|
||||
|
||||
"@puppeteer/browsers": ["@puppeteer/browsers@3.2.1", "", { "dependencies": { "modern-tar": "^0.8.0", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-KDz+3qDRdBAlRlMjmKyj6dEs33YHTk/xRHEENSXq6TNnhgoU15ruSHtEBeVF6OZ9tBDY55Se4P0nFMNsipzU9A=="],
|
||||
|
||||
"@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.6", "", { "os": "android", "cpu": "arm" }, "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.6", "", { "os": "android", "cpu": "arm64" }, "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.6", "", { "os": "linux", "cpu": "arm" }, "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.6", "", { "os": "none", "cpu": "arm64" }, "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@4.4.3", "", { "dependencies": { "@shikijs/primitive": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" } }, "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ=="],
|
||||
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w=="],
|
||||
|
||||
"@shikijs/langs": ["@shikijs/langs@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A=="],
|
||||
|
||||
"@shikijs/primitive": ["@shikijs/primitive@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ=="],
|
||||
|
||||
"@shikijs/themes": ["@shikijs/themes@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/types@4.4.3", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g=="],
|
||||
|
||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||
|
||||
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.11", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.4.0", "", {}, "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ=="],
|
||||
|
||||
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
|
||||
|
||||
"@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="],
|
||||
@@ -115,13 +350,21 @@
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"am-i-vibing": ["am-i-vibing@0.4.0", "", { "dependencies": { "process-ancestry": "^0.1.0" }, "bin": { "am-i-vibing": "dist/cli.mjs" } }, "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"archiver": ["archiver@8.0.0", "", { "dependencies": { "async": "^3.2.4", "buffer-crc32": "^1.0.0", "is-stream": "^4.0.0", "lazystream": "^1.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^3.0.0", "tar-stream": "^3.0.0", "zip-stream": "^7.0.2" } }, "sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||
|
||||
"astro": ["astro@7.2.10", "", { "dependencies": { "@astrojs/compiler-rs": "^0.4.0", "@astrojs/internal-helpers": "0.11.0", "@astrojs/markdown-satteri": "0.4.0", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^9.0.0", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "find-proc": "0.1.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.3.0", "jsonc-parser": "^3.3.1", "magic-string": "^1.0.0", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.5", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.35.4" }, "peerDependencies": { "@astrojs/markdown-remark": "^7.3.0" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-uPtD+nK6kQXugyq4kiMPwj9OI3Sae6HSerA5X+fuv2CPaDwxmokFPPFlGRKIAXH0QAKu+zot2q6yrLG61wFmqg=="],
|
||||
|
||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||
|
||||
@@ -129,6 +372,8 @@
|
||||
|
||||
"b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="],
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
|
||||
@@ -161,19 +406,37 @@
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
|
||||
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"chromium-bidi": ["chromium-bidi@17.0.2", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg=="],
|
||||
|
||||
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
|
||||
|
||||
"cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="],
|
||||
|
||||
"compress-commons": ["compress-commons@7.0.1", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^7.0.1", "is-stream": "^4.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
"cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="],
|
||||
|
||||
"cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
@@ -187,20 +450,36 @@
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
|
||||
|
||||
"css-select": ["css-select@7.0.0", "", { "dependencies": { "boolbase": "^2.0.0", "css-what": "^8.0.0", "domhandler": "^6.0.1", "domutils": "^4.0.2", "nth-check": "^3.0.1" } }, "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g=="],
|
||||
|
||||
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
|
||||
|
||||
"css-what": ["css-what@8.0.0", "", {}, "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw=="],
|
||||
|
||||
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"devalue": ["devalue@5.8.2", "", {}, "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"devtools-protocol": ["devtools-protocol@0.0.1666840", "", {}, "sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg=="],
|
||||
|
||||
"diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="],
|
||||
|
||||
"domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="],
|
||||
@@ -209,6 +488,8 @@
|
||||
|
||||
"domutils": ["domutils@4.0.2", "", { "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", "domhandler": "^6.0.0" } }, "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA=="],
|
||||
|
||||
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
@@ -223,8 +504,12 @@
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
@@ -237,6 +522,8 @@
|
||||
|
||||
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||
|
||||
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
|
||||
|
||||
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
|
||||
@@ -249,18 +536,36 @@
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
||||
|
||||
"fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="],
|
||||
|
||||
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
|
||||
|
||||
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||
|
||||
"find-proc": ["find-proc@0.1.0", "", {}, "sha512-OaOpEYv2PiQ7SQ5LIrl+deA1XaWcxEjnpM6VuWXTUvn+teIXxeFTLDmu18/zDQpFmHN4o3oDBX+BT0AGwEhemg=="],
|
||||
|
||||
"flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
|
||||
|
||||
"fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="],
|
||||
|
||||
"fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
@@ -277,16 +582,32 @@
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="],
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"hono": ["hono@4.12.14", "", {}, "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w=="],
|
||||
|
||||
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
|
||||
|
||||
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||
|
||||
"htmlparser2": ["htmlparser2@12.0.0", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "domutils": "^4.0.2", "entities": "^8.0.0" } }, "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw=="],
|
||||
|
||||
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
@@ -299,6 +620,12 @@
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
|
||||
|
||||
"is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||
@@ -311,6 +638,8 @@
|
||||
|
||||
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="],
|
||||
|
||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||
|
||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||
@@ -319,24 +648,66 @@
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
|
||||
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||
|
||||
"lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
|
||||
|
||||
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
"lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
|
||||
|
||||
"magic-string": ["magic-string@1.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g=="],
|
||||
|
||||
"magicast": ["magicast@0.5.4", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w=="],
|
||||
|
||||
"marked": ["marked@18.0.11", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||
|
||||
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||
|
||||
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||
|
||||
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||
|
||||
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
@@ -347,10 +718,22 @@
|
||||
|
||||
"modern-tar": ["modern-tar@0.8.4", "", {}, "sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"neotraverse": ["neotraverse@1.0.1", "", {}, "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w=="],
|
||||
|
||||
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
|
||||
|
||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||
|
||||
"node-mock-http": ["node-mock-http@1.0.5", "", {}, "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"nth-check": ["nth-check@3.0.1", "", { "dependencies": { "boolbase": "^2.0.0" } }, "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ=="],
|
||||
@@ -359,26 +742,58 @@
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="],
|
||||
|
||||
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
|
||||
"ohash": ["ohash@2.0.12", "", {}, "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
|
||||
|
||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
|
||||
|
||||
"p-limit": ["p-limit@7.3.2", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-Ll0w3fU24vYpXoZmjjZIee6bJQDgG0oAyo1PdmFYI8UDwJJddaHAypxIH9avUu+t+lSsAwKVsb1jDCMIIChliw=="],
|
||||
|
||||
"p-queue": ["p-queue@9.3.3", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA=="],
|
||||
|
||||
"p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
|
||||
|
||||
"package-manager-detector": ["package-manager-detector@1.8.0", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
||||
|
||||
"piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"playwright": ["playwright@1.62.1", "", { "dependencies": { "playwright-core": "1.62.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg=="],
|
||||
|
||||
"playwright-core": ["playwright-core@1.62.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="],
|
||||
|
||||
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
|
||||
|
||||
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||
|
||||
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||
|
||||
"process-ancestry": ["process-ancestry@0.1.0", "", {}, "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg=="],
|
||||
|
||||
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||
|
||||
"property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"puppeteer": ["puppeteer@25.9.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.1", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1666840", "lilconfig": "^3.1.3", "puppeteer-core": "25.9.0", "typed-query-selector": "^2.12.2" }, "bin": { "puppeteer": "lib/puppeteer/node/cli.js" } }, "sha512-2JqQszD2pyDTpIvBH1ZCXdrHgENVNdJIeOM6asbwHRgWknFiaLd1gNB91w/B/0hQHNpafkpxa90lPpBaJM87Hw=="],
|
||||
@@ -387,6 +802,8 @@
|
||||
|
||||
"qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
|
||||
|
||||
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
@@ -395,24 +812,48 @@
|
||||
|
||||
"readdir-glob": ["readdir-glob@3.0.0", "", { "dependencies": { "minimatch": "^10.2.2" } }, "sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw=="],
|
||||
|
||||
"readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="],
|
||||
|
||||
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||
|
||||
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
||||
|
||||
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="],
|
||||
|
||||
"rolldown": ["rolldown@1.2.6", "", { "dependencies": { "@oxc-project/types": "=0.147.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.6", "@rolldown/binding-android-arm64": "1.2.6", "@rolldown/binding-darwin-arm64": "1.2.6", "@rolldown/binding-darwin-x64": "1.2.6", "@rolldown/binding-freebsd-x64": "1.2.6", "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", "@rolldown/binding-linux-arm64-gnu": "1.2.6", "@rolldown/binding-linux-arm64-musl": "1.2.6", "@rolldown/binding-linux-ppc64-gnu": "1.2.6", "@rolldown/binding-linux-s390x-gnu": "1.2.6", "@rolldown/binding-linux-x64-gnu": "1.2.6", "@rolldown/binding-linux-x64-musl": "1.2.6", "@rolldown/binding-openharmony-arm64": "1.2.6", "@rolldown/binding-win32-arm64-msvc": "1.2.6", "@rolldown/binding-win32-x64-msvc": "1.2.6" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"satteri": ["satteri@0.10.5", "", { "dependencies": { "@types/estree-jsx": "^1.0.5", "@types/hast": "^3.0.5", "@types/mdast": "^4.0.4", "@types/unist": "^3.0.3" }, "optionalDependencies": { "@bruits/satteri-darwin-arm64": "0.10.5", "@bruits/satteri-darwin-x64": "0.10.5", "@bruits/satteri-linux-arm64-gnu": "0.10.5", "@bruits/satteri-linux-arm64-musl": "0.10.5", "@bruits/satteri-linux-x64-gnu": "0.10.5", "@bruits/satteri-linux-x64-musl": "0.10.5", "@bruits/satteri-wasm32-wasi": "0.10.5", "@bruits/satteri-win32-arm64-msvc": "0.10.5", "@bruits/satteri-win32-x64-msvc": "0.10.5" } }, "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ=="],
|
||||
|
||||
"sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
|
||||
|
||||
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"sharp": ["sharp@0.35.4", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.4", "@img/sharp-darwin-x64": "0.35.4", "@img/sharp-freebsd-wasm32": "0.35.4", "@img/sharp-libvips-darwin-arm64": "1.3.3", "@img/sharp-libvips-darwin-x64": "1.3.3", "@img/sharp-libvips-linux-arm": "1.3.3", "@img/sharp-libvips-linux-arm64": "1.3.3", "@img/sharp-libvips-linux-ppc64": "1.3.3", "@img/sharp-libvips-linux-riscv64": "1.3.3", "@img/sharp-libvips-linux-s390x": "1.3.3", "@img/sharp-libvips-linux-x64": "1.3.3", "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", "@img/sharp-libvips-linuxmusl-x64": "1.3.3", "@img/sharp-linux-arm": "0.35.4", "@img/sharp-linux-arm64": "0.35.4", "@img/sharp-linux-ppc64": "0.35.4", "@img/sharp-linux-riscv64": "0.35.4", "@img/sharp-linux-s390x": "0.35.4", "@img/sharp-linux-x64": "0.35.4", "@img/sharp-linuxmusl-arm64": "0.35.4", "@img/sharp-linuxmusl-x64": "0.35.4", "@img/sharp-webcontainers-wasm32": "0.35.4", "@img/sharp-win32-arm64": "0.35.4", "@img/sharp-win32-ia32": "0.35.4", "@img/sharp-win32-x64": "0.35.4" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"shiki": ["shiki@4.4.3", "", { "dependencies": { "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", "@shikijs/engine-oniguruma": "4.4.3", "@shikijs/langs": "4.4.3", "@shikijs/themes": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||
@@ -421,8 +862,14 @@
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"smol-toml": ["smol-toml@1.8.0", "", {}, "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
@@ -433,32 +880,80 @@
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"svelte": ["svelte@5.56.10", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-Lcxbj8I/KAbpY+VjtY4ENQBV0dDCipfGAhqb51XQZ67CIQqXgsv/8dPkbILaj4Fb6/b6JAEM/PIVbILXgDQy2g=="],
|
||||
|
||||
"svgo": ["svgo@4.1.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^6.0.0", "css-tree": "^3.0.1", "css-what": "^7.0.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "1.6.1" }, "bin": { "svgo": "bin/svgo.js" } }, "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q=="],
|
||||
|
||||
"tar-stream": ["tar-stream@3.1.8", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ=="],
|
||||
|
||||
"teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="],
|
||||
|
||||
"text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
|
||||
|
||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||
|
||||
"tinyclip": ["tinyclip@0.1.15", "", {}, "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="],
|
||||
|
||||
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
|
||||
|
||||
"ultrahtml": ["ultrahtml@1.7.0", "", {}, "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g=="],
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
|
||||
|
||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||
|
||||
"unifont": ["unifont@0.7.5", "", { "dependencies": { "css-tree": "^3.1.0", "ohash": "^2.0.11", "undici": "^8.0.0" } }, "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg=="],
|
||||
|
||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||
|
||||
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="],
|
||||
|
||||
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
||||
|
||||
"webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
@@ -469,12 +964,16 @@
|
||||
|
||||
"ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
|
||||
|
||||
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
|
||||
|
||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||
|
||||
"zip-stream": ["zip-stream@7.0.5", "", { "dependencies": { "compress-commons": "^7.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w=="],
|
||||
@@ -483,10 +982,14 @@
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@4.0.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q=="],
|
||||
|
||||
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.27", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw=="],
|
||||
|
||||
"@bruits/satteri-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
@@ -495,14 +998,58 @@
|
||||
|
||||
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.27", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"chromium-bidi/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||
|
||||
"eventsource/eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"magicast/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"magicast/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"svelte/aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
||||
|
||||
"svelte/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"svgo/css-select": ["css-select@6.0.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^7.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "nth-check": "^2.1.1" } }, "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw=="],
|
||||
|
||||
"svgo/css-what": ["css-what@7.0.0", "", {}, "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ=="],
|
||||
|
||||
"unifont/undici": ["undici@8.10.1", "", {}, "sha512-YQ3WlbqjYMmNpdvDH64jAgLjxuAR9+649calDWhbshYaeQGO2bR4nI94ORJmwI3J9YhoKQnpyGOK+0zlWS5N5Q=="],
|
||||
|
||||
"vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||
|
||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"magicast/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"magicast/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"svgo/css-select/boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"svgo/css-select/domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||
|
||||
"svgo/css-select/domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
"svgo/css-select/nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"svgo/css-select/domhandler/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
|
||||
"svgo/css-select/domutils/dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
|
||||
"svgo/css-select/domutils/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
|
||||
"svgo/css-select/domutils/dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# Design context architecture
|
||||
|
||||
How the design interview, the design context document, and everything the user
|
||||
gives us during the seed process are organized, and why.
|
||||
|
||||
## The problem this layout solves
|
||||
|
||||
The design context is one product concept. Before this layout it was scattered
|
||||
across two directories with no owner:
|
||||
|
||||
- `.impeccable/design-interview/` held the questionnaire submission, uploaded
|
||||
fonts, staged brand assets, and two runtime files.
|
||||
- `.impeccable/visual-cues/` held the cue generation workspace, and also, buried
|
||||
inside `cues.json`, the `context` object carrying the entire chat half of the
|
||||
interview plus the `modes` set. Neither is a generation artifact.
|
||||
- The design context document was not a file at all. It was a client-side render
|
||||
that existed only after a submit, died with its session, and could not be
|
||||
reopened.
|
||||
- Three scripts hardcoded the storage directory independently, with no shared
|
||||
constant. The agent-side poll CLI treats a missing session file as a clean
|
||||
exit, so a half-finished rename would fail silently and no test would catch it.
|
||||
|
||||
Export, import, resume, rebuild, and edit each had to reassemble state from those
|
||||
places with implicit coupling. One canonical store with one code owner replaces
|
||||
that.
|
||||
|
||||
## File layout, in the user's project
|
||||
|
||||
```
|
||||
.impeccable/design-context/ the store: the one exportable unit
|
||||
context.json { schemaVersion, modes, context } the chat half of the run
|
||||
answers.json the questionnaire submission, flat form-field shape
|
||||
assets/ brand files the user supplied
|
||||
fonts/ font faces the user uploaded
|
||||
cue.png the chosen hero, copied at submit
|
||||
runtime/ session.json, journal.jsonl, draft.json (gitignored)
|
||||
exports/ design-context.md, design-context.bundle.json (gitignored)
|
||||
|
||||
.impeccable/visual-cues/ the generation workspace, unchanged
|
||||
brief.md, <slug>.png x6, cues.json (cues + palette only), fonts.json
|
||||
```
|
||||
|
||||
The reasoning, in the order the decisions were made:
|
||||
|
||||
1. **Durable versus regenerable is the axis.** It matches the policy this repo's
|
||||
own `.gitignore` states: generated sidecars and config may be tracked, but
|
||||
runtime recovery state and local assets stay local. Everything in the store
|
||||
above `runtime/` is a user decision or a user-supplied file, so it is
|
||||
trackable and exportable. The workspace is regenerable process output and
|
||||
stays ignored.
|
||||
2. **`context.json` moves the chat half out of `cues.json`.** The context object
|
||||
was never a cue artifact. It rode there because that was the only file the
|
||||
picker served. With its own home it survives cue regeneration, imports without
|
||||
dragging six hero images along, and can be edited by the save flow without
|
||||
touching the generation workspace.
|
||||
3. **`cue.png` makes the document self-sufficient.** The Color article renders
|
||||
the chosen cue only when the cue manifest still lists it, which means the
|
||||
document breaks once the workspace is cleaned or in a project that received
|
||||
the context by import. Copying the one chosen hero into the store at submit
|
||||
time costs a few hundred kilobytes and buys rendering independence. The five
|
||||
unpicked heroes stay in the workspace as art direction leftovers.
|
||||
4. **`runtime/` isolates ephemera.** Session discovery, the journal, and the
|
||||
mid-questionnaire draft never belong in an export or a commit. One gitignore
|
||||
line covers all of it permanently.
|
||||
5. **PRODUCT.md and DESIGN.md stay at the project root.** They are the canonical
|
||||
documents the whole toolchain reads. `context.json` carries distilled copies
|
||||
with provenance, never replacements. Edits that touch product truth reconcile
|
||||
back into PRODUCT.md through the agent rather than the store growing a second
|
||||
product record.
|
||||
|
||||
## Code layout
|
||||
|
||||
The shape mirrors `skill/scripts/live/`, which solves the same class of problem.
|
||||
|
||||
```
|
||||
skill/scripts/design-context/
|
||||
store.mjs the only code that knows store paths or writes store files
|
||||
bindings.mjs the editable-field registry for the document
|
||||
session-routes.mjs HTTP handlers for the document edit session
|
||||
portability.mjs the export bundle format and its import validation
|
||||
|
||||
skill/scripts/
|
||||
picker-server.mjs static serving, boot contract, submit, autosave, spawn
|
||||
picker-doc-session.mjs the session shell: http server, timers, token
|
||||
picker-doc-poll.mjs the agent's poll CLI
|
||||
design-context-export.mjs / design-context-import.mjs
|
||||
|
||||
picker/scripts/
|
||||
boot.js one memoized fetch of the boot contract
|
||||
hydrate.js restoring a previous run into the questionnaire
|
||||
palette-picker.js the questionnaire; owns running hydration
|
||||
design-context.js the document; document mode, pending ledger, save bar
|
||||
```
|
||||
|
||||
Three ownership rules keep this correct as it grows.
|
||||
|
||||
**store.mjs is the single writer of store files.** Server, session, and import
|
||||
all go through it. Every write is atomic: write a temporary file beside the
|
||||
target, then rename over it, so a reader never sees a torn file.
|
||||
|
||||
**Reads come off disk per request.** Files are the truth. Process memory is a
|
||||
cache at best. This is live mode's stat-keyed cache rule in its simplest form,
|
||||
which is all a small store needs.
|
||||
|
||||
**During a live session, only the session process writes store files.** The agent
|
||||
edits its own documents, DESIGN.md and PRODUCT.md, directly. Any answer or
|
||||
context updates it needs to make ride in its reply payload for the session to
|
||||
apply. Without that split, an agent doing a read, modify, write cycle on the
|
||||
answers file can silently drop a change the session wrote in between.
|
||||
|
||||
## The live session
|
||||
|
||||
The document is a working surface, not a report. The patterns below are taken
|
||||
from live mode, scaled to one page, one store, and one agent.
|
||||
|
||||
1. **Append-only journal with pure replay.** The session appends one line per
|
||||
render-relevant event to `runtime/journal.jsonl`: applied changes, batch
|
||||
transitions, and request transitions. A booting session replays the file to
|
||||
recover its sequence number and any unacknowledged batch. Lines it cannot use,
|
||||
including records written by an older release that carry no sequence number,
|
||||
become diagnostics rather than failures.
|
||||
2. **Stage, then apply.** Edits accumulate in a client-side ledger keyed by field
|
||||
binding. Re-editing a field updates the new value but keeps the first original,
|
||||
so the record of what changed stays true across repeated edits.
|
||||
3. **Delivery that survives a dead agent.** One batch is outstanding at a time and
|
||||
is journaled. A poll leases it, a reply acknowledges it, and a session restart
|
||||
re-offers anything unacknowledged. An invalid reply returns a corrective hint
|
||||
while the lease holds, so the agent can fix its own message.
|
||||
4. **Server-owned truth, DOM as cache.** The state endpoint carries the journal
|
||||
sequence and the current batch. The client compares sequence numbers and
|
||||
re-fetches when the server has moved ahead. That re-fetch and re-render is the
|
||||
hot reload. The count of staged changes before Apply is the client's own,
|
||||
because nothing has reached the server yet.
|
||||
5. **One in-flight lock.** While a batch is unacknowledged the document's edit
|
||||
affordances are disabled. A re-render triggered by anything else re-applies
|
||||
still-staged edits onto the fresh DOM, so a background event never wipes work
|
||||
the user has not saved.
|
||||
6. **Loopback discovery with a per-run token.** The session records its process
|
||||
id, port, and token; readers probe liveness before trusting the record; the
|
||||
token, not the origin, is the security boundary.
|
||||
|
||||
What was deliberately left out, because each answers a question this surface does
|
||||
not ask: the evidence gathering and verification pipeline and the repair loop
|
||||
(live cannot know which source file a DOM edit belongs to, while every field here
|
||||
has an authoritative address and applying is a deterministic write); server-sent
|
||||
events and reload-resume state (a two-second poll already covers it); poll lanes
|
||||
and priorities and chunking (one agent, one batch); the roots manifest (one fixed
|
||||
path under the project root).
|
||||
|
||||
## Data flow
|
||||
|
||||
```
|
||||
SEED
|
||||
agent stages assets into the store, runs the cue pipeline, writes the
|
||||
workspace and the store's context.json, then launches the picker
|
||||
client boots, prefills from a draft or a previous submission, autosaves
|
||||
submit writes answers, copies the chosen hero, forks the session, exits 0
|
||||
agent seeds DESIGN.md from the answers, then enters the edit loop
|
||||
|
||||
REOPEN
|
||||
the picker server runs in document mode, reusing or spawning a session
|
||||
the client renders the document directly, with no submit
|
||||
the session ending is the agent's completion signal
|
||||
|
||||
EDIT
|
||||
staged changes leave the ledger as one batch, the session applies them to
|
||||
the store and queues the downstream work, the agent reconciles DESIGN.md
|
||||
and PRODUCT.md prose and replies, the sequence advances, the tab re-fetches
|
||||
and re-renders
|
||||
|
||||
EXPORT AND IMPORT
|
||||
the store compiles to a readable markdown document plus a lossless bundle
|
||||
importing a bundle rebuilds a working store in another project
|
||||
```
|
||||
@@ -40,10 +40,11 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build:picker": "node scripts/build-picker.mjs",
|
||||
"build:skills": "bun run scripts/build.js --skip-root-sync",
|
||||
"build:skills:release": "bun run scripts/build.js",
|
||||
"build": "bun run build:skills && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
|
||||
"build:release": "bun run build:skills:release && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
|
||||
"build": "bun run build:picker && bun run build:skills && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
|
||||
"build:release": "bun run build:picker && bun run build:skills:release && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
|
||||
"build:browser": "node scripts/build-browser-detector.js",
|
||||
"build:extension": "node scripts/build-extension.js",
|
||||
"clean": "rm -rf dist build",
|
||||
@@ -93,6 +94,7 @@
|
||||
"@babel/parser": "^8.0.4",
|
||||
"ai": "^7.0.14",
|
||||
"archiver": "^8.0.0",
|
||||
"astro": "^7.0.0",
|
||||
"playwright": "^1.59.1",
|
||||
"svelte": "^5",
|
||||
"zod": "^4.3.6"
|
||||
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 72 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<style>
|
||||
/* Light OS theme: black icon. Dark OS theme: pristine kinpaku gold,
|
||||
matching the header logo (--ks-kinpaku, #ffb900) rather than the old
|
||||
muddier #d8a83a. */
|
||||
path { fill: #141207; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #ffb900; }
|
||||
}
|
||||
</style>
|
||||
<path d="M7 1 L18 1 L8 31 L7 31 Q1 31 1 25 L1 7 Q1 1 7 1 Z"/>
|
||||
<path d="M22 1 L25 1 Q31 1 31 7 L31 25 Q31 31 25 31 L12 31 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 501 B |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: './picker',
|
||||
outDir: './build-picker',
|
||||
output: 'static',
|
||||
build: {
|
||||
assets: 'assets',
|
||||
assetsPrefix: '.',
|
||||
format: 'directory',
|
||||
},
|
||||
devToolbar: {
|
||||
enabled: false,
|
||||
},
|
||||
vite: {
|
||||
build: {
|
||||
assetsInlineLimit: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,559 @@
|
||||
---
|
||||
/*
|
||||
The wireframe landing page every option screen previews its answer on. One
|
||||
copy so a change to the page's anatomy lands on every screen at once.
|
||||
|
||||
mode: 'bars' fills the hero with bar stacks, which is what a screen judging
|
||||
color, layout, or depth needs. 'type' swaps them for real text elements and
|
||||
adds the data-type-* hooks the font screen writes specimen copy into; that
|
||||
screen is judging glyphs, so bars would tell it nothing.
|
||||
|
||||
Every body is drawn both ways for the same reason. The slots stay one tree and
|
||||
one set of class names; what changes is whether a slot holds a data-type-*
|
||||
hook or the bars that stand in for the words. Only the font screen's boards are
|
||||
filled, so a hook on any other board would promise copy that never arrives,
|
||||
and a paragraph is a stack of bars rather than one because the block a
|
||||
structural answer acts on is the shape of the passage, not a single line.
|
||||
|
||||
phone: false drops the handset and leaves the desktop artboard alone in the
|
||||
frame, for a screen whose answer only reads at desktop width.
|
||||
*/
|
||||
interface Props {
|
||||
mode?: 'bars' | 'type';
|
||||
/*
|
||||
Which surface the board draws. The chrome and the frame are shared and only
|
||||
the two bodies change, the same arrangement the four wireframes on the
|
||||
surface question already use, so a change to the nav lands on all of them at
|
||||
once. Named boards are the ones the font screen switches between.
|
||||
*/
|
||||
surface?: 'persuade' | 'operate' | 'read' | 'experience';
|
||||
phone?: boolean;
|
||||
class?: string;
|
||||
strategyHook?: boolean;
|
||||
typeHook?: boolean;
|
||||
cursor?: boolean;
|
||||
/*
|
||||
Marks an artboard as one that accumulates the structural answers. The
|
||||
committed layout, boundary, corner, and depth selectors key on it, which is
|
||||
how each screen shows the page as decided so far without those answers
|
||||
reaching back to the screens that came before them.
|
||||
*/
|
||||
carry?: boolean;
|
||||
/*
|
||||
The layout screen's boards carry parts no other board draws: the shared
|
||||
alignment-grid component on every desktop and handset body, and on the
|
||||
index a pagination rail for the handset plus the project rail and support
|
||||
row that only the freeform answer places (layout-portfolio.css parks them
|
||||
until it does). The index projects also take the data-project-index and
|
||||
data-project-tone hooks that screen's sheet keys plate placement and
|
||||
image treatment on. Mounted only where asked, because the motion screen
|
||||
reads the same index body through :nth-of-type and an extra sibling would
|
||||
shift its scenes.
|
||||
*/
|
||||
portfolioExtras?: boolean;
|
||||
motionCell?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
mode = 'bars',
|
||||
surface,
|
||||
phone = true,
|
||||
class: className = '',
|
||||
strategyHook = false,
|
||||
typeHook = false,
|
||||
cursor = false,
|
||||
carry = false,
|
||||
portfolioExtras = false,
|
||||
motionCell,
|
||||
} = Astro.props;
|
||||
const type = mode === 'type';
|
||||
const ops = surface === 'operate';
|
||||
const docs = surface === 'read';
|
||||
const index = surface === 'experience';
|
||||
const classes = ['picker-artboard', className].filter(Boolean).join(' ');
|
||||
// Empty string renders the bare attribute the font screen's selectors expect;
|
||||
// undefined drops it entirely on the other screens.
|
||||
const hook = type ? '' : undefined;
|
||||
---
|
||||
|
||||
<div
|
||||
class={classes}
|
||||
data-artboard=""
|
||||
data-surface={surface}
|
||||
data-motion-cell={motionCell}
|
||||
data-carry={carry ? '' : undefined}
|
||||
data-strategy-preview={strategyHook ? '' : undefined}
|
||||
data-type-preview={typeHook ? '' : undefined}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="ps-desktop">
|
||||
{portfolioExtras && <div class="ps-layout-grid" aria-hidden="true"></div>}
|
||||
<div class="ps-nav">
|
||||
<div class="ps-nav-lede">
|
||||
{!type && <i class="ps-menu"></i>}
|
||||
<div class="ps-brand-block" data-type-brand={hook}></div>
|
||||
{!type && <i class="ps-brand-word"></i>}
|
||||
</div>
|
||||
{type ? (
|
||||
<div class="ps-nav-bars"><i data-type-nav></i><i data-type-nav></i><i data-type-nav></i><i data-type-nav></i></div>
|
||||
) : (
|
||||
<div class="ps-nav-bars"><i></i><i></i><i></i><i></i></div>
|
||||
)}
|
||||
{!type && (
|
||||
<div class="ps-nav-tail">
|
||||
<i class="ps-search"></i>
|
||||
<div class="ps-nav-action" data-type-nav-action={hook}></div>
|
||||
<i class="ps-avatar"></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{ops ? (
|
||||
<div class="ps-ops">
|
||||
{/* The rail is where a tool keeps its navigation, so it carries the
|
||||
longest run of micro labels on any board. The open one is a filled
|
||||
plate rather than a recolor: a selection is a state, not a word. */}
|
||||
<div class="ps-ops-rail">
|
||||
{[0, 1, 2].map((n) => (
|
||||
<span class:list={['ps-ops-item', n === 1 && 'ps-ops-item--on']}>
|
||||
<i></i><b data-type-rail={hook}></b>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div class="ps-ops-main">
|
||||
<strong class="ps-ops-title" data-type-section-title={hook}></strong>
|
||||
<div class="ps-ops-metrics">
|
||||
{[0, 1, 2].map(() => (
|
||||
<span class="ps-ops-metric">
|
||||
<b data-type-figure={hook}></b>
|
||||
<span data-type-proof={hook}></span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{/* A chart is the one thing a dashboard has that no other surface
|
||||
does, and it sets two kinds of text a pair has to be judged on
|
||||
together: a heading over a plot, and a row of category labels
|
||||
sitting under it at the smallest step on the board. The bars
|
||||
themselves stay schematic, drawn as a share of the plot rather
|
||||
than from numbers, because what is being chosen here is a pair
|
||||
and not a reading of the data. */}
|
||||
<div class="ps-ops-chart">
|
||||
<b class="ps-ops-chart-title" data-type-chart-title={hook}></b>
|
||||
<div class="ps-ops-plot">
|
||||
{[46, 66, 100, 58, 44].map((h, n) => (
|
||||
<i class:list={[n === 2 && 'ps-ops-bar--lead']} style={`--h: ${h}%`}></i>
|
||||
))}
|
||||
</div>
|
||||
<div class="ps-ops-lanes">
|
||||
{[0, 1, 2, 3, 4].map(() => <b data-type-lane={hook}></b>)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ps-ops-table">
|
||||
<span class="ps-ops-row ps-ops-row--head">
|
||||
<i></i>
|
||||
<b data-type-column={hook}></b><b data-type-column={hook}></b><b data-type-column={hook}></b>
|
||||
</span>
|
||||
{[0, 1, 2].map((n) => (
|
||||
<span class:list={['ps-ops-row', n === 1 && 'ps-ops-row--on']}>
|
||||
<i></i>
|
||||
<b data-type-gallery-title={hook}></b>
|
||||
<b data-type-gallery-meta={hook}></b>
|
||||
<b data-type-amount={hook}></b>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ps-ops-panel">
|
||||
<b class="ps-ops-panel-title" data-type-panel={hook}></b>
|
||||
<span class="ps-ops-field"><b data-type-panel={hook}></b><i class="ps-ops-chev"></i></span>
|
||||
{[0, 1].map((n) => (
|
||||
<span class="ps-ops-switch">
|
||||
<b data-type-switch={hook}></b>
|
||||
<i class:list={['ps-ops-toggle', n === 0 && 'ps-ops-toggle--on']}></i>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : docs ? (
|
||||
<div class="ps-docs">
|
||||
{/* The rail names sections of one document rather than pages of a site,
|
||||
which is why it cannot take the nav's words: the two would read as
|
||||
the same list drawn twice. */}
|
||||
<div class="ps-docs-rail">
|
||||
{[0, 1, 2, 3].map((n) => (
|
||||
<span class:list={['ps-docs-item', n === 1 && 'ps-docs-item--on']}>
|
||||
<i></i><b data-type-rail={hook}></b>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{/* Two heading levels and the passages between them. A documentation
|
||||
page is the one surface where the relationship between a page title
|
||||
and the heading under it is visible at a glance, so the board shows
|
||||
both rather than the single heading the drawing carries. */}
|
||||
<div class="ps-docs-main">
|
||||
<h2 class="pt-headline" data-type-headline={hook}>{!type && <Fragment><i></i><i></i></Fragment>}</h2>
|
||||
<p class="ps-docs-lede" data-type-body={hook}>{!type && [0, 1].map(() => <i></i>)}</p>
|
||||
<strong class="ps-docs-sub" data-type-section-title={hook}></strong>
|
||||
<p class="ps-docs-para" data-type-passage={hook}>{!type && [0, 1, 2].map(() => <i></i>)}</p>
|
||||
<p class="ps-docs-para" data-type-passage={hook}>{!type && [0, 1, 2].map(() => <i></i>)}</p>
|
||||
{/* Prose, a list, and a callout are what a documentation page is
|
||||
made of, and the board carried two of the three. The list is
|
||||
also the only setting here that asks the body face to hold a
|
||||
short ragged line against a marker. */}
|
||||
<ul class="ps-docs-list">
|
||||
{[0, 1].map(() => <li data-type-item={hook}></li>)}
|
||||
</ul>
|
||||
<div class="ps-docs-note">
|
||||
<span class="ps-docs-note-copy">
|
||||
<b data-type-note-label={hook}></b>
|
||||
<span data-type-note-body={hook}>{!type && [0, 1].map(() => <i></i>)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : index && !type ? (
|
||||
/* The index as a wireframe, for the screen that judges movement rather
|
||||
than glyphs. Same four bands as the set version below, drawn in bars:
|
||||
a page title, two staggered entries, and the carousel rail. */
|
||||
<div class="ps-index">
|
||||
{portfolioExtras && (
|
||||
<div class="ps-project-rail" aria-hidden="true"><i></i><i></i><i></i></div>
|
||||
)}
|
||||
<i class="ps-index-head"></i>
|
||||
{[0, 1].map((n) => (
|
||||
<div class:list={['ps-index-row', n === 1 && 'ps-index-row--flip']}>
|
||||
<div
|
||||
class="ps-image"
|
||||
data-project-index={portfolioExtras ? String(n + 1) : undefined}
|
||||
data-project-tone={portfolioExtras ? (n === 0 ? 'green' : 'light') : undefined}
|
||||
></div>
|
||||
<div class="ps-index-cap">
|
||||
<i class="ps-index-name"></i>
|
||||
<i class="ps-index-tag"></i>
|
||||
<span class="ps-index-lines"><b></b><b></b></span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{portfolioExtras && (
|
||||
<div class="ps-support" aria-hidden="true">
|
||||
<div class="ps-image ps-support-image" data-project-tone="light"></div>
|
||||
<span class="ps-support-meta"><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
)}
|
||||
<div class="ps-index-rail">
|
||||
<i class="ps-index-arrow"></i>
|
||||
<div class="ps-index-track">
|
||||
{[0, 1, 2, 3].map((n) => (
|
||||
<b class:list={[n === 0 && 'ps-index-stop--on']}></b>
|
||||
))}
|
||||
</div>
|
||||
<i class="ps-index-arrow ps-index-arrow--next"></i>
|
||||
</div>
|
||||
</div>
|
||||
) : index ? (
|
||||
<div class="ps-index">
|
||||
{/* The page title the drawing has no room for, over the two staggered
|
||||
entries it draws. The second entry is what the handset shows too,
|
||||
and it is the one that proves the caption ladder repeats: a pair set
|
||||
on an index is read down a run of entries rather than at one of
|
||||
them. Alternating the plate keeps the run from collapsing into a
|
||||
single column at a width that has room for two. */}
|
||||
<h2 class="pt-headline" data-type-headline></h2>
|
||||
{[0, 1].map((n) => (
|
||||
<div class:list={['ps-index-row', n === 1 && 'ps-index-row--flip']}>
|
||||
<div class="ps-image"></div>
|
||||
<div class="ps-index-cap">
|
||||
<i class="ps-index-avatar" aria-hidden="true"></i>
|
||||
<strong class="ps-index-title" data-type-gallery-title></strong>
|
||||
<span class="ps-index-meta" data-type-gallery-meta></span>
|
||||
<span class="ps-index-note" data-type-caption></span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{/* The chevrons stay shapes because an arrow is not a word. The track's
|
||||
segments are, so they are set: a carousel's stops are named. */}
|
||||
<div class="ps-index-rail">
|
||||
<i class="ps-index-arrow"></i>
|
||||
<div class="ps-index-track">
|
||||
{[0, 1, 2, 3].map((n) => (
|
||||
<b class:list={[n === 0 && 'ps-index-stop--on']} data-type-stop></b>
|
||||
))}
|
||||
</div>
|
||||
<i class="ps-index-arrow ps-index-arrow--next"></i>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Fragment>
|
||||
<div class="ps-hero">
|
||||
<div class="ps-hero-copy">
|
||||
{type ? (
|
||||
<Fragment>
|
||||
<h2 class="pt-headline" data-type-headline></h2>
|
||||
<p class="pt-body" data-type-body></p>
|
||||
<div class="ps-actions"><i data-type-cta-primary></i><i data-type-cta-secondary></i></div>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<div class="ps-eyebrow"></div>
|
||||
<div class="ps-headline"><i></i><i></i></div>
|
||||
<div class="ps-copy"><i></i><i></i><i></i></div>
|
||||
<div class="ps-actions"><i></i><i></i></div>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
<div class="ps-image"></div>
|
||||
</div>
|
||||
<div class="ps-proof">
|
||||
{[0, 1, 2, 3].map((n) => (
|
||||
<Fragment>
|
||||
{n > 0 && <i class="ps-proof-divider" aria-hidden="true"></i>}
|
||||
<div class="ps-proof-item">
|
||||
<i></i>
|
||||
{type
|
||||
? <span data-type-proof></span>
|
||||
: <span class="ps-proof-lines"><b></b><b></b></span>}
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div class="ps-layout-editorial-spacer" data-layout-editorial-spacer aria-hidden="true"></div>
|
||||
<div class="ps-editorial">
|
||||
<div class="ps-editorial-copy">
|
||||
<strong data-type-section-title={hook}></strong>
|
||||
{type
|
||||
? <p class="pt-section-body" data-type-section-body></p>
|
||||
: <span><i></i><i></i></span>}
|
||||
<em data-type-section-link={hook}></em>
|
||||
</div>
|
||||
{/* Three cards once the labels are words, four while they are bars. The
|
||||
type board sets three because a fourth column leaves the copy beside
|
||||
it under the width the widest pairing needs, and the preview copy is
|
||||
written to that count: a fourth card here would draw a picture with
|
||||
no caption under it. The bar composition is measured at four by the
|
||||
layout screens, so it keeps them. */}
|
||||
<div class="ps-gallery">
|
||||
{(type ? [0, 1, 2] : [0, 1, 2, 3]).map(() => (
|
||||
<div class="ps-gallery-item">
|
||||
<i></i>
|
||||
{type
|
||||
? <span><b data-type-gallery-caption></b></span>
|
||||
: <span><b></b><b></b><b></b></span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
<div class="ps-footer">
|
||||
{type ? (
|
||||
<div class="ps-footer-links"><i data-type-footer-link></i><i data-type-footer-link></i><i data-type-footer-link></i><i data-type-footer-link></i></div>
|
||||
) : (
|
||||
<div class="ps-footer-links"><i></i><i></i><i></i><i></i></div>
|
||||
)}
|
||||
<div class="ps-footer-mark" data-type-footer-mark={hook}></div>
|
||||
</div>
|
||||
{/*
|
||||
Off-grid marks, drawn only where the page has already left the field:
|
||||
opacity rides --pvs-drift, so on the disciplined answers they are
|
||||
rendered and invisible and the answers that pay for them are the ones
|
||||
that asked. Absolute, so the desktop grid still has exactly its five
|
||||
declared rows.
|
||||
*/}
|
||||
{!type && (
|
||||
<Fragment>
|
||||
<i class="ps-scatter ps-scatter--lead" aria-hidden="true"></i>
|
||||
<i class="ps-scatter ps-scatter--trail" aria-hidden="true"></i>
|
||||
</Fragment>
|
||||
)}
|
||||
{cursor && <i class="ps-cursor" data-cursor aria-hidden="true"></i>}
|
||||
</div>
|
||||
|
||||
{phone && (
|
||||
<div class="ps-phone">
|
||||
<div class="ps-phone-top">
|
||||
<div class="ps-nav-lede">
|
||||
<div class="ps-brand-block" data-type-brand={hook}></div>
|
||||
{!type && <i class="ps-brand-word"></i>}
|
||||
</div>
|
||||
{!type && (
|
||||
<div class="ps-nav-tail">
|
||||
<div class="ps-nav-action" data-type-menu-action={hook}></div>
|
||||
<i class="ps-menu"></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{ops ? (
|
||||
<div class="ps-phone-body">
|
||||
<strong class="ps-ops-title" data-type-section-title={hook}></strong>
|
||||
<span class="ps-ops-metric">
|
||||
<b data-type-figure={hook}></b>
|
||||
<span data-type-proof={hook}></span>
|
||||
</span>
|
||||
{/* Three lanes rather than five. The handset keeps the leading one, so
|
||||
the emphasis stays on the same lane the desktop board marks, and it
|
||||
drops the tail: five labels at this width would either collide or
|
||||
be set under the floor this preview is allowed to reach. */}
|
||||
<div class="ps-ops-chart">
|
||||
<b class="ps-ops-chart-title" data-type-chart-title={hook}></b>
|
||||
<div class="ps-ops-plot">
|
||||
{[52, 74, 100].map((h, n) => (
|
||||
<i class:list={[n === 2 && 'ps-ops-bar--lead']} style={`--h: ${h}%`}></i>
|
||||
))}
|
||||
</div>
|
||||
<div class="ps-ops-lanes">
|
||||
{[0, 1, 2].map(() => <b data-type-lane={hook}></b>)}
|
||||
</div>
|
||||
</div>
|
||||
{/* The table has nowhere to put four columns at this width, so it
|
||||
collapses to the two cells that carry the row: what it is and what
|
||||
it comes to. */}
|
||||
<div class="ps-ops-table">
|
||||
{[0, 1, 2].map((n) => (
|
||||
<span class:list={['ps-ops-row', n === 1 && 'ps-ops-row--on']}>
|
||||
<i></i>
|
||||
<b data-type-gallery-title={hook}></b>
|
||||
<b data-type-amount={hook}></b>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div class="ps-ops-panel">
|
||||
{[0, 1].map((n) => (
|
||||
<span class="ps-ops-switch">
|
||||
<b data-type-switch={hook}></b>
|
||||
<i class:list={['ps-ops-toggle', n === 0 && 'ps-ops-toggle--on']}></i>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : docs ? (
|
||||
<div class="ps-phone-body">
|
||||
{/* The crumb is the drawing's, and it is a phone element there because
|
||||
the rail it stands in for has nowhere to go at this width. */}
|
||||
<span class="ps-docs-crumb" data-type-crumb={hook}></span>
|
||||
<h2 class="pt-headline" data-type-headline={hook}>{!type && <Fragment><i></i><i></i></Fragment>}</h2>
|
||||
<p class="ps-docs-lede" data-type-body={hook}>{!type && [0, 1].map(() => <i></i>)}</p>
|
||||
<strong class="ps-docs-sub" data-type-section-title={hook}></strong>
|
||||
<p class="ps-docs-para" data-type-passage={hook}>{!type && [0, 1, 2].map(() => <i></i>)}</p>
|
||||
<ul class="ps-docs-list">
|
||||
{[0, 1].map(() => <li data-type-item={hook}></li>)}
|
||||
</ul>
|
||||
<div class="ps-docs-note">
|
||||
<span class="ps-docs-note-copy">
|
||||
<b data-type-note-label={hook}></b>
|
||||
<span data-type-note-body={hook}>{!type && [0, 1].map(() => <i></i>)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : index ? (
|
||||
<div class="ps-phone-body">
|
||||
{/* No page title here. At this width the display step would take the
|
||||
card, and what a handset shows of an index is the work. */}
|
||||
{portfolioExtras && (
|
||||
<div class="ps-phone-project-rail" aria-hidden="true"><i></i><i></i><i></i></div>
|
||||
)}
|
||||
{portfolioExtras && <div class="ps-layout-grid" aria-hidden="true"></div>}
|
||||
{[0, 1].map((n) => (
|
||||
<Fragment>
|
||||
<div
|
||||
class="ps-image"
|
||||
data-project-index={portfolioExtras ? String(n + 1) : undefined}
|
||||
data-project-tone={portfolioExtras ? (n === 0 ? 'green' : 'light') : undefined}
|
||||
></div>
|
||||
<div
|
||||
class="ps-index-cap"
|
||||
data-project-index={portfolioExtras ? String(n + 1) : undefined}
|
||||
>
|
||||
{type ? (
|
||||
<Fragment>
|
||||
<i class="ps-index-avatar" aria-hidden="true"></i>
|
||||
<strong class="ps-index-title" data-type-gallery-title></strong>
|
||||
<span class="ps-index-meta" data-type-gallery-meta></span>
|
||||
<span class="ps-index-note" data-type-caption></span>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<i class="ps-index-name"></i>
|
||||
<i class="ps-index-tag"></i>
|
||||
<span class="ps-index-lines"><b></b><b></b></span>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
{portfolioExtras && (
|
||||
<Fragment>
|
||||
<div class="ps-support ps-phone-support" aria-hidden="true">
|
||||
<div class="ps-image ps-support-image" data-project-tone="light"></div>
|
||||
<span class="ps-support-meta"><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
{/* The desktop pagination rail restated at the foot of the stack. */}
|
||||
<div class="ps-index-rail ps-phone-pagination">
|
||||
<i class="ps-index-arrow"></i>
|
||||
<div class="ps-index-track">
|
||||
{[0, 1, 2, 3].map((n) => (
|
||||
<b class:list={[n === 0 && 'ps-index-stop--on']}></b>
|
||||
))}
|
||||
</div>
|
||||
<i class="ps-index-arrow ps-index-arrow--next"></i>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div class="ps-phone-body">
|
||||
{portfolioExtras && <div class="ps-layout-grid" aria-hidden="true"></div>}
|
||||
<div class="ps-image"></div>
|
||||
{type ? (
|
||||
<Fragment>
|
||||
<h2 class="pt-headline" data-type-headline></h2>
|
||||
<p class="pt-body" data-type-body></p>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<div class="ps-headline"><i></i><i></i></div>
|
||||
<div class="ps-copy"><i></i><i></i></div>
|
||||
</Fragment>
|
||||
)}
|
||||
<div class="ps-actions">
|
||||
<i data-type-cta-primary={hook}></i><i data-type-cta-secondary={hook}></i>
|
||||
</div>
|
||||
<div class="ps-proof">
|
||||
{[0, 1].map((n) => (
|
||||
<Fragment>
|
||||
{n > 0 && <i class="ps-proof-divider" aria-hidden="true"></i>}
|
||||
<div class="ps-proof-item">
|
||||
<i></i>
|
||||
{type
|
||||
? <span data-type-proof></span>
|
||||
: <span class="ps-proof-lines"><b></b><b></b></span>}
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div class="ps-editorial-copy">
|
||||
<strong data-type-section-title={hook}></strong>
|
||||
{type
|
||||
? <p class="pt-section-body" data-type-section-body></p>
|
||||
: <span><i></i><i></i></span>}
|
||||
</div>
|
||||
<div class="ps-gallery">
|
||||
{[0, 1].map(() => (
|
||||
<div class="ps-gallery-item">
|
||||
<i></i>
|
||||
{type
|
||||
? <span><b data-type-gallery-caption></b></span>
|
||||
: <span><b></b><b></b></span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!type && (
|
||||
<div class="ps-phone-footer">
|
||||
<Fragment><i></i><i></i><i></i></Fragment>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
/*
|
||||
One question, one artboard: the shape screens 03 onward all share. A screen
|
||||
that only remaps artboard slots needs nothing of its own here, so it arrives
|
||||
as data and its design lives in its own stylesheet.
|
||||
*/
|
||||
import Artboard from './Artboard.astro';
|
||||
import { SURFACE_ANSWERS, surfacesAsked, surfaceTabsAttrs } from '../data/surfaces.js';
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
/* The sentence that stands in for the description on a surface this option
|
||||
does not suit. Present only on options some surface rules out. */
|
||||
blocked?: string;
|
||||
}
|
||||
|
||||
/* A screen whose answer reads differently on each surface draws a board per
|
||||
surface instead of repainting one, the arrangement the font screen already
|
||||
uses. `class` is the modifier that board's own scene keys on. */
|
||||
interface Board {
|
||||
surface: 'persuade' | 'operate' | 'read' | 'experience';
|
||||
class?: string;
|
||||
cell?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
step: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
title: string;
|
||||
legend: string;
|
||||
cta: string;
|
||||
options: Option[];
|
||||
previewClass: string;
|
||||
phone?: boolean;
|
||||
boards?: Board[];
|
||||
derived?: boolean;
|
||||
}
|
||||
|
||||
const { id, step, slug, name, title, legend, cta, options, previewClass, phone = true, boards, derived = false } = Astro.props;
|
||||
const titleId = `picker-${slug}-title`;
|
||||
const artboardClass = phone ? previewClass : `${previewClass} picker-artboard--solo`;
|
||||
// A question listed in the matrix is answered once per chosen surface, which
|
||||
// buys it a tab strip on the frame and a field per surface to answer into.
|
||||
// Everything else about the screen is unchanged by opting in.
|
||||
const perSurface = SURFACE_ANSWERS[name];
|
||||
// Only the surfaces the question is put to. A surface with no field here is a
|
||||
// surface that was never asked, and that absence is the whole of how the script
|
||||
// and the final document know it.
|
||||
const asked = perSurface ? surfacesAsked(name) : [];
|
||||
const tabsAttrs = perSurface ? surfaceTabsAttrs(name) : null;
|
||||
---
|
||||
|
||||
<section class="picker-screen" data-screen={id} data-step={step} aria-hidden="true" aria-labelledby={titleId}>
|
||||
<div class="picker-container">
|
||||
<div class="picker-strategy" data-question={slug}>
|
||||
<h2 id={titleId} class="picker-question-title">{title}</h2>
|
||||
|
||||
<div class="picker-strategy-grid">
|
||||
<div class="picker-type-rail">
|
||||
<fieldset
|
||||
class="picker-strategy-options picker-strategy-choices picker-type-options"
|
||||
style={`--pk-choices:${options.length}`}
|
||||
>
|
||||
<legend>{legend}</legend>
|
||||
|
||||
{options.map((option, index) => (
|
||||
<label class="picker-strategy-option" data-blocked-reason={option.blocked}>
|
||||
<input type="radio" name={name} value={option.value} checked={index === 0} />
|
||||
<span class="picker-strategy-copy">
|
||||
<span class="picker-strategy-title">{option.title}</span>
|
||||
<span class="picker-strategy-desc">{option.desc}</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
|
||||
{/* One field per surface, disabled until its tile is chosen, so a
|
||||
surface nobody asked for stays out of the answers and a surface
|
||||
nobody opened still leaves one. A flat question keeps the same
|
||||
fields for the same reading and withholds the name, which is the
|
||||
whole of what a form submits. */}
|
||||
{asked.map((mode) => (
|
||||
<input
|
||||
type="hidden"
|
||||
data-surface-field={`${name}-${mode}`}
|
||||
name={perSurface.flat ? undefined : `${name}-${mode}`}
|
||||
disabled
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{boards || tabsAttrs ? (
|
||||
/* All of them mounted and one shown, so a tab switch costs a hidden
|
||||
attribute rather than a rebuild. The stage is the box the strip takes
|
||||
its row on, above the frame, which is why a screen with one board and
|
||||
a strip is wrapped in it too. */
|
||||
<div class="picker-board-stage" data-surface-stage={derived ? '' : undefined}>
|
||||
{tabsAttrs && <div class="picker-surface-tabs" {...tabsAttrs} hidden></div>}
|
||||
{(boards ?? [{} as Board]).map((board) => (
|
||||
<Artboard
|
||||
class={[artboardClass, board.class].filter(Boolean).join(' ')}
|
||||
surface={board.surface}
|
||||
motionCell={board.cell}
|
||||
phone={phone}
|
||||
cursor={slug === 'motion'}
|
||||
carry={slug !== 'motion'}
|
||||
portfolioExtras={slug === 'layout'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Artboard
|
||||
class={artboardClass}
|
||||
phone={phone}
|
||||
cursor={slug === 'motion'}
|
||||
carry={slug !== 'motion'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="picker-actions-stack">
|
||||
<button class="ks-button ks-button-primary" type="button" data-advance="next">
|
||||
{cta}
|
||||
<span class="ks-button-arrow" aria-hidden="true">
|
||||
<svg viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square">
|
||||
<path d="M0 4h14M10 0l4 4-4 4"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<button class="picker-back" type="button" data-advance="prev">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,429 @@
|
||||
[
|
||||
{
|
||||
"id": "side-tab",
|
||||
"name": "Side-tab accent border",
|
||||
"description": "Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.",
|
||||
"group": "slop",
|
||||
"discipline": "Visual Details"
|
||||
},
|
||||
{
|
||||
"id": "border-accent-on-rounded",
|
||||
"name": "Border accent on rounded element",
|
||||
"description": "Thick accent border on a rounded card — the border clashes with the rounded corners. Remove the border or the border-radius.",
|
||||
"group": "slop",
|
||||
"discipline": "Visual Details"
|
||||
},
|
||||
{
|
||||
"id": "overused-font",
|
||||
"name": "Overused font",
|
||||
"description": "Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.",
|
||||
"group": "slop",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "flat-type-hierarchy",
|
||||
"name": "Flat type hierarchy",
|
||||
"description": "Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).",
|
||||
"group": "slop",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "gradient-text",
|
||||
"name": "Gradient text",
|
||||
"description": "Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.",
|
||||
"group": "slop",
|
||||
"discipline": "Color & Contrast"
|
||||
},
|
||||
{
|
||||
"id": "ai-color-palette",
|
||||
"name": "AI color palette",
|
||||
"description": "Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.",
|
||||
"group": "slop",
|
||||
"discipline": "Color & Contrast"
|
||||
},
|
||||
{
|
||||
"id": "cream-palette",
|
||||
"name": "Cream / beige palette",
|
||||
"description": "A warm cream or beige page background has become the default \"tasteful\" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.",
|
||||
"group": "slop",
|
||||
"discipline": "Color & Contrast"
|
||||
},
|
||||
{
|
||||
"id": "nested-cards",
|
||||
"name": "Nested cards",
|
||||
"description": "Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.",
|
||||
"group": "slop",
|
||||
"discipline": "Layout & Space"
|
||||
},
|
||||
{
|
||||
"id": "monotonous-spacing",
|
||||
"name": "Monotonous spacing",
|
||||
"description": "The same spacing value used everywhere — no rhythm, no variation. Use tight groupings for related items and generous separations between sections.",
|
||||
"group": "slop",
|
||||
"discipline": "Layout & Space"
|
||||
},
|
||||
{
|
||||
"id": "bounce-easing",
|
||||
"name": "Bounce or elastic easing",
|
||||
"description": "Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.",
|
||||
"group": "slop",
|
||||
"discipline": "Motion"
|
||||
},
|
||||
{
|
||||
"id": "pulsing-dot",
|
||||
"name": "Pulsing status dot",
|
||||
"description": "Small pulsing status dots simulate liveness decoratively. Reserve pulse animation for indicators tied to genuinely live, changing data; a static indicator with clear labeling is honest and calmer.",
|
||||
"group": "slop",
|
||||
"discipline": "Motion"
|
||||
},
|
||||
{
|
||||
"id": "blinking-cursor",
|
||||
"name": "Decorative blinking cursor",
|
||||
"description": "A blinking text cursor animated into a hero or landing section simulates typing where no input exists. It borrows the dev-tool aesthetic as decoration. Real editable fields draw their own caret; anywhere else, let the composition hold attention without a fake prompt.",
|
||||
"group": "slop",
|
||||
"discipline": "Motion"
|
||||
},
|
||||
{
|
||||
"id": "shape-assembled-illustration",
|
||||
"name": "Shape-assembled illustration",
|
||||
"description": "A large inline SVG that builds a pictorial scene from a pile of primitive shapes reads as placeholder clip art, not illustration. Icons, logos, and data graphics are fine at their scale; a hero-sized visual deserves real artwork, a photograph, or a deliberately drawn graphic.",
|
||||
"group": "slop",
|
||||
"discipline": "Imagery"
|
||||
},
|
||||
{
|
||||
"id": "organic-clip-path",
|
||||
"name": "Organic contour drawn as clip-path",
|
||||
"description": "A clip-path polygon with many arbitrary vertices, or a curved clip-path path(), is CSS approximating a torn edge, blob, or silhouette. It reads as the cheap version of the effect and is usually a produced or photographic material replaced with code. Derive an alpha matte from the real image, or ship the shape as a cut-out raster; keep clip-path for geometry (cut corners, diagonals, hexagons).",
|
||||
"group": "quality",
|
||||
"discipline": "Imagery"
|
||||
},
|
||||
{
|
||||
"id": "buried-raster",
|
||||
"name": "Raster buried under a wash or opacity",
|
||||
"description": "A background image under a near-opaque gradient wash, or a raster on an element at near-zero opacity, never reaches the screen: the page shows the wash, and the produced texture or photo ships as a compliance token. Let the material show (a tint under 0.9 alpha, a blend mode, an opacity you can see) or remove the file.",
|
||||
"group": "quality",
|
||||
"discipline": "Imagery"
|
||||
},
|
||||
{
|
||||
"id": "dark-glow",
|
||||
"name": "Glowing shadow accents",
|
||||
"description": "Colored glow shadows — a zero-offset chromatic halo (box- or text-shadow) on any background, or any colored blurred shadow on a dark background — are the default \"cool\" look of AI-generated UIs. Use neutral elevation shadows and subtle, purposeful lighting instead.",
|
||||
"group": "slop",
|
||||
"discipline": "Color & Contrast"
|
||||
},
|
||||
{
|
||||
"id": "radial-halo",
|
||||
"name": "Radial-gradient background halo",
|
||||
"description": "A chromatic radial-gradient wash — saturated at the center, fading to transparent — used as a decorative background glow on a dark page. Same tell as glowing shadows, drawn with a gradient instead of a shadow. Ground the surface with a solid or subtly shifted background instead.",
|
||||
"group": "slop",
|
||||
"discipline": "Color & Contrast"
|
||||
},
|
||||
{
|
||||
"id": "radial-spotlight-glow",
|
||||
"name": "Decorative radial spotlight glow",
|
||||
"description": "A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a \"spotlight.\" It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.",
|
||||
"group": "slop",
|
||||
"discipline": "Color & Contrast"
|
||||
},
|
||||
{
|
||||
"id": "marquee",
|
||||
"name": "Auto-scrolling marquee",
|
||||
"description": "Continuously auto-scrolling content demands attention it has not earned and hides half its content at any moment. Reserve motion for content that changes; let readers move at their own pace.",
|
||||
"group": "slop",
|
||||
"discipline": "Motion"
|
||||
},
|
||||
{
|
||||
"id": "icon-tile-stack",
|
||||
"name": "Icon tile stacked above heading",
|
||||
"description": "A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.",
|
||||
"group": "slop",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "italic-serif-display",
|
||||
"name": "Italic serif display headline",
|
||||
"description": "Oversized italic serif (Fraunces, Recoleta, Playfair, Newsreader-italic) as the primary hero headline reads as taste in isolation but has become the universal AI-startup landing page hero. Set roman, or move to a non-serif display face. Editorial / magazine register may legitimately want this — judge by context.",
|
||||
"group": "slop",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "hero-eyebrow-chip",
|
||||
"name": "Hero eyebrow / pill chip",
|
||||
"description": "A tiny uppercase letter-spaced label sitting immediately above an oversized hero headline — or the same shape rendered as a pill chip — is now the default AI SaaS hero. Drop the eyebrow, integrate the kicker into the headline, or run it as a navigation breadcrumb instead.",
|
||||
"group": "slop",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "kicker-above-heading",
|
||||
"name": "Kicker / eyebrow label above heading",
|
||||
"description": "A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.",
|
||||
"group": "slop",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "numbered-section-labels",
|
||||
"name": "Tiny numbered section labels",
|
||||
"description": "Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence.",
|
||||
"group": "slop",
|
||||
"discipline": "Layout & Space"
|
||||
},
|
||||
{
|
||||
"id": "em-dash-overuse",
|
||||
"name": "Em-dash overuse",
|
||||
"description": "Em-dash saturation in body copy is an AI cadence tell. Advisory only: humans use em-dashes legitimately, so this fires only on saturation — at least 8 em-dashes (— or --) at a density near one per 500 characters of body text — never on a long article that uses a few. Prefer commas, colons, periods, or parentheses.",
|
||||
"group": "slop",
|
||||
"discipline": "Copy"
|
||||
},
|
||||
{
|
||||
"id": "marketing-buzzword",
|
||||
"name": "Marketing buzzword",
|
||||
"description": "Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.",
|
||||
"group": "slop",
|
||||
"discipline": "Copy"
|
||||
},
|
||||
{
|
||||
"id": "aphoristic-cadence",
|
||||
"name": "Aphoristic-cadence copy",
|
||||
"description": "Three or more sections landing on a short rebuttal sentence (\"X. No Y.\" / \"X. Just Y.\") or a manufactured-contrast aphorism (\"Not a feature. A platform.\") reads as AI cadence, not voice. Once is fine; the pattern is the tell.",
|
||||
"group": "slop",
|
||||
"discipline": "Copy"
|
||||
},
|
||||
{
|
||||
"id": "oversized-h1",
|
||||
"name": "Oversized hero headline",
|
||||
"description": "A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.",
|
||||
"group": "slop",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "extreme-negative-tracking",
|
||||
"name": "Crushed letter spacing",
|
||||
"description": "Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.",
|
||||
"group": "slop",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "broken-image",
|
||||
"name": "Broken or placeholder image",
|
||||
"description": "<img> tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.",
|
||||
"group": "quality",
|
||||
"discipline": "Imagery"
|
||||
},
|
||||
{
|
||||
"id": "script-error",
|
||||
"name": "Uncaught script error on load",
|
||||
"description": "A script threw an uncaught exception or failed to parse while the page loaded. Broken JavaScript silently kills reveals, interactions, and dynamic content, and can leave most of a page invisible. Fix the error before judging anything else.",
|
||||
"group": "quality",
|
||||
"discipline": "Quality"
|
||||
},
|
||||
{
|
||||
"id": "content-hidden-at-rest",
|
||||
"name": "Content invisible at rest",
|
||||
"description": "A large share of the page text sits at opacity 0 or visibility hidden even after every reveal handler had a chance to run. This is the failed-reveal signature: the content shipped but never becomes visible. Make content visible by default and let JavaScript enhance its entrance instead of gating its existence.",
|
||||
"group": "quality",
|
||||
"discipline": "Layout & Space"
|
||||
},
|
||||
{
|
||||
"id": "edge-flush-cards",
|
||||
"name": "Cards flush against the scroller edge",
|
||||
"description": "Cards inside a horizontal scroller or tab panel sit flush against the container edge at rest while keeping a gutter on the other side, so their edges and rounded corners get cut off. Usually the panel is sized wider than its clip box. Keep a consistent inset on both sides.",
|
||||
"group": "quality",
|
||||
"discipline": "Layout & Space"
|
||||
},
|
||||
{
|
||||
"id": "text-occlusion",
|
||||
"name": "Text occluded by an overlapping element",
|
||||
"description": "Text is painted under an opaque element or a second text run, so part of it cannot be read. A decorative box, a stacked layer, or an inline element with leaked padding lands on the words instead of beside them. Give overlapping layers room, or move the text out from under the layer above it.",
|
||||
"group": "quality",
|
||||
"discipline": "Layout & Space"
|
||||
},
|
||||
{
|
||||
"id": "first-viewport-column-overflow",
|
||||
"name": "One column stretches the first viewport",
|
||||
"description": "A multi-column opening section lets one column run far past the fold while its sibling fits in a single viewport, so the short column floats in dead space and the fold falls deep inside one section. Balance the columns, cap the tall one, or let the long content flow below the opening row.",
|
||||
"group": "quality",
|
||||
"discipline": "Layout & Space"
|
||||
},
|
||||
{
|
||||
"id": "gray-on-color",
|
||||
"name": "Gray text on colored background",
|
||||
"description": "Gray text looks washed out on colored backgrounds. Use a darker shade of the background color instead, or white/near-white for contrast.",
|
||||
"group": "quality",
|
||||
"discipline": "Color & Contrast"
|
||||
},
|
||||
{
|
||||
"id": "low-contrast",
|
||||
"name": "Low contrast text",
|
||||
"description": "Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.",
|
||||
"group": "quality",
|
||||
"discipline": "Quality"
|
||||
},
|
||||
{
|
||||
"id": "layout-transition",
|
||||
"name": "Layout property animation",
|
||||
"description": "Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.",
|
||||
"group": "quality",
|
||||
"discipline": "Motion"
|
||||
},
|
||||
{
|
||||
"id": "line-length",
|
||||
"name": "Line length too long",
|
||||
"description": "Text lines wider than ~80 characters are hard to read. The eye loses its place tracking back to the start of the next line. Add a max-width (65ch to 75ch) to text containers.",
|
||||
"group": "quality",
|
||||
"discipline": "Layout & Space"
|
||||
},
|
||||
{
|
||||
"id": "cramped-padding",
|
||||
"name": "Cramped padding",
|
||||
"description": "Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.",
|
||||
"group": "quality",
|
||||
"discipline": "Layout & Space"
|
||||
},
|
||||
{
|
||||
"id": "body-text-viewport-edge",
|
||||
"name": "Body text touching viewport edge",
|
||||
"description": "Body paragraphs render flush against the left or right viewport edge with no container providing horizontal padding. Wrap content in a container with at least 16px (ideally 24-32px) of horizontal padding, or apply max-width with mx-auto.",
|
||||
"group": "quality",
|
||||
"discipline": "Layout & Space"
|
||||
},
|
||||
{
|
||||
"id": "tight-leading",
|
||||
"name": "Tight line height",
|
||||
"description": "Line height below 1.3x the font size makes multi-line text hard to read. Use 1.5 to 1.7 for body text so lines have room to breathe.",
|
||||
"group": "quality",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "skipped-heading",
|
||||
"name": "Skipped heading level",
|
||||
"description": "Heading levels should not skip (e.g. h1 then h3 with no h2). Screen readers use heading hierarchy for navigation. Skipping levels breaks the document outline.",
|
||||
"group": "quality",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "heading-rhythm",
|
||||
"name": "Heading crowded against the previous block",
|
||||
"description": "A heading binds to the content it introduces, so the rendered space above it should exceed the space below it. When headings across a page sit as close or closer to the block above than to their own content, every section reads as if it captions the previous one. Open up the space above each heading.",
|
||||
"group": "quality",
|
||||
"discipline": "Layout & Space"
|
||||
},
|
||||
{
|
||||
"id": "justified-text",
|
||||
"name": "Justified text",
|
||||
"description": "Justified text without hyphenation creates uneven word spacing (\"rivers of white\"). Use text-align: left for body text, or enable hyphens: auto if you must justify.",
|
||||
"group": "quality",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "tiny-text",
|
||||
"name": "Tiny body text",
|
||||
"description": "Body text below 12px is hard to read, especially on high-DPI screens. Use at least 14px for body content, 16px is ideal.",
|
||||
"group": "quality",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "undersized-ui-text",
|
||||
"name": "Undersized functional text",
|
||||
"description": "Interactive and content-bearing UI text (links, buttons, nav items, labels, table cells, meta rows, timecodes) below 11px is a legibility failure, not a style choice. WCAG sets no absolute pixel floor, but functional text under 11px is a defensible quality bar: it fails on high-DPI and small viewports and it degrades tap and read targets. The 11px floor holds even inside a footer; only non-interactive legal smallprint gets the softer 10px floor. Being ON the DESIGN.md size ramp does not exempt a value here: adding 8px to the ramp launders the token but not the legibility problem, and that is exactly the escape hatch this rule closes. Exempts sup/sub, visually-hidden (sr-only) text, and code/terminal contexts. Decorative letterspaced micro-labels are still functional and stay in scope.",
|
||||
"group": "quality",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "all-caps-body",
|
||||
"name": "All-caps body text",
|
||||
"description": "Long passages in uppercase are hard to read. We recognize words by shape (ascenders and descenders), which all-caps removes. Reserve uppercase for short labels and headings.",
|
||||
"group": "quality",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "wide-tracking",
|
||||
"name": "Wide letter spacing on body text",
|
||||
"description": "Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.",
|
||||
"group": "quality",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "text-overflow",
|
||||
"name": "Content overflowing its container",
|
||||
"description": "Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.",
|
||||
"group": "quality",
|
||||
"discipline": "Layout & Space"
|
||||
},
|
||||
{
|
||||
"id": "repeated-container-text",
|
||||
"name": "Same text repeated inside one container",
|
||||
"description": "The same literal text rendered three or more times in structurally different spots inside a single card or panel is redundant messaging — usually a status or label wired into every slot of a template. Say it once, in the slot where it matters most.",
|
||||
"group": "quality",
|
||||
"discipline": "Quality"
|
||||
},
|
||||
{
|
||||
"id": "clipped-overflow-container",
|
||||
"name": "Positioned child clipped by overflow container",
|
||||
"description": "A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.",
|
||||
"group": "quality",
|
||||
"discipline": "Layout & Space"
|
||||
},
|
||||
{
|
||||
"id": "design-system-font",
|
||||
"name": "Font outside DESIGN.md",
|
||||
"description": "A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.",
|
||||
"group": "quality",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "design-system-color",
|
||||
"name": "Color outside DESIGN.md",
|
||||
"description": "A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.",
|
||||
"group": "quality",
|
||||
"discipline": "Color & Contrast"
|
||||
},
|
||||
{
|
||||
"id": "design-system-radius",
|
||||
"name": "Radius outside DESIGN.md",
|
||||
"description": "A border-radius value is outside the DESIGN.md rounded scale. Use a documented radius token or update the design system if the new shape is intentional.",
|
||||
"group": "quality",
|
||||
"discipline": "Visual Details"
|
||||
},
|
||||
{
|
||||
"id": "design-system-font-size",
|
||||
"name": "Font size outside DESIGN.md",
|
||||
"description": "A literal font-size is off the type ramp documented in DESIGN.md typography. Use a documented size step or update the design system if the new step is intentional.",
|
||||
"group": "quality",
|
||||
"discipline": "Typography"
|
||||
},
|
||||
{
|
||||
"id": "gpt-thin-border-wide-shadow",
|
||||
"name": "Hairline border with wide shadow",
|
||||
"description": "A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.",
|
||||
"group": "fingerprints",
|
||||
"discipline": "Visual Details"
|
||||
},
|
||||
{
|
||||
"id": "repeating-stripes-gradient",
|
||||
"name": "Repeating-gradient stripes",
|
||||
"description": "Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.",
|
||||
"group": "fingerprints",
|
||||
"discipline": "Visual Details"
|
||||
},
|
||||
{
|
||||
"id": "codex-grid-background",
|
||||
"name": "Decorative grid-line background",
|
||||
"description": "A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.",
|
||||
"group": "fingerprints",
|
||||
"discipline": "Visual Details"
|
||||
},
|
||||
{
|
||||
"id": "theater-slop-phrase",
|
||||
"name": "Theater framing copy",
|
||||
"description": "Dismissing something as \"theater\" is a recurring generated-copy tic. Say plainly what the thing does or does not do.",
|
||||
"group": "fingerprints",
|
||||
"discipline": "Copy"
|
||||
},
|
||||
{
|
||||
"id": "image-hover-transform",
|
||||
"name": "Image hover transform",
|
||||
"description": "Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.",
|
||||
"group": "slop",
|
||||
"discipline": "Motion"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
The four kinds of surface a design system can be asked to serve, straight from
|
||||
the skill's modes, and the questions that are answered once for each of them.
|
||||
|
||||
Values travel as `surface-modes` (multi-select) from screen 01b; the agent
|
||||
pre-checks what PRODUCT.md suggests via the design-context store's
|
||||
context.json, and the visitor corrects it. Persuade is the markup default so
|
||||
the answer can never arrive empty on runs that carry no hint.
|
||||
*/
|
||||
export const SURFACE_MODES = ['persuade', 'operate', 'read', 'experience'];
|
||||
|
||||
/*
|
||||
A question listed here is answered once per chosen surface rather than once
|
||||
per run, because the answer that suits the marketing page rarely suits the
|
||||
tool it sells. Each entry carries:
|
||||
|
||||
- `allow`, the options that surface can take. Anything left out stays in the
|
||||
list, turned off, with the option's own `data-blocked-reason` in place of
|
||||
its description. Whether an option is out belongs to the option and not to
|
||||
the pairing, so the reason is written once beside the copy it replaces.
|
||||
Omitted where the question rules nothing out, which is not the same as
|
||||
listing nothing, so the attribute is left off rather than left empty.
|
||||
- `fallback`, what the surface lands on when nobody ever opens its tab. Every
|
||||
chosen surface leaves an answer, so every surface needs one. Omitted where
|
||||
the options are not known until the run deals them, and the first row on
|
||||
the list stands in.
|
||||
- `answered` and `unanswered`, what the tab tells a screen reader. `{}` is
|
||||
where the chosen option's own label goes, lowercased so it reads as part of
|
||||
the sentence around it. `properName` turns that off for a question whose
|
||||
options are named after something rather than described.
|
||||
- `flat`, for a question whose tabs are there to look with rather than to
|
||||
split. The per-surface reading is still kept while the screen is open, so an
|
||||
option a surface rules out cannot be the answer left behind, but only the
|
||||
leading surface's choice is written down. A key per surface would promise
|
||||
whoever reads the answers a distinction the run has nowhere to spend.
|
||||
|
||||
A question that leaves a surface out of `surfaces` is not asked of that
|
||||
surface at all, which is a stronger statement than withholding an option from
|
||||
it. Withholding says the surface would answer this badly; leaving it out says
|
||||
the surface has no stake in the question. So the tab strip only offers the
|
||||
surfaces the question names, and a run whose surfaces are all left out never
|
||||
sees the screen and records no answer for it. Motion and layout are scoped
|
||||
that way today; the rest name all four.
|
||||
|
||||
Persuade is allowed everything on every question: earning attention is the
|
||||
whole job of the surface, and no answer here is too much for it. The other
|
||||
three are ruled by what the surface is for rather than by how loud an option
|
||||
is. A tool has to stay scannable, which rules out the loudest answers and,
|
||||
on boundaries, the emptiest one as well. A page read at length is one plane
|
||||
and one column. On a portfolio the work leads, so it is the interface that
|
||||
has to recede; the page itself is still allowed to be dramatic, which is why
|
||||
drenched survives there and four working colors do not.
|
||||
*/
|
||||
export const SURFACE_ANSWERS = {
|
||||
'color-strategy': {
|
||||
tablist: 'Surface being colored',
|
||||
answered: 'colored {}',
|
||||
unanswered: 'no color strategy chosen yet',
|
||||
surfaces: {
|
||||
persuade: { allow: 'restrained committed full-palette drenched', fallback: 'full-palette' },
|
||||
operate: { allow: 'restrained committed full-palette', fallback: 'full-palette' },
|
||||
read: { allow: 'restrained committed full-palette', fallback: 'full-palette' },
|
||||
// The one surface that cannot take four working colors, so it keeps the
|
||||
// quietest answer its allow list has.
|
||||
experience: { allow: 'restrained committed drenched', fallback: 'restrained' },
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
Flat, and the only question here that rules nothing out. There is no pair
|
||||
that suits a landing page and is forbidden on a dashboard; a pair that fails
|
||||
the dashboard is a bad pair, and the model composing fonts.json is told to
|
||||
rank all six against the strictest surface the run names. So no allow list
|
||||
is written, and every row stays live on every tab.
|
||||
|
||||
The tabs earn their place all the same. One pair is chosen and one type
|
||||
system comes out of the run, and the thing a visitor cannot otherwise do is
|
||||
see that pair set as a dashboard, as a document, and as an index before
|
||||
committing to it. The strip here is a way of looking rather than a second
|
||||
decision.
|
||||
*/
|
||||
'font-pair': {
|
||||
tablist: 'Surface the pair is shown on',
|
||||
answered: 'set in {}',
|
||||
unanswered: 'no pair chosen yet',
|
||||
properName: true,
|
||||
flat: true,
|
||||
surfaces: {
|
||||
persuade: {},
|
||||
operate: {},
|
||||
read: {},
|
||||
experience: {},
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
One of the two questions that are not put to every surface. Motion energy is
|
||||
a claim on attention, and only two of the four surfaces are in a position to
|
||||
make one: a landing page earning a decision and a portfolio presenting work.
|
||||
A tool and a document are moved through rather than watched, and their motion
|
||||
follows from what the interface is doing rather than from a house style, so
|
||||
asking them would collect an answer nothing should act on. An app-UI-only or
|
||||
docs-only run therefore never reaches this screen.
|
||||
|
||||
Both surfaces that are asked can take all three energies, so nothing is
|
||||
withheld and no option carries a reason. Their defaults differ, because what
|
||||
a surface does with movement when nobody says otherwise is the whole of what
|
||||
it is for: a page earning a decision has to answer the pointer it is trying
|
||||
to keep, and a portfolio stages the work's arrival, which is what a reveal
|
||||
is for.
|
||||
*/
|
||||
'motion-energy': {
|
||||
tablist: 'Surface being moved',
|
||||
answered: '{} movement',
|
||||
unanswered: 'no motion energy chosen yet',
|
||||
surfaces: {
|
||||
persuade: { fallback: 'responsive' },
|
||||
experience: { fallback: 'choreographed' },
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
The other question that is not put to every surface, and the only one that is
|
||||
scoped and flat at once. How strict a layout feels is a claim about the
|
||||
composition of the page itself, so it is asked of the two surfaces whose page
|
||||
is the thing being looked at: one earning a decision, one presenting work. A
|
||||
tool and a document are composed by what they have to hold instead, a rail
|
||||
against a working column against a panel, one column at one measure, so the
|
||||
question has nowhere to move on them and an answer taken there would be a
|
||||
decision nobody made. An app-UI-only or docs-only run therefore never reaches
|
||||
this screen either.
|
||||
|
||||
Both surfaces that are asked can take all three answers, so nothing is
|
||||
withheld and no option carries a reason. Their defaults differ: a page
|
||||
earning a decision needs one block to lead, which is what the breaks in the
|
||||
aligned grid are for, and a gallery hangs work square so the only irregular
|
||||
edges on the page are the work's own.
|
||||
|
||||
Flat, because one answer is all the run has to spend. The twelve-column ruler
|
||||
on this screen describes a page, and it describes one whether the run ships a
|
||||
dashboard or a gallery, so what the two of them leave is previewed on every
|
||||
board the screens after it draw, the shell and the document included. A key
|
||||
per surface would promise whoever reads the answers a distinction with
|
||||
nowhere to land, and the tabs are left as a way of looking at the one answer
|
||||
on both pages that asked for it.
|
||||
*/
|
||||
'layout-structure': {
|
||||
tablist: 'Surface being laid out',
|
||||
answered: '{} layout',
|
||||
unanswered: 'no layout structure chosen yet',
|
||||
flat: true,
|
||||
surfaces: {
|
||||
persuade: { fallback: 'balanced' },
|
||||
experience: { fallback: 'simple-grid' },
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
Open space is out on a tool. Operate asks for a ground of its own under
|
||||
sidebars, toolbars, and panels, and the open answer is defined by every
|
||||
ground being the same one. Its default is that second ground rather than
|
||||
panels, which spend an edge and an inset on every object and buy density
|
||||
back at the price of it.
|
||||
|
||||
Cards and panels are out on the two surfaces whose content is the point.
|
||||
A page read at length is one column, and a page of work is the work.
|
||||
*/
|
||||
'boundary-style': {
|
||||
tablist: 'Surface being separated',
|
||||
answered: 'separated by {}',
|
||||
unanswered: 'no boundary style chosen yet',
|
||||
surfaces: {
|
||||
persuade: { allow: 'open-space thin-dividers surface-changes cards-and-panels', fallback: 'open-space' },
|
||||
operate: { allow: 'thin-dividers surface-changes cards-and-panels', fallback: 'surface-changes' },
|
||||
read: { allow: 'open-space thin-dividers surface-changes', fallback: 'open-space' },
|
||||
experience: { allow: 'open-space thin-dividers surface-changes', fallback: 'open-space' },
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
The least mode-sensitive question in the run, and the matrix says so. A
|
||||
radius is not a claim on the reader's attention the way a color or a shadow
|
||||
is, and the two surfaces that look like candidates both survive scrutiny:
|
||||
fully round controls are the house style of an entire mobile platform, so
|
||||
ruling them out of app UI would put this screen against Material rather
|
||||
than against a mistake, and a document has too few shapes for the answer to
|
||||
reach. Only the portfolio blocks anything, and its default is sharp, since
|
||||
a gallery hangs work square and the work's own edges are the shapes on the
|
||||
page.
|
||||
*/
|
||||
'corner-style': {
|
||||
tablist: 'Surface being shaped',
|
||||
answered: '{} corners',
|
||||
unanswered: 'no corner style chosen yet',
|
||||
surfaces: {
|
||||
persuade: { allow: 'sharp slightly-soft friendly pill', fallback: 'slightly-soft' },
|
||||
operate: { allow: 'sharp slightly-soft friendly pill', fallback: 'slightly-soft' },
|
||||
read: { allow: 'sharp slightly-soft friendly pill', fallback: 'slightly-soft' },
|
||||
experience: { allow: 'sharp slightly-soft friendly', fallback: 'sharp' },
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
Floating is out wherever the page is worked in or read at length, which is
|
||||
the line color strategy already drew for drenched. Lift that deep is how an
|
||||
overlay says it is temporary; spent on every resting panel it stops meaning
|
||||
anything and leaves a dense screen harder to scan. The portfolio keeps it:
|
||||
lifting the work off the page is a way of presenting the work.
|
||||
*/
|
||||
'depth-style': {
|
||||
tablist: 'Surface being lifted',
|
||||
answered: '{} depth',
|
||||
unanswered: 'no depth style chosen yet',
|
||||
surfaces: {
|
||||
persuade: { allow: 'flat soft-lift floating', fallback: 'soft-lift' },
|
||||
operate: { allow: 'flat soft-lift', fallback: 'flat' },
|
||||
read: { allow: 'flat soft-lift', fallback: 'flat' },
|
||||
experience: { allow: 'flat soft-lift floating', fallback: 'flat' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/* The matrix reaches the browser on the surface tiles, which is already where
|
||||
the script looks for everything a surface knows about itself. One pair of
|
||||
attributes per question, read by name rather than by dataset key so the
|
||||
question's own value is the lookup. A question this surface is not asked
|
||||
contributes nothing, and the absent per-surface field is what the script
|
||||
reads that from. */
|
||||
export const surfaceAttrs = (mode) => Object.fromEntries(
|
||||
Object.entries(SURFACE_ANSWERS).flatMap(([name, question]) => {
|
||||
const { allow, fallback } = question.surfaces[mode] ?? {};
|
||||
return [
|
||||
...(allow ? [[`data-allow-${name}`, allow]] : []),
|
||||
...(fallback ? [[`data-default-${name}`, fallback]] : []),
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
/* Which surfaces a question is put to at all, in tile order. The per-surface
|
||||
fields a screen renders are the browser's copy of this, and the leading
|
||||
applicable surface is the one whose answer the bare key carries. */
|
||||
export const surfacesAsked = (name) => SURFACE_MODES.filter(
|
||||
(mode) => mode in SURFACE_ANSWERS[name].surfaces,
|
||||
);
|
||||
|
||||
/* Everything the script needs to run a per-surface question is on its tab
|
||||
strip, so a screen opts in by rendering one of these and nothing else. */
|
||||
export const surfaceTabsAttrs = (name) => ({
|
||||
'data-surface-tabs': name,
|
||||
'data-surface-flat': SURFACE_ANSWERS[name].flat ? '' : undefined,
|
||||
'data-surface-proper-name': SURFACE_ANSWERS[name].properName ? '' : undefined,
|
||||
'data-surface-answered': SURFACE_ANSWERS[name].answered,
|
||||
'data-surface-unanswered': SURFACE_ANSWERS[name].unanswered,
|
||||
role: 'tablist',
|
||||
'aria-label': SURFACE_ANSWERS[name].tablist,
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
import '../styles/picker.css';
|
||||
// Loaded after the shared sheet so a screen's own answers win on source order
|
||||
// as well as specificity. One file per question, each owning only the slots
|
||||
// its options rewrite.
|
||||
import '../styles/screens/motion.css';
|
||||
import '../styles/screens/layout.css';
|
||||
import '../styles/screens/boundaries.css';
|
||||
import '../styles/screens/corners.css';
|
||||
import '../styles/screens/depth.css';
|
||||
// The layout screen's portfolio board, loaded after the three sheets above on
|
||||
// purpose: it pins that board's material against their committed defaults by
|
||||
// winning ties on source order. The header of the file has the argument.
|
||||
import '../styles/screens/layout-portfolio.css';
|
||||
// The design context document that replaces the picker after submission.
|
||||
import '../styles/design-context.css';
|
||||
// The lean design context document, ported from the standalone demo. Order
|
||||
// mirrors the demo page's stylesheet order so the cascade lands identically;
|
||||
// command and settings sit where dcx-document.css used to @import them.
|
||||
import '../styles/dcx/dcx-audience.css';
|
||||
import '../styles/dcx/dcx-detail.css';
|
||||
import '../styles/dcx/dcx-components.css';
|
||||
import '../styles/dcx/dcx-hierarchy.css';
|
||||
import '../styles/dcx/dcx-command.css';
|
||||
import '../styles/dcx/dcx-settings.css';
|
||||
import '../styles/dcx/dcx-document.css';
|
||||
import '../styles/dcx/dcx-rail.css';
|
||||
import '../styles/dcx/dcx-hooks.css';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title = 'Impeccable design interview',
|
||||
description = 'Choose the design direction your agent will use.',
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="description" content={description} />
|
||||
<title>{title}</title>
|
||||
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Albert+Sans:wght@300;400;500;600;700&family=Alumni+Sans:wght@100;300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<!-- Declared here, not in picker.css, so Vite leaves the path alone. It is
|
||||
server-absolute because a url() inside var() resolves against the
|
||||
stylesheet that consumes it, which lives one directory down. -->
|
||||
<body class="picker-page" style="--pk-foil: url('/assets/kinpaku-gold-leaf.jpg')">
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
/** The boot contract: one fetch that tells the page how to start.
|
||||
*
|
||||
* Both scripts on the page read it, so it is fetched once and shared. A server
|
||||
* that predates the contract answers 404, and the page starts the way it always
|
||||
* did: a blank questionnaire.
|
||||
*/
|
||||
|
||||
let bootPromise = null;
|
||||
|
||||
export function getBoot() {
|
||||
bootPromise ??= fetch('/boot.json')
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.catch(() => null)
|
||||
.then((data) => ({
|
||||
mode: data?.mode === 'doc' ? 'doc' : 'questionnaire',
|
||||
prior: data?.prior && typeof data.prior === 'object' && !Array.isArray(data.prior) ? data.prior : null,
|
||||
priorSource: data?.priorSource || null,
|
||||
doc: data?.doc || null,
|
||||
}));
|
||||
return bootPromise;
|
||||
}
|
||||
|
||||
/* Restoring a previous run happens inside the questionnaire, which owns the
|
||||
state being restored. The document waits on this before it renders, so it
|
||||
never reads a form that is still half filled. It resolves either way: a run
|
||||
with nothing to restore is ready immediately. */
|
||||
let settle;
|
||||
export const hydrationReady = new Promise((resolve) => { settle = resolve; });
|
||||
|
||||
/* The attribute is the observable half: it drives the note on the start screen
|
||||
and gives anything watching the page one thing to wait for. */
|
||||
export function markHydrated(source) {
|
||||
document.body.dataset.hydrated = source || 'none';
|
||||
settle(source || null);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
const clamp = (value) => Math.min(1, Math.max(0, value));
|
||||
const linearize = (value) => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
||||
const gamma = (value) => value <= 0.0031308 ? 12.92 * value : 1.055 * value ** (1 / 2.4) - 0.055;
|
||||
const parseHex = (hex) => hex.match(/[\da-f]{2}/gi).map((value) => Number.parseInt(value, 16) / 255);
|
||||
|
||||
function toLinearRgb([L, C, H]) {
|
||||
const angle = H * Math.PI / 180;
|
||||
const a = C * Math.cos(angle);
|
||||
const b = C * Math.sin(angle);
|
||||
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
||||
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
||||
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
||||
return [
|
||||
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
||||
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
||||
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
|
||||
];
|
||||
}
|
||||
|
||||
export function oklchToHex([lightness, chroma, hue]) {
|
||||
const L = clamp(lightness);
|
||||
let C = Math.max(0, chroma);
|
||||
let rgb = toLinearRgb([L, C, hue]);
|
||||
while (C > 0 && rgb.some((channel) => channel < 0 || channel > 1)) {
|
||||
C = Math.max(0, C - 0.005);
|
||||
rgb = toLinearRgb([L, C, hue]);
|
||||
}
|
||||
return `#${rgb.map((channel) => Math.round(clamp(gamma(channel)) * 255).toString(16).padStart(2, '0')).join('').toUpperCase()}`;
|
||||
}
|
||||
|
||||
export function hexToOklch(hex) {
|
||||
const [red, green, blue] = parseHex(hex).map(linearize);
|
||||
const l = Math.cbrt(0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue);
|
||||
const m = Math.cbrt(0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue);
|
||||
const s = Math.cbrt(0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue);
|
||||
const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
|
||||
const a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
|
||||
const b = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
|
||||
const C = Math.hypot(a, b);
|
||||
return [L, C, C < 0.00001 ? 0 : (Math.atan2(b, a) * 180 / Math.PI + 360) % 360];
|
||||
}
|
||||
|
||||
export function formatOklch(hex) {
|
||||
const [L, C, H] = hexToOklch(hex);
|
||||
return `oklch(${(L * 100).toFixed(1)}% ${C.toFixed(3)} ${H.toFixed(1)})`;
|
||||
}
|
||||
|
||||
/** Expand one curated seed into the four questionnaire roles. */
|
||||
export function seedToRoles(seed) {
|
||||
const [L, C, H] = seed.oklch;
|
||||
return {
|
||||
primary: oklchToHex([L, C, H]),
|
||||
secondary: oklchToHex([L < 0.5 ? L + 0.18 : L - 0.18, C * 0.6, H]),
|
||||
tertiary: oklchToHex([0.62, Math.min(0.23, Math.max(C, 0.15)), (H + 60) % 360]),
|
||||
neutral: oklchToHex([L < 0.55 ? 0.96 : 0.2, 0.01, H]),
|
||||
};
|
||||
}
|
||||
|
||||
// Relative luminance of the colors --pk-ink-dark and --pk-ink-light resolve
|
||||
// to in styles/picker.css: oklch(14% 0.018 95) and oklch(99% 0.008 95).
|
||||
const INK_DARK_LUMINANCE = 0.0027;
|
||||
const INK_LIGHT_LUMINANCE = 0.9716;
|
||||
|
||||
// The ink sits on a color the user picked, so it must not follow the picker's
|
||||
// own theme: --ks-champagne and friends invert between light and dark and
|
||||
// would blank the label on exactly the swatches that need it most. Comparing
|
||||
// both ratios beats a fixed lightness threshold, which picks the losing ink
|
||||
// for mid-tones sitting near the cutoff.
|
||||
export function contrastInk(hex) {
|
||||
const swatch = relativeLuminance(hex);
|
||||
const against = (ink) => ratio(ink, swatch);
|
||||
return against(INK_DARK_LUMINANCE) >= against(INK_LIGHT_LUMINANCE)
|
||||
? 'var(--pk-ink-dark)'
|
||||
: 'var(--pk-ink-light)';
|
||||
}
|
||||
|
||||
/** Hex counterpart to contrastInk(), for readableOn() grounds that are CSS vars. */
|
||||
export function contrastInkHex(hex) {
|
||||
return contrastInk(hex).includes('light')
|
||||
? oklchToHex([0.99, 0.008, 95])
|
||||
: oklchToHex([0.14, 0.018, 95]);
|
||||
}
|
||||
|
||||
function relativeLuminance(hex) {
|
||||
const [red, green, blue] = parseHex(hex).map(linearize);
|
||||
return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
|
||||
}
|
||||
|
||||
function ratio(a, b) {
|
||||
const [hi, lo] = a > b ? [a, b] : [b, a];
|
||||
return (hi + 0.05) / (lo + 0.05);
|
||||
}
|
||||
|
||||
/**
|
||||
* The accent as type rather than as a fill. A color picked because it looks
|
||||
* right as a shape on the page is not thereby readable as words on it, and a
|
||||
* pale primary on paper is the common way that goes wrong. Only lightness
|
||||
* moves, so the hue the user chose is the hue that shows up; a color that
|
||||
* already clears the target comes back untouched.
|
||||
*/
|
||||
export function readableOn(accent, ground, target = 4.5) {
|
||||
const groundLuminance = relativeLuminance(ground);
|
||||
if (ratio(relativeLuminance(accent), groundLuminance) >= target) return accent;
|
||||
const [L, C, H] = hexToOklch(accent);
|
||||
// Toward whichever end of the range the ground leaves room in, by the same
|
||||
// comparison contrastInk makes rather than a fixed lightness cutoff.
|
||||
const darker = ratio(INK_DARK_LUMINANCE, groundLuminance) >= ratio(INK_LIGHT_LUMINANCE, groundLuminance);
|
||||
const step = darker ? -0.015 : 0.015;
|
||||
for (let lightness = L + step; lightness > 0.03 && lightness < 1; lightness += step) {
|
||||
const candidate = oklchToHex([lightness, C, H]);
|
||||
if (ratio(relativeLuminance(candidate), groundLuminance) >= target) return candidate;
|
||||
}
|
||||
return darker ? '#000000' : '#FFFFFF';
|
||||
}
|
||||
|
||||
// Floors for judging the neutral on screen 02. The neutral paints the large
|
||||
// surfaces of every preview, so two pairs matter: the fixed swatch inks that
|
||||
// set text on it, and the primary fills that sit on it.
|
||||
const NEUTRAL_INK_FLOOR = 7;
|
||||
const NEUTRAL_PRIMARY_FLOOR = 3;
|
||||
|
||||
/**
|
||||
* Plain-language warnings when the palette's neutral will cause contrast
|
||||
* trouble: an array with one line per failed check, empty when it is safe.
|
||||
* The checks run independently, so a neutral that fails both reports both,
|
||||
* mid-tone first.
|
||||
*
|
||||
* Check 1: the better of the two fixed inks must reach 7:1 (the WCAG AAA
|
||||
* body-text figure) on the neutral. The inks are near-black and near-white,
|
||||
* so 4.5:1 is nearly impossible to fail; 7:1 is the floor that catches
|
||||
* mid-tone neutrals which leave no headroom for muted and secondary text.
|
||||
*
|
||||
* Check 2: the primary must reach 3:1 (WCAG 1.4.11 non-text contrast)
|
||||
* against the neutral, because primary button fills and accents sit directly
|
||||
* on neutral surfaces and readableOn() only rescues text, never fills.
|
||||
*
|
||||
* The strings are read by people configuring a palette, not by developers:
|
||||
* no ratios, no standards names. Step 8's test matches on them.
|
||||
*/
|
||||
export function neutralContrastIssue({ neutral, primary }) {
|
||||
const surface = relativeLuminance(neutral);
|
||||
const bestInk = Math.max(
|
||||
ratio(INK_DARK_LUMINANCE, surface),
|
||||
ratio(INK_LIGHT_LUMINANCE, surface),
|
||||
);
|
||||
const issues = [];
|
||||
if (bestInk < NEUTRAL_INK_FLOOR) {
|
||||
issues.push('This background is too close to a middle gray, so text on it will be hard to read. Try a much lighter or much darker color.');
|
||||
}
|
||||
if (ratio(relativeLuminance(primary), surface) < NEUTRAL_PRIMARY_FLOOR) {
|
||||
issues.push('Your main color and this background are too similar, so buttons and cards will blend in. Try more difference between them.');
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/* Session-aware URLs for the document's own images (section foils, rail
|
||||
textures, placeholder brand assets, the components photo).
|
||||
|
||||
The submit-flow picker server exits the moment /submit resolves, and article
|
||||
images only load when a view opens, which is always after that exit. The doc
|
||||
session lives on, so once design-context.js announces one on
|
||||
window.dcxDocSession, every document asset routes through it (the session's
|
||||
/assets route arrives with that announcement). Until then paths pass through
|
||||
untouched and the page's own origin serves them — which is the whole story in
|
||||
document mode, where the picker server stays up. */
|
||||
export const dcxAsset = (assetPath) => {
|
||||
const session = window.dcxDocSession;
|
||||
if (!session?.base || !session?.token) return assetPath;
|
||||
return `${session.base}${assetPath}?token=${encodeURIComponent(session.token)}`;
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
import { dcxAsset } from './assets.js';
|
||||
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
const SECTION_META = [
|
||||
{
|
||||
labels: ["Who they are"],
|
||||
slug: "who-they-are",
|
||||
variant: "people",
|
||||
lede: "The core people this experience must speak to.",
|
||||
icon: "audience-groups-foil.png",
|
||||
},
|
||||
{
|
||||
labels: ["Emotional journey", "Emotional state"],
|
||||
slug: "emotional-journey",
|
||||
variant: "journey",
|
||||
lede: "The change in confidence the experience should create.",
|
||||
icon: "emotional-journey-foil.png",
|
||||
},
|
||||
{
|
||||
labels: ["Needs"],
|
||||
slug: "needs",
|
||||
variant: "list",
|
||||
lede: "What the experience must make clear and easy.",
|
||||
icon: "needs-foil.png",
|
||||
},
|
||||
{
|
||||
labels: ["Trust triggers"],
|
||||
slug: "trust-triggers",
|
||||
variant: "list",
|
||||
lede: "The signals that turn interest into confidence.",
|
||||
icon: "trust-triggers-foil.png",
|
||||
},
|
||||
{
|
||||
labels: ["Who must not be excluded"],
|
||||
slug: "inclusion",
|
||||
variant: "list",
|
||||
lede: "Access requirements that belong in the core experience.",
|
||||
icon: "inclusion-foil.png",
|
||||
},
|
||||
];
|
||||
|
||||
let syncFrame = 0;
|
||||
|
||||
const findMeta = (label) => SECTION_META.find((meta) => meta.labels.includes(label));
|
||||
|
||||
const unwrapColumns = (article) => {
|
||||
article.querySelectorAll(":scope > .dcx-cols").forEach((columns) => {
|
||||
const fragment = document.createDocumentFragment();
|
||||
[...columns.children].forEach((child) => fragment.appendChild(child));
|
||||
columns.replaceWith(fragment);
|
||||
});
|
||||
};
|
||||
|
||||
const enhanceAudienceArticle = (article) => {
|
||||
if (article.dataset.dcxAudienceEnhanced === "true") return;
|
||||
|
||||
article.classList.add("dcx-audience");
|
||||
article.querySelector(":scope > header")?.classList.add("dcx-audience-hero");
|
||||
unwrapColumns(article);
|
||||
|
||||
const sections = [...article.querySelectorAll(":scope > .dcx-block[data-label]")];
|
||||
sections.forEach((section, index) => {
|
||||
const label = section.dataset.label || "";
|
||||
const meta = findMeta(label);
|
||||
if (!meta) return;
|
||||
|
||||
const oldLabel = section.querySelector(":scope > .dcx-block-label");
|
||||
oldLabel?.remove();
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "dcx-audience-section-body";
|
||||
while (section.firstChild) body.appendChild(section.firstChild);
|
||||
|
||||
const heading = document.createElement("div");
|
||||
heading.className = "dcx-audience-section-heading";
|
||||
|
||||
const title = document.createElement("h3");
|
||||
title.className = "dcx-audience-section-title";
|
||||
title.id = `dcx-audience-${meta.slug}-title`;
|
||||
title.textContent = label;
|
||||
|
||||
const lede = document.createElement("p");
|
||||
lede.className = "dcx-audience-section-lede";
|
||||
lede.textContent = meta.lede;
|
||||
heading.append(title, lede);
|
||||
|
||||
const figure = document.createElement("figure");
|
||||
figure.className = "dcx-audience-section-icon";
|
||||
figure.setAttribute("data-dcx-hide-on-error", "");
|
||||
figure.setAttribute("aria-hidden", "true");
|
||||
|
||||
const image = document.createElement("img");
|
||||
image.src = dcxAsset(`/assets/audience/${meta.icon}`);
|
||||
image.alt = "";
|
||||
image.width = 256;
|
||||
image.height = 256;
|
||||
image.decoding = "async";
|
||||
if (index > 0) image.loading = "lazy";
|
||||
figure.appendChild(image);
|
||||
|
||||
const header = document.createElement("header");
|
||||
header.className = "dcx-audience-section-head";
|
||||
header.append(heading, figure);
|
||||
|
||||
section.classList.add("dcx-audience-section", `dcx-audience-section--${meta.variant}`);
|
||||
section.id = `dcx-audience-${meta.slug}`;
|
||||
if (!article.dataset.dcxCategory) {
|
||||
section.setAttribute("role", "region");
|
||||
section.setAttribute("aria-labelledby", title.id);
|
||||
}
|
||||
|
||||
if (meta.variant === "journey") {
|
||||
const journey = body.querySelector(".dcx-callout-pair");
|
||||
if (journey) {
|
||||
journey.classList.add("dcx-audience-journey");
|
||||
if (journey.children.length > 1) {
|
||||
const arrow = document.createElement("div");
|
||||
arrow.className = "dcx-audience-journey-arrow";
|
||||
arrow.setAttribute("aria-hidden", "true");
|
||||
arrow.textContent = "→";
|
||||
journey.children[0].after(arrow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (meta.variant === "list") {
|
||||
body.querySelector(".dcx-list")?.classList.add("dcx-audience-list");
|
||||
}
|
||||
|
||||
section.append(header, body);
|
||||
});
|
||||
|
||||
article.dataset.dcxAudienceEnhanced = "true";
|
||||
};
|
||||
|
||||
const syncAudience = () => {
|
||||
syncFrame = 0;
|
||||
const expander = document.querySelector(".dcx-expander");
|
||||
if (!expander) return;
|
||||
|
||||
const continuousArticle = expander.querySelector('.dcx-main > .dcx-article[data-dcx-category="audience"]');
|
||||
if (continuousArticle) {
|
||||
enhanceAudienceArticle(continuousArticle);
|
||||
return;
|
||||
}
|
||||
|
||||
const audienceItem = expander.querySelector('.dcx-nav-list li[data-category="audience"].is-active');
|
||||
const article = expander.querySelector(".dcx-main > .dcx-article");
|
||||
if (audienceItem && article) enhanceAudienceArticle(article);
|
||||
};
|
||||
|
||||
const scheduleSync = () => {
|
||||
if (syncFrame) return;
|
||||
syncFrame = requestAnimationFrame(syncAudience);
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(scheduleSync);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
window.addEventListener("pageshow", scheduleSync);
|
||||
scheduleSync();
|
||||
})();
|
||||
@@ -0,0 +1,346 @@
|
||||
import { dcxAsset } from './assets.js';
|
||||
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
const DETAIL_META = {
|
||||
product: {
|
||||
"Purpose": ["Why the product exists and what success looks like.", "/assets/product/product-purpose-foil.png"],
|
||||
"Positioning": ["The strategic space this product should occupy.", "/assets/product/product-positioning-foil.png"],
|
||||
"Primary conversion": ["The single action the experience should make inevitable.", "/assets/product/product-primary-conversion-foil.png"],
|
||||
"What must be clear first": ["The facts people need before anything else.", "/assets/product/product-clear-first-foil.png"],
|
||||
"Product principles": ["The rules that keep every product decision aligned.", "/assets/product/product-principles-foil.png"],
|
||||
"Operating context": ["Where and how the product must work.", "/assets/product/product-operating-context-foil.png"],
|
||||
"Surfaces": ["The chosen experiences and the job each one performs.", "/assets/product/product-surfaces-foil.png"],
|
||||
},
|
||||
brand: {
|
||||
"Personality": ["The character every expression should carry.", "/assets/brand/brand-personality-foil.png"],
|
||||
"Voice": ["How the brand sounds—and what it deliberately avoids.", "/assets/brand/brand-voice-foil.png"],
|
||||
"Principles": ["The rules that turn character into consistent choices.", "/assets/brand/brand-principles-foil.png"],
|
||||
"Commitments": ["The promises the brand must keep.", "/assets/brand/brand-commitments-foil.png"],
|
||||
"Named references": ["Useful signals to borrow without becoming an imitation."],
|
||||
"Anti-reference": ["The direction the brand must deliberately avoid."],
|
||||
"Marks": ["The identity assets already available to the system."],
|
||||
"Boards and references": ["Visual evidence that should inform the work."],
|
||||
"Assets provided": ["The source material available for production."],
|
||||
"Brand assets": ["Logos, moodboards, and visual references available to the system."],
|
||||
},
|
||||
color: {
|
||||
"The cue": ["", "/assets/color/color-palette-foil.png"],
|
||||
"Also generated": ["Adjacent directions retained as useful context."],
|
||||
"Palette": ["The core color relationships for the experience.", "/assets/color/color-palette-foil.png"],
|
||||
"Strategy per surface": ["How the palette changes emphasis across contexts.", "/assets/color/color-strategy-per-surface-foil.png"],
|
||||
},
|
||||
typography: {
|
||||
"The pair": ["Two typefaces with distinct, complementary jobs.", "/assets/typography/typography-pair-foil.png"],
|
||||
"Type scale": ["The hierarchy that gives content pace and proportion.", "/assets/typography/typography-type-scale-foil.png"],
|
||||
"In running text": ["How the pair behaves when the content gets real."],
|
||||
},
|
||||
components: {
|
||||
"Buttons": ["Variants, sizes, icons, and loading states."],
|
||||
"Input fields": ["States, sizes, affixes, and multiline input."],
|
||||
"Cards": ["Content, media, horizontal, and action compositions."],
|
||||
},
|
||||
material: {
|
||||
"The page, as chosen": ["The page anatomy each surface committed, and what it represents."],
|
||||
"Motion per surface": ["How movement supports each context without becoming spectacle."],
|
||||
"Motion": ["How movement should support the experience."],
|
||||
"Accessibility": ["The rules that keep the material system readable and operable."],
|
||||
"Layout structure": ["The spatial system that organizes the page.", "/assets/material/material-layout-structure-foil.png"],
|
||||
"Boundaries per surface": ["Where separation is visible—and where space does the work.", "/assets/material/material-boundaries-per-surface-foil.png"],
|
||||
"Corners per surface": ["How edge character changes with context.", "/assets/material/material-corners-per-surface-foil.png"],
|
||||
"Depth per surface": ["How hierarchy is expressed without decorative elevation.", "/assets/material/material-depth-per-surface-foil.png"],
|
||||
"Iconography": ["The chosen icon library, its character, grid, stroke, and license."],
|
||||
},
|
||||
hooks: {
|
||||
"How it runs": [""],
|
||||
"Built-in rules": ["Choose what the detector watches."],
|
||||
"Custom rules": [""],
|
||||
},
|
||||
};
|
||||
|
||||
let syncFrame = 0;
|
||||
|
||||
const slugify = (value) => value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
|
||||
const wrapPrincipleContent = (section, category) => {
|
||||
section.querySelectorAll(":scope .dcx-principles > li").forEach((item) => {
|
||||
let copy = item.querySelector(":scope > .dcx-detail-principle-copy");
|
||||
if (!copy) {
|
||||
copy = document.createElement("div");
|
||||
copy.className = "dcx-detail-principle-copy";
|
||||
while (item.firstChild) copy.appendChild(item.firstChild);
|
||||
item.appendChild(copy);
|
||||
}
|
||||
if (category === "product" && section.dataset.label === "Product principles") {
|
||||
const detail = copy.querySelector(":scope > strong:first-child")?.nextSibling;
|
||||
if (detail?.nodeType === Node.TEXT_NODE) {
|
||||
detail.textContent = detail.textContent.replace(/^\s*[—–-]+\s*/, " ");
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const demoteBodyHeadings = (body) => {
|
||||
body.querySelectorAll("h3").forEach((heading) => {
|
||||
const replacement = document.createElement("h4");
|
||||
[...heading.attributes].forEach((attribute) => {
|
||||
replacement.setAttribute(attribute.name, attribute.value);
|
||||
});
|
||||
while (heading.firstChild) replacement.appendChild(heading.firstChild);
|
||||
heading.replaceWith(replacement);
|
||||
});
|
||||
};
|
||||
|
||||
const placeProductPlatform = (section, category) => {
|
||||
if (category !== "product" || section.dataset.label !== "Purpose") return;
|
||||
|
||||
const callout = section.querySelector(":scope .dcx-purpose .dcx-callout");
|
||||
const name = callout?.querySelector(":scope > .dcx-callout-name");
|
||||
const platform = section.querySelector(":scope .dcx-purpose > .dcx-platform-pill");
|
||||
if (!callout || !name || !platform) return;
|
||||
|
||||
platform.classList.remove("dcx-chip");
|
||||
name.after(platform);
|
||||
};
|
||||
|
||||
const removeProvenanceNotes = (section) => {
|
||||
section.querySelectorAll(":scope .dcx-fan-note").forEach((note) => note.remove());
|
||||
};
|
||||
|
||||
/* A "default" chip marks a surface that kept its preset instead of being
|
||||
configured, and the sentence that said so was a provenance note, which the
|
||||
pass above removes. Restate it in a class this pass keeps, so a value the
|
||||
run never chose is never read as one it did. The "leading" chip shares the
|
||||
class and means something else, hence the text test. */
|
||||
const notePresetDefaults = (body) => {
|
||||
const chips = [...body.querySelectorAll(".dcx-default-mark")];
|
||||
if (!chips.some((chip) => chip.textContent.trim() === "default")) return;
|
||||
if (body.querySelector(":scope > .dcx-default-note")) return;
|
||||
const note = document.createElement("p");
|
||||
note.className = "dcx-default-note";
|
||||
note.textContent = "Entries marked default kept their surface's preset; this run never set them. Run /impeccable design-context edit to answer them.";
|
||||
body.appendChild(note);
|
||||
};
|
||||
|
||||
const compactColorArticle = (article) => {
|
||||
if (article.dataset.dcxColorCompacted === "true") return;
|
||||
|
||||
const sections = new Map(
|
||||
[...article.querySelectorAll(":scope > .dcx-block[data-label]")]
|
||||
.map((section) => [section.dataset.label, section]),
|
||||
);
|
||||
const cueSection = sections.get("The cue");
|
||||
const paletteSection = sections.get("Palette");
|
||||
const strategySection = sections.get("Strategy per surface");
|
||||
const cue = cueSection?.querySelector(":scope > .dcx-cue");
|
||||
const fan = paletteSection?.querySelector(":scope > .dcx-fan");
|
||||
|
||||
cueSection?.querySelector(":scope > .dcx-fan-note")?.remove();
|
||||
strategySection?.querySelector(":scope > .dcx-fan-note")?.remove();
|
||||
sections.get("Roles and values")?.remove();
|
||||
sections.get("Interview direction")?.remove();
|
||||
|
||||
if (cue && fan) {
|
||||
const cueCard = cue.querySelector(":scope > .dcx-cue-card");
|
||||
const panels = [...fan.querySelectorAll(":scope > .dcx-fan-panel")];
|
||||
if (cueCard && panels.length) {
|
||||
cueCard.querySelector(":scope > .dcx-cue-tag")?.remove();
|
||||
cueCard.querySelector(":scope > .dcx-cue-name")?.remove();
|
||||
|
||||
const bands = document.createElement("ul");
|
||||
bands.className = "dcx-cue-bands";
|
||||
bands.setAttribute("aria-label", "Committed palette");
|
||||
|
||||
panels.forEach((panel) => {
|
||||
const role = panel.querySelector(":scope .dcx-fan-name")?.textContent.trim() || "Color";
|
||||
const value = panel.querySelector(":scope .dcx-fan-value")?.textContent.trim() || "";
|
||||
const color = panel.style.getPropertyValue("--panel-swatch");
|
||||
const ink = panel.style.getPropertyValue("--panel-ink");
|
||||
|
||||
const item = document.createElement("li");
|
||||
item.className = "dcx-cue-band-item";
|
||||
|
||||
const swatch = document.createElement("div");
|
||||
swatch.className = "dcx-cue-band";
|
||||
swatch.style.setProperty("--band-color", color);
|
||||
swatch.style.setProperty("--band-ink", ink);
|
||||
|
||||
const hex = document.createElement("code");
|
||||
hex.className = "dcx-cue-band-value";
|
||||
hex.textContent = value;
|
||||
swatch.appendChild(hex);
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.className = "dcx-cue-band-label";
|
||||
label.textContent = role;
|
||||
item.append(swatch, label);
|
||||
bands.appendChild(item);
|
||||
});
|
||||
|
||||
cueCard.querySelectorAll(":scope > .dcx-cue-role").forEach((role) => role.remove());
|
||||
cueCard.appendChild(bands);
|
||||
paletteSection.remove();
|
||||
}
|
||||
}
|
||||
|
||||
const heroLede = article.querySelector(":scope > header > .dcx-lede");
|
||||
if (heroLede) heroLede.textContent = "Committed palette and per-surface strategy.";
|
||||
article.dataset.dcxColorCompacted = "true";
|
||||
};
|
||||
|
||||
const replaceMaterialPagePreviews = (article) => {
|
||||
const section = article.querySelector(':scope > .dcx-block[data-label="The page, as chosen"]');
|
||||
const previews = section?.querySelector(":scope > .dcx-surfaces");
|
||||
if (!previews) return;
|
||||
|
||||
/* One definition per chosen surface, published by the data layer at render
|
||||
time (design-context.js renderDocument), appended under the boards so
|
||||
the drawings and the words both survive. A missing list changes
|
||||
nothing: the boards already stand on their own. */
|
||||
const entries = Array.isArray(window.dcxSurfaceDefs) ? window.dcxSurfaceDefs : [];
|
||||
if (!entries.length) return;
|
||||
|
||||
const definitions = document.createElement("dl");
|
||||
definitions.className = "dcx-defs";
|
||||
entries.forEach(({ label, description }) => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "dcx-def";
|
||||
const term = document.createElement("dt");
|
||||
term.textContent = label;
|
||||
const detail = document.createElement("dd");
|
||||
detail.textContent = description;
|
||||
item.append(term, detail);
|
||||
definitions.appendChild(item);
|
||||
});
|
||||
|
||||
previews.after(definitions);
|
||||
};
|
||||
|
||||
const enhanceSection = (section, category, iconIndex) => {
|
||||
const label = section.dataset.label || "Section";
|
||||
const [ledeText, icon] = DETAIL_META[category]?.[label] || ["The decisions that define this part of the system."];
|
||||
const slug = `${category}-${slugify(label)}`;
|
||||
|
||||
section.querySelector(":scope > .dcx-block-label")?.remove();
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "dcx-detail-section-body";
|
||||
while (section.firstChild) body.appendChild(section.firstChild);
|
||||
|
||||
const heading = document.createElement("div");
|
||||
heading.className = "dcx-detail-section-heading";
|
||||
|
||||
const title = document.createElement("h3");
|
||||
title.className = "dcx-detail-section-title";
|
||||
title.id = `dcx-${slug}-title`;
|
||||
title.textContent = label;
|
||||
|
||||
heading.appendChild(title);
|
||||
if (ledeText) {
|
||||
const lede = document.createElement("p");
|
||||
lede.className = "dcx-detail-section-lede";
|
||||
lede.textContent = ledeText;
|
||||
heading.appendChild(lede);
|
||||
}
|
||||
|
||||
const header = document.createElement("header");
|
||||
header.className = "dcx-detail-section-head";
|
||||
header.appendChild(heading);
|
||||
|
||||
if (icon) {
|
||||
header.classList.add("dcx-detail-section-head--with-icon");
|
||||
const figure = document.createElement("figure");
|
||||
figure.className = "dcx-detail-section-icon";
|
||||
figure.setAttribute("aria-hidden", "true");
|
||||
figure.setAttribute("data-dcx-hide-on-error", "");
|
||||
|
||||
const image = document.createElement("img");
|
||||
image.src = dcxAsset(icon);
|
||||
image.alt = "";
|
||||
image.width = 256;
|
||||
image.height = 256;
|
||||
image.decoding = "async";
|
||||
if (section.closest(".dcx-article")?.dataset.dcxCategory || iconIndex > 0) image.loading = "lazy";
|
||||
figure.appendChild(image);
|
||||
header.appendChild(figure);
|
||||
}
|
||||
|
||||
section.classList.add("dcx-detail-section", `dcx-detail-section--${slugify(label)}`);
|
||||
section.id = `dcx-${slug}`;
|
||||
if (!section.closest(".dcx-article")?.dataset.dcxCategory) {
|
||||
section.setAttribute("role", "region");
|
||||
section.setAttribute("aria-labelledby", title.id);
|
||||
}
|
||||
section.append(header, body);
|
||||
|
||||
demoteBodyHeadings(body);
|
||||
body.querySelector(":scope > .dcx-list")?.classList.add("dcx-detail-list");
|
||||
body.querySelector(":scope > .dcx-defs")?.classList.add("dcx-detail-defs");
|
||||
wrapPrincipleContent(section, category);
|
||||
placeProductPlatform(section, category);
|
||||
removeProvenanceNotes(section);
|
||||
notePresetDefaults(body);
|
||||
};
|
||||
|
||||
const enhanceArticle = (article, category) => {
|
||||
if (article.dataset.dcxDetailEnhanced === category) return;
|
||||
|
||||
if (category === "color") compactColorArticle(article);
|
||||
if (category === "typography") {
|
||||
article.querySelectorAll(":scope .dcx-pair-why").forEach((note) => note.remove());
|
||||
article.querySelector(':scope > .dcx-block[data-label="Interview direction"]')?.remove();
|
||||
}
|
||||
if (category === "iconography") {
|
||||
article.querySelector(':scope > .dcx-block[data-label="The hand"]')?.remove();
|
||||
}
|
||||
if (category === "material") {
|
||||
replaceMaterialPagePreviews(article);
|
||||
article.querySelectorAll(":scope > .dcx-block[data-label] > .dcx-fan-note")
|
||||
.forEach((note) => note.remove());
|
||||
}
|
||||
article.classList.add("dcx-detail-article", `dcx-detail-article--${category}`);
|
||||
article.querySelector(":scope > header")?.classList.add("dcx-detail-hero");
|
||||
|
||||
let iconIndex = 0;
|
||||
article.querySelectorAll(":scope > .dcx-block[data-label]").forEach((section) => {
|
||||
const hasIcon = Boolean(DETAIL_META[category]?.[section.dataset.label]?.[1]);
|
||||
enhanceSection(section, category, iconIndex);
|
||||
if (hasIcon) iconIndex += 1;
|
||||
});
|
||||
|
||||
article.dataset.dcxDetailEnhanced = category;
|
||||
};
|
||||
|
||||
const syncDetails = () => {
|
||||
syncFrame = 0;
|
||||
const expander = document.querySelector(".dcx-expander");
|
||||
const continuousArticles = expander?.querySelectorAll('.dcx-main > .dcx-article[data-dcx-category]');
|
||||
if (continuousArticles?.length) {
|
||||
continuousArticles.forEach((article) => {
|
||||
const category = article.dataset.dcxCategory;
|
||||
if (category !== "audience" && DETAIL_META[category]) enhanceArticle(article, category);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const activeItem = expander?.querySelector(".dcx-nav-list > li.is-active[data-category]");
|
||||
const article = expander?.querySelector(".dcx-main > .dcx-article");
|
||||
const category = activeItem?.dataset.category;
|
||||
if (!article || !category || category === "audience" || !DETAIL_META[category]) return;
|
||||
enhanceArticle(article, category);
|
||||
};
|
||||
|
||||
const scheduleSync = () => {
|
||||
if (syncFrame) return;
|
||||
syncFrame = requestAnimationFrame(syncDetails);
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(scheduleSync);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
window.addEventListener("pageshow", scheduleSync);
|
||||
scheduleSync();
|
||||
})();
|
||||
@@ -0,0 +1,963 @@
|
||||
import RULES from '../../data/hook-rules.json';
|
||||
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
const STORAGE_KEY = "dcx-hooks-preview-v1";
|
||||
const MOBILE_FAMILIES = window.matchMedia("(max-width: 560px)");
|
||||
const REDUCED_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
const FAMILY_META = {
|
||||
fingerprints: {
|
||||
label: "Fingerprints",
|
||||
description: "Recurring signatures found across generated interfaces.",
|
||||
},
|
||||
slop: {
|
||||
label: "UI tells",
|
||||
description: "Common generated-UI habits that make a design feel interchangeable.",
|
||||
},
|
||||
quality: {
|
||||
label: "Quality floor",
|
||||
description: "Measurable defects in legibility, hierarchy, overflow, and system consistency.",
|
||||
},
|
||||
};
|
||||
const DISCIPLINE_ORDER = [
|
||||
"Visual Details",
|
||||
"Typography",
|
||||
"Color & Contrast",
|
||||
"Layout & Space",
|
||||
"Motion",
|
||||
"Imagery",
|
||||
"Copy",
|
||||
"Quality",
|
||||
];
|
||||
const EXCEPTION_KINDS = {
|
||||
value: { label: "Silence one value", hint: "One rule stops flagging one exact value, everywhere." },
|
||||
"rule-in-files": { label: "Silence a rule in files", hint: "One rule goes quiet in matching files and stays live everywhere else." },
|
||||
file: { label: "Skip a file entirely", hint: "Every rule skips matching files. The widest exception; prefer the two above." },
|
||||
};
|
||||
|
||||
/* Only the view preference persists in the browser. The state that matters
|
||||
lives in the project's .impeccable/config.json and arrives from the doc
|
||||
session, so the page shows what the hook will actually do, not a preview. */
|
||||
const loadView = () => {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null");
|
||||
return { activeFamily: parsed && FAMILY_META[parsed.activeFamily] ? parsed.activeFamily : "fingerprints" };
|
||||
} catch {
|
||||
return { activeFamily: "fingerprints" };
|
||||
}
|
||||
};
|
||||
|
||||
const view = loadView();
|
||||
|
||||
const persistView = () => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ activeFamily: view.activeFamily }));
|
||||
} catch {
|
||||
// The file:// preview may deny storage; the in-memory controls still work.
|
||||
}
|
||||
};
|
||||
|
||||
/* live is the last state the project confirmed; draft is what the controls
|
||||
show. Apply sends the whole draft and the server's echo becomes the new
|
||||
live, so the page and .impeccable/config.json can only disagree while the
|
||||
Apply bar is visible and says so. */
|
||||
let live = null;
|
||||
let draft = null;
|
||||
let liveError = "";
|
||||
let applying = false;
|
||||
let appliedFlash = false;
|
||||
let appliedFlashTimer = 0;
|
||||
let fetchStarted = false;
|
||||
|
||||
const docSession = () => window.dcxDocSession || null;
|
||||
const hooksUrl = () => {
|
||||
const session = docSession();
|
||||
if (!session?.base || !session?.token) return "";
|
||||
return `${session.base}/doc/hooks?token=${encodeURIComponent(session.token)}`;
|
||||
};
|
||||
|
||||
const cloneState = (state) => JSON.parse(JSON.stringify(state));
|
||||
|
||||
const entryKey = (entry) => {
|
||||
const files = Array.isArray(entry.files) && entry.files.length > 0 ? [...entry.files].sort().join("\u001f") : "";
|
||||
return `${entry.rule}\u0000${entry.value}\u0000${files}`;
|
||||
};
|
||||
|
||||
const changeCount = () => {
|
||||
if (!live || !draft) return 0;
|
||||
let count = draft.enabled !== live.enabled ? 1 : 0;
|
||||
const liveRules = new Set(live.ignoreRules);
|
||||
const draftRules = new Set(draft.ignoreRules);
|
||||
for (const id of draftRules) if (!liveRules.has(id)) count += 1;
|
||||
for (const id of liveRules) if (!draftRules.has(id)) count += 1;
|
||||
const liveFiles = new Set(live.ignoreFiles);
|
||||
const draftFiles = new Set(draft.ignoreFiles);
|
||||
for (const glob of draftFiles) if (!liveFiles.has(glob)) count += 1;
|
||||
for (const glob of liveFiles) if (!draftFiles.has(glob)) count += 1;
|
||||
const liveValues = new Map(live.ignoreValues.map((entry) => [entryKey(entry), entry]));
|
||||
const draftValues = new Map(draft.ignoreValues.map((entry) => [entryKey(entry), entry]));
|
||||
for (const key of draftValues.keys()) if (!liveValues.has(key)) count += 1;
|
||||
for (const key of liveValues.keys()) if (!draftValues.has(key)) count += 1;
|
||||
return count;
|
||||
};
|
||||
|
||||
/* Three-way rebase for an apply that lost the race: keep every edit the
|
||||
visitor made (draft against the state the page read) and land it on what
|
||||
the project now holds, so nothing another writer added is dropped. */
|
||||
const rebaseDraft = (oldLive, oldDraft, newLive) => {
|
||||
const next = cloneState(newLive);
|
||||
if (oldDraft.enabled !== oldLive.enabled) next.enabled = oldDraft.enabled;
|
||||
for (const key of ["ignoreRules", "ignoreFiles"]) {
|
||||
const removed = new Set(oldLive[key].filter((item) => !oldDraft[key].includes(item)));
|
||||
const added = oldDraft[key].filter((item) => !oldLive[key].includes(item));
|
||||
next[key] = next[key].filter((item) => !removed.has(item));
|
||||
for (const item of added) if (!next[key].includes(item)) next[key].push(item);
|
||||
}
|
||||
const oldKeys = new Set(oldLive.ignoreValues.map(entryKey));
|
||||
const draftKeys = new Set(oldDraft.ignoreValues.map(entryKey));
|
||||
const removedKeys = new Set([...oldKeys].filter((key) => !draftKeys.has(key)));
|
||||
next.ignoreValues = next.ignoreValues.filter((entry) => !removedKeys.has(entryKey(entry)));
|
||||
const presentKeys = new Set(next.ignoreValues.map(entryKey));
|
||||
for (const entry of oldDraft.ignoreValues) {
|
||||
const key = entryKey(entry);
|
||||
if (!oldKeys.has(key) && !presentKeys.has(key)) {
|
||||
next.ignoreValues.push(cloneState(entry));
|
||||
presentKeys.add(key);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
const escapeHtml = (value) => String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """);
|
||||
|
||||
const slugify = (value) => String(value)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "") || "custom-rule";
|
||||
|
||||
const compactDescription = (value) => {
|
||||
const text = String(value).trim();
|
||||
const firstSentence = text.match(/^.*?[.!?](?:\s|$)/)?.[0]?.trim();
|
||||
return firstSentence || text;
|
||||
};
|
||||
|
||||
const ruleName = (id) => RULES.find((rule) => rule.id === id)?.name || id;
|
||||
|
||||
const templateMarkup = () => `
|
||||
<article class="dcx-article">
|
||||
<header>
|
||||
<h2 class="dcx-title">Hooks</h2>
|
||||
<p class="dcx-lede">Checks that catch design regressions while you work.</p>
|
||||
</header>
|
||||
<section class="dcx-block" data-label="How it runs">
|
||||
<span class="dcx-block-label">How it runs</span>
|
||||
<div class="dcx-hooks-intro">
|
||||
<p class="dcx-hooks-definition">Hooks watch interface changes and surface problems before they spread.</p>
|
||||
<div class="dcx-hooks-status" data-hooks-status>
|
||||
<div class="dcx-hooks-status-copy">
|
||||
<strong data-hooks-master-copy>Enable hooks</strong>
|
||||
<p data-hooks-master-detail>Reading this project’s settings…</p>
|
||||
</div>
|
||||
<div class="dcx-hooks-status-control">
|
||||
<span class="dcx-hooks-status-state" data-hooks-master-state>On</span>
|
||||
<label class="dcx-hooks-switch dcx-hooks-switch--master">
|
||||
<input type="checkbox" role="switch" data-hooks-master aria-label="Enable design hooks">
|
||||
<span aria-hidden="true"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="dcx-hooks-flow">
|
||||
<div>
|
||||
<dt>On each edit</dt>
|
||||
<dd>Checks the changed UI.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>At session end</dt>
|
||||
<dd>Runs a full pass on touched UI files.</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
<section class="dcx-block" data-label="Built-in rules">
|
||||
<span class="dcx-block-label">Built-in rules</span>
|
||||
<div class="dcx-hooks-browser" data-hooks-browser>
|
||||
<div class="dcx-hooks-families" role="tablist" aria-label="Rule families" data-hooks-families></div>
|
||||
<div class="dcx-hooks-rule-panel" id="dcx-hooks-rule-panel" role="tabpanel">
|
||||
<div class="dcx-hooks-toolbar">
|
||||
<label class="dcx-hooks-search">
|
||||
<input type="search" autocomplete="off" aria-label="Search rules" placeholder="Search rules" data-hooks-search>
|
||||
</label>
|
||||
<p class="dcx-hooks-summary" data-hooks-summary aria-live="polite"></p>
|
||||
</div>
|
||||
<div class="dcx-hooks-rule-groups" data-hooks-rule-groups></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="dcx-block" data-label="Exceptions">
|
||||
<span class="dcx-block-label">Exceptions</span>
|
||||
<div class="dcx-hooks-custom" data-hooks-custom>
|
||||
<div class="dcx-hooks-custom-toolbar">
|
||||
<p data-hooks-custom-count>No exceptions.</p>
|
||||
<button class="dcx-hooks-button" type="button" data-hooks-add aria-expanded="false" aria-controls="dcx-hooks-custom-form">Add exception</button>
|
||||
</div>
|
||||
<form class="dcx-hooks-custom-form" id="dcx-hooks-custom-form" data-hooks-form hidden>
|
||||
<label>
|
||||
<span>Kind</span>
|
||||
<select name="kind" data-hooks-kind>
|
||||
${Object.entries(EXCEPTION_KINDS).map(([id, meta]) => `<option value="${id}">${escapeHtml(meta.label)}</option>`).join("")}
|
||||
</select>
|
||||
</label>
|
||||
<p class="dcx-hooks-kind-hint" data-hooks-kind-hint>${escapeHtml(EXCEPTION_KINDS.value.hint)}</p>
|
||||
<label data-hooks-field="rule">
|
||||
<span>Rule</span>
|
||||
<select name="rule">
|
||||
${[...RULES].sort((a, b) => a.name.localeCompare(b.name)).map((rule) => `<option value="${escapeHtml(rule.id)}">${escapeHtml(rule.name)}</option>`).join("")}
|
||||
</select>
|
||||
</label>
|
||||
<label data-hooks-field="value">
|
||||
<span>Value</span>
|
||||
<input name="value" maxlength="200" placeholder="e.g. Inter, or #7BA98F">
|
||||
</label>
|
||||
<label data-hooks-field="files" hidden>
|
||||
<span>Files</span>
|
||||
<input name="files" maxlength="400" placeholder="Globs, comma separated: src/legacy/**, docs/demo.html">
|
||||
</label>
|
||||
<label data-hooks-field="reason">
|
||||
<span>Reason</span>
|
||||
<input name="reason" maxlength="200" placeholder="Optional: who decided, and the evidence">
|
||||
</label>
|
||||
<div class="dcx-hooks-form-actions">
|
||||
<button class="dcx-hooks-button dcx-hooks-button--quiet" type="button" data-hooks-cancel>Cancel</button>
|
||||
<button class="dcx-hooks-button" type="submit">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="dcx-hooks-custom-list" data-hooks-custom-list></div>
|
||||
<p class="dcx-hooks-storage-note" data-hooks-live-note>Exceptions apply to the design hook and to npx impeccable detect in this project.</p>
|
||||
</div>
|
||||
</section>
|
||||
<div class="dcx-hooks-applybar" data-hooks-applybar hidden>
|
||||
<p class="dcx-hooks-applybar-copy" data-hooks-applybar-copy aria-live="polite"></p>
|
||||
<div class="dcx-hooks-applybar-actions">
|
||||
<button class="dcx-hooks-button dcx-hooks-button--quiet" type="button" data-hooks-discard>Discard</button>
|
||||
<button class="dcx-hooks-button" type="button" data-hooks-apply>Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
|
||||
const renameInterfaceToHooks = () => {
|
||||
const tile = document.querySelector('.dcx-tile[data-category="interface"]');
|
||||
if (tile) {
|
||||
tile.dataset.category = "hooks";
|
||||
tile.dataset.name = "Hooks";
|
||||
tile.setAttribute("aria-label", "Open Hooks");
|
||||
const title = tile.querySelector(".dcx-tile-title");
|
||||
if (title) title.textContent = "Hooks";
|
||||
}
|
||||
|
||||
const shellTemplate = document.querySelector("#dcx-shell-template");
|
||||
const navItem = shellTemplate?.content.querySelector('li[data-category="interface"]');
|
||||
const navLink = navItem?.querySelector(".dcx-nav-link");
|
||||
if (navItem && navLink) {
|
||||
navItem.dataset.category = "hooks";
|
||||
navLink.href = "#hooks";
|
||||
navLink.dataset.dcxNav = "hooks";
|
||||
navLink.textContent = "Hooks";
|
||||
}
|
||||
};
|
||||
|
||||
const installTemplate = () => {
|
||||
if (document.querySelector("#dcx-detail-hooks")) return;
|
||||
const template = document.createElement("template");
|
||||
template.id = "dcx-detail-hooks";
|
||||
template.innerHTML = templateMarkup();
|
||||
document.querySelector("#dcx-detail-interface")?.after(template);
|
||||
};
|
||||
|
||||
const familyRules = (family) => RULES.filter((rule) => rule.group === family);
|
||||
const isEnabled = (id) => !(draft ? draft.ignoreRules.includes(id) : false);
|
||||
const interactive = () => Boolean(live && draft && !applying);
|
||||
|
||||
const revealSelectedFamily = (target) => {
|
||||
if (!MOBILE_FAMILIES.matches) return;
|
||||
const selected = target.querySelector('[data-hooks-family][aria-selected="true"]');
|
||||
if (!selected) return;
|
||||
|
||||
const viewport = target.getBoundingClientRect();
|
||||
const item = selected.getBoundingClientRect();
|
||||
let delta = 0;
|
||||
if (item.left < viewport.left + 3) delta = item.left - viewport.left - 3;
|
||||
else if (item.right > viewport.right - 3) delta = item.right - viewport.right + 3;
|
||||
if (Math.abs(delta) < 1) return;
|
||||
target.scrollTo({
|
||||
left: Math.max(0, target.scrollLeft + delta),
|
||||
behavior: REDUCED_MOTION.matches ? "auto" : "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
const disciplineAnimations = new WeakMap();
|
||||
const customFormAnimations = new WeakMap();
|
||||
let syncFrame = 0;
|
||||
|
||||
const setDisciplineOpen = (details, expanded) => {
|
||||
const panel = details.querySelector(":scope > .dcx-hooks-disclosure");
|
||||
const inner = panel?.querySelector(":scope > .dcx-hooks-disclosure-inner");
|
||||
if (!panel || !inner) {
|
||||
details.open = expanded;
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = disciplineAnimations.get(details);
|
||||
const currentHeight = panel.getBoundingClientRect().height;
|
||||
const currentOpacity = Number.parseFloat(getComputedStyle(panel).opacity) || 0;
|
||||
previous?.cancel();
|
||||
|
||||
if (REDUCED_MOTION.matches) {
|
||||
disciplineAnimations.delete(details);
|
||||
details.classList.remove("is-closing");
|
||||
details.open = expanded;
|
||||
panel.style.removeProperty("height");
|
||||
panel.style.removeProperty("opacity");
|
||||
return;
|
||||
}
|
||||
|
||||
details.open = true;
|
||||
details.classList.toggle("is-closing", !expanded);
|
||||
const fromHeight = previous ? currentHeight : expanded ? 0 : currentHeight;
|
||||
const fromOpacity = previous ? currentOpacity : expanded ? 0 : 1;
|
||||
const toHeight = expanded ? inner.scrollHeight : 0;
|
||||
const toOpacity = expanded ? 1 : 0;
|
||||
|
||||
const animation = panel.animate([
|
||||
{ height: `${fromHeight}px`, opacity: fromOpacity },
|
||||
{ height: `${toHeight}px`, opacity: toOpacity },
|
||||
], {
|
||||
duration: 360,
|
||||
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
|
||||
fill: "both",
|
||||
});
|
||||
disciplineAnimations.set(details, animation);
|
||||
|
||||
animation.finished.then(() => {
|
||||
if (disciplineAnimations.get(details) !== animation) return;
|
||||
disciplineAnimations.delete(details);
|
||||
details.classList.remove("is-closing");
|
||||
details.open = expanded;
|
||||
animation.cancel();
|
||||
panel.style.removeProperty("height");
|
||||
panel.style.removeProperty("opacity");
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const setCustomFormOpen = (form, expanded) => {
|
||||
const previous = customFormAnimations.get(form);
|
||||
const currentHeight = form.hidden ? 0 : form.getBoundingClientRect().height;
|
||||
const currentOpacity = form.hidden ? 0 : Number.parseFloat(getComputedStyle(form).opacity) || 1;
|
||||
previous?.cancel();
|
||||
|
||||
if (REDUCED_MOTION.matches) {
|
||||
customFormAnimations.delete(form);
|
||||
form.hidden = !expanded;
|
||||
return;
|
||||
}
|
||||
|
||||
if (expanded) form.hidden = false;
|
||||
const toHeight = expanded ? form.scrollHeight : 0;
|
||||
form.style.overflow = "clip";
|
||||
const animation = form.animate([
|
||||
{ height: `${currentHeight}px`, opacity: currentOpacity },
|
||||
{ height: `${toHeight}px`, opacity: expanded ? 1 : 0 },
|
||||
], {
|
||||
duration: 320,
|
||||
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
|
||||
fill: "both",
|
||||
});
|
||||
customFormAnimations.set(form, animation);
|
||||
|
||||
animation.finished.then(() => {
|
||||
if (customFormAnimations.get(form) !== animation) return;
|
||||
customFormAnimations.delete(form);
|
||||
form.hidden = !expanded;
|
||||
animation.cancel();
|
||||
form.style.removeProperty("height");
|
||||
form.style.removeProperty("opacity");
|
||||
form.style.removeProperty("overflow");
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const toggleDiscipline = (article, summary) => {
|
||||
const disclosure = summary.parentElement;
|
||||
const expanded = !disclosure.open || disclosure.classList.contains("is-closing");
|
||||
const query = article.querySelector("[data-hooks-search]")?.value.trim();
|
||||
if (expanded && !query) {
|
||||
disclosure.parentElement?.querySelectorAll(":scope > .dcx-hooks-discipline[open]").forEach((sibling) => {
|
||||
if (sibling !== disclosure) setDisciplineOpen(sibling, false);
|
||||
});
|
||||
}
|
||||
setDisciplineOpen(disclosure, expanded);
|
||||
};
|
||||
|
||||
const renderFamilies = (article) => {
|
||||
const target = article.querySelector("[data-hooks-families]");
|
||||
if (!target) return;
|
||||
target.innerHTML = Object.entries(FAMILY_META).map(([id, meta]) => {
|
||||
const rules = familyRules(id);
|
||||
const enabled = rules.filter((rule) => isEnabled(rule.id)).length;
|
||||
const selected = view.activeFamily === id;
|
||||
return `
|
||||
<button
|
||||
id="dcx-hooks-family-${id}"
|
||||
class="dcx-hooks-family${selected ? " is-active" : ""}"
|
||||
type="button"
|
||||
role="tab"
|
||||
tabindex="${selected ? "0" : "-1"}"
|
||||
aria-selected="${selected}"
|
||||
aria-label="${escapeHtml(meta.label)}, ${enabled} of ${rules.length} selected"
|
||||
aria-controls="dcx-hooks-rule-panel"
|
||||
data-hooks-family="${id}"
|
||||
>
|
||||
<span class="dcx-hooks-family-name">${escapeHtml(meta.label)}</span>
|
||||
<span class="dcx-hooks-family-count">${enabled}/${rules.length}</span>
|
||||
</button>
|
||||
`;
|
||||
}).join("");
|
||||
const panel = article.querySelector("#dcx-hooks-rule-panel");
|
||||
panel?.setAttribute("aria-labelledby", `dcx-hooks-family-${view.activeFamily}`);
|
||||
requestAnimationFrame(() => revealSelectedFamily(target));
|
||||
};
|
||||
|
||||
const renderRules = (article) => {
|
||||
const target = article.querySelector("[data-hooks-rule-groups]");
|
||||
const summary = article.querySelector("[data-hooks-summary]");
|
||||
const search = article.querySelector("[data-hooks-search]");
|
||||
if (!target || !summary) return;
|
||||
|
||||
const query = (search?.value || "").trim().toLowerCase();
|
||||
const rules = familyRules(view.activeFamily);
|
||||
const filtered = rules.filter((rule) => !query
|
||||
|| `${rule.id} ${rule.name} ${rule.description} ${rule.discipline}`.toLowerCase().includes(query));
|
||||
const enabled = rules.filter((rule) => isEnabled(rule.id)).length;
|
||||
summary.textContent = query
|
||||
? `${filtered.length} matching ${filtered.length === 1 ? "rule" : "rules"}`
|
||||
: `${enabled} of ${rules.length} selected`;
|
||||
|
||||
const groups = new Map();
|
||||
filtered.forEach((rule) => {
|
||||
if (!groups.has(rule.discipline)) groups.set(rule.discipline, []);
|
||||
groups.get(rule.discipline).push(rule);
|
||||
});
|
||||
|
||||
const orderedGroups = [...groups.entries()].sort(([a], [b]) => {
|
||||
const ai = DISCIPLINE_ORDER.indexOf(a);
|
||||
const bi = DISCIPLINE_ORDER.indexOf(b);
|
||||
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi) || a.localeCompare(b);
|
||||
});
|
||||
|
||||
if (!orderedGroups.length) {
|
||||
target.innerHTML = '<p class="dcx-hooks-empty">No rules match this search.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const disabledUi = interactive() ? "" : "disabled";
|
||||
target.innerHTML = orderedGroups.map(([discipline, entries], index) => {
|
||||
const disclosureId = `dcx-hooks-${view.activeFamily}-${slugify(discipline)}`;
|
||||
const summaryId = `${disclosureId}-summary`;
|
||||
return `
|
||||
<details class="dcx-hooks-discipline" ${query || index === 0 ? "open" : ""}>
|
||||
<summary id="${summaryId}">
|
||||
<span>${escapeHtml(discipline)}</span>
|
||||
<span>${entries.length}</span>
|
||||
</summary>
|
||||
<div class="dcx-hooks-disclosure" id="${disclosureId}" role="region" aria-labelledby="${summaryId}">
|
||||
<div class="dcx-hooks-disclosure-inner">
|
||||
<ul class="dcx-hooks-rules">
|
||||
${entries.map((rule) => `
|
||||
<li class="dcx-hooks-rule" data-rule-id="${escapeHtml(rule.id)}">
|
||||
<div class="dcx-hooks-rule-copy">
|
||||
<strong>${escapeHtml(rule.name)}</strong>
|
||||
<p>${escapeHtml(compactDescription(rule.description))}</p>
|
||||
</div>
|
||||
<label class="dcx-hooks-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
data-hooks-rule="${escapeHtml(rule.id)}"
|
||||
aria-label="Enable ${escapeHtml(rule.name)}"
|
||||
${isEnabled(rule.id) ? "checked" : ""}
|
||||
${disabledUi}
|
||||
>
|
||||
<span aria-hidden="true"></span>
|
||||
</label>
|
||||
</li>
|
||||
`).join("")}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
`;
|
||||
}).join("");
|
||||
};
|
||||
|
||||
/* One row per exception the project holds, whatever wrote it: entries added
|
||||
here, by an agent's triage, or by npx impeccable ignores all render the
|
||||
same, and Remove queues a real removal for Apply. */
|
||||
const renderExceptions = (article) => {
|
||||
const target = article.querySelector("[data-hooks-custom-list]");
|
||||
const count = article.querySelector("[data-hooks-custom-count]");
|
||||
const note = article.querySelector("[data-hooks-live-note]");
|
||||
const add = article.querySelector("[data-hooks-add]");
|
||||
if (!target) return;
|
||||
|
||||
const values = draft ? draft.ignoreValues : [];
|
||||
const files = draft ? draft.ignoreFiles : [];
|
||||
const total = values.length + files.length;
|
||||
if (count) {
|
||||
count.textContent = total
|
||||
? `${total} ${total === 1 ? "exception" : "exceptions"}`
|
||||
: "No exceptions.";
|
||||
}
|
||||
if (add) add.disabled = !interactive();
|
||||
if (note) {
|
||||
note.textContent = interactive() || applying
|
||||
? "Exceptions apply to the design hook and to npx impeccable detect in this project."
|
||||
: "The editing session has ended. /impeccable design-context reopens it.";
|
||||
}
|
||||
|
||||
target.innerHTML = "";
|
||||
values.forEach((entry, index) => {
|
||||
const row = document.createElement("article");
|
||||
row.className = "dcx-hooks-custom-rule";
|
||||
|
||||
const copy = document.createElement("div");
|
||||
copy.className = "dcx-hooks-rule-copy";
|
||||
const kind = document.createElement("span");
|
||||
kind.className = "dcx-hooks-custom-discipline";
|
||||
kind.textContent = entry.value === "*" ? "Rule, in files" : "Value";
|
||||
const name = document.createElement("strong");
|
||||
name.textContent = entry.value === "*"
|
||||
? ruleName(entry.rule)
|
||||
: `${ruleName(entry.rule)}: ${entry.value}`;
|
||||
const detail = document.createElement("p");
|
||||
detail.textContent = entry.files?.length
|
||||
? `In ${entry.files.join(", ")}`
|
||||
: "Everywhere in this project.";
|
||||
copy.append(kind, name, detail);
|
||||
if (entry.reason) {
|
||||
const reason = document.createElement("p");
|
||||
reason.className = "dcx-hooks-custom-reason";
|
||||
reason.textContent = entry.reason;
|
||||
copy.append(reason);
|
||||
}
|
||||
|
||||
const controls = document.createElement("div");
|
||||
controls.className = "dcx-hooks-custom-controls";
|
||||
const remove = document.createElement("button");
|
||||
remove.className = "dcx-hooks-remove";
|
||||
remove.type = "button";
|
||||
remove.dataset.hooksRemoveValue = String(index);
|
||||
remove.setAttribute("aria-label", `Remove exception for ${ruleName(entry.rule)}`);
|
||||
remove.textContent = "Remove";
|
||||
remove.disabled = !interactive();
|
||||
controls.append(remove);
|
||||
row.append(copy, controls);
|
||||
target.appendChild(row);
|
||||
});
|
||||
|
||||
files.forEach((glob, index) => {
|
||||
const row = document.createElement("article");
|
||||
row.className = "dcx-hooks-custom-rule";
|
||||
|
||||
const copy = document.createElement("div");
|
||||
copy.className = "dcx-hooks-rule-copy";
|
||||
const kind = document.createElement("span");
|
||||
kind.className = "dcx-hooks-custom-discipline";
|
||||
kind.textContent = "Skipped files";
|
||||
const name = document.createElement("strong");
|
||||
name.textContent = glob;
|
||||
const detail = document.createElement("p");
|
||||
detail.textContent = "Every rule skips matching files.";
|
||||
copy.append(kind, name, detail);
|
||||
|
||||
const controls = document.createElement("div");
|
||||
controls.className = "dcx-hooks-custom-controls";
|
||||
const remove = document.createElement("button");
|
||||
remove.className = "dcx-hooks-remove";
|
||||
remove.type = "button";
|
||||
remove.dataset.hooksRemoveFile = String(index);
|
||||
remove.setAttribute("aria-label", `Stop skipping ${glob}`);
|
||||
remove.textContent = "Remove";
|
||||
remove.disabled = !interactive();
|
||||
controls.append(remove);
|
||||
row.append(copy, controls);
|
||||
target.appendChild(row);
|
||||
});
|
||||
};
|
||||
|
||||
const syncMaster = (article) => {
|
||||
const input = article.querySelector("[data-hooks-master]");
|
||||
const status = article.querySelector("[data-hooks-status]");
|
||||
const copy = article.querySelector("[data-hooks-master-copy]");
|
||||
const detail = article.querySelector("[data-hooks-master-detail]");
|
||||
const stateText = article.querySelector("[data-hooks-master-state]");
|
||||
if (!input || !status || !copy || !detail || !stateText) return;
|
||||
|
||||
const enabled = draft ? draft.enabled : true;
|
||||
input.checked = enabled;
|
||||
input.disabled = !interactive();
|
||||
status.classList.toggle("is-paused", !enabled);
|
||||
copy.textContent = "Enable hooks";
|
||||
if (draft) {
|
||||
detail.textContent = "Runs in this project; changes land in .impeccable/config.json when you press Apply.";
|
||||
} else if (liveError) {
|
||||
detail.textContent = liveError;
|
||||
} else if (!docSession()) {
|
||||
detail.textContent = "The editing session has ended. /impeccable design-context reopens it.";
|
||||
} else {
|
||||
detail.textContent = "Reading this project’s settings…";
|
||||
}
|
||||
stateText.textContent = enabled ? "On" : "Off";
|
||||
};
|
||||
|
||||
const syncApplyBar = (article) => {
|
||||
const bar = article.querySelector("[data-hooks-applybar]");
|
||||
const copy = article.querySelector("[data-hooks-applybar-copy]");
|
||||
const apply = article.querySelector("[data-hooks-apply]");
|
||||
const discard = article.querySelector("[data-hooks-discard]");
|
||||
if (!bar || !copy || !apply || !discard) return;
|
||||
|
||||
const changes = changeCount();
|
||||
const show = Boolean(live && draft) && (changes > 0 || applying || appliedFlash);
|
||||
bar.hidden = !show;
|
||||
if (!show) return;
|
||||
|
||||
apply.disabled = applying || changes === 0;
|
||||
discard.disabled = applying || changes === 0;
|
||||
if (applying) {
|
||||
copy.textContent = "Applying…";
|
||||
} else if (changes > 0) {
|
||||
copy.textContent = `${changes} ${changes === 1 ? "change" : "changes"} not applied yet.`;
|
||||
} else {
|
||||
copy.textContent = "Applied to .impeccable/config.json.";
|
||||
}
|
||||
};
|
||||
|
||||
const renderArticle = (article) => {
|
||||
syncMaster(article);
|
||||
renderFamilies(article);
|
||||
renderRules(article);
|
||||
renderExceptions(article);
|
||||
syncApplyBar(article);
|
||||
};
|
||||
|
||||
const hooksArticles = () => [...document.querySelectorAll('.dcx-article[data-dcx-category="hooks"]')];
|
||||
const renderAll = () => hooksArticles().forEach(renderArticle);
|
||||
|
||||
const fetchLiveState = async () => {
|
||||
const url = hooksUrl();
|
||||
if (!url || fetchStarted) return;
|
||||
fetchStarted = true;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const body = await response.json().catch(() => null);
|
||||
if (!response.ok || !body?.ok || !body.state) {
|
||||
throw new Error(body?.error || `The doc session answered ${response.status}.`);
|
||||
}
|
||||
live = body.state;
|
||||
draft = cloneState(live);
|
||||
liveError = "";
|
||||
} catch {
|
||||
liveError = "Could not read this project’s hook settings; the controls stay read-only.";
|
||||
/* A later remount retries: the session may only now be announced. */
|
||||
fetchStarted = false;
|
||||
}
|
||||
renderAll();
|
||||
};
|
||||
|
||||
const applyDraft = async (article) => {
|
||||
const url = hooksUrl();
|
||||
const session = docSession();
|
||||
if (!url || !session || !draft || applying) return;
|
||||
applying = true;
|
||||
appliedFlash = false;
|
||||
if (appliedFlashTimer) {
|
||||
window.clearTimeout(appliedFlashTimer);
|
||||
appliedFlashTimer = 0;
|
||||
}
|
||||
renderAll();
|
||||
try {
|
||||
const response = await fetch(`${session.base}/doc/hooks`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token: session.token, state: { ...draft, baseline: live } }),
|
||||
});
|
||||
const body = await response.json().catch(() => null);
|
||||
if (!response.ok || !body?.ok || !body.state) {
|
||||
throw new Error(body?.error || `The doc session answered ${response.status}.`);
|
||||
}
|
||||
live = body.state;
|
||||
draft = cloneState(live);
|
||||
appliedFlash = true;
|
||||
appliedFlashTimer = window.setTimeout(() => {
|
||||
appliedFlashTimer = 0;
|
||||
appliedFlash = false;
|
||||
renderAll();
|
||||
}, 2600);
|
||||
} catch (error) {
|
||||
const message = String(error.message || error);
|
||||
applying = false;
|
||||
if (message.includes("hook config changed on disk") && live && draft) {
|
||||
const oldLive = live;
|
||||
const oldDraft = draft;
|
||||
try {
|
||||
const refreshed = await fetch(url);
|
||||
const refreshedBody = await refreshed.json().catch(() => null);
|
||||
if (refreshed.ok && refreshedBody?.ok && refreshedBody.state) {
|
||||
live = refreshedBody.state;
|
||||
draft = rebaseDraft(oldLive, oldDraft, live);
|
||||
renderAll();
|
||||
/* Written after renderAll so the bar's own copy cannot eat it. */
|
||||
const copy = article.querySelector("[data-hooks-applybar-copy]");
|
||||
if (copy) copy.textContent = "Another run changed this project's hook settings while you edited. Your changes were re-applied on top; review and press Apply again.";
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* The session dropped mid-conflict; fall through to the plain error. */
|
||||
}
|
||||
}
|
||||
syncApplyBar(article);
|
||||
/* After the bar re-renders its change count, the reason overwrites it;
|
||||
written after the sync so the sync cannot eat it. */
|
||||
const copy = article.querySelector("[data-hooks-applybar-copy]");
|
||||
if (copy) copy.textContent = `Not applied: ${message}`;
|
||||
return;
|
||||
}
|
||||
applying = false;
|
||||
renderAll();
|
||||
};
|
||||
|
||||
const initializeMountedArticles = () => {
|
||||
syncFrame = 0;
|
||||
hooksArticles().forEach((article) => {
|
||||
if (article.dataset.dcxHooksReady === "true") return;
|
||||
article.dataset.dcxHooksReady = "true";
|
||||
renderArticle(article);
|
||||
});
|
||||
if (live === null) fetchLiveState();
|
||||
};
|
||||
|
||||
const scheduleSync = () => {
|
||||
if (syncFrame) return;
|
||||
syncFrame = requestAnimationFrame(initializeMountedArticles);
|
||||
};
|
||||
|
||||
const syncKindFields = (form) => {
|
||||
const kind = form.querySelector("[data-hooks-kind]")?.value || "value";
|
||||
const hint = form.querySelector("[data-hooks-kind-hint]");
|
||||
if (hint) hint.textContent = EXCEPTION_KINDS[kind]?.hint || "";
|
||||
form.querySelector('[data-hooks-field="rule"]')?.toggleAttribute("hidden", kind === "file");
|
||||
form.querySelector('[data-hooks-field="value"]')?.toggleAttribute("hidden", kind !== "value");
|
||||
form.querySelector('[data-hooks-field="files"]')?.toggleAttribute("hidden", kind === "value");
|
||||
form.querySelector('[data-hooks-field="reason"]')?.toggleAttribute("hidden", kind === "file");
|
||||
};
|
||||
|
||||
const parseGlobList = (value) => [...new Set(String(value)
|
||||
.split(/[\n,]/)
|
||||
.map((glob) => glob.trim())
|
||||
.filter(Boolean))];
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
const article = event.target.closest('.dcx-article[data-dcx-category="hooks"]');
|
||||
if (!article) return;
|
||||
|
||||
const summary = event.target.closest(".dcx-hooks-discipline > summary");
|
||||
if (summary) {
|
||||
event.preventDefault();
|
||||
toggleDiscipline(article, summary);
|
||||
return;
|
||||
}
|
||||
|
||||
const family = event.target.closest("[data-hooks-family]");
|
||||
if (family) {
|
||||
const restoreFocus = family === document.activeElement;
|
||||
view.activeFamily = family.dataset.hooksFamily;
|
||||
persistView();
|
||||
renderFamilies(article);
|
||||
renderRules(article);
|
||||
if (restoreFocus) {
|
||||
article.querySelector(`[data-hooks-family="${view.activeFamily}"]`)?.focus({ preventScroll: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const add = event.target.closest("[data-hooks-add]");
|
||||
if (add && interactive()) {
|
||||
const form = article.querySelector("[data-hooks-form]");
|
||||
if (!form) return;
|
||||
syncKindFields(form);
|
||||
setCustomFormOpen(form, true);
|
||||
add.setAttribute("aria-expanded", "true");
|
||||
form.querySelector("[data-hooks-kind]")?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const cancel = event.target.closest("[data-hooks-cancel]");
|
||||
if (cancel) {
|
||||
const form = article.querySelector("[data-hooks-form]");
|
||||
form?.reset();
|
||||
if (form) setCustomFormOpen(form, false);
|
||||
const addButton = article.querySelector("[data-hooks-add]");
|
||||
addButton?.setAttribute("aria-expanded", "false");
|
||||
addButton?.focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const removeValue = event.target.closest("[data-hooks-remove-value]");
|
||||
if (removeValue && interactive()) {
|
||||
draft.ignoreValues.splice(Number(removeValue.dataset.hooksRemoveValue), 1);
|
||||
renderAll();
|
||||
(article.querySelector(".dcx-hooks-remove") || article.querySelector("[data-hooks-add]"))
|
||||
?.focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const removeFile = event.target.closest("[data-hooks-remove-file]");
|
||||
if (removeFile && interactive()) {
|
||||
draft.ignoreFiles.splice(Number(removeFile.dataset.hooksRemoveFile), 1);
|
||||
renderAll();
|
||||
(article.querySelector(".dcx-hooks-remove") || article.querySelector("[data-hooks-add]"))
|
||||
?.focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const discard = event.target.closest("[data-hooks-discard]");
|
||||
if (discard && live) {
|
||||
draft = cloneState(live);
|
||||
renderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
const apply = event.target.closest("[data-hooks-apply]");
|
||||
if (apply) applyDraft(article);
|
||||
});
|
||||
|
||||
document.addEventListener("input", (event) => {
|
||||
if (!event.target.matches("[data-hooks-search]")) return;
|
||||
const article = event.target.closest('.dcx-article[data-dcx-category="hooks"]');
|
||||
if (article) renderRules(article);
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
const summary = event.target.closest(".dcx-hooks-discipline > summary");
|
||||
if (summary && ["Enter", " "].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
if (!event.repeat) {
|
||||
const article = summary.closest('.dcx-article[data-dcx-category="hooks"]');
|
||||
if (article) toggleDiscipline(article, summary);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const family = event.target.closest("[data-hooks-family]");
|
||||
if (!family || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
|
||||
const article = family.closest('.dcx-article[data-dcx-category="hooks"]');
|
||||
const buttons = [...article.querySelectorAll("[data-hooks-family]")];
|
||||
const index = buttons.indexOf(family);
|
||||
if (index < 0) return;
|
||||
|
||||
event.preventDefault();
|
||||
const direction = ["ArrowRight", "ArrowDown"].includes(event.key) ? 1 : -1;
|
||||
const nextIndex = event.key === "Home"
|
||||
? 0
|
||||
: event.key === "End"
|
||||
? buttons.length - 1
|
||||
: (index + direction + buttons.length) % buttons.length;
|
||||
buttons[nextIndex].click();
|
||||
article.querySelector(`[data-hooks-family="${view.activeFamily}"]`)?.focus();
|
||||
});
|
||||
|
||||
document.addEventListener("change", (event) => {
|
||||
const article = event.target.closest('.dcx-article[data-dcx-category="hooks"]');
|
||||
if (!article) return;
|
||||
|
||||
if (event.target.matches("[data-hooks-kind]")) {
|
||||
const form = event.target.closest("[data-hooks-form]");
|
||||
if (form) syncKindFields(form);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!interactive()) return;
|
||||
|
||||
if (event.target.matches("[data-hooks-master]")) {
|
||||
draft.enabled = event.target.checked;
|
||||
syncMaster(article);
|
||||
syncApplyBar(article);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.matches("[data-hooks-rule]")) {
|
||||
const id = event.target.dataset.hooksRule;
|
||||
if (event.target.checked) draft.ignoreRules = draft.ignoreRules.filter((entry) => entry !== id);
|
||||
else if (!draft.ignoreRules.includes(id)) draft.ignoreRules.push(id);
|
||||
renderFamilies(article);
|
||||
syncApplyBar(article);
|
||||
const query = article.querySelector("[data-hooks-search]")?.value.trim();
|
||||
const summary = article.querySelector("[data-hooks-summary]");
|
||||
if (!query && summary) {
|
||||
const rules = familyRules(view.activeFamily);
|
||||
summary.textContent = `${rules.filter((rule) => isEnabled(rule.id)).length} of ${rules.length} selected`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("submit", (event) => {
|
||||
const form = event.target.closest("[data-hooks-form]");
|
||||
if (!form) return;
|
||||
event.preventDefault();
|
||||
const article = form.closest('.dcx-article[data-dcx-category="hooks"]');
|
||||
if (!article || !interactive()) return;
|
||||
|
||||
const data = new FormData(form);
|
||||
const kind = String(data.get("kind") || "value");
|
||||
const rule = String(data.get("rule") || "").trim();
|
||||
const value = String(data.get("value") || "").trim();
|
||||
const files = parseGlobList(data.get("files") || "");
|
||||
const reason = String(data.get("reason") || "").trim();
|
||||
|
||||
if (kind === "file") {
|
||||
if (!files.length) return;
|
||||
files.forEach((glob) => {
|
||||
if (!draft.ignoreFiles.includes(glob)) draft.ignoreFiles.push(glob);
|
||||
});
|
||||
} else {
|
||||
const entry = kind === "value"
|
||||
? { rule, value }
|
||||
: { rule, value: "*", files };
|
||||
if (!entry.rule || (kind === "value" && !entry.value)) return;
|
||||
if (kind === "rule-in-files" && !files.length) return;
|
||||
if (reason) entry.reason = reason;
|
||||
const keys = new Set(draft.ignoreValues.map(entryKey));
|
||||
if (!keys.has(entryKey(entry))) draft.ignoreValues.push(entry);
|
||||
}
|
||||
|
||||
form.reset();
|
||||
syncKindFields(form);
|
||||
setCustomFormOpen(form, false);
|
||||
const addButton = article.querySelector("[data-hooks-add]");
|
||||
addButton?.setAttribute("aria-expanded", "false");
|
||||
renderAll();
|
||||
addButton?.focus({ preventScroll: true });
|
||||
});
|
||||
|
||||
renameInterfaceToHooks();
|
||||
installTemplate();
|
||||
|
||||
const observer = new MutationObserver(scheduleSync);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
window.addEventListener("pageshow", scheduleSync);
|
||||
scheduleSync();
|
||||
})();
|
||||
@@ -0,0 +1,223 @@
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
if (window.__dcxSettingsInstalled) return;
|
||||
window.__dcxSettingsInstalled = true;
|
||||
|
||||
const REDUCED_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
const COMMANDS = [
|
||||
{
|
||||
label: "Open",
|
||||
description: "Pick up where you left off.",
|
||||
command: "/impeccable design-context open",
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
description: "Make a few changes to this design context.",
|
||||
command: "/impeccable design-context edit",
|
||||
},
|
||||
{
|
||||
label: "Export",
|
||||
description: "Save a copy to share or keep elsewhere.",
|
||||
command: "/impeccable design-context export",
|
||||
},
|
||||
{
|
||||
label: "Import",
|
||||
description: "Bring a saved design context into this project.",
|
||||
command: "/impeccable design-context import",
|
||||
},
|
||||
];
|
||||
|
||||
const settingsIcon = `
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>`;
|
||||
|
||||
const closeIcon = `
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M6.5 6.5l11 11M17.5 6.5l-11 11"></path>
|
||||
</svg>`;
|
||||
|
||||
const installTopbarActions = (root) => {
|
||||
const topbar = root?.querySelector?.(".dcx-topbar");
|
||||
const templateClose = topbar?.querySelector(".dcx-close");
|
||||
if (!topbar || !templateClose || topbar.querySelector("[data-dcx-settings-open]")) return;
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "dcx-topbar-actions";
|
||||
|
||||
const trigger = document.createElement("button");
|
||||
trigger.className = "dcx-settings-trigger";
|
||||
trigger.type = "button";
|
||||
trigger.dataset.dcxSettingsOpen = "";
|
||||
trigger.setAttribute("aria-label", "Design context settings");
|
||||
trigger.setAttribute("aria-haspopup", "dialog");
|
||||
trigger.setAttribute("aria-controls", "dcx-context-settings");
|
||||
trigger.setAttribute("aria-expanded", "false");
|
||||
trigger.innerHTML = settingsIcon;
|
||||
|
||||
templateClose.before(actions);
|
||||
actions.append(trigger, templateClose);
|
||||
};
|
||||
|
||||
const shellTemplate = document.querySelector("#dcx-shell-template");
|
||||
installTopbarActions(shellTemplate?.content);
|
||||
document.querySelectorAll(".dcx-expander").forEach(installTopbarActions);
|
||||
|
||||
let modal = document.querySelector("#dcx-context-settings");
|
||||
if (!modal) {
|
||||
modal = document.createElement("dialog");
|
||||
modal.id = "dcx-context-settings";
|
||||
modal.className = "picker-modal dcx-settings-modal";
|
||||
modal.setAttribute("aria-labelledby", "dcx-context-settings-title");
|
||||
modal.setAttribute("aria-describedby", "dcx-context-settings-lede");
|
||||
modal.innerHTML = `
|
||||
<div class="picker-modal-inner dcx-settings-panel" data-dcx-command-context>
|
||||
<header class="picker-modal-head dcx-settings-head">
|
||||
<div>
|
||||
<h2 id="dcx-context-settings-title">Design context commands</h2>
|
||||
<p id="dcx-context-settings-lede">Choose what you’d like to do with this design context.</p>
|
||||
</div>
|
||||
<button class="dcx-settings-close" type="button" data-dcx-settings-close aria-label="Close settings">
|
||||
${closeIcon}
|
||||
</button>
|
||||
</header>
|
||||
<div class="dcx-settings-commands">
|
||||
${COMMANDS.map(({ label, description, command }) => `
|
||||
<section class="dcx-settings-command">
|
||||
<div class="dcx-settings-command-copy">
|
||||
<h3>${label}</h3>
|
||||
<p>${description}</p>
|
||||
</div>
|
||||
<div class="dcx-command-copy">
|
||||
<span class="dcx-command-copy__prompt" aria-hidden="true">$</span>
|
||||
<code>${command}</code>
|
||||
<button class="dcx-command-copy__button" type="button" data-dcx-copy-command="${command}" aria-label="Copy ${label.toLowerCase()} command">
|
||||
<svg class="dcx-command-copy__copy-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
<svg class="dcx-command-copy__check-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M20 6 9 17l-5-5"></path>
|
||||
</svg>
|
||||
<span class="dcx-command-copy__label">Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>`).join("")}
|
||||
</div>
|
||||
<p class="dcx-command-status" role="status" aria-live="polite"></p>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
let invoker = null;
|
||||
let closeTimer = 0;
|
||||
|
||||
const resetCopyState = () => {
|
||||
modal.querySelectorAll("[data-dcx-copy-command]").forEach((button) => {
|
||||
window.clearTimeout(button._dcxCopyTimer);
|
||||
button.classList.remove("copied");
|
||||
button.removeAttribute("data-copied");
|
||||
});
|
||||
const status = modal.querySelector(".dcx-command-status");
|
||||
if (status) status.textContent = "";
|
||||
};
|
||||
|
||||
const openSettings = (trigger) => {
|
||||
window.clearTimeout(closeTimer);
|
||||
resetCopyState();
|
||||
invoker = trigger;
|
||||
trigger.setAttribute("aria-expanded", "true");
|
||||
if (!modal.open) modal.showModal();
|
||||
requestAnimationFrame(() => {
|
||||
modal.classList.add("is-visible");
|
||||
modal.querySelector("[data-dcx-settings-close]")?.focus({ preventScroll: true });
|
||||
});
|
||||
};
|
||||
|
||||
const closeSettings = ({ restoreFocus = true, instant = false } = {}) => {
|
||||
if (!modal.open) return;
|
||||
window.clearTimeout(closeTimer);
|
||||
modal.classList.remove("is-visible");
|
||||
|
||||
const finish = () => {
|
||||
if (!modal.open) return;
|
||||
modal.close();
|
||||
resetCopyState();
|
||||
invoker?.setAttribute?.("aria-expanded", "false");
|
||||
if (restoreFocus && invoker instanceof HTMLElement && invoker.isConnected) {
|
||||
invoker.focus({ preventScroll: true });
|
||||
}
|
||||
invoker = null;
|
||||
};
|
||||
|
||||
if (instant || REDUCED_MOTION.matches) finish();
|
||||
else closeTimer = window.setTimeout(finish, 200);
|
||||
};
|
||||
|
||||
window.closeDcxSettings = closeSettings;
|
||||
|
||||
document.addEventListener("click", async (event) => {
|
||||
const openButton = event.target.closest?.("[data-dcx-settings-open]");
|
||||
if (openButton) {
|
||||
event.preventDefault();
|
||||
openSettings(openButton);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest?.("[data-dcx-settings-close]")) {
|
||||
closeSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
modal.addEventListener("click", (event) => {
|
||||
if (event.target !== modal) return;
|
||||
const rect = modal.getBoundingClientRect();
|
||||
const outside = event.clientX < rect.left
|
||||
|| event.clientX > rect.right
|
||||
|| event.clientY < rect.top
|
||||
|| event.clientY > rect.bottom;
|
||||
if (outside) closeSettings();
|
||||
});
|
||||
|
||||
modal.addEventListener("cancel", (event) => {
|
||||
event.preventDefault();
|
||||
closeSettings();
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (!modal.open) return;
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
closeSettings();
|
||||
return;
|
||||
}
|
||||
if (event.key !== "Tab") return;
|
||||
|
||||
const focusable = [...modal.querySelectorAll("button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])")]
|
||||
.filter((element) => element.getClientRects().length > 0 && !element.hidden);
|
||||
if (!focusable.length) {
|
||||
event.preventDefault();
|
||||
modal.focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
const active = document.activeElement;
|
||||
if (event.shiftKey && (active === first || !modal.contains(active))) {
|
||||
event.preventDefault();
|
||||
last.focus({ preventScroll: true });
|
||||
} else if (!event.shiftKey && (active === last || !modal.contains(active))) {
|
||||
event.preventDefault();
|
||||
first.focus({ preventScroll: true });
|
||||
}
|
||||
}, true);
|
||||
|
||||
document.addEventListener("dcx:document-mounted", (event) => {
|
||||
installTopbarActions(event.target);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,189 @@
|
||||
/** Restoring a previous run into the questionnaire.
|
||||
*
|
||||
* The one rule this file is built around: setting `.checked` or `.value` from
|
||||
* script fires no event, and every committer on this page runs off change
|
||||
* events. A bare write therefore leaves the radio saying one thing and the
|
||||
* hidden field the rest of the run reads saying another. So every answer is
|
||||
* restored by clicking its control, or by calling the function the control
|
||||
* would have called.
|
||||
*
|
||||
* The order matters too. Clicking a radio runs record(), which stamps
|
||||
* data-chosen on every field it writes, so which answers a person actually
|
||||
* visited is restored last, after every click has had its say.
|
||||
*
|
||||
* The questionnaire owns the state being restored, so palette-picker.js passes
|
||||
* the handles this needs rather than this file reaching into it.
|
||||
*/
|
||||
|
||||
const ROLES = ['primary', 'secondary', 'tertiary', 'neutral'];
|
||||
|
||||
/* Groups whose answer is one radio the whole run reads. Absent keys are left
|
||||
alone: a run that was never asked about movement must come back without a
|
||||
movement key, not with a default standing in for a decision. */
|
||||
const RADIO_GROUPS = [
|
||||
'color-strategy',
|
||||
'motion-energy',
|
||||
'layout-structure',
|
||||
'boundary-style',
|
||||
'corner-style',
|
||||
'depth-style',
|
||||
'type-scale',
|
||||
'icon-pack',
|
||||
];
|
||||
|
||||
const asText = (value) => (typeof value === 'string' ? value : '');
|
||||
|
||||
/* Clicking is the whole point: activation runs the change handlers that commit
|
||||
the answer, which a bare `checked = true` would skip. */
|
||||
function clickOption(name, value) {
|
||||
if (!value) return false;
|
||||
const input = document.querySelector(`input[name="${name}"][value="${CSS.escape(value)}"]`);
|
||||
if (!input || input.disabled) return false;
|
||||
input.click();
|
||||
return true;
|
||||
}
|
||||
|
||||
function surfaceFields() {
|
||||
return [...document.querySelectorAll('input[type="hidden"][data-surface-field]')];
|
||||
}
|
||||
|
||||
export function hydrateAnswers(prior, ctx) {
|
||||
if (!prior) return;
|
||||
|
||||
restoreSurfaces(prior, ctx);
|
||||
for (const group of RADIO_GROUPS) {
|
||||
if (group in prior) clickOption(group, asText(prior[group]));
|
||||
}
|
||||
restoreSurfaceFields(prior);
|
||||
restorePalette(prior, ctx);
|
||||
restoreFonts(prior, ctx);
|
||||
restoreIconMeta(prior);
|
||||
restoreChosen(prior, ctx);
|
||||
}
|
||||
|
||||
/* Which surfaces the run covers, first, because every per-surface field is
|
||||
filled with its default the moment a tile is checked. */
|
||||
function restoreSurfaces(prior, ctx) {
|
||||
const raw = prior['surface-modes'];
|
||||
const wanted = new Set(Array.isArray(raw) ? raw : (asText(raw) ? [raw] : []));
|
||||
if (!wanted.size) return;
|
||||
// The same guard the agent's own hint carries: a set naming no real tile
|
||||
// would otherwise clear the run's only required answer.
|
||||
if (!ctx.modeInputs.some((input) => wanted.has(input.value))) return;
|
||||
for (const input of ctx.modeInputs) input.checked = wanted.has(input.value);
|
||||
ctx.syncModes();
|
||||
}
|
||||
|
||||
function restoreSurfaceFields(prior) {
|
||||
for (const field of surfaceFields()) {
|
||||
const key = field.dataset.surfaceField;
|
||||
if (field.disabled || !(key in prior)) continue;
|
||||
const value = asText(prior[key]);
|
||||
if (value) field.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/* The deck has no programmatic selection path: the committing click reads the
|
||||
card the scroller is parked on. So the card's own state is written first,
|
||||
which is also what stops a later reorder or reset from reverting the fields
|
||||
to the colors the cue was dealt with. */
|
||||
function restorePalette(prior, ctx) {
|
||||
const source = asText(prior['palette-source']);
|
||||
const colors = {};
|
||||
for (const role of ROLES) {
|
||||
const hex = asText(prior[`palette-${role}`]);
|
||||
if (hex) colors[role] = hex;
|
||||
}
|
||||
if (!source && !Object.keys(colors).length) return;
|
||||
|
||||
/* The fields come first, and unconditionally: the colors are the answer, the
|
||||
source is what the document names them by, and neither depends on the deck
|
||||
still being able to show the card they came from. A document reopened after
|
||||
the generation workspace was cleaned has no deck at all, and it still has a
|
||||
palette. */
|
||||
const sourceField = document.querySelector('[name="palette-source"]');
|
||||
if (sourceField && source) sourceField.value = source;
|
||||
for (const role of ROLES) {
|
||||
const field = document.querySelector(`[name="palette-${role}"]`);
|
||||
if (field && colors[role]) field.value = colors[role];
|
||||
}
|
||||
|
||||
// Nothing was dealt, so there is no card to park on and nothing to repaint.
|
||||
if (!ctx.cards.length) return;
|
||||
|
||||
const index = ctx.cards.findIndex((item) => item.id === source);
|
||||
const target = index === -1 ? 0 : index;
|
||||
/* Writing the card's own state is also what stops a later reorder or reset
|
||||
from reverting the fields to the colors the cue was dealt with. */
|
||||
const state = ctx.states.get(ctx.cards[target]?.id);
|
||||
if (state?.colors) Object.assign(state.colors, colors);
|
||||
ctx.setCurrent(target);
|
||||
ctx.render();
|
||||
ctx.syncDeckScroll();
|
||||
}
|
||||
|
||||
/* A pair still on the rail is chosen by clicking it, which runs syncFontPair.
|
||||
A pair that is not, an upload or a set of faces this run was not dealt, is
|
||||
rebuilt as the custom pair and registered before its card is added: the rail
|
||||
resolves a click through the manifest, so a card the manifest does not know
|
||||
cannot be chosen a second time. */
|
||||
function restoreFonts(prior, ctx) {
|
||||
const wanted = asText(prior['font-pair']);
|
||||
const heading = asText(prior['font-heading']);
|
||||
const body = asText(prior['font-body']);
|
||||
const manifest = ctx.fontManifest();
|
||||
|
||||
if (wanted && wanted !== 'custom' && manifest.pairs.some(({ id }) => id === wanted)) {
|
||||
if (clickOption('font-pair', wanted)) return;
|
||||
}
|
||||
if (!heading || !body) return;
|
||||
|
||||
const pair = {
|
||||
id: 'custom',
|
||||
name: 'Custom',
|
||||
heading: { family: heading, weight: 600, source: asText(prior['font-heading-source']) },
|
||||
body: { family: body, weight: 400, source: asText(prior['font-body-source']) },
|
||||
why: 'From your last run',
|
||||
};
|
||||
manifest.pairs = [pair, ...manifest.pairs.filter(({ id }) => id !== 'custom')];
|
||||
for (const node of ctx.pairNodes()) {
|
||||
if (node.querySelector('input')?.value === 'custom') ctx.removePairCard(node);
|
||||
}
|
||||
ctx.addPairCard(pair, { checked: true, first: true });
|
||||
ctx.loadCustomFace(pair);
|
||||
ctx.syncFontPair(pair);
|
||||
ctx.applyHoist({ force: true });
|
||||
}
|
||||
|
||||
/* The pack's own radio carries the license and URL when it is still on offer;
|
||||
these are the record of one that is not. */
|
||||
function restoreIconMeta(prior) {
|
||||
for (const key of ['icon-pack-name', 'icon-pack-license', 'icon-pack-url']) {
|
||||
const field = document.querySelector(`[name="${key}"]`);
|
||||
const value = asText(prior[key]);
|
||||
if (field && !field.value && value) field.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/* Last, because every click above stamped its own. A default nobody opened and
|
||||
a default someone confirmed hold the same value, so only this list tells them
|
||||
apart, and the document says which is which. */
|
||||
function restoreChosen(prior, ctx) {
|
||||
let chosen = null;
|
||||
try {
|
||||
const parsed = JSON.parse(asText(prior._chosen) || 'null');
|
||||
if (Array.isArray(parsed)) chosen = new Set(parsed);
|
||||
} catch {
|
||||
/* Written by a run that did not keep the distinction. */
|
||||
}
|
||||
for (const field of surfaceFields()) {
|
||||
const key = field.dataset.surfaceField;
|
||||
if (field.disabled) continue;
|
||||
// A run that kept no list was confirmed wholesale at submit, so every
|
||||
// answer it carries counts as visited.
|
||||
const visited = chosen ? chosen.has(key) : key in prior;
|
||||
if (visited) field.dataset.chosen = 'yes';
|
||||
else delete field.dataset.chosen;
|
||||
}
|
||||
ctx.syncChosenField();
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/* Audience detail treatment shared with demos/audience-scroll/docs.html. */
|
||||
|
||||
.dcx-audience {
|
||||
--dcx-audience-icon-size: 100px;
|
||||
}
|
||||
|
||||
.dcx-audience > .dcx-audience-hero {
|
||||
margin-bottom: clamp(48px, 6vw, 76px);
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section {
|
||||
margin-top: 0;
|
||||
margin-bottom: clamp(48px, 6vw, 76px);
|
||||
scroll-margin-top: 18px;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section-head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) var(--dcx-audience-icon-size);
|
||||
gap: clamp(24px, 4vw, 44px);
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section-heading {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section-title {
|
||||
margin: 0;
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.25;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section-lede {
|
||||
max-width: 52ch;
|
||||
margin: 10px 0 0;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section-icon {
|
||||
width: var(--dcx-audience-icon-size);
|
||||
height: var(--dcx-audience-icon-size);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section-icon img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section--people .dcx-defs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section--people .dcx-def {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
padding: 18px 20px 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: var(--panel-bg);
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section--people .dcx-def dt {
|
||||
color: var(--ks-champagne);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section--people .dcx-def dd {
|
||||
margin-top: 9px;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.58;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-journey {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 30px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-journey .dcx-callout {
|
||||
min-width: 0;
|
||||
padding: 18px 20px 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: var(--panel-bg);
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-journey .dcx-callout + .dcx-callout {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-journey-arrow {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--accent);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-list {
|
||||
counter-reset: dcx-audience-item;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-list li {
|
||||
counter-increment: dcx-audience-item;
|
||||
position: static;
|
||||
display: grid;
|
||||
grid-template-columns: 32px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: baseline;
|
||||
min-width: 0;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: var(--panel-bg);
|
||||
color: var(--ks-text);
|
||||
font-size: 0.96rem;
|
||||
line-height: 1.6;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-list li::before {
|
||||
content: counter(dcx-audience-item, decimal-leading-zero);
|
||||
position: static;
|
||||
width: auto;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.dcx-audience .dcx-audience-section-head {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section--people .dcx-defs,
|
||||
.dcx-audience .dcx-audience-journey {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-journey-arrow {
|
||||
height: 22px;
|
||||
justify-content: start;
|
||||
padding-left: 20px;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.dcx-audience .dcx-audience-section-head {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-section-title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-list li {
|
||||
grid-template-columns: 26px minmax(0, 1fr);
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dcx-audience *,
|
||||
.dcx-audience *::before,
|
||||
.dcx-audience *::after {
|
||||
transition-duration: 0.001ms !important;
|
||||
transition-delay: 0ms !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/* Compact command-copy control, matched to the homepage Get started treatment. */
|
||||
|
||||
.dcx-command-copy {
|
||||
--dcx-command-accent: var(--ks-kinpaku);
|
||||
--dcx-command-ground: var(--ks-code-block-bg, var(--ks-lacquer-deep));
|
||||
--dcx-command-ink: var(--ks-code-block-fg, var(--ks-text));
|
||||
--dcx-command-copy-border: var(--ks-rule);
|
||||
--dcx-command-copy-hover: oklch(77% 0.14 82 / 0.08);
|
||||
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 52px;
|
||||
padding: 9px 9px 9px 18px;
|
||||
border: 1px solid var(--dcx-command-accent);
|
||||
border-radius: 2px;
|
||||
background: var(--dcx-command-ground);
|
||||
}
|
||||
|
||||
.dcx-command-copy__prompt {
|
||||
flex: none;
|
||||
color: var(--dcx-command-accent);
|
||||
font: 500 1.125rem/1 var(--ks-mono);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dcx-command-copy > code {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: auto;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--dcx-command-ink);
|
||||
font: 400 0.98rem/1.35 var(--ks-mono);
|
||||
letter-spacing: 0;
|
||||
scrollbar-width: thin;
|
||||
user-select: all;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dcx-command-copy__button {
|
||||
position: relative;
|
||||
display: inline-grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--dcx-command-copy-border);
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
color: var(--dcx-command-accent);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 180ms var(--ks-ease),
|
||||
border-color 180ms var(--ks-ease),
|
||||
color 180ms var(--ks-ease),
|
||||
transform 120ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.dcx-command-copy__button:hover,
|
||||
.dcx-command-copy__button.copied {
|
||||
border-color: currentColor;
|
||||
background: var(--dcx-command-copy-hover);
|
||||
}
|
||||
|
||||
.dcx-command-copy__button:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.dcx-command-copy__button:focus-visible {
|
||||
outline: 2px solid var(--ks-patina);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
.dcx-command-copy__button svg {
|
||||
grid-area: 1 / 1;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
transition:
|
||||
opacity 140ms var(--ks-ease),
|
||||
transform 180ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.dcx-command-copy__copy-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
opacity: 1;
|
||||
stroke-width: 1.6;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.dcx-command-copy__check-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0;
|
||||
stroke-width: 2.1;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
transform: scale(0.86);
|
||||
}
|
||||
|
||||
.dcx-command-copy__check-icon path {
|
||||
stroke-dasharray: 24;
|
||||
stroke-dashoffset: 24;
|
||||
}
|
||||
|
||||
.dcx-command-copy__button.copied .dcx-command-copy__copy-icon {
|
||||
opacity: 0;
|
||||
transform: scale(0.82);
|
||||
}
|
||||
|
||||
.dcx-command-copy__button.copied .dcx-command-copy__check-icon {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.dcx-command-copy__button.copied .dcx-command-copy__check-icon path {
|
||||
animation: dcx-command-check-draw 260ms var(--ks-ease) forwards;
|
||||
}
|
||||
|
||||
.dcx-command-copy__label,
|
||||
.dcx-command-status {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
border: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@keyframes dcx-command-check-draw {
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.dcx-command-copy {
|
||||
gap: 10px;
|
||||
padding: 9px 8px 9px 14px;
|
||||
}
|
||||
|
||||
.dcx-command-copy > code {
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dcx-command-copy__button,
|
||||
.dcx-command-copy__button svg {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.dcx-command-copy__button.copied .dcx-command-copy__check-icon path {
|
||||
animation: none;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
/* A visual component inventory: specimens first, documentation chrome kept quiet. */
|
||||
|
||||
.dcx-detail-article--components .dcx-component-stack {
|
||||
display: grid;
|
||||
gap: clamp(42px, 5vw, 64px);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-example {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-example > figcaption {
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-example h4 {
|
||||
margin: 0;
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 1.08rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-canvas {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: clamp(230px, 27vw, 300px);
|
||||
padding: clamp(30px, 5vw, 58px);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 8px;
|
||||
background: var(--ks-lacquer-deep);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-canvas--center {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-row--baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-button {
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 9px;
|
||||
min-height: 44px;
|
||||
padding: 0 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-button svg,
|
||||
.dcx-detail-article--components .dcx-demo-icon-button svg,
|
||||
.dcx-detail-article--components .dcx-demo-input svg {
|
||||
flex: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.7;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-button--primary {
|
||||
border-color: var(--ks-kinpaku);
|
||||
background: var(--ks-kinpaku);
|
||||
color: var(--ks-lacquer-deep);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-button--secondary {
|
||||
border-color: var(--ks-graphite-2);
|
||||
background: var(--ks-graphite-2);
|
||||
color: var(--ks-champagne);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-button--tertiary {
|
||||
border-color: color-mix(in oklch, var(--ks-patina) 55%, var(--ks-rule));
|
||||
background: color-mix(in oklch, var(--ks-patina) 13%, transparent);
|
||||
color: var(--ks-patina-pale);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-button--outline {
|
||||
border-color: var(--ks-rule);
|
||||
background: transparent;
|
||||
color: var(--ks-champagne);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-button--ghost {
|
||||
padding-inline: 12px;
|
||||
background: transparent;
|
||||
color: var(--ks-text);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-button--danger {
|
||||
border-color: var(--ks-vermilion);
|
||||
background: var(--ks-vermilion);
|
||||
color: var(--ks-champagne);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-button--danger-soft {
|
||||
border-color: transparent;
|
||||
background: color-mix(in oklch, var(--ks-vermilion) 20%, transparent);
|
||||
color: color-mix(in oklch, var(--ks-vermilion) 74%, var(--ks-champagne));
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-button--small {
|
||||
min-height: 36px;
|
||||
padding-inline: 16px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-button--large {
|
||||
min-height: 54px;
|
||||
padding-inline: 26px;
|
||||
font-size: 0.96rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-icon-button {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50%;
|
||||
color: var(--ks-champagne);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-icon-button--secondary {
|
||||
background: var(--ks-graphite-2);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-icon-button--outline {
|
||||
border-color: var(--ks-rule);
|
||||
color: var(--ks-patina);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-icon-button--danger {
|
||||
background: var(--ks-vermilion);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-button--loading {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid color-mix(in oklch, var(--ks-lacquer-deep) 35%, transparent);
|
||||
border-top-color: var(--ks-lacquer-deep);
|
||||
border-radius: 50%;
|
||||
animation: dcx-component-spin 900ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dcx-component-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-canvas--fields,
|
||||
.dcx-detail-article--components .dcx-component-canvas--cards {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-demo-grid {
|
||||
display: grid;
|
||||
align-items: stretch;
|
||||
width: min(100%, 900px);
|
||||
min-width: 0;
|
||||
gap: clamp(16px, 2.2vw, 22px);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-demo-grid--three {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-demo-grid--two {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-demo-grid--single {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
width: min(100%, 680px);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-demo-grid--baseline {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-field {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-field-label {
|
||||
color: var(--ks-text-muted);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-input {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 48px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 3px;
|
||||
background: var(--ks-lacquer-raised);
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.4;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-input.is-placeholder {
|
||||
color: var(--ks-text-mute-deep);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-field small {
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-field.is-invalid .dcx-demo-input {
|
||||
border-color: var(--ks-vermilion);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-field.is-invalid small {
|
||||
color: color-mix(in oklch, var(--ks-vermilion) 76%, var(--ks-champagne));
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-field--small .dcx-demo-input {
|
||||
min-height: 38px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-field--large .dcx-demo-input {
|
||||
min-height: 58px;
|
||||
font-size: 0.96rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-input--affix {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-input--affix > span {
|
||||
flex: none;
|
||||
color: var(--ks-patina);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-input--suffix {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-demo-input--textarea {
|
||||
align-items: flex-start;
|
||||
min-height: 132px;
|
||||
padding-block: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 268px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 8px;
|
||||
background: var(--panel-bg);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card-body {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
padding: clamp(20px, 2.6vw, 28px);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card h5 {
|
||||
margin: 14px 0 8px;
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 1.04rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.38;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card p,
|
||||
.dcx-detail-article--components .dcx-showcase-card blockquote {
|
||||
margin: 0;
|
||||
color: var(--ks-text);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.62;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card blockquote {
|
||||
margin-top: 24px;
|
||||
color: var(--ks-champagne);
|
||||
font-size: 1.02rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card-meta {
|
||||
margin-top: auto;
|
||||
padding-top: 22px;
|
||||
color: var(--ks-text-muted);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card dl {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card dl > div {
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card dt {
|
||||
color: var(--ks-text-faint);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card dd {
|
||||
margin: 0;
|
||||
color: var(--ks-text);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card-link {
|
||||
margin-top: auto;
|
||||
padding-top: 22px;
|
||||
color: var(--ks-patina);
|
||||
font-size: 0.84rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card--media img {
|
||||
width: 100%;
|
||||
height: clamp(190px, 23vw, 240px);
|
||||
object-fit: cover;
|
||||
object-position: 50% 38%;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card--media-close img {
|
||||
object-position: 50% 66%;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-canvas--single-card {
|
||||
min-height: clamp(300px, 34vw, 380px);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card--horizontal {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 0.85fr) minmax(0, 1.15fr);
|
||||
width: min(100%, 780px);
|
||||
min-height: 290px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card--horizontal > img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 290px;
|
||||
object-fit: cover;
|
||||
object-position: 50% 42%;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card--action .dcx-showcase-card-body {
|
||||
min-height: 286px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card--action .dcx-demo-button {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 1020px) {
|
||||
.dcx-detail-article--components .dcx-component-demo-grid--three {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-demo-grid--three > :last-child:nth-child(odd) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.dcx-detail-article--components .dcx-component-stack {
|
||||
gap: 36px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-canvas {
|
||||
min-height: 210px;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-demo-grid--three,
|
||||
.dcx-detail-article--components .dcx-component-demo-grid--two {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-demo-grid--three > :last-child:nth-child(odd) {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card--horizontal {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-showcase-card--horizontal > img {
|
||||
height: 230px;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-component-kind {
|
||||
color: var(--ks-text-faint);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
line-height: 1.45;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-components-command {
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
width: min(100%, 620px);
|
||||
min-width: 0;
|
||||
margin-top: clamp(28px, 3.5vw, 40px);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-components-command-copy h4 {
|
||||
margin: 0 0 6px;
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dcx-detail-article--components .dcx-components-command-copy p {
|
||||
max-width: 54ch;
|
||||
margin: 0;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Specimens in the committed palette: when the run committed one,
|
||||
the inventory renders in it. design-context.js sets
|
||||
.dcx-palette-live on <body> and paints --pkc-* there (the body
|
||||
survives document remounts; the article does not). Without the
|
||||
gate the vendored kinpaku colors above stand. Danger stays
|
||||
--ks-vermilion: semantic, not brand. Geometry untouched.
|
||||
============================================================ */
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-component-canvas {
|
||||
background: var(--pkc-neutral);
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-demo-button--primary {
|
||||
border-color: var(--pkc-primary);
|
||||
background: var(--pkc-primary);
|
||||
color: var(--pkc-p-ink);
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-demo-button--secondary {
|
||||
border-color: color-mix(in oklab, var(--pkc-n-ink) 14%, var(--pkc-neutral));
|
||||
background: color-mix(in oklab, var(--pkc-n-ink) 14%, var(--pkc-neutral));
|
||||
color: var(--pkc-n-ink);
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-demo-button--tertiary {
|
||||
border-color: color-mix(in oklab, var(--pkc-tertiary) 55%, var(--pkc-neutral));
|
||||
background: color-mix(in oklab, var(--pkc-tertiary) 13%, transparent);
|
||||
color: var(--pkc-t-on-n);
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-demo-button--outline {
|
||||
border-color: color-mix(in oklab, var(--pkc-n-ink) 22%, transparent);
|
||||
color: var(--pkc-n-ink);
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-demo-button--ghost {
|
||||
color: color-mix(in oklab, var(--pkc-n-ink) 92%, var(--pkc-neutral));
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-demo-icon-button--secondary {
|
||||
background: color-mix(in oklab, var(--pkc-n-ink) 14%, var(--pkc-neutral));
|
||||
color: var(--pkc-n-ink);
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-demo-icon-button--outline {
|
||||
border-color: color-mix(in oklab, var(--pkc-n-ink) 22%, transparent);
|
||||
color: var(--pkc-s-on-n);
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-demo-spinner {
|
||||
border-color: color-mix(in oklab, var(--pkc-p-ink) 35%, transparent);
|
||||
border-top-color: var(--pkc-p-ink);
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-demo-field-label {
|
||||
color: color-mix(in oklab, var(--pkc-n-ink) 75%, var(--pkc-neutral));
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-demo-input {
|
||||
border-color: color-mix(in oklab, var(--pkc-n-ink) 22%, transparent);
|
||||
background: color-mix(in oklab, var(--pkc-n-ink) 7%, var(--pkc-neutral));
|
||||
color: var(--pkc-n-ink);
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-demo-input.is-placeholder {
|
||||
color: color-mix(in oklab, var(--pkc-n-ink) 55%, var(--pkc-neutral));
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-demo-field:not(.is-invalid) small {
|
||||
color: color-mix(in oklab, var(--pkc-n-ink) 75%, var(--pkc-neutral));
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-demo-input--affix > span {
|
||||
color: var(--pkc-s-on-n);
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card {
|
||||
border-color: color-mix(in oklab, var(--pkc-n-ink) 18%, transparent);
|
||||
background: color-mix(in oklab, var(--pkc-n-ink) 5%, var(--pkc-neutral));
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card h5,
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card blockquote {
|
||||
color: var(--pkc-n-ink);
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card p,
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card dd {
|
||||
color: color-mix(in oklab, var(--pkc-n-ink) 92%, var(--pkc-neutral));
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card-meta {
|
||||
color: color-mix(in oklab, var(--pkc-n-ink) 75%, var(--pkc-neutral));
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card dt,
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card .dcx-component-kind {
|
||||
color: color-mix(in oklab, var(--pkc-n-ink) 64%, var(--pkc-neutral));
|
||||
}
|
||||
|
||||
.dcx-palette-live .dcx-detail-article--components .dcx-showcase-card-link {
|
||||
color: var(--pkc-s-on-n);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Specimens in the committed kit: faces, corners, depth, and the
|
||||
scale step, the same contract .dcx-palette-live holds for color.
|
||||
design-context.js paintKit publishes --pkc-font-*, --pkc-radius-*,
|
||||
--pkc-shadow-*, and --pkc-scale-ratio on <body> and gates them
|
||||
behind .dcx-kit-live; without the gate the vendored kinpaku kit
|
||||
above stands. Documentation chrome (the canvas frame, figcaption
|
||||
headings, the mono eyebrows and metadata) stays the document's
|
||||
own furniture and is deliberately left alone.
|
||||
============================================================ */
|
||||
.dcx-kit-live .dcx-detail-article--components .dcx-component-canvas {
|
||||
font-family: var(--pkc-font-body);
|
||||
}
|
||||
|
||||
.dcx-kit-live .dcx-detail-article--components .dcx-demo-button,
|
||||
.dcx-kit-live .dcx-detail-article--components .dcx-demo-field-label,
|
||||
.dcx-kit-live .dcx-detail-article--components .dcx-demo-input,
|
||||
.dcx-kit-live .dcx-detail-article--components .dcx-showcase-card p,
|
||||
.dcx-kit-live .dcx-detail-article--components .dcx-showcase-card blockquote {
|
||||
font-family: var(--pkc-font-body);
|
||||
}
|
||||
|
||||
.dcx-kit-live .dcx-detail-article--components .dcx-showcase-card h5 {
|
||||
font-family: var(--pkc-font-heading);
|
||||
font-weight: var(--pkc-font-heading-weight);
|
||||
font-size: calc(0.88rem * var(--pkc-scale-ratio, 1.18));
|
||||
}
|
||||
|
||||
.dcx-kit-live .dcx-detail-article--components .dcx-demo-button,
|
||||
.dcx-kit-live .dcx-detail-article--components .dcx-demo-input {
|
||||
border-radius: var(--pkc-radius-control);
|
||||
}
|
||||
|
||||
.dcx-kit-live .dcx-detail-article--components .dcx-showcase-card {
|
||||
border-radius: var(--pkc-radius-surface);
|
||||
box-shadow: var(--pkc-shadow-card);
|
||||
}
|
||||
|
||||
.dcx-kit-live .dcx-detail-article--components :is(
|
||||
.dcx-demo-button--primary,
|
||||
.dcx-demo-button--secondary,
|
||||
.dcx-demo-button--danger
|
||||
) {
|
||||
box-shadow: var(--pkc-shadow-control);
|
||||
}
|
||||
|
||||
.dcx-kit-live .dcx-detail-article--components .dcx-demo-input {
|
||||
box-shadow: var(--pkc-shadow-surface);
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
/* Audience-led editorial treatment for the remaining Design Context screens. */
|
||||
|
||||
.dcx-detail-article {
|
||||
--dcx-detail-icon-size: 100px;
|
||||
}
|
||||
|
||||
.dcx-detail-article > .dcx-detail-hero {
|
||||
margin-bottom: clamp(48px, 6vw, 76px);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-section {
|
||||
margin-top: 0;
|
||||
margin-bottom: clamp(56px, 7vw, 88px);
|
||||
scroll-margin-top: 18px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-section-head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(80px, 150px);
|
||||
gap: clamp(24px, 4vw, 44px);
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-section-head--with-icon {
|
||||
grid-template-columns: minmax(0, 1fr) var(--dcx-detail-icon-size);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-section-head:not(.dcx-detail-section-head--with-icon)::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border-inline-end: 46px solid var(--accent);
|
||||
background: color-mix(in oklch, var(--ks-patina) 72%, transparent);
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-section-heading {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-section-title {
|
||||
margin: 0;
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.25;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-section-lede {
|
||||
max-width: 54ch;
|
||||
margin: 10px 0 0;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-section-icon {
|
||||
width: var(--dcx-detail-icon-size);
|
||||
height: var(--dcx-detail-icon-size);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-section-icon img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-section-body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article :is(
|
||||
.dcx-callout,
|
||||
.dcx-voice-cell,
|
||||
.dcx-ref-card,
|
||||
.dcx-pick,
|
||||
.dcx-pair-card,
|
||||
.dcx-proof,
|
||||
.dcx-cue-card,
|
||||
.dcx-empty
|
||||
) {
|
||||
border-color: transparent;
|
||||
border-radius: 8px;
|
||||
background: var(--panel-bg);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-purpose {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-purpose-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-purpose .dcx-callout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: baseline;
|
||||
column-gap: 18px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-purpose .dcx-callout > :not(.dcx-callout-name, .dcx-platform-pill) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-platform-pill {
|
||||
justify-self: end;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--ks-text-faint);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-list {
|
||||
counter-reset: dcx-detail-item;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-list li {
|
||||
counter-increment: dcx-detail-item;
|
||||
position: static;
|
||||
display: grid;
|
||||
grid-template-columns: 32px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: baseline;
|
||||
min-width: 0;
|
||||
padding: 18px 20px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: var(--panel-bg);
|
||||
color: var(--ks-text);
|
||||
font-size: 0.96rem;
|
||||
line-height: 1.6;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-list li::before {
|
||||
content: counter(dcx-detail-item, decimal-leading-zero);
|
||||
position: static;
|
||||
width: auto;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-principles,
|
||||
.dcx-detail-article .dcx-principles--cols {
|
||||
counter-reset: principle;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-principles--cols {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-principles li {
|
||||
display: grid;
|
||||
grid-template-columns: 32px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
min-width: 0;
|
||||
padding: 18px 20px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: var(--panel-bg);
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.94rem;
|
||||
line-height: 1.6;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-principles li::before {
|
||||
padding-top: 0.1em;
|
||||
color: var(--accent);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-principle-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-principle-copy strong {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: var(--ks-champagne);
|
||||
font-size: 1rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-defs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-defs .dcx-def {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
padding: 18px 20px 20px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: var(--panel-bg);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-defs .dcx-def dt {
|
||||
color: var(--ks-champagne);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-defs .dcx-def dd {
|
||||
margin-top: 8px;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-voice,
|
||||
.dcx-detail-article .dcx-voice-pair {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-ref-cards,
|
||||
.dcx-detail-article .dcx-picks,
|
||||
.dcx-detail-article .dcx-pair,
|
||||
.dcx-detail-article .dcx-surfaces {
|
||||
gap: clamp(16px, 2.5vw, 26px);
|
||||
}
|
||||
|
||||
.dcx-detail-article--brand .dcx-brand-assets-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: clamp(18px, 2.5vw, 26px);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dcx-detail-article--brand .dcx-brand-asset,
|
||||
.dcx-detail-article--brand .dcx-brand-asset figure {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--brand .dcx-brand-asset figure {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--brand .dcx-brand-asset-frame {
|
||||
position: relative;
|
||||
display: grid;
|
||||
aspect-ratio: 4 / 3;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
place-items: center;
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 8px;
|
||||
background: var(--panel-bg);
|
||||
}
|
||||
|
||||
.dcx-detail-article--brand .dcx-brand-asset[data-asset-kind="logo"] .dcx-brand-asset-frame {
|
||||
background-color: var(--panel-bg);
|
||||
background-image:
|
||||
linear-gradient(to right, var(--ks-rule) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--ks-rule) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--brand .dcx-brand-asset-frame img {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dcx-detail-article--brand .dcx-brand-asset[data-asset-kind="logo"] img {
|
||||
width: 72%;
|
||||
height: 72%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.dcx-detail-article--brand .dcx-brand-asset:not([data-asset-kind="logo"]) img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.dcx-detail-article--brand .dcx-brand-asset figcaption {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--brand .dcx-brand-asset figcaption strong {
|
||||
min-width: 0;
|
||||
color: var(--ks-text);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dcx-detail-article--brand .dcx-brand-asset figcaption span {
|
||||
flex: none;
|
||||
color: var(--ks-text-faint);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.14em;
|
||||
line-height: 1.45;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-ref-card,
|
||||
.dcx-detail-article .dcx-pick,
|
||||
.dcx-detail-article .dcx-pair-card {
|
||||
padding: clamp(18px, 2.5vw, 26px);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-pick {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(7rem, 0.65fr) minmax(0, 1.35fr);
|
||||
grid-template-areas:
|
||||
"surface title"
|
||||
"surface description";
|
||||
align-content: start;
|
||||
column-gap: clamp(18px, 2.5vw, 28px);
|
||||
row-gap: 7px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-pick--proof {
|
||||
grid-template-areas:
|
||||
"surface title"
|
||||
"surface description"
|
||||
"proof proof";
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-pick--proof > .dcx-proof {
|
||||
grid-area: proof;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-pick-surface {
|
||||
grid-area: surface;
|
||||
align-self: start;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-pick-title {
|
||||
grid-area: title;
|
||||
font-family: var(--ks-font);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-pick-desc {
|
||||
grid-area: description;
|
||||
color: var(--ks-text);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.58;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
@media (width <= 620px) {
|
||||
.dcx-detail-article .dcx-pick {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-areas:
|
||||
"surface"
|
||||
"title"
|
||||
"description";
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-pick--proof {
|
||||
grid-template-areas:
|
||||
"surface"
|
||||
"title"
|
||||
"description"
|
||||
"proof";
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-pick-surface {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-surface-card {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-surface-copy {
|
||||
padding: 0 clamp(14px, 2vw, 22px) 4px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-surface-name {
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--board,
|
||||
.dcx-detail-article .dcx-cue-frame {
|
||||
overflow: hidden;
|
||||
border-color: transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-cue {
|
||||
gap: clamp(16px, 2.5vw, 26px);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-cue-strip {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-fan {
|
||||
overflow: hidden;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--color .dcx-cue {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.dcx-detail-article--color .dcx-cue-card {
|
||||
container-name: dcx-cue-palette;
|
||||
container-type: inline-size;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
gap: 0;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: var(--panel-bg);
|
||||
}
|
||||
|
||||
.dcx-detail-article--color .dcx-cue-bands {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--ks-rule);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dcx-detail-article--color .dcx-cue-band-item {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr) 46px;
|
||||
min-width: 0;
|
||||
background: var(--panel-bg);
|
||||
}
|
||||
|
||||
.dcx-detail-article--color .dcx-cue-band {
|
||||
background: var(--band-color);
|
||||
color: var(--band-ink);
|
||||
min-width: 0;
|
||||
min-height: 175px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dcx-detail-article--color .dcx-cue-band-value {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 14px;
|
||||
left: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-family: var(--ks-mono);
|
||||
font-size: clamp(0.58rem, 0.85vw, 0.7rem);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dcx-detail-article--color .dcx-cue-band-label {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-width: 0;
|
||||
padding: 0 6px;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--ks-champagne);
|
||||
font-size: clamp(0.76rem, 1.1vw, 0.92rem);
|
||||
font-weight: 500;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@container dcx-cue-palette (max-width: 439px) {
|
||||
.dcx-detail-article--color .dcx-cue-bands {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.dcx-detail-article--color .dcx-cue-band {
|
||||
min-height: 124px;
|
||||
}
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-swatches {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-swatch {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--panel-bg);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-swatch-chip {
|
||||
min-height: 140px;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-swatch-meta h4 {
|
||||
margin: 0 0 6px;
|
||||
color: var(--ks-text);
|
||||
font-family: var(--ks-font-display);
|
||||
font-size: 1.3rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-pair-name {
|
||||
color: var(--ks-champagne);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--scale,
|
||||
.dcx-detail-article .dcx-proof--specimen,
|
||||
.dcx-detail-article .dcx-proof--icons {
|
||||
padding: clamp(18px, 2.5vw, 28px);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--scale > .picker-scale-sheet {
|
||||
row-gap: 0;
|
||||
padding-block: clamp(36px, 4vw, 52px);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--scale .ts-row {
|
||||
padding-block: clamp(14px, 2vw, 22px);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--specimen {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--specimen > .picker-scale-specimen {
|
||||
gap: clamp(28px, 3vw, 36px);
|
||||
padding-block: clamp(36px, 4vw, 52px);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--specimen .sp-h1 {
|
||||
font-size: clamp(2.5rem, 4.25vw, 3.25rem);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--specimen .sp-h2 {
|
||||
font-size: clamp(2rem, 3vw, 2.5rem);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--specimen .sp-h3 {
|
||||
font-size: 1.65rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--specimen .sp-h4 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--specimen .sp-lede {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--specimen :is(.sp-p, .sp-list) {
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--specimen .sp-quote p {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--specimen .sp-cite,
|
||||
.dcx-detail-article .dcx-proof--specimen :is(.sp-code, .sp-pre) {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-proof--specimen .sp-pre {
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.dcx-detail-article .sp-pre {
|
||||
max-width: 100%;
|
||||
overflow-x: visible;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.dcx-detail-article .sp-pre code {
|
||||
white-space: inherit;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-fan-note,
|
||||
.dcx-detail-article .dcx-default-note {
|
||||
max-width: 72ch;
|
||||
margin-top: 12px;
|
||||
color: var(--ks-text-faint);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.dcx-detail-article .dcx-principles--cols,
|
||||
.dcx-detail-article .dcx-detail-defs,
|
||||
.dcx-detail-article--brand .dcx-brand-assets-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-swatch {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.dcx-detail-article .dcx-detail-section-head {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-section-head:not(.dcx-detail-section-head--with-icon) {
|
||||
grid-template-columns: minmax(0, 1fr) 72px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-purpose {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.dcx-detail-article .dcx-detail-section-head {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-section-title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-list li,
|
||||
.dcx-detail-article .dcx-principles li {
|
||||
grid-template-columns: 26px minmax(0, 1fr);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--color .dcx-cue-band-value {
|
||||
right: 8px;
|
||||
bottom: 12px;
|
||||
left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dcx-detail-article *,
|
||||
.dcx-detail-article *::before,
|
||||
.dcx-detail-article *::after {
|
||||
transition-duration: 0.001ms !important;
|
||||
transition-delay: 0ms !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/* Continuous expanded document. The opening category grid is intentionally untouched. */
|
||||
|
||||
.dcx-main[data-dcx-continuous="true"] {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.dcx-document-title {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.dcx-main[data-dcx-continuous="true"] > .dcx-document-article {
|
||||
box-sizing: border-box;
|
||||
scroll-margin-top: 18px;
|
||||
}
|
||||
|
||||
.dcx-main[data-dcx-continuous="true"] > .dcx-document-article + .dcx-document-article {
|
||||
border-top: 1px solid var(--ks-rule);
|
||||
margin-top: clamp(48px, 6vw, 80px);
|
||||
padding-top: clamp(36px, 5vw, 64px);
|
||||
}
|
||||
|
||||
.dcx-expander[data-dcx-document="true"] .dcx-sub-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dcx-expander[data-dcx-document="true"] .dcx-nav-link[aria-expanded="true"] {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.dcx-main[data-dcx-continuous="true"] > .dcx-document-article + .dcx-document-article {
|
||||
margin-top: 42px;
|
||||
padding-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dcx-main[data-dcx-continuous="true"] {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/* Shared information hierarchy for every Design Context article.
|
||||
Labels identify a field; values carry the decision; supporting copy explains it. */
|
||||
|
||||
.dcx-expander {
|
||||
--dcx-hierarchy-title: var(--ks-champagne);
|
||||
--dcx-hierarchy-value: var(--ks-text);
|
||||
--dcx-hierarchy-support: var(--ks-text-muted);
|
||||
--dcx-hierarchy-label: var(--ks-text-faint);
|
||||
--dcx-hierarchy-label-size: 0.68rem;
|
||||
--dcx-hierarchy-value-size: 0.96rem;
|
||||
}
|
||||
|
||||
/* Definition lists: labels recede, committed values lead. */
|
||||
.dcx-detail-article .dcx-detail-defs .dcx-def dt,
|
||||
.dcx-audience .dcx-audience-section--people .dcx-def dt {
|
||||
color: var(--dcx-hierarchy-label);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: var(--dcx-hierarchy-label-size);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
line-height: 1.45;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-defs .dcx-def dd,
|
||||
.dcx-audience .dcx-audience-section--people .dcx-def dd {
|
||||
margin-top: 9px;
|
||||
color: var(--dcx-hierarchy-value);
|
||||
font-size: var(--dcx-hierarchy-value-size);
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-detail-defs .dcx-def dd strong {
|
||||
color: inherit;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* The prose inside a decision component is content, not metadata. */
|
||||
.dcx-detail-article .dcx-callout > p:not(.dcx-callout-name),
|
||||
.dcx-audience .dcx-callout > p:not(.dcx-callout-name),
|
||||
.dcx-detail-article .dcx-principles li,
|
||||
.dcx-detail-article .dcx-voice-cell > p,
|
||||
.dcx-detail-article .dcx-ref-takeaway,
|
||||
.dcx-detail-article .dcx-surface-goal,
|
||||
.dcx-detail-article .dcx-swatch-meta > p,
|
||||
.dcx-detail-article .dcx-scale-head,
|
||||
.dcx-detail-article .dcx-pick-desc,
|
||||
.dcx-detail-article .dcx-asset-caption > p,
|
||||
.dcx-detail-article .dcx-prose {
|
||||
color: var(--dcx-hierarchy-value);
|
||||
}
|
||||
|
||||
/* These callout headings describe the field; the following paragraph is the answer. */
|
||||
.dcx-audience .dcx-audience-journey .dcx-callout-name,
|
||||
.dcx-detail-article .dcx-detail-section--positioning .dcx-callout-name,
|
||||
.dcx-detail-article .dcx-detail-section--primary-conversion .dcx-callout-name,
|
||||
.dcx-detail-article .dcx-detail-section--anti-reference .dcx-callout-name {
|
||||
margin-bottom: 9px;
|
||||
color: var(--dcx-hierarchy-label);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: var(--dcx-hierarchy-label-size);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
line-height: 1.45;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dcx-audience .dcx-audience-journey .dcx-callout-name + p,
|
||||
.dcx-detail-article .dcx-detail-section--positioning .dcx-callout-name + p,
|
||||
.dcx-detail-article .dcx-detail-section--primary-conversion .dcx-callout-name + p,
|
||||
.dcx-detail-article .dcx-detail-section--anti-reference .dcx-callout-name + p {
|
||||
color: var(--dcx-hierarchy-value);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Preserve prominent answer titles inside components. */
|
||||
.dcx-detail-article :is(
|
||||
.dcx-detail-principle-copy strong,
|
||||
.dcx-ref-name,
|
||||
.dcx-surface-name,
|
||||
.dcx-pair-name,
|
||||
.dcx-pick-title,
|
||||
.dcx-empty-title
|
||||
) {
|
||||
color: var(--dcx-hierarchy-title);
|
||||
}
|
||||
|
||||
.dcx-detail-article .dcx-ref-name {
|
||||
font-family: var(--ks-font);
|
||||
}
|
||||
|
||||
/* In the palette, the role is the label and the hex is the committed value. */
|
||||
.dcx-detail-article--color .dcx-cue-band-label {
|
||||
color: var(--dcx-hierarchy-label);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: var(--dcx-hierarchy-label-size);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dcx-detail-article--color .dcx-cue-band-value {
|
||||
font-size: clamp(0.7rem, 1vw, 0.78rem);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Type-scale rows follow the same label / value / supporting-value order. */
|
||||
.dcx-detail-article--typography .ts-sample {
|
||||
color: var(--ks-champagne) !important;
|
||||
}
|
||||
|
||||
.dcx-detail-article--typography .ts-tag {
|
||||
color: var(--dcx-hierarchy-label);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 16px !important;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.dcx-detail-article--typography .ts-values {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--typography .ts-values b {
|
||||
color: var(--dcx-hierarchy-value);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dcx-detail-article--typography .ts-values i {
|
||||
color: var(--dcx-hierarchy-support);
|
||||
}
|
||||
@@ -0,0 +1,707 @@
|
||||
/* Quiet, readable Hooks controls for the continuous Design Context document. */
|
||||
|
||||
.dcx-detail-article--hooks {
|
||||
--dcx-hooks-rule: color-mix(in oklch, var(--ks-text) 18%, transparent);
|
||||
--dcx-hooks-rule-strong: color-mix(in oklch, var(--ks-text) 32%, transparent);
|
||||
--dcx-hooks-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-detail-section {
|
||||
margin-bottom: clamp(72px, 8vw, 104px);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-detail-section-head {
|
||||
margin-bottom: clamp(24px, 3vw, 32px);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-detail-section-lede {
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-definition {
|
||||
max-width: 58ch;
|
||||
margin: 0;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 1rem;
|
||||
line-height: 1.72;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 28px;
|
||||
margin-top: 24px;
|
||||
padding: 20px 0;
|
||||
border-block: 1px solid var(--dcx-hooks-rule);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-status-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-status strong {
|
||||
display: block;
|
||||
color: var(--ks-champagne);
|
||||
font: 600 1rem/1.4 var(--ks-font);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-status p {
|
||||
margin: 5px 0 0;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-status-control {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-status-state {
|
||||
min-width: 22px;
|
||||
color: var(--ks-text);
|
||||
font: 500 0.7rem/1 var(--ks-mono);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-status.is-paused .dcx-hooks-status-state {
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-flow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-flow > div {
|
||||
min-width: 0;
|
||||
padding: 20px 0 2px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-flow > div + div {
|
||||
padding-left: 28px;
|
||||
border-left: 1px solid var(--dcx-hooks-rule);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-flow dt,
|
||||
.dcx-detail-article--hooks .dcx-hooks-search > span,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form label > span,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-discipline {
|
||||
color: var(--ks-text-muted);
|
||||
font: 500 0.68rem/1.4 var(--ks-mono);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-flow dd {
|
||||
margin: 7px 0 0;
|
||||
color: var(--ks-text);
|
||||
font-size: 0.94rem;
|
||||
line-height: 1.58;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-switch {
|
||||
position: relative;
|
||||
display: inline-grid;
|
||||
flex: 0 0 auto;
|
||||
width: 46px;
|
||||
height: 44px;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-switch input {
|
||||
position: absolute;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-switch > span {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 38px;
|
||||
height: 20px;
|
||||
border: 1px solid var(--dcx-hooks-rule-strong);
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
transition: border-color 180ms ease, background-color 180ms ease;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-switch > span::after {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--ks-text-muted);
|
||||
content: "";
|
||||
transition: transform 260ms var(--dcx-hooks-ease), background-color 180ms ease;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-switch input:checked + span {
|
||||
border-color: var(--ks-text-muted);
|
||||
background: var(--ks-graphite-2);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-switch input:checked + span::after {
|
||||
background: var(--ks-kinpaku);
|
||||
transform: translateX(18px);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-switch input:focus-visible + span {
|
||||
outline: 2px solid var(--ks-kinpaku);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-browser {
|
||||
min-width: 0;
|
||||
border-block: 1px solid var(--dcx-hooks-rule);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-families {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: clamp(24px, 4vw, 42px);
|
||||
min-width: 0;
|
||||
padding-inline: 3px;
|
||||
overflow-x: auto;
|
||||
border-bottom: 1px solid var(--dcx-hooks-rule);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-family {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
min-height: 62px;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: var(--ks-text-muted);
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-family::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
height: 1px;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-family:hover {
|
||||
color: var(--ks-patina);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-family.is-active {
|
||||
color: var(--ks-kinpaku);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-family.is-active::after {
|
||||
background: var(--ks-kinpaku);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-family:focus-visible {
|
||||
outline: 2px solid var(--ks-kinpaku);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-family-name {
|
||||
font: 600 0.92rem/1.35 var(--ks-font);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-family-count {
|
||||
color: var(--ks-text-muted);
|
||||
font: 500 0.68rem/1.35 var(--ks-mono);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-rule-panel {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-toolbar {
|
||||
display: flex;
|
||||
min-height: 68px;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 12px 0 14px;
|
||||
border-bottom: 1px solid var(--dcx-hooks-rule);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-search {
|
||||
display: grid;
|
||||
flex: 1 1 auto;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-search input,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form input,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form select,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form textarea {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--dcx-hooks-rule-strong);
|
||||
border-radius: 0;
|
||||
color: var(--ks-text);
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-search input {
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-search input::placeholder,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form input::placeholder,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form textarea::placeholder {
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-search input:focus,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form input:focus,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form select:focus,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form textarea:focus {
|
||||
border-color: var(--ks-patina);
|
||||
outline: 1px solid var(--ks-patina);
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-summary {
|
||||
flex: 0 0 auto;
|
||||
margin: 0 0 10px;
|
||||
color: var(--ks-text-muted);
|
||||
font: 500 0.68rem/1.4 var(--ks-mono);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-discipline {
|
||||
border-bottom: 1px solid var(--dcx-hooks-rule);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-discipline > summary {
|
||||
display: flex;
|
||||
min-height: 56px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
color: var(--ks-text);
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-discipline > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-discipline > summary::before {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-right: 10px;
|
||||
border-right: 1px solid currentColor;
|
||||
border-bottom: 1px solid currentColor;
|
||||
content: "";
|
||||
transform: rotate(-45deg);
|
||||
transition: transform 360ms var(--dcx-hooks-ease);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-discipline[open]:not(.is-closing) > summary::before {
|
||||
color: var(--ks-patina);
|
||||
transform: rotate(45deg) translate(-2px, -2px);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-discipline > summary > span:first-child {
|
||||
flex: 1;
|
||||
font: 600 0.94rem/1.4 var(--ks-font);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-discipline > summary > span:last-child {
|
||||
color: var(--ks-text-muted);
|
||||
font: 500 0.68rem/1.4 var(--ks-mono);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-discipline > summary:focus-visible {
|
||||
outline: 2px solid var(--ks-kinpaku);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-disclosure {
|
||||
display: block;
|
||||
overflow: clip;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-disclosure-inner {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-rules {
|
||||
margin: 0;
|
||||
padding: 0 0 10px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-rule {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 16px 0;
|
||||
border-top: 1px solid var(--dcx-hooks-rule);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-rule-copy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-rule-copy code {
|
||||
overflow: hidden;
|
||||
color: var(--ks-text-muted);
|
||||
font: 500 0.68rem/1.45 var(--ks-mono);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-rule-copy strong {
|
||||
color: var(--ks-champagne);
|
||||
font: 600 0.96rem/1.4 var(--ks-font);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-rule-copy p {
|
||||
max-width: 64ch;
|
||||
margin: 5px 0 0;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.58;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-rule > .dcx-hooks-switch {
|
||||
margin-inline: 2px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-empty {
|
||||
margin: 0;
|
||||
padding: 32px 0;
|
||||
color: var(--ks-text);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom {
|
||||
border-top: 1px solid var(--dcx-hooks-rule);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-toolbar {
|
||||
display: flex;
|
||||
min-height: 64px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
border-bottom: 1px solid var(--dcx-hooks-rule);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-toolbar p {
|
||||
margin: 0;
|
||||
color: var(--ks-text);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-button,
|
||||
.dcx-detail-article--hooks .dcx-hooks-remove {
|
||||
min-height: 42px;
|
||||
border: 1px solid var(--dcx-hooks-rule-strong);
|
||||
border-radius: 0;
|
||||
color: var(--ks-text);
|
||||
background: transparent;
|
||||
font: 600 0.82rem/1 var(--ks-font);
|
||||
cursor: pointer;
|
||||
transition: border-color 180ms ease, background-color 180ms ease, color 180ms ease;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-button {
|
||||
padding: 0 17px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-button:hover {
|
||||
border-color: var(--ks-text);
|
||||
background: color-mix(in oklch, var(--ks-text) 8%, transparent);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-button:focus-visible,
|
||||
.dcx-detail-article--hooks .dcx-hooks-remove:focus-visible {
|
||||
outline: 2px solid var(--ks-kinpaku);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.25fr) minmax(170px, 0.75fr);
|
||||
gap: 20px;
|
||||
padding: 26px 0;
|
||||
border-bottom: 1px solid var(--dcx-hooks-rule);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form label {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form input,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form select {
|
||||
height: 44px;
|
||||
padding: 0 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form textarea {
|
||||
min-height: 104px;
|
||||
padding: 11px 12px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.55;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form-description,
|
||||
.dcx-detail-article--hooks .dcx-hooks-form-actions {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-list {
|
||||
border-bottom: 1px solid var(--dcx-hooks-rule);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-list:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-rule {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 18px 0;
|
||||
border-bottom: 1px solid var(--dcx-hooks-rule);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-rule:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-discipline {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-remove {
|
||||
min-height: 36px;
|
||||
padding: 0 11px;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-storage-note {
|
||||
margin: 14px 0 0;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.dcx-detail-article--hooks .dcx-hooks-families {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-family {
|
||||
min-height: 58px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.dcx-detail-article--hooks .dcx-detail-section {
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-status {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-flow {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-flow > div {
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-flow > div + div {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-families {
|
||||
gap: 22px;
|
||||
overscroll-behavior-inline: contain;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-toolbar,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
gap: 12px;
|
||||
padding-block: 16px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-summary {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-rule,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-rule {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
padding: 22px 0;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form label,
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form-description,
|
||||
.dcx-detail-article--hooks .dcx-hooks-form-actions {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-form-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-form-actions .dcx-hooks-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dcx-detail-article--hooks .dcx-hooks-switch > span,
|
||||
.dcx-detail-article--hooks .dcx-hooks-switch > span::after,
|
||||
.dcx-detail-article--hooks .dcx-hooks-discipline > summary::before {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Live wiring: the exceptions form's kind-driven fields and the
|
||||
Apply bar. State loads from and lands in the project's
|
||||
.impeccable/config.json through the doc session; the bar shows
|
||||
only while the draft and the project disagree.
|
||||
============================================================ */
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-form label[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-kind-hint {
|
||||
grid-column: 1 / -1;
|
||||
margin: -6px 0 0;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.5;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-custom-reason {
|
||||
margin: 2px 0 0;
|
||||
font-size: 0.82rem;
|
||||
font-style: italic;
|
||||
color: var(--ks-text-faint);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-switch input:disabled + span,
|
||||
.dcx-detail-article--hooks .dcx-hooks-button:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-applybar {
|
||||
position: sticky;
|
||||
bottom: 18px;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 26px;
|
||||
padding: 14px 18px;
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 12px;
|
||||
background: var(--ks-graphite-2);
|
||||
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-applybar[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-applybar-copy {
|
||||
margin: 0;
|
||||
font-size: 0.88rem;
|
||||
color: var(--ks-text);
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-applybar-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.dcx-detail-article--hooks .dcx-hooks-applybar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dcx-detail-article--hooks .dcx-hooks-applybar-actions .dcx-hooks-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
/* Shared material rail for every Design Context category. */
|
||||
|
||||
.dcx-nav.dcx-material-rail-host {
|
||||
--dcx-rail-text-gap: 18px;
|
||||
--dcx-rail-child-indent: 14px;
|
||||
--dcx-rail-item-padding-block: 8px;
|
||||
--dcx-rail-motion-duration: 520ms;
|
||||
--dcx-rail-motion-easing: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--dcx-rail-category-duration: 520ms;
|
||||
--dcx-rail-category-easing: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--dcx-rail-branch-duration: 320ms;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.dcx-material-rail-host .dcx-nav-label,
|
||||
.dcx-material-rail-host .dcx-nav-list {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dcx-material-rail-host .dcx-nav-link {
|
||||
padding-inline-start: var(--dcx-rail-text-gap);
|
||||
border-left-color: color-mix(in oklch, var(--ks-patina) 42%, transparent);
|
||||
}
|
||||
|
||||
.dcx-material-rail-host .dcx-subnav {
|
||||
padding: 2px 0 8px var(--dcx-rail-child-indent);
|
||||
}
|
||||
|
||||
@media (min-width: 921px) {
|
||||
.dcx-expander[data-dcx-document="true"] .dcx-material-rail-host .dcx-subnav {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
max-height: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transform: translateY(-4px);
|
||||
animation: none;
|
||||
transition:
|
||||
max-height var(--dcx-rail-category-duration) var(--dcx-rail-category-easing),
|
||||
padding-top var(--dcx-rail-category-duration) var(--dcx-rail-category-easing),
|
||||
padding-bottom var(--dcx-rail-category-duration) var(--dcx-rail-category-easing),
|
||||
opacity 240ms var(--dcx-rail-category-easing),
|
||||
transform 360ms var(--dcx-rail-category-easing),
|
||||
visibility 0s linear var(--dcx-rail-category-duration);
|
||||
}
|
||||
|
||||
.dcx-expander[data-dcx-document="true"] .dcx-material-rail-host li.is-active > .dcx-subnav {
|
||||
max-height: var(--dcx-subnav-height, 24rem);
|
||||
padding-top: 2px;
|
||||
padding-bottom: 8px;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
transform: translateY(0);
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
.dcx-expander[data-dcx-document="true"] .dcx-material-rail-host li.is-active > .dcx-subnav--summary {
|
||||
gap: 0;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.dcx-material-rail-host .dcx-sub-link {
|
||||
padding: var(--dcx-rail-item-padding-block) 0;
|
||||
padding-inline-start: var(--dcx-rail-text-gap);
|
||||
border: 0;
|
||||
border-left: 2px solid color-mix(in oklch, var(--ks-patina) 42%, transparent);
|
||||
line-height: 1.45;
|
||||
transition:
|
||||
color 160ms var(--ks-ease),
|
||||
border-color 160ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.dcx-material-rail-host .dcx-sub-link[aria-current="location"] {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dcx-material-rail-host .dcx-subnav--summary {
|
||||
gap: 0;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.dcx-material-rail-host .dcx-subnav--summary .dcx-sub-link {
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dcx-material-rail-host .dcx-subnav--summary .dcx-sub-link[aria-current="location"] {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.dcx-material-rail {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dcx-material-rail__patina-base {
|
||||
fill: var(--ks-patina);
|
||||
}
|
||||
|
||||
.dcx-material-rail__gold-base {
|
||||
fill: var(--ks-kinpaku);
|
||||
}
|
||||
|
||||
.dcx-material-rail__patina-image {
|
||||
opacity: 0.68;
|
||||
filter: saturate(1.18) contrast(1.06);
|
||||
}
|
||||
|
||||
.dcx-material-rail__gold-image {
|
||||
opacity: 0.7;
|
||||
filter: saturate(1.15) contrast(1.08);
|
||||
}
|
||||
|
||||
.dcx-material-rail__track,
|
||||
.dcx-material-rail__active,
|
||||
.dcx-material-rail__transition-track,
|
||||
.dcx-material-rail__transition-active {
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.dcx-material-rail__track {
|
||||
stroke: var(--ks-patina);
|
||||
stroke: url("#dcx-material-rail-patina") var(--ks-patina);
|
||||
opacity: 0;
|
||||
transition: opacity var(--dcx-rail-category-duration) var(--dcx-rail-category-easing);
|
||||
}
|
||||
|
||||
.dcx-material-rail-host[data-rail-ready] .dcx-material-rail__track {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.dcx-material-rail__track[data-entering="true"] {
|
||||
opacity: 0 !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.dcx-material-rail__active {
|
||||
stroke: var(--accent);
|
||||
stroke: url("#dcx-material-rail-gold") var(--accent);
|
||||
opacity: 0;
|
||||
transition:
|
||||
stroke-dasharray var(--dcx-rail-motion-duration) var(--dcx-rail-motion-easing),
|
||||
stroke-dashoffset var(--dcx-rail-motion-duration) var(--dcx-rail-motion-easing),
|
||||
opacity 180ms ease;
|
||||
}
|
||||
|
||||
.dcx-material-rail__active[data-ready="true"] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dcx-material-rail__active[data-initial="true"] {
|
||||
transition-property: opacity;
|
||||
}
|
||||
|
||||
.dcx-material-rail__active[data-entering="true"] {
|
||||
opacity: 0 !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.dcx-material-rail__transition-track {
|
||||
opacity: 0;
|
||||
transition: opacity var(--dcx-rail-branch-duration) var(--dcx-rail-category-easing);
|
||||
}
|
||||
|
||||
.dcx-material-rail__transition-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dcx-material-rail__transition-track[data-visible="true"] {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.dcx-material-rail__transition-active[data-visible="true"] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dcx-material-rail__transition-track[data-initial="true"],
|
||||
.dcx-material-rail__transition-active[data-initial="true"] {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.dcx-material-rail__transition-active {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
transition:
|
||||
transform var(--dcx-rail-category-duration) var(--dcx-rail-category-easing),
|
||||
opacity 180ms ease;
|
||||
}
|
||||
|
||||
.dcx-material-rail__track[data-morphing="true"] {
|
||||
opacity: 0.58 !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.dcx-material-rail__transition-active[data-settling="true"] {
|
||||
transition:
|
||||
transform var(--dcx-rail-branch-duration) var(--dcx-rail-category-easing),
|
||||
opacity 180ms ease;
|
||||
}
|
||||
|
||||
.dcx-material-rail__active[data-handoff="true"],
|
||||
.dcx-material-rail__transition-active[data-handoff="true"] {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.dcx-material-rail-host[data-rail-ready] .dcx-nav-link,
|
||||
.dcx-material-rail-host[data-rail-ready] .dcx-sub-link,
|
||||
.dcx-material-rail-host[data-rail-ready] .dcx-nav-link:hover,
|
||||
.dcx-material-rail-host[data-rail-ready] .dcx-nav-link:focus-visible,
|
||||
.dcx-material-rail-host[data-rail-ready] .dcx-sub-link:hover,
|
||||
.dcx-material-rail-host[data-rail-ready] .dcx-sub-link:focus-visible {
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.dcx-material-rail {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dcx-material-rail-host .dcx-nav-link {
|
||||
padding: 8px 10px;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.dcx-material-rail-host .dcx-subnav {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dcx-material-rail-host *,
|
||||
.dcx-material-rail-host *::before,
|
||||
.dcx-material-rail-host *::after {
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.001ms !important;
|
||||
transition-delay: 0ms !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
.dcx-topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: none;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.dcx-topbar-actions .dcx-close:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.dcx-settings-trigger,
|
||||
.dcx-settings-close {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--ks-text);
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
transition:
|
||||
color 160ms var(--ks-ease),
|
||||
border-color 160ms var(--ks-ease),
|
||||
transform 220ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.dcx-settings-trigger svg,
|
||||
.dcx-settings-close svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 1.45;
|
||||
}
|
||||
|
||||
.dcx-settings-trigger:hover,
|
||||
.dcx-settings-trigger:focus-visible,
|
||||
.dcx-settings-close:hover,
|
||||
.dcx-settings-close:focus-visible {
|
||||
border-color: var(--accent-line);
|
||||
color: var(--accent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dcx-settings-trigger:hover svg,
|
||||
.dcx-settings-trigger:focus-visible svg {
|
||||
transform: rotate(24deg);
|
||||
}
|
||||
|
||||
.dcx-settings-trigger svg {
|
||||
transition: transform 260ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.dcx-settings-close:hover,
|
||||
.dcx-settings-close:focus-visible {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.dcx-settings-trigger:focus-visible,
|
||||
.dcx-settings-close:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.dcx-settings-modal {
|
||||
width: min(680px, calc(100vw - 24px));
|
||||
max-width: none;
|
||||
max-height: calc(100svh - 24px);
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 6px;
|
||||
background: var(--ks-lacquer);
|
||||
color: var(--ks-text);
|
||||
box-shadow: 0 32px 90px oklch(0% 0 0 / 0.64);
|
||||
opacity: 0;
|
||||
transform: translateY(12px) scale(0.985);
|
||||
transition:
|
||||
opacity 180ms ease,
|
||||
transform 280ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.dcx-settings-modal::backdrop {
|
||||
background: oklch(0% 0 0 / 0);
|
||||
backdrop-filter: blur(0);
|
||||
transition:
|
||||
background-color 180ms ease,
|
||||
backdrop-filter 240ms ease;
|
||||
}
|
||||
|
||||
.dcx-settings-modal.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
.dcx-settings-modal.is-visible::backdrop {
|
||||
background: oklch(0% 0 0 / 0.76);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.dcx-settings-panel {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
padding: clamp(24px, 3vw, 30px);
|
||||
}
|
||||
|
||||
.dcx-settings-head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 24px;
|
||||
padding-bottom: clamp(20px, 2.6vw, 26px);
|
||||
}
|
||||
|
||||
.dcx-settings-head h2 {
|
||||
margin: 0;
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font);
|
||||
font-size: clamp(1.45rem, 2.6vw, 1.75rem);
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.dcx-settings-head p {
|
||||
max-width: 52ch;
|
||||
margin: 10px 0 0;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.94rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.dcx-settings-commands {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
margin: 0;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.dcx-settings-modal .dcx-command-copy {
|
||||
border-color: var(--ks-rule);
|
||||
}
|
||||
|
||||
.dcx-settings-command {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 0 0 20px;
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.dcx-settings-command-copy {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-items: baseline;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dcx-settings-command h3 {
|
||||
margin: 0;
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.dcx-settings-command p {
|
||||
margin: 0;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.dcx-settings-command:last-child {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-height: 820px) {
|
||||
.dcx-settings-panel {
|
||||
padding-block: 18px;
|
||||
}
|
||||
|
||||
.dcx-settings-head {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.dcx-settings-head p {
|
||||
margin-top: 6px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.dcx-settings-commands {
|
||||
gap: 12px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.dcx-settings-command {
|
||||
gap: 8px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.dcx-settings-command-copy {
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.dcx-topbar-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dcx-settings-modal {
|
||||
width: calc(100vw - 24px);
|
||||
max-height: calc(100svh - 24px);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dcx-settings-panel {
|
||||
padding: 22px 20px 18px;
|
||||
}
|
||||
|
||||
.dcx-settings-head {
|
||||
gap: 18px;
|
||||
padding-bottom: 22px;
|
||||
}
|
||||
|
||||
.dcx-settings-head h2 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.dcx-settings-head p {
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.dcx-settings-command {
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.dcx-settings-command-copy {
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dcx-settings-modal,
|
||||
.dcx-settings-modal::backdrop,
|
||||
.dcx-settings-trigger,
|
||||
.dcx-settings-trigger svg,
|
||||
.dcx-settings-close {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
/* ============================================================
|
||||
Screen 08: boundary style.
|
||||
|
||||
What tells the reader one section has ended and the next has begun. Four
|
||||
answers, in order of how much material they spend: nothing, a line, a
|
||||
change of ground, an edge you can see.
|
||||
|
||||
Bands are mixed from the artboard's own ink and ground rather than named
|
||||
colors, so a drenched page separates with a lighter wash of its primary
|
||||
instead of turning grey.
|
||||
|
||||
The tokens moved here are section-scale, and they are the grounds and edges
|
||||
themselves. Depth works at element scale and only ever adds to what is set
|
||||
here, as a shadow or as a tonal overlay, so the two answers compose rather
|
||||
than overwrite each other.
|
||||
============================================================ */
|
||||
|
||||
/* The four answers differ in material, not in vertical rhythm: a tint, a
|
||||
hairline, an inset, an edge. None of them pads the proof band, because on the
|
||||
desktop artboard that band is a fixed grid row and the row heights belong to
|
||||
the layout answer on screen 07. Padding it was not buying air, it was
|
||||
claiming room the row did not have: the copy overflowed the padding box
|
||||
downward, which is why a tinted band read as squeezed and sat low in its own
|
||||
tint, and why the open answer quietly stretched the row and shifted every
|
||||
section under it. */
|
||||
|
||||
/* ── Open space ──────────────────────────────────────────────
|
||||
Every rule off and every ground the same, so the only thing left holding
|
||||
the sections apart is the air between them. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="open-space"]) .picker-preview-boundaries,
|
||||
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="open-space"]:checked)) [data-carry] {
|
||||
--pvs-divider-w: 0px;
|
||||
--pvs-band: transparent;
|
||||
--pvs-band-alt: transparent;
|
||||
--pvs-band-top: transparent;
|
||||
--pvs-panel: transparent;
|
||||
--pvs-panel-edge-w: 0px;
|
||||
--pvs-panel-pad: 0px;
|
||||
--pvs-proof-inset: 0px;
|
||||
}
|
||||
|
||||
/* ── Thin dividers ───────────────────────────────────────────
|
||||
One hairline per boundary and nothing else: at the nav, above and below
|
||||
the proof band, between its columns, and over the footer. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-preview-boundaries,
|
||||
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked)) [data-carry] {
|
||||
--pvs-divider-w: 1px;
|
||||
--pvs-divider-c: color-mix(in oklab, var(--pvs-ink) 24%, var(--pvs-ground));
|
||||
--pvs-band: transparent;
|
||||
--pvs-band-alt: transparent;
|
||||
--pvs-band-top: transparent;
|
||||
--pvs-panel: transparent;
|
||||
--pvs-panel-edge-w: 0px;
|
||||
--pvs-panel-pad: 0px;
|
||||
--pvs-proof-inset: 0px;
|
||||
}
|
||||
|
||||
/* ── Surface changes ─────────────────────────────────────────
|
||||
No lines at all. Chrome sits on one tone, the proof band on another, and
|
||||
the boundary is wherever the ground changes under your eye. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-preview-boundaries,
|
||||
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked)) [data-carry] {
|
||||
--pvs-divider-w: 0px;
|
||||
--pvs-band: color-mix(in oklab, var(--pvs-ink) 7%, var(--pvs-ground));
|
||||
--pvs-band-alt: color-mix(in oklab, var(--pvs-ink) 13%, var(--pvs-ground));
|
||||
--pvs-band-top: color-mix(in oklab, var(--pvs-ink) 7%, var(--pvs-ground));
|
||||
--pvs-panel: transparent;
|
||||
--pvs-panel-edge-w: 0px;
|
||||
--pvs-panel-pad: 0px;
|
||||
--pvs-proof-inset: 0px;
|
||||
}
|
||||
|
||||
/* ── Cards and panels ────────────────────────────────────────
|
||||
Content moves inside containers. The proof band pulls in off the page
|
||||
margins and grows an edge, the cards do the same, and the page reads as a
|
||||
set of objects on a ground rather than a run of sections. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-preview-boundaries,
|
||||
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] {
|
||||
--pvs-divider-w: 0px;
|
||||
--pvs-band: transparent;
|
||||
--pvs-band-alt: color-mix(in oklab, var(--pvs-ink) 5%, var(--pvs-ground));
|
||||
--pvs-band-top: transparent;
|
||||
--pvs-panel: color-mix(in oklab, var(--pvs-ink) 5%, var(--pvs-ground));
|
||||
--pvs-panel-edge: color-mix(in oklab, var(--pvs-ink) 20%, var(--pvs-ground));
|
||||
--pvs-panel-edge-w: 1px;
|
||||
--pvs-panel-pad: 5%;
|
||||
--pvs-proof-inset: var(--pvs-gutter);
|
||||
}
|
||||
|
||||
/* The proof band is the one section that can become a container without the
|
||||
page being restructured, so it carries the difference between a tinted
|
||||
band and a panel: the inset pulls it off the page margins and the edge
|
||||
only exists where an inset was asked for. */
|
||||
[data-carry] .ps-desktop > .ps-proof {
|
||||
margin-inline: var(--pvs-proof-inset, 0px);
|
||||
border: var(--pvs-panel-edge-w) solid var(--pvs-panel-edge);
|
||||
border-radius: var(--pvs-radius-surface);
|
||||
}
|
||||
|
||||
/* The handset's chrome carries its own tint in the base artboard, which is a
|
||||
boundary the open answers did not ask for. Both ends take the band. */
|
||||
[data-carry] .ps-phone-top,
|
||||
[data-carry] .ps-phone-footer {
|
||||
background-color: var(--pvs-band-top, var(--pvs-surface));
|
||||
}
|
||||
|
||||
/* Percentage padding resolves against the card's own width, so one value
|
||||
holds at both artboard sizes instead of swamping the handset. */
|
||||
[data-carry] .ps-gallery-item {
|
||||
padding: var(--pvs-panel-pad, 0px);
|
||||
}
|
||||
|
||||
/* The handset's card row is already the block closest to the footer, so it
|
||||
takes a shorter version of the same padding rather than growing into it. */
|
||||
[data-carry] .ps-phone-body .ps-gallery-item {
|
||||
padding: calc(var(--pvs-panel-pad, 0px) * 0.5);
|
||||
}
|
||||
|
||||
/* ── The three region bodies ──────────────────────────────────
|
||||
A shell, a document, and an index are separated in different places from a
|
||||
landing page, so the same four answers are spent on different parts. What is
|
||||
shared is the material: the answers above set a hairline, a ground, or an edge,
|
||||
and each body below reads the one it has a use for.
|
||||
|
||||
The dividing rules on these bodies were hardcoded hairlines, which is the
|
||||
thing this screen is asking about. A shell with a permanent rule down its rail
|
||||
answers Open space with a line still on screen. */
|
||||
[data-carry] :is(.ps-ops-rail, .ps-ops-main, .ps-docs-rail) {
|
||||
border-right: var(--pvs-divider-w) solid var(--pvs-divider-c);
|
||||
}
|
||||
|
||||
/* A table is a run of sections stacked, so the hairline answer rules it. The
|
||||
plot's own baseline is not in here: an axis is part of the chart's drawing and
|
||||
not a boundary between two things. */
|
||||
[data-carry] .ps-ops-table {
|
||||
border-top: var(--pvs-divider-w) solid var(--pvs-divider-c);
|
||||
}
|
||||
|
||||
[data-carry] .ps-ops-row + .ps-ops-row {
|
||||
border-top: var(--pvs-divider-w) solid var(--pvs-divider-c);
|
||||
}
|
||||
|
||||
[data-carry] .ps-index-row + .ps-index-row,
|
||||
[data-carry] .ps-index-rail {
|
||||
border-top: var(--pvs-divider-w) solid var(--pvs-divider-c);
|
||||
}
|
||||
|
||||
[data-carry] :is(.ps-index-row + .ps-index-row, .ps-index-rail) {
|
||||
padding-top: calc(var(--pvs-divider-w) * 8);
|
||||
}
|
||||
|
||||
/* Grounds. Each one fills its own region and stops at the page margin rather than
|
||||
bleeding to the frame edge. That is the same claim the layout screen's header
|
||||
makes about these boards: their regions sit on the page grid, so the rail's
|
||||
ground starts where the rail starts, on the first column line. Reaching the
|
||||
edge would need the margin restated as a share of a different box, and the two
|
||||
percentages do not resolve against the same width, so the tone would land a
|
||||
point or two off the very line the board is drawn to check. */
|
||||
[data-carry] :is(.ps-ops-rail, .ps-ops-panel, .ps-docs-rail) {
|
||||
background-color: var(--pvs-band);
|
||||
}
|
||||
|
||||
/* Alternating grounds are what a run of entries has instead of bands: the second
|
||||
entry sits on a different tone from the first, and the boundary is where the
|
||||
tone changes. */
|
||||
[data-carry] .ps-index-row--flip {
|
||||
background-color: var(--pvs-band-alt);
|
||||
}
|
||||
|
||||
/* A table's heading row is the one place on a shell where a change of ground
|
||||
does the whole job a rule would have done. */
|
||||
[data-carry] .ps-ops-row--head {
|
||||
background-color: var(--pvs-band-alt);
|
||||
}
|
||||
|
||||
/* The callout is the document's only container, so it is where this screen's
|
||||
answer lands. Its accent edge is not part of the answer: that is what marks
|
||||
the block as a callout, and it stays under all three. */
|
||||
[data-carry] .ps-docs-note {
|
||||
padding: calc(var(--pt-base) * 0.7) calc(var(--pt-base) * 0.9);
|
||||
background-color: var(--pvs-band-alt);
|
||||
border-left: calc(var(--pt-base) * 0.25) solid var(--pvs-accent-c);
|
||||
}
|
||||
|
||||
/* Cards, on a tool. Offered to Persuade and Operate only, so a shell is the one
|
||||
region body that draws it: the chart, the table, and the side panel become
|
||||
objects on a ground, and the rules that had been dividing them go, because an
|
||||
edge and a hairline saying the same thing twice is the fault the answer after
|
||||
Open space is meant to avoid. */
|
||||
[data-carry] :is(.ps-ops-chart, .ps-ops-table, .ps-ops-panel) {
|
||||
background-color: var(--pvs-panel);
|
||||
border: var(--pvs-panel-edge-w) solid var(--pvs-panel-edge);
|
||||
}
|
||||
|
||||
[data-carry] :is(.ps-ops-chart, .ps-ops-table) {
|
||||
padding: var(--pvs-panel-pad, 0px);
|
||||
}
|
||||
|
||||
/* An open rail plate is a state and reads as one already, so under the card
|
||||
answer the rest of the rail joins it rather than the plate growing an edge. */
|
||||
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] .ps-ops-item,
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-preview-boundaries .ps-ops-item {
|
||||
background-color: var(--pvs-panel);
|
||||
}
|
||||
|
||||
/* Gallery parity (plan-18): the persuade artboard kept on the boundaries and
|
||||
corners screens flattens its shadows and re-grids the proof strip, ported
|
||||
from the gallery's previews-38-41-proof-spacing block. Scoped to the two
|
||||
screens' own stages; document proofs and hub clones keep carried shadows
|
||||
through the dcx mirrors. */
|
||||
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"]) .picker-board-stage > [data-artboard][data-surface="persuade"] :is(*, *::before, *::after) {
|
||||
box-shadow: none !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"]) .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-desktop .ps-proof {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
|
||||
}
|
||||
|
||||
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"]) .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-phone .ps-proof {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
||||
position: relative;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"]) .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-phone .ps-editorial-copy {
|
||||
margin-top: 16px !important;
|
||||
}
|
||||
|
||||
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"]) .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-desktop .ps-proof-item {
|
||||
padding-right: 24px !important;
|
||||
}
|
||||
|
||||
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"]) .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-proof-divider {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Cells 40 and 41 only: band washes under surface-changes, and both band
|
||||
rules under cards-and-panels, exactly the gallery's cellNum conditionals. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-proof,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-proof,
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-proof,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-proof {
|
||||
background-color: color-mix(in oklab, var(--pvs-band-alt) 65%, white) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-footer,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-footer,
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-footer,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-footer {
|
||||
background-color: color-mix(in oklab, var(--pvs-band) 65%, white) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-phone .ps-proof,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-phone .ps-proof {
|
||||
column-gap: 16px !important;
|
||||
padding-right: var(--pvs-gutter) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-gallery-item,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > [data-artboard][data-surface="persuade"] .ps-gallery-item {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* ==== Derived stage (plan-18): screens 08, 09, 10 mount strategy-preview
|
||||
clones for the surfaces the gallery derives. Geometry mirrors the artboard
|
||||
footprint; colors are locked to the restrained base for operate, read and
|
||||
experience clones, matching the gallery's baseCellId map. ==== */
|
||||
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview {
|
||||
grid-row: 2;
|
||||
aspect-ratio: 1.865 / 1;
|
||||
width: 100%;
|
||||
--pv-primary: var(--pkc-primary, var(--ks-champagne));
|
||||
--pv-secondary: var(--pkc-secondary, var(--ks-patina));
|
||||
--pv-tertiary: var(--pkc-tertiary, var(--ks-kinpaku));
|
||||
--pv-neutral: var(--pkc-neutral, var(--ks-lacquer-raised));
|
||||
--pv-n-ink: var(--pkc-n-ink, var(--ks-champagne));
|
||||
--pv-p-ink: var(--pkc-p-ink, var(--ks-champagne));
|
||||
--derived-rule: color-mix(in oklab, var(--pv-strong) 24%, var(--pv-neutral));
|
||||
--derived-band: color-mix(in oklab, var(--pv-strong) 7%, var(--pv-neutral));
|
||||
--derived-band-alt: color-mix(in oklab, var(--pv-strong) 13%, var(--pv-neutral));
|
||||
--derived-panel: color-mix(in oklab, var(--pv-strong) 5%, var(--pv-neutral));
|
||||
--derived-panel-edge: color-mix(in oklab, var(--pv-strong) 20%, var(--pv-neutral));
|
||||
}
|
||||
|
||||
/* Restrained lock for the non-persuade clones, mirrored from the strategy
|
||||
remap so the derived boards hold the gallery's base regardless of the
|
||||
run's strategy answer. */
|
||||
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview:not([data-surface="persuade"]) {
|
||||
--pv-secondary: color-mix(in oklab, var(--pkc-neutral) 78%, var(--pkc-n-ink));
|
||||
--pv-tertiary: color-mix(in oklab, var(--pkc-neutral) 86%, var(--pkc-n-ink));
|
||||
--pv-t-ink: var(--pkc-n-ink);
|
||||
--pv-t-on-t: var(--pkc-n-ink);
|
||||
--pv-t-on-n: color-mix(in oklab, var(--pkc-n-ink) 62%, var(--pkc-neutral));
|
||||
--pv-t-on-p: var(--pkc-p-ink);
|
||||
}
|
||||
|
||||
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview.picker-preview--docs {
|
||||
--pd-quote: var(--pd-ink);
|
||||
}
|
||||
|
||||
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview.picker-preview--docs :is(.pd-note-dot, .pd-note-lines i) {
|
||||
background: var(--pd-ink);
|
||||
}
|
||||
|
||||
/* Wireframe weights. The gallery derives these boards from the strategy
|
||||
stage, so the ops, docs and portfolio clones carry that stage's 1.35 desktop
|
||||
step and nothing else; their bar geometry stays the authored base. */
|
||||
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview:is(.picker-preview--ops, .picker-preview--docs, .picker-preview--gallery) .pv-desktop {
|
||||
--pv-text: 1.35;
|
||||
}
|
||||
|
||||
/* Boundaries read (gallery cells 45 to 47) is the one derived group the
|
||||
gallery reweights: both viewports step to 1.65 and the rail dot and note dot
|
||||
widen. Corners read and depth read draw at the base weight. */
|
||||
.picker-screen[data-screen="08"] .picker-board-stage > .picker-preview.picker-preview--docs :is(.pv-desktop, .pv-phone) {
|
||||
--pv-text: 1.65;
|
||||
}
|
||||
|
||||
.picker-screen[data-screen="08"] .picker-board-stage > .picker-preview.picker-preview--docs .pd-item i { width: calc(4.07 * var(--pv-x)); }
|
||||
.picker-screen[data-screen="08"] .picker-board-stage > .picker-preview.picker-preview--docs .pd-note-dot { width: calc(4.07 * var(--pv-x)); }
|
||||
|
||||
/* Experience cap avatars on derived boards, from the gallery derived sheet. */
|
||||
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview--gallery .pv-desktop .pg-cap { padding-top: 5.2cqh; gap: 2.6cqh; }
|
||||
:is(.picker-screen[data-screen="08"], .picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview--gallery .pv-desktop .pg-cap::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: calc(11.5 * var(--pv-x));
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid color-mix(in oklab, var(--pv-secondary) 55%, var(--pv-neutral));
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, color-mix(in oklab, var(--pv-secondary) 24%, var(--pv-neutral)), color-mix(in oklab, var(--pv-secondary) 46%, var(--pv-neutral)));
|
||||
}
|
||||
|
||||
/* Phone field chevron alignment on ops clones, from the gallery derived sheet
|
||||
(cells 44 and 55-58; the corners screen applies it under every option). */
|
||||
:is(.picker-screen[data-screen="09"], .picker-screen[data-screen="10"]) .picker-board-stage > .picker-preview .pv-phone .po-field > .po-chev { align-self: center; transform: translateY(-25%) rotate(45deg); }
|
||||
|
||||
/* Boundary additions on derived clones: the base geometry stays fixed; only
|
||||
separation changes. Ported from the gallery derived sheet. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="open-space"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .po-side, .po-main, .pd-side),
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="open-space"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .po-side, .po-main, .pd-side) {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="open-space"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-divider, .pg-row--flip, .pg-rail),
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="open-space"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-divider, .pg-row--flip, .pg-rail) {
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="open-space"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="open-space"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:is(.picker-preview--ops, .picker-preview--docs, .picker-preview--gallery) .pv-nav,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:is(.picker-preview--ops, .picker-preview--docs, .picker-preview--gallery) .pv-nav {
|
||||
border-bottom: 1px solid var(--derived-rule) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-side, .po-main, .pd-side),
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-side, .po-main, .pd-side) {
|
||||
border-right: 1px solid var(--derived-rule) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider {
|
||||
background: var(--derived-rule) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-row + .po-row,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-row + .po-row {
|
||||
box-shadow: inset 0 1px 0 var(--derived-rule);
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:not([data-surface="experience"]) .pg-row--flip,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:not([data-surface="experience"]) .pg-row--flip {
|
||||
box-shadow: inset 0 1px 0 var(--derived-rule);
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:not([data-surface="experience"]) .pg-rail,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:not([data-surface="experience"]) .pg-rail {
|
||||
border-top: 1px solid var(--derived-rule);
|
||||
}
|
||||
|
||||
/* Cell 49: experience thin-dividers redraws its flip row and phone image
|
||||
separations by pseudo-element rules. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-row--flip,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-row--flip {
|
||||
position: relative;
|
||||
margin-top: 10cqh !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-row--flip::before,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-row--flip::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -5cqh;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 1px;
|
||||
background: var(--derived-rule);
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-rail,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-rail,
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-rail,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-rail {
|
||||
border-top: 0 !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body > .pg-cap + .pv-image,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body > .pg-cap + .pv-image {
|
||||
margin-top: 8cqh !important;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body > .pg-cap + .pv-image::before,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body > .pg-cap + .pv-image::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -4cqh;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 1px;
|
||||
background: var(--derived-rule);
|
||||
}
|
||||
|
||||
/* Cell 50: experience surface-changes. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-row--flip,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-row--flip {
|
||||
margin-top: 5cqh !important;
|
||||
padding-block: 5cqh !important;
|
||||
box-shadow: none !important;
|
||||
background: color-mix(in oklab, var(--derived-band-alt) 60%, white 40%) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body > .pg-cap + .pv-image,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body > .pg-cap + .pv-image {
|
||||
margin-top: 8cqh !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pg-phone-body {
|
||||
background: linear-gradient(to bottom, transparent 0 50%, color-mix(in oklab, var(--derived-band-alt) 60%, white 40%) 50% 100%) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pv-phone .pv-tabbar,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="experience"] .pv-phone .pv-tabbar {
|
||||
background: color-mix(in oklab, var(--derived-band-alt) 60%, white 40%) !important;
|
||||
}
|
||||
|
||||
/* Surface changes on every derived clone. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .po-side, .po-main, .pd-side),
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .po-side, .po-main, .pd-side) {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .pv-phone-top, .pv-tabbar),
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .pv-phone-top, .pv-tabbar) {
|
||||
background: var(--derived-band) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-side, .po-panel, .pd-side),
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-side, .po-panel, .pd-side) {
|
||||
background: var(--derived-band) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:not([data-surface="experience"]) :is(.pd-note, .pg-row--flip),
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview:not([data-surface="experience"]) :is(.pd-note, .pg-row--flip) {
|
||||
background: var(--derived-band-alt) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Cards and panels. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .po-side, .po-main),
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.pv-nav, .po-side, .po-main) {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-divider {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-side, .po-chart, .po-rows, .po-panel, .po-phone-body .po-chart, .po-phone-body .po-field, .po-phone-body .po-prow),
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview :is(.po-side, .po-chart, .po-rows, .po-panel, .po-phone-body .po-chart, .po-phone-body .po-field, .po-phone-body .po-prow) {
|
||||
background: var(--derived-panel) !important;
|
||||
border: 1px solid var(--derived-panel-edge) !important;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-chart,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-chart {
|
||||
padding-inline: calc(3 * var(--pv-x));
|
||||
}
|
||||
|
||||
/* Cell 44: operate cards-and-panels details. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-desktop .po-chart,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-desktop .po-chart {
|
||||
padding: calc(3 * var(--pv-x)) calc(5 * var(--pv-x)) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-phone .po-toggle,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-phone .po-toggle {
|
||||
margin-left: auto !important;
|
||||
margin-right: 12px !important;
|
||||
padding-right: 12px !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-phone .po-prow,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-phone .po-prow {
|
||||
padding-left: 12px !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-phone .po-field > .po-chev,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview[data-surface="operate"] .pv-phone .po-field > .po-chev {
|
||||
align-self: center !important;
|
||||
transform: translateY(-25%) rotate(45deg) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-rows,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked) .picker-screen[data-screen="08"] .picker-board-stage > .picker-preview .po-rows {
|
||||
padding-inline: calc(2 * var(--pv-x));
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/* ============================================================
|
||||
Screen 09: corner style.
|
||||
|
||||
One question, four radii, and a deliberate limit: the text bars barely move
|
||||
and the dots do not move at all.
|
||||
|
||||
A bar in this wireframe stands for a line of type. Round it like a button
|
||||
and it stops reading as text and starts reading as a tag, which would make
|
||||
the pill answer look like a different page rather than the same page with
|
||||
rounder corners. So bars top out a few pixels short of their own height.
|
||||
|
||||
The dots stay circular in every answer, Sharp included. A dot is a mark
|
||||
rather than a shape with corners; squaring it changes what it is, not how
|
||||
round it is, and the Sharp page reads as sharp without it.
|
||||
============================================================ */
|
||||
|
||||
/* ── Sharp ───────────────────────────────────────────────────
|
||||
Nothing rounded. Every corner is drafted. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="sharp"]) .picker-preview-corners,
|
||||
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="sharp"]:checked)) [data-carry] {
|
||||
--pvs-radius-frame: 0px;
|
||||
--pvs-radius-frame-phone: 0px;
|
||||
--pvs-radius-surface: 0px;
|
||||
--pvs-radius-control: 0px;
|
||||
--pvs-radius-bar: 0px;
|
||||
}
|
||||
|
||||
/* ── Slightly soft ───────────────────────────────────────────
|
||||
Enough radius to take the edge off and not enough to be remarked on. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="slightly-soft"]) .picker-preview-corners,
|
||||
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="slightly-soft"]:checked)) [data-carry] {
|
||||
--pvs-radius-frame: 4px;
|
||||
--pvs-radius-frame-phone: 5px;
|
||||
--pvs-radius-surface: 3px;
|
||||
--pvs-radius-control: 3px;
|
||||
--pvs-radius-bar: 2px;
|
||||
}
|
||||
|
||||
/* ── Friendly ────────────────────────────────────────────────
|
||||
Cards, pictures, and buttons are clearly rounded, and the handset rounds
|
||||
with them. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="friendly"]) .picker-preview-corners,
|
||||
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="friendly"]:checked)) [data-carry] {
|
||||
--pvs-radius-frame: 7px;
|
||||
--pvs-radius-frame-phone: 11px;
|
||||
--pvs-radius-surface: 8px;
|
||||
--pvs-radius-control: 8px;
|
||||
--pvs-radius-bar: 3px;
|
||||
}
|
||||
|
||||
/* ── Pill-like ───────────────────────────────────────────────
|
||||
Controls go fully round. Surfaces round hard but stay rectangles, because
|
||||
a card at half its own height is no longer a card. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="pill"]) .picker-preview-corners,
|
||||
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="pill"]:checked)) [data-carry] {
|
||||
--pvs-radius-frame: 9px;
|
||||
--pvs-radius-frame-phone: 15px;
|
||||
--pvs-radius-surface: 12px;
|
||||
--pvs-radius-control: 999px;
|
||||
--pvs-radius-bar: 3px;
|
||||
}
|
||||
|
||||
/* Corner radii on the derived clones. Dots remain dots while frames,
|
||||
surfaces, controls, and text bars take the selected radius. Ported from
|
||||
the gallery derived sheet; values match the artboard radius ramp above. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="sharp"]) .picker-screen[data-screen="09"] .picker-board-stage,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="sharp"]:checked) .picker-screen[data-screen="09"] .picker-board-stage { --derived-frame-radius: 0px; --derived-phone-radius: 0px; --derived-surface-radius: 0px; --derived-control-radius: 0px; --derived-bar-radius: 0px; }
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="slightly-soft"]) .picker-screen[data-screen="09"] .picker-board-stage,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="slightly-soft"]:checked) .picker-screen[data-screen="09"] .picker-board-stage { --derived-frame-radius: 4px; --derived-phone-radius: 5px; --derived-surface-radius: 3px; --derived-control-radius: 3px; --derived-bar-radius: 2px; }
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="friendly"]) .picker-screen[data-screen="09"] .picker-board-stage,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="friendly"]:checked) .picker-screen[data-screen="09"] .picker-board-stage { --derived-frame-radius: 7px; --derived-phone-radius: 11px; --derived-surface-radius: 8px; --derived-control-radius: 8px; --derived-bar-radius: 3px; }
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="pill"]) .picker-screen[data-screen="09"] .picker-board-stage,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="pill"]:checked) .picker-screen[data-screen="09"] .picker-board-stage { --derived-frame-radius: 9px; --derived-phone-radius: 15px; --derived-surface-radius: 12px; --derived-control-radius: 999px; --derived-bar-radius: 3px; }
|
||||
|
||||
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview .pv-desktop { border-radius: var(--derived-frame-radius) !important; }
|
||||
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview .pv-phone { border-radius: var(--derived-phone-radius) !important; }
|
||||
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview :is(.pv-image, .pv-mini-card, .po-chart, .po-rows, .po-panel, .pg-row) { border-radius: var(--derived-surface-radius) !important; }
|
||||
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview .pd-note { border-radius: 0 var(--derived-surface-radius) var(--derived-surface-radius) 0 !important; }
|
||||
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview :is(.pv-pill, .pv-actions i, .po-item--on, .po-field, .po-row, .po-toggle) { border-radius: var(--derived-control-radius) !important; }
|
||||
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview .po-toggle::after { border-radius: var(--derived-control-radius) !important; }
|
||||
.picker-screen[data-screen="09"] .picker-board-stage > .picker-preview :is(.pv-nav-bars i, .pv-pill::after, .pv-eyebrow, .pv-headline i, .pv-copy-bar, .po-line, .po-cell, .po-heading, .po-label, .pd-item span, .pd-heading, .pd-para i, .pd-note-lines i, .pg-cap i, .pg-track i) { border-radius: var(--derived-bar-radius) !important; }
|
||||
@@ -0,0 +1,173 @@
|
||||
/* ============================================================
|
||||
Screen 10: depth style.
|
||||
|
||||
How far off the page an element sits. Boundaries fills section grounds;
|
||||
this file lifts individual elements, and the two answers compose instead
|
||||
of arguing.
|
||||
|
||||
Every answer here is a cast shadow. They go in the --pvs-shadow-* slots and
|
||||
need something solid under them, which is why the block at the bottom moves
|
||||
the card's lift onto the picture inside it wherever the boundary answer left
|
||||
the card transparent: a shadow around an invisible rectangle reads as a bug.
|
||||
|
||||
Shadows are sized for the artboard rather than for a real page. The frame
|
||||
is about a third of browser width, so a blur that would read as gentle at
|
||||
full size disappears here; the offsets and blurs below are already scaled.
|
||||
|
||||
They are also black rather than mixed from the artboard's ink. Ink follows
|
||||
the ground, so on a drenched page an ink shadow turns pale and the lift
|
||||
comes out as a glow. Light falls the same way whatever color the page is.
|
||||
============================================================ */
|
||||
|
||||
/* ── Flat ────────────────────────────────────────────────────
|
||||
One plane. Nothing casts. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="flat"]) .picker-preview-depth,
|
||||
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="flat"]:checked)) [data-carry] {
|
||||
--pvs-shadow-chrome: none;
|
||||
--pvs-shadow-card: none;
|
||||
--pvs-shadow-control: none;
|
||||
--pvs-shadow-surface: none;
|
||||
}
|
||||
|
||||
/* ── Soft lift ───────────────────────────────────────────────
|
||||
Cards and controls come off the page by a millimetre. Two shadows on the
|
||||
cards: a tight one for the contact edge, a wider one for the ambient. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="soft-lift"]) .picker-preview-depth,
|
||||
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="soft-lift"]:checked)) [data-carry] {
|
||||
--pvs-shadow-chrome: 0 1px 2px rgb(0 0 0 / 0.1);
|
||||
--pvs-shadow-card:
|
||||
0 1px 1px rgb(0 0 0 / 0.15),
|
||||
0 3px 7px rgb(0 0 0 / 0.14);
|
||||
--pvs-shadow-control: 0 1px 2px rgb(0 0 0 / 0.24);
|
||||
--pvs-shadow-surface: 0 2px 5px rgb(0 0 0 / 0.14);
|
||||
}
|
||||
|
||||
/* ── Floating ────────────────────────────────────────────────
|
||||
A clear gap between the panels and the page. Offsets grow faster than
|
||||
blurs, so the lift reads as height rather than as fog. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="floating"]) .picker-preview-depth,
|
||||
:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="floating"]:checked)) [data-carry] {
|
||||
--pvs-shadow-chrome: 0 2px 6px rgb(0 0 0 / 0.16);
|
||||
--pvs-shadow-card:
|
||||
0 2px 3px rgb(0 0 0 / 0.2),
|
||||
0 9px 18px rgb(0 0 0 / 0.24);
|
||||
--pvs-shadow-control: 0 3px 6px rgb(0 0 0 / 0.32);
|
||||
--pvs-shadow-surface: 0 6px 14px rgb(0 0 0 / 0.22);
|
||||
}
|
||||
|
||||
/* ── Where a cast shadow is allowed to land ──────────────────
|
||||
Only off something solid. Under the two boundary answers that give the
|
||||
card and the proof band a ground, the section itself lifts. Under the two
|
||||
that leave them transparent, the lift moves inward to the picture, which
|
||||
is the only part of the card that is a surface in its own right. */
|
||||
#picker-form:has(input[name="boundary-style"][value="cards-and-panels"]:checked) [data-carry] :is(.ps-gallery-item, .ps-desktop > .ps-proof),
|
||||
#picker-form:has(input[name="boundary-style"][value="surface-changes"]:checked) [data-carry] .ps-desktop > .ps-proof {
|
||||
box-shadow: var(--pvs-shadow-card);
|
||||
}
|
||||
|
||||
#picker-form:not(:has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] .ps-gallery-item {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#picker-form:not(:has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] .ps-gallery-item > i {
|
||||
box-shadow: var(--pvs-shadow-card);
|
||||
}
|
||||
|
||||
/* Chrome is a ground only under the surface answer, so that is the only one
|
||||
where the bar at the top of the page casts. The footers are left out: a
|
||||
footer's shadow would fall out of the frame and be clipped away, so the
|
||||
only trace of it would be a hard line. */
|
||||
#picker-form:not(:has(input[name="boundary-style"][value="surface-changes"]:checked)) [data-carry] :is(.ps-nav, .ps-phone-top) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ── The same rule, on the three region bodies ────────────────
|
||||
Each part is asked the same question the card above was: does the boundary
|
||||
answer leave a ground under it. The answer differs part by part, because a
|
||||
shell's regions, a document's callout, and an index's alternating band are
|
||||
each filled by a different one of the four answers.
|
||||
|
||||
A rail and a document's rail take a ground only where sections are told apart
|
||||
by tone, so that is the only answer they lift under. The side panel is filled
|
||||
by that answer and by the card answer, so it lifts under both. */
|
||||
#picker-form:not(:has(input[name="boundary-style"][value="surface-changes"]:checked)) [data-carry] :is(.ps-ops-rail, .ps-docs-rail) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#picker-form:not(:has(input[name="boundary-style"][value="surface-changes"]:checked)):not(:has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] .ps-ops-panel {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* The chart and the table are containers only under the card answer. Under the
|
||||
others the lift moves inward to the plot's bars, which are the one part of a
|
||||
chart that is a surface in its own right, exactly as the landing page's card
|
||||
hands its lift to the picture inside it. */
|
||||
#picker-form:not(:has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] :is(.ps-ops-chart, .ps-ops-table) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#picker-form:not(:has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] .ps-ops-plot > i {
|
||||
box-shadow: var(--pvs-shadow-card);
|
||||
}
|
||||
|
||||
/* The callout and the index's second entry are both filled by the tone answer
|
||||
alone, so both are held to it. */
|
||||
#picker-form:not(:has(input[name="boundary-style"][value="surface-changes"]:checked)) [data-carry] .ps-docs-note {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#picker-form:has(input[name="boundary-style"][value="surface-changes"]:checked) [data-carry] .ps-index-row--flip {
|
||||
box-shadow: var(--pvs-shadow-card);
|
||||
}
|
||||
|
||||
/* Depth additions on derived clones. Flat is the base; the other answers
|
||||
lift only solid, meaningful objects. Ported from the gallery derived
|
||||
sheet; shadow alphas stay literal because they are shadows, not palette
|
||||
roles. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="flat"]) .picker-screen[data-screen="10"] .picker-board-stage,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="flat"]:checked) .picker-screen[data-screen="10"] .picker-board-stage { --derived-shadow-chrome: none; --derived-shadow-card: none; --derived-shadow-control: none; --derived-shadow-surface: none; }
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="soft-lift"]) .picker-screen[data-screen="10"] .picker-board-stage,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="soft-lift"]:checked) .picker-screen[data-screen="10"] .picker-board-stage { --derived-shadow-chrome: 0 1px 2px rgb(0 0 0 / 0.08); --derived-shadow-card: 0 2px 4px rgb(0 0 0 / 0.1); --derived-shadow-control: 0 1px 2px rgb(0 0 0 / 0.1); --derived-shadow-surface: 0 2px 4px rgb(0 0 0 / 0.1); }
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="floating"]) .picker-screen[data-screen="10"] .picker-board-stage,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="floating"]:checked) .picker-screen[data-screen="10"] .picker-board-stage { --derived-shadow-chrome: 0 1px 3px rgb(0 0 0 / 0.1); --derived-shadow-card: 0 4px 8px rgb(0 0 0 / 0.14); --derived-shadow-control: 0 2px 4px rgb(0 0 0 / 0.12); --derived-shadow-surface: 0 4px 8px rgb(0 0 0 / 0.14); }
|
||||
|
||||
.picker-screen[data-screen="10"] .picker-board-stage > .picker-preview :is(.pv-nav, .pv-phone-top) { box-shadow: var(--derived-shadow-chrome) !important; }
|
||||
.picker-screen[data-screen="10"] .picker-board-stage > .picker-preview :is(.pv-pill, .pv-actions i, .po-item--on, .po-field, .po-toggle) { box-shadow: var(--derived-shadow-control) !important; }
|
||||
.picker-screen[data-screen="10"] .picker-board-stage > .picker-preview :is(.pv-image, .pv-mini-card, .pd-note) { box-shadow: var(--derived-shadow-card) !important; }
|
||||
.picker-screen[data-screen="10"] .picker-board-stage > .picker-preview .pg-cap::before { box-shadow: var(--derived-shadow-card) !important; }
|
||||
.picker-screen[data-screen="10"] .picker-board-stage > .picker-preview :is(.po-chart i, .po-side, .po-panel) { box-shadow: var(--derived-shadow-surface) !important; }
|
||||
|
||||
/* Depth persuade rode the gallery's drenched base cell unconditionally, which
|
||||
painted the board in the primary for every run. The swap now applies only
|
||||
when the run actually chose the drenched strategy; otherwise the committed
|
||||
base from the shared 08/09/10 map (boundaries.css) carries the board. */
|
||||
#picker-form:has(input[name="color-strategy"][value="drenched"]:checked) .picker-screen[data-screen="10"] .picker-board-stage > .picker-preview[data-surface="persuade"] {
|
||||
--pv-neutral: var(--pkc-primary);
|
||||
--pv-n-ink: var(--pkc-p-ink);
|
||||
--pv-primary: var(--pkc-p-ink);
|
||||
--pv-p-ink: var(--pkc-primary);
|
||||
--pv-p-on-n: var(--pkc-p-ink);
|
||||
--pv-p-on-p: var(--pkc-primary);
|
||||
--pv-t-on-n: var(--pkc-t-on-p);
|
||||
--pv-secondary: var(--pkc-secondary);
|
||||
--pv-tertiary: var(--pkc-tertiary);
|
||||
--pv-bone: color-mix(in oklab, var(--pkc-p-ink) 55%, var(--pkc-primary));
|
||||
--pv-strong: var(--pkc-p-ink);
|
||||
--pv-secondary-wash: color-mix(in oklab, var(--pkc-p-ink) 16%, var(--pkc-primary));
|
||||
}
|
||||
|
||||
#picker-form:has(input[name="color-strategy"][value="drenched"]:checked) .picker-screen[data-screen="10"] .picker-board-stage > .picker-preview[data-surface="persuade"] .pv-desktop .pv-image {
|
||||
background: linear-gradient(135deg, color-mix(in oklab, var(--pkc-p-ink) 16%, var(--pkc-primary)), color-mix(in oklab, var(--pkc-secondary) 34%, var(--pkc-primary))) !important;
|
||||
border-color: color-mix(in oklab, var(--pkc-secondary) 52%, var(--pkc-primary)) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(input[name="color-strategy"][value="drenched"]:checked) .picker-screen[data-screen="10"] .picker-board-stage > .picker-preview[data-surface="persuade"] .pv-desktop .pv-image::after { background: var(--pkc-tertiary) !important; }
|
||||
|
||||
#picker-form:has(input[name="color-strategy"][value="drenched"]:checked) .picker-screen[data-screen="10"] .picker-board-stage > .picker-preview[data-surface="persuade"] .pv-desktop .pv-actions i:last-child {
|
||||
border-color: color-mix(in oklab, var(--pkc-secondary) 68%, var(--pkc-p-ink)) !important;
|
||||
background: color-mix(in oklab, var(--pkc-secondary) 18%, var(--pkc-primary)) !important;
|
||||
}
|
||||
|
||||
#picker-form:has(input[name="color-strategy"][value="drenched"]:checked) .picker-screen[data-screen="10"] .picker-board-stage > .picker-preview[data-surface="persuade"] .pv-desktop .pv-nav-bars i:nth-child(2) {
|
||||
background: color-mix(in oklab, var(--pkc-secondary) 72%, var(--pkc-p-ink)) !important;
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* impeccable.style — Neo kinpaku tokens (page-global, single source of truth)
|
||||
*
|
||||
* Every brand-bearing color, surface, text color, hairline rule, type-scale
|
||||
* value, and motion easing used on the site lives here. New pages should read
|
||||
* these tokens via var(--ks-*) rather than hand-typing oklch values.
|
||||
*
|
||||
* Pages may override individual tokens locally when there's a documented
|
||||
* reason (e.g. .home-kinpaku bumps --ks-rule alpha for sharper UI borders
|
||||
* against bento tiles). When in doubt, do not override — match.
|
||||
*
|
||||
* Imported by design-system.css, home-kinpaku.css, and designing-kinpaku.css.
|
||||
* If a new page-level stylesheet needs these tokens, @import this file at the
|
||||
* top so the values are available at :root.
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* color-scheme follows html.light / html.dark class on <html> */
|
||||
/* ============================================================
|
||||
Brand anchors. Two anchors carry the brand — kinpaku gold for
|
||||
commitment and brand voice, verdigris patina for state and
|
||||
contrast. Everything else is supporting role.
|
||||
============================================================ */
|
||||
|
||||
/* Kinpaku gold. The rich, warm, vibrant primary. */
|
||||
--ks-kinpaku: oklch(84% 0.19 80.46); /* primary accent, wordmark, icon */
|
||||
--ks-kinpaku-vivid: oklch(87% 0.20 85); /* hover on gold fills: lit, not washed */
|
||||
--ks-kinpaku-pale: oklch(86% 0.07 84); /* pale fills, tints on dark */
|
||||
--ks-kinpaku-rich: oklch(77% 0.13 82); /* active CTA, severity-medium */
|
||||
--ks-kinpaku-deep: oklch(61% 0.085 78); /* borders against the brand */
|
||||
--ks-dark-ink: oklch(14% 0.018 95); /* foreground on gold; never theme-remapped */
|
||||
|
||||
/* Verdigris patina. The secondary accent — state, signal, selection. */
|
||||
--ks-patina: oklch(70% 0.12 188); /* secondary accent */
|
||||
--ks-patina-pale: oklch(82% 0.07 188); /* hover lift on patina */
|
||||
--ks-patina-deep: oklch(49% 0.08 188); /* deep oxide, dark variants */
|
||||
|
||||
/* Semantic foregrounds for state, selection, and interactive labels. */
|
||||
--ks-state-ink: oklch(70% 0.12 188);
|
||||
|
||||
/* Warning — vermilion. Used sparingly, only for failures and warnings. */
|
||||
--ks-vermilion: oklch(58% 0.15 35);
|
||||
|
||||
/* ============================================================
|
||||
Surfaces. Warm-black lacquer carries everything.
|
||||
============================================================ */
|
||||
--ks-lacquer: oklch(7% 0.006 95); /* page ground */
|
||||
--ks-lacquer-deep: oklch(4% 0.004 95); /* deepest inset */
|
||||
--ks-lacquer-raised: oklch(11% 0.006 95); /* raised panels, inputs */
|
||||
--ks-graphite: oklch(15% 0.008 95); /* inactive surfaces */
|
||||
--ks-graphite-2: oklch(19% 0.008 95); /* one step up from graphite */
|
||||
|
||||
/* ============================================================
|
||||
Text. The whole text ramp runs near-neutral and bright so copy,
|
||||
headings, and labels read crisp on lacquer instead of mushing
|
||||
into the warm floor. Warmth now lives only in the gold accents
|
||||
and the surfaces, not in the type. (--ks-champagne keeps its name
|
||||
for compatibility but is no longer warm.)
|
||||
============================================================ */
|
||||
--ks-champagne: oklch(91% 0 0); /* headlines, <strong> — neutral white */
|
||||
--ks-text: oklch(88% 0 0); /* body — neutral near-white */
|
||||
--ks-text-muted: oklch(72% 0 0); /* captions, meta */
|
||||
--ks-text-faint: oklch(62% 0 0); /* subdued */
|
||||
--ks-text-mute-deep: oklch(52% 0 0); /* disabled */
|
||||
|
||||
/* ============================================================
|
||||
Rules. The hairline is neutral so borders/dividers/labels don't
|
||||
carry warmth (warmth lives in the gold accents and surfaces). For a
|
||||
gold accent border, use --ks-kinpaku directly, not a readability variant.
|
||||
============================================================ */
|
||||
--ks-rule: oklch(78% 0 0 / 0.16);
|
||||
|
||||
/* ============================================================
|
||||
Code. One treatment site-wide. Inline code is a neutral graphite
|
||||
chip; blocks / CLI commands are a deep lacquer terminal with a
|
||||
neutral hairline. Gold is only for code that is itself a command
|
||||
link (--ks-code-cmd). All code rules read these — don't hand-type.
|
||||
============================================================ */
|
||||
--ks-code-fg: oklch(91% 0 0); /* inline code text */
|
||||
--ks-code-bg: var(--ks-graphite-2); /* inline code chip */
|
||||
--ks-code-radius: 3px;
|
||||
--ks-code-pad: 0.3em 0.5em; /* inline code chip padding */
|
||||
--ks-code-block-fg: oklch(86% 0 0); /* block / CLI text */
|
||||
--ks-code-block-bg: var(--ks-lacquer-deep);
|
||||
--ks-code-block-border: var(--ks-rule);
|
||||
--ks-code-block-radius: 2px;
|
||||
--ks-code-cmd: var(--ks-kinpaku); /* code that's a command link */
|
||||
|
||||
/* ============================================================
|
||||
Typography — two faces.
|
||||
- --ks-font-display: Alumni Sans. A weightable humanist grotesque;
|
||||
the hero h1 runs at a thin hairline (100) for elegance, section
|
||||
h2s at a heavier cut (300) to anchor each block.
|
||||
- --ks-font-wordmark: Alumni Sans, same face as display, held at
|
||||
weight 400 for the brand lockup.
|
||||
- --ks-font: Albert Sans. Quiet geometric humanist, used for body
|
||||
and UI text. Pairs cleanly with the Alumni Sans display without
|
||||
competing.
|
||||
All loaded via Base.astro's Google Fonts link.
|
||||
============================================================ */
|
||||
--ks-font-display: "Alumni Sans", "Albert Sans", Arial, sans-serif;
|
||||
--ks-font-wordmark: "Alumni Sans", "Albert Sans", Arial, sans-serif;
|
||||
--ks-font: "Albert Sans", "Avenir Next", "Helvetica Neue", Arial, system-ui, sans-serif;
|
||||
--ks-mono: "SFMono-Regular", "Roboto Mono", "JetBrains Mono", Consolas, monospace;
|
||||
|
||||
/* ============================================================
|
||||
Type scale. These are the actual sizes the homepage ships with —
|
||||
new pages should read them via var(...) rather than hand-typing.
|
||||
The display vs headline weight split is deliberate: the hero h1
|
||||
reads thin (100) for elegance; section h2s read heavier (300) to
|
||||
anchor each block. Section h2s look heavier than the h1 on purpose.
|
||||
============================================================ */
|
||||
|
||||
/* Display — hero h1. */
|
||||
--ks-type-display-size: clamp(3.4rem, 6.5vw, 5.6rem);
|
||||
--ks-type-display-weight: 100;
|
||||
--ks-type-display-line: 1.02;
|
||||
--ks-type-display-track: -0.01em;
|
||||
|
||||
/* Headline — section h2. Heavier than display. */
|
||||
--ks-type-headline-size: clamp(2.6rem, 4vw, 3.4rem);
|
||||
--ks-type-headline-weight: 300;
|
||||
--ks-type-headline-line: 1.04;
|
||||
|
||||
/* Title — card and panel headings (h3). */
|
||||
--ks-type-title-size: 1.18rem;
|
||||
--ks-type-title-weight: 500;
|
||||
--ks-type-title-line: 1.35;
|
||||
|
||||
/* Body. */
|
||||
--ks-type-body-size: 1.02rem;
|
||||
--ks-type-body-line: 1.8;
|
||||
|
||||
/* Eyebrow — small mono labels above titles. */
|
||||
--ks-type-eyebrow-size: 0.7rem;
|
||||
--ks-type-eyebrow-track: 0.18em;
|
||||
|
||||
/* Wordmark — IMPECCABLE in the header. */
|
||||
--ks-type-wordmark-size: 1.15rem;
|
||||
--ks-type-wordmark-track: 0.42em;
|
||||
|
||||
/* Mono — code, terminal, audit lines. */
|
||||
--ks-type-mono-size: 0.72rem;
|
||||
--ks-type-mono-track: 0.22em;
|
||||
|
||||
/* ============================================================
|
||||
Motion.
|
||||
============================================================ */
|
||||
--ks-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Light mode — pale surfaces, dark ink type, and kinpaku kept as the
|
||||
real brand gold. Activated via html.light on
|
||||
<html> (set by theme.js + inline FOUC guard in Base.astro).
|
||||
============================================================ */
|
||||
html.light {
|
||||
color-scheme: light;
|
||||
|
||||
/* Brand gold — same fills as dark mode (logo, buttons, marks).
|
||||
For decorative small text on paper, use patina. Use --ks-link-on-paper
|
||||
for body-sized links that need stronger contrast. */
|
||||
--ks-kinpaku: oklch(84% 0.19 80.46);
|
||||
--ks-kinpaku-vivid: oklch(87% 0.20 85);
|
||||
--ks-kinpaku-pale: oklch(86% 0.07 84);
|
||||
--ks-kinpaku-rich: oklch(77% 0.13 82);
|
||||
--ks-kinpaku-deep: oklch(61% 0.085 78);
|
||||
--ks-kinpaku-ink: var(--ks-patina);
|
||||
|
||||
/* Text links + active nav on paper — patina, not fill gold. */
|
||||
--ks-link-on-paper: var(--ks-patina-deep);
|
||||
--ks-link-on-paper-hover: var(--ks-patina);
|
||||
--ks-link-on-paper-line: oklch(49% 0.08 188 / 0.42);
|
||||
--ks-nav-active: var(--ks-patina-deep);
|
||||
|
||||
/* Patina — brand values for fills; deep for text on paper. */
|
||||
--ks-patina: oklch(70% 0.12 188);
|
||||
--ks-patina-pale: oklch(82% 0.07 188);
|
||||
--ks-patina-deep: oklch(49% 0.08 188);
|
||||
|
||||
--ks-state-ink: var(--ks-patina-deep);
|
||||
|
||||
--ks-vermilion: oklch(52% 0.16 35);
|
||||
|
||||
/* Surfaces — warm paper and cream panels. */
|
||||
--ks-lacquer: oklch(97% 0.012 95);
|
||||
--ks-lacquer-deep: oklch(94% 0.014 95);
|
||||
--ks-lacquer-raised: oklch(99% 0.008 95);
|
||||
--ks-graphite: oklch(91% 0.012 95);
|
||||
--ks-graphite-2: oklch(88% 0.014 95);
|
||||
|
||||
/* Text — dark ink on paper. */
|
||||
--ks-champagne: oklch(18% 0.02 95);
|
||||
--ks-text: oklch(25% 0.018 95);
|
||||
--ks-text-muted: oklch(45% 0.015 95);
|
||||
--ks-text-faint: oklch(55% 0.012 95);
|
||||
--ks-text-mute-deep: oklch(65% 0.01 95);
|
||||
|
||||
/* Rules — dark hairlines. */
|
||||
--ks-rule: oklch(25% 0.02 95 / 0.12);
|
||||
|
||||
/* Code — light chips, dark text. */
|
||||
--ks-code-fg: oklch(28% 0.02 95);
|
||||
--ks-code-bg: var(--ks-graphite);
|
||||
--ks-code-block-fg: oklch(30% 0.02 95);
|
||||
--ks-code-block-bg: var(--ks-lacquer-raised);
|
||||
--ks-code-block-border: var(--ks-rule);
|
||||
--ks-code-cmd: var(--ks-link-on-paper);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { cp, copyFile, mkdir, rm, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs';
|
||||
import { composeHookRules } from './lib/hook-rule-presentation.js';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const buildDir = path.join(root, 'build-picker');
|
||||
const outputDir = path.join(root, 'skill/scripts/picker');
|
||||
// Static assets vendored under picker/assets/ (the site now lives in the
|
||||
// private impeccable-site repo, so the picker carries its own copies). These
|
||||
// are referenced from markup at runtime, so Vite never sees them.
|
||||
const assetsSource = path.join(root, 'picker/assets');
|
||||
const assetsOutput = path.join(outputDir, 'assets');
|
||||
const runtimeAssets = ['hero-dark.jpg', 'kinpaku-gold-leaf.jpg'];
|
||||
// The design context document's images, vendored from the standalone demo:
|
||||
// per-section foil icons, the rail textures, the brand-asset placeholders, and
|
||||
// the components photo. Whole directories, so a demo re-sync stays a plain copy.
|
||||
const runtimeAssetDirs = ['audience', 'product', 'brand', 'color', 'typography', 'material', 'components'];
|
||||
// The page links ./favicon.svg, so it is also served from the output root.
|
||||
const faviconSource = path.join(assetsSource, 'favicon.svg');
|
||||
const faviconOutput = path.join(outputDir, 'favicon.svg');
|
||||
// The icon specimen is fetched at runtime rather than inlined, so it ships
|
||||
// beside the page instead of going through Vite. Regenerate it with
|
||||
// `node scripts/vendor-icons.mjs`.
|
||||
const iconDataSource = path.join(root, 'picker/data/icon-packs.json');
|
||||
const iconDataOutput = path.join(outputDir, 'icon-packs.json');
|
||||
|
||||
await rm(buildDir, { recursive: true, force: true });
|
||||
// The Hooks page's rule list, composed from the canonical detector registry so
|
||||
// the document never drifts from what the hook actually enforces. Committed and
|
||||
// regenerated every build; tests/hook-rule-presentation.test.js guards the sync.
|
||||
await writeFile(
|
||||
path.join(root, 'picker/data/hook-rules.json'),
|
||||
`${JSON.stringify(composeHookRules(ANTIPATTERNS), null, 2)}\n`,
|
||||
);
|
||||
execFileSync(
|
||||
'bun',
|
||||
['x', 'astro', 'build', '--config', 'picker/astro.config.mjs'],
|
||||
{ cwd: root, stdio: 'inherit' },
|
||||
);
|
||||
|
||||
await rm(outputDir, { recursive: true, force: true });
|
||||
await cp(buildDir, outputDir, { recursive: true });
|
||||
await mkdir(assetsOutput, { recursive: true });
|
||||
await copyFile(faviconSource, faviconOutput);
|
||||
await copyFile(iconDataSource, iconDataOutput);
|
||||
for (const asset of runtimeAssets) {
|
||||
await copyFile(path.join(assetsSource, asset), path.join(assetsOutput, asset));
|
||||
}
|
||||
for (const dir of runtimeAssetDirs) {
|
||||
await cp(path.join(assetsSource, dir), path.join(assetsOutput, dir), { recursive: true });
|
||||
}
|
||||
await rm(buildDir, { recursive: true, force: true });
|
||||
|
||||
console.log(`Built ${path.relative(root, outputDir)}/`);
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
Presentation metadata for the Hooks page of the design context document.
|
||||
|
||||
The rule ids, names, and descriptions come from the canonical registry
|
||||
(cli/engine/registry/antipatterns.mjs); this module adds only how the document
|
||||
groups and labels them. `FINGERPRINT_IDS` promotes a handful of slop rules into
|
||||
their own "Fingerprints" family (model-specific signatures); `DISCIPLINES` gives
|
||||
every rule a discipline label for the collapsible groups. Both must cover the
|
||||
registry exactly — tests/hook-rule-presentation.test.js fails the suite when a
|
||||
rule is added without a discipline, so the document can never silently miss one.
|
||||
*/
|
||||
|
||||
export const FINGERPRINT_IDS = new Set([
|
||||
'gpt-thin-border-wide-shadow',
|
||||
'repeating-stripes-gradient',
|
||||
'codex-grid-background',
|
||||
'theater-slop-phrase',
|
||||
]);
|
||||
|
||||
export const DISCIPLINES = {
|
||||
'side-tab': 'Visual Details',
|
||||
'border-accent-on-rounded': 'Visual Details',
|
||||
'overused-font': 'Typography',
|
||||
'flat-type-hierarchy': 'Typography',
|
||||
'gradient-text': 'Color & Contrast',
|
||||
'ai-color-palette': 'Color & Contrast',
|
||||
'cream-palette': 'Color & Contrast',
|
||||
'nested-cards': 'Layout & Space',
|
||||
'monotonous-spacing': 'Layout & Space',
|
||||
'bounce-easing': 'Motion',
|
||||
'pulsing-dot': 'Motion',
|
||||
'blinking-cursor': 'Motion',
|
||||
'shape-assembled-illustration': 'Imagery',
|
||||
'organic-clip-path': 'Imagery',
|
||||
'buried-raster': 'Imagery',
|
||||
'dark-glow': 'Color & Contrast',
|
||||
'radial-halo': 'Color & Contrast',
|
||||
'radial-spotlight-glow': 'Color & Contrast',
|
||||
'marquee': 'Motion',
|
||||
'icon-tile-stack': 'Typography',
|
||||
'italic-serif-display': 'Typography',
|
||||
'hero-eyebrow-chip': 'Typography',
|
||||
'kicker-above-heading': 'Typography',
|
||||
'numbered-section-labels': 'Layout & Space',
|
||||
'em-dash-overuse': 'Copy',
|
||||
'marketing-buzzword': 'Copy',
|
||||
'aphoristic-cadence': 'Copy',
|
||||
'oversized-h1': 'Typography',
|
||||
'extreme-negative-tracking': 'Typography',
|
||||
'broken-image': 'Imagery',
|
||||
'script-error': 'Quality',
|
||||
'content-hidden-at-rest': 'Layout & Space',
|
||||
'edge-flush-cards': 'Layout & Space',
|
||||
'text-occlusion': 'Layout & Space',
|
||||
'first-viewport-column-overflow': 'Layout & Space',
|
||||
'gray-on-color': 'Color & Contrast',
|
||||
'low-contrast': 'Quality',
|
||||
'layout-transition': 'Motion',
|
||||
'line-length': 'Layout & Space',
|
||||
'cramped-padding': 'Layout & Space',
|
||||
'body-text-viewport-edge': 'Layout & Space',
|
||||
'tight-leading': 'Typography',
|
||||
'skipped-heading': 'Typography',
|
||||
'heading-rhythm': 'Layout & Space',
|
||||
'justified-text': 'Typography',
|
||||
'tiny-text': 'Typography',
|
||||
'undersized-ui-text': 'Typography',
|
||||
'all-caps-body': 'Typography',
|
||||
'wide-tracking': 'Typography',
|
||||
'text-overflow': 'Layout & Space',
|
||||
'repeated-container-text': 'Quality',
|
||||
'clipped-overflow-container': 'Layout & Space',
|
||||
'design-system-font': 'Typography',
|
||||
'design-system-color': 'Color & Contrast',
|
||||
'design-system-radius': 'Visual Details',
|
||||
'design-system-font-size': 'Typography',
|
||||
'gpt-thin-border-wide-shadow': 'Visual Details',
|
||||
'repeating-stripes-gradient': 'Visual Details',
|
||||
'codex-grid-background': 'Visual Details',
|
||||
'theater-slop-phrase': 'Copy',
|
||||
'image-hover-transform': 'Motion',
|
||||
};
|
||||
|
||||
/* One entry per registry rule, in registry order, in the exact shape the
|
||||
document's Hooks module renders. */
|
||||
export function composeHookRules(antipatterns) {
|
||||
return antipatterns.map((rule) => ({
|
||||
id: rule.id,
|
||||
name: rule.name,
|
||||
description: rule.description,
|
||||
group: FINGERPRINT_IDS.has(rule.id) ? 'fingerprints' : rule.category,
|
||||
discipline: DISCIPLINES[rule.id],
|
||||
}));
|
||||
}
|
||||
@@ -36,6 +36,7 @@ export const SKILL_CATEGORIES = {
|
||||
init: 'system',
|
||||
document: 'system',
|
||||
extract: 'system',
|
||||
'design-context': 'system',
|
||||
live: 'system',
|
||||
};
|
||||
|
||||
|
||||
@@ -109,9 +109,11 @@ function readSkillScripts(scriptsDir) {
|
||||
if (PER_PROJECT_SCRIPT_ARTIFACTS.has(entry.name)) continue;
|
||||
|
||||
const relPath = path.relative(scriptsDir, entryPath).split(path.sep).join('/');
|
||||
const isBinary = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.woff', '.woff2']
|
||||
.includes(path.extname(entry.name).toLowerCase());
|
||||
scripts.push({
|
||||
name: relPath,
|
||||
content: fs.readFileSync(entryPath, 'utf-8'),
|
||||
content: fs.readFileSync(entryPath, isBinary ? undefined : 'utf-8'),
|
||||
filePath: entryPath,
|
||||
});
|
||||
}
|
||||
@@ -643,8 +645,9 @@ const EXCLUDED_FROM_SUGGESTIONS = new Set([
|
||||
// These are the commands that audit/critique/etc. reference when suggesting next steps.
|
||||
const IMPECCABLE_SUB_COMMANDS = [
|
||||
'adapt', 'animate', 'audit', 'bolder', 'clarify', 'colorize',
|
||||
'critique', 'delight', 'distill', 'document', 'harden', 'layout',
|
||||
'onboard', 'optimize', 'overdrive', 'polish', 'quieter', 'shape', 'typeset',
|
||||
'critique', 'delight', 'design-context', 'distill', 'document', 'harden',
|
||||
'layout', 'onboard', 'optimize', 'overdrive', 'polish', 'quieter', 'shape',
|
||||
'typeset',
|
||||
];
|
||||
|
||||
export function replacePlaceholders(content, provider, commandNames = [], allSkillNames = []) {
|
||||
@@ -700,6 +703,7 @@ export function replacePlaceholders(content, provider, commandNames = [], allSki
|
||||
* here by an exact string match.
|
||||
*/
|
||||
export function replaceScriptProviderMarker(content, provider, buildProvider = provider) {
|
||||
if (Buffer.isBuffer(content)) return content;
|
||||
const placeholders = PROVIDER_PLACEHOLDERS[provider] || PROVIDER_PLACEHOLDERS.cursor;
|
||||
const commandPrefix = placeholders.command_prefix || '/';
|
||||
const prefixMarker = "export const IMPECCABLE_COMMAND_PREFIX = '/'; // @impeccable-provider-command-prefix";
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
// For the skill component, also reruns `bun run build:release` and refuses if the
|
||||
// regenerated harness directories drift from what is committed.
|
||||
|
||||
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync, readdirSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -88,6 +89,24 @@ function runMutating(cmd) {
|
||||
execSync(cmd, { cwd: repoRoot, stdio: 'inherit' });
|
||||
}
|
||||
|
||||
function directoryHash(directory) {
|
||||
if (!existsSync(directory)) return null;
|
||||
const hash = createHash('sha256');
|
||||
|
||||
function addDirectory(current) {
|
||||
for (const entry of readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
const entryPath = path.join(current, entry.name);
|
||||
const relative = path.relative(directory, entryPath).split(path.sep).join('/');
|
||||
hash.update(relative);
|
||||
if (entry.isDirectory()) addDirectory(entryPath);
|
||||
else if (entry.isFile()) hash.update(readFileSync(entryPath));
|
||||
}
|
||||
}
|
||||
|
||||
addDirectory(directory);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
step(`Reading version from ${cfg.manifest}`);
|
||||
const manifest = JSON.parse(readFileSync(path.join(repoRoot, cfg.manifest), 'utf8'));
|
||||
const version = manifest.version;
|
||||
@@ -115,7 +134,14 @@ if (cfg.buildCmd) {
|
||||
if (dryRun) {
|
||||
console.log(` [dry-run] ${cfg.buildCmd}`);
|
||||
} else {
|
||||
const pickerOutput = component === 'skill'
|
||||
? path.join(repoRoot, 'skill/scripts/picker')
|
||||
: null;
|
||||
const pickerHashBefore = pickerOutput ? directoryHash(pickerOutput) : null;
|
||||
execSync(cfg.buildCmd, { cwd: repoRoot, stdio: 'inherit' });
|
||||
if (pickerHashBefore && directoryHash(pickerOutput) !== pickerHashBefore) {
|
||||
fail(`Picker build output was stale. Run \`bun run build:picker\`, then re-run the release check.`);
|
||||
}
|
||||
const postBuild = run('git status --porcelain');
|
||||
if (postBuild) {
|
||||
fail(`Build produced uncommitted changes. Run \`${cfg.buildCmd}\`, commit the result, then re-run.\n${postBuild}`);
|
||||
|
||||
@@ -25,8 +25,9 @@ export const SUITES = {
|
||||
description: 'Build, provider transforms, CLI helpers, context, and storage unit tests.',
|
||||
triggers: [
|
||||
...COMMON_INFRA_PATTERNS,
|
||||
/^picker\//,
|
||||
/^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/,
|
||||
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|comp-diff|comp-spec|build-phase|font-match|data\/font-index|concept-seed|generate-image|context|context-signals|critique-storage|design-parser|doctor|hook|impeccable-paths|is-generated|lib\/(artifact-schema|png|raster|image-metrics|font-fingerprint|font-index|hero-checks|composition-catalog|concept-catalog|provider|staleness|staleness-deep|staleness-notice|surface-briefs|target-slug|template-extensions)|pin|surface-brief))/,
|
||||
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|comp-diff|comp-spec|build-phase|font-match|data\/font-index|concept-seed|generate-image|context|context-signals|critique-storage|design-context|design-parser|doctor|hook|impeccable-paths|is-generated|lib\/(artifact-schema|png|raster|image-metrics|font-fingerprint|font-index|hero-checks|composition-catalog|concept-catalog|provider|staleness|staleness-deep|staleness-notice|surface-briefs|target-slug|template-extensions)|picker|pin|surface-brief))/,
|
||||
/^README(\.npm)?\.md$/,
|
||||
/^cli\/bin\//,
|
||||
],
|
||||
@@ -47,6 +48,7 @@ export const SUITES = {
|
||||
'tests/validate-plugin-versions.test.js',
|
||||
'tests/validate-plugin-manifest.test.js',
|
||||
'tests/plugin-paths.test.js',
|
||||
'tests/hook-rule-presentation.test.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -70,6 +72,7 @@ export const SUITES = {
|
||||
'tests/hook.test.mjs',
|
||||
'tests/impeccable-paths.test.mjs',
|
||||
'tests/openai-plugin.test.mjs',
|
||||
'tests/picker-server.test.mjs',
|
||||
'tests/pin.test.mjs',
|
||||
'tests/release.test.mjs',
|
||||
'tests/doctor.test.mjs',
|
||||
|
||||
@@ -0,0 +1,538 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Vendor one fixed specimen from each open-source icon pack the picker offers.
|
||||
*
|
||||
* node scripts/vendor-icons.mjs # rebuild from the pinned versions
|
||||
* node scripts/vendor-icons.mjs --latest # re-resolve each pack's latest first
|
||||
* node scripts/vendor-icons.mjs --dry-run # resolve and report, write nothing
|
||||
*
|
||||
* Every pack draws the same 24 concepts, so the specimen grid compares packs
|
||||
* rather than vocabularies. Output is picker/data/icon-packs.json: pack
|
||||
* metadata plus sanitized inner SVG markup, small enough to ship with the
|
||||
* picker and offline once it is there.
|
||||
*/
|
||||
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const outputPath = path.join(root, 'picker/data/icon-packs.json');
|
||||
const dryRun = process.argv.includes('--dry-run');
|
||||
const useLatest = process.argv.includes('--latest');
|
||||
|
||||
/** The specimen. Order is the reading order of the grid. */
|
||||
const CONCEPTS = [
|
||||
'home', 'search', 'user', 'settings', 'bell', 'heart',
|
||||
'star', 'calendar', 'mail', 'message', 'trash', 'download',
|
||||
'upload', 'plus', 'check', 'close', 'chevron-right', 'arrow-right',
|
||||
'menu', 'filter', 'edit', 'lock', 'eye', 'external-link',
|
||||
];
|
||||
|
||||
/**
|
||||
* Candidate names per concept, best glyph first. Packs disagree on almost
|
||||
* every noun here, so the resolver walks the list against each pack's real
|
||||
* file listing and takes the first hit. Anything this misses gets an explicit
|
||||
* entry in the pack's `overrides`.
|
||||
*/
|
||||
const ALIASES = {
|
||||
home: ['home', 'house'],
|
||||
search: ['search', 'magnifying-glass'],
|
||||
user: ['user', 'person', 'account'],
|
||||
settings: ['settings', 'gear', 'gear-six', 'cog'],
|
||||
bell: ['bell', 'notification'],
|
||||
heart: ['heart', 'favourite'],
|
||||
star: ['star'],
|
||||
calendar: ['calendar', 'calendar-blank', 'calendar-days', 'calendar3'],
|
||||
mail: ['mail', 'envelope', 'envelope-closed', 'email'],
|
||||
message: [
|
||||
'message-circle', 'chat-circle', 'chat-bubble-left', 'comment',
|
||||
'chat-bubble', 'message-square', 'chat-dots', 'message', 'chat',
|
||||
],
|
||||
trash: ['trash-2', 'trash', 'trash-can', 'delete-bin'],
|
||||
download: ['download', 'download-simple', 'arrow-down-tray'],
|
||||
upload: ['upload', 'upload-simple', 'arrow-up-tray'],
|
||||
plus: ['plus', 'add'],
|
||||
check: ['check', 'checkmark', 'tick'],
|
||||
close: ['x', 'x-mark', 'cross-1', 'xmark', 'close'],
|
||||
'chevron-right': ['chevron-right', 'caret-right', 'nav-arrow-right', 'arrow-right-s'],
|
||||
'arrow-right': ['arrow-right'],
|
||||
menu: ['menu', 'bars-3', 'hamburger-menu', 'three-bars', 'list', 'menu-2'],
|
||||
filter: ['filter', 'funnel', 'funnel-simple', 'mixer-horizontal'],
|
||||
edit: ['pencil', 'pencil-1', 'pencil-simple', 'edit-pencil', 'edit-2', 'edit'],
|
||||
lock: ['lock', 'lock-closed', 'lock-key'],
|
||||
eye: ['eye', 'eye-open', 'view'],
|
||||
'external-link': [
|
||||
'external-link', 'arrow-square-out', 'arrow-top-right-on-square',
|
||||
'box-arrow-up-right', 'link-external', 'open-new-window',
|
||||
'square-arrow-out-up-right', 'share-box',
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* One row per pack. `dir` and `suffix` describe where the variant we want
|
||||
* lives; `count` is measured from the real listing of that variant, not
|
||||
* copied from a marketing page. `note` is the row's one line of editorial.
|
||||
*/
|
||||
const PACKS = [
|
||||
{
|
||||
id: 'lucide',
|
||||
name: 'Lucide',
|
||||
license: 'ISC',
|
||||
url: 'https://lucide.dev',
|
||||
note: 'Feather\u2019s successor. Same 2px round-cap hand, far wider catalog, still shipping.',
|
||||
grid: '24',
|
||||
weight: '2px stroke',
|
||||
source: { kind: 'npm', pkg: 'lucide-static', version: '1.27.0' },
|
||||
dir: 'icons/',
|
||||
viewBox: '0 0 24 24',
|
||||
attrs: {
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': '2',
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'feather',
|
||||
name: 'Feather',
|
||||
license: 'MIT',
|
||||
url: 'https://feathericons.com',
|
||||
note: 'The 2px original most stroke sets descend from. Small, even, no longer growing.',
|
||||
grid: '24',
|
||||
weight: '2px stroke',
|
||||
source: { kind: 'npm', pkg: 'feather-icons', version: '4.29.2' },
|
||||
dir: 'dist/icons/',
|
||||
viewBox: '0 0 24 24',
|
||||
attrs: {
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': '2',
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'phosphor',
|
||||
name: 'Phosphor',
|
||||
license: 'MIT',
|
||||
url: 'https://phosphoricons.com',
|
||||
note: 'Drawn on a 256 grid with a soft geometric hand. Six weights upstream, regular here.',
|
||||
grid: '256',
|
||||
weight: 'regular weight',
|
||||
source: { kind: 'npm', pkg: '@phosphor-icons/core', version: '2.1.1' },
|
||||
dir: 'assets/regular/',
|
||||
viewBox: '0 0 256 256',
|
||||
attrs: { fill: 'currentColor' },
|
||||
},
|
||||
{
|
||||
id: 'heroicons',
|
||||
name: 'Heroicons',
|
||||
license: 'MIT',
|
||||
url: 'https://heroicons.com',
|
||||
note: 'Tailwind\u2019s set. 1.5px strokes at 24, with solid and 20px mini twins for dense UI.',
|
||||
grid: '24',
|
||||
weight: '1.5px stroke',
|
||||
source: { kind: 'npm', pkg: 'heroicons', version: '2.2.0' },
|
||||
dir: '24/outline/',
|
||||
viewBox: '0 0 24 24',
|
||||
attrs: {
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': '1.5',
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'tabler',
|
||||
name: 'Tabler',
|
||||
license: 'MIT',
|
||||
url: 'https://tabler.io/icons',
|
||||
note: 'The widest stroke catalog here, uniform enough that mixed rows read as one hand.',
|
||||
grid: '24',
|
||||
weight: '2px stroke',
|
||||
source: { kind: 'npm', pkg: '@tabler/icons', version: '3.45.0' },
|
||||
dir: 'icons/outline/',
|
||||
viewBox: '0 0 24 24',
|
||||
attrs: {
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': '2',
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'radix',
|
||||
name: 'Radix Icons',
|
||||
license: 'MIT',
|
||||
url: 'https://www.radix-ui.com/icons',
|
||||
note: 'A 15px grid built for product chrome. Crisp at control size, thin if you scale it up.',
|
||||
grid: '15',
|
||||
weight: 'filled paths',
|
||||
source: { kind: 'gh', repo: 'radix-ui/icons', version: '112af91ad275a63c3a29b0da2588342af74ef9bf' },
|
||||
dir: 'packages/radix-icons/icons/',
|
||||
viewBox: '0 0 15 15',
|
||||
attrs: { fill: 'none' },
|
||||
},
|
||||
{
|
||||
id: 'bootstrap',
|
||||
name: 'Bootstrap Icons',
|
||||
license: 'MIT',
|
||||
url: 'https://icons.getbootstrap.com',
|
||||
note: 'Filled paths on a 16 grid, so weight holds at any size. More literal than the stroke sets.',
|
||||
grid: '16',
|
||||
weight: 'filled paths',
|
||||
source: { kind: 'npm', pkg: 'bootstrap-icons', version: '1.13.1' },
|
||||
dir: 'icons/',
|
||||
exclude: (name) => name.endsWith('-fill'),
|
||||
viewBox: '0 0 16 16',
|
||||
attrs: { fill: 'currentColor' },
|
||||
},
|
||||
{
|
||||
id: 'iconoir',
|
||||
name: 'Iconoir',
|
||||
license: 'MIT',
|
||||
url: 'https://iconoir.com',
|
||||
note: '1.5px strokes on 24 with an open, geometric hand. Roomier than Lucide at the same size.',
|
||||
grid: '24',
|
||||
weight: '1.5px stroke',
|
||||
source: { kind: 'npm', pkg: 'iconoir', version: '7.11.1' },
|
||||
dir: 'icons/regular/',
|
||||
viewBox: '0 0 24 24',
|
||||
attrs: {
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': '1.5',
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'octicons',
|
||||
name: 'Octicons',
|
||||
license: 'MIT',
|
||||
url: 'https://primer.style/octicons',
|
||||
note: 'GitHub\u2019s set, redrawn per size rather than scaled. Compact and developer-fluent.',
|
||||
grid: '24',
|
||||
weight: 'filled paths',
|
||||
source: { kind: 'npm', pkg: '@primer/octicons', version: '19.31.0' },
|
||||
dir: 'build/svg/',
|
||||
suffix: '-24',
|
||||
viewBox: '0 0 24 24',
|
||||
attrs: { fill: 'currentColor' },
|
||||
},
|
||||
{
|
||||
id: 'hugeicons',
|
||||
name: 'Hugeicons',
|
||||
license: 'MIT (free core)',
|
||||
url: 'https://hugeicons.com',
|
||||
note: '1.5px rounded strokes on 24. The free core is one style; the other styles are paid.',
|
||||
grid: '24',
|
||||
weight: '1.5px stroke',
|
||||
source: { kind: 'npm', pkg: '@hugeicons/core-free-icons', version: '4.2.3' },
|
||||
dir: 'dist/esm/',
|
||||
extension: '.js',
|
||||
viewBox: '0 0 24 24',
|
||||
attrs: {
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': '1.5',
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'remix',
|
||||
name: 'Remix Icon',
|
||||
license: 'Apache-2.0',
|
||||
url: 'https://remixicon.com',
|
||||
note: 'Line and fill twins across a broad catalog. Neutral, a touch heavier than the 2px sets.',
|
||||
grid: '24',
|
||||
weight: 'filled paths',
|
||||
source: { kind: 'npm', pkg: 'remixicon', version: '4.9.1' },
|
||||
dir: 'icons/',
|
||||
suffix: '-line',
|
||||
viewBox: '0 0 24 24',
|
||||
attrs: { fill: 'currentColor' },
|
||||
},
|
||||
];
|
||||
|
||||
/** Explicit picks where the alias walk lands on a worse glyph, or nothing. */
|
||||
const OVERRIDES = {
|
||||
lucide: { edit: 'pencil', menu: 'menu' },
|
||||
feather: { edit: 'edit-3', menu: 'menu' },
|
||||
phosphor: { menu: 'list', settings: 'gear-six' },
|
||||
// Tabler's menu is two rules, and Bootstrap files the funnel apart from the
|
||||
// stacked-rule filter. Both rows compare better against the other nine.
|
||||
tabler: { menu: 'menu-2' },
|
||||
bootstrap: { menu: 'list', message: 'chat', settings: 'gear', filter: 'funnel' },
|
||||
octicons: { menu: 'three-bars', settings: 'gear', message: 'comment' },
|
||||
radix: { menu: 'hamburger-menu', settings: 'gear', filter: 'mixer-horizontal' },
|
||||
// Iconoir's x.svg is the letter, not the close control.
|
||||
iconoir: { close: 'xmark' },
|
||||
// Remix files a hand bell under bell and a hex plate under settings, so the
|
||||
// two rows that should read as notification and preferences do not.
|
||||
remix: { bell: 'notification-2', settings: 'settings-3' },
|
||||
// Hugeicons names in PascalCase and numbers its variants, so nothing here is
|
||||
// guessable from the concept. Every pick is deliberate.
|
||||
hugeicons: {
|
||||
home: 'Home01Icon', search: 'Search01Icon', user: 'UserIcon',
|
||||
settings: 'Settings01Icon', bell: 'Notification01Icon', heart: 'FavouriteIcon',
|
||||
star: 'StarIcon', calendar: 'Calendar01Icon', mail: 'Mail01Icon',
|
||||
message: 'BubbleChatIcon', trash: 'Delete02Icon', download: 'Download01Icon',
|
||||
upload: 'Upload01Icon', plus: 'Add01Icon', check: 'Tick02Icon',
|
||||
close: 'Cancel01Icon', 'chevron-right': 'ChevronRightIcon',
|
||||
// ArrowRight01 is Hugeicons' chevron, and LockIcon is a keyhole disc.
|
||||
'arrow-right': 'ArrowRight02Icon', menu: 'Menu01Icon', filter: 'FilterIcon',
|
||||
edit: 'PencilEdit01Icon', lock: 'SquareLock01Icon', eye: 'ViewIcon',
|
||||
'external-link': 'ExternalLinkIcon',
|
||||
},
|
||||
};
|
||||
|
||||
const ALLOWED_TAGS = new Set([
|
||||
'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse', 'g',
|
||||
]);
|
||||
const ALLOWED_ATTRS = new Set([
|
||||
'd', 'cx', 'cy', 'r', 'rx', 'ry', 'x', 'y', 'x1', 'y1', 'x2', 'y2',
|
||||
'width', 'height', 'points', 'transform', 'fill', 'fill-rule', 'clip-rule',
|
||||
'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin',
|
||||
'stroke-miterlimit', 'stroke-dasharray', 'opacity', 'fill-opacity',
|
||||
'stroke-opacity',
|
||||
]);
|
||||
|
||||
async function getJson(url) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`${response.status} ${url}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function getText(url) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`${response.status} ${url}`);
|
||||
return response.text();
|
||||
}
|
||||
|
||||
/** Resolve the version to pin, then list every file the package ships. */
|
||||
async function listFiles(pack) {
|
||||
const source = useLatest ? { ...pack.source, version: null } : pack.source;
|
||||
if (source.kind === 'npm') {
|
||||
const version = source.version
|
||||
|| (await getJson(`https://data.jsdelivr.com/v1/packages/npm/${source.pkg}`)).tags.latest;
|
||||
const listing = await getJson(
|
||||
`https://data.jsdelivr.com/v1/packages/npm/${source.pkg}@${version}?structure=flat`,
|
||||
);
|
||||
return {
|
||||
version,
|
||||
base: `https://cdn.jsdelivr.net/npm/${source.pkg}@${version}`,
|
||||
files: listing.files.map((file) => file.name.replace(/^\//, '')),
|
||||
};
|
||||
}
|
||||
// GitHub sources are pinned to a commit: branches move, and a specimen that
|
||||
// silently redraws itself is worse than one that fails loudly.
|
||||
const version = source.version
|
||||
|| (await getJson(`https://api.github.com/repos/${source.repo}/commits/HEAD`)).sha;
|
||||
const tree = await getJson(
|
||||
`https://api.github.com/repos/${source.repo}/git/trees/${version}?recursive=1`,
|
||||
);
|
||||
return {
|
||||
version,
|
||||
base: `https://raw.githubusercontent.com/${source.repo}/${version}`,
|
||||
files: tree.tree.filter((node) => node.type === 'blob').map((node) => node.path),
|
||||
};
|
||||
}
|
||||
|
||||
function basenames(pack, files) {
|
||||
const extension = pack.extension || '.svg';
|
||||
const map = new Map();
|
||||
for (const file of files) {
|
||||
if (!file.startsWith(pack.dir) || !file.endsWith(extension)) continue;
|
||||
const name = file.slice(pack.dir.length, -extension.length);
|
||||
// Remix nests by category (icons/system/home-line.svg); everything else is
|
||||
// flat, and a nested name would only collide with itself.
|
||||
map.set(name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name, file);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function variantNames(pack, names) {
|
||||
const suffix = pack.suffix || '';
|
||||
return [...names]
|
||||
.filter((name) => name.endsWith(suffix))
|
||||
.filter((name) => !pack.exclude?.(name));
|
||||
}
|
||||
|
||||
function resolveName(pack, concept, names) {
|
||||
const suffix = pack.suffix || '';
|
||||
const override = OVERRIDES[pack.id]?.[concept];
|
||||
const candidates = override ? [override] : ALIASES[concept];
|
||||
for (const candidate of candidates) {
|
||||
for (const name of [
|
||||
`${candidate}${suffix}`,
|
||||
candidate,
|
||||
`${candidate}-01${suffix}`,
|
||||
`${candidate}-1${suffix}`,
|
||||
]) {
|
||||
if (names.has(name) && !pack.exclude?.(name)) return name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Keep drawing elements and drawing attributes. Drop everything else. */
|
||||
function sanitize(markup) {
|
||||
const body = markup
|
||||
.replace(/<!--[\s\S]*?-->/g, '')
|
||||
.replace(/<\?[\s\S]*?\?>/g, '')
|
||||
.replace(/<(title|desc|style|script|metadata)\b[\s\S]*?<\/\1>/gi, '')
|
||||
.replace(/<\/?svg\b[^>]*>/gi, '');
|
||||
|
||||
let dropped = 0;
|
||||
const cleaned = body.replace(/<\/?([a-zA-Z][a-zA-Z0-9-]*)([^>]*)>/g, (match, rawTag, rawAttrs) => {
|
||||
const tag = rawTag.toLowerCase();
|
||||
if (!ALLOWED_TAGS.has(tag)) {
|
||||
dropped += 1;
|
||||
return '';
|
||||
}
|
||||
if (match.startsWith('</')) return `</${tag}>`;
|
||||
|
||||
// Names carry digits (x1, y2), so the class cannot be letters only.
|
||||
const attrs = [...rawAttrs.matchAll(/([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*"([^"]*)"/g)]
|
||||
.map(([, key, value]) => [key.toLowerCase(), value.trim()])
|
||||
.filter(([key]) => ALLOWED_ATTRS.has(key));
|
||||
// Tabler opens every icon with an invisible bounding path, and a few packs
|
||||
// carry the same trick. It draws nothing, so it only costs bytes.
|
||||
const paint = Object.fromEntries(attrs);
|
||||
if (paint.fill === 'none' && paint.stroke === 'none') return '';
|
||||
if (paint.opacity === '0') return '';
|
||||
|
||||
const rendered = attrs.map(([key, value]) => `${key}="${value}"`).join(' ');
|
||||
const selfClosing = match.endsWith('/>');
|
||||
return `<${tag}${rendered ? ` ${rendered}` : ''}${selfClosing ? '/>' : '>'}`;
|
||||
});
|
||||
|
||||
return { body: cleaned.replace(/>\s+</g, '><').replace(/\s+/g, ' ').trim(), dropped };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hugeicons ships React-shaped tuples rather than SVG files:
|
||||
* [["path", { d: "...", key: "0" }], ...]. Same drawing, one hop away.
|
||||
*/
|
||||
function tuplesToMarkup(module) {
|
||||
const start = module.indexOf('[', module.indexOf('='));
|
||||
const end = module.lastIndexOf(']');
|
||||
if (start === -1 || end < start) throw new Error('No icon tuple array found');
|
||||
const literal = module
|
||||
.slice(start, end + 1)
|
||||
.replace(/([{,])\s*([A-Za-z][A-Za-z0-9]*)\s*:/g, '$1"$2":');
|
||||
|
||||
const tuples = JSON.parse(literal);
|
||||
return tuples
|
||||
.map(([tag, props]) => {
|
||||
const attrs = Object.entries(props)
|
||||
.filter(([key]) => key !== 'key')
|
||||
.map(([key, value]) => [key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`), value])
|
||||
.map(([key, value]) => `${key}="${value}"`)
|
||||
.join(' ');
|
||||
return `<${tag} ${attrs}/>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
const rootAttr = (markup, name) => markup.match(new RegExp(`<svg[^>]*\\b${name}="([^"]*)"`, 'i'))?.[1];
|
||||
|
||||
async function vendorPack(pack) {
|
||||
const { version, base, files } = await listFiles(pack);
|
||||
const names = basenames(pack, files);
|
||||
const count = variantNames(pack, names.keys()).length;
|
||||
if (!count) throw new Error(`${pack.id}: no files under ${pack.dir}`);
|
||||
|
||||
const glyphs = [];
|
||||
const misses = [];
|
||||
let droppedTotal = 0;
|
||||
let viewBoxMismatch = null;
|
||||
|
||||
for (const concept of CONCEPTS) {
|
||||
const name = resolveName(pack, concept, names);
|
||||
if (!name) {
|
||||
misses.push(concept);
|
||||
continue;
|
||||
}
|
||||
const raw = await getText(`${base}/${encodeURI(names.get(name))}`);
|
||||
const markup = pack.extension === '.js' ? tuplesToMarkup(raw) : raw;
|
||||
if (pack.extension !== '.js') {
|
||||
const viewBox = rootAttr(markup, 'viewBox');
|
||||
if (viewBox && viewBox !== pack.viewBox) viewBoxMismatch ??= `${name}: ${viewBox}`;
|
||||
}
|
||||
const { body, dropped } = sanitize(markup);
|
||||
droppedTotal += dropped;
|
||||
if (!body) throw new Error(`${pack.id}/${name}: sanitized to nothing`);
|
||||
glyphs.push({ concept, name, body });
|
||||
}
|
||||
|
||||
return {
|
||||
pack: {
|
||||
id: pack.id,
|
||||
name: pack.name,
|
||||
license: pack.license,
|
||||
url: pack.url,
|
||||
note: pack.note,
|
||||
grid: pack.grid,
|
||||
weight: pack.weight,
|
||||
count,
|
||||
version,
|
||||
viewBox: pack.viewBox,
|
||||
attrs: pack.attrs,
|
||||
glyphs,
|
||||
},
|
||||
report: { misses, dropped: droppedTotal, viewBoxMismatch },
|
||||
};
|
||||
}
|
||||
|
||||
const results = await Promise.all(PACKS.map(async (pack) => {
|
||||
try {
|
||||
return { pack, ...(await vendorPack(pack)) };
|
||||
} catch (error) {
|
||||
return { pack, error };
|
||||
}
|
||||
}));
|
||||
|
||||
let failed = false;
|
||||
const packs = [];
|
||||
for (const result of results) {
|
||||
if (result.error) {
|
||||
failed = true;
|
||||
console.error(`FAIL ${result.pack.id}: ${result.error.message}`);
|
||||
continue;
|
||||
}
|
||||
const { pack, report } = result;
|
||||
packs.push(pack);
|
||||
const flags = [
|
||||
report.misses.length ? `MISSING ${report.misses.join(', ')}` : null,
|
||||
report.viewBoxMismatch ? `VIEWBOX ${report.viewBoxMismatch}` : null,
|
||||
report.dropped ? `dropped ${report.dropped} tags` : null,
|
||||
].filter(Boolean);
|
||||
if (report.misses.length) failed = true;
|
||||
console.log(
|
||||
`${pack.id.padEnd(11)} v${String(pack.version).slice(0, 12).padEnd(13)}`
|
||||
+ `${String(pack.count).padStart(5)} glyphs ${pack.glyphs.length}/${CONCEPTS.length}`
|
||||
+ `${flags.length ? ` ${flags.join(' | ')}` : ''}`,
|
||||
);
|
||||
console.log(` ${pack.glyphs.map((glyph) => `${glyph.concept}=${glyph.name}`).join(' ')}`);
|
||||
}
|
||||
|
||||
if (!dryRun && packs.length) {
|
||||
const payload = {
|
||||
generated: new Date().toISOString().slice(0, 10),
|
||||
concepts: CONCEPTS,
|
||||
packs,
|
||||
};
|
||||
await mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, `${JSON.stringify(payload)}\n`);
|
||||
const bytes = Buffer.byteLength(JSON.stringify(payload));
|
||||
console.log(`\nWrote ${path.relative(root, outputPath)} (${(bytes / 1024).toFixed(0)} KB)`);
|
||||
}
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -48,6 +48,7 @@ Choose the mode from the requested surface, not the product, and persist it only
|
||||
| `init` | Build | Capture durable product context in PRODUCT.md | [reference/init.md](reference/init.md) |
|
||||
| `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) |
|
||||
| `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) |
|
||||
| `design-context [open/edit/export/import]` | Build | Reopen, revise, export, or import the design interview and its document | [reference/design-context.md](reference/design-context.md) |
|
||||
| `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) |
|
||||
| `audit [target]` | Evaluate | Technical quality checks (a11y, perf, responsive) | [reference/audit.md](reference/audit.md) · native: [reference/audit.native.md](reference/audit.native.md) |
|
||||
| `polish [target]` | Refine | Final quality pass before shipping | [reference/polish.md](reference/polish.md) |
|
||||
|
||||
@@ -22,7 +22,7 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh
|
||||
|
||||
## Decision Comps
|
||||
|
||||
When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; report a card too thin to brief a comp, never pad it from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world. A native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run.
|
||||
When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields, PRODUCT.md, and on a questionnaire-seed world the packet's cue image with its **Cue, in words:** passage, nothing else; report a card too thin to brief a comp, never pad it from imagination. When the packet carries the cue, attach it to the generation call as a reference image and restate the passage's materials and light in the prompt in your own words; an attached image with no restated words leaves the model guessing what to take from it, and the cue's still-life composition never transfers, only its materials, palette, and light. A staged logo in the packet rides the same way: attach it and name it as the exact mark to reproduce, never a shape to reinterpret. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world. A native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run.
|
||||
|
||||
## Input Contract
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ Expect: the original request; the confirmed user answers; the artifact path(s);
|
||||
## Checks, in order
|
||||
|
||||
0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round.
|
||||
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too: the surface brief naming the approved comp, or an `approved` flag in its sidecar. Comp-round comps with no recorded pick mean the approval point was skipped, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
|
||||
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. When a seed or prior DESIGN.md predates this build, it matches the built world, and "matches" is evidence you grep, not an impression: custom properties DESIGN.md defines that no rule consumes, literals sitting a step from a defined token, geometry a named rule bans (a 999px pill against a Slightly Soft rule). Each hit is a material finding under the craft floor's token rule, and an approved comp excuses none of them: the comp rules composition; the world rules material. On a new world with no seed, DESIGN.md is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too: the surface brief naming the approved comp, or an `approved` flag in its sidecar. Comp-round comps with no recorded pick mean the approval point was skipped, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
|
||||
2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element; its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Three rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality (CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. A fix that requires producing an asset says so explicitly ("produce: <region> as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement.
|
||||
3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition.
|
||||
3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. A committed motion energy shows up as eased state changes in the shipped code; a recorded energy with zero transitions is a finding. The card governs commitment and finish, never composition.
|
||||
4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped, a material fix ahead of any craft point. Then, for each of the five blocks: does the render keep the promise? Apply the memory test to the first viewport.
|
||||
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every raster region of the spec shipped as its plate (the spec names the file; the page references it; the region's diff row is not `missing`), not a gradient, an inline SVG, or a many-vertex `clip-path` standing in for it, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind a wash is a compliance token, not a shipped material, and the detector's `buried-raster` and `organic-clip-path` findings in the packet are material fixes.
|
||||
6. **Floor.** Read the craft floor's Refuse list and hold the screenshots against it: kickers and eyebrows, hard offset shadows outside a neobrutalist world, glyph icons, system display faces, gradient text, side stripes, and the rest. A banned element is a material fix even when it matches nothing in the comp: the builder loaded the same ban before writing it, and fidelity to a comp cannot authorize what the floor refuses. The parent's hook findings cover this mechanically where hooks run; this check exists because hookless harnesses reach you with none, and the last two live sessions shipped five kickers past a reviewer that never looked.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Load this after the direction is settled, and build without announcing the checklist. A pinned brief or the committed visual world overrides anything here; your own habit does not. When the design hook is active it already enforces the mechanical checks below as you edit: act on its findings instead of re-auditing each rule. <!-- rule:skill-craft-floor -->
|
||||
|
||||
A wholesale file rewrite, after a hook block or an error recovery, starts by re-opening DESIGN.md and the token sheet, so the new file restates the recorded system rather than your memory of it; memory is where a display token becomes a hand-tuned clamp. The ranking lives in [new-work.md](new-work.md): the comp rules composition; the world rules material, and a rewrite changes neither. <!-- rule:skill-rebuild-from-the-record -->
|
||||
|
||||
## Verify
|
||||
|
||||
Each of these is a check on the built result, not an intention. Run them together in the batched inspection rounds, not as separate screenshot trips; the checks share one render.
|
||||
@@ -11,6 +13,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
|
||||
- **Spacing:** tight groups, generous separation, more space above a heading than below it. Read the computed values. <!-- rule:skill-layout-spacing-rhythm -->
|
||||
- **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. <!-- rule:skill-typo-floor --> <!-- rule:skill-ban-text-overflow -->
|
||||
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. <!-- rule:skill-motion-floor --> <!-- rule:skill-motion-materials-palette --> <!-- rule:skill-motion-no-section-fade -->
|
||||
- **Tokens:** a size or color a step from a defined token takes the token: `0.82rem` beside a `0.8rem` token is the token, not a new size, and inline SVG or JS-drawn strokes take `var(--role)` or `currentColor`. Wire or remove what nothing consumes before finish; done means every defined custom property is consumed by at least one rule. <!-- rule:skill-tokens-are-consumed -->
|
||||
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. <!-- rule:skill-floor-shipping -->
|
||||
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. <!-- rule:skill-craft-browser-surfaces -->
|
||||
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. <!-- rule:skill-copy-design-material -->
|
||||
@@ -37,7 +40,7 @@ Surface habits:
|
||||
- Sparklines, progress rings, and soft-shadowed rounded rectangles standing in for content. <!-- rule:skill-reflex-decorative-chrome -->
|
||||
- Monospace as a costume for "technical" rather than for code, data, or measurement. <!-- rule:skill-reflex-mono-as-technical -->
|
||||
- A system display face (Impact, Arial Black, the platform sans) as the display voice of an own-world page. Source and self-host a face whose character matches the approved lettering; the closest installed font is a failure, not a fallback. <!-- rule:skill-ban-system-display-face -->
|
||||
- Unicode glyphs or emoji standing in for an icon system. Icons are drawn, from a real library or authored SVG, in one consistent stroke and weight. <!-- rule:skill-ban-glyph-icons -->
|
||||
- Unicode glyphs or emoji standing in for an icon system. Icons are drawn, from a real library or authored SVG, in one consistent stroke and weight. When a pack icon fails to fetch, take the nearest icon from the same pack, jsDelivr as the fallback CDN; drawing a replacement from scratch is an exception the user signs off on, so the one-pack promise survives a failed fetch. <!-- rule:skill-ban-glyph-icons -->
|
||||
- Geometric masks standing in for organic contours. A circle, polygon, or radial-gradient cutout approximating a photographic subject's edge is the cheap version of the effect and reads worse than omitting it. Derive an alpha matte from the actual image, or produce a cut-out asset. <!-- rule:skill-ban-geometric-occlusion-mask -->
|
||||
- Light or dark picked by category. Pick it from the use scene: who, where, under what ambient light. <!-- rule:skill-reflex-theme-by-habit -->
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Design Context
|
||||
|
||||
Loaded by `{{command_prefix}}impeccable design-context`. Owns the design interview record, the document built from it, and its portable form. The interview itself is created by `{{command_prefix}}impeccable document` seed mode; this command is everything afterwards.
|
||||
|
||||
## Where it lives
|
||||
|
||||
One store, under the project root:
|
||||
|
||||
```text
|
||||
.impeccable/design-context/
|
||||
context.json the chat half of the interview: product, audience, brand, interview
|
||||
answers.json the questionnaire's decisions
|
||||
assets/ brand files the user supplied
|
||||
fonts/ font faces the user uploaded
|
||||
cue.png the chosen cue image, copied at submit
|
||||
runtime/ session.json, journal.jsonl, draft.json (local, gitignored)
|
||||
exports/ the written-out forms (local, gitignored)
|
||||
```
|
||||
|
||||
`.impeccable/visual-cues/` is separate on purpose: it is the generation workspace, regenerable and gitignored, and the document no longer depends on it. The store is the user's own record and is theirs to commit.
|
||||
|
||||
## No argument
|
||||
|
||||
Report status in two lines, then act:
|
||||
|
||||
- Whether `answers.json` exists, and when it was last written.
|
||||
- Whether a draft is waiting (`runtime/draft.json`), whether DESIGN.md is seeded, and whether a session is live (`runtime/session.json` naming a running process).
|
||||
|
||||
With answers on disk, do `open`. Without them, say the design context is created by the questionnaire and offer `{{command_prefix}}impeccable document`. Never start the questionnaire unasked.
|
||||
|
||||
## open
|
||||
|
||||
Reopen the document, live for edits.
|
||||
|
||||
Run `node {{scripts_path}}/picker-server.mjs --doc` from the project root as a foreground command and parse its `PICKER_URL` line. Open it and wait exactly as [visual-cues.md](visual-cues.md)'s launch paragraph does: its harness-browser ladder (in-IDE browser first, then another browser tool, then the system opener, then telling the user the URL) and its wait-on-the-foreground-process rule. Skip everything earlier in its Step 7: the cue announcement and the `modes` and `context` writes belong to a run that is generating cues, and this one is not.
|
||||
|
||||
Then enter the document edit loop below. The process exiting is the signal:
|
||||
|
||||
- `DOC_SESSION_ENDED` and exit 0: the document was closed. Say so in one line; the loop is over.
|
||||
- Exit 2: it timed out or was never opened. Say it can be reopened with the same command, and never relaunch unprompted.
|
||||
- Exit 1: no interview exists. Route to `{{command_prefix}}impeccable document`.
|
||||
|
||||
## edit
|
||||
|
||||
Re-run the questionnaire over the previous answers.
|
||||
|
||||
Say in one line what it will do before launching, and settle DESIGN.md in the same breath, because a new run replaces the seed the last one produced: *"This re-runs the questionnaire with your previous answers filled in. When you finish, I will refresh DESIGN.md from the new answers. Refresh it, overwrite it, or merge by hand?"* That is the whole consent for this run; do not ask again afterwards.
|
||||
|
||||
Then run `node {{scripts_path}}/picker-server.mjs`, using the same launch ladder and wait rule as `open`. Prefill happens on its own: an unfinished run resumes from its draft, a finished one loads its answers, and `--fresh` starts blank. Cues and `context.json` already exist from the previous run, so do not regenerate cues and do not repeat Step 7's pre-launch writes.
|
||||
|
||||
On exit 0, go to [document.md](document.md) Steps 5-6 and write the seed from the new `answers.json`, honoring the choice made before launch. On exit 2, nothing was answered and nothing changed.
|
||||
|
||||
If `.impeccable/visual-cues/cues.json` is missing, the questionnaire cannot run: its palette screen loads the dealt cues and the built-in seeds together and neither arrives without that file. Say so and offer a full `{{command_prefix}}impeccable document --seed` run instead.
|
||||
|
||||
## export
|
||||
|
||||
```text
|
||||
node {{scripts_path}}/design-context-export.mjs [--out DIR] [--no-assets]
|
||||
```
|
||||
|
||||
Writes two files and prints an `EXPORTED` line for each. Tell the user what each is for, in one line each:
|
||||
|
||||
- `design-context.md` is the design context as one readable document. It is what to hand another tool, another agent, or a collaborator who needs to follow this design.
|
||||
- `design-context.bundle.json` is the same context in a form `{{command_prefix}}impeccable design-context import` reads, including the files the user supplied.
|
||||
|
||||
Do not read the export back into the conversation; the user asked for a file, not a recitation.
|
||||
|
||||
## import
|
||||
|
||||
```text
|
||||
node {{scripts_path}}/design-context-import.mjs <bundle.json> [--design skip|write] [--force]
|
||||
```
|
||||
|
||||
It refuses a project that already has a design context unless `--force`, and refuses while a document is open either way. Report what it prints:
|
||||
|
||||
- `DESIGN_MD carried` with a DESIGN.md already here: ask whether to refresh it from the imported context, overwrite it, or merge by hand, then act.
|
||||
- `DESIGN_MD carried` with none here: offer to write it (`--design write`) or to re-seed from the imported answers through [document.md](document.md) Steps 5-6.
|
||||
- `DESIGN_MD absent`: say the bundle carried decisions but no design document, and offer to seed one.
|
||||
|
||||
Then offer `open`.
|
||||
|
||||
## The document edit loop
|
||||
|
||||
The document is a working surface. Follow [visual-cues.md](visual-cues.md)'s "The document edit loop" section; it is the canonical contract for polling, the event kinds, and the reply commands. Two things to hold on to while you are in it:
|
||||
|
||||
- **The session is the only writer of the store.** Never edit `answers.json` or `context.json` yourself while a session runs. Values you settle travel on your reply, through `--answers` or `--context`. DESIGN.md and PRODUCT.md are yours to write directly.
|
||||
- **A `save_batch` is already applied.** The user's values are in the store before you hear about them. Your work is the prose those values leave stale, in whichever document the event's `downstream` names.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Never poll `answers.json` while a server runs. The process exiting is the signal.
|
||||
- Never drive the questionnaire yourself. The answers are the user's, and a run you filled in is a run they did not make.
|
||||
- Editing in the document changes values that are already there. A field the interview never captured is added by asking in chat, not by this command.
|
||||
@@ -64,6 +64,7 @@ Omit irrelevant sections rather than filling them with invented rules. Put respo
|
||||
## When to run
|
||||
|
||||
- New-work found a coherent incumbent visual system but no `DESIGN.md`.
|
||||
- New-work paused before its direction roll on a project with no `DESIGN.md` and the user accepted the seed questionnaire recommendation; run seed mode.
|
||||
- The first implementation of a new world is complete and its provisional decisions need to be carbonized.
|
||||
- An existing `DESIGN.md` is stale (the design has drifted).
|
||||
- Before a large redesign, to capture the current state as a reference.
|
||||
@@ -73,9 +74,9 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user
|
||||
## Two paths
|
||||
|
||||
- **Scan mode** (default): the project has design tokens, components, or rendered output. Extract, then confirm descriptive language. Use when there's code to analyze.
|
||||
- **Seed mode**: the project is pre-implementation. Ensure PRODUCT.md exists, then reuse new-work's visual-world workshop and write its directional DESIGN.md seed. Re-run in scan mode once there's code.
|
||||
- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Decide first whether the browser questionnaire can run, gather any existing brand assets, interview in chat (three named references and one anti-reference when the questionnaire will run; five high-level answers when it will not), then write a seed DESIGN.md marked `<!-- SEED -->` that carries every decision the interview and the questionnaire made. Re-run in scan mode once there's code.
|
||||
|
||||
Decide by scanning first (Scan mode Step 1). If the scan finds no tokens, no component files, and no rendered site, offer seed mode; don't silently switch. `/impeccable document --seed` requests new-work's world workshop, but it does not authorize replacing coherent code: when an incumbent system exists, offer scan mode or route an explicit identity-replacement request through new-work.
|
||||
Decide by scanning first (Scan mode Step 1). If the scan finds no tokens, no component files, and no rendered site, offer seed mode; don't silently switch. `/impeccable document --seed` forces seed mode on a pre-implementation project, but it does not authorize replacing coherent code: when an incumbent system exists, offer scan mode or route an explicit identity-replacement request through new-work.
|
||||
|
||||
## Scan mode (approach C: auto-extract, then confirm descriptive language)
|
||||
|
||||
@@ -308,7 +309,7 @@ The `html` and `css` fields must be **self-contained, drop-in snippets** that re
|
||||
|
||||
1. **Tailwind expansion.** If the source uses Tailwind (className="bg-primary text-white rounded-lg px-6 py-3"), expand every utility to literal CSS properties in the `css` string. Do **not** reference Tailwind classes; do **not** assume a Tailwind CSS bundle is loaded. Each component is self-contained.
|
||||
2. **Token resolution.** If the project exposes tokens as CSS custom properties on `:root` (e.g. `--color-primary`, `--radius-md`), reference them via `var(--color-primary)`; they inherit through the shadow DOM and stay live-bound. If tokens live only in JS theme objects (styled-components, CSS-in-JS), resolve to literal values at generation time.
|
||||
3. **Icons.** Inline as SVG. Do not reference Lucide/Heroicons packages, icon fonts, or `<img src="...">`. A typical icon is 16-24px; copy the SVG path data directly.
|
||||
3. **Icons.** Inline as SVG. Do not reference Lucide/Heroicons packages, icon fonts, or `<img src="...">`. A typical icon is 16-24px; copy the SVG path data directly. This is about how a snippet ships, not about which family the project draws from: when the design names an icon set, keep using that set's glyphs and paste their path data in.
|
||||
4. **States.** Include `:hover`, `:focus-visible`, and (if meaningful) `:active` rules inline. A static default-only snapshot makes the panel feel dead. Hover + focus rules in the CSS make it feel alive.
|
||||
5. **Reset bloat.** Extract only the component's *distinctive* CSS (background, color, padding, border-radius, typography, transition). Skip universal resets (`box-sizing: border-box`, `line-height: inherit`, `-webkit-font-smoothing`). The panel already has a neutral canvas; don't re-ship resets.
|
||||
6. **Scoped class names.** Prefix every class with `ds-` (e.g. `ds-btn-primary`, `ds-input-search`) so component CSS doesn't collide with other components' CSS in the same shadow DOM.
|
||||
@@ -349,46 +350,138 @@ Your own write is the freshest source; subsequent commands in this session don't
|
||||
|
||||
## Seed mode
|
||||
|
||||
For projects with no visual system to extract yet. Produces a user-chosen visual-world scaffold, not a fabricated token spec.
|
||||
|
||||
### Step 1: Route through new-work's workshop
|
||||
For projects with no visual system to extract yet. Produces a minimal, user-chosen scaffold, not a fabricated token spec.
|
||||
|
||||
PRODUCT.md is the prerequisite. If it is missing, load [init.md](init.md) and complete its product interview first. Do not create a visual identity without durable product context.
|
||||
|
||||
If PRODUCT.md exists, load [new-work.md](new-work.md) and resolve visual authority. Seed mode requires a concrete first surface: use the target the user named, or ask what they want to make first. Run new-work's **Create or replace the visual world** flow, then **Commit the world**, so the visual world and its first expression are chosen together. Stop after the directional DESIGN.md seed and surface brief; do not implement. A structured simulated user counts as the user and must get the same choice.
|
||||
### Step 1: Decide the path, confirm seed mode, and ask for assets
|
||||
|
||||
If new-work already completed the workshop in this session, use its chosen direction directly. Do not ask again.
|
||||
The browser questionnaire asks color strategy and motion per surface and picks concrete typefaces and a type scale by eye, so whether it will run decides what the chat interview may ask. Decide the path **before the first question**, never after the interview:
|
||||
|
||||
### Step 2: Write seed DESIGN.md
|
||||
- **The harness has native image generation** (Codex's `image_gen`, an equivalent MCP tool, or similar): the questionnaire path; the cues are generated directly at Step 4, no setup needed. This branch wins even when `.impeccable/.env` already holds an `IMAGE_GEN_API_KEY` or an earlier run in another harness left a wrapper script behind; those are fallbacks for keyless harnesses, not the preferred path. A native tool that **cannot generate** (zero credits, failed auth) counts as absent: fall through to the next branch without asking, and mention the swap in the final report.
|
||||
- **No usable native path, key already in `.impeccable/.env`**: the questionnaire path, with no pause and no questions. Load [image-api.md](image-api.md) and use its shipped wrapper; it pre-answers everything this path has ever stopped to ask, including which provider the key belongs to.
|
||||
- **No usable native path, no key**: pause. {{ask_instruction}} Ask whether the user wants generated visual cues to pick a palette by eye. *"I can generate a few small palette-and-mood images so you choose a direction visually instead of from descriptions. That needs an image-generation API key (FLUX and Google Nano Banana are supported out of the box; other providers work too), stored as `IMAGE_GEN_API_KEY` in `.impeccable/.env`. Add one, or skip straight to the chat interview?"* If a key arrives, write it to `.impeccable/.env` together with `IMAGE_GEN_PROVIDER` (`bfl` for FLUX, `gemini` for Nano Banana, the provider's own name for anything else; when the user does not say, let the wrapper infer it from the key). Confirm that file is listed in the project's `.gitignore` (add it if missing; a committed key is a leak), then load [image-api.md](image-api.md). Its shipped wrapper is the whole integration for the built-in providers; only a provider it does not know earns the project-local wrapper that file specifies. A key arriving makes this the questionnaire path.
|
||||
- **The user opts out, or no key arrives**: the interview-only path. The assets ask below, the five questions in Step 3, then Steps 5-6 from the interview alone.
|
||||
|
||||
Use the canonical section order from Scan mode. Populate the selected workshop direction and leave unresolved implementation facts as honest placeholders. The seed commits a world and its invariants; it does not pretend implementation tokens already exist.
|
||||
Then confirm seed mode and ask for assets, framed for the path:
|
||||
|
||||
Lead the file with:
|
||||
- **Questionnaire path**: *"There's no existing visual system to scan. You'll pick the visual direction by eye in a browser questionnaire; before I generate its options, three quick things. First: if you have any visual assets (a logo, reference or product images, moodboards), drop them in or point me at the files. They're extra context that makes the first DESIGN.md seed more accurate. You can re-run `/impeccable document` once there's code, to capture the real tokens and components. OK?"*
|
||||
- **Interview-only path**: *"There's no existing visual system to scan. I'll ask five quick questions to seed a starter DESIGN.md. First: if you have any visual assets (a logo, reference or product images, moodboards), drop them in or point me at the files. They'll ground the questions in what you already have. You can re-run `/impeccable document` once there's code, to capture the real tokens and components. OK?"*
|
||||
|
||||
Also glance for assets already in the project (`assets/`, `public/`, `brand/`, image files at the root); name anything found so the user can confirm it's relevant. Assets are optional: one ask, then proceed with whatever arrived.
|
||||
|
||||
If the user prefers to skip entirely, stop. No file.
|
||||
|
||||
### Step 2: Read the assets
|
||||
|
||||
Look at every asset provided (attached in chat or a file path) and record what it tells you, before writing the questions:
|
||||
|
||||
- **Logo**: sample the exact colors, note letterform character (geometric / humanist / serif) and temperature.
|
||||
- **Reference / product images**: density, palette, type feel; what the user is drawn to.
|
||||
- **Moodboards**: recurring hues, textures, era, register cues.
|
||||
|
||||
On the questionnaire path, the files themselves also feed the design context document the picker shows after the last question. When the user provided actual files (a logo, a mood board, a reference image), copy each one into `.impeccable/design-context/assets/`, keeping its filename. Record every staged file for Step 4's context write: it becomes an object entry in `context.json` `context.assets`, `{ "file": "<filename>", "kind": "logo" | "moodboard" | "reference", "note": "<one-line observation>" }`, where the note is what this step read off it. An observation with no file behind it stays a plain string entry, as before. On the interview-only path, stage nothing; the observations feed the questions and the seed alone.
|
||||
|
||||
These observations exist to sharpen Step 3. **No assets: skip straight to Step 3** with generic options.
|
||||
|
||||
### Step 3: The interview
|
||||
|
||||
Group each path's questions into one `AskUserQuestion` interaction. Options must be concrete. Keep skill vocabulary (seed, register, anti-reference) out of question text; ask for the thing in words the user would use. Ask like a magazine editor profiling the brand: curious and narrative, drawing out the feel the surface should carry.
|
||||
|
||||
**Questionnaire path: two questions, nothing more.** With Step 1's assets ask these are the whole chat interview; the questionnaire asks everything else by eye.
|
||||
|
||||
1. **Three named references.** Brands, products, printed objects. Not adjectives. When Step 2 produced observations, ground candidate names in them (references drawn from the moodboard's era).
|
||||
2. **One anti-reference.** What the product should NOT feel like. Also named.
|
||||
|
||||
**Do not ask about color, typography, or motion here; the questionnaire owns them.** It asks color strategy and motion per surface and picks concrete typefaces and a type scale, so a chat answer would be asked again by eye and one of the two would be thrown away. Both answered, go straight to Step 4.
|
||||
|
||||
**Interview-only path: five questions.** When Step 2 produced observations, ground the options in them: offer the logo's sampled color as a hue anchor in Q1, a type direction that matches the letterforms in Q2, candidate named references drawn from the moodboard's era in Q4. The user should recognize their own material in the choices.
|
||||
|
||||
1. **Color strategy.** Pick one:
|
||||
- Restrained: tinted neutrals + one accent ≤10%
|
||||
- Committed: one saturated color carries 30–60% of the surface
|
||||
- Full palette: 3–4 named color roles, each deliberate
|
||||
- Drenched: the surface IS the color
|
||||
|
||||
Then: one hue family or anchor reference ("deep teal", "mustard", "Klim #ff4500 orange").
|
||||
|
||||
2. **Typography direction.** Pick one (specific fonts come later):
|
||||
- Serif display + sans body
|
||||
- Single sans (warm / technical / geometric / humanist; pick a feel)
|
||||
- Display + mono
|
||||
- Mono-forward
|
||||
- Editorial script + sans
|
||||
|
||||
3. **Motion energy.** Pick one:
|
||||
- Restrained: state changes only
|
||||
- Responsive: feedback + transitions, no choreography
|
||||
- Choreographed: orchestrated entrances, scroll-driven sequences
|
||||
|
||||
4. **Three named references.** Brands, products, printed objects. Not adjectives.
|
||||
|
||||
5. **One anti-reference.** What it should NOT feel like. Also named.
|
||||
|
||||
### Step 4: Launch the questionnaire (questionnaire path only)
|
||||
|
||||
**Interview-only path: skip this step.** Go to Step 5 and seed from the answers alone. Step 1 already settled the capability question; do not re-open it here.
|
||||
|
||||
On the questionnaire path, **stop and load [visual-cues.md](visual-cues.md)** and follow its pipeline; it owns everything from the one-line user announcement and the persona palette studio through generation, `cues.json`, and the picker pause. Do not restate its mechanics here or in chat. The picker's exit is the handoff: when the server exits 0 and `.impeccable/design-context/answers.json` lands, come back here and run Steps 5-6 with that file in hand.
|
||||
|
||||
### Step 5: Write seed DESIGN.md
|
||||
|
||||
Use the canonical section order from Scan mode. Populate what the interview, the assets, and the questionnaire answer; leave the rest as honest placeholders. The seed is a scaffold, not a fabricated spec, but a decision the user actually made in the picker is real and belongs in the file at full strength.
|
||||
|
||||
Mark the file as a seed with this comment as the first line of the markdown body, immediately after the frontmatter's closing `---` (the frontmatter must open the file or token parsers will not see it):
|
||||
|
||||
```markdown
|
||||
<!-- SEED: established with the user before implementation; re-run /impeccable document once there's code to capture the actual tokens and components. -->
|
||||
```
|
||||
|
||||
Per-section guidance in seed mode:
|
||||
**Two seeds exist**, and which one you write depends on whether Step 4's picker ran:
|
||||
|
||||
- **Overview**: the chosen design thesis, layout behavior, material character, imagery stance, motion grammar, and reusable signature. Keep the selected first-surface expression in its surface brief; do not promote its composition into the global world.
|
||||
- **Colors**: the selected palette strategy and roles. Include values only when the user, an existing asset, or new-work's exploration established them; otherwise mark them `[to be resolved during implementation]`.
|
||||
- **Typography**: the selected type character and role relationship. Include font names only when established; otherwise mark the pairing `[to be resolved during implementation]`.
|
||||
- **Layout**: the selected spatial grammar and responsive behavior, without pretending exact measurements are settled.
|
||||
- **Elevation & Depth**: the selected material and depth behavior, stated as an invariant rather than inferred from a generic preset.
|
||||
- **Shapes**: the selected form and corner language.
|
||||
**Interview-only seed** (the user opted out of generation, or no key arrived). Per-section guidance:
|
||||
|
||||
- **Overview**: Creative North Star and philosophy phrased from the answers (color strategy + motion energy + references). Reference the user's anti-reference directly.
|
||||
- **Colors**: Color strategy as a Named Rule (e.g. *"The Drenched Rule. The surface IS the color."*). Hue family or anchor reference. Colors sampled from a provided logo are real; include them with exact values and note the source. Everything else stays `[to be resolved during implementation]`; those sampled anchors are the only hex this seed may carry.
|
||||
- **Typography**: the direction the user picked (e.g. "Serif display + sans body"). No font names yet: `[font pairing to be chosen at implementation]`.
|
||||
- **Layout** and **Shapes**: omit unless an asset or answer established a spatial or form preference; do not invent grids or corner language pre-implementation.
|
||||
- **Elevation & Depth**: inferred from motion energy. Restrained/Responsive → flat by default; Choreographed → layered. One sentence.
|
||||
- **Components**: omit entirely; no components exist yet.
|
||||
- **Do's and Don'ts**: record the durable guardrails confirmed during the world choice, not task-local refusals.
|
||||
- **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5.
|
||||
|
||||
Seed mode writes a minimal frontmatter with `name` and `description` only; no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `.impeccable/design.json` sidecar in seed mode for the same reason: nothing to render.
|
||||
This seed writes a minimal frontmatter with `name` and `description` only; no colors, typography, rounded, spacing, or components yet.
|
||||
|
||||
### Step 3: Confirm
|
||||
**Questionnaire seed** (`.impeccable/design-context/answers.json` exists from this run). The user answered every screen by eye, so the seed carries their answers as decisions, not directions. **`_chosen` names the fields they actually set**: it holds a JSON-encoded array of per-surface keys, and a `<key>-<mode>` field missing from that array is a **preset** the picker minted when the surface was switched on, not an answer. Read the answers file plus the picked cue's palette entry in `.impeccable/visual-cues/cues.json` (`palette-source` names it), and map:
|
||||
|
||||
- **Frontmatter**: `name` and `description`, plus real `colors` (the four `palette-*` hex values under descriptive slugs; these are picked, not sampled) and real `typography` (`font-heading` and `font-body` are exact family names; give each role its family and weight intent, leave sizes for implementation). Derive the two text inks and record them under `colors` too: one near-black and one near-white, the pair the picker's previews already set their text in over these exact surfaces, each holding 4.5:1 against the grounds it will carry copy on, so a builder needing body-text contrast finds ink in the system instead of inventing a fifth color. Still no `rounded`, `spacing`, or `components`: the corner and spacing answers are qualitative, and nothing is built.
|
||||
- **Overview**: Creative North Star and philosophy phrased from the questionnaire's color-strategy and motion answers plus the chat references; reference the user's anti-reference directly. Name the chosen surfaces (`surface-modes`) and what each is for. Movement stays here, after the North Star, but the questionnaire asks it of a landing page and a portfolio only, so write what the keys support:
|
||||
- `motion-energy-<mode>` keys present, all agreeing: one philosophy sentence for the product, as before.
|
||||
- Keys present and disagreeing: one sentence per surface, named (*"The landing page moves on state change only; the portfolio stages entrances and drives sequences on scroll."*). The bare `motion-energy` is the leading one of the two.
|
||||
- **No `motion-energy` key at all**: the run has neither of those surfaces, so movement was never asked. Say nothing about it, and do not fill the gap from the register; this path's chat interview never asked about motion, so there is nothing to borrow. The next Scan-mode run reads the real transitions out of the code.
|
||||
- **Colors**: the four roles with their picked hex, noting the cue they came from. Name the chosen cue by its slug and name its kept image at `.impeccable/design-context/cue.png`, so a later build opens the picture the palette came from instead of imagining it; note that the unpicked cue images stay in `.impeccable/visual-cues/` for later art direction. Then open the kept image and describe it into the same section, three or four sentences under a **Cue, in words:** lead: the physical material each palette role lives on in the picture (cloth, glass, enamel, paper), the light and its temperature, the surface finish and grain, and the one material move that makes the image itself. Name each color as it appears on its material; `#C92823` as soft matte wrapping cloth instructs an image model where the bare hex only tints. Generation prompts on this world restate this passage (new-work.md and visualize.md say where), so a seed that records only the cue's file path leaves the material world to the model's imagination. The **chosen** strategy becomes the Named Rule. When surfaces differ (`color-strategy-<mode>` keys), state each surface's strategy and which surface leads (the bare key's owner).
|
||||
- **Typography**: the real pair by name, the pairing's character, and the type scale as a rule: `type-scale` names it, `type-scale-ratio` is the ratio (e.g. *"Major third: each heading step is 1.25x the last"*). Base size and exact steps stay `[resolved at implementation]`. A `font-heading-source` / `font-body-source` value means a user-provided font file; record where it lives.
|
||||
- **Layout**: `boundary-style` (how sections separate) per surface when the `-<mode>` keys differ, plus `layout-structure` (how pages are composed), which the questionnaire asks of a landing page and a portfolio only. No invented grids beyond what the answers state.
|
||||
- `layout-structure` present: one bare key and no `-<mode>` keys, so state it as a rule for the whole product rather than per surface.
|
||||
- **No `layout-structure` key at all**: the run has neither of those surfaces, so composition was never asked. Say nothing about how strict the grid is, and let `boundary-style` carry the section.
|
||||
- **Elevation & Depth**: `depth-style` per surface, stated directly; the questionnaire answered this, so do not re-infer it from motion energy.
|
||||
- **Shapes**: `corner-style` per surface.
|
||||
- **Components**: still omit; nothing exists yet.
|
||||
- **Do's and Don'ts**: the interview-only guidance, plus a Do fixing the icon source: every icon comes from the chosen pack (`icon-pack-name`, license, URL), no mixed sets. When the interview staged brand files (`context.assets` object entries in `.impeccable/design-context/context.json`), add one Do per file naming its path under `.impeccable/design-context/assets/`, its kind, and its note; a staged logo is the product's real mark and the build uses the file itself.
|
||||
|
||||
Per-surface answers come back for every chosen surface, presets included, and a difference between surfaces is a decision the picker enforced, not an inconsistency to smooth over (the option lists differ per surface, so a pick one surface allows can be unavailable on another and that surface falls to its preset). **Write a preset as provisional**, on the surface's own line: name the value, say it is that surface's default because the surface was never configured, and keep it out of the Named Rules and out of every product-wide sentence. Naming an untouched preset as a rule invents a law the user never chose. Where all surfaces agree **and `_chosen` shows the agreement was picked**, state the answer once for the product. `motion-energy` and `layout-structure` are the two keys that can be missing entirely, since movement and composition are asked of a landing page and a portfolio only; [visual-cues.md](visual-cues.md) has the full contract.
|
||||
|
||||
Both seeds skip the `.impeccable/design.json` sidecar: nothing to render yet. Real tokens for sizes, spacing, and components land on the next Scan-mode run.
|
||||
|
||||
### Step 6: Confirm
|
||||
|
||||
1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment).
|
||||
2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar."
|
||||
3. On the questionnaire path, add one line: the interview is kept, and `{{command_prefix}}impeccable design-context` reopens the document, re-runs the questionnaire over these answers, or writes the context out for another tool. See [design-context.md](design-context.md).
|
||||
|
||||
Your own write is the freshest source; no reload needed.
|
||||
|
||||
When the questionnaire ran, the confirm is not the end of the turn: the design context document in the user's tab is live for edits through the session the picker forked. Follow the document edit loop in [visual-cues.md](visual-cues.md): poll, apply `edit_request`s to this same DESIGN.md, reply. A color the user changed in the tab before your seed write is already in `answers.json`; one changed after arrives as a `save_batch` event, its value already in the store and its description in DESIGN.md yours to bring in line.
|
||||
|
||||
## Style guidelines
|
||||
|
||||
- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places; the frontmatter is normative.
|
||||
|
||||
@@ -102,6 +102,7 @@ node {{scripts_path}}/hook-admin.mjs ignore-file "src/legacy/Card.tsx"
|
||||
|
||||
- Never modify `.impeccable/config.json` or `.impeccable/config.local.json` by hand from this command. Always go through `hook-admin.mjs` so writes stay validated and the file shape stays consistent. One exception: `detector.extensions` has no admin action, so when the user asks to cover a template stack, edit that one field in `.impeccable/config.json` directly and leave the rest of the file untouched.
|
||||
- Do not edit the hook scripts themselves (`hook.mjs`, `hook-lib.mjs`, `hook-before-edit.mjs`) from this flow. Those are skill plumbing.
|
||||
- The design context document's Hooks page reads and writes this same config through `hook-admin.mjs` (`state` and `apply`, its machine channel, called by the doc session); those two verbs are not part of this command's routing. Entries it wrote are user decisions: the person pressed Apply in the page, so treat them like any user-made config.
|
||||
- Cursor can block a proposed write when the detector finds a real issue. Claude Code, Codex, and GitHub Copilot do not block the edit; they emit a post-edit reminder instead. Disabling stops both blocking and reminders.
|
||||
- The hook is bundled with the Impeccable skill and installed through project-local manifests: `.claude/settings.local.json`, `.codex/hooks.json`, `.cursor/hooks.json`, and `.github/hooks/impeccable.json`. On Codex, the user must approve the hook via `/hooks` the first time. On Cursor, confirm hooks are enabled under Settings -> Hooks. On GitHub Copilot, the CLI loads `.github/hooks/impeccable.json` once it is committed to the repository's default branch, and the cloud agent reads it from the repo directly.
|
||||
|
||||
|
||||