@ethlete/cli 2.1.0-next.8 → 2.1.0-next.9

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/design/define-call.ts +92 -0
  3. package/design/web/env.d.ts +11 -0
  4. package/design/web/frame.html +17 -0
  5. package/design/web/frame.ts +38 -0
  6. package/design/web/host.css +494 -0
  7. package/design/web/host.ts +445 -0
  8. package/design/web/index.html +11 -0
  9. package/package.json +12 -1
  10. package/src/index.js +14 -0
  11. package/src/index.js.map +1 -1
  12. package/src/lib/design/calls-tsconfig.d.ts +6 -0
  13. package/src/lib/design/calls-tsconfig.js +37 -0
  14. package/src/lib/design/calls-tsconfig.js.map +1 -0
  15. package/src/lib/design/check.d.ts +11 -0
  16. package/src/lib/design/check.js +192 -0
  17. package/src/lib/design/check.js.map +1 -0
  18. package/src/lib/design/design-command.d.ts +10 -0
  19. package/src/lib/design/design-command.js +32 -0
  20. package/src/lib/design/design-command.js.map +1 -0
  21. package/src/lib/design/index.d.ts +5 -0
  22. package/src/lib/design/index.js +9 -0
  23. package/src/lib/design/index.js.map +1 -0
  24. package/src/lib/design/paths.d.ts +24 -0
  25. package/src/lib/design/paths.js +31 -0
  26. package/src/lib/design/paths.js.map +1 -0
  27. package/src/lib/design/serve.d.ts +7 -0
  28. package/src/lib/design/serve.js +144 -0
  29. package/src/lib/design/serve.js.map +1 -0
  30. package/src/lib/index.d.ts +1 -0
  31. package/src/lib/index.js +1 -0
  32. package/src/lib/index.js.map +1 -1
  33. package/src/lib/release.js +21 -3
  34. package/src/lib/release.js.map +1 -1
  35. package/src/lib/update/packages.d.ts +28 -8
  36. package/src/lib/update/packages.js +95 -36
  37. package/src/lib/update/packages.js.map +1 -1
  38. package/src/lib/update/plan.d.ts +5 -6
  39. package/src/lib/update/plan.js +22 -4
  40. package/src/lib/update/plan.js.map +1 -1
  41. package/src/lib/update/update-command.js +16 -11
  42. package/src/lib/update/update-command.js.map +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @ethlete/cli
2
2
 
3
+ ## 2.1.0-next.9
4
+
5
+ ### Minor Changes
6
+
7
+ - `et update` now writes the new range into every `package.json` in the repo, not only the root one, and refuses a dist tag that points at an older version than the repo is on.
8
+ - `et design [checkout]` serves the design page of any repository that keeps a `.ethlete/design`
9
+ folder, and `et design check` says why a call does not render.
10
+
11
+ ### Patch Changes
12
+
13
+ - `et release` now works with Changesets 3 and aborts when there are no pending changesets.
14
+ - `et design` now rules a drawing with its own Tailwind 4 pipeline, so a repository on Tailwind 3, or none at all, can hold design calls.
15
+ - `et design check --call` now reports `NO BROWSER` when neither the checkout nor the package
16
+ resolves `playwright`, instead of throwing the module loader's own error.
17
+
3
18
  ## 2.1.0-next.8
4
19
 
5
20
  ### Minor Changes
@@ -0,0 +1,92 @@
1
+ /**
2
+ * A value inside a template. An array joins with nothing between, and `null`, `undefined` and
3
+ * `false` write nothing, so `${rows.map(row)}` and `${isOn && html`…`}` both read as they look.
4
+ */
5
+ const flatten = (value: unknown): string => {
6
+ if (Array.isArray(value)) return value.map(flatten).join('');
7
+ if (value === null || value === undefined || value === false) return '';
8
+
9
+ return String(value);
10
+ };
11
+
12
+ const join = (strings: TemplateStringsArray, values: unknown[]) =>
13
+ strings.reduce((out, part, index) => out + flatten(values[index - 1]) + part);
14
+
15
+ /**
16
+ * Marks a template literal as HTML, so Prettier formats it and an editor highlights it. It escapes
17
+ * nothing: a drawing writes its own markup, and nothing a call draws comes from outside the
18
+ * repository.
19
+ */
20
+ export const html = (strings: TemplateStringsArray, ...values: unknown[]) => join(strings, values);
21
+
22
+ /** Marks a template literal as CSS, for the same reason `html` does it for markup. */
23
+ export const css = (strings: TemplateStringsArray, ...values: unknown[]) => join(strings, values);
24
+
25
+ /**
26
+ * One drawn answer, as the frame renders it: the markup that goes in the frame root, and the CSS
27
+ * that styles it. A drawing runs no logic, so it is a value the module builds when the frame
28
+ * imports it, not a component the frame has to instantiate.
29
+ */
30
+ export type Drawing = {
31
+ body: string;
32
+ styles: string;
33
+ };
34
+
35
+ /** Types a drawing where it is written, so a mistake reads in its own file and not in `call.ts`. */
36
+ export const drawing = (drawn: Drawing): Drawing => drawn;
37
+
38
+ /**
39
+ * One drawn answer to a call. `load` resolves the module whose default export is the
40
+ * drawing that answers it, so a broken option breaks its own frame only.
41
+ */
42
+ export type CallOption = {
43
+ key: string;
44
+ name: string;
45
+ /** Left out by a view: a single drawing answers no question, so it argues nothing. */
46
+ claim?: string;
47
+ cost?: string;
48
+ /** Left out while the call is open. The user sets it, never the drawing. */
49
+ verdict?: 'chosen' | 'rejected';
50
+ /** The `key` of the round that drew it. Left out by a call that runs no rounds. */
51
+ round?: string;
52
+ load: () => Promise<{ default: Drawing }>;
53
+ };
54
+
55
+ /**
56
+ * One pass over a call: the options drawn together, and what came out of them. A round
57
+ * whose options all carry a verdict is settled, and the page folds it down to its winner.
58
+ */
59
+ export type CallRound = {
60
+ key: string;
61
+ /** What this pass asked, in a few words. */
62
+ title: string;
63
+ /** What it answered. Write it once the user has ruled, so the next round reads as a reply. */
64
+ note: string;
65
+ };
66
+
67
+ /**
68
+ * How a call is drawn. A wireframe shows the bare workflow with mocked values, and draws no
69
+ * logic and no interaction state. A design call draws the real thing.
70
+ */
71
+ export type CallMode = 'wireframe' | 'design';
72
+
73
+ /**
74
+ * One open question of an exploration, with every option drawn at the same geometry.
75
+ * A call with one option and no claim is a view: one reference picture, drawn full width.
76
+ */
77
+ export type Call = {
78
+ /** The feature this call belongs to, for example 'the hour strip'. Left out by a loose call. */
79
+ feature?: string;
80
+ eyebrow: string;
81
+ headline: string;
82
+ intro: string;
83
+ /** The width every option frame gets, in px. The geometry the thing ships in. */
84
+ frameWidth: number;
85
+ /** Left out by a call that draws the real thing, which is what `design` means. */
86
+ mode?: CallMode;
87
+ /** Left out by a short call. With rounds, the intro says only what the call is about. */
88
+ rounds?: CallRound[];
89
+ options: CallOption[];
90
+ };
91
+
92
+ export const defineCall = (call: Call): Call => call;
@@ -0,0 +1,11 @@
1
+ declare module 'virtual:design-explore' {
2
+ import type { Call } from '@design-explore';
3
+
4
+ export const calls: Record<string, () => Promise<{ default: Call }>>;
5
+ export const defaultCall: string | null;
6
+ }
7
+
8
+ declare module 'virtual:design-explore/env' {
9
+ /** Loads the stylesheet the project draws under. A project the config names not draws bare. */
10
+ export const loadEnv: (project: string) => Promise<unknown>;
11
+ }
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <!--head-->
6
+ <style>
7
+ html,
8
+ body {
9
+ margin: 0;
10
+ }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ <script type="module" src="./frame.ts"></script>
16
+ </body>
17
+ </html>
@@ -0,0 +1,38 @@
1
+ import { calls } from 'virtual:design-explore';
2
+ import { loadEnv } from 'virtual:design-explore/env';
3
+
4
+ const params = new URLSearchParams(location.search);
5
+ const slug = params.get('call') ?? '';
6
+ const key = params.get('option') ?? '';
7
+
8
+ await loadEnv(slug.split('/')[0] ?? '');
9
+
10
+ const root = document.querySelector('#root') as HTMLElement;
11
+
12
+ const fail = (message: string) => {
13
+ root.textContent = message;
14
+ root.setAttribute('style', 'padding:16px;font:14px monospace;color:#f0a0a0;background:#1a1414');
15
+ };
16
+
17
+ const call = calls[slug];
18
+ if (!call) fail(`design-explore: no call "${slug}"`);
19
+ else {
20
+ const option = (await call()).default.options.find((o) => o.key === key);
21
+ if (!option) fail(`design-explore: call "${slug}" has no option "${key}"`);
22
+ else {
23
+ const loaded = (await option.load()).default;
24
+ const style = document.createElement('style');
25
+
26
+ style.textContent = loaded.styles;
27
+ document.head.append(style);
28
+ root.innerHTML = loaded.body;
29
+ }
30
+ }
31
+
32
+ const report = () => {
33
+ const height = Math.ceil(document.documentElement.getBoundingClientRect().height);
34
+ parent.postMessage({ type: 'design-explore:height', option: key, height }, '*');
35
+ };
36
+
37
+ new ResizeObserver(report).observe(document.documentElement);
38
+ report();
@@ -0,0 +1,494 @@
1
+ :root {
2
+ --h-ground: #131313;
3
+ --h-panel: #1b1b1b;
4
+ --h-ink: #e6e2da;
5
+ --h-ink-3: #8b8578;
6
+ --h-line: #2c2c2c;
7
+ --h-prose: 21rem;
8
+ }
9
+
10
+ body {
11
+ display: grid;
12
+ margin: 0;
13
+ grid-template-columns: 18rem 1fr;
14
+ min-height: 100vh;
15
+ background: var(--h-ground);
16
+ color: var(--h-ink);
17
+ font:
18
+ 300 14px/1.5 system-ui,
19
+ sans-serif;
20
+ }
21
+
22
+ /* The tree stands still while the calls scroll, and scrolls on its own when it is longer than the window. */
23
+ nav {
24
+ position: sticky;
25
+ top: 0;
26
+ display: flex;
27
+ height: 100vh;
28
+ box-sizing: border-box;
29
+ flex-direction: column;
30
+ align-self: start;
31
+ gap: 20px;
32
+ padding: 24px 12px;
33
+ border-right: 1px solid var(--h-line);
34
+ overflow-y: auto;
35
+ overscroll-behavior: contain;
36
+ }
37
+
38
+ nav section {
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: 2px;
42
+ }
43
+
44
+ nav > section > h3 {
45
+ margin: 0 0 4px 12px;
46
+ color: var(--h-ink-3);
47
+ font-family: ui-monospace, monospace;
48
+ font-size: 10px;
49
+ font-weight: 400;
50
+ letter-spacing: 0.2em;
51
+ text-transform: uppercase;
52
+ }
53
+
54
+ nav a {
55
+ padding: 8px 12px;
56
+ border-radius: 6px;
57
+ color: var(--h-ink-3);
58
+ text-decoration: none;
59
+ font-family: ui-monospace, monospace;
60
+ font-size: 12px;
61
+ }
62
+
63
+ nav a:hover {
64
+ background: var(--h-panel);
65
+ }
66
+
67
+ nav a[aria-current] {
68
+ background: var(--h-panel);
69
+ color: var(--h-ink);
70
+ }
71
+
72
+ .nav-rounds {
73
+ display: flex;
74
+ flex-direction: column;
75
+ margin: 2px 0 6px 12px;
76
+ border-left: 1px solid var(--h-line);
77
+ }
78
+
79
+ .nav-rounds a {
80
+ padding: 5px 10px;
81
+ border-radius: 0 6px 6px 0;
82
+ font-size: 11px;
83
+ }
84
+
85
+ main {
86
+ display: flex;
87
+ flex-direction: column;
88
+ gap: 56px;
89
+ padding: 48px 56px;
90
+ overflow-x: auto;
91
+ }
92
+
93
+ .round {
94
+ display: flex;
95
+ flex-direction: column;
96
+ gap: 32px;
97
+ scroll-margin-top: 24px;
98
+ }
99
+
100
+ .round-head {
101
+ display: flex;
102
+ max-width: 68ch;
103
+ flex-direction: column;
104
+ gap: 8px;
105
+ padding-top: 24px;
106
+ border-top: 1px solid var(--h-line);
107
+ }
108
+
109
+ .round-head h2 {
110
+ margin: 0;
111
+ font-size: 19px;
112
+ font-weight: 300;
113
+ }
114
+
115
+ .round-head p {
116
+ margin: 0;
117
+ color: var(--h-ink-3);
118
+ text-wrap: pretty;
119
+ }
120
+
121
+ .views {
122
+ display: flex;
123
+ margin-top: 4px;
124
+ gap: 2px;
125
+ }
126
+
127
+ .views a {
128
+ padding: 5px 12px;
129
+ border: 1px solid var(--h-line);
130
+ color: var(--h-ink-3);
131
+ text-decoration: none;
132
+ font-family: ui-monospace, monospace;
133
+ font-size: 11px;
134
+ letter-spacing: 0.12em;
135
+ }
136
+
137
+ .views a:first-child {
138
+ border-radius: 6px 0 0 6px;
139
+ }
140
+
141
+ .views a:last-child {
142
+ border-radius: 0 6px 6px 0;
143
+ }
144
+
145
+ .views a[aria-current] {
146
+ background: var(--h-panel);
147
+ color: var(--h-ink);
148
+ }
149
+
150
+ /* The result is the end of the chain, so the chain itself sits under it as a line of keys. */
151
+ .trail {
152
+ display: flex;
153
+ align-items: center;
154
+ margin-top: 4px;
155
+ gap: 8px;
156
+ }
157
+
158
+ .trail a {
159
+ padding: 4px 9px;
160
+ border: 1px solid var(--h-line);
161
+ border-radius: 5px;
162
+ color: var(--h-ink-3);
163
+ text-decoration: none;
164
+ font-family: ui-monospace, monospace;
165
+ font-size: 11px;
166
+ letter-spacing: 0.1em;
167
+ }
168
+
169
+ .trail a:hover {
170
+ color: var(--h-ink);
171
+ }
172
+
173
+ .trail a[aria-current] {
174
+ border-color: #7fd4bf;
175
+ color: #7fd4bf;
176
+ }
177
+
178
+ .arrow {
179
+ color: var(--h-line);
180
+ font-size: 12px;
181
+ }
182
+
183
+ .result .round-head,
184
+ .tray .round-head {
185
+ border-top-color: var(--h-ink-3);
186
+ }
187
+
188
+ .fold {
189
+ align-self: start;
190
+ margin-top: 4px;
191
+ color: var(--h-ink-3);
192
+ text-decoration: none;
193
+ font-family: ui-monospace, monospace;
194
+ font-size: 11px;
195
+ letter-spacing: 0.1em;
196
+ }
197
+
198
+ .fold::before {
199
+ content: '\203A\00a0';
200
+ }
201
+
202
+ .fold:hover {
203
+ color: var(--h-ink);
204
+ }
205
+
206
+ /* A folded option keeps its name on the page, so the thread of the round is still readable. */
207
+ .folded {
208
+ display: flex;
209
+ max-width: 68ch;
210
+ flex-direction: column;
211
+ gap: 2px;
212
+ margin: 0;
213
+ padding: 0;
214
+ list-style: none;
215
+ }
216
+
217
+ .folded li {
218
+ display: flex;
219
+ align-items: baseline;
220
+ justify-content: space-between;
221
+ gap: 16px;
222
+ padding: 7px 10px;
223
+ border-radius: 6px;
224
+ opacity: 0.55;
225
+ }
226
+
227
+ .folded li[data-verdict='chosen'] .tag {
228
+ color: #7fd4bf;
229
+ }
230
+
231
+ .folded li:hover {
232
+ background: var(--h-panel);
233
+ opacity: 1;
234
+ }
235
+
236
+ .folded a {
237
+ color: var(--h-ink);
238
+ text-decoration: none;
239
+ font-family: ui-monospace, monospace;
240
+ font-size: 11px;
241
+ letter-spacing: 0.12em;
242
+ text-transform: uppercase;
243
+ }
244
+
245
+ header {
246
+ display: flex;
247
+ max-width: 68ch;
248
+ flex-direction: column;
249
+ gap: 12px;
250
+ }
251
+
252
+ .eyebrow {
253
+ color: var(--h-ink-3);
254
+ font-family: ui-monospace, monospace;
255
+ font-size: 11px;
256
+ letter-spacing: 0.24em;
257
+ text-transform: uppercase;
258
+ }
259
+
260
+ h1 {
261
+ margin: 0;
262
+ font-size: 28px;
263
+ font-weight: 300;
264
+ }
265
+
266
+ header p {
267
+ margin: 0;
268
+ color: var(--h-ink-3);
269
+ text-wrap: pretty;
270
+ }
271
+
272
+ /*
273
+ * A track is as wide as the frame it holds, so the options wrap to the next band when the window
274
+ * runs out of room. Row flow is what lets them wrap: each column takes four rows, so the next one
275
+ * starts beside it and only a full band pushes to a new line.
276
+ */
277
+ .grid {
278
+ display: grid;
279
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--frame-width, 0px)), 1fr));
280
+ grid-auto-flow: row;
281
+ gap: 72px 40px;
282
+ }
283
+
284
+ .column {
285
+ display: grid;
286
+ grid-row: span 4;
287
+ grid-template-rows: subgrid;
288
+ gap: 12px;
289
+ }
290
+
291
+ /* A view is one picture, so it gets the whole width and no subgrid. */
292
+ .grid[data-view] .column {
293
+ grid-row: auto;
294
+ grid-template-rows: none;
295
+ }
296
+
297
+ /* A column can be wider than a line of text should be, so the text keeps its own measure. */
298
+ .column h3,
299
+ .claim,
300
+ .cost {
301
+ max-width: var(--h-prose);
302
+ }
303
+
304
+ .stage {
305
+ display: flex;
306
+ padding: 24px 0;
307
+ justify-content: flex-start;
308
+ }
309
+
310
+ .column h3 {
311
+ display: flex;
312
+ align-items: baseline;
313
+ justify-content: space-between;
314
+ gap: 16px;
315
+ margin: 0;
316
+ font-family: ui-monospace, monospace;
317
+ font-size: 11px;
318
+ font-weight: 500;
319
+ letter-spacing: 0.18em;
320
+ text-transform: uppercase;
321
+ }
322
+
323
+ .column h3 a {
324
+ color: var(--h-ink);
325
+ text-decoration: none;
326
+ }
327
+
328
+ /*
329
+ * Two 1100px frames never fit a screen side by side, and shrinking them hides the pixels the
330
+ * comparison is about. The picks are stacked in one box at full size instead.
331
+ */
332
+ .overlay {
333
+ position: relative;
334
+ overflow: hidden;
335
+ cursor: crosshair;
336
+ }
337
+
338
+ .overlay iframe {
339
+ position: absolute;
340
+ top: 0;
341
+ left: 0;
342
+ pointer-events: none;
343
+ }
344
+
345
+ .seam {
346
+ position: absolute;
347
+ top: 0;
348
+ bottom: 0;
349
+ width: 1px;
350
+ background: #7fd4bf;
351
+ }
352
+
353
+ .switch {
354
+ display: flex;
355
+ align-items: center;
356
+ margin-top: 4px;
357
+ gap: 6px;
358
+ }
359
+
360
+ .switch button {
361
+ padding: 5px 11px;
362
+ border: 1px solid var(--h-line);
363
+ border-radius: 5px;
364
+ background: none;
365
+ color: var(--h-ink-3);
366
+ font-family: ui-monospace, monospace;
367
+ font-size: 11px;
368
+ letter-spacing: 0.1em;
369
+ cursor: pointer;
370
+ }
371
+
372
+ .switch button[aria-current] {
373
+ border-color: #7fd4bf;
374
+ color: #7fd4bf;
375
+ }
376
+
377
+ .hint {
378
+ margin-left: 8px;
379
+ color: var(--h-ink-3);
380
+ font-size: 11px;
381
+ }
382
+
383
+ .grid.sheet iframe {
384
+ display: block;
385
+ transform: scale(var(--de-scale, 1));
386
+ transform-origin: top left;
387
+ }
388
+
389
+ /* The contact sheet draws every option at once, so one screen holds the whole call. */
390
+ .grid.sheet {
391
+ --thumb-width: calc(var(--frame-width) * var(--de-scale));
392
+
393
+ grid-template-columns: repeat(auto-fill, minmax(var(--thumb-width), max-content));
394
+ gap: 28px 20px;
395
+ }
396
+
397
+ .thumb {
398
+ display: flex;
399
+ width: var(--thumb-width);
400
+ flex-direction: column;
401
+ gap: 8px;
402
+ color: inherit;
403
+ text-decoration: none;
404
+ }
405
+
406
+ .thumb-frame {
407
+ overflow: hidden;
408
+ width: 100%;
409
+ border: 1px solid var(--h-line);
410
+ border-radius: 4px;
411
+ }
412
+
413
+ /* A thumbnail is read, never driven, and a click on it belongs to the link around it. */
414
+ .thumb-frame iframe {
415
+ pointer-events: none;
416
+ }
417
+
418
+ .thumb-name {
419
+ font-family: ui-monospace, monospace;
420
+ font-size: 10px;
421
+ letter-spacing: 0.12em;
422
+ text-transform: uppercase;
423
+ }
424
+
425
+ .thumb[data-verdict='rejected'] {
426
+ opacity: 0.45;
427
+ transition: opacity 0.15s;
428
+ }
429
+
430
+ .thumb:hover {
431
+ opacity: 1;
432
+ }
433
+
434
+ .thumb[data-verdict='chosen'] .thumb-name {
435
+ color: #7fd4bf;
436
+ }
437
+
438
+ .thumb[data-picked] .thumb-frame,
439
+ .column[data-picked] .stage {
440
+ outline: 1px solid #7fd4bf;
441
+ outline-offset: 4px;
442
+ }
443
+
444
+ /* A rejected option stays readable, and comes back to full strength under the pointer. */
445
+ .column[data-verdict='rejected'] {
446
+ opacity: 0.45;
447
+ transition: opacity 0.15s;
448
+ }
449
+
450
+ .column[data-verdict='rejected']:hover {
451
+ opacity: 1;
452
+ }
453
+
454
+ [data-verdict='chosen'] .tag {
455
+ color: #7fd4bf;
456
+ }
457
+
458
+ .tag {
459
+ color: var(--h-ink-3);
460
+ font-size: 10px;
461
+ letter-spacing: 0.16em;
462
+ }
463
+
464
+ iframe {
465
+ border: 0;
466
+ background: transparent;
467
+ }
468
+
469
+ .claim,
470
+ .cost {
471
+ margin: 0;
472
+ font-size: 12px;
473
+ line-height: 1.5;
474
+ text-wrap: pretty;
475
+ }
476
+
477
+ .cost {
478
+ color: var(--h-ink-3);
479
+ }
480
+
481
+ .problem {
482
+ margin: 0;
483
+ padding: 12px 16px;
484
+ border-radius: 6px;
485
+ background: #1a1414;
486
+ color: #f0a0a0;
487
+ font-family: ui-monospace, monospace;
488
+ font-size: 12px;
489
+ }
490
+
491
+ .empty {
492
+ padding: 40px;
493
+ color: var(--h-ink-3);
494
+ }