feat: gallery + generic blueprint viewer, add Grid blueprint
Refactor the List-specific screen into a data-driven, reusable viewer: - generic Blueprint type + shared LoadState machine + registry - vue-router: / (gallery) and /b/:slug (illustration) - BlueprintViewer renders any blueprint; specimens stay bespoke - add Grid blueprint (6 functions incl. Position) + GridSpecimen
This commit is contained in:
65
CONTEXT.md
Normal file
65
CONTEXT.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Context — ubiquitous language
|
||||||
|
|
||||||
|
The shared vocabulary for `blueprints`. These terms should appear verbatim in
|
||||||
|
conversation, code, file names, and commits. This file is a glossary only — no
|
||||||
|
implementation details (those live in `DESIGN.md`).
|
||||||
|
|
||||||
|
## Blueprint
|
||||||
|
|
||||||
|
A named contract for a recurring UI pattern, taken from the `blueprint-ontology`
|
||||||
|
repo — declarative, categorical, abstract (a _concept_ in Jackson's sense). It
|
||||||
|
defines what must be true of the pattern: its signature, state machine, behaviors,
|
||||||
|
invariants, functions, critical-performance thresholds, and failure modes. Examples:
|
||||||
|
`List`, `Grid`, `Table`, `Async`.
|
||||||
|
|
||||||
|
Blueprints come in two roles (from the ontology):
|
||||||
|
|
||||||
|
- **Feature** — a standalone pattern a user interacts with (`List`, `Grid`, `Form`).
|
||||||
|
- **Capability** — a composable blueprint that adds state/behavior onto a host
|
||||||
|
feature (`Async`, `Paginated`, `Selectable`).
|
||||||
|
|
||||||
|
## Illustration
|
||||||
|
|
||||||
|
This app's rendered, interactive presentation of a single blueprint. `blueprints`
|
||||||
|
is a gallery of illustrations. "Illustration" is the noun for the whole per-blueprint
|
||||||
|
screen; do not use "demo" (implies a live working widget — these are static) or
|
||||||
|
"page" (routing concept, not domain).
|
||||||
|
|
||||||
|
## Viewer
|
||||||
|
|
||||||
|
The one reusable component that renders any blueprint's illustration. It draws the
|
||||||
|
generic parts from a blueprint's typed data and slots in that blueprint's bespoke
|
||||||
|
Specimen. Every illustration is the Viewer applied to one blueprint.
|
||||||
|
|
||||||
|
## Specimen
|
||||||
|
|
||||||
|
The interactive-looking but **static** visual mock of the blueprint's actual UI —
|
||||||
|
the left pane of the Viewer. Bespoke per blueprint (a Grid specimen looks nothing
|
||||||
|
like a List's). Selecting a Function highlights the region of the specimen that
|
||||||
|
function owns. Not "mockup", not "demo".
|
||||||
|
|
||||||
|
## Readout
|
||||||
|
|
||||||
|
The right pane of the Viewer: the blueprint's formal contract for the selected
|
||||||
|
Function — source capability, responsibility, state touched, behaviors (pre →
|
||||||
|
effect), invariants, failure modes, critical-performance thresholds.
|
||||||
|
|
||||||
|
## Companion
|
||||||
|
|
||||||
|
A supporting schematic shown below the Viewer's two panes. Two kinds exist so far:
|
||||||
|
the **composition map** (how a feature extends/composes with capabilities) and the
|
||||||
|
**state machine** (the blueprint's lifecycle). A blueprint may have neither, one, or
|
||||||
|
both.
|
||||||
|
|
||||||
|
## Function
|
||||||
|
|
||||||
|
One row of a blueprint's "Functional analysis" — a named responsibility of the
|
||||||
|
pattern (e.g. List's `Display`, `Scroll`, `Load`). Functions are the unit the Viewer
|
||||||
|
lets you explore, tab by tab. Distinct from **Behavior** (a concrete state
|
||||||
|
transition like `loadMore()`); behaviors are the mechanics underneath a function.
|
||||||
|
|
||||||
|
## Gallery
|
||||||
|
|
||||||
|
The app's index (landing screen): every blueprint in the ontology, each as a card
|
||||||
|
linking to its illustration — including blueprints not yet illustrated, shown as
|
||||||
|
**planned**.
|
||||||
70
DESIGN.md
Normal file
70
DESIGN.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Design — `blueprints`
|
||||||
|
|
||||||
|
Decisions for building `blueprints` out from a single List illustration into a
|
||||||
|
gallery of many. Terminology: see [CONTEXT.md](CONTEXT.md).
|
||||||
|
|
||||||
|
## D1 · Content sourcing: hand-authored typed data + bespoke specimen
|
||||||
|
|
||||||
|
Each blueprint is a **typed TS data module** (`src/data/<name>Blueprint.ts`),
|
||||||
|
transcribed by hand from its ontology README, paired with a **bespoke Specimen**
|
||||||
|
component. The generic **Viewer** renders everything else (Readout, composition map,
|
||||||
|
state machine) from that data.
|
||||||
|
|
||||||
|
- **Why not parse the ontology at build time?** The Specimen must be hand-built for
|
||||||
|
every blueprint regardless (the ontology only provides an ASCII sketch), so parsing
|
||||||
|
would automate only the cheap, structured text — at the cost of fragile
|
||||||
|
markdown/mermaid parsing and a build-time coupling to the ontology repo's exact
|
||||||
|
format. Not worth it.
|
||||||
|
- **Source of truth** stays the `blueprint-ontology` repo; the data modules are a
|
||||||
|
faithful transcription, cited in each module's header.
|
||||||
|
|
||||||
|
Structural split this implies:
|
||||||
|
|
||||||
|
| Part | Generic (data-driven) | Bespoke (per blueprint) |
|
||||||
|
| ---------------------------------- | ------------------------------------ | ----------------------------------- |
|
||||||
|
| Chrome (title block, tabs, legend) | ✓ | |
|
||||||
|
| Readout (dossier per function) | ✓ | |
|
||||||
|
| Composition map | ✓ (from `extends` + `composes` data) | |
|
||||||
|
| State machine | ✓ (from nodes/edges data) | |
|
||||||
|
| Specimen | | ✓ component slotted into the Viewer |
|
||||||
|
|
||||||
|
## D2 · Routing: vue-router, URL per blueprint
|
||||||
|
|
||||||
|
Add `vue-router`. Routes: `/` → Gallery, `/b/:slug` → the blueprint's illustration
|
||||||
|
(e.g. `/b/list`). Deep-linkable and shareable; browser back/forward works. The nginx
|
||||||
|
SPA fallback (`try_files … /index.html`) already serves client routes, so no server
|
||||||
|
change is needed. A blueprint registry (`src/data/blueprints.ts`) maps `slug →
|
||||||
|
{ data module, specimen component, meta }` and is the single source both the router
|
||||||
|
and the Gallery read from.
|
||||||
|
|
||||||
|
## D3 · Gallery: illustrated-only, grows over time
|
||||||
|
|
||||||
|
The Gallery lists **only** blueprints that have an illustration; it grows as we add
|
||||||
|
them. No greyed "planned" cards. Rationale: keeps the landing clean and truthful, and
|
||||||
|
avoids maintaining metadata for 45 not-yet-built entries. A blueprint appears in the
|
||||||
|
Gallery precisely when it is registered in `src/data/blueprints.ts`.
|
||||||
|
|
||||||
|
## D4 · Build-out slice #1: foundation + Grid
|
||||||
|
|
||||||
|
**Foundation (refactor, no UX change to List):**
|
||||||
|
|
||||||
|
- `pnpm add vue-router`; `App.vue` → thin shell with a header (link home) + `<RouterView/>`.
|
||||||
|
- `src/views/GalleryView.vue` (`/`) — cards from the registry.
|
||||||
|
- `src/views/BlueprintView.vue` (`/b/:slug`) — resolves the registry entry, renders the Viewer.
|
||||||
|
- `src/components/BlueprintViewer.vue` — the **generic Viewer**, generalized from the
|
||||||
|
current List-specific `App.vue`: title block, legend, function tabs + full view,
|
||||||
|
two panes, optional composition map + state machine, footer. Props: a `Blueprint`
|
||||||
|
object + the blueprint's Specimen component (injected via `<component :is>`).
|
||||||
|
- Generic `Blueprint` type in `src/data/blueprint.ts` (signature, functions,
|
||||||
|
coreInvariants, optional composition, optional stateMachine, meta). `listBlueprint.ts`
|
||||||
|
conforms to it. Specimens move to `src/specimens/`.
|
||||||
|
- `src/data/blueprints.ts` — registry: `slug → { meta, blueprint, specimen }`.
|
||||||
|
|
||||||
|
**Content:** `gridBlueprint.ts` + `src/specimens/GridSpecimen.vue` (2D tile grid).
|
||||||
|
|
||||||
|
**Ships:** Gallery listing List + Grid; `/b/list` (same UX, refactored) and `/b/grid`.
|
||||||
|
|
||||||
|
**Gates before push:** `pnpm build` clean · `pnpm lint` · `pnpm fmt`; then commit → push
|
||||||
|
(auto-deploys) → verify `https://blueprints.apoena.dev`.
|
||||||
|
|
||||||
|
Subsequent slices add one blueprint each (Table, Feed, …).
|
||||||
@@ -11,6 +11,15 @@ The first screen illustrates the **List** blueprint and its seven functions —
|
|||||||
Display · Scroll · Load · Filter · Sort · Select · Act — as a specimen-under-glass with a
|
Display · Scroll · Load · Filter · Sort · Select · Act — as a specimen-under-glass with a
|
||||||
full contract readout, an anatomy/composition map, and the `LoadState` machine.
|
full contract readout, an anatomy/composition map, and the `LoadState` machine.
|
||||||
|
|
||||||
|
<!-- docs:start -->
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [CONTEXT.md](CONTEXT.md) — ubiquitous language (Blueprint, Viewer, Specimen, Readout, …)
|
||||||
|
- [DESIGN.md](DESIGN.md) — architecture & build-out decisions
|
||||||
|
|
||||||
|
<!-- docs:end -->
|
||||||
|
|
||||||
## Develop
|
## Develop
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"daisyui": "^5.6.6",
|
"daisyui": "^5.6.6",
|
||||||
"vue": "^3.5.39"
|
"vue": "^3.5.39",
|
||||||
|
"vue-router": "^5.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
|
|||||||
373
pnpm-lock.yaml
generated
373
pnpm-lock.yaml
generated
@@ -14,16 +14,19 @@ importers:
|
|||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.39
|
specifier: ^3.5.39
|
||||||
version: 3.5.39(typescript@6.0.3)
|
version: 3.5.39(typescript@6.0.3)
|
||||||
|
vue-router:
|
||||||
|
specifier: ^5.1.0
|
||||||
|
version: 5.1.0(@vue/compiler-sfc@3.5.39)(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39(typescript@6.0.3))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.3.2
|
specifier: ^4.3.2
|
||||||
version: 4.3.2(vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0))
|
version: 4.3.2(vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.13.2
|
specifier: ^24.13.2
|
||||||
version: 24.13.2
|
version: 24.13.2
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^6.0.7
|
specifier: ^6.0.7
|
||||||
version: 6.0.7(vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0))(vue@3.5.39(typescript@6.0.3))
|
version: 6.0.7(vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39(typescript@6.0.3))
|
||||||
'@vue/tsconfig':
|
'@vue/tsconfig':
|
||||||
specifier: ^0.9.1
|
specifier: ^0.9.1
|
||||||
version: 0.9.1(typescript@6.0.3)(vue@3.5.39(typescript@6.0.3))
|
version: 0.9.1(typescript@6.0.3)(vue@3.5.39(typescript@6.0.3))
|
||||||
@@ -41,30 +44,51 @@ importers:
|
|||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^8.1.1
|
specifier: ^8.1.1
|
||||||
version: 8.1.2(@types/node@24.13.2)(jiti@2.7.0)
|
version: 8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
|
||||||
vue-tsc:
|
vue-tsc:
|
||||||
specifier: ^3.3.5
|
specifier: ^3.3.5
|
||||||
version: 3.3.6(typescript@6.0.3)
|
version: 3.3.6(typescript@6.0.3)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@babel/generator@8.0.0':
|
||||||
|
resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==}
|
||||||
|
engines: {node: ^22.18.0 || >=24.11.0}
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.29.7':
|
'@babel/helper-string-parser@7.29.7':
|
||||||
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@8.0.0':
|
||||||
|
resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==}
|
||||||
|
engines: {node: ^22.18.0 || >=24.11.0}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.29.7':
|
'@babel/helper-validator-identifier@7.29.7':
|
||||||
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@8.0.2':
|
||||||
|
resolution: {integrity: sha512-9Fr9QeyCAyi1BR1jKZ6uYQ24EIhQUx5ReHfQU7drOE+TPOb+w11/dsqLkMOT2U29OdCT71XajrOT8xDc1C7orA==}
|
||||||
|
engines: {node: ^22.18.0 || >=24.11.0}
|
||||||
|
|
||||||
'@babel/parser@7.29.7':
|
'@babel/parser@7.29.7':
|
||||||
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
|
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@babel/parser@8.0.0':
|
||||||
|
resolution: {integrity: sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ==}
|
||||||
|
engines: {node: ^22.18.0 || >=24.11.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@babel/types@7.29.7':
|
'@babel/types@7.29.7':
|
||||||
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/types@8.0.0':
|
||||||
|
resolution: {integrity: sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==}
|
||||||
|
engines: {node: ^22.18.0 || >=24.11.0}
|
||||||
|
|
||||||
'@emnapi/core@1.11.1':
|
'@emnapi/core@1.11.1':
|
||||||
resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
|
resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
|
||||||
|
|
||||||
@@ -538,6 +562,9 @@ packages:
|
|||||||
'@tybys/wasm-util@0.10.3':
|
'@tybys/wasm-util@0.10.3':
|
||||||
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
||||||
|
|
||||||
|
'@types/jsesc@2.5.1':
|
||||||
|
resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==}
|
||||||
|
|
||||||
'@types/node@24.13.2':
|
'@types/node@24.13.2':
|
||||||
resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==}
|
resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==}
|
||||||
|
|
||||||
@@ -557,6 +584,15 @@ packages:
|
|||||||
'@volar/typescript@2.4.28':
|
'@volar/typescript@2.4.28':
|
||||||
resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
|
resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
|
||||||
|
|
||||||
|
'@vue-macros/common@3.1.2':
|
||||||
|
resolution: {integrity: sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^2.7.0 || ^3.2.25
|
||||||
|
peerDependenciesMeta:
|
||||||
|
vue:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vue/compiler-core@3.5.39':
|
'@vue/compiler-core@3.5.39':
|
||||||
resolution: {integrity: sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==}
|
resolution: {integrity: sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==}
|
||||||
|
|
||||||
@@ -569,6 +605,15 @@ packages:
|
|||||||
'@vue/compiler-ssr@3.5.39':
|
'@vue/compiler-ssr@3.5.39':
|
||||||
resolution: {integrity: sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==}
|
resolution: {integrity: sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==}
|
||||||
|
|
||||||
|
'@vue/devtools-api@8.1.5':
|
||||||
|
resolution: {integrity: sha512-YJipMVAKe5wT5CWf5kTYCaNV7NMNjFVxJkIkJaJ4W/nCxEBzlZzrOsYKeCymdCrFZmBS/+wTWFoUs3Jf/Q6XSQ==}
|
||||||
|
|
||||||
|
'@vue/devtools-kit@8.1.5':
|
||||||
|
resolution: {integrity: sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA==}
|
||||||
|
|
||||||
|
'@vue/devtools-shared@8.1.5':
|
||||||
|
resolution: {integrity: sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ==}
|
||||||
|
|
||||||
'@vue/language-core@3.3.6':
|
'@vue/language-core@3.3.6':
|
||||||
resolution: {integrity: sha512-LgBMZAy2sR3cQWknpyaxnI6yBkqDfLBPkbdhwRhQCvzfNJRQXPilgQIrdI/v4ytJ0sAq9bWhaPsjqBqneomJ3Q==}
|
resolution: {integrity: sha512-LgBMZAy2sR3cQWknpyaxnI6yBkqDfLBPkbdhwRhQCvzfNJRQXPilgQIrdI/v4ytJ0sAq9bWhaPsjqBqneomJ3Q==}
|
||||||
|
|
||||||
@@ -600,9 +645,35 @@ packages:
|
|||||||
vue:
|
vue:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
acorn@8.17.0:
|
||||||
|
resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
alien-signals@3.2.1:
|
alien-signals@3.2.1:
|
||||||
resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
|
resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
|
||||||
|
|
||||||
|
ast-kit@2.2.0:
|
||||||
|
resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
|
ast-walker-scope@0.9.0:
|
||||||
|
resolution: {integrity: sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
|
birpc@2.9.0:
|
||||||
|
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
|
||||||
|
|
||||||
|
chokidar@5.0.0:
|
||||||
|
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
||||||
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
|
confbox@0.1.8:
|
||||||
|
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
|
||||||
|
|
||||||
|
confbox@0.2.4:
|
||||||
|
resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==}
|
||||||
|
|
||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
@@ -624,6 +695,9 @@ packages:
|
|||||||
estree-walker@2.0.2:
|
estree-walker@2.0.2:
|
||||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
|
exsolve@1.1.0:
|
||||||
|
resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==}
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -641,10 +715,23 @@ packages:
|
|||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
|
hookable@5.5.3:
|
||||||
|
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||||
|
|
||||||
jiti@2.7.0:
|
jiti@2.7.0:
|
||||||
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jsesc@3.1.0:
|
||||||
|
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
json5@2.2.3:
|
||||||
|
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
lightningcss-android-arm64@1.32.0:
|
lightningcss-android-arm64@1.32.0:
|
||||||
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@@ -719,9 +806,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
local-pkg@1.2.1:
|
||||||
|
resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
magic-string-ast@1.0.3:
|
||||||
|
resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
|
mlly@1.8.2:
|
||||||
|
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||||
|
|
||||||
muggle-string@0.4.1:
|
muggle-string@0.4.1:
|
||||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||||
|
|
||||||
@@ -759,6 +857,12 @@ packages:
|
|||||||
path-browserify@1.0.1:
|
path-browserify@1.0.1:
|
||||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||||
|
|
||||||
|
pathe@2.0.3:
|
||||||
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
|
perfect-debounce@2.1.0:
|
||||||
|
resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -766,15 +870,31 @@ packages:
|
|||||||
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
pkg-types@1.3.1:
|
||||||
|
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||||
|
|
||||||
|
pkg-types@2.3.1:
|
||||||
|
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
|
||||||
|
|
||||||
postcss@8.5.16:
|
postcss@8.5.16:
|
||||||
resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
|
resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
quansync@0.2.11:
|
||||||
|
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
||||||
|
|
||||||
|
readdirp@5.0.0:
|
||||||
|
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
|
||||||
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
rolldown@1.1.4:
|
rolldown@1.1.4:
|
||||||
resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==}
|
resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
scule@1.3.0:
|
||||||
|
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -802,9 +922,49 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
ufo@1.6.4:
|
||||||
|
resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
|
||||||
|
|
||||||
undici-types@7.18.2:
|
undici-types@7.18.2:
|
||||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||||
|
|
||||||
|
unplugin-utils@0.3.2:
|
||||||
|
resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
|
unplugin@3.3.0:
|
||||||
|
resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@farmfe/core': '*'
|
||||||
|
'@rspack/core': '*'
|
||||||
|
bun-types-no-globals: '*'
|
||||||
|
esbuild: '*'
|
||||||
|
rolldown: '*'
|
||||||
|
rollup: '*'
|
||||||
|
unloader: '*'
|
||||||
|
vite: '*'
|
||||||
|
webpack: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@farmfe/core':
|
||||||
|
optional: true
|
||||||
|
'@rspack/core':
|
||||||
|
optional: true
|
||||||
|
bun-types-no-globals:
|
||||||
|
optional: true
|
||||||
|
esbuild:
|
||||||
|
optional: true
|
||||||
|
rolldown:
|
||||||
|
optional: true
|
||||||
|
rollup:
|
||||||
|
optional: true
|
||||||
|
unloader:
|
||||||
|
optional: true
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
webpack:
|
||||||
|
optional: true
|
||||||
|
|
||||||
vite@8.1.2:
|
vite@8.1.2:
|
||||||
resolution: {integrity: sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==}
|
resolution: {integrity: sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -851,6 +1011,24 @@ packages:
|
|||||||
vscode-uri@3.1.0:
|
vscode-uri@3.1.0:
|
||||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||||
|
|
||||||
|
vue-router@5.1.0:
|
||||||
|
resolution: {integrity: sha512-HAbiLzLEHQwxPgvsbOJDAwtavszEgLwri6XfyrsPECIFez8+59xc9LofWVdc/HEaSRT822lJ8H9Ns38VVond5g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@pinia/colada': '>=0.21.2'
|
||||||
|
'@vue/compiler-sfc': ^3.5.34
|
||||||
|
pinia: ^3.0.4
|
||||||
|
vite: ^7.0.0 || ^8.0.0
|
||||||
|
vue: ^3.5.34
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@pinia/colada':
|
||||||
|
optional: true
|
||||||
|
'@vue/compiler-sfc':
|
||||||
|
optional: true
|
||||||
|
pinia:
|
||||||
|
optional: true
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
|
||||||
vue-tsc@3.3.6:
|
vue-tsc@3.3.6:
|
||||||
resolution: {integrity: sha512-ERXGgbKSBGFUkavrJ1Iwj0ZVxKqB/5UOx65IXy7fPf2UsoI21n3ssQLwdvx8xyUGgJe9PvSZTM/FYzIdwUDPFA==}
|
resolution: {integrity: sha512-ERXGgbKSBGFUkavrJ1Iwj0ZVxKqB/5UOx65IXy7fPf2UsoI21n3ssQLwdvx8xyUGgJe9PvSZTM/FYzIdwUDPFA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -865,21 +1043,51 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
webpack-virtual-modules@0.6.2:
|
||||||
|
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
||||||
|
|
||||||
|
yaml@2.9.0:
|
||||||
|
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
|
||||||
|
engines: {node: '>= 14.6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@babel/generator@8.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 8.0.0
|
||||||
|
'@babel/types': 8.0.0
|
||||||
|
'@jridgewell/gen-mapping': 0.3.13
|
||||||
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
'@types/jsesc': 2.5.1
|
||||||
|
jsesc: 3.1.0
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.29.7': {}
|
'@babel/helper-string-parser@7.29.7': {}
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@8.0.0': {}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.29.7': {}
|
'@babel/helper-validator-identifier@7.29.7': {}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@8.0.2': {}
|
||||||
|
|
||||||
'@babel/parser@7.29.7':
|
'@babel/parser@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
|
'@babel/parser@8.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@babel/types': 8.0.0
|
||||||
|
|
||||||
'@babel/types@7.29.7':
|
'@babel/types@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-string-parser': 7.29.7
|
'@babel/helper-string-parser': 7.29.7
|
||||||
'@babel/helper-validator-identifier': 7.29.7
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
|
|
||||||
|
'@babel/types@8.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@babel/helper-string-parser': 8.0.0
|
||||||
|
'@babel/helper-validator-identifier': 8.0.2
|
||||||
|
|
||||||
'@emnapi/core@1.11.1':
|
'@emnapi/core@1.11.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/wasi-threads': 1.2.2
|
'@emnapi/wasi-threads': 1.2.2
|
||||||
@@ -1150,26 +1358,28 @@ snapshots:
|
|||||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.2
|
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.2
|
||||||
'@tailwindcss/oxide-win32-x64-msvc': 4.3.2
|
'@tailwindcss/oxide-win32-x64-msvc': 4.3.2
|
||||||
|
|
||||||
'@tailwindcss/vite@4.3.2(vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0))':
|
'@tailwindcss/vite@4.3.2(vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tailwindcss/node': 4.3.2
|
'@tailwindcss/node': 4.3.2
|
||||||
'@tailwindcss/oxide': 4.3.2
|
'@tailwindcss/oxide': 4.3.2
|
||||||
tailwindcss: 4.3.2
|
tailwindcss: 4.3.2
|
||||||
vite: 8.1.2(@types/node@24.13.2)(jiti@2.7.0)
|
vite: 8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.3':
|
'@tybys/wasm-util@0.10.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/jsesc@2.5.1': {}
|
||||||
|
|
||||||
'@types/node@24.13.2':
|
'@types/node@24.13.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.18.2
|
undici-types: 7.18.2
|
||||||
|
|
||||||
'@vitejs/plugin-vue@6.0.7(vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0))(vue@3.5.39(typescript@6.0.3))':
|
'@vitejs/plugin-vue@6.0.7(vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39(typescript@6.0.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.1
|
'@rolldown/pluginutils': 1.0.1
|
||||||
vite: 8.1.2(@types/node@24.13.2)(jiti@2.7.0)
|
vite: 8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
|
||||||
vue: 3.5.39(typescript@6.0.3)
|
vue: 3.5.39(typescript@6.0.3)
|
||||||
|
|
||||||
'@volar/language-core@2.4.28':
|
'@volar/language-core@2.4.28':
|
||||||
@@ -1184,6 +1394,16 @@ snapshots:
|
|||||||
path-browserify: 1.0.1
|
path-browserify: 1.0.1
|
||||||
vscode-uri: 3.1.0
|
vscode-uri: 3.1.0
|
||||||
|
|
||||||
|
'@vue-macros/common@3.1.2(vue@3.5.39(typescript@6.0.3))':
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-sfc': 3.5.39
|
||||||
|
ast-kit: 2.2.0
|
||||||
|
local-pkg: 1.2.1
|
||||||
|
magic-string-ast: 1.0.3
|
||||||
|
unplugin-utils: 0.3.2
|
||||||
|
optionalDependencies:
|
||||||
|
vue: 3.5.39(typescript@6.0.3)
|
||||||
|
|
||||||
'@vue/compiler-core@3.5.39':
|
'@vue/compiler-core@3.5.39':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
@@ -1214,6 +1434,19 @@ snapshots:
|
|||||||
'@vue/compiler-dom': 3.5.39
|
'@vue/compiler-dom': 3.5.39
|
||||||
'@vue/shared': 3.5.39
|
'@vue/shared': 3.5.39
|
||||||
|
|
||||||
|
'@vue/devtools-api@8.1.5':
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-kit': 8.1.5
|
||||||
|
|
||||||
|
'@vue/devtools-kit@8.1.5':
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-shared': 8.1.5
|
||||||
|
birpc: 2.9.0
|
||||||
|
hookable: 5.5.3
|
||||||
|
perfect-debounce: 2.1.0
|
||||||
|
|
||||||
|
'@vue/devtools-shared@8.1.5': {}
|
||||||
|
|
||||||
'@vue/language-core@3.3.6':
|
'@vue/language-core@3.3.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@volar/language-core': 2.4.28
|
'@volar/language-core': 2.4.28
|
||||||
@@ -1253,8 +1486,31 @@ snapshots:
|
|||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
vue: 3.5.39(typescript@6.0.3)
|
vue: 3.5.39(typescript@6.0.3)
|
||||||
|
|
||||||
|
acorn@8.17.0: {}
|
||||||
|
|
||||||
alien-signals@3.2.1: {}
|
alien-signals@3.2.1: {}
|
||||||
|
|
||||||
|
ast-kit@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.29.7
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
ast-walker-scope@0.9.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.29.7
|
||||||
|
'@babel/types': 7.29.7
|
||||||
|
ast-kit: 2.2.0
|
||||||
|
|
||||||
|
birpc@2.9.0: {}
|
||||||
|
|
||||||
|
chokidar@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
readdirp: 5.0.0
|
||||||
|
|
||||||
|
confbox@0.1.8: {}
|
||||||
|
|
||||||
|
confbox@0.2.4: {}
|
||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
daisyui@5.6.6: {}
|
daisyui@5.6.6: {}
|
||||||
@@ -1270,6 +1526,8 @@ snapshots:
|
|||||||
|
|
||||||
estree-walker@2.0.2: {}
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
|
exsolve@1.1.0: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.4):
|
fdir@6.5.0(picomatch@4.0.4):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
@@ -1279,8 +1537,14 @@ snapshots:
|
|||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
|
hookable@5.5.3: {}
|
||||||
|
|
||||||
jiti@2.7.0: {}
|
jiti@2.7.0: {}
|
||||||
|
|
||||||
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
|
json5@2.2.3: {}
|
||||||
|
|
||||||
lightningcss-android-arm64@1.32.0:
|
lightningcss-android-arm64@1.32.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -1330,10 +1594,27 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.32.0
|
lightningcss-win32-arm64-msvc: 1.32.0
|
||||||
lightningcss-win32-x64-msvc: 1.32.0
|
lightningcss-win32-x64-msvc: 1.32.0
|
||||||
|
|
||||||
|
local-pkg@1.2.1:
|
||||||
|
dependencies:
|
||||||
|
mlly: 1.8.2
|
||||||
|
pkg-types: 2.3.1
|
||||||
|
quansync: 0.2.11
|
||||||
|
|
||||||
|
magic-string-ast@1.0.3:
|
||||||
|
dependencies:
|
||||||
|
magic-string: 0.30.21
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
mlly@1.8.2:
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.17.0
|
||||||
|
pathe: 2.0.3
|
||||||
|
pkg-types: 1.3.1
|
||||||
|
ufo: 1.6.4
|
||||||
|
|
||||||
muggle-string@0.4.1: {}
|
muggle-string@0.4.1: {}
|
||||||
|
|
||||||
nanoid@3.3.15: {}
|
nanoid@3.3.15: {}
|
||||||
@@ -1386,16 +1667,36 @@ snapshots:
|
|||||||
|
|
||||||
path-browserify@1.0.1: {}
|
path-browserify@1.0.1: {}
|
||||||
|
|
||||||
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
|
perfect-debounce@2.1.0: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.4: {}
|
picomatch@4.0.4: {}
|
||||||
|
|
||||||
|
pkg-types@1.3.1:
|
||||||
|
dependencies:
|
||||||
|
confbox: 0.1.8
|
||||||
|
mlly: 1.8.2
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
pkg-types@2.3.1:
|
||||||
|
dependencies:
|
||||||
|
confbox: 0.2.4
|
||||||
|
exsolve: 1.1.0
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
postcss@8.5.16:
|
postcss@8.5.16:
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid: 3.3.15
|
nanoid: 3.3.15
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
quansync@0.2.11: {}
|
||||||
|
|
||||||
|
readdirp@5.0.0: {}
|
||||||
|
|
||||||
rolldown@1.1.4:
|
rolldown@1.1.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/types': 0.138.0
|
'@oxc-project/types': 0.138.0
|
||||||
@@ -1417,6 +1718,8 @@ snapshots:
|
|||||||
'@rolldown/binding-win32-arm64-msvc': 1.1.4
|
'@rolldown/binding-win32-arm64-msvc': 1.1.4
|
||||||
'@rolldown/binding-win32-x64-msvc': 1.1.4
|
'@rolldown/binding-win32-x64-msvc': 1.1.4
|
||||||
|
|
||||||
|
scule@1.3.0: {}
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
tailwindcss@4.3.2: {}
|
tailwindcss@4.3.2: {}
|
||||||
@@ -1435,9 +1738,25 @@ snapshots:
|
|||||||
|
|
||||||
typescript@6.0.3: {}
|
typescript@6.0.3: {}
|
||||||
|
|
||||||
|
ufo@1.6.4: {}
|
||||||
|
|
||||||
undici-types@7.18.2: {}
|
undici-types@7.18.2: {}
|
||||||
|
|
||||||
vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0):
|
unplugin-utils@0.3.2:
|
||||||
|
dependencies:
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.4
|
||||||
|
|
||||||
|
unplugin@3.3.0(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)):
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/remapping': 2.3.5
|
||||||
|
picomatch: 4.0.4
|
||||||
|
webpack-virtual-modules: 0.6.2
|
||||||
|
optionalDependencies:
|
||||||
|
rolldown: 1.1.4
|
||||||
|
vite: 8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
|
||||||
|
|
||||||
|
vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
@@ -1448,9 +1767,43 @@ snapshots:
|
|||||||
'@types/node': 24.13.2
|
'@types/node': 24.13.2
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
|
yaml: 2.9.0
|
||||||
|
|
||||||
vscode-uri@3.1.0: {}
|
vscode-uri@3.1.0: {}
|
||||||
|
|
||||||
|
vue-router@5.1.0(@vue/compiler-sfc@3.5.39)(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39(typescript@6.0.3)):
|
||||||
|
dependencies:
|
||||||
|
'@babel/generator': 8.0.0
|
||||||
|
'@vue-macros/common': 3.1.2(vue@3.5.39(typescript@6.0.3))
|
||||||
|
'@vue/devtools-api': 8.1.5
|
||||||
|
ast-walker-scope: 0.9.0
|
||||||
|
chokidar: 5.0.0
|
||||||
|
json5: 2.2.3
|
||||||
|
local-pkg: 1.2.1
|
||||||
|
magic-string: 0.30.21
|
||||||
|
mlly: 1.8.2
|
||||||
|
muggle-string: 0.4.1
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.4
|
||||||
|
scule: 1.3.0
|
||||||
|
tinyglobby: 0.2.17
|
||||||
|
unplugin: 3.3.0(rolldown@1.1.4)(vite@8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0))
|
||||||
|
unplugin-utils: 0.3.2
|
||||||
|
vue: 3.5.39(typescript@6.0.3)
|
||||||
|
yaml: 2.9.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@vue/compiler-sfc': 3.5.39
|
||||||
|
vite: 8.1.2(@types/node@24.13.2)(jiti@2.7.0)(yaml@2.9.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@farmfe/core'
|
||||||
|
- '@rspack/core'
|
||||||
|
- bun-types-no-globals
|
||||||
|
- esbuild
|
||||||
|
- rolldown
|
||||||
|
- rollup
|
||||||
|
- unloader
|
||||||
|
- webpack
|
||||||
|
|
||||||
vue-tsc@3.3.6(typescript@6.0.3):
|
vue-tsc@3.3.6(typescript@6.0.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@volar/typescript': 2.4.28
|
'@volar/typescript': 2.4.28
|
||||||
@@ -1466,3 +1819,7 @@ snapshots:
|
|||||||
'@vue/shared': 3.5.39
|
'@vue/shared': 3.5.39
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
|
|
||||||
|
webpack-virtual-modules@0.6.2: {}
|
||||||
|
|
||||||
|
yaml@2.9.0: {}
|
||||||
|
|||||||
450
src/App.vue
450
src/App.vue
@@ -1,446 +1,38 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts"></script>
|
||||||
import { computed, ref } from "vue"
|
|
||||||
import { FUNCTIONS, COMPOSED_SIG, CORE_INVARIANTS } from "@/data/listBlueprint"
|
|
||||||
import ListSpecimen from "@/components/ListSpecimen.vue"
|
|
||||||
import FunctionReadout from "@/components/FunctionReadout.vue"
|
|
||||||
import CompositionMap from "@/components/CompositionMap.vue"
|
|
||||||
import StateMachine from "@/components/StateMachine.vue"
|
|
||||||
|
|
||||||
const TABS = [
|
|
||||||
{ id: "full", label: "▣ FULL VIEW", full: true },
|
|
||||||
...FUNCTIONS.map((f) => ({ id: f.id, label: f.name, full: false })),
|
|
||||||
]
|
|
||||||
|
|
||||||
const activeId = ref("full")
|
|
||||||
const activeFn = computed(() => FUNCTIONS.find((f) => f.id === activeId.value) ?? null)
|
|
||||||
const specId = computed(() => (activeFn.value ? `FIG.${activeFn.value.fig}` : "FIG.00"))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sheet">
|
<div class="app">
|
||||||
<!-- TITLE BLOCK -->
|
<header class="topbar">
|
||||||
<div class="titleblock">
|
<RouterLink to="/" class="brand"><span class="mark">▣</span> blueprints</RouterLink>
|
||||||
<div class="title-main">
|
<span class="sub">UI-pattern contracts, illustrated</span>
|
||||||
<h1>
|
</header>
|
||||||
List<span class="brk"><</span>Item<span class="brk">></span> — Functional Blueprint
|
<RouterView />
|
||||||
</h1>
|
|
||||||
<div class="sub">
|
|
||||||
A scrollable, ordered set of items · an interactive schematic of its 7 functions
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="tb-cell">
|
|
||||||
<div class="tb-k">Extends</div>
|
|
||||||
<div class="tb-v">Set<Item></div>
|
|
||||||
</div>
|
|
||||||
<div class="tb-cell">
|
|
||||||
<div class="tb-k">Sheet</div>
|
|
||||||
<div class="tb-v">1 / 1</div>
|
|
||||||
</div>
|
|
||||||
<div class="tb-cell">
|
|
||||||
<div class="tb-k">Composition</div>
|
|
||||||
<div class="tb-v">+5 caps</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- LEGEND -->
|
|
||||||
<div class="legend">
|
|
||||||
<span
|
|
||||||
><span class="sw">◈</span> <b>highlighted region</b> — the part of the list this function
|
|
||||||
owns</span
|
|
||||||
>
|
|
||||||
<span><b>░</b> skeleton placeholder</span>
|
|
||||||
<span><b>⊆</b> subset</span>
|
|
||||||
<span><b>←</b> assignment / effect</span>
|
|
||||||
<span><span class="warn">⚠</span> failure mode</span>
|
|
||||||
<span><span class="perf">⚡</span> critical-performance threshold</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- VIEWER -->
|
|
||||||
<h2 class="section">Functional viewer — select a function</h2>
|
|
||||||
<div class="tabs">
|
|
||||||
<button
|
|
||||||
v-for="t in TABS"
|
|
||||||
:key="t.id"
|
|
||||||
class="tab"
|
|
||||||
:class="{ active: activeId === t.id, full: t.full }"
|
|
||||||
@click="activeId = t.id"
|
|
||||||
>
|
|
||||||
{{ t.label }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="viewer">
|
|
||||||
<div class="panel">
|
|
||||||
<div class="cap">
|
|
||||||
<span>Specimen · List<Item></span><span class="id">{{ specId }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="body"><ListSpecimen :active-id="activeId" /></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel">
|
|
||||||
<div class="cap"><span>Readout · Contract</span><span class="id">§ list.als</span></div>
|
|
||||||
<div class="body">
|
|
||||||
<FunctionReadout v-if="activeFn" :fn="activeFn" />
|
|
||||||
<div v-else>
|
|
||||||
<div class="flex flex-wrap items-baseline gap-2">
|
|
||||||
<span class="text-lg font-bold tracking-wide text-secondary">FULL VIEW</span>
|
|
||||||
</div>
|
|
||||||
<p class="mb-4 mt-1 text-xs text-base-content/50">
|
|
||||||
The standard composition and its seven functions. Select a function above to focus it.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="rblock">
|
|
||||||
<div class="rk"><span>Composed signature</span><span class="rule" /></div>
|
|
||||||
<div class="sig-block">
|
|
||||||
<div class="sig-head">{{ COMPOSED_SIG.header }}</div>
|
|
||||||
<div v-for="f in COMPOSED_SIG.fields" :key="f.name" class="sig-field">
|
|
||||||
<span class="sig-name">{{ f.name }}</span>
|
|
||||||
<span class="sig-type">{{ f.type }}</span>
|
|
||||||
<span class="sig-note">-- {{ f.note }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="sig-types">
|
|
||||||
<div v-for="t in COMPOSED_SIG.types" :key="t">{{ t }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rblock">
|
|
||||||
<div class="rk"><span>Core invariants</span><span class="rule" /></div>
|
|
||||||
<ul class="tight">
|
|
||||||
<li v-for="i in CORE_INVARIANTS" :key="i">{{ i }}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rblock">
|
|
||||||
<div class="rk"><span>Function index</span><span class="rule" /></div>
|
|
||||||
<div class="fn-index">
|
|
||||||
<template v-for="f in FUNCTIONS" :key="f.id">
|
|
||||||
<button class="fi-n" @click="activeId = f.id">{{ f.name }}</button>
|
|
||||||
<div class="fi-d">{{ f.verb }}</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- COMPANION 1 -->
|
|
||||||
<h2 class="section">Anatomy — where the 7 functions come from</h2>
|
|
||||||
<div class="panel">
|
|
||||||
<div class="cap">
|
|
||||||
<span>Composition map</span><span class="id">Set ◁ List ◁ capabilities</span>
|
|
||||||
</div>
|
|
||||||
<div class="body svg-wrap"><CompositionMap /></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- COMPANION 2 -->
|
|
||||||
<h2 class="section">Lifecycle — LoadState machine</h2>
|
|
||||||
<div class="panel">
|
|
||||||
<div class="cap">
|
|
||||||
<span>Composed machine · List + Async + Paginated + Pullable</span
|
|
||||||
><span class="id">loadState : LoadState</span>
|
|
||||||
</div>
|
|
||||||
<div class="body svg-wrap"><StateMachine /></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="credit">
|
|
||||||
<div>
|
|
||||||
<span class="ck">SOURCE</span> blueprint-ontology / blueprints / list · formal model:
|
|
||||||
<code>list.als</code>, <code>list.test.als</code>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="ck">READS AS</span> a <i>concept</i> (Jackson) / <i>contract</i> — declarative,
|
|
||||||
categorical, abstract
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="ck">SCOPE</span> standard composition:
|
|
||||||
<code>List + Async + Paginated + Pullable + Selectable + Filterable</code>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.sheet {
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 12px 20px;
|
||||||
max-width: 1180px;
|
max-width: 1180px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 22px 20px 60px;
|
|
||||||
color: var(--color-base-content);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* title block */
|
|
||||||
.titleblock {
|
|
||||||
border: 1.5px solid rgba(200, 226, 255, 0.3);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 130px 90px 120px;
|
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent);
|
|
||||||
}
|
|
||||||
.tb-cell {
|
|
||||||
border-left: 1px solid rgba(200, 226, 255, 0.14);
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
.tb-k {
|
|
||||||
color: #4f7099;
|
|
||||||
font-size: 10px;
|
|
||||||
letter-spacing: 0.14em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
.tb-v {
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
.title-main {
|
|
||||||
padding: 12px 14px 10px;
|
|
||||||
}
|
|
||||||
.title-main h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 20px;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.title-main h1 .brk {
|
|
||||||
color: var(--color-accent);
|
|
||||||
}
|
|
||||||
.title-main .sub {
|
|
||||||
color: #7ea6cd;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* legend */
|
|
||||||
.legend {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px 20px;
|
|
||||||
margin: 12px 0 18px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid rgba(200, 226, 255, 0.14);
|
|
||||||
color: #7ea6cd;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
.legend b {
|
|
||||||
color: var(--color-base-content);
|
|
||||||
}
|
|
||||||
.legend .sw {
|
|
||||||
color: var(--color-accent);
|
|
||||||
}
|
|
||||||
.legend .warn {
|
|
||||||
color: var(--color-error);
|
|
||||||
}
|
|
||||||
.legend .perf {
|
|
||||||
color: var(--color-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
font-size: 11px;
|
|
||||||
letter-spacing: 0.22em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #7ea6cd;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 28px 0 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.section::after {
|
|
||||||
content: "";
|
|
||||||
flex: 1;
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(200, 226, 255, 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tabs */
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.tab {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 12px;
|
|
||||||
color: #7ea6cd;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid rgba(200, 226, 255, 0.14);
|
|
||||||
padding: 5px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
transition: all 0.12s;
|
|
||||||
}
|
|
||||||
.tab:hover {
|
|
||||||
color: var(--color-base-content);
|
|
||||||
border-color: rgba(200, 226, 255, 0.3);
|
|
||||||
}
|
|
||||||
.tab.active {
|
|
||||||
color: var(--color-accent-content);
|
|
||||||
background: var(--color-accent);
|
|
||||||
border-color: var(--color-accent);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.tab.full {
|
|
||||||
color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
.tab.full.active {
|
|
||||||
color: var(--color-secondary-content);
|
|
||||||
background: var(--color-secondary);
|
|
||||||
border-color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* viewer */
|
|
||||||
.viewer {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
@media (max-width: 820px) {
|
|
||||||
.viewer {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.panel {
|
|
||||||
border: 1.5px solid rgba(200, 226, 255, 0.3);
|
|
||||||
background: var(--color-base-200);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.panel::before,
|
|
||||||
.panel::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.panel::before {
|
|
||||||
top: -1px;
|
|
||||||
left: -1px;
|
|
||||||
border-top: 1.5px solid var(--color-accent);
|
|
||||||
border-left: 1.5px solid var(--color-accent);
|
|
||||||
}
|
|
||||||
.panel::after {
|
|
||||||
bottom: -1px;
|
|
||||||
right: -1px;
|
|
||||||
border-bottom: 1.5px solid var(--color-accent);
|
|
||||||
border-right: 1.5px solid var(--color-accent);
|
|
||||||
}
|
|
||||||
.panel .cap {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 7px 12px;
|
|
||||||
border-bottom: 1px solid rgba(200, 226, 255, 0.14);
|
border-bottom: 1px solid rgba(200, 226, 255, 0.14);
|
||||||
font-size: 10px;
|
|
||||||
letter-spacing: 0.18em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #7ea6cd;
|
|
||||||
background: rgba(255, 255, 255, 0.02);
|
|
||||||
}
|
}
|
||||||
.panel .cap .id {
|
.brand {
|
||||||
color: #4f7099;
|
color: var(--color-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
.panel .body {
|
.brand .mark {
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
.svg-wrap {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* full-view readout */
|
|
||||||
.rblock {
|
|
||||||
margin-bottom: 0.875rem;
|
|
||||||
}
|
|
||||||
.rk {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
font-size: 10px;
|
|
||||||
letter-spacing: 0.16em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: color-mix(in srgb, var(--color-base-content) 50%, transparent);
|
|
||||||
margin-bottom: 0.35rem;
|
|
||||||
}
|
|
||||||
.rk .rule {
|
|
||||||
flex: 1;
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(200, 226, 255, 0.14);
|
|
||||||
}
|
|
||||||
.tight {
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 1rem;
|
|
||||||
list-style: disc;
|
|
||||||
}
|
|
||||||
.tight li {
|
|
||||||
margin: 0.125rem 0;
|
|
||||||
}
|
|
||||||
.sig-block {
|
|
||||||
background: var(--color-base-100);
|
|
||||||
border: 1px solid rgba(200, 226, 255, 0.14);
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.7;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
.sig-head {
|
|
||||||
color: var(--color-base-content);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
.sig-field {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 8.5rem 8.5rem 1fr;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.sig-name {
|
|
||||||
padding-left: 1rem;
|
|
||||||
color: var(--color-base-content);
|
|
||||||
}
|
|
||||||
.sig-type {
|
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
}
|
}
|
||||||
.sig-note {
|
.sub {
|
||||||
color: #4f7099;
|
|
||||||
}
|
|
||||||
.sig-types {
|
|
||||||
margin-top: 8px;
|
|
||||||
color: #7ea6cd;
|
|
||||||
}
|
|
||||||
.fn-index {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
gap: 3px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
.fi-n {
|
|
||||||
color: var(--color-accent);
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
text-align: left;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.fi-n:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.fi-d {
|
|
||||||
color: #7ea6cd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credit {
|
|
||||||
margin-top: 34px;
|
|
||||||
padding-top: 14px;
|
|
||||||
border-top: 1px solid rgba(200, 226, 255, 0.14);
|
|
||||||
color: #4f7099;
|
color: #4f7099;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1.8;
|
letter-spacing: 0.04em;
|
||||||
}
|
|
||||||
.credit .ck {
|
|
||||||
color: #7ea6cd;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
469
src/components/BlueprintViewer.vue
Normal file
469
src/components/BlueprintViewer.vue
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch, type Component } from "vue"
|
||||||
|
import type { Blueprint } from "@/data/blueprint"
|
||||||
|
import FunctionReadout from "@/components/FunctionReadout.vue"
|
||||||
|
import CompositionMap from "@/components/CompositionMap.vue"
|
||||||
|
import StateMachine from "@/components/StateMachine.vue"
|
||||||
|
|
||||||
|
const props = defineProps<{ blueprint: Blueprint; specimen: Component }>()
|
||||||
|
|
||||||
|
const activeId = ref("full")
|
||||||
|
// reset to the overview whenever we switch blueprints
|
||||||
|
watch(
|
||||||
|
() => props.blueprint.slug,
|
||||||
|
() => {
|
||||||
|
activeId.value = "full"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const tabs = computed(() => [
|
||||||
|
{ id: "full", label: "▣ FULL VIEW", full: true },
|
||||||
|
...props.blueprint.functions.map((f) => ({ id: f.id, label: f.name, full: false })),
|
||||||
|
])
|
||||||
|
const activeFn = computed(
|
||||||
|
() => props.blueprint.functions.find((f) => f.id === activeId.value) ?? null,
|
||||||
|
)
|
||||||
|
const specId = computed(() => (activeFn.value ? `FIG.${activeFn.value.fig}` : "FIG.00"))
|
||||||
|
|
||||||
|
const nameParts = computed(() => {
|
||||||
|
const m = props.blueprint.name.match(/^([^<]*)(<.*>)?$/)
|
||||||
|
return { base: m?.[1] ?? props.blueprint.name, generic: m?.[2] ?? "" }
|
||||||
|
})
|
||||||
|
const funcCount = computed(() => props.blueprint.functions.length)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sheet">
|
||||||
|
<!-- TITLE BLOCK -->
|
||||||
|
<div class="titleblock">
|
||||||
|
<div class="title-main">
|
||||||
|
<h1>
|
||||||
|
{{ nameParts.base }}<span class="brk">{{ nameParts.generic }}</span> — Functional
|
||||||
|
Blueprint
|
||||||
|
</h1>
|
||||||
|
<div class="sub">
|
||||||
|
{{ blueprint.tagline }} · an interactive schematic of its {{ funcCount }} functions
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tb-cell">
|
||||||
|
<div class="tb-k">Extends</div>
|
||||||
|
<div class="tb-v">{{ blueprint.extendsName ?? "—" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="tb-cell">
|
||||||
|
<div class="tb-k">Role</div>
|
||||||
|
<div class="tb-v">{{ blueprint.role }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="tb-cell">
|
||||||
|
<div class="tb-k">Composition</div>
|
||||||
|
<div class="tb-v">
|
||||||
|
{{ blueprint.composesCount ? `+${blueprint.composesCount} caps` : "—" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LEGEND -->
|
||||||
|
<div class="legend">
|
||||||
|
<span><span class="sw">◈</span> <b>highlighted region</b> — the part this function owns</span>
|
||||||
|
<span><b>░</b> skeleton placeholder</span>
|
||||||
|
<span><b>⊆</b> subset</span>
|
||||||
|
<span><b>←</b> assignment / effect</span>
|
||||||
|
<span><span class="warn">⚠</span> failure mode</span>
|
||||||
|
<span><span class="perf">⚡</span> critical-performance threshold</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VIEWER -->
|
||||||
|
<h2 class="section">Functional viewer — select a function</h2>
|
||||||
|
<div class="tabs">
|
||||||
|
<button
|
||||||
|
v-for="t in tabs"
|
||||||
|
:key="t.id"
|
||||||
|
class="tab"
|
||||||
|
:class="{ active: activeId === t.id, full: t.full }"
|
||||||
|
@click="activeId = t.id"
|
||||||
|
>
|
||||||
|
{{ t.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="viewer">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="cap">
|
||||||
|
<span>Specimen · {{ blueprint.name }}</span
|
||||||
|
><span class="id">{{ specId }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="body"><component :is="specimen" :active-id="activeId" /></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="cap">
|
||||||
|
<span>Readout · Contract</span><span class="id">§ {{ blueprint.slug }}.als</span>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<FunctionReadout v-if="activeFn" :fn="activeFn" />
|
||||||
|
<div v-else>
|
||||||
|
<div class="flex flex-wrap items-baseline gap-2">
|
||||||
|
<span class="text-lg font-bold tracking-wide text-secondary">FULL VIEW</span>
|
||||||
|
</div>
|
||||||
|
<p class="mb-4 mt-1 text-xs text-base-content/50">
|
||||||
|
The standard composition and its {{ funcCount }} functions. Select a function above to
|
||||||
|
focus it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="rblock">
|
||||||
|
<div class="rk"><span>Composed signature</span><span class="rule" /></div>
|
||||||
|
<div class="sig-block">
|
||||||
|
<div class="sig-head">{{ blueprint.sig.header }}</div>
|
||||||
|
<div v-for="f in blueprint.sig.fields" :key="f.name" class="sig-field">
|
||||||
|
<span class="sig-name">{{ f.name }}</span>
|
||||||
|
<span class="sig-type">{{ f.type }}</span>
|
||||||
|
<span class="sig-note">-- {{ f.note }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sig-types">
|
||||||
|
<div v-for="t in blueprint.sig.types" :key="t">{{ t }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rblock">
|
||||||
|
<div class="rk"><span>Core invariants</span><span class="rule" /></div>
|
||||||
|
<ul class="tight">
|
||||||
|
<li v-for="i in blueprint.coreInvariants" :key="i">{{ i }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rblock">
|
||||||
|
<div class="rk"><span>Function index</span><span class="rule" /></div>
|
||||||
|
<div class="fn-index">
|
||||||
|
<template v-for="f in blueprint.functions" :key="f.id">
|
||||||
|
<button class="fi-n" @click="activeId = f.id">{{ f.name }}</button>
|
||||||
|
<div class="fi-d">{{ f.verb }}</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- COMPANION 1: composition -->
|
||||||
|
<template v-if="blueprint.composition">
|
||||||
|
<h2 class="section">Anatomy — where the {{ funcCount }} functions come from</h2>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="cap">
|
||||||
|
<span>Composition map</span><span class="id">host ◁ capabilities</span>
|
||||||
|
</div>
|
||||||
|
<div class="body svg-wrap"><CompositionMap :composition="blueprint.composition" /></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- COMPANION 2: state machine -->
|
||||||
|
<template v-if="blueprint.stateMachine">
|
||||||
|
<h2 class="section">Lifecycle — state machine</h2>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="cap">
|
||||||
|
<span>Composed lifecycle</span><span class="id">loadState : LoadState</span>
|
||||||
|
</div>
|
||||||
|
<div class="body svg-wrap"><StateMachine :machine="blueprint.stateMachine" /></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<footer class="credit">
|
||||||
|
<div>
|
||||||
|
<span class="ck">SOURCE</span> blueprint-ontology / blueprints / {{ blueprint.slug }} ·
|
||||||
|
formal model: <code>{{ blueprint.slug }}.als</code>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="ck">READS AS</span> a <i>concept</i> (Jackson) / <i>contract</i> — declarative,
|
||||||
|
categorical, abstract
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="ck">SCOPE</span> <code>{{ blueprint.sig.header }}</code>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sheet {
|
||||||
|
max-width: 1180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 22px 20px 60px;
|
||||||
|
color: var(--color-base-content);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* title block */
|
||||||
|
.titleblock {
|
||||||
|
border: 1.5px solid rgba(200, 226, 255, 0.3);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 130px 100px 120px;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent);
|
||||||
|
}
|
||||||
|
.tb-cell {
|
||||||
|
border-left: 1px solid rgba(200, 226, 255, 0.14);
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
.tb-k {
|
||||||
|
color: #4f7099;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.tb-v {
|
||||||
|
margin-top: 2px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
.title-main {
|
||||||
|
padding: 12px 14px 10px;
|
||||||
|
}
|
||||||
|
.title-main h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.title-main h1 .brk {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
.title-main .sub {
|
||||||
|
color: #7ea6cd;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* legend */
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 20px;
|
||||||
|
margin: 12px 0 18px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid rgba(200, 226, 255, 0.14);
|
||||||
|
color: #7ea6cd;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.legend b {
|
||||||
|
color: var(--color-base-content);
|
||||||
|
}
|
||||||
|
.legend .sw {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
.legend .warn {
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
.legend .perf {
|
||||||
|
color: var(--color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #7ea6cd;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 28px 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.section::after {
|
||||||
|
content: "";
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(200, 226, 255, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tabs */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.tab {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7ea6cd;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(200, 226, 255, 0.14);
|
||||||
|
padding: 5px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
transition: all 0.12s;
|
||||||
|
}
|
||||||
|
.tab:hover {
|
||||||
|
color: var(--color-base-content);
|
||||||
|
border-color: rgba(200, 226, 255, 0.3);
|
||||||
|
}
|
||||||
|
.tab.active {
|
||||||
|
color: var(--color-accent-content);
|
||||||
|
background: var(--color-accent);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.tab.full {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
.tab.full.active {
|
||||||
|
color: var(--color-secondary-content);
|
||||||
|
background: var(--color-secondary);
|
||||||
|
border-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* viewer */
|
||||||
|
.viewer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.viewer {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
border: 1.5px solid rgba(200, 226, 255, 0.3);
|
||||||
|
background: var(--color-base-200);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.panel::before,
|
||||||
|
.panel::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.panel::before {
|
||||||
|
top: -1px;
|
||||||
|
left: -1px;
|
||||||
|
border-top: 1.5px solid var(--color-accent);
|
||||||
|
border-left: 1.5px solid var(--color-accent);
|
||||||
|
}
|
||||||
|
.panel::after {
|
||||||
|
bottom: -1px;
|
||||||
|
right: -1px;
|
||||||
|
border-bottom: 1.5px solid var(--color-accent);
|
||||||
|
border-right: 1.5px solid var(--color-accent);
|
||||||
|
}
|
||||||
|
.panel .cap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border-bottom: 1px solid rgba(200, 226, 255, 0.14);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #7ea6cd;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
.panel .cap .id {
|
||||||
|
color: #4f7099;
|
||||||
|
}
|
||||||
|
.panel .body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.svg-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* full-view readout */
|
||||||
|
.rblock {
|
||||||
|
margin-bottom: 0.875rem;
|
||||||
|
}
|
||||||
|
.rk {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: color-mix(in srgb, var(--color-base-content) 50%, transparent);
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
.rk .rule {
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(200, 226, 255, 0.14);
|
||||||
|
}
|
||||||
|
.tight {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1rem;
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
.tight li {
|
||||||
|
margin: 0.125rem 0;
|
||||||
|
}
|
||||||
|
.sig-block {
|
||||||
|
background: var(--color-base-100);
|
||||||
|
border: 1px solid rgba(200, 226, 255, 0.14);
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.7;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.sig-head {
|
||||||
|
color: var(--color-base-content);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.sig-field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 8.5rem 8.5rem 1fr;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.sig-name {
|
||||||
|
padding-left: 1rem;
|
||||||
|
color: var(--color-base-content);
|
||||||
|
}
|
||||||
|
.sig-type {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
.sig-note {
|
||||||
|
color: #4f7099;
|
||||||
|
}
|
||||||
|
.sig-types {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #7ea6cd;
|
||||||
|
}
|
||||||
|
.fn-index {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 3px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
.fi-n {
|
||||||
|
color: var(--color-accent);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.fi-n:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.fi-d {
|
||||||
|
color: #7ea6cd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit {
|
||||||
|
margin-top: 34px;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-top: 1px solid rgba(200, 226, 255, 0.14);
|
||||||
|
color: #4f7099;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.credit .ck {
|
||||||
|
color: #7ea6cd;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import { COMP_CAPS, COMP_FUNCS, COMP_LINKS } from "@/data/listBlueprint"
|
import type { Composition } from "@/data/blueprint"
|
||||||
|
|
||||||
|
const props = defineProps<{ composition: Composition }>()
|
||||||
|
|
||||||
const PAL = {
|
const PAL = {
|
||||||
boxFill: "#103763",
|
boxFill: "#103763",
|
||||||
@@ -20,27 +22,29 @@ const bh = 34
|
|||||||
const top = 16
|
const top = 16
|
||||||
const lstep = 40
|
const lstep = 40
|
||||||
const rstep = 52
|
const rstep = 52
|
||||||
const H = top + COMP_CAPS.length * lstep + 16
|
|
||||||
|
|
||||||
const capY = (i: number) => top + i * lstep + bh / 2
|
const capY = (i: number) => top + i * lstep + bh / 2
|
||||||
const funcY = (i: number) => top + i * rstep + bh / 2
|
const funcY = (i: number) => top + i * rstep + bh / 2
|
||||||
|
|
||||||
|
const H = computed(() => top + props.composition.caps.length * lstep + 16)
|
||||||
|
|
||||||
const capIndex = computed(
|
const capIndex = computed(
|
||||||
() => Object.fromEntries(COMP_CAPS.map((c, i) => [c.id, i])) as Record<string, number>,
|
() =>
|
||||||
|
Object.fromEntries(props.composition.caps.map((c, i) => [c.id, i])) as Record<string, number>,
|
||||||
)
|
)
|
||||||
const funcIndex = computed(
|
const funcIndex = computed(
|
||||||
() => Object.fromEntries(COMP_FUNCS.map((f, i) => [f, i])) as Record<string, number>,
|
() => Object.fromEntries(props.composition.funcs.map((f, i) => [f, i])) as Record<string, number>,
|
||||||
)
|
)
|
||||||
|
|
||||||
const leftBoxes = computed(() =>
|
const leftBoxes = computed(() =>
|
||||||
COMP_CAPS.map((c, i) => ({ ...c, x: lx, y: capY(i) - bh / 2, w: lw, h: bh })),
|
props.composition.caps.map((c, i) => ({ ...c, x: lx, y: capY(i) - bh / 2, w: lw, h: bh })),
|
||||||
)
|
)
|
||||||
const rightBoxes = computed(() =>
|
const rightBoxes = computed(() =>
|
||||||
COMP_FUNCS.map((f, i) => ({ label: f, x: rx, y: funcY(i) - bh / 2, w: rw, h: bh })),
|
props.composition.funcs.map((f, i) => ({ label: f, x: rx, y: funcY(i) - bh / 2, w: rw, h: bh })),
|
||||||
)
|
)
|
||||||
|
|
||||||
const edges = computed(() =>
|
const edges = computed(() =>
|
||||||
COMP_LINKS.map(([a, b]) => {
|
props.composition.links.map(([a, b]) => {
|
||||||
const y1 = capY(capIndex.value[a])
|
const y1 = capY(capIndex.value[a])
|
||||||
const y2 = funcY(funcIndex.value[b])
|
const y2 = funcY(funcIndex.value[b])
|
||||||
const x1 = lx + lw
|
const x1 = lx + lw
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { BlueprintFunction } from "@/data/listBlueprint"
|
import type { BlueprintFunction } from "@/data/blueprint"
|
||||||
|
|
||||||
defineProps<{ fn: BlueprintFunction }>()
|
defineProps<{ fn: BlueprintFunction }>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import { SM_NODES, SM_EDGES, SM_COLORS, type SmKind, type SmNode } from "@/data/listBlueprint"
|
import { SM_COLORS, type SmKind, type SmNode, type StateMachine } from "@/data/blueprint"
|
||||||
|
|
||||||
|
const props = defineProps<{ machine: StateMachine }>()
|
||||||
|
|
||||||
const paper = "#0d2f54"
|
const paper = "#0d2f54"
|
||||||
const nodeFill = "#103763"
|
const nodeFill = "#103763"
|
||||||
@@ -8,7 +10,9 @@ const nodeStroke = "#3d5f85"
|
|||||||
const ink = "#dbe9f7"
|
const ink = "#dbe9f7"
|
||||||
const inkDim = "#7ea6cd"
|
const inkDim = "#7ea6cd"
|
||||||
|
|
||||||
const byId: Record<string, SmNode> = Object.fromEntries(SM_NODES.map((n) => [n.id, n]))
|
const byId = computed<Record<string, SmNode>>(() =>
|
||||||
|
Object.fromEntries(props.machine.nodes.map((n) => [n.id, n])),
|
||||||
|
)
|
||||||
|
|
||||||
function center(n: SmNode): [number, number] {
|
function center(n: SmNode): [number, number] {
|
||||||
return [n.x + n.w / 2, n.y + n.h / 2]
|
return [n.x + n.w / 2, n.y + n.h / 2]
|
||||||
@@ -31,15 +35,17 @@ const markers = (Object.keys(SM_COLORS) as (keyof typeof SM_COLORS)[]).map((k) =
|
|||||||
color: SM_COLORS[k],
|
color: SM_COLORS[k],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const loading = byId.loading
|
const init = computed(() => {
|
||||||
const loadingCy = loading.y + loading.h / 2
|
const loading = byId.value.loading
|
||||||
const initEnd = edgePoint(loading, 26, loadingCy)
|
const cy = loading.y + loading.h / 2
|
||||||
const initPath = `M31 ${loadingCy} L ${initEnd[0]} ${initEnd[1]}`
|
const e = edgePoint(loading, 26, cy)
|
||||||
|
return { cy, path: `M31 ${cy} L ${e[0]} ${e[1]}` }
|
||||||
|
})
|
||||||
|
|
||||||
const edges = computed(() =>
|
const edges = computed(() =>
|
||||||
SM_EDGES.map((e) => {
|
props.machine.edges.map((e) => {
|
||||||
const a = byId[e.from]
|
const a = byId.value[e.from]
|
||||||
const b = byId[e.to]
|
const b = byId.value[e.to]
|
||||||
const [acx, acy] = center(a)
|
const [acx, acy] = center(a)
|
||||||
const [bcx, bcy] = center(b)
|
const [bcx, bcy] = center(b)
|
||||||
const [sx, sy] = edgePoint(a, bcx, bcy)
|
const [sx, sy] = edgePoint(a, bcx, bcy)
|
||||||
@@ -82,7 +88,7 @@ const legendItems = (() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<svg viewBox="0 -6 900 330" role="img" aria-label="LoadState machine" class="schematic">
|
<svg viewBox="0 -6 900 330" role="img" aria-label="State machine" class="schematic">
|
||||||
<defs>
|
<defs>
|
||||||
<marker
|
<marker
|
||||||
v-for="m in markers"
|
v-for="m in markers"
|
||||||
@@ -100,9 +106,9 @@ const legendItems = (() => {
|
|||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<!-- initial transition -->
|
<!-- initial transition -->
|
||||||
<circle cx="26" :cy="loadingCy" r="5" :fill="SM_COLORS.init" />
|
<circle cx="26" :cy="init.cy" r="5" :fill="SM_COLORS.init" />
|
||||||
<path
|
<path
|
||||||
:d="initPath"
|
:d="init.path"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke-width="1.4"
|
stroke-width="1.4"
|
||||||
:stroke="SM_COLORS.init"
|
:stroke="SM_COLORS.init"
|
||||||
@@ -118,7 +124,7 @@ const legendItems = (() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- states -->
|
<!-- states -->
|
||||||
<g v-for="n in SM_NODES" :key="n.id">
|
<g v-for="n in machine.nodes" :key="n.id">
|
||||||
<rect
|
<rect
|
||||||
:x="n.x"
|
:x="n.x"
|
||||||
:y="n.y"
|
:y="n.y"
|
||||||
|
|||||||
99
src/data/blueprint.ts
Normal file
99
src/data/blueprint.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// Generic types shared by every blueprint's data module. A concrete blueprint
|
||||||
|
// (list, grid, …) is a `Blueprint` object; the generic Viewer renders it, and a
|
||||||
|
// bespoke Specimen component is paired with it in the registry (src/data/blueprints.ts).
|
||||||
|
|
||||||
|
export interface SigField {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
note: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComposedSig {
|
||||||
|
header: string
|
||||||
|
fields: SigField[]
|
||||||
|
types: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Behavior {
|
||||||
|
sig: string
|
||||||
|
pre: string
|
||||||
|
eff: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlueprintFunction {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
fig: string
|
||||||
|
caps: string[]
|
||||||
|
verb: string
|
||||||
|
state: [string, string][]
|
||||||
|
behaviors: Behavior[]
|
||||||
|
invariants: string[]
|
||||||
|
failures: string[]
|
||||||
|
perf: string[]
|
||||||
|
notes: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Composition map ────────────────────────────────────────────────────────
|
||||||
|
export interface CapNode {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
sub: string
|
||||||
|
core?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Composition {
|
||||||
|
caps: CapNode[]
|
||||||
|
funcs: string[]
|
||||||
|
links: [string, string][]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── State machine ──────────────────────────────────────────────────────────
|
||||||
|
export interface SmNode {
|
||||||
|
id: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SmKind = "succeed" | "fail" | "refresh" | "loadMore"
|
||||||
|
|
||||||
|
export interface SmEdge {
|
||||||
|
from: string
|
||||||
|
to: string
|
||||||
|
label: string
|
||||||
|
kind: SmKind
|
||||||
|
off: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StateMachine {
|
||||||
|
nodes: SmNode[]
|
||||||
|
edges: SmEdge[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SM_COLORS: Record<SmKind | "init", string> = {
|
||||||
|
succeed: "#7fdca0",
|
||||||
|
fail: "#ff8f8f",
|
||||||
|
refresh: "#ffb84d",
|
||||||
|
loadMore: "#8fd0ff",
|
||||||
|
init: "#4f7099",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Blueprint ──────────────────────────────────────────────────────────────
|
||||||
|
export type BlueprintRole = "feature" | "capability"
|
||||||
|
|
||||||
|
export interface Blueprint {
|
||||||
|
slug: string
|
||||||
|
name: string
|
||||||
|
tagline: string
|
||||||
|
role: BlueprintRole
|
||||||
|
extendsName?: string
|
||||||
|
composesCount?: number
|
||||||
|
sig: ComposedSig
|
||||||
|
functions: BlueprintFunction[]
|
||||||
|
coreInvariants: string[]
|
||||||
|
composition?: Composition
|
||||||
|
stateMachine?: StateMachine
|
||||||
|
}
|
||||||
26
src/data/blueprints.ts
Normal file
26
src/data/blueprints.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Registry: the single source the router and Gallery both read from. A blueprint
|
||||||
|
// appears in the app precisely when it is registered here — pairing its typed data
|
||||||
|
// with its bespoke Specimen component.
|
||||||
|
|
||||||
|
import type { Component } from "vue"
|
||||||
|
import type { Blueprint } from "./blueprint"
|
||||||
|
import { list } from "./listBlueprint"
|
||||||
|
import { grid } from "./gridBlueprint"
|
||||||
|
import ListSpecimen from "@/specimens/ListSpecimen.vue"
|
||||||
|
import GridSpecimen from "@/specimens/GridSpecimen.vue"
|
||||||
|
|
||||||
|
export interface RegistryEntry {
|
||||||
|
blueprint: Blueprint
|
||||||
|
specimen: Component
|
||||||
|
}
|
||||||
|
|
||||||
|
export const REGISTRY: Record<string, RegistryEntry> = {
|
||||||
|
list: { blueprint: list, specimen: ListSpecimen },
|
||||||
|
grid: { blueprint: grid, specimen: GridSpecimen },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BLUEPRINTS: Blueprint[] = Object.values(REGISTRY).map((e) => e.blueprint)
|
||||||
|
|
||||||
|
export function entryFor(slug: string): RegistryEntry | undefined {
|
||||||
|
return REGISTRY[slug]
|
||||||
|
}
|
||||||
206
src/data/gridBlueprint.ts
Normal file
206
src/data/gridBlueprint.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
// The Grid blueprint, as data. Lifted verbatim from the blueprint-ontology
|
||||||
|
// `grid` README. Note: Grid has 6 functions (incl. the Grid-specific `Position`,
|
||||||
|
// no Sort/Act) and shares the composed LoadState machine with List.
|
||||||
|
|
||||||
|
import type { Blueprint } from "./blueprint"
|
||||||
|
import { LOADSTATE_MACHINE } from "./loadStateMachine"
|
||||||
|
|
||||||
|
export const grid: Blueprint = {
|
||||||
|
slug: "grid",
|
||||||
|
name: "Grid<Item>",
|
||||||
|
tagline: "A parameterized, scrollable set of items in a 2D cell layout.",
|
||||||
|
role: "feature",
|
||||||
|
extendsName: "Set<Item>",
|
||||||
|
composesCount: 6,
|
||||||
|
|
||||||
|
sig: {
|
||||||
|
header: "Grid<Item> + Async + Paginated + Pullable + Selectable + Filterable",
|
||||||
|
fields: [
|
||||||
|
{ name: "items", type: "seq Item", note: "row-major; loaded subset of total" },
|
||||||
|
{ name: "columns", type: "Int", note: "number of columns (≥ 1)" },
|
||||||
|
{ name: "totalCount", type: "Int", note: "Paginated" },
|
||||||
|
{ name: "loadState", type: "LoadState", note: "Async · Paginated · Pullable" },
|
||||||
|
{ name: "selectionMode", type: "SelectionMode", note: "Selectable" },
|
||||||
|
{ name: "selected", type: "set Item", note: "Selectable" },
|
||||||
|
{ name: "visible", type: "set Item", note: "Filterable" },
|
||||||
|
],
|
||||||
|
types: [
|
||||||
|
"LoadState = Idle | Loading | Refreshing | LoadingMore | Error",
|
||||||
|
"SelectionMode = None | Single | Multi",
|
||||||
|
"row(i) = i / columns col(i) = i mod columns (derived, not stored)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
functions: [
|
||||||
|
{
|
||||||
|
id: "display",
|
||||||
|
name: "Display",
|
||||||
|
fig: "01",
|
||||||
|
caps: ["Grid", "Async"],
|
||||||
|
verb: "Render items in a 2D cell layout; handle empty, loading, and error states distinctly.",
|
||||||
|
state: [
|
||||||
|
["items", "seq Item"],
|
||||||
|
["loadState", "LoadState"],
|
||||||
|
],
|
||||||
|
behaviors: [],
|
||||||
|
invariants: [
|
||||||
|
"#4 items = ∅ is a valid state (empty grid, not an error)",
|
||||||
|
"#8 all cells have equal dimensions",
|
||||||
|
],
|
||||||
|
failures: [
|
||||||
|
"empty vs error conflation — server returns 0 items with 4xx; empty state treated as error, user sees wrong UI",
|
||||||
|
"incomplete trailing row — #items mod columns ≠ 0; trailing cells must render as empty placeholders, not crash",
|
||||||
|
"variable-height cell break — items with differing heights; row alignment lost, grid degenerates to a non-uniform layout",
|
||||||
|
],
|
||||||
|
perf: ["visible rows appear within 300 ms of navigation"],
|
||||||
|
notes: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "scroll",
|
||||||
|
name: "Scroll",
|
||||||
|
fig: "02",
|
||||||
|
caps: ["Grid"],
|
||||||
|
verb: "Allow the user to navigate the full item sequence along one axis; virtualize off-screen rows.",
|
||||||
|
state: [["items", "seq Item"]],
|
||||||
|
behaviors: [],
|
||||||
|
invariants: [],
|
||||||
|
failures: [],
|
||||||
|
perf: [
|
||||||
|
"60 fps minimum — off-screen rows must not be rendered (row virtualization required)",
|
||||||
|
"rows outside the render window must release their cell view references (memory)",
|
||||||
|
],
|
||||||
|
notes: [
|
||||||
|
"Grid scrolls one axis; the row (not the individual cell) is the unit of virtualization.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "load",
|
||||||
|
name: "Load",
|
||||||
|
fig: "03",
|
||||||
|
caps: ["Async", "Paginated", "Pullable"],
|
||||||
|
verb: "Fetch the initial page; support refresh (restart) and pagination (append).",
|
||||||
|
state: [
|
||||||
|
["loadState", "LoadState"],
|
||||||
|
["totalCount", "Int"],
|
||||||
|
],
|
||||||
|
behaviors: [
|
||||||
|
{
|
||||||
|
sig: "refresh()",
|
||||||
|
pre: "—",
|
||||||
|
eff: "loadState ← refreshing, reload items from page 1, selected ← ∅",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sig: "loadMore()",
|
||||||
|
pre: "#items < totalCount, loadState = idle",
|
||||||
|
eff: "loadState ← loadingMore, append next page to items",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invariants: ["#6 #items ≤ totalCount — loaded count never exceeds the server total"],
|
||||||
|
failures: [
|
||||||
|
"duplicate load — loadMore() called while loadState = loadingMore; duplicate items appended, identity-uniqueness lost",
|
||||||
|
],
|
||||||
|
perf: ["loadMore appends new rows without layout shift or scroll-position jump"],
|
||||||
|
notes: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "position",
|
||||||
|
name: "Position",
|
||||||
|
fig: "04",
|
||||||
|
caps: ["Grid"],
|
||||||
|
verb: "Derive each item's (row, col) from its index and columns; recompute on resize.",
|
||||||
|
state: [["columns", "Int"]],
|
||||||
|
behaviors: [
|
||||||
|
{
|
||||||
|
sig: "resize(n)",
|
||||||
|
pre: "n ≥ 1",
|
||||||
|
eff: "columns ← n, reflow layout; scroll position preserved by item",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invariants: [
|
||||||
|
"#1 columns ≥ 1 — at least one column at all times",
|
||||||
|
"#7 row(i) = i / columns, col(i) = i mod columns — position deterministically derived from index",
|
||||||
|
],
|
||||||
|
failures: [
|
||||||
|
"layout thrash on resize — columns changes while items are mid-scroll; visible rows reflow, scroll position jumps to a different item",
|
||||||
|
],
|
||||||
|
perf: ["column reflow completes within one frame (≤ 16 ms) for in-memory data"],
|
||||||
|
notes: [
|
||||||
|
"Derived: row(i) = i / columns (integer division), col(i) = i mod columns, rows = ⌈#items / columns⌉.",
|
||||||
|
"Open question: adaptive column count (floor(width / minCellWidth)) is treated as a consumer concern; the model uses a fixed integer.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "filter",
|
||||||
|
name: "Filter",
|
||||||
|
fig: "05",
|
||||||
|
caps: ["Filterable"],
|
||||||
|
verb: "Restrict the visible set without mutating the underlying sequence.",
|
||||||
|
state: [["visible", "set Item"]],
|
||||||
|
behaviors: [{ sig: "filter(pred)", pre: "—", eff: "visible ← { x ∈ items | pred(x) }" }],
|
||||||
|
invariants: ["#5 visible ⊆ items — filter is purely restrictive"],
|
||||||
|
failures: [],
|
||||||
|
perf: [],
|
||||||
|
notes: ["Ephemeral: derives visible from items without mutating the sequence."],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
name: "Select",
|
||||||
|
fig: "06",
|
||||||
|
caps: ["Selectable"],
|
||||||
|
verb: "Track user intent over a subset of items (none / single / multi).",
|
||||||
|
state: [
|
||||||
|
["selectionMode", "SelectionMode"],
|
||||||
|
["selected", "set Item"],
|
||||||
|
],
|
||||||
|
behaviors: [
|
||||||
|
{
|
||||||
|
sig: "select(x)",
|
||||||
|
pre: "x ∈ visible, selectionMode ≠ none",
|
||||||
|
eff: "selected ← selected ∪ {x}",
|
||||||
|
},
|
||||||
|
{ sig: "deselect(x)", pre: "x ∈ selected", eff: "selected ← selected \\ {x}" },
|
||||||
|
{ sig: "selectAll()", pre: "selectionMode = multi", eff: "selected ← visible" },
|
||||||
|
{ sig: "clearSelection()", pre: "—", eff: "selected ← ∅" },
|
||||||
|
],
|
||||||
|
invariants: ["selected ⊆ items — selection is always over loaded items"],
|
||||||
|
failures: [
|
||||||
|
"stale selection after refresh — refresh() clears items but not selected; selected ⊄ items, invariant violated",
|
||||||
|
],
|
||||||
|
perf: [],
|
||||||
|
notes: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
coreInvariants: [
|
||||||
|
"columns ≥ 1 — at least one column at all times",
|
||||||
|
"members = elems(items) — set and sequence are always in sync",
|
||||||
|
"items are identity-unique within items",
|
||||||
|
"items = ∅ is a valid state (empty grid, not an error)",
|
||||||
|
"all cells have equal dimensions",
|
||||||
|
],
|
||||||
|
|
||||||
|
composition: {
|
||||||
|
caps: [
|
||||||
|
{ id: "core", label: "Grid : Set", sub: "items, columns", core: true },
|
||||||
|
{ id: "async", label: "Async", sub: "loadState" },
|
||||||
|
{ id: "paginated", label: "Paginated", sub: "totalCount" },
|
||||||
|
{ id: "pullable", label: "Pullable", sub: "refresh" },
|
||||||
|
{ id: "filterable", label: "Filterable", sub: "visible" },
|
||||||
|
{ id: "selectable", label: "Selectable", sub: "selected" },
|
||||||
|
],
|
||||||
|
funcs: ["Display", "Scroll", "Load", "Position", "Filter", "Select"],
|
||||||
|
links: [
|
||||||
|
["core", "Display"],
|
||||||
|
["core", "Scroll"],
|
||||||
|
["core", "Position"],
|
||||||
|
["async", "Display"],
|
||||||
|
["async", "Load"],
|
||||||
|
["paginated", "Load"],
|
||||||
|
["pullable", "Load"],
|
||||||
|
["filterable", "Filter"],
|
||||||
|
["selectable", "Select"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
stateMachine: LOADSTATE_MACHINE,
|
||||||
|
}
|
||||||
@@ -1,34 +1,19 @@
|
|||||||
// The List blueprint, as data. Lifted verbatim from the blueprint-ontology
|
// The List blueprint, as data. Lifted verbatim from the blueprint-ontology
|
||||||
// `list` README: signature, behaviors, invariants, failure modes, performance.
|
// `list` README: signature, behaviors, invariants, failure modes, performance.
|
||||||
// Rendered by the components under src/components as an interactive schematic.
|
// The generic Viewer renders this; ListSpecimen supplies the bespoke visual.
|
||||||
|
|
||||||
export interface SampleItem {
|
import type { Blueprint } from "./blueprint"
|
||||||
n: string
|
import { LOADSTATE_MACHINE } from "./loadStateMachine"
|
||||||
s: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SAMPLE: SampleItem[] = [
|
export const list: Blueprint = {
|
||||||
{ n: "Item A", s: "#a1f3 · ready" },
|
slug: "list",
|
||||||
{ n: "Item B", s: "#b2e8 · ready" },
|
name: "List<Item>",
|
||||||
{ n: "Item C", s: "#c7d1 · ready" },
|
tagline: "A scrollable, ordered set of items.",
|
||||||
{ n: "Item D", s: "#d0a4 · ready" },
|
role: "feature",
|
||||||
{ n: "Item E", s: "#e5b9 · ready" },
|
extendsName: "Set<Item>",
|
||||||
{ n: "Item F", s: "#f3c2 · ready" },
|
composesCount: 5,
|
||||||
{ n: "Item G", s: "#01d7 · ready" },
|
|
||||||
{ n: "Item H", s: "#12e0 · ready" },
|
|
||||||
]
|
|
||||||
|
|
||||||
export interface SigField {
|
sig: {
|
||||||
name: string
|
|
||||||
type: string
|
|
||||||
note: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const COMPOSED_SIG: {
|
|
||||||
header: string
|
|
||||||
fields: SigField[]
|
|
||||||
types: string[]
|
|
||||||
} = {
|
|
||||||
header: "List<Item> + Async + Paginated + Pullable + Selectable + Filterable",
|
header: "List<Item> + Async + Paginated + Pullable + Selectable + Filterable",
|
||||||
fields: [
|
fields: [
|
||||||
{ name: "items", type: "seq Item", note: "List: ordered, loaded subset" },
|
{ name: "items", type: "seq Item", note: "List: ordered, loaded subset" },
|
||||||
@@ -42,29 +27,9 @@ export const COMPOSED_SIG: {
|
|||||||
"LoadState = Idle | Loading | Refreshing | LoadingMore | Error",
|
"LoadState = Idle | Loading | Refreshing | LoadingMore | Error",
|
||||||
"SelectionMode = None | Single | Multi",
|
"SelectionMode = None | Single | Multi",
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
|
|
||||||
export interface Behavior {
|
functions: [
|
||||||
sig: string
|
|
||||||
pre: string
|
|
||||||
eff: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlueprintFunction {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
fig: string
|
|
||||||
caps: string[]
|
|
||||||
verb: string
|
|
||||||
state: [string, string][]
|
|
||||||
behaviors: Behavior[]
|
|
||||||
invariants: string[]
|
|
||||||
failures: string[]
|
|
||||||
perf: string[]
|
|
||||||
notes: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FUNCTIONS: BlueprintFunction[] = [
|
|
||||||
{
|
{
|
||||||
id: "display",
|
id: "display",
|
||||||
name: "Display",
|
name: "Display",
|
||||||
@@ -227,23 +192,16 @@ export const FUNCTIONS: BlueprintFunction[] = [
|
|||||||
perf: [],
|
perf: [],
|
||||||
notes: ["Open question: swipe direction — left | right only, or also up | down?"],
|
notes: ["Open question: swipe direction — left | right only, or also up | down?"],
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
|
|
||||||
export const CORE_INVARIANTS: string[] = [
|
coreInvariants: [
|
||||||
"items = ∅ is a valid state (empty list, not an error)",
|
"items = ∅ is a valid state (empty list, not an error)",
|
||||||
"items are identity-unique — no duplicates by key",
|
"items are identity-unique — no duplicates by key",
|
||||||
"elems[items] = members — ordered form stays in sync with the set",
|
"elems[items] = members — ordered form stays in sync with the set",
|
||||||
]
|
],
|
||||||
|
|
||||||
// ── Composition map model ──────────────────────────────────────────────────
|
composition: {
|
||||||
export interface CapNode {
|
caps: [
|
||||||
id: string
|
|
||||||
label: string
|
|
||||||
sub: string
|
|
||||||
core?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const COMP_CAPS: CapNode[] = [
|
|
||||||
{ id: "core", label: "List : Set", sub: "items : seq Item", core: true },
|
{ id: "core", label: "List : Set", sub: "items : seq Item", core: true },
|
||||||
{ id: "async", label: "Async", sub: "loadState" },
|
{ id: "async", label: "Async", sub: "loadState" },
|
||||||
{ id: "paginated", label: "Paginated", sub: "totalCount" },
|
{ id: "paginated", label: "Paginated", sub: "totalCount" },
|
||||||
@@ -252,11 +210,9 @@ export const COMP_CAPS: CapNode[] = [
|
|||||||
{ id: "selectable", label: "Selectable", sub: "selected" },
|
{ id: "selectable", label: "Selectable", sub: "selected" },
|
||||||
{ id: "sortable", label: "Sortable", sub: "cmp" },
|
{ id: "sortable", label: "Sortable", sub: "cmp" },
|
||||||
{ id: "swipeable", label: "Swipeable", sub: "+ Reorderable" },
|
{ id: "swipeable", label: "Swipeable", sub: "+ Reorderable" },
|
||||||
]
|
],
|
||||||
|
funcs: ["Display", "Scroll", "Load", "Filter", "Sort", "Select", "Act"],
|
||||||
export const COMP_FUNCS: string[] = ["Display", "Scroll", "Load", "Filter", "Sort", "Select", "Act"]
|
links: [
|
||||||
|
|
||||||
export const COMP_LINKS: [string, string][] = [
|
|
||||||
["core", "Display"],
|
["core", "Display"],
|
||||||
["core", "Scroll"],
|
["core", "Scroll"],
|
||||||
["async", "Display"],
|
["async", "Display"],
|
||||||
@@ -267,52 +223,8 @@ export const COMP_LINKS: [string, string][] = [
|
|||||||
["selectable", "Select"],
|
["selectable", "Select"],
|
||||||
["sortable", "Sort"],
|
["sortable", "Sort"],
|
||||||
["swipeable", "Act"],
|
["swipeable", "Act"],
|
||||||
]
|
],
|
||||||
|
},
|
||||||
|
|
||||||
// ── LoadState machine model ────────────────────────────────────────────────
|
stateMachine: LOADSTATE_MACHINE,
|
||||||
export interface SmNode {
|
|
||||||
id: string
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
w: number
|
|
||||||
h: number
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SmKind = "succeed" | "fail" | "refresh" | "loadMore"
|
|
||||||
|
|
||||||
export interface SmEdge {
|
|
||||||
from: string
|
|
||||||
to: string
|
|
||||||
label: string
|
|
||||||
kind: SmKind
|
|
||||||
off: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SM_NODES: SmNode[] = [
|
|
||||||
{ id: "loading", x: 70, y: 150, w: 110, h: 40, label: "Loading" },
|
|
||||||
{ id: "idle", x: 300, y: 150, w: 96, h: 40, label: "Idle" },
|
|
||||||
{ id: "refreshing", x: 520, y: 56, w: 130, h: 40, label: "Refreshing" },
|
|
||||||
{ id: "loadingmore", x: 520, y: 250, w: 140, h: 40, label: "LoadingMore" },
|
|
||||||
{ id: "error", x: 770, y: 150, w: 96, h: 40, label: "Error" },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const SM_EDGES: SmEdge[] = [
|
|
||||||
{ from: "loading", to: "idle", label: "succeed()", kind: "succeed", off: 0 },
|
|
||||||
{ from: "loading", to: "error", label: "fail()", kind: "fail", off: 150 },
|
|
||||||
{ from: "idle", to: "refreshing", label: "refresh()", kind: "refresh", off: -16 },
|
|
||||||
{ from: "refreshing", to: "idle", label: "succeed()", kind: "succeed", off: -16 },
|
|
||||||
{ from: "idle", to: "loadingmore", label: "loadMore()", kind: "loadMore", off: 16 },
|
|
||||||
{ from: "loadingmore", to: "idle", label: "succeed()", kind: "succeed", off: 16 },
|
|
||||||
{ from: "refreshing", to: "error", label: "fail()", kind: "fail", off: 14 },
|
|
||||||
{ from: "loadingmore", to: "error", label: "fail()", kind: "fail", off: -14 },
|
|
||||||
{ from: "error", to: "refreshing", label: "refresh()", kind: "refresh", off: 70 },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const SM_COLORS: Record<SmKind | "init", string> = {
|
|
||||||
succeed: "#7fdca0",
|
|
||||||
fail: "#ff8f8f",
|
|
||||||
refresh: "#ffb84d",
|
|
||||||
loadMore: "#8fd0ff",
|
|
||||||
init: "#4f7099",
|
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/data/loadStateMachine.ts
Normal file
25
src/data/loadStateMachine.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { StateMachine } from "./blueprint"
|
||||||
|
|
||||||
|
// The composed LoadState lifecycle: Idle / Loading / Refreshing / LoadingMore / Error.
|
||||||
|
// Shared verbatim by every blueprint that composes Async + Paginated + Pullable
|
||||||
|
// (List, Grid, Feed, Table, …). Node coordinates are hand-placed for a clean layout.
|
||||||
|
export const LOADSTATE_MACHINE: StateMachine = {
|
||||||
|
nodes: [
|
||||||
|
{ id: "loading", x: 70, y: 150, w: 110, h: 40, label: "Loading" },
|
||||||
|
{ id: "idle", x: 300, y: 150, w: 96, h: 40, label: "Idle" },
|
||||||
|
{ id: "refreshing", x: 520, y: 56, w: 130, h: 40, label: "Refreshing" },
|
||||||
|
{ id: "loadingmore", x: 520, y: 250, w: 140, h: 40, label: "LoadingMore" },
|
||||||
|
{ id: "error", x: 770, y: 150, w: 96, h: 40, label: "Error" },
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{ from: "loading", to: "idle", label: "succeed()", kind: "succeed", off: 0 },
|
||||||
|
{ from: "loading", to: "error", label: "fail()", kind: "fail", off: 150 },
|
||||||
|
{ from: "idle", to: "refreshing", label: "refresh()", kind: "refresh", off: -16 },
|
||||||
|
{ from: "refreshing", to: "idle", label: "succeed()", kind: "succeed", off: -16 },
|
||||||
|
{ from: "idle", to: "loadingmore", label: "loadMore()", kind: "loadMore", off: 16 },
|
||||||
|
{ from: "loadingmore", to: "idle", label: "succeed()", kind: "succeed", off: 16 },
|
||||||
|
{ from: "refreshing", to: "error", label: "fail()", kind: "fail", off: 14 },
|
||||||
|
{ from: "loadingmore", to: "error", label: "fail()", kind: "fail", off: -14 },
|
||||||
|
{ from: "error", to: "refreshing", label: "refresh()", kind: "refresh", off: 70 },
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createApp } from "vue"
|
import { createApp } from "vue"
|
||||||
import "./style.css"
|
import "./style.css"
|
||||||
import App from "./App.vue"
|
import App from "./App.vue"
|
||||||
|
import { router } from "./router"
|
||||||
|
|
||||||
createApp(App).mount("#app")
|
createApp(App).use(router).mount("#app")
|
||||||
|
|||||||
17
src/router.ts
Normal file
17
src/router.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router"
|
||||||
|
import GalleryView from "@/views/GalleryView.vue"
|
||||||
|
import BlueprintView from "@/views/BlueprintView.vue"
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{ path: "/", name: "gallery", component: GalleryView },
|
||||||
|
{ path: "/b/:slug", name: "blueprint", component: BlueprintView, props: true },
|
||||||
|
{ path: "/:pathMatch(.*)*", redirect: "/" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes,
|
||||||
|
scrollBehavior() {
|
||||||
|
return { top: 0 }
|
||||||
|
},
|
||||||
|
})
|
||||||
501
src/specimens/GridSpecimen.vue
Normal file
501
src/specimens/GridSpecimen.vue
Normal file
@@ -0,0 +1,501 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
|
const props = defineProps<{ activeId: string }>()
|
||||||
|
|
||||||
|
const LABELS = ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
|
||||||
|
|
||||||
|
interface Tile {
|
||||||
|
key: string
|
||||||
|
label?: string
|
||||||
|
skeleton?: boolean
|
||||||
|
dim?: boolean
|
||||||
|
sel?: boolean
|
||||||
|
ghost?: boolean
|
||||||
|
check?: boolean
|
||||||
|
pos?: string
|
||||||
|
hlTag?: string
|
||||||
|
}
|
||||||
|
interface Toolbar {
|
||||||
|
search?: { q?: string; hl?: boolean }
|
||||||
|
select?: { meta: string; hl?: boolean }
|
||||||
|
badge?: string
|
||||||
|
badgeNote?: string
|
||||||
|
columnsBadge?: string
|
||||||
|
}
|
||||||
|
interface Frame {
|
||||||
|
kind: "grid" | "display"
|
||||||
|
note?: string
|
||||||
|
toolbar?: Toolbar
|
||||||
|
columns: number
|
||||||
|
tiles: Tile[]
|
||||||
|
scrollbar?: { hl?: boolean; top: number; h: number }
|
||||||
|
footer?: { text: string; hl?: boolean }
|
||||||
|
gridTag?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function tile(i: number, extra: Partial<Tile> = {}): Tile {
|
||||||
|
return { key: `t${i}`, label: `Item ${LABELS[i]}`, ...extra }
|
||||||
|
}
|
||||||
|
function skel(i: number): Tile {
|
||||||
|
return { key: `s${i}`, skeleton: true }
|
||||||
|
}
|
||||||
|
function range(a: number, b: number) {
|
||||||
|
const out: number[] = []
|
||||||
|
for (let i = a; i <= b; i++) out.push(i)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLS = 3
|
||||||
|
|
||||||
|
const frame = computed<Frame>(() => {
|
||||||
|
switch (props.activeId) {
|
||||||
|
case "full":
|
||||||
|
return {
|
||||||
|
kind: "grid",
|
||||||
|
columns: COLS,
|
||||||
|
toolbar: { search: { hl: true }, select: { meta: "multi", hl: true } },
|
||||||
|
tiles: [
|
||||||
|
tile(0, { hlTag: "◈ DISPLAY" }),
|
||||||
|
tile(1),
|
||||||
|
tile(2),
|
||||||
|
tile(3),
|
||||||
|
tile(4),
|
||||||
|
tile(5, { pos: "r1·c2", hlTag: "◈ POSITION" }),
|
||||||
|
],
|
||||||
|
scrollbar: { hl: true, top: 10, h: 46 },
|
||||||
|
footer: { text: "↻ loadMore ▾ · items 6 / 48", hl: true },
|
||||||
|
}
|
||||||
|
case "display":
|
||||||
|
return {
|
||||||
|
kind: "display",
|
||||||
|
columns: COLS,
|
||||||
|
note: "Distinct states, cells of equal dimensions. Skeleton cells mirror real cell size so the transition to Idle causes no layout shift.",
|
||||||
|
tiles: [],
|
||||||
|
}
|
||||||
|
case "scroll":
|
||||||
|
return {
|
||||||
|
kind: "grid",
|
||||||
|
columns: COLS,
|
||||||
|
note: "▓ render window — only these rows exist in the DOM. The row (not the cell) is the unit of virtualization; faded rows release their cell view references.",
|
||||||
|
tiles: [
|
||||||
|
...range(0, 5).map((i) => tile(i)),
|
||||||
|
...range(6, 8).map((i) => tile(i, { ghost: true })),
|
||||||
|
],
|
||||||
|
scrollbar: { hl: true, top: 28, h: 44 },
|
||||||
|
}
|
||||||
|
case "load":
|
||||||
|
return {
|
||||||
|
kind: "grid",
|
||||||
|
columns: COLS,
|
||||||
|
toolbar: { badge: "loadState = LoadingMore", badgeNote: "appending page 2…" },
|
||||||
|
tiles: [...range(0, 5).map((i) => tile(i)), skel(6), skel(7), skel(8)],
|
||||||
|
footer: { text: "↻ loadMore ▾ · items 6 / 48", hl: true },
|
||||||
|
}
|
||||||
|
case "position":
|
||||||
|
return {
|
||||||
|
kind: "grid",
|
||||||
|
columns: COLS,
|
||||||
|
toolbar: { columnsBadge: "columns = 3" },
|
||||||
|
note: "row(i) = i / columns · col(i) = i mod columns — each cell's (row, col) is derived from its index; resize(n) reflows.",
|
||||||
|
tiles: range(0, 8).map((i) => tile(i, { pos: `r${Math.floor(i / COLS)}·c${i % COLS}` })),
|
||||||
|
gridTag: "◈ POSITION",
|
||||||
|
}
|
||||||
|
case "filter":
|
||||||
|
return {
|
||||||
|
kind: "grid",
|
||||||
|
columns: COLS,
|
||||||
|
toolbar: { search: { q: '"b"', hl: true } },
|
||||||
|
note: 'visible = { x ∈ items | name matches "b" } · the underlying items sequence is untouched.',
|
||||||
|
tiles: [
|
||||||
|
tile(0, { dim: true }),
|
||||||
|
tile(1, { hlTag: "◈ visible" }),
|
||||||
|
tile(2, { dim: true }),
|
||||||
|
tile(3, { dim: true }),
|
||||||
|
tile(4, { dim: true }),
|
||||||
|
tile(5, { dim: true }),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
case "select":
|
||||||
|
return {
|
||||||
|
kind: "grid",
|
||||||
|
columns: COLS,
|
||||||
|
toolbar: { select: { meta: "MULTI · 2 selected", hl: true } },
|
||||||
|
note: "selectionMode = Multi · selected = { Item B, Item E } ⊆ visible",
|
||||||
|
tiles: [
|
||||||
|
tile(0, { check: false }),
|
||||||
|
tile(1, { check: true, sel: true }),
|
||||||
|
tile(2, { check: false }),
|
||||||
|
tile(3, { check: false }),
|
||||||
|
tile(4, { check: true, sel: true }),
|
||||||
|
tile(5, { check: false }),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return { kind: "grid", columns: COLS, tiles: [] }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const gridStyle = computed(() => ({ gridTemplateColumns: `repeat(${frame.value.columns}, 1fr)` }))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="specimen">
|
||||||
|
<!-- Display: distinct states -->
|
||||||
|
<template v-if="frame.kind === 'display'">
|
||||||
|
<p class="note">{{ frame.note }}</p>
|
||||||
|
<div class="display-grid">
|
||||||
|
<div class="mini">
|
||||||
|
<div class="mh">Empty · items = ∅</div>
|
||||||
|
<div class="mb">
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="big">∅</div>
|
||||||
|
no items — valid state
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mini">
|
||||||
|
<div class="mh">Loading</div>
|
||||||
|
<div class="mb">
|
||||||
|
<div class="tilegrid mini-tg">
|
||||||
|
<div v-for="i in 4" :key="i" class="tile skeleton"><div class="tskel" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mini">
|
||||||
|
<div class="mh">Error</div>
|
||||||
|
<div class="mb">
|
||||||
|
<div class="error-state">⚠ failed to load<br /><span class="retry">↻ retry</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mini">
|
||||||
|
<div class="mh">Idle</div>
|
||||||
|
<div class="mb">
|
||||||
|
<div class="tilegrid mini-tg">
|
||||||
|
<div v-for="i in 4" :key="i" class="tile">
|
||||||
|
<div class="tlabel">Item {{ LABELS[i - 1] }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Grid-shaped frames -->
|
||||||
|
<template v-else>
|
||||||
|
<p v-if="frame.note" class="note">{{ frame.note }}</p>
|
||||||
|
|
||||||
|
<div v-if="frame.toolbar" class="spec-toolbar">
|
||||||
|
<div v-if="frame.toolbar.badge" class="badge">{{ frame.toolbar.badge }}</div>
|
||||||
|
<span v-if="frame.toolbar.badgeNote" class="badge-note">{{ frame.toolbar.badgeNote }}</span>
|
||||||
|
<div v-if="frame.toolbar.columnsBadge" class="badge">{{ frame.toolbar.columnsBadge }}</div>
|
||||||
|
<div
|
||||||
|
v-if="frame.toolbar.search"
|
||||||
|
class="search"
|
||||||
|
:class="{ hl: frame.toolbar.search.hl }"
|
||||||
|
:data-tag="frame.toolbar.search.hl ? '◈ FILTER' : undefined"
|
||||||
|
>
|
||||||
|
⌕
|
||||||
|
<span v-if="frame.toolbar.search.q" class="q">{{ frame.toolbar.search.q }}</span>
|
||||||
|
<span v-else class="ph">search…</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="frame.toolbar.select"
|
||||||
|
class="ctl"
|
||||||
|
:class="frame.toolbar.select.hl ? ['hl', 'tag-r'] : []"
|
||||||
|
:data-tag="frame.toolbar.select.hl ? '◈ SELECT' : undefined"
|
||||||
|
>
|
||||||
|
☰ {{ frame.toolbar.select.meta }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gridbox" :class="frame.gridTag ? ['hl', 'tag-b'] : []" :data-tag="frame.gridTag">
|
||||||
|
<div class="gridwrap">
|
||||||
|
<div class="tilegrid" :style="gridStyle">
|
||||||
|
<div
|
||||||
|
v-for="t in frame.tiles"
|
||||||
|
:key="t.key"
|
||||||
|
class="tile"
|
||||||
|
:class="{
|
||||||
|
dim: t.dim,
|
||||||
|
sel: t.sel,
|
||||||
|
ghost: t.ghost,
|
||||||
|
skeleton: t.skeleton,
|
||||||
|
hl: t.hlTag,
|
||||||
|
}"
|
||||||
|
:data-tag="t.hlTag"
|
||||||
|
>
|
||||||
|
<span v-if="t.check !== undefined" class="chk" :class="{ on: t.check }">{{
|
||||||
|
t.check ? "✓" : ""
|
||||||
|
}}</span>
|
||||||
|
<div v-if="t.skeleton" class="tskel" />
|
||||||
|
<template v-else>
|
||||||
|
<div class="tlabel">{{ t.label }}</div>
|
||||||
|
<div v-if="t.pos" class="tpos">{{ t.pos }}</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="frame.scrollbar"
|
||||||
|
class="scrollbar"
|
||||||
|
:class="frame.scrollbar.hl ? ['hl', 'tag-r'] : []"
|
||||||
|
:data-tag="frame.scrollbar.hl ? '◈ SCROLL' : undefined"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="thumb"
|
||||||
|
:style="{ top: frame.scrollbar.top + '%', height: frame.scrollbar.h + '%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="frame.footer"
|
||||||
|
class="footer-load"
|
||||||
|
:class="frame.footer.hl ? ['hl', 'tag-b'] : []"
|
||||||
|
:data-tag="frame.footer.hl ? '◈ LOAD' : undefined"
|
||||||
|
>
|
||||||
|
{{ frame.footer.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.specimen {
|
||||||
|
--ink: var(--color-base-content);
|
||||||
|
--ink-faint: #4f7099;
|
||||||
|
--amber: var(--color-accent);
|
||||||
|
--amber-dim: color-mix(in srgb, var(--color-accent) 14%, transparent);
|
||||||
|
--cyan: var(--color-secondary);
|
||||||
|
--red: var(--color-error);
|
||||||
|
--paper: var(--color-base-100);
|
||||||
|
--line: rgba(200, 226, 255, 0.14);
|
||||||
|
--line-strong: rgba(200, 226, 255, 0.3);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
.spec-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: color-mix(in srgb, var(--ink) 60%, transparent);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.badge-note {
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
.search {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--paper);
|
||||||
|
padding: 5px 9px;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.ph {
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
.q {
|
||||||
|
color: var(--amber);
|
||||||
|
}
|
||||||
|
.ctl {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
padding: 5px 9px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
color: var(--cyan);
|
||||||
|
padding: 1px 7px;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
.gridbox {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--paper);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.gridwrap {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.tilegrid {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.tile {
|
||||||
|
border: 1px dashed var(--line-strong);
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.tlabel {
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.tpos {
|
||||||
|
color: var(--cyan);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.chk {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: 5px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 1px solid var(--ink-faint);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--amber);
|
||||||
|
}
|
||||||
|
.chk.on {
|
||||||
|
border-color: var(--amber);
|
||||||
|
background: var(--amber-dim);
|
||||||
|
}
|
||||||
|
.tile.dim {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
.tile.dim .tlabel {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.tile.sel {
|
||||||
|
background: var(--amber-dim);
|
||||||
|
border-color: var(--amber);
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
.tile.ghost {
|
||||||
|
opacity: 0.32;
|
||||||
|
background: rgba(143, 208, 255, 0.05);
|
||||||
|
}
|
||||||
|
.tile.skeleton {
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
.tskel {
|
||||||
|
width: 60%;
|
||||||
|
height: 10px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--ink-faint),
|
||||||
|
var(--ink-faint) 6px,
|
||||||
|
transparent 6px,
|
||||||
|
transparent 12px
|
||||||
|
);
|
||||||
|
opacity: 0.5;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
flex: none;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-left: 1px solid var(--line);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.scrollbar .thumb {
|
||||||
|
position: absolute;
|
||||||
|
left: 1px;
|
||||||
|
right: 1px;
|
||||||
|
background: var(--ink-faint);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.footer-load {
|
||||||
|
padding: 8px 11px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
text-align: center;
|
||||||
|
color: var(--cyan);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.display-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.mini {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--paper);
|
||||||
|
}
|
||||||
|
.mini .mh {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
color: color-mix(in srgb, var(--ink) 60%, transparent);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.mini .mb {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.mini-tg {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.mini-tg .tile {
|
||||||
|
aspect-ratio: 3 / 2;
|
||||||
|
}
|
||||||
|
.mini-tg .tlabel {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.empty-state {
|
||||||
|
padding: 16px 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
.empty-state .big {
|
||||||
|
font-size: 22px;
|
||||||
|
color: color-mix(in srgb, var(--ink) 60%, transparent);
|
||||||
|
}
|
||||||
|
.error-state {
|
||||||
|
padding: 16px 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--red);
|
||||||
|
line-height: 1.9;
|
||||||
|
}
|
||||||
|
.error-state .retry {
|
||||||
|
color: var(--cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* callouts */
|
||||||
|
.hl {
|
||||||
|
outline: 1.5px dashed var(--amber);
|
||||||
|
outline-offset: 3px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.hl[data-tag]::after {
|
||||||
|
content: attr(data-tag);
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
left: 8px;
|
||||||
|
background: var(--paper);
|
||||||
|
color: var(--amber);
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border: 1px solid var(--amber);
|
||||||
|
white-space: nowrap;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
line-height: 1.5;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.hl.tag-r[data-tag]::after {
|
||||||
|
left: auto;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
.hl.tag-b[data-tag]::after {
|
||||||
|
top: auto;
|
||||||
|
bottom: -10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import { SAMPLE, type SampleItem } from "@/data/listBlueprint"
|
|
||||||
|
// Bespoke sample data for the List specimen (visual only).
|
||||||
|
interface SampleItem {
|
||||||
|
n: string
|
||||||
|
s: string
|
||||||
|
}
|
||||||
|
const SAMPLE: SampleItem[] = [
|
||||||
|
{ n: "Item A", s: "#a1f3 · ready" },
|
||||||
|
{ n: "Item B", s: "#b2e8 · ready" },
|
||||||
|
{ n: "Item C", s: "#c7d1 · ready" },
|
||||||
|
{ n: "Item D", s: "#d0a4 · ready" },
|
||||||
|
{ n: "Item E", s: "#e5b9 · ready" },
|
||||||
|
{ n: "Item F", s: "#f3c2 · ready" },
|
||||||
|
{ n: "Item G", s: "#01d7 · ready" },
|
||||||
|
{ n: "Item H", s: "#12e0 · ready" },
|
||||||
|
]
|
||||||
|
|
||||||
const props = defineProps<{ activeId: string }>()
|
const props = defineProps<{ activeId: string }>()
|
||||||
|
|
||||||
40
src/views/BlueprintView.vue
Normal file
40
src/views/BlueprintView.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { entryFor } from "@/data/blueprints"
|
||||||
|
import BlueprintViewer from "@/components/BlueprintViewer.vue"
|
||||||
|
|
||||||
|
const props = defineProps<{ slug: string }>()
|
||||||
|
const entry = computed(() => entryFor(props.slug))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BlueprintViewer v-if="entry" :blueprint="entry.blueprint" :specimen="entry.specimen" />
|
||||||
|
<div v-else class="notfound">
|
||||||
|
<p>
|
||||||
|
No blueprint <code>{{ slug }}</code> is illustrated yet.
|
||||||
|
</p>
|
||||||
|
<RouterLink to="/" class="back">▸ back to the gallery</RouterLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.notfound {
|
||||||
|
max-width: 1180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #9db8d2;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.notfound code {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
.back {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 14px;
|
||||||
|
color: var(--color-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.back:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
124
src/views/GalleryView.vue
Normal file
124
src/views/GalleryView.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { BLUEPRINTS } from "@/data/blueprints"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="gallery">
|
||||||
|
<h1 class="g-title">Blueprint gallery</h1>
|
||||||
|
<p class="g-sub">
|
||||||
|
Interactive illustrations of UI-pattern blueprints from the
|
||||||
|
<code>blueprint-ontology</code>. {{ BLUEPRINTS.length }} illustrated so far — more to come.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<RouterLink v-for="b in BLUEPRINTS" :key="b.slug" :to="`/b/${b.slug}`" class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<span class="c-name">{{ b.name }}</span>
|
||||||
|
<span class="c-role" :class="b.role">{{ b.role }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="c-tag">{{ b.tagline }}</p>
|
||||||
|
<div class="c-meta">
|
||||||
|
<span v-if="b.extendsName">extends {{ b.extendsName }}</span>
|
||||||
|
<span>{{ b.functions.length }} functions</span>
|
||||||
|
</div>
|
||||||
|
<div class="c-open">open ▸</div>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gallery {
|
||||||
|
max-width: 1180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 20px 60px;
|
||||||
|
color: var(--color-base-content);
|
||||||
|
}
|
||||||
|
.g-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.g-sub {
|
||||||
|
color: #7ea6cd;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 6px 0 24px;
|
||||||
|
}
|
||||||
|
.g-sub code {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
border: 1.5px solid rgba(200, 226, 255, 0.3);
|
||||||
|
background: var(--color-base-200);
|
||||||
|
padding: 14px 16px 12px;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.12s;
|
||||||
|
}
|
||||||
|
.card::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
left: -1px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-top: 1.5px solid var(--color-accent);
|
||||||
|
border-left: 1.5px solid var(--color-accent);
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
border-color: var(--color-secondary);
|
||||||
|
background: var(--color-base-300);
|
||||||
|
}
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.c-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
.c-role {
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border: 1px solid;
|
||||||
|
padding: 1px 6px;
|
||||||
|
}
|
||||||
|
.c-role.feature {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
border-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
.c-role.capability {
|
||||||
|
color: #7ea6cd;
|
||||||
|
border-color: #4f7099;
|
||||||
|
}
|
||||||
|
.c-tag {
|
||||||
|
color: #9db8d2;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 8px 0 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.c-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
color: #4f7099;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.c-open {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: var(--color-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user