@yojahny/wp-design-library 0.3.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/README.md +52 -9
- package/bin/library.mjs +11 -0
- package/entries/ada/entry.md +55 -0
- package/entries/ada/strip.webp +0 -0
- package/entries/air/captures/2/measure.json +325 -0
- package/entries/air/captures/2/strip.webp +0 -0
- package/entries/air/entry.md +62 -0
- package/entries/air/strip.webp +0 -0
- package/entries/ais-community-dark-depth/strip.webp +0 -0
- package/entries/apple-home/captures/2/measure.json +91 -0
- package/entries/apple-home/captures/2/strip.webp +0 -0
- package/entries/apple-home/entry.md +64 -0
- package/entries/apple-home/strip.webp +0 -0
- package/entries/apple-macbook-neo/captures/2/measure.json +570 -0
- package/entries/apple-macbook-neo/captures/2/strip.webp +0 -0
- package/entries/apple-macbook-neo/entry.md +62 -0
- package/entries/apple-macbook-neo/strip.webp +0 -0
- package/entries/apple-watch-ultra-4/entry.md +63 -0
- package/entries/apple-watch-ultra-4/measure.json +481 -0
- package/entries/apple-watch-ultra-4/strip.webp +0 -0
- package/entries/bpco/captures/2/strip.webp +0 -0
- package/entries/bpco/captures/3/strip.webp +0 -0
- package/entries/bpco/entry.md +81 -0
- package/entries/bpco/strip.webp +0 -0
- package/entries/column/captures/2/measure.json +996 -0
- package/entries/column/captures/2/strip.webp +0 -0
- package/entries/column/entry.md +82 -0
- package/entries/column/strip.webp +0 -0
- package/entries/cthdrl/captures/2/measure.json +251 -0
- package/entries/cthdrl/captures/2/strip.webp +0 -0
- package/entries/cthdrl/entry.md +86 -0
- package/entries/cthdrl/strip.webp +0 -0
- package/entries/dala-craftedbygc/entry.md +69 -0
- package/entries/dala-craftedbygc/strip.webp +0 -0
- package/entries/dock/captures/2/measure.json +206 -0
- package/entries/dock/captures/2/strip.webp +0 -0
- package/entries/dock/entry.md +62 -0
- package/entries/dock/strip.webp +0 -0
- package/entries/dub/captures/2/measure.json +37 -0
- package/entries/dub/captures/2/strip.webp +0 -0
- package/entries/dub/entry.md +61 -0
- package/entries/dub/strip.webp +0 -0
- package/entries/duolingo/captures/2/measure.json +138 -0
- package/entries/duolingo/captures/2/strip.webp +0 -0
- package/entries/duolingo/entry.md +65 -0
- package/entries/duolingo/strip.webp +0 -0
- package/entries/elevenlabs/captures/2/measure.json +160 -0
- package/entries/elevenlabs/captures/2/strip.webp +0 -0
- package/entries/elevenlabs/entry.md +62 -0
- package/entries/elevenlabs/strip.webp +0 -0
- package/entries/flying-papers/captures/2/measure.json +291 -0
- package/entries/flying-papers/captures/2/strip.webp +0 -0
- package/entries/flying-papers/entry.md +61 -0
- package/entries/flying-papers/strip.webp +0 -0
- package/entries/gsap/captures/2/measure.json +120 -0
- package/entries/gsap/captures/2/strip.webp +0 -0
- package/entries/gsap/entry.md +62 -0
- package/entries/gsap/strip.webp +0 -0
- package/entries/letters/captures/2/measure.json +20 -0
- package/entries/letters/captures/2/strip.webp +0 -0
- package/entries/letters/entry.md +62 -0
- package/entries/letters/strip.webp +0 -0
- package/entries/micro/captures/2/measure.json +852 -0
- package/entries/micro/captures/2/strip.webp +0 -0
- package/entries/micro/entry.md +61 -0
- package/entries/micro/strip.webp +0 -0
- package/entries/monopo-saigon/captures/2/measure.json +145 -0
- package/entries/monopo-saigon/captures/2/strip.webp +0 -0
- package/entries/monopo-saigon/entry.md +64 -0
- package/entries/monopo-saigon/strip.webp +0 -0
- package/entries/notion/captures/2/measure.json +338 -0
- package/entries/notion/captures/2/strip.webp +0 -0
- package/entries/notion/entry.md +66 -0
- package/entries/notion/strip.webp +0 -0
- package/entries/origin-financial/captures/2/measure.json +169 -0
- package/entries/origin-financial/captures/2/strip.webp +0 -0
- package/entries/origin-financial/entry.md +65 -0
- package/entries/origin-financial/strip.webp +0 -0
- package/entries/oryzo-ai/captures/2/measure.json +39 -0
- package/entries/oryzo-ai/captures/2/strip.webp +0 -0
- package/entries/oryzo-ai/entry.md +76 -0
- package/entries/oryzo-ai/strip.webp +0 -0
- package/entries/silencio/captures/2/strip.webp +0 -0
- package/entries/silencio/entry.md +114 -0
- package/entries/silencio/motion/product-story/frame-0.webp +0 -0
- package/entries/silencio/motion/product-story/frame-1.webp +0 -0
- package/entries/silencio/motion/product-story/frame-2.webp +0 -0
- package/entries/silencio/motion/product-story/frame-3.webp +0 -0
- package/entries/silencio/motion/product-story/frame-4.webp +0 -0
- package/entries/silencio/motion/product-story/frame-5.webp +0 -0
- package/entries/silencio/motion/product-story/frame-6.webp +0 -0
- package/entries/silencio/motion/product-story/frame-7.webp +0 -0
- package/entries/silencio/strip.webp +0 -0
- package/entries/steep/captures/2/measure.json +20 -0
- package/entries/steep/captures/2/strip.webp +0 -0
- package/entries/steep/entry.md +63 -0
- package/entries/steep/strip.webp +0 -0
- package/entries/stripe/captures/2/measure.json +892 -0
- package/entries/stripe/captures/2/strip.webp +0 -0
- package/entries/stripe/entry.md +61 -0
- package/entries/stripe/strip.webp +0 -0
- package/entries/superpower/captures/2/measure.json +179 -0
- package/entries/superpower/captures/2/strip.webp +0 -0
- package/entries/superpower/entry.md +64 -0
- package/entries/superpower/strip.webp +0 -0
- package/entries/superr/entry.md +54 -0
- package/entries/superr/strip.webp +0 -0
- package/entries/svz-weare/captures/2/measure.json +129 -0
- package/entries/svz-weare/captures/2/strip.webp +0 -0
- package/entries/svz-weare/entry.md +147 -0
- package/entries/svz-weare/motion/hero-transition/frame-0.webp +0 -0
- package/entries/svz-weare/motion/hero-transition/frame-1.webp +0 -0
- package/entries/svz-weare/motion/hero-transition/frame-2.webp +0 -0
- package/entries/svz-weare/motion/hero-transition/frame-3.webp +0 -0
- package/entries/svz-weare/motion/hero-transition/frame-4.webp +0 -0
- package/entries/svz-weare/motion/hero-transition/frame-5.webp +0 -0
- package/entries/svz-weare/motion/hero-transition/frame-6.webp +0 -0
- package/entries/svz-weare/motion/hero-transition/frame-7.webp +0 -0
- package/entries/svz-weare/strip.webp +0 -0
- package/entries/thoughtlab/captures/2/measure.json +93 -0
- package/entries/thoughtlab/captures/2/strip.webp +0 -0
- package/entries/thoughtlab/entry.md +62 -0
- package/entries/thoughtlab/strip.webp +0 -0
- package/entries/ventriloc/entry.md +54 -0
- package/entries/ventriloc/strip.webp +0 -0
- package/entries/vivid-and-co/entry.md +58 -0
- package/entries/vivid-and-co/strip.webp +0 -0
- package/package.json +24 -5
- package/src/cli/add.mjs +100 -24
- package/src/cli/motion.mjs +168 -0
- package/src/cli/refresh.mjs +6 -0
- package/src/cli/ui.mjs +2 -1
- package/src/entry.mjs +14 -1
- package/src/index/build.mjs +9 -3
- package/src/index/embed.mjs +1 -1
- package/src/ingest/agreement.mjs +33 -0
- package/src/ingest/attach.mjs +67 -0
- package/src/ingest/draft.mjs +5 -2
- package/src/ingest/frames.mjs +20 -6
- package/src/ingest/measure.mjs +34 -1
- package/src/ingest/observed.mjs +29 -0
- package/src/mcp/http.mjs +1 -1
- package/src/mcp/instructions.mjs +2 -1
- package/src/mcp/prompts.mjs +12 -1
- package/src/mcp/resources.mjs +14 -0
- package/src/mcp/tools.mjs +48 -5
- package/src/motion.mjs +94 -0
- package/src/ui/build.mjs +83 -7
- package/src/ui/serve.mjs +29 -4
- package/src/ui/templates/entry.html +9 -1
- package/entries/ais-community-dark-depth/strip.png +0 -0
- package/tests/README.md +0 -33
- package/tests/checks/cli-check.sh +0 -13
- package/tests/checks/docker.sh +0 -10
- package/tests/checks/entry-detail.sh +0 -52
- package/tests/checks/entry.sh +0 -55
- package/tests/checks/export.sh +0 -16
- package/tests/checks/fetch-on-start.sh +0 -26
- package/tests/checks/frames-dense.sh +0 -27
- package/tests/checks/http.sh +0 -22
- package/tests/checks/hygiene.sh +0 -29
- package/tests/checks/inbox.sh +0 -27
- package/tests/checks/index-degrade.sh +0 -32
- package/tests/checks/index.sh +0 -36
- package/tests/checks/ingest-mp4.sh +0 -47
- package/tests/checks/ingest.sh +0 -39
- package/tests/checks/instructions.sh +0 -31
- package/tests/checks/licence-gate.sh +0 -24
- package/tests/checks/mcp-stdout.sh +0 -31
- package/tests/checks/measure.sh +0 -79
- package/tests/checks/minors.sh +0 -84
- package/tests/checks/optional-deps.sh +0 -55
- package/tests/checks/prompt-add-entry.sh +0 -28
- package/tests/checks/recommend.sh +0 -107
- package/tests/checks/resources.sh +0 -47
- package/tests/checks/rrf.sh +0 -120
- package/tests/checks/seed-sync.sh +0 -22
- package/tests/checks/similar.sh +0 -62
- package/tests/checks/ui-build.sh +0 -59
- package/tests/checks/vocab.sh +0 -30
- package/tests/fixtures/entry-ok/entry.md +0 -41
- package/tests/fixtures/entry-ok/strip.png +0 -0
- package/tests/fixtures/page/index.html +0 -47
- package/tests/fixtures/three-frame.webp +0 -0
- package/tests/run.sh +0 -16
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"url": "https://www.thoughtlab.com/",
|
|
3
|
+
"at": "2026-09-16",
|
|
4
|
+
"viewport": [
|
|
5
|
+
1440,
|
|
6
|
+
390
|
|
7
|
+
],
|
|
8
|
+
"tokens": {
|
|
9
|
+
"canvas": "#000000",
|
|
10
|
+
"ink": "#cccccc",
|
|
11
|
+
"accent": "#ffffff",
|
|
12
|
+
"fontDisplay": "sui",
|
|
13
|
+
"fontBody": "sui"
|
|
14
|
+
},
|
|
15
|
+
"sections": [
|
|
16
|
+
{
|
|
17
|
+
"index": 0,
|
|
18
|
+
"tag": "header",
|
|
19
|
+
"id": "",
|
|
20
|
+
"class": "sh fix top left x pt30",
|
|
21
|
+
"top": 0,
|
|
22
|
+
"height": 72,
|
|
23
|
+
"animations": []
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"index": 1,
|
|
27
|
+
"tag": "article",
|
|
28
|
+
"id": "home",
|
|
29
|
+
"class": "",
|
|
30
|
+
"top": 0,
|
|
31
|
+
"height": 8966,
|
|
32
|
+
"animations": []
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"index": 2,
|
|
36
|
+
"tag": "section",
|
|
37
|
+
"id": "",
|
|
38
|
+
"class": "hero rel x",
|
|
39
|
+
"top": 0,
|
|
40
|
+
"height": 900,
|
|
41
|
+
"animations": []
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"index": 3,
|
|
45
|
+
"tag": "section",
|
|
46
|
+
"id": "",
|
|
47
|
+
"class": "js-sel mb30 s:mb150",
|
|
48
|
+
"top": 900,
|
|
49
|
+
"height": 2700,
|
|
50
|
+
"animations": []
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"index": 4,
|
|
54
|
+
"tag": "section",
|
|
55
|
+
"id": "",
|
|
56
|
+
"class": "caps mb60 s:mb150 rel",
|
|
57
|
+
"top": 4626,
|
|
58
|
+
"height": 626,
|
|
59
|
+
"animations": []
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"index": 5,
|
|
63
|
+
"tag": "section",
|
|
64
|
+
"id": "",
|
|
65
|
+
"class": "news-list",
|
|
66
|
+
"top": 5469,
|
|
67
|
+
"height": 2219,
|
|
68
|
+
"animations": []
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"index": 6,
|
|
72
|
+
"tag": "section",
|
|
73
|
+
"id": "",
|
|
74
|
+
"class": "h-contact",
|
|
75
|
+
"top": 7688,
|
|
76
|
+
"height": 819,
|
|
77
|
+
"animations": []
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"index": 7,
|
|
81
|
+
"tag": "footer",
|
|
82
|
+
"id": "",
|
|
83
|
+
"class": "sf pb45 c-white",
|
|
84
|
+
"top": 8651,
|
|
85
|
+
"height": 315,
|
|
86
|
+
"animations": []
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"screenshots": {
|
|
90
|
+
"390": "screen-390.png",
|
|
91
|
+
"1440": "screen-1440.png"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: thoughtlab
|
|
3
|
+
title: 'ThoughtLab: data-led consulting landing'
|
|
4
|
+
tier: inspiration
|
|
5
|
+
source:
|
|
6
|
+
kind: public-site
|
|
7
|
+
url: https://www.thoughtlab.com/
|
|
8
|
+
license: Captured from a Refero-hosted recording and the live site; description only, no redistribution of the source.
|
|
9
|
+
captured:
|
|
10
|
+
method: both
|
|
11
|
+
at: '2026-09-16'
|
|
12
|
+
tool: wp-design-library@0.4.0
|
|
13
|
+
captures:
|
|
14
|
+
- method: frames
|
|
15
|
+
input: /tmp/thoughtlab-refero.mp4
|
|
16
|
+
artifacts:
|
|
17
|
+
- strip.webp
|
|
18
|
+
frames: 36
|
|
19
|
+
duration_ms: 24533
|
|
20
|
+
- method: both
|
|
21
|
+
input: https://www.thoughtlab.com/
|
|
22
|
+
artifacts:
|
|
23
|
+
- captures/2/strip.webp
|
|
24
|
+
- captures/2/measure.json
|
|
25
|
+
- captures/2/screen-1440.png
|
|
26
|
+
- captures/2/screen-390.png
|
|
27
|
+
media:
|
|
28
|
+
frames: 36
|
|
29
|
+
duration_ms: 24533
|
|
30
|
+
roles: [hero, feature, capability, proof, closing, footer]
|
|
31
|
+
feel: [dark, premium, editorial, depth, cold]
|
|
32
|
+
palette:
|
|
33
|
+
canvas: '#000000'
|
|
34
|
+
ink: '#cccccc'
|
|
35
|
+
accent: '#ff2257'
|
|
36
|
+
type:
|
|
37
|
+
display: sui
|
|
38
|
+
body: sui
|
|
39
|
+
motion:
|
|
40
|
+
devices: [reveal, pan, drift, wipe, tabs]
|
|
41
|
+
notes: 'The supplied recording moves through a sequence of project names while irregular glass-like image portals change their internal imagery and edge light. Large condensed labels reveal beside each portal, then the composition wipes to the next case. The live capture confirms the hero portal and fixed header, but does not expose CSS animation records or individual input triggers.'
|
|
42
|
+
ported_from: null
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## What it does
|
|
46
|
+
|
|
47
|
+
ThoughtLab presents category-first brand and product work as a dark portfolio sequence. One large organic portal acts as a moving canvas for selected cases, followed by capability, news, and contact sections.
|
|
48
|
+
|
|
49
|
+
## Section roster
|
|
50
|
+
|
|
51
|
+
1. Fixed logo, partnership claim, contact action, and dark hero with an irregular reflective portal.
|
|
52
|
+
2. Selected-work sequence cycling Wander Jaunt, Varex Imaging, Capsule House, and other case imagery.
|
|
53
|
+
3. Capability statement and proof/content sections.
|
|
54
|
+
4. News list, contact prompt, and compact footer.
|
|
55
|
+
|
|
56
|
+
## Why it works
|
|
57
|
+
|
|
58
|
+
The evolving portal gives every case a shared visual frame while keeping the page sparse. Black space, silver type, and a single hot-pink action make the content feel like a studio reel instead of a conventional agency grid.
|
|
59
|
+
|
|
60
|
+
## Sources
|
|
61
|
+
|
|
62
|
+
The Refero MP4 at https://images.refero.design/styles/refero.design/video/504bf76a-aab7-459c-9496-1f520fd0324a.mp4 provides the 24.53-second project sequence and portal transitions. The live capture at https://www.thoughtlab.com/ supplies current desktop/mobile layout and measured tokens. Both sources show the same dark portfolio direction.
|
|
Binary file
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: ventriloc
|
|
3
|
+
title: 'Ventriloc: business intelligence platform'
|
|
4
|
+
tier: inspiration
|
|
5
|
+
source:
|
|
6
|
+
kind: public-site
|
|
7
|
+
url: https://ventriloc.ca/en
|
|
8
|
+
license: Captured from a Refero-hosted recording and the live site; description only, no redistribution of the source.
|
|
9
|
+
captured:
|
|
10
|
+
method: frames
|
|
11
|
+
at: '2026-09-16'
|
|
12
|
+
tool: wp-design-library@0.4.0
|
|
13
|
+
captures:
|
|
14
|
+
- method: frames
|
|
15
|
+
input: /tmp/ventriloc-refero.mp4
|
|
16
|
+
artifacts:
|
|
17
|
+
- strip.webp
|
|
18
|
+
frames: 36
|
|
19
|
+
duration_ms: 6567
|
|
20
|
+
media:
|
|
21
|
+
frames: 36
|
|
22
|
+
duration_ms: 6567
|
|
23
|
+
roles: [hero, proof, feature]
|
|
24
|
+
feel: [light, premium, saas, minimal]
|
|
25
|
+
palette:
|
|
26
|
+
canvas: '#f5f5f5'
|
|
27
|
+
ink: '#202020'
|
|
28
|
+
accent: '#828282'
|
|
29
|
+
type:
|
|
30
|
+
display: PolySans, sans-serif
|
|
31
|
+
body: Inter, sans-serif
|
|
32
|
+
motion:
|
|
33
|
+
devices: [reveal, pan]
|
|
34
|
+
notes: Eyeballed from 36 sampled frames, not measured. Dashboard cards and chart lines animate into the right side of the hero while the headline stays anchored on the left; later sampled frames show the metric states changing. Individual triggers are not visible.
|
|
35
|
+
ported_from: null
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## What it does
|
|
39
|
+
|
|
40
|
+
Ventriloc presents business intelligence as a clean analytics-led growth service.
|
|
41
|
+
|
|
42
|
+
## Section roster
|
|
43
|
+
|
|
44
|
+
1. Split hero with analytics promise, actions, and animated chart cards.
|
|
45
|
+
2. Partner-logo proof row.
|
|
46
|
+
3. Dashboard metric demonstration.
|
|
47
|
+
|
|
48
|
+
## Why it works
|
|
49
|
+
|
|
50
|
+
The restrained page gives its charts plenty of space, making the data cards feel like evidence rather than visual filler. The minimal treatment asks the headline to carry most of the personality.
|
|
51
|
+
|
|
52
|
+
## Sources
|
|
53
|
+
|
|
54
|
+
The Refero MP4 supplies the sampled motion sequence. https://ventriloc.ca/en remains the source URL; its incomplete live render was not retained as a capture.
|
|
Binary file
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: vivid-and-co
|
|
3
|
+
title: 'Vivid & Co.: refractive creative-studio landing'
|
|
4
|
+
tier: inspiration
|
|
5
|
+
source:
|
|
6
|
+
kind: public-site
|
|
7
|
+
url: https://vividand.co/
|
|
8
|
+
license: Captured from the supplied recording and live site; description only, no redistribution of the source.
|
|
9
|
+
captured:
|
|
10
|
+
method: frames
|
|
11
|
+
at: '2026-09-16'
|
|
12
|
+
tool: wp-design-library@0.4.0
|
|
13
|
+
captures:
|
|
14
|
+
- method: frames
|
|
15
|
+
input: /home/yojahny/Desktop/inspiration/evivid.mp4
|
|
16
|
+
artifacts:
|
|
17
|
+
- strip.webp
|
|
18
|
+
frames: 36
|
|
19
|
+
duration_ms: 36833
|
|
20
|
+
media:
|
|
21
|
+
frames: 36
|
|
22
|
+
duration_ms: 36833
|
|
23
|
+
roles: [hero, explainer, capability, proof, closing, footer]
|
|
24
|
+
feel: [dark, premium, editorial, minimal, depth, cold]
|
|
25
|
+
palette:
|
|
26
|
+
canvas: '#101010'
|
|
27
|
+
ink: '#fffdf9'
|
|
28
|
+
accent: '#67d8ff'
|
|
29
|
+
type:
|
|
30
|
+
display: '"Neue Montreal"'
|
|
31
|
+
body: '"Neue Montreal", sans-serif'
|
|
32
|
+
motion:
|
|
33
|
+
devices: [reveal, pan, drift, stagger, wipe]
|
|
34
|
+
notes: 'The supplied recording shows a refractive block assembly slowly rotating and drifting through the opening statements, while large type changes in staged fragments around it. Each chapter uses a restrained fade or wipe into a new text composition; the prismatic light field remains as a connecting layer. The live capture records a long sequence of SVG animations across the page. It confirms that the chromatic object and surrounding graphic treatment are animated, but this capture does not establish individual interaction triggers.'
|
|
35
|
+
ported_from: null
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## What it does
|
|
39
|
+
|
|
40
|
+
Vivid & Co. introduces a full-service agency and strategic advisor through large, declarative statements instead of a project grid. A black, lightly textured field carries minimal white type, small proof details, and a recurring translucent object whose rainbow edges add movement and depth.
|
|
41
|
+
|
|
42
|
+
## Section roster
|
|
43
|
+
|
|
44
|
+
1. Compact header with expertise, work, team, careers, and contact navigation.
|
|
45
|
+
2. Hero that introduces Vivid & Co. beside the refractive block assembly.
|
|
46
|
+
3. Brand-value statement and a large argument for integrated strategy, PR, and creative identity.
|
|
47
|
+
4. Expertise and process sections with sparse text blocks, sector labels, and the moving object.
|
|
48
|
+
5. Client and press proof row.
|
|
49
|
+
6. Team introduction, people profile, advisor/investor list, and careers close.
|
|
50
|
+
7. Contact-led footer.
|
|
51
|
+
|
|
52
|
+
## Why it works
|
|
53
|
+
|
|
54
|
+
The interface has the pacing of a short title sequence. Each large statement gets an uncluttered frame, then the page uses small supporting copy and hairline dividers to reset the scale. The glass-like object concentrates the site’s color, so the rest of the layout can stay almost monochrome without feeling flat.
|
|
55
|
+
|
|
56
|
+
## Sources
|
|
57
|
+
|
|
58
|
+
The supplied 36.83-second recording provides the complete desktop sequence, including the changing opening statements, object movement, expertise/process copy, proof row, team profile, advisors, and closing. The live capture at https://vividand.co/ supplies the current desktop and mobile layouts, measured type/tokens, and the page-wide SVG animation evidence. Both sources show the same black editorial direction and prismatic-object treatment.
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yojahny/wp-design-library",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Design reference corpus and MCP server for claude-wp-builder",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -55,8 +55,27 @@
|
|
|
55
55
|
"docker-compose.yml",
|
|
56
56
|
"docker/",
|
|
57
57
|
"entries/",
|
|
58
|
-
"src/",
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
|
|
58
|
+
"src/cli/add.mjs",
|
|
59
|
+
"src/cli/check.mjs",
|
|
60
|
+
"src/cli/export.mjs",
|
|
61
|
+
"src/cli/index.mjs",
|
|
62
|
+
"src/cli/motion.mjs",
|
|
63
|
+
"src/cli/refresh.mjs",
|
|
64
|
+
"src/cli/save.mjs",
|
|
65
|
+
"src/cli/serve.mjs",
|
|
66
|
+
"src/cli/ui.mjs",
|
|
67
|
+
"src/entry.mjs",
|
|
68
|
+
"src/index/",
|
|
69
|
+
"src/ingest/",
|
|
70
|
+
"src/mcp/",
|
|
71
|
+
"src/motion.mjs",
|
|
72
|
+
"src/paths.mjs",
|
|
73
|
+
"src/ui/",
|
|
74
|
+
"src/vocab.mjs",
|
|
75
|
+
"vocab.yaml",
|
|
76
|
+
"!entries/**/screen-*.png"
|
|
77
|
+
],
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@aws-sdk/client-s3": "^3.1134.0"
|
|
80
|
+
}
|
|
62
81
|
}
|
package/src/cli/add.mjs
CHANGED
|
@@ -1,29 +1,48 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import sharp from 'sharp';
|
|
3
4
|
import { paths } from '../paths.mjs';
|
|
4
|
-
import { sampleFrames, compositeStrip } from '../ingest/frames.mjs';
|
|
5
|
+
import { sampleFrames, compositeStrip, writeStrip } from '../ingest/frames.mjs';
|
|
5
6
|
import { writeDraft } from '../ingest/draft.mjs';
|
|
7
|
+
import { attachCapture } from '../ingest/attach.mjs';
|
|
6
8
|
import { measure } from '../ingest/measure.mjs';
|
|
7
9
|
import { loadVocab, resolveTerm } from '../vocab.mjs';
|
|
10
|
+
import { parseEntry } from '../entry.mjs';
|
|
8
11
|
|
|
9
12
|
function opt(args, name) { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; }
|
|
10
13
|
|
|
14
|
+
// captures/<n>/ where n is the new capture's 1-based position. Capture 1 has no directory —
|
|
15
|
+
// its files stay at the entry root, so existing entries and get_entry's `strip` are untouched.
|
|
16
|
+
function captureDir(dir, index) {
|
|
17
|
+
return index === 1 ? dir : path.join(dir, 'captures', String(index));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The new capture's 1-based position. An entry written before captures[] existed has one
|
|
21
|
+
// capture recorded in its top-level fields, so it counts as 1 and the new one is 2. An entry
|
|
22
|
+
// that EXISTS can never yield 1: index===1 is the wipe trigger, so a hand-written
|
|
23
|
+
// `captures: []` would otherwise turn --attach into --force and destroy the entry.
|
|
24
|
+
function nextCaptureIndex(dir) {
|
|
25
|
+
if (!existsSync(path.join(dir, 'entry.md'))) return 1;
|
|
26
|
+
const { fm } = parseEntry(dir);
|
|
27
|
+
return Math.max(fm.captured?.captures?.length ?? 1, 1) + 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
const MEDIA = /\.(webp|gif|mp4|mov|webm|mkv)$/i;
|
|
12
31
|
const URL_RE = /^(https?:\/\/|file:\/\/)/;
|
|
13
32
|
function slugify(name) { return name.replace(/\.[^.]+$/, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); }
|
|
14
33
|
|
|
15
|
-
// Composites recorded scroll-stop screenshots into a strip.
|
|
34
|
+
// Composites recorded scroll-stop screenshots into a strip.webp the same way sampleFrames
|
|
16
35
|
// does for video/animated-image input.
|
|
17
|
-
async function stripFromFrames(framePaths,
|
|
36
|
+
async function stripFromFrames(framePaths, out) {
|
|
18
37
|
const tiles = await Promise.all(framePaths.map((f) => sharp(f).png().toBuffer()));
|
|
19
38
|
const first = await sharp(framePaths[0]).metadata();
|
|
20
|
-
await compositeStrip(tiles, first.width, first.height,
|
|
39
|
+
await compositeStrip(tiles, first.width, first.height, out);
|
|
21
40
|
}
|
|
22
41
|
|
|
23
42
|
// URL/file:// input: measure the live page with system Chrome instead of sampling
|
|
24
43
|
// frames from a recording. Scratch-dir-first, same as the frame path: a total
|
|
25
44
|
// failure (nothing captured at all) must leave nothing under entries/.
|
|
26
|
-
async function addFromUrl({ p, input, slug, title, source, dir, record }) {
|
|
45
|
+
async function addFromUrl({ p, input, slug, title, source, dir, record, force, attach }) {
|
|
27
46
|
const fs = await import('node:fs');
|
|
28
47
|
const os = await import('node:os');
|
|
29
48
|
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'wpdl-add-'));
|
|
@@ -41,32 +60,61 @@ async function addFromUrl({ p, input, slug, title, source, dir, record }) {
|
|
|
41
60
|
|
|
42
61
|
const today = new Date().toISOString().slice(0, 10);
|
|
43
62
|
const tool = `wp-design-library@${p.version}`;
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
// force always wipes and recreates as capture 1, exactly as it does today, regardless of
|
|
64
|
+
// attach; only a plain attach (no force) merges onto the existing capture list.
|
|
65
|
+
const index = (!force && attach) ? nextCaptureIndex(dir) : 1;
|
|
66
|
+
// Composite into scratch first, same as the frame path: every byte of the new capture has to
|
|
67
|
+
// exist before anything under entries/ is touched. sharp can fail on a recorded frame well
|
|
68
|
+
// after measure() returned usable data, and the wipe below is unrecoverable.
|
|
69
|
+
const scratchStrip = path.join(scratch, 'strip.webp');
|
|
46
70
|
let captured, palette, type;
|
|
47
71
|
if (result.ok) {
|
|
48
72
|
captured = { method: record ? 'both' : 'measured', at: today, tool };
|
|
49
73
|
const t = result.measure.tokens;
|
|
50
74
|
palette = { canvas: t.canvas || '', ink: t.ink || '', accent: t.accent || '' };
|
|
51
75
|
type = { display: t.fontDisplay || '', body: t.fontBody || '' };
|
|
52
|
-
if (record && result.frames?.length) await stripFromFrames(result.frames,
|
|
53
|
-
else
|
|
54
|
-
for (const f of ['measure.json', 'screen-1440.png', 'screen-390.png']) {
|
|
55
|
-
fs.copyFileSync(path.join(scratch, f), path.join(dir, f));
|
|
56
|
-
}
|
|
76
|
+
if (record && result.frames?.length) await stripFromFrames(result.frames, scratchStrip);
|
|
77
|
+
else await writeStrip(sharp(result.screenshots[1440]), scratchStrip);
|
|
57
78
|
} else {
|
|
58
79
|
// record succeeded but the token/section measurement itself failed: keep the
|
|
59
80
|
// frames (spec §11), label the method accordingly, and record why.
|
|
60
81
|
captured = { method: 'frames', at: today, tool, error: result.error };
|
|
61
|
-
await stripFromFrames(result.frames,
|
|
82
|
+
await stripFromFrames(result.frames, scratchStrip);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Gated on index, not force directly: index===1 is the same decision the attach-vs-recreate
|
|
86
|
+
// branch below acts on, so the two can never disagree — a regression that breaks the line
|
|
87
|
+
// above (force stops forcing index to 1) also stops this wipe from firing, instead of wiping
|
|
88
|
+
// a directory out from under a capture the buggy code is about to attach to. It fires only
|
|
89
|
+
// here, below the compositing, so nothing between measure() and a written strip can destroy
|
|
90
|
+
// an existing entry and leave nothing in its place; what follows is copies out of scratch.
|
|
91
|
+
if (index === 1) fs.rmSync(dir, { recursive: true, force: true });
|
|
92
|
+
const outDir = captureDir(dir, index);
|
|
93
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
94
|
+
const strip = path.join(outDir, 'strip.webp');
|
|
95
|
+
fs.copyFileSync(scratchStrip, strip);
|
|
96
|
+
if (result.ok) {
|
|
97
|
+
for (const f of ['measure.json', 'screen-1440.png', 'screen-390.png']) {
|
|
98
|
+
fs.copyFileSync(path.join(scratch, f), path.join(outDir, f));
|
|
99
|
+
}
|
|
62
100
|
}
|
|
63
101
|
fs.rmSync(scratch, { recursive: true, force: true });
|
|
64
102
|
|
|
65
|
-
const
|
|
103
|
+
const artifactNames = ['strip.webp'];
|
|
104
|
+
if (result.ok) artifactNames.push('measure.json', 'screen-1440.png', 'screen-390.png');
|
|
105
|
+
const artifacts = artifactNames.map((f) => path.relative(dir, path.join(outDir, f)));
|
|
106
|
+
const capture = { method: captured.method, input, artifacts };
|
|
107
|
+
|
|
108
|
+
if (index > 1) {
|
|
109
|
+
const r = await attachCapture({ entriesDir: p.entries, slug, capture, palette, type, sourceUrl: input, tool });
|
|
110
|
+
return { dir: r.dir, strip, captures: r.captures.length, measured: result.ok };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const d = writeDraft({ entriesDir: p.entries, slug, title, source: { ...source, url: source.url ?? input }, media: {}, captured, palette, type, captures: [capture] });
|
|
66
114
|
return { dir: d.dir, strip, blanks: d.blanks, measured: result.ok };
|
|
67
115
|
}
|
|
68
116
|
|
|
69
|
-
export async function addEntry({ input, slug, title, source, force = false, frames, record = false }) {
|
|
117
|
+
export async function addEntry({ input, slug, title, source, force = false, frames, record = false, attach = false }) {
|
|
70
118
|
const p = paths();
|
|
71
119
|
if (!input || !slug || !title || !source?.kind || !source?.license) throw new Error('add: input, slug, title, --source and --license are required');
|
|
72
120
|
// Refused here (draft time), not first at save: resolveTerm is the single vocab gate
|
|
@@ -75,17 +123,18 @@ export async function addEntry({ input, slug, title, source, force = false, fram
|
|
|
75
123
|
return { refused: `source.kind: unknown source term "${source.kind}"` };
|
|
76
124
|
}
|
|
77
125
|
const dir = path.join(p.entries, slug);
|
|
78
|
-
const { mkdirSync, mkdtempSync,
|
|
126
|
+
const { mkdirSync, mkdtempSync, renameSync, copyFileSync, unlinkSync, rmSync } = await import('node:fs');
|
|
79
127
|
const os = await import('node:os');
|
|
80
|
-
|
|
81
|
-
|
|
128
|
+
const exists = existsSync(path.join(dir, 'entry.md'));
|
|
129
|
+
if (exists && !force && !attach) {
|
|
130
|
+
return { refused: `entry ${slug} exists; pass attach to add a capture, or force to overwrite` };
|
|
82
131
|
}
|
|
83
132
|
if (URL_RE.test(input)) {
|
|
84
|
-
return addFromUrl({ p, input, slug, title, source, dir, record });
|
|
133
|
+
return addFromUrl({ p, input, slug, title, source, dir, record, force, attach });
|
|
85
134
|
}
|
|
86
135
|
// Sample into a scratch dir first: a refusal must leave nothing under entries/.
|
|
87
136
|
const scratch = mkdtempSync(path.join(os.tmpdir(), 'wpdl-add-'));
|
|
88
|
-
const scratchStrip = path.join(scratch, 'strip.
|
|
137
|
+
const scratchStrip = path.join(scratch, 'strip.webp');
|
|
89
138
|
let media;
|
|
90
139
|
try {
|
|
91
140
|
media = await sampleFrames(input, scratchStrip, { frames });
|
|
@@ -94,8 +143,20 @@ export async function addEntry({ input, slug, title, source, force = false, fram
|
|
|
94
143
|
if (err.message?.startsWith('ffmpeg')) return { refused: err.message };
|
|
95
144
|
throw err;
|
|
96
145
|
}
|
|
97
|
-
|
|
98
|
-
|
|
146
|
+
// force always wipes and recreates as capture 1, exactly as it does today, regardless of
|
|
147
|
+
// attach; only a plain attach (no force) merges onto the existing capture list.
|
|
148
|
+
const index = (!force && attach) ? nextCaptureIndex(dir) : 1;
|
|
149
|
+
// Gated on index, not force directly: index===1 is the same decision the attach-vs-recreate
|
|
150
|
+
// branch below acts on, so the two can never disagree — a regression that breaks the line
|
|
151
|
+
// above (force stops forcing index to 1) also stops this wipe from firing, instead of wiping
|
|
152
|
+
// a directory out from under a capture the buggy code is about to attach to. The wipe happens
|
|
153
|
+
// only here — after sampleFrames has already succeeded into scratch (a sampling failure above
|
|
154
|
+
// is already handled by the catch block) — so it can never destroy an existing entry and
|
|
155
|
+
// leave nothing in its place.
|
|
156
|
+
if (index === 1) rmSync(dir, { recursive: true, force: true });
|
|
157
|
+
const outDir = captureDir(dir, index);
|
|
158
|
+
mkdirSync(outDir, { recursive: true });
|
|
159
|
+
const strip = path.join(outDir, 'strip.webp');
|
|
99
160
|
try {
|
|
100
161
|
renameSync(scratchStrip, strip);
|
|
101
162
|
} catch {
|
|
@@ -103,8 +164,22 @@ export async function addEntry({ input, slug, title, source, force = false, fram
|
|
|
103
164
|
unlinkSync(scratchStrip);
|
|
104
165
|
}
|
|
105
166
|
rmSync(scratch, { recursive: true, force: true });
|
|
106
|
-
|
|
107
|
-
const
|
|
167
|
+
|
|
168
|
+
const rel = path.relative(dir, strip);
|
|
169
|
+
const capture = {
|
|
170
|
+
method: 'frames', input, artifacts: [rel],
|
|
171
|
+
frames: media.frames, duration_ms: media.durationMs,
|
|
172
|
+
};
|
|
173
|
+
const mediaFields = { frames: media.frames, duration_ms: media.durationMs };
|
|
174
|
+
const tool = `wp-design-library@${p.version}`;
|
|
175
|
+
|
|
176
|
+
if (index > 1) {
|
|
177
|
+
const r = await attachCapture({ entriesDir: p.entries, slug, capture, media: mediaFields, tool });
|
|
178
|
+
return { dir: r.dir, strip, captures: r.captures.length, frames: media.frames };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const captured = { method: 'frames', at: new Date().toISOString().slice(0, 10), tool };
|
|
182
|
+
const d = writeDraft({ entriesDir: p.entries, slug, title, source, media: mediaFields, captured, captures: [capture] });
|
|
108
183
|
return { dir: d.dir, strip, blanks: d.blanks, frames: media.frames };
|
|
109
184
|
}
|
|
110
185
|
|
|
@@ -157,6 +232,7 @@ export async function run(args) {
|
|
|
157
232
|
force: args.includes('--force'),
|
|
158
233
|
frames,
|
|
159
234
|
record: args.includes('--record'),
|
|
235
|
+
attach: args.includes('--attach'),
|
|
160
236
|
});
|
|
161
237
|
process.stdout.write(JSON.stringify(out) + '\n');
|
|
162
238
|
return out.refused ? 1 : 0;
|