@yojahny/wp-design-library 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -5
- package/bin/library.mjs +1 -0
- package/entries/aker/entry.md +107 -0
- package/entries/amrit-palace/entry.md +105 -0
- package/entries/apple-airpods-5/entry.md +110 -0
- package/entries/apple-airpods-pro/entry.md +106 -0
- package/entries/apple-watch-se-3/entry.md +107 -0
- package/entries/apple-watch-series-12/entry.md +108 -0
- package/entries/arsenijs-fabrica/entry.md +100 -0
- package/entries/augen-pro/entry.md +100 -0
- package/entries/auros/entry.md +110 -0
- package/entries/awe-network/entry.md +101 -0
- package/entries/ayocin/entry.md +101 -0
- package/entries/bmw/entry.md +101 -0
- package/entries/busuu/entry.md +100 -0
- package/entries/caldera/entry.md +106 -0
- package/entries/calendly/entry.md +106 -0
- package/entries/custo/entry.md +100 -0
- package/entries/dayos-ai/entry.md +109 -0
- package/entries/dimension/entry.md +106 -0
- package/entries/dovetail/entry.md +108 -0
- package/entries/ease-health/entry.md +105 -0
- package/entries/factory-ai/entry.md +106 -0
- package/entries/flighty/entry.md +105 -0
- package/entries/frame-io/entry.md +108 -0
- package/entries/hungry-tiger/entry.md +106 -0
- package/entries/hyer-aviation/entry.md +108 -0
- package/entries/impilo/entry.md +108 -0
- package/entries/monad/entry.md +108 -0
- package/entries/n8n/entry.md +108 -0
- package/entries/palais/entry.md +100 -0
- package/entries/partiful/entry.md +104 -0
- package/entries/phantom/entry.md +105 -0
- package/entries/podia/entry.md +102 -0
- package/entries/posthog/entry.md +101 -0
- package/entries/redbrick-coffee/entry.md +98 -0
- package/entries/samara/entry.md +107 -0
- package/entries/sauce-labs/entry.md +110 -0
- package/entries/seline-analytics/entry.md +106 -0
- package/entries/sequel/entry.md +104 -0
- package/entries/slash/entry.md +110 -0
- package/entries/supabase/entry.md +107 -0
- package/entries/t1-energy/entry.md +101 -0
- package/entries/vana/entry.md +100 -0
- package/entries/veed/entry.md +98 -0
- package/entries/webflow/entry.md +108 -0
- package/entries/zkpass/entry.md +103 -0
- package/package.json +2 -1
- package/src/cli/upload.mjs +57 -0
- package/src/ingest/save.mjs +28 -6
- package/src/ingest/upload.mjs +110 -0
- package/src/mcp/http.mjs +21 -0
- package/src/mcp/prompts.mjs +71 -10
- package/src/mcp/tools.mjs +104 -8
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: vana
|
|
3
|
+
title: 'Vana: personal data infrastructure'
|
|
4
|
+
tier: inspiration
|
|
5
|
+
source:
|
|
6
|
+
kind: public-site
|
|
7
|
+
url: https://www.vana.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-24'
|
|
12
|
+
tool: wp-design-library@1.2.0
|
|
13
|
+
captures:
|
|
14
|
+
- method: frames
|
|
15
|
+
input: https://images.refero.design/styles/refero.design/video/a3563017-dcef-4a36-bd07-9f638e3e4fdf.mp4
|
|
16
|
+
artifacts:
|
|
17
|
+
- key: entries/vana/1/strip.webp
|
|
18
|
+
bytes: 129848
|
|
19
|
+
sha256: 3ed79e491c42f46490b0aee20cec99e99b83319c848a2c307b794610e755d7f2
|
|
20
|
+
uploaded: true
|
|
21
|
+
frames: 18
|
|
22
|
+
duration_ms: 8783
|
|
23
|
+
- method: both
|
|
24
|
+
input: https://www.vana.com/
|
|
25
|
+
artifacts:
|
|
26
|
+
- key: entries/vana/2/strip.webp
|
|
27
|
+
bytes: 261970
|
|
28
|
+
sha256: 0a1eb61643cfe6475cb92a903344deb022bb44e30d53b408946a1d14969097d3
|
|
29
|
+
uploaded: true
|
|
30
|
+
- key: entries/vana/2/measure.json
|
|
31
|
+
bytes: 1554
|
|
32
|
+
sha256: ad5962af583cfc2489fee5073cd43acb5a3e6fc3712857381715b0367bc75af2
|
|
33
|
+
uploaded: true
|
|
34
|
+
- key: entries/vana/2/screen-1440.png
|
|
35
|
+
bytes: 509200
|
|
36
|
+
sha256: 35e85bd7c81da6402dc39c9f07100b9174c50ea1c54c38c2b8acbd5f3cf19b12
|
|
37
|
+
uploaded: true
|
|
38
|
+
- key: entries/vana/2/screen-390.png
|
|
39
|
+
bytes: 353891
|
|
40
|
+
sha256: cfda7194da8a0482b9e58a3d40dc06edf62fbe2ed37f9c5bea362c3a8cb004e2
|
|
41
|
+
uploaded: true
|
|
42
|
+
media:
|
|
43
|
+
frames: 18
|
|
44
|
+
duration_ms: 8783
|
|
45
|
+
roles:
|
|
46
|
+
- hero
|
|
47
|
+
- explainer
|
|
48
|
+
- capability
|
|
49
|
+
- feature
|
|
50
|
+
- proof
|
|
51
|
+
- closing
|
|
52
|
+
- footer
|
|
53
|
+
feel:
|
|
54
|
+
- dark
|
|
55
|
+
- cold
|
|
56
|
+
- saas
|
|
57
|
+
- depth
|
|
58
|
+
- premium
|
|
59
|
+
palette:
|
|
60
|
+
canvas: '#090a13'
|
|
61
|
+
ink: '#f1f1fc'
|
|
62
|
+
accent: '#6959ff'
|
|
63
|
+
type:
|
|
64
|
+
display: large neutral sans, tight leading
|
|
65
|
+
body: compact neutral sans, technical data measure
|
|
66
|
+
motion:
|
|
67
|
+
devices:
|
|
68
|
+
- reveal
|
|
69
|
+
- pan
|
|
70
|
+
- stagger
|
|
71
|
+
- spotlight
|
|
72
|
+
notes: >-
|
|
73
|
+
Eyeballed from 18 sampled frames, not measured. The Refero recording opens on a dark globe and purple data points,
|
|
74
|
+
then pans through ownership cards, a developer stack diagram, and a bright protocol chapter. The current site at
|
|
75
|
+
vana.org has a lavender hero and redesigned content grid. Exact map dynamics, card interactions, pointer response,
|
|
76
|
+
and reduced-motion behavior are not measurable from sampled frames.
|
|
77
|
+
ported_from: null
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## What it does
|
|
81
|
+
|
|
82
|
+
An archived data-ownership infrastructure story that argues people should control and reuse the information they create. Dark global imagery and modular cards visualize an otherwise invisible data layer.
|
|
83
|
+
|
|
84
|
+
## Section roster
|
|
85
|
+
|
|
86
|
+
1. Dark globe and AI ownership claim.
|
|
87
|
+
2. Personal data use cases.
|
|
88
|
+
3. Developer and participation paths.
|
|
89
|
+
4. Protocol stack diagram.
|
|
90
|
+
5. Open-data chapter and closing.
|
|
91
|
+
|
|
92
|
+
## Why it works
|
|
93
|
+
|
|
94
|
+
The globe and layered cards turn a policy-like subject into a spatial system, while a bright final chapter gives the technical story a clear turn. The cost is that abstract imagery and small labels need plain-language support and strong contrast.
|
|
95
|
+
|
|
96
|
+
## Sources
|
|
97
|
+
|
|
98
|
+
- [Refero style page](https://styles.refero.design/style/2a3cdb2e-effe-406b-932e-37e4dc88ab1d) and its [recorded MP4](https://images.refero.design/styles/refero.design/video/a3563017-dcef-4a36-bd07-9f638e3e4fdf.mp4) supply the sampled motion and section sequence.
|
|
99
|
+
- The live capture at https://vana.org/ supplies current copy, responsive screenshots, and measured layout.
|
|
100
|
+
- The submitted vana.com redirects to vana.org. The live site is a substantial lavender redesign, so trust the recording for this archived dark visual system and vana.org for current positioning.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: veed
|
|
3
|
+
title: 'VEED: AI video creation platform'
|
|
4
|
+
tier: inspiration
|
|
5
|
+
source:
|
|
6
|
+
kind: public-site
|
|
7
|
+
url: https://www.veed.io/
|
|
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-24'
|
|
12
|
+
tool: wp-design-library@1.2.0
|
|
13
|
+
captures:
|
|
14
|
+
- method: frames
|
|
15
|
+
input: https://images.refero.design/styles/refero.design/video/78c29fbf-be4e-4fc4-ad25-b377b1c6c751.mp4
|
|
16
|
+
artifacts:
|
|
17
|
+
- key: entries/veed/1/strip.webp
|
|
18
|
+
bytes: 239780
|
|
19
|
+
sha256: d18b9392525a700aadb1a55172390c8cfd55706e7035a1a093915653f097cede
|
|
20
|
+
uploaded: true
|
|
21
|
+
frames: 23
|
|
22
|
+
duration_ms: 11267
|
|
23
|
+
- method: both
|
|
24
|
+
input: https://www.veed.io/
|
|
25
|
+
artifacts:
|
|
26
|
+
- key: entries/veed/2/strip.webp
|
|
27
|
+
bytes: 389818
|
|
28
|
+
sha256: a203e2016d1ed93a2ca3f51297f0f2151d8f4fb18b6724269c5fbeb4001ed120
|
|
29
|
+
uploaded: true
|
|
30
|
+
- key: entries/veed/2/measure.json
|
|
31
|
+
bytes: 5957
|
|
32
|
+
sha256: 68777c4b3c38d474c2a5cebccabff6b0c76ab507ff3d446a1af02d781c4a2ded
|
|
33
|
+
uploaded: true
|
|
34
|
+
- key: entries/veed/2/screen-1440.png
|
|
35
|
+
bytes: 372084
|
|
36
|
+
sha256: d3d62a04e1adb830c125804e298e3df6ffc0f0ae56bfe6afe7e1c3ee1cb6514e
|
|
37
|
+
uploaded: true
|
|
38
|
+
- key: entries/veed/2/screen-390.png
|
|
39
|
+
bytes: 90686
|
|
40
|
+
sha256: 166d40b1f9597e84a82d89cc1994e168052df2eda906a570c3aafca1fd7ec255
|
|
41
|
+
uploaded: true
|
|
42
|
+
media:
|
|
43
|
+
frames: 23
|
|
44
|
+
duration_ms: 11267
|
|
45
|
+
roles:
|
|
46
|
+
- hero
|
|
47
|
+
- proof
|
|
48
|
+
- feature
|
|
49
|
+
- capability
|
|
50
|
+
- closing
|
|
51
|
+
- footer
|
|
52
|
+
feel:
|
|
53
|
+
- light
|
|
54
|
+
- dark
|
|
55
|
+
- saas
|
|
56
|
+
- playful
|
|
57
|
+
- depth
|
|
58
|
+
palette:
|
|
59
|
+
canvas: '#ffffff'
|
|
60
|
+
ink: '#111111'
|
|
61
|
+
accent: '#9cff39'
|
|
62
|
+
type:
|
|
63
|
+
display: bold neutral sans, compact leading
|
|
64
|
+
body: neutral sans, concise product measure
|
|
65
|
+
motion:
|
|
66
|
+
devices:
|
|
67
|
+
- reveal
|
|
68
|
+
- pan
|
|
69
|
+
- stack
|
|
70
|
+
- stagger
|
|
71
|
+
notes: >-
|
|
72
|
+
Eyeballed from 23 sampled frames, not measured. A sparse AI-video hero gives way to a moving row of creator
|
|
73
|
+
portraits and generated-video previews. Dark product panels stack examples, then editor controls and use-case cards
|
|
74
|
+
reveal in sequence before testimonial and footer content.
|
|
75
|
+
ported_from: null
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## What it does
|
|
79
|
+
|
|
80
|
+
A video creation platform landing page that demonstrates social-ready output before explaining the AI generation and editing workflow. Creator faces and sample videos make the abstract promise tangible.
|
|
81
|
+
|
|
82
|
+
## Section roster
|
|
83
|
+
|
|
84
|
+
1. AI-video hero and creation action.
|
|
85
|
+
2. Creator portraits and team proof.
|
|
86
|
+
3. Generated video examples.
|
|
87
|
+
4. Editing and brand controls.
|
|
88
|
+
5. Use-case cards and closing action.
|
|
89
|
+
|
|
90
|
+
## Why it works
|
|
91
|
+
|
|
92
|
+
Showing sample output early makes the offer credible, while the restrained white canvas and green accent keep product UI easy to read. The cost is that video previews and dense editing controls need efficient loading and accessible alternatives.
|
|
93
|
+
|
|
94
|
+
## Sources
|
|
95
|
+
|
|
96
|
+
- [Refero style page](https://styles.refero.design/style/fff821ec-a3bf-41a5-aea2-626185bcd227) and its [recorded MP4](https://images.refero.design/styles/refero.design/video/78c29fbf-be4e-4fc4-ad25-b377b1c6c751.mp4) supply the sampled motion and section sequence.
|
|
97
|
+
- The live capture at https://www.veed.io/ supplies current copy, responsive screenshots, and measured layout.
|
|
98
|
+
- The sources share the same AI-video direction. Trust the recording for example-card movement and the live site for current tools and pricing.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: webflow
|
|
3
|
+
title: 'Webflow: layered website-experience platform'
|
|
4
|
+
tier: inspiration
|
|
5
|
+
source:
|
|
6
|
+
kind: public-site
|
|
7
|
+
url: https://webflow.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-22'
|
|
12
|
+
tool: wp-design-library@1.2.0
|
|
13
|
+
captures:
|
|
14
|
+
- method: frames
|
|
15
|
+
input: 'https://images.refero.design/styles/refero.design/video/4271252f-7d14-4594-8489-b8c23a843bd9.mp4'
|
|
16
|
+
artifacts:
|
|
17
|
+
- key: entries/webflow/1/strip.webp
|
|
18
|
+
bytes: 367942
|
|
19
|
+
sha256: c2c5e860ff83f814d4db8c7a9e83a9a6c0a2b2aca69171dd984cea4540846a97
|
|
20
|
+
uploaded: true
|
|
21
|
+
frames: 36
|
|
22
|
+
duration_ms: 29320
|
|
23
|
+
- method: both
|
|
24
|
+
input: https://webflow.com/
|
|
25
|
+
artifacts:
|
|
26
|
+
- key: entries/webflow/2/strip.webp
|
|
27
|
+
bytes: 316688
|
|
28
|
+
sha256: 4ae2299a39d0f8f06ce898d9dafc7ef75bcf172eeba99d582f4018ed9037902b
|
|
29
|
+
uploaded: true
|
|
30
|
+
- key: entries/webflow/2/measure.json
|
|
31
|
+
bytes: 5564
|
|
32
|
+
sha256: 01dde97a6e81d37aed1aa3684707be2b6cc0b0cea3193243e0d194a94dbe4077
|
|
33
|
+
uploaded: true
|
|
34
|
+
- key: entries/webflow/2/screen-1440.png
|
|
35
|
+
bytes: 284671
|
|
36
|
+
sha256: 326c76e7779a2ce13eb2687ad017d7709cc3f34e21b79108dd075c26cc275404
|
|
37
|
+
uploaded: true
|
|
38
|
+
- key: entries/webflow/2/screen-390.png
|
|
39
|
+
bytes: 54072
|
|
40
|
+
sha256: 03b140fef199644dafdd8aa1576769a5fcce2c9c57bf2761a7919c821897f16f
|
|
41
|
+
uploaded: true
|
|
42
|
+
media:
|
|
43
|
+
frames: 36
|
|
44
|
+
duration_ms: 29320
|
|
45
|
+
roles:
|
|
46
|
+
- hero
|
|
47
|
+
- feature
|
|
48
|
+
- proof
|
|
49
|
+
- capability
|
|
50
|
+
- testimonial
|
|
51
|
+
- explainer
|
|
52
|
+
- closing
|
|
53
|
+
- footer
|
|
54
|
+
feel:
|
|
55
|
+
- light
|
|
56
|
+
- saas
|
|
57
|
+
- premium
|
|
58
|
+
- minimal
|
|
59
|
+
- depth
|
|
60
|
+
palette:
|
|
61
|
+
canvas: '#ffffff'
|
|
62
|
+
ink: '#080808'
|
|
63
|
+
accent: '#146ef5'
|
|
64
|
+
type:
|
|
65
|
+
display: bold modern sans, heavy weight, tight tracking
|
|
66
|
+
body: neutral sans, regular weight, open product-marketing measure
|
|
67
|
+
motion:
|
|
68
|
+
devices:
|
|
69
|
+
- reveal
|
|
70
|
+
- pin
|
|
71
|
+
- pan
|
|
72
|
+
- parallax
|
|
73
|
+
- stagger
|
|
74
|
+
- stack
|
|
75
|
+
- tabs
|
|
76
|
+
notes: >-
|
|
77
|
+
Eyeballed from 36 sampled frames, not measured. The recorded hero layers a blue gradient bed behind product-choice
|
|
78
|
+
cards, then pins a browser canvas while Webflow UI panels pan and stack around a live site preview. Capability
|
|
79
|
+
sections reveal as staggered image-and-metric columns, customer proof advances through portrait cards, and later AI
|
|
80
|
+
and collaboration canvases slide into place before updates, community work, and the footer. The current live page
|
|
81
|
+
has changed its hero and carries a persistent cookie panel; exact tab, carousel, and pointer behavior cannot be
|
|
82
|
+
established from sampled frames.
|
|
83
|
+
ported_from: null
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## What it does
|
|
87
|
+
|
|
88
|
+
Webflow presents its website-experience platform through a choice-led hero, visual building canvas, publishing and optimization capabilities, customer outcomes, AI-assisted workflows, recent updates, and community-made work.
|
|
89
|
+
|
|
90
|
+
## Section roster
|
|
91
|
+
|
|
92
|
+
1. Product-choice hero and primary conversion actions.
|
|
93
|
+
2. Layered builder canvas showing design and code workflows.
|
|
94
|
+
3. Build, publish, and optimize feature columns.
|
|
95
|
+
4. Brand-scale proof and customer outcome cards.
|
|
96
|
+
5. Agentic web-marketing and collaboration explanation.
|
|
97
|
+
6. AI knowledge and business workflow canvas.
|
|
98
|
+
7. Updates, conference material, Made in Webflow gallery, and footer.
|
|
99
|
+
|
|
100
|
+
## Why it works
|
|
101
|
+
|
|
102
|
+
Large direct typography and real product canvases explain a technically broad platform without hiding the interface, while blue accents keep conversion and navigation obvious across a long page. The cost to weigh is that stacked UI screenshots, customer cards, and changing campaign content create a heavy page that can age quickly and compete for attention.
|
|
103
|
+
|
|
104
|
+
## Sources
|
|
105
|
+
|
|
106
|
+
- The [Refero style page](https://styles.refero.design/style/31471407-598a-45fd-a505-d921980d8855) and its MP4 supply the earlier growth-engine hero, builder-canvas choreography, feature stacking, customer-card sequence, and complete scroll narrative.
|
|
107
|
+
- The live capture at https://webflow.com/ supplies the current Build for what’s next hero, present campaign content, navigation, responsive screenshots, and measured layout.
|
|
108
|
+
- The hero and some downstream campaign sections differ materially; the live page is trusted for current structure and copy, while the recording is retained as the motion reference for the captured design version.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: zkpass
|
|
3
|
+
title: 'zkPass: cryptographic trust-network landing'
|
|
4
|
+
tier: inspiration
|
|
5
|
+
source:
|
|
6
|
+
kind: public-site
|
|
7
|
+
url: https://zkpass.org/
|
|
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-24'
|
|
12
|
+
tool: wp-design-library@1.2.0
|
|
13
|
+
captures:
|
|
14
|
+
- method: frames
|
|
15
|
+
input: https://images.refero.design/styles/refero.design/video/225d4ed7-26d4-4495-b8d3-c6b32d9af232.mp4
|
|
16
|
+
artifacts:
|
|
17
|
+
- key: entries/zkpass/1/strip.webp
|
|
18
|
+
bytes: 246594
|
|
19
|
+
sha256: 35b485103534d1ac10bc1116b644e19eb41cdbb066d381f366de71b9295b3d63
|
|
20
|
+
uploaded: true
|
|
21
|
+
frames: 36
|
|
22
|
+
duration_ms: 20717
|
|
23
|
+
- method: both
|
|
24
|
+
input: https://zkpass.org/
|
|
25
|
+
artifacts:
|
|
26
|
+
- key: entries/zkpass/2/strip.webp
|
|
27
|
+
bytes: 264812
|
|
28
|
+
sha256: 473a2275e5161b08fca4d61ac72d3f2507589197e9e32a09e66a1ea48619e296
|
|
29
|
+
uploaded: true
|
|
30
|
+
- key: entries/zkpass/2/measure.json
|
|
31
|
+
bytes: 4020
|
|
32
|
+
sha256: 2bbe4c4567883a5b1e1d3ef48dc51cf2a981837fb654dd5c87ea2adbfe61cc43
|
|
33
|
+
uploaded: true
|
|
34
|
+
- key: entries/zkpass/2/screen-1440.png
|
|
35
|
+
bytes: 450714
|
|
36
|
+
sha256: c5d496cde94be116f563652bb4fb9c6ada3506383d6102f0fd758191fdfd246b
|
|
37
|
+
uploaded: true
|
|
38
|
+
- key: entries/zkpass/2/screen-390.png
|
|
39
|
+
bytes: 68539
|
|
40
|
+
sha256: 6adc33d258808e9085e2db57ac09f88eb774287440db9048ea8226b987be6a1a
|
|
41
|
+
uploaded: true
|
|
42
|
+
media:
|
|
43
|
+
frames: 36
|
|
44
|
+
duration_ms: 20717
|
|
45
|
+
roles:
|
|
46
|
+
- hero
|
|
47
|
+
- explainer
|
|
48
|
+
- capability
|
|
49
|
+
- proof
|
|
50
|
+
- feature
|
|
51
|
+
- closing
|
|
52
|
+
- footer
|
|
53
|
+
feel:
|
|
54
|
+
- dark
|
|
55
|
+
- brutalist
|
|
56
|
+
- cold
|
|
57
|
+
- premium
|
|
58
|
+
- depth
|
|
59
|
+
palette:
|
|
60
|
+
canvas: '#050605'
|
|
61
|
+
ink: '#eeeee8'
|
|
62
|
+
accent: '#c4ff35'
|
|
63
|
+
type:
|
|
64
|
+
display: high-contrast editorial serif and technical sans
|
|
65
|
+
body: compact technical sans, dense protocol measure
|
|
66
|
+
motion:
|
|
67
|
+
devices:
|
|
68
|
+
- reveal
|
|
69
|
+
- wipe
|
|
70
|
+
- pan
|
|
71
|
+
- stagger
|
|
72
|
+
- spotlight
|
|
73
|
+
notes: >-
|
|
74
|
+
Eyeballed from 36 sampled frames, not measured. A black particle field and neon green symbol open the page.
|
|
75
|
+
Oversized Proof and Credit statements wipe between dark product panels; verification flows, code-like diagrams, and
|
|
76
|
+
principle rows reveal in sequence before the green closing mark. Exact particle simulation, verification demo
|
|
77
|
+
behavior, pointer response, and reduced-motion behavior are not measurable from sampled frames.
|
|
78
|
+
ported_from: null
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## What it does
|
|
82
|
+
|
|
83
|
+
A privacy and verification protocol landing page that organizes a technical product around proof, credit, and reusable trust. Near-black surfaces and acid-green signals make the system feel cryptographic while large headings give dense material a readable hierarchy.
|
|
84
|
+
|
|
85
|
+
## Section roster
|
|
86
|
+
|
|
87
|
+
1. Particle-field hero and privacy claim.
|
|
88
|
+
2. Proof and credit product framing.
|
|
89
|
+
3. Three verification methods.
|
|
90
|
+
4. Human and agent audience paths.
|
|
91
|
+
5. Technical primitives and protocol principles.
|
|
92
|
+
6. Product diagrams and dispatches.
|
|
93
|
+
7. Closing proof statement and footer.
|
|
94
|
+
|
|
95
|
+
## Why it works
|
|
96
|
+
|
|
97
|
+
The green signal color guides attention through otherwise dense technical panels, while oversized serif assertions make each concept memorable. The cost is that small diagram labels and low-light UI require strong contrast and responsive simplification.
|
|
98
|
+
|
|
99
|
+
## Sources
|
|
100
|
+
|
|
101
|
+
- [Refero style page](https://styles.refero.design/style/3a398f82-579e-4ec4-a442-9962bf007edf) and its [recorded MP4](https://images.refero.design/styles/refero.design/video/225d4ed7-26d4-4495-b8d3-c6b32d9af232.mp4) supply the opening particle field, product transitions, verification panels, protocol chapters, and closing.
|
|
102
|
+
- The live capture at https://zkpass.org/ supplies current terminology, responsive screenshots, and measured layout.
|
|
103
|
+
- The sources agree closely. Trust the recording for transition continuity and the live page for current protocol claims.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yojahny/wp-design-library",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Design reference corpus and MCP server for claude-wp-builder",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"src/cli/refresh.mjs",
|
|
66
66
|
"src/cli/save.mjs",
|
|
67
67
|
"src/cli/serve.mjs",
|
|
68
|
+
"src/cli/upload.mjs",
|
|
68
69
|
"src/cli/ui.mjs",
|
|
69
70
|
"src/entry.mjs",
|
|
70
71
|
"src/index/",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
|
|
5
|
+
function opt(args, name) {
|
|
6
|
+
const i = args.indexOf(name);
|
|
7
|
+
return i === -1 ? undefined : args[i + 1];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function uploadFile({ file, server, token, source, license, url, title, fetcher = fetch }) {
|
|
11
|
+
if (!file) throw new Error('usage: library upload <file> --server <url> [--source kind --license text --url source-url --title title]');
|
|
12
|
+
if (!server) throw new Error('--server or LIBRARY_SERVER is required');
|
|
13
|
+
if (!token) throw new Error('LIBRARY_TOKEN is required');
|
|
14
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) throw new Error(`file does not exist: ${file}`);
|
|
15
|
+
if ((source && !license) || (!source && license)) throw new Error('--source and --license must be provided together');
|
|
16
|
+
|
|
17
|
+
const name = path.basename(file);
|
|
18
|
+
const endpoint = new URL(`/inbox/${encodeURIComponent(name)}`, server);
|
|
19
|
+
if (!['http:', 'https:'].includes(endpoint.protocol)) throw new Error('--server must be an HTTP(S) URL');
|
|
20
|
+
const headers = {
|
|
21
|
+
authorization: `Bearer ${token}`,
|
|
22
|
+
'content-type': 'application/octet-stream',
|
|
23
|
+
'content-length': String(fs.statSync(file).size),
|
|
24
|
+
};
|
|
25
|
+
if (source) headers['x-library-source'] = source;
|
|
26
|
+
if (license) headers['x-library-license'] = license;
|
|
27
|
+
if (url) headers['x-library-url'] = url;
|
|
28
|
+
if (title) headers['x-library-title'] = title;
|
|
29
|
+
|
|
30
|
+
const response = await fetcher(endpoint, {
|
|
31
|
+
method: 'PUT', headers,
|
|
32
|
+
body: Readable.toWeb(fs.createReadStream(file)),
|
|
33
|
+
duplex: 'half',
|
|
34
|
+
});
|
|
35
|
+
const text = await response.text();
|
|
36
|
+
if (!response.ok) throw new Error(`upload refused (${response.status}): ${text}`);
|
|
37
|
+
return JSON.parse(text);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function run(args) {
|
|
41
|
+
try {
|
|
42
|
+
const result = await uploadFile({
|
|
43
|
+
file: args[0],
|
|
44
|
+
server: opt(args, '--server') ?? process.env.LIBRARY_SERVER,
|
|
45
|
+
token: process.env.LIBRARY_TOKEN,
|
|
46
|
+
source: opt(args, '--source'),
|
|
47
|
+
license: opt(args, '--license'),
|
|
48
|
+
url: opt(args, '--url'),
|
|
49
|
+
title: opt(args, '--title'),
|
|
50
|
+
});
|
|
51
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
52
|
+
return 0;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
process.stderr.write(`${err.message}\n`);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/ingest/save.mjs
CHANGED
|
@@ -1,11 +1,33 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { parseEntry, validateEntry } from '../entry.mjs';
|
|
2
3
|
import { buildIndex } from '../index/build.mjs';
|
|
3
4
|
|
|
4
5
|
export async function saveEntry({ dir, vocab, indexPath, entriesDir, models, p }) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
const batch = await saveEntries({ dirs: [dir], vocab, indexPath, entriesDir, models, p });
|
|
7
|
+
return batch.results[0];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function saveEntries({ dirs, vocab, indexPath, entriesDir, models, p }) {
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const results = dirs.map((dir) => {
|
|
13
|
+
const requested = path.basename(dir);
|
|
14
|
+
if (seen.has(requested)) {
|
|
15
|
+
return { ok: false, slug: requested, errors: ['duplicate slug in batch'], refused: 'duplicate slug in batch' };
|
|
16
|
+
}
|
|
17
|
+
seen.add(requested);
|
|
18
|
+
try {
|
|
19
|
+
const e = parseEntry(dir);
|
|
20
|
+
const r = validateEntry(e, vocab);
|
|
21
|
+
if (!r.ok) return { ok: false, slug: e.slug, errors: r.errors, refused: r.errors.join('; ') };
|
|
22
|
+
return { ok: true, slug: e.slug, errors: [] };
|
|
23
|
+
} catch (err) {
|
|
24
|
+
return { ok: false, slug: requested, errors: [err.message], refused: err.message };
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const saved = results.filter((r) => r.ok).length;
|
|
29
|
+
// A batch rebuilds once after every draft has been validated. Rebuilding after each
|
|
30
|
+
// slug makes a 2,000-entry approval quadratic even though the final index is identical.
|
|
31
|
+
if (saved) await buildIndex({ entriesDir, indexPath, vocab, models, p });
|
|
32
|
+
return { results, saved, refused: results.length - saved };
|
|
11
33
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
export const MEDIA_EXTENSIONS = new Set(['.webp', '.gif', '.mp4', '.mov', '.webm', '.mkv']);
|
|
6
|
+
export const DEFAULT_UPLOAD_MAX_BYTES = 512 * 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
export class UploadError extends Error {
|
|
9
|
+
constructor(status, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function oneLine(value, name) {
|
|
16
|
+
if (value == null || value === '') return undefined;
|
|
17
|
+
if (typeof value !== 'string' || /[\r\n]/.test(value)) throw new UploadError(400, `${name} must be one line`);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function uploadMetadata(headers) {
|
|
22
|
+
const kind = oneLine(headers['x-library-source'], 'x-library-source');
|
|
23
|
+
const license = oneLine(headers['x-library-license'], 'x-library-license');
|
|
24
|
+
const title = oneLine(headers['x-library-title'], 'x-library-title');
|
|
25
|
+
const url = oneLine(headers['x-library-url'], 'x-library-url');
|
|
26
|
+
|
|
27
|
+
if ((kind && !license) || (!kind && license)) {
|
|
28
|
+
throw new UploadError(400, 'x-library-source and x-library-license must be provided together');
|
|
29
|
+
}
|
|
30
|
+
if (kind && !['own', 'open', 'paid', 'public-site'].includes(kind)) {
|
|
31
|
+
throw new UploadError(400, 'x-library-source must be own, open, paid, or public-site');
|
|
32
|
+
}
|
|
33
|
+
if (url) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(url);
|
|
36
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error();
|
|
37
|
+
} catch {
|
|
38
|
+
throw new UploadError(400, 'x-library-url must be a full HTTP(S) URL');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return kind ? { kind, license, ...(title ? { title } : {}), ...(url ? { url } : {}) } : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function uploadFilename(raw) {
|
|
45
|
+
let filename;
|
|
46
|
+
try { filename = decodeURIComponent(raw); } catch { throw new UploadError(400, 'invalid encoded filename'); }
|
|
47
|
+
if (!filename || filename !== path.basename(filename) || filename === '.' || filename === '..' || /[\0\r\n]/.test(filename)) {
|
|
48
|
+
throw new UploadError(400, 'filename must be one file name without a path');
|
|
49
|
+
}
|
|
50
|
+
if (!MEDIA_EXTENSIONS.has(path.extname(filename).toLowerCase())) {
|
|
51
|
+
throw new UploadError(415, `unsupported media extension; use ${[...MEDIA_EXTENSIONS].join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
return filename;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function receiveInboxUpload({ req, dataDir, rawFilename, maxBytes = DEFAULT_UPLOAD_MAX_BYTES }) {
|
|
57
|
+
const filename = uploadFilename(rawFilename);
|
|
58
|
+
const metadata = uploadMetadata(req.headers);
|
|
59
|
+
const declared = Number(req.headers['content-length']);
|
|
60
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
61
|
+
throw new UploadError(413, `upload exceeds ${maxBytes} byte limit`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const inbox = path.join(dataDir, 'inbox');
|
|
65
|
+
const destination = path.join(inbox, filename);
|
|
66
|
+
const sidecar = `${destination}.json`;
|
|
67
|
+
fs.mkdirSync(inbox, { recursive: true });
|
|
68
|
+
if (fs.existsSync(destination) || fs.existsSync(sidecar)) {
|
|
69
|
+
throw new UploadError(409, `${filename} already exists in inbox`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const temporary = path.join(inbox, `.${filename}.${randomUUID()}.upload`);
|
|
73
|
+
const handle = await fs.promises.open(temporary, 'wx');
|
|
74
|
+
const digest = createHash('sha256');
|
|
75
|
+
let bytes = 0;
|
|
76
|
+
try {
|
|
77
|
+
for await (const chunk of req) {
|
|
78
|
+
bytes += chunk.length;
|
|
79
|
+
if (bytes > maxBytes) throw new UploadError(413, `upload exceeds ${maxBytes} byte limit`);
|
|
80
|
+
digest.update(chunk);
|
|
81
|
+
await handle.write(chunk);
|
|
82
|
+
}
|
|
83
|
+
if (bytes === 0) throw new UploadError(400, 'upload body is empty');
|
|
84
|
+
await handle.sync();
|
|
85
|
+
await handle.close();
|
|
86
|
+
|
|
87
|
+
// link() publishes atomically and refuses a concurrent upload with the same name.
|
|
88
|
+
try {
|
|
89
|
+
await fs.promises.link(temporary, destination);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err.code === 'EEXIST') throw new UploadError(409, `${filename} already exists in inbox`);
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
if (metadata) {
|
|
95
|
+
try {
|
|
96
|
+
await fs.promises.writeFile(sidecar, `${JSON.stringify(metadata)}\n`, { flag: 'wx' });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
await fs.promises.unlink(destination).catch(() => {});
|
|
99
|
+
if (err.code === 'EEXIST') throw new UploadError(409, `${filename}.json already exists in inbox`);
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
await fs.promises.unlink(temporary);
|
|
104
|
+
return { ok: true, file: filename, bytes, sha256: digest.digest('hex'), sidecar: Boolean(metadata) };
|
|
105
|
+
} catch (err) {
|
|
106
|
+
await handle.close().catch(() => {});
|
|
107
|
+
await fs.promises.unlink(temporary).catch(() => {});
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/mcp/http.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { paths } from '../paths.mjs';
|
|
|
7
7
|
import { openIndex } from '../index/query.mjs';
|
|
8
8
|
import { getEmbedder } from '../index/embed.mjs';
|
|
9
9
|
import { serveFile } from '../ui/serve.mjs';
|
|
10
|
+
import { receiveInboxUpload, UploadError, DEFAULT_UPLOAD_MAX_BYTES } from '../ingest/upload.mjs';
|
|
10
11
|
|
|
11
12
|
function authorized(header, token) {
|
|
12
13
|
const want = Buffer.from(`Bearer ${token}`), got = Buffer.from(header ?? '');
|
|
@@ -41,6 +42,26 @@ async function handle(req, res, p, token, embedder) {
|
|
|
41
42
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
42
43
|
return res.end(JSON.stringify({ ok: true, entries, arms, artifacts, version: p.version }));
|
|
43
44
|
}
|
|
45
|
+
if (url.pathname.startsWith('/inbox/')) {
|
|
46
|
+
if (!authorized(req.headers.authorization, token)) { res.writeHead(401); return res.end('unauthorized'); }
|
|
47
|
+
if (req.method !== 'PUT') { res.writeHead(405, { allow: 'PUT' }); return res.end('method not allowed'); }
|
|
48
|
+
const configured = Number(process.env.LIBRARY_UPLOAD_MAX_BYTES);
|
|
49
|
+
const maxBytes = Number.isSafeInteger(configured) && configured > 0 ? configured : DEFAULT_UPLOAD_MAX_BYTES;
|
|
50
|
+
try {
|
|
51
|
+
const result = await receiveInboxUpload({
|
|
52
|
+
req,
|
|
53
|
+
dataDir: p.data,
|
|
54
|
+
rawFilename: url.pathname.slice('/inbox/'.length),
|
|
55
|
+
maxBytes,
|
|
56
|
+
});
|
|
57
|
+
res.writeHead(201, { 'content-type': 'application/json' });
|
|
58
|
+
return res.end(JSON.stringify(result));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (!(err instanceof UploadError)) throw err;
|
|
61
|
+
res.writeHead(err.status, { 'content-type': 'application/json' });
|
|
62
|
+
return res.end(JSON.stringify({ ok: false, refused: err.message }));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
44
65
|
if (url.pathname === '/mcp') {
|
|
45
66
|
if (!authorized(req.headers.authorization, token)) { res.writeHead(401); return res.end('unauthorized'); }
|
|
46
67
|
// ponytail: SDK 1.30's stateless transport (sessionIdGenerator: undefined)
|