@struq/skill 0.5.0 → 0.5.2
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 +144 -107
- package/cli/struq.mjs +2 -2
- package/package.json +8 -6
- package/skills/struq/blocks/CATALOG.md +1 -1
- package/skills/struq/blocks/catalog.json +1 -1
- package/skills/struq/blocks/film.json +1 -1
- package/skills/struq/canon/canon.json +1 -1
- package/skills/struq/references/iconography.md +1 -1
- package/skills/struq/scripts/check.mjs +5 -4
- package/skills/struq/scripts/images.mjs +64 -37
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<h1 align="center">
|
|
2
|
-
<a href="https://struq.
|
|
2
|
+
<a href="https://struq.app/skill">
|
|
3
3
|
<picture>
|
|
4
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/
|
|
5
|
-
<img alt="Struq skill: studio-grade sites from the AI you already use." src="https://cdn.jsdelivr.net/gh/
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/hero-dark.svg">
|
|
5
|
+
<img alt="Struq skill: studio-grade sites from the AI you already use." src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/hero-light.svg" width="100%">
|
|
6
6
|
</picture>
|
|
7
7
|
</a>
|
|
8
8
|
</h1>
|
|
@@ -12,15 +12,17 @@
|
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
|
-
<a href="https://struq.
|
|
15
|
+
<a href="https://struq.app/skill">Website</a>
|
|
16
16
|
·
|
|
17
|
-
<a href="https://
|
|
17
|
+
<a href="https://struq.app/docs">Docs</a>
|
|
18
18
|
·
|
|
19
|
-
<a href="
|
|
19
|
+
<a href="https://struq.app/showcase">Showcase</a>
|
|
20
20
|
·
|
|
21
|
-
<a href="
|
|
21
|
+
<a href="#the-struq-mcp">Struq MCP</a>
|
|
22
22
|
·
|
|
23
|
-
<a href="
|
|
23
|
+
<a href="https://struq.app/icons">Struq Icons</a>
|
|
24
|
+
·
|
|
25
|
+
<a href="https://github.com/struqapp/struq-design/discussions">Discussions</a>
|
|
24
26
|
</p>
|
|
25
27
|
|
|
26
28
|
<br>
|
|
@@ -29,81 +31,68 @@
|
|
|
29
31
|
npx @struq/skill@latest
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
It finds the AI tools on your machine and installs the skill into each
|
|
34
|
+
It finds the AI tools on your machine and installs the skill into each one. Then give your agent a brief:
|
|
33
35
|
|
|
34
36
|
```text
|
|
35
37
|
/struq a portfolio for an architecture studio in Rotterdam, dark, calm, lots of photography
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
| Agent | How to call it |
|
|
41
|
-
|---|---|
|
|
42
|
-
| Claude Code | `/struq <brief>`, or `/struq:struq <brief>` when installed as a plugin |
|
|
43
|
-
| Codex | `$struq <brief>` |
|
|
44
|
-
| Cursor, OpenCode | name it: "use the struq skill to build ..." |
|
|
40
|
+
<p align="center"><sub>MIT · no account · Node 18 or newer · Claude Code, Cursor, Codex, OpenCode, Gemini CLI, Copilot and Windsurf</sub></p>
|
|
45
41
|
|
|
46
42
|
<br>
|
|
47
43
|
|
|
48
|
-
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/
|
|
44
|
+
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/film-dark.svg"><img src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/film-light.svg" width="26" height="26" alt=""></picture> Watch the Struq skill in 58 seconds
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
https://github.com/user-attachments/assets/19587b11-ef4c-4cf6-81ef-87230923e567
|
|
51
47
|
|
|
52
|
-
|
|
48
|
+
<p align="center"><sub>The launch film, silent. Also on <a href="https://struq.app">struq.app</a>.</sub></p>
|
|
53
49
|
|
|
54
|
-
| Plain agent | With the Struq skill |
|
|
55
|
-
|---|---|
|
|
56
|
-
|  |  |
|
|
57
|
-
|
|
58
|
-
<details>
|
|
59
|
-
<summary><b>Seen the left one before?</b></summary>
|
|
60
50
|
<br>
|
|
61
51
|
|
|
62
|
-
|
|
52
|
+
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/story-dark.svg"><img src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/story-light.svg" width="26" height="26" alt=""></picture> One brief, one stop, one site
|
|
63
53
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
<picture>
|
|
55
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/story-dark.svg">
|
|
56
|
+
<img alt="Three frames: a brief typed into the agent, the plan the skill stops once to show, and the site assembling and passing its check." src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/story-light.svg" width="100%">
|
|
57
|
+
</picture>
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
|---|---|
|
|
70
|
-
|  |  |
|
|
59
|
+
Your agent settles the idea, the motion language, the motif and a storyboard of scenes before it writes any markup. It stops once to show you that plan and asks for a logo, photos or real numbers if you have them. Then it builds from the plan and runs its own check until the page passes. It never invents a testimonial or a number the brief did not give.
|
|
71
60
|
|
|
72
|
-
<details>
|
|
73
|
-
<summary><b>Where did "12 restaurants" come from?</b></summary>
|
|
74
61
|
<br>
|
|
75
62
|
|
|
76
|
-
|
|
63
|
+
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/showcase-dark.svg"><img src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/showcase-light.svg" width="26" height="26" alt=""></picture> What a directed site looks like
|
|
77
64
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
65
|
+
<p align="center">
|
|
66
|
+
<a href="https://struq.app/showcase/vuur"><img alt="Vuur, a restaurant site built with the Struq skill, playing its opening and first scene." src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/showcase/vuur.webp" width="100%"></a>
|
|
67
|
+
</p>
|
|
81
68
|
|
|
82
|
-
|
|
83
|
-
|---|---|
|
|
84
|
-
|  |  |
|
|
69
|
+
<p align="center"><sub><b>Vuur</b>, a restaurant with a wood-fired oven. Built with the Struq skill by Claude Opus 5.5.</sub></p>
|
|
85
70
|
|
|
86
|
-
<
|
|
87
|
-
<
|
|
88
|
-
<
|
|
71
|
+
<p align="center">
|
|
72
|
+
<a href="https://struq.app/showcase/forge"><img alt="Ostrava Steel, a knife forge site." src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/showcase/forge.webp" width="49%"></a>
|
|
73
|
+
<a href="https://struq.app/showcase/brume"><img alt="Brume, a lighthouse hotel site." src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/showcase/brume.webp" width="49%"></a>
|
|
74
|
+
</p>
|
|
89
75
|
|
|
90
|
-
|
|
76
|
+
<p align="center">
|
|
77
|
+
<a href="https://struq.app/showcase/lowlight"><img alt="Lowlight, a film studio site." src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/showcase/lowlight.webp" width="49%"></a>
|
|
78
|
+
<a href="https://struq.app/showcase/halide"><img alt="Halide, a site directed in ink." src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/showcase/halide.webp" width="49%"></a>
|
|
79
|
+
</p>
|
|
91
80
|
|
|
92
|
-
</
|
|
81
|
+
<p align="center"><sub><b>Ostrava Steel</b>, <b>Brume</b>, <b>Lowlight</b> and <b>Halide</b> were directed the Struq way and written by Claude Opus as one file, before the skill carried the method. Much of what the skill knows about craft came from them. All five run live on <a href="https://struq.app/showcase">struq.app/showcase</a>.</sub></p>
|
|
93
82
|
|
|
94
|
-
Where the
|
|
83
|
+
Where the skill still loses: craft under pressure. An ambitious page with pinned scenes has more that can break than a plain one, and a reveal can still come out empty on a first build. The check catches most of it, and each release works on the rest.
|
|
95
84
|
|
|
96
85
|
<br>
|
|
97
86
|
|
|
98
|
-
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/
|
|
99
|
-
|
|
100
|
-
Your agent picks the direction, sets the order and writes the copy. The motion comes from Struq's own catalogue: 44 admitted blocks, each a real React file with three variants, working in light and dark.
|
|
87
|
+
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/blocks-dark.svg"><img src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/blocks-light.svg" width="26" height="26" alt=""></picture> The blocks carry the motion
|
|
101
88
|
|
|
102
89
|
<picture>
|
|
103
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/
|
|
104
|
-
<img alt="Six Struq blocks playing at once: a preloader, a hero, a page transition, a manifesto scrub, an image growing to full bleed and a WebGL background." src="https://cdn.jsdelivr.net/gh/
|
|
90
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/blocks-dark.webp">
|
|
91
|
+
<img alt="Six Struq blocks playing at once: a preloader, a hero, a page transition, a manifesto scrub, an image growing to full bleed and a WebGL background." src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/blocks-light.webp" width="100%">
|
|
105
92
|
</picture>
|
|
106
93
|
|
|
94
|
+
Your agent picks the direction, sets the order and writes the copy. The motion comes from Struq's own catalogue: 44 admitted blocks, each a real React file that works in light and dark, with a still version for phones and for reduced motion.
|
|
95
|
+
|
|
107
96
|
<details>
|
|
108
97
|
<summary><b>All 44, by kind</b></summary>
|
|
109
98
|
<br>
|
|
@@ -120,44 +109,103 @@ Your agent picks the direction, sets the order and writes the copy. The motion c
|
|
|
120
109
|
| Sections | 15 |
|
|
121
110
|
| Footers | 2 |
|
|
122
111
|
|
|
123
|
-
Each one with its files, packages
|
|
112
|
+
Each one with its files, packages and props: [the catalogue](skills/struq/blocks/CATALOG.md). Every GSAP plugin they use is free on npm.
|
|
124
113
|
|
|
125
114
|
</details>
|
|
126
115
|
|
|
127
116
|
<br>
|
|
128
117
|
|
|
129
|
-
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/
|
|
118
|
+
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/direction-dark.svg"><img src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/direction-light.svg" width="26" height="26" alt=""></picture> Direction before markup
|
|
119
|
+
|
|
120
|
+
<picture>
|
|
121
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/direction-dark.svg">
|
|
122
|
+
<img alt="Three directions from the skill's canon, each in its own palette, type pairing and motion language: weight, cut and curtain." src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/direction-light.svg" width="100%">
|
|
123
|
+
</picture>
|
|
124
|
+
|
|
125
|
+
Three briefs from the skill's worked examples, three directions from its canon. Each gets a palette, a type pairing and a motion language: weight settles, cut cuts, curtain lifts. The skill writes the choice into one token block that every block reads, so the whole page holds to it.
|
|
126
|
+
|
|
127
|
+
<br>
|
|
128
|
+
|
|
129
|
+
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/mcp-dark.svg"><img src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/mcp-light.svg" width="26" height="26" alt=""></picture> The Struq MCP
|
|
130
130
|
|
|
131
131
|
<picture>
|
|
132
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/
|
|
133
|
-
<img alt="
|
|
132
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/mcp-dark.svg">
|
|
133
|
+
<img alt="Your agent calling the Struq MCP's seven read-only tools at struq.app/api/mcp." src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/mcp-light.svg" width="100%">
|
|
134
134
|
</picture>
|
|
135
135
|
|
|
136
|
+
The skill works on its own, from the catalogue it ships with. Connect the Struq MCP and your agent reads the live catalogue instead, plus the whole Struq library. It is free; it asks for a free account because your AI tool connects as you.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
claude mcp add --transport http --scope user struq https://struq.app/api/mcp
|
|
140
|
+
```
|
|
141
|
+
|
|
136
142
|
<details>
|
|
137
|
-
<summary><b>
|
|
143
|
+
<summary><b>Codex, Cursor, VS Code, Windsurf and the Claude app</b></summary>
|
|
138
144
|
<br>
|
|
139
145
|
|
|
140
|
-
|
|
146
|
+
Codex, in `~/.codex/config.toml`:
|
|
147
|
+
|
|
148
|
+
```toml
|
|
149
|
+
[mcp_servers.struq]
|
|
150
|
+
url = "https://struq.app/api/mcp"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Cursor, in `~/.cursor/mcp.json` (then approve Struq under Settings, MCP):
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"mcpServers": {
|
|
158
|
+
"struq": { "url": "https://struq.app/api/mcp" }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
VS Code, Windsurf, Antigravity and the Claude app each want a slightly different shape. [The connect guide](https://struq.app/docs/connect) has every one, checked against each tool's own docs.
|
|
164
|
+
|
|
165
|
+
</details>
|
|
166
|
+
|
|
167
|
+
<details>
|
|
168
|
+
<summary><b>The seven tools</b></summary>
|
|
169
|
+
<br>
|
|
170
|
+
|
|
171
|
+
| Tool | What it does |
|
|
141
172
|
|---|---|
|
|
142
|
-
|
|
|
143
|
-
|
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
173
|
+
| `struq_richting` | The direction: one palette, one type pairing and one photo pack, as the token block the blocks read |
|
|
174
|
+
| `struq_blokken` | Lists the blocks, by kind or by words |
|
|
175
|
+
| `struq_blok` | One block as real files, with its packages, props and a usage example |
|
|
176
|
+
| `struq_controle` | The check your agent runs against its own files before it calls the work done |
|
|
177
|
+
| `struq_list_assets` | Pages through the library: palettes, typography, design systems, sections and imagery |
|
|
178
|
+
| `struq_search_assets` | Searches the library by mood, sector or name |
|
|
179
|
+
| `struq_get_asset` | One asset with its prompt and its exact colours and fonts |
|
|
148
180
|
|
|
149
|
-
|
|
181
|
+
All of them read; none of them changes anything in your account or your project. [More on the MCP](https://struq.app/mcp).
|
|
150
182
|
|
|
151
183
|
</details>
|
|
152
184
|
|
|
153
185
|
<br>
|
|
154
186
|
|
|
155
|
-
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/
|
|
187
|
+
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/icons-dark.svg"><img src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/icons-light.svg" width="26" height="26" alt=""></picture> Struq Icons
|
|
188
|
+
|
|
189
|
+
<a href="https://struq.app/icons">
|
|
190
|
+
<picture>
|
|
191
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/icons-dark.svg">
|
|
192
|
+
<img alt="Sixteen Struq Icons, each playing its own hover move." src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/icons-light.svg" width="100%">
|
|
193
|
+
</picture>
|
|
194
|
+
</a>
|
|
195
|
+
|
|
196
|
+
Struq builds reach for Struq Icons by default. Each moving icon makes one small move under the cursor that says what it does, and every icon on this page is one of them.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
npm install @struq/icons
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
<br>
|
|
203
|
+
|
|
204
|
+
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/details-dark.svg"><img src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/h/details-light.svg" width="26" height="26" alt=""></picture> The details
|
|
156
205
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
- **Check.** `check.mjs` runs until the page passes. With a browser tool it also looks at the page at phone and desktop width and fixes what it sees.
|
|
206
|
+
<details>
|
|
207
|
+
<summary><b>Commands, per agent</b></summary>
|
|
208
|
+
<br>
|
|
161
209
|
|
|
162
210
|
| Command | What it does |
|
|
163
211
|
|---|---|
|
|
@@ -166,20 +214,12 @@ Stack: Next.js App Router and Tailwind v4 (Vite by hand, see `references/stack-t
|
|
|
166
214
|
| `/struq audit <path-or-url>` | Changes nothing. Returns every tell with its fix and the three changes that pay off most |
|
|
167
215
|
| `/struq direct <brief>` | Directed mode, for one continuous 3D experience across chapters |
|
|
168
216
|
|
|
169
|
-
In Codex the command is `$struq
|
|
170
|
-
|
|
171
|
-
<br>
|
|
172
|
-
|
|
173
|
-
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqnl/struq-design@main/assets/readme/h/download-dark.svg"><img src="https://cdn.jsdelivr.net/gh/struqnl/struq-design@main/assets/readme/h/download-light.svg" width="26" height="26" alt=""></picture> Install
|
|
217
|
+
In Codex the command is `$struq`. As a Claude Code plugin it is `/struq:struq`. In Cursor and OpenCode, ask for the struq skill by name.
|
|
174
218
|
|
|
175
|
-
|
|
176
|
-
npx @struq/skill@latest
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
That covers Claude Code, Cursor, Codex, OpenCode, Gemini CLI, Copilot and Windsurf. Run it again to update; `npx @struq/skill@latest remove` takes it out. Node 18 or newer.
|
|
219
|
+
</details>
|
|
180
220
|
|
|
181
221
|
<details>
|
|
182
|
-
<summary><b>
|
|
222
|
+
<summary><b>Install options</b></summary>
|
|
183
223
|
<br>
|
|
184
224
|
|
|
185
225
|
```bash
|
|
@@ -187,14 +227,17 @@ That covers Claude Code, Cursor, Codex, OpenCode, Gemini CLI, Copilot and Windsu
|
|
|
187
227
|
npx @struq/skill@latest --claude
|
|
188
228
|
npx @struq/skill@latest --project
|
|
189
229
|
|
|
230
|
+
# update by running it again, or take it out
|
|
231
|
+
npx @struq/skill@latest remove
|
|
232
|
+
|
|
190
233
|
# with the open skills CLI, which knows 70+ agents
|
|
191
|
-
npx skills add
|
|
234
|
+
npx skills add struqapp/struq-design -g
|
|
192
235
|
```
|
|
193
236
|
|
|
194
237
|
As a Claude Code plugin (the command is then `/struq:struq`):
|
|
195
238
|
|
|
196
239
|
```text
|
|
197
|
-
/plugin marketplace add
|
|
240
|
+
/plugin marketplace add struqapp/struq-design
|
|
198
241
|
/plugin install struq@struq
|
|
199
242
|
```
|
|
200
243
|
|
|
@@ -202,37 +245,31 @@ The screenshot step uses whatever browser tool your agent has (Playwright or sim
|
|
|
202
245
|
|
|
203
246
|
</details>
|
|
204
247
|
|
|
248
|
+
<details>
|
|
249
|
+
<summary><b>What it fetches and sends</b></summary>
|
|
205
250
|
<br>
|
|
206
251
|
|
|
207
|
-
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqnl/struq-design@main/assets/readme/h/palette-dark.svg"><img src="https://cdn.jsdelivr.net/gh/struqnl/struq-design@main/assets/readme/h/palette-light.svg" width="26" height="26" alt=""></picture> The icons it reaches for
|
|
208
|
-
|
|
209
|
-
Struq builds use [Struq Icons](https://struq.nl/iconen) by default: 182 icons in two tones, and each of the 118 full icons makes one small move under the cursor that says what it does. Every icon on this page is one of them.
|
|
210
|
-
|
|
211
|
-
<a href="https://struq.nl/iconen">
|
|
212
|
-
<picture>
|
|
213
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/npm/@struq/icons@1/readme/motion-dark.svg">
|
|
214
|
-
<img alt="Eight Struq icons playing their hover motion: lock, delete, settings, mail, history, sound, hint, bug." src="https://cdn.jsdelivr.net/npm/@struq/icons@1/readme/motion-light.svg" width="100%">
|
|
215
|
-
</picture>
|
|
216
|
-
</a>
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
npm install @struq/icons
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
<br>
|
|
223
|
-
|
|
224
|
-
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqnl/struq-design@main/assets/readme/h/shield-dark.svg"><img src="https://cdn.jsdelivr.net/gh/struqnl/struq-design@main/assets/readme/h/shield-light.svg" width="26" height="26" alt=""></picture> What it fetches and sends
|
|
225
|
-
|
|
226
252
|
The Struq skill is instructions, the block files and five small Node scripts (`blocks.mjs`, `canon.mjs`, `images.mjs`, `check.mjs`, `log.mjs`) that your agent runs in your project. They read and write files there, including a build log, `STRUQ-LOG.md`.
|
|
227
253
|
|
|
228
254
|
- **Blocks and canon** ship inside the skill. Nothing is fetched to install a block or write the tokens.
|
|
229
255
|
- **Fonts** load from Google Fonts (or Fontshare, for a face that lives there) in the visitor's browser, as on any site with web fonts.
|
|
230
|
-
- **Photos**, for every slot your own photos do not fill: `images.mjs` reads the public pack list at `https://struq.
|
|
231
|
-
- **The MCP
|
|
256
|
+
- **Photos**, for every slot your own photos do not fill: `images.mjs` reads the public pack list at `https://struq.app/api/media/packs` and downloads the chosen images from `https://struq.app/media/packs/`, or from the same paths on the old domain when struq.app does not answer. Offline it writes plates graded to your palette instead.
|
|
257
|
+
- **The MCP** (`https://struq.app/api/mcp`), if you connect it: your agent calls Struq's tools with your brief. Nothing else leaves your machine.
|
|
232
258
|
- **The installer** downloads the package from npm, looks for your AI tools' folders in your home folder and copies the skill into them. It sends nothing.
|
|
233
259
|
|
|
234
|
-
|
|
260
|
+
</details>
|
|
261
|
+
|
|
262
|
+
- **Something went wrong?** [Open an issue](https://github.com/struqapp/struq-design/issues/new/choose) and paste your `STRUQ-LOG.md`: it shows where the build went off.
|
|
263
|
+
- **A question, or a site you built?** [Discussions](https://github.com/struqapp/struq-design/discussions). Before and after screenshots are the most useful thing you can share.
|
|
264
|
+
- **Want to change the skill?** [CONTRIBUTING.md](https://github.com/struqapp/struq-design/blob/main/CONTRIBUTING.md) maps the repository and the house rules. Every release is in the [changelog](https://github.com/struqapp/struq-design/blob/main/CHANGELOG.md).
|
|
235
265
|
|
|
236
|
-
|
|
266
|
+
<br>
|
|
237
267
|
|
|
238
|
-
|
|
268
|
+
<p align="center">
|
|
269
|
+
<a href="https://struq.app">
|
|
270
|
+
<picture>
|
|
271
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/outro-dark.svg">
|
|
272
|
+
<img alt="Struq. Free and open source, MIT." src="https://cdn.jsdelivr.net/gh/struqapp/struq-design@v0.5.2/assets/readme/outro-light.svg" width="100%">
|
|
273
|
+
</picture>
|
|
274
|
+
</a>
|
|
275
|
+
</p>
|
package/cli/struq.mjs
CHANGED
|
@@ -34,7 +34,7 @@ Usage
|
|
|
34
34
|
npx @struq/skill@latest remove remove the skill again (takes the same flags)
|
|
35
35
|
npx @struq/skill@latest --help show this
|
|
36
36
|
|
|
37
|
-
Run it again at any time to update. More: https://struq.
|
|
37
|
+
Run it again at any time to update. More: https://struq.app/skill
|
|
38
38
|
`;
|
|
39
39
|
|
|
40
40
|
function parse(argv) {
|
|
@@ -119,7 +119,7 @@ function run(argv) {
|
|
|
119
119
|
for (const tool of new Map(list.map(({ tool }) => [tool.label, tool])).values()) {
|
|
120
120
|
out.push(` ${tool.label.padEnd(15)}${tool.use}`);
|
|
121
121
|
}
|
|
122
|
-
out.push('', 'Run this again at any time to update. More: https://struq.
|
|
122
|
+
out.push('', 'Run this again at any time to update. More: https://struq.app/skill');
|
|
123
123
|
}
|
|
124
124
|
process.stdout.write(`${out.join('\n')}\n`);
|
|
125
125
|
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@struq/skill",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Install the free Struq skill into Claude Code, Cursor, Codex and other AI coding tools: websites at the level of a design studio, built by your own AI.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author": "Struq (https://struq.
|
|
7
|
-
"homepage": "https://struq.
|
|
6
|
+
"author": "Struq (https://struq.app)",
|
|
7
|
+
"homepage": "https://struq.app/skill",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/struqapp/struq-design.git"
|
|
11
11
|
},
|
|
12
|
-
"bugs": "https://github.com/
|
|
12
|
+
"bugs": "https://github.com/struqapp/struq-design/issues",
|
|
13
13
|
"keywords": [
|
|
14
14
|
"agent-skill",
|
|
15
15
|
"skills",
|
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
"scripts": {
|
|
38
38
|
"test": "node --test test/install.test.mjs test/check.test.mjs test/packs.test.mjs test/tokens.test.mjs test/blocks.test.mjs test/gate.test.mjs test/arc.test.mjs test/plugin.test.mjs",
|
|
39
39
|
"release:blocks": "node scripts/release-blocks.mjs",
|
|
40
|
-
"release:plugin": "node scripts/build-plugin.mjs"
|
|
40
|
+
"release:plugin": "node scripts/build-plugin.mjs",
|
|
41
|
+
"prepack": "node scripts/readme-urls.mjs pack",
|
|
42
|
+
"postpack": "node scripts/readme-urls.mjs restore"
|
|
41
43
|
},
|
|
42
44
|
"publishConfig": {
|
|
43
45
|
"access": "public"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Block catalogue
|
|
2
2
|
|
|
3
|
-
Generated by scripts/release-blocks.mjs from Struq origin/main at
|
|
3
|
+
Generated by scripts/release-blocks.mjs from Struq origin/main at a538491. Do not edit: run the release script again.
|
|
4
4
|
44 admitted blocks. Install one with `node <skill-dir>/scripts/blocks.mjs add <slug>`; read its file for the props.
|
|
5
5
|
Every block works light and dark; a variant is a different composition of the same idea, passed as `variant`.
|
|
6
6
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"source": {
|
|
3
3
|
"repo": "Struq blocks/film/arcs.ts",
|
|
4
4
|
"ref": "origin/main",
|
|
5
|
-
"commit": "
|
|
5
|
+
"commit": "a538491"
|
|
6
6
|
},
|
|
7
7
|
"ARC_RULES": [
|
|
8
8
|
"A loud scene moves with the scroll (a pin, a scrub, a reveal to full bleed); a quiet scene is type and a photo that enter once and sit still. Tag a scene (loud) when the block's energy is 2 or more, (quiet) at 1 or less or for an own scene.",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Icons
|
|
2
2
|
|
|
3
3
|
Struq Icons is the default: `npm i @struq/icons` (MIT), the Iconify set `struq` for non-React stacks, browsable at
|
|
4
|
-
[struq.
|
|
4
|
+
[struq.app/icons](https://struq.app/icons). A two-tone set on one 24 grid, the base tone for the shape and the accent
|
|
5
5
|
on the part that carries the meaning, each icon with its own hover motion.
|
|
6
6
|
|
|
7
7
|
## Whether an icon earns its place
|
|
@@ -159,7 +159,7 @@ const plan = read("STRUQ.md");
|
|
|
159
159
|
const pack = mod.match(/imagePack = "([^"]+)"/)?.[1] ?? "?";
|
|
160
160
|
if (imageFit === "none" && !srcs.length) add(G, "PASS", `No photo of pack "${pack}" fits this brief (imageFit none): the opener carries the first screen, and the approval stop asks for photos`);
|
|
161
161
|
else if (missing.length) add(G, "FAIL", `${missing.length} image(s) in ${libFile} are not on disk. Run images.mjs ${pack} again.`, missing);
|
|
162
|
-
else if (plates.length === srcs.length) add(G, "WARN", `
|
|
162
|
+
else if (plates.length === srcs.length) add(G, "WARN", `Struq was unreachable: all ${srcs.length} images are fallback plates. Run images.mjs ${pack} again when online.`);
|
|
163
163
|
else if (plates.length) add(G, "WARN", `${plates.length} fallback plate(s) among the pack images. Run images.mjs ${pack} again when online.`, plates);
|
|
164
164
|
else add(G, "PASS", `${srcs.length} beeldbank images from pack "${pack}", each a verified image file`);
|
|
165
165
|
// An off-topic photo is worse than no photo: a pack the brief does not point at must show
|
|
@@ -234,13 +234,14 @@ const plan = read("STRUQ.md");
|
|
|
234
234
|
{ scope: own, exclude: /placeholder=["'{]/ },
|
|
235
235
|
);
|
|
236
236
|
const emptyFrames = hits(/<(div|figure|span)\b[^>]*(aspect-|aspectRatio)[^>]*(\/>|>\s*<\/(div|figure|span)>)/, { scope: own });
|
|
237
|
-
|
|
237
|
+
// Both domains: struq.app is home, and struq.nl still serves the media for older installs.
|
|
238
|
+
const hotlinks = hits(/https?:\/\/(www\.)?struq\.(app|nl)\/media\//, { scope: own });
|
|
238
239
|
if (placeholders.length || emptyFrames.length) {
|
|
239
240
|
add(G, "FAIL", "Placeholder or empty frames. A Struq site never ships an empty frame: put a MediaFrame with a pack image in each.", [...placeholders, ...emptyFrames]);
|
|
240
241
|
} else add(G, "PASS", "No placeholder or empty image frames");
|
|
241
|
-
if (hotlinks.length) add(G, "FAIL", "Hotlinked
|
|
242
|
+
if (hotlinks.length) add(G, "FAIL", "Hotlinked Struq images: download them with images.mjs and use the local /images/ path.", hotlinks);
|
|
242
243
|
|
|
243
|
-
const credit = uses(/imageCredit|struq\.nl\/beeldbank/);
|
|
244
|
+
const credit = uses(/imageCredit|struq\.app\/photos|struq\.nl\/beeldbank/);
|
|
244
245
|
if (libFile && !credit) add(G, "WARN", "No image credit. Put imageCredit (\"Beeld: Struq\") in the footer as one quiet text-small link.");
|
|
245
246
|
}
|
|
246
247
|
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
// given, and photos whose alt names a subject the client does not do are left out, best fit first.
|
|
12
12
|
//
|
|
13
13
|
// What it does, in order:
|
|
14
|
-
// 1. Reads the pack from https://struq.
|
|
14
|
+
// 1. Reads the pack from https://struq.app/api/media/packs/<slug> (public, no key, CC0 images),
|
|
15
|
+
// or from https://struq.nl when struq.app does not answer.
|
|
15
16
|
// 2. Downloads all 24 images to public/images/<slot>-<n>.webp and checks each one is a real image.
|
|
16
17
|
// 3. Writes lib/struq-images.ts (src/lib/ when the project uses src/): every image with its alt,
|
|
17
18
|
// size and description, grouped by slot, plus the credit line. Components import from it.
|
|
18
|
-
// 4. If
|
|
19
|
+
// 4. If Struq cannot be reached, or a file fails its check, it writes a graded fallback plate
|
|
19
20
|
// in the site palette for that slot instead, so no frame is ever empty, and says so.
|
|
20
21
|
//
|
|
21
22
|
// Node 18 or later, no dependencies. Safe to run again: a file is kept only when the last run
|
|
@@ -26,12 +27,21 @@ import { join, resolve } from "node:path";
|
|
|
26
27
|
import { appendLog } from "./log.mjs";
|
|
27
28
|
import { fitImages, packForBrief, packReason } from "./packs.mjs";
|
|
28
29
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
30
|
+
// Struq lives at struq.app. The old domain struq.nl keeps answering the pack API and the media for
|
|
31
|
+
// installed skills, so a run tries struq.app first and falls back to struq.nl when struq.app gives a
|
|
32
|
+
// network error, a non-2xx answer or something that is not the pack.
|
|
33
|
+
// STRUQ_API_BASE points a test run at another Struq environment (for example https://staging.struq.app),
|
|
34
|
+
// with no fallback. Real users never set it: the default is production.
|
|
35
|
+
const DEFAULT_BASES = ["https://struq.app", "https://struq.nl"];
|
|
36
|
+
const BASES = process.env.STRUQ_API_BASE ? [process.env.STRUQ_API_BASE.replace(/\/+$/, "")] : DEFAULT_BASES;
|
|
37
|
+
const PACKS = "/api/media/packs";
|
|
38
|
+
const HOSTS = BASES.map((b) => b.replace(/^https?:\/\//, "")).join(" and ");
|
|
39
|
+
/** The base that last answered the pack API; image downloads try it first. */
|
|
40
|
+
let activeBase = BASES[0];
|
|
41
|
+
/** The bases in the order to try: the one that answered first, then the rest. */
|
|
42
|
+
const basesInOrder = () => [activeBase, ...BASES.filter((b) => b !== activeBase)];
|
|
43
|
+
/** Download URLs on the same bases as the API (directPath), or the absolute publicUrl. */
|
|
44
|
+
const imageUrls = (img) => (img.directPath ? basesInOrder().map((b) => `${b}${img.directPath}`) : [img.publicUrl]);
|
|
35
45
|
const SLOTS = ["hero", "band", "card", "column", "detail", "backdrop"];
|
|
36
46
|
const FALLBACK_COUNTS = { hero: 3, band: 2, card: 8, column: 4, detail: 4, backdrop: 3 };
|
|
37
47
|
const FALLBACK_SIZE = {
|
|
@@ -53,10 +63,22 @@ const brief = flag("--brief");
|
|
|
53
63
|
const slug = args.find((a, i) => !a.startsWith("--") && args[i - 1] !== "--dir" && args[i - 1] !== "--brief") ?? (brief ? packForBrief(brief) : undefined);
|
|
54
64
|
const why = brief && !args.find((a, i) => !a.startsWith("--") && args[i - 1] !== "--dir" && args[i - 1] !== "--brief") ? packReason(brief) : "named on the command line";
|
|
55
65
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
/** GET a path of the pack API from each base in turn; the first JSON answer wins and sets activeBase. */
|
|
67
|
+
async function getJson(path) {
|
|
68
|
+
const errors = [];
|
|
69
|
+
for (const base of basesInOrder()) {
|
|
70
|
+
const url = `${base}${path}`;
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(20000), headers: { accept: "application/json" } });
|
|
73
|
+
if (!res.ok) throw new Error(`${url} answered ${res.status}`);
|
|
74
|
+
const json = await res.json();
|
|
75
|
+
activeBase = base;
|
|
76
|
+
return json;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
errors.push(error.message.startsWith(url) ? error.message : `${url}: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new Error(errors.join("; "));
|
|
60
82
|
}
|
|
61
83
|
|
|
62
84
|
function isImage(buf) {
|
|
@@ -77,20 +99,23 @@ function fileIsImage(path) {
|
|
|
77
99
|
}
|
|
78
100
|
}
|
|
79
101
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
/** Tries each URL twice in turn; returns the URL that gave a real image, or null. */
|
|
103
|
+
async function download(urls, path) {
|
|
104
|
+
for (const url of urls) {
|
|
105
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
|
108
|
+
if (!res.ok) continue;
|
|
109
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
110
|
+
if (!isImage(buf)) continue;
|
|
111
|
+
writeFileSync(path, buf);
|
|
112
|
+
return url;
|
|
113
|
+
} catch {
|
|
114
|
+
// retry once, then the next base, then fall back
|
|
115
|
+
}
|
|
91
116
|
}
|
|
92
117
|
}
|
|
93
|
-
return
|
|
118
|
+
return null;
|
|
94
119
|
}
|
|
95
120
|
|
|
96
121
|
function readPalette() {
|
|
@@ -161,7 +186,7 @@ function writeModule(entries, packSlug, fallback, imageFit = "full") {
|
|
|
161
186
|
return ` ${s}: [\n${rows}\n ],`;
|
|
162
187
|
}).join("\n");
|
|
163
188
|
const source = `// Written by the Struq skill (scripts/images.mjs). Do not hand-edit the list: run the script again
|
|
164
|
-
// to change pack. Pack: ${packSlug}${fallback ? " (FALLBACK PLATES:
|
|
189
|
+
// to change pack. Pack: ${packSlug}${fallback ? " (FALLBACK PLATES: Struq was unreachable, run the script again when online)" : ""}.
|
|
165
190
|
// Beeldbank images are CC0. Translate the alt texts when the site is not in Dutch.
|
|
166
191
|
|
|
167
192
|
export type StruqImage = {
|
|
@@ -192,7 +217,7 @@ export const imagePack = ${JSON.stringify(packSlug)};
|
|
|
192
217
|
export const imageFit: "full" | "partial" | "none" = ${JSON.stringify(imageFit)};
|
|
193
218
|
|
|
194
219
|
/** The credit line for the footer: one quiet text-small link, never a badge. */
|
|
195
|
-
export const imageCredit = { label: "Beeld: Struq", href: "https://struq.
|
|
220
|
+
export const imageCredit = { label: "Beeld: Struq", href: "https://struq.app/photos" };
|
|
196
221
|
|
|
197
222
|
export const images: Record<ImageSlot, StruqImage[]> = {
|
|
198
223
|
${body}
|
|
@@ -226,7 +251,7 @@ function removeStale(outDir, manifest) {
|
|
|
226
251
|
}
|
|
227
252
|
|
|
228
253
|
async function list() {
|
|
229
|
-
const index = await getJson(
|
|
254
|
+
const index = await getJson(PACKS);
|
|
230
255
|
console.log(`${index.packCount} packs, ${index.totalImages} images, ${index.license}\n`);
|
|
231
256
|
for (const p of index.packs) console.log(`${p.slug.padEnd(10)} ${p.description}`);
|
|
232
257
|
}
|
|
@@ -250,11 +275,11 @@ async function main() {
|
|
|
250
275
|
let droppedImages = [];
|
|
251
276
|
let imageFit = "full";
|
|
252
277
|
try {
|
|
253
|
-
pack = await getJson(`${
|
|
278
|
+
pack = await getJson(`${PACKS}/${encodeURIComponent(slug)}`);
|
|
254
279
|
if (!Array.isArray(pack?.images) || pack.images.length === 0) throw new Error("pack has no images");
|
|
255
280
|
} catch (error) {
|
|
256
281
|
try {
|
|
257
|
-
const index = await getJson(
|
|
282
|
+
const index = await getJson(PACKS);
|
|
258
283
|
const known = index.packs.map((p) => p.slug);
|
|
259
284
|
if (!known.includes(slug)) {
|
|
260
285
|
console.error(`Unknown pack "${slug}". Packs: ${known.join(", ")}`);
|
|
@@ -263,7 +288,7 @@ async function main() {
|
|
|
263
288
|
} catch {
|
|
264
289
|
// the API itself is down: fall through to the plates
|
|
265
290
|
}
|
|
266
|
-
console.warn(`WARN
|
|
291
|
+
console.warn(`WARN ${HOSTS} unreachable (${error.message}). Writing graded fallback plates instead.`);
|
|
267
292
|
pack = null;
|
|
268
293
|
}
|
|
269
294
|
|
|
@@ -293,10 +318,12 @@ async function main() {
|
|
|
293
318
|
while (cursor < jobs.length) {
|
|
294
319
|
const job = jobs[cursor++];
|
|
295
320
|
const target = join(outDir, `${job.name}.webp`);
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
321
|
+
const urls = imageUrls(job.img);
|
|
322
|
+
const last = previous.get(`public/images/${job.name}.webp`);
|
|
323
|
+
// The same photo from either base counts as the same source.
|
|
324
|
+
const kept = urls.includes(last) && fileIsImage(target);
|
|
325
|
+
const source = kept ? last : await download(urls, target);
|
|
326
|
+
if (source) {
|
|
300
327
|
real++;
|
|
301
328
|
entries.push({
|
|
302
329
|
slot: job.img.slot,
|
|
@@ -345,7 +372,7 @@ async function main() {
|
|
|
345
372
|
width: size[0],
|
|
346
373
|
height: size[1],
|
|
347
374
|
ratio: `${size[0]}:${size[1]}`,
|
|
348
|
-
description: "Fallback plate in the site palette (
|
|
375
|
+
description: "Fallback plate in the site palette (Struq was unreachable).",
|
|
349
376
|
unoptimized: true,
|
|
350
377
|
});
|
|
351
378
|
manifest.push({ file: `public/images/${slot}-${n}.svg`, id: null, slot, source: "fallback" });
|
|
@@ -372,13 +399,13 @@ async function main() {
|
|
|
372
399
|
console.log(`${`images.${e.slot}[${e.order - 1}]`.padEnd(18)} ${e.ratio.padEnd(5)} ${e.description.slice(0, 110)}`);
|
|
373
400
|
}
|
|
374
401
|
if (plates > 0) {
|
|
375
|
-
console.log(`\nWARN ${plates} slot(s) use fallback plates. Run this script again when
|
|
402
|
+
console.log(`\nWARN ${plates} slot(s) use fallback plates. Run this script again when Struq answers.`);
|
|
376
403
|
}
|
|
377
404
|
appendLog(root, "Step 4 Images", [
|
|
378
405
|
`images.mjs: pack ${slug} (${why}), ${real} images downloaded and verified, ${plates} fallback plates.`,
|
|
379
406
|
...(droppedImages.length ? [`Left out ${droppedImages.length} photos that do not fit this client: ${[...new Set(droppedImages.map((d) => d.reason))].join("; ")}.`] : []),
|
|
380
407
|
...(imageFit !== "full" ? [`Image fit: ${imageFit}${imageFit === "none" ? ", the opener is typographic or WebGL, and the approval stop asks for photos" : ""}.`] : []),
|
|
381
|
-
`Files in public/images, list in ${module.replace(root, ".")}${
|
|
408
|
+
`Files in public/images, list in ${module.replace(root, ".")}${activeBase !== DEFAULT_BASES[0] ? `, from ${activeBase}` : ""}.`,
|
|
382
409
|
]);
|
|
383
410
|
console.log(imageFit === "none" ? "\nOK no fitting photos: build the opener without them" : real > 0 ? "\nOK images ready" : "\nOK fallback plates ready (no frame is empty)");
|
|
384
411
|
}
|