@hozu/devtools 0.15.0 → 0.17.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/dist/notes.d.ts CHANGED
@@ -9,6 +9,8 @@ export interface AgentNote {
9
9
  at: string | null;
10
10
  /** A page path where the target is shown, when one is known. */
11
11
  path: string | null;
12
+ /** Only the instances of the target whose text contains this (one row of a list), like browse's `in "<text>"`. */
13
+ within: string | null;
12
14
  text: string;
13
15
  created: string;
14
16
  }
@@ -1 +1 @@
1
- {"version":3,"file":"notes.d.ts","sourceRoot":"","sources":["../src/notes.ts"],"names":[],"mappings":"AAGA,kEAAkE;AAClE,MAAM,WAAW,SAAS;IACxB,CAAC,EAAE,MAAM,CAAA;IACT,mFAAmF;IACnF,EAAE,EAAE,MAAM,CAAA;IACV,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAA;IACb,6CAA6C;IAC7C,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACjB,gEAAgE;IAChE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAOD,eAAO,MAAM,SAAS,qBAAqB,CAAA;AAoB3C,eAAO,MAAM,SAAS,SAAU,MAAM,KAAG,SAAS,EAAsB,CAAA;AAExE,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,GAAG,SAAS,CAAC,EAAE,GAAG,OAAa,GAAG,SAAS,CAKzG;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAM7D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED,yGAAyG;AACzG,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAcpE"}
1
+ {"version":3,"file":"notes.d.ts","sourceRoot":"","sources":["../src/notes.ts"],"names":[],"mappings":"AAGA,kEAAkE;AAClE,MAAM,WAAW,SAAS;IACxB,CAAC,EAAE,MAAM,CAAA;IACT,mFAAmF;IACnF,EAAE,EAAE,MAAM,CAAA;IACV,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAA;IACb,6CAA6C;IAC7C,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACjB,gEAAgE;IAChE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,kHAAkH;IAClH,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAOD,eAAO,MAAM,SAAS,qBAAqB,CAAA;AAoB3C,eAAO,MAAM,SAAS,SAAU,MAAM,KAAG,SAAS,EAAsB,CAAA;AAExE,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,GAAG,SAAS,CAAC,EAAE,GAAG,OAAa,GAAG,SAAS,CAKzG;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAM7D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED,yGAAyG;AACzG,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAepE"}
package/dist/notes.js CHANGED
@@ -49,6 +49,7 @@ export function replyMarkdown(note, reply) {
49
49
  `- Note: ${note.text}`,
50
50
  note.at ? `- Where: \`${note.at}:1\`` : `- Where: ${note.id}`,
51
51
  ...(note.path ? [`- Page: ${note.path}`] : []),
52
+ ...(note.within ? [`- In: "${note.within}"`] : []),
52
53
  `- Reply: ${reply}`,
53
54
  '',
54
55
  `The note stays on the page until \`hozu show --done ${note.n}\` removes it.`,
package/dist/notes.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"notes.js","sourceRoot":"","sources":["../src/notes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"notes.js","sourceRoot":"","sources":["../src/notes.ts"],"names":[],"mappings":"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"}
@@ -1,11 +1,13 @@
1
1
  import { describeFor, friendlyName, questionFor } from '../plain.js';
2
2
  import { joinRequests, labelOf, openRequestsLine, requestMarkdown, scopesFor, } from '../prompt.js';
3
3
  import { held, hold, node, one, page, remove, save, saved, theme, tree } from './api.js';
4
+ import { assetsBoard } from './assets.js';
4
5
  import { h, read, write } from './dom.js';
5
6
  import { drawer } from './effects.js';
6
7
  import { previewLabel, renderLayers } from './layers.js';
7
8
  import { logo } from './logo.js';
8
9
  import { lookSection, preview } from './look.js';
10
+ import { distances, sizeOf } from './measure.js';
9
11
  import { agentNotes } from './notes.js';
10
12
  import { css, outlineCss } from './style.js';
11
13
  import { firstText, previewText, restoreText, textSection } from './text.js';
@@ -31,6 +33,9 @@ const state = {
31
33
  };
32
34
  const persist = () => write(key, state);
33
35
  const host = document.createElement('hozu-devtools');
36
+ host.setAttribute('data-lenis-prevent', '');
37
+ for (const type of ['wheel', 'touchstart', 'touchmove', 'keydown'])
38
+ host.addEventListener(type, (event) => event.stopPropagation(), { passive: true });
34
39
  const shadow = host.attachShadow({ mode: 'open' });
35
40
  const root = h('div', { class: 'root' });
36
41
  const hover = h('div', { class: 'box', hidden: true }, [h('div', { class: 'tag' })]);
@@ -59,7 +64,119 @@ const api = drawer({
59
64
  },
60
65
  closed: () => { },
61
66
  });
62
- root.append(bench, panel, dock, api.el);
67
+ const screenCookie = 'hozu-dev-preview';
68
+ const screenOf = () => {
69
+ const raw = new RegExp(`(?:^|;\\s*)${screenCookie}=([^;]+)`).exec(document.cookie)?.[1];
70
+ try {
71
+ return raw ? decodeURIComponent(raw) : null;
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ };
77
+ const setScreen = (value) => {
78
+ document.cookie = value
79
+ ? `${screenCookie}=${encodeURIComponent(value)}; path=/; SameSite=Lax`
80
+ : `${screenCookie}=; path=/; max-age=0; SameSite=Lax`;
81
+ };
82
+ const showKey = 'hozu-devtools:assets-show';
83
+ function showInstances(c) {
84
+ const here = c.uses.flatMap((u) => [...doc.querySelectorAll(`[data-hz="${CSS.escape(u.node)}"]`)]);
85
+ if (!here.length) {
86
+ const page = c.uses.flatMap((u) => u.pages).find((p) => !p.params);
87
+ if (!page)
88
+ return;
89
+ try {
90
+ sessionStorage.setItem(showKey, c.id);
91
+ }
92
+ catch { }
93
+ win.location.href = page.path;
94
+ return;
95
+ }
96
+ assets.close();
97
+ state.mode = 'select';
98
+ state.picks = [
99
+ ...state.picks.filter(hasContent),
100
+ ...here.map((el) => ({
101
+ id: el.getAttribute('data-hz') ?? '',
102
+ index: indexOf(el),
103
+ note: '',
104
+ scope: 'component',
105
+ visible: visibleOf(el),
106
+ })),
107
+ ];
108
+ state.active = state.picks.length - 1;
109
+ state.panel = 'inspector';
110
+ persist();
111
+ renderDock();
112
+ drawPicks();
113
+ void renderPanel();
114
+ }
115
+ const assets = assetsBoard({
116
+ plain: state.audience === 'builder',
117
+ theme: () => theme(),
118
+ stylesheets: () => [...doc.querySelectorAll('link[rel="stylesheet"]')].map((l) => l.href).filter(Boolean),
119
+ show: (c) => showInstances(c),
120
+ change: (c, note) => {
121
+ const first = c.uses[0];
122
+ if (!first)
123
+ return;
124
+ state.picks.push({ id: first.node, index: 0, note, scope: 'component', visible: '' });
125
+ state.active = state.picks.length - 1;
126
+ persist();
127
+ assets.close();
128
+ renderDock();
129
+ open('changes', null, 'draft');
130
+ },
131
+ screen: (route, path, name) => {
132
+ setScreen(`${route}:${name}`);
133
+ if (win.location.pathname === path)
134
+ win.location.reload();
135
+ else
136
+ win.location.href = path;
137
+ },
138
+ });
139
+ async function screensSection() {
140
+ const res = await fetch(`/_hozu/dev/previews?path=${encodeURIComponent(win.location.pathname)}`, {
141
+ cache: 'no-store',
142
+ }).catch(() => null);
143
+ const out = res?.ok
144
+ ? (await res.json())
145
+ : null;
146
+ if (!out?.route || !out.previews.length)
147
+ return null;
148
+ return h('div', { class: 'sec' }, [
149
+ h('div', { class: 'label' }, ['Previews (previews.ts)']),
150
+ ...out.previews.map((p) => {
151
+ const value = `${out.route}:${p.name}`;
152
+ const on = out.current === value;
153
+ return h('div', { class: 'look' }, [
154
+ h('span', { class: 'what' }, [p.name]),
155
+ h('button', {
156
+ class: on ? 'link done' : 'link',
157
+ type: 'button',
158
+ onclick: () => {
159
+ setScreen(on ? null : value);
160
+ win.location.reload();
161
+ },
162
+ }, [on ? 'Exit' : 'Preview']),
163
+ ]);
164
+ }),
165
+ ]);
166
+ }
167
+ root.append(bench, panel, dock, api.el, assets.board);
168
+ try {
169
+ const pending = sessionStorage.getItem(showKey);
170
+ if (pending) {
171
+ sessionStorage.removeItem(showKey);
172
+ void assets.catalog().then((list) => {
173
+ const c = list?.find((x) => x.id === pending);
174
+ if (c)
175
+ setTimeout(() => showInstances(c), 300);
176
+ });
177
+ }
178
+ }
179
+ catch { }
63
180
  document.documentElement.append(host);
64
181
  const darkScheme = matchMedia('(prefers-color-scheme: dark)');
65
182
  function applyTheme() {
@@ -72,6 +189,8 @@ let win = window;
72
189
  let frameEl = null;
73
190
  const frames = h('div');
74
191
  let outline = null;
192
+ let measuring = false;
193
+ const measure = h('div', { class: 'measure', hidden: true });
75
194
  const agent = agentNotes({
76
195
  doc: () => doc,
77
196
  win: () => win,
@@ -89,7 +208,7 @@ function mountOutline(target) {
89
208
  outline?.remove();
90
209
  const el = target.createElement('hozu-devtools-outline');
91
210
  el.style.cssText = 'all:initial;position:fixed;inset:0;pointer-events:none;z-index:2147483646';
92
- el.attachShadow({ mode: 'open' }).append(h('style', {}, [outlineCss]), frames, boxes, agent.layer, hover);
211
+ el.attachShadow({ mode: 'open' }).append(h('style', {}, [outlineCss]), frames, boxes, agent.layer, hover, measure);
93
212
  target.documentElement.append(el);
94
213
  outline = el;
95
214
  }
@@ -131,6 +250,9 @@ function place(box, el) {
131
250
  target.style.width = `${r.width + 4}px`;
132
251
  target.style.height = `${r.height + 4}px`;
133
252
  }
253
+ const size = box.querySelector(':scope > .size');
254
+ if (size)
255
+ size.textContent = sizeOf(r);
134
256
  const tag = box.firstElementChild;
135
257
  if (!tag?.classList.contains('tag'))
136
258
  return;
@@ -175,6 +297,37 @@ async function showHover(el, force = false) {
175
297
  if (hovered === el)
176
298
  tag.replaceChildren(...tagText(n, el));
177
299
  }
300
+ function drawMeasure() {
301
+ const active = state.picks[state.active];
302
+ const from = active ? elementOf(active) : hovered;
303
+ const to = active ? hovered : (hovered?.parentElement?.closest('[data-hz]') ?? null);
304
+ if (!measuring || state.mode !== 'select' || !from || !to || from === to) {
305
+ measure.hidden = true;
306
+ return;
307
+ }
308
+ const a = from.getBoundingClientRect();
309
+ const b = to.getBoundingClientRect();
310
+ const px = (n) => `${n}px`;
311
+ const target = h('div', { class: 'm-target' });
312
+ Object.assign(target.style, { left: px(b.left), top: px(b.top), width: px(b.width), height: px(b.height) });
313
+ const parts = [target];
314
+ for (const l of distances(a, b)) {
315
+ const line = h('div', { class: `m-line ${l.axis}` });
316
+ const label = h('div', { class: 'm-label' }, [String(Math.round(l.to - l.from))]);
317
+ const mid = (l.from + l.to) / 2;
318
+ if (l.axis === 'h') {
319
+ Object.assign(line.style, { left: px(l.from), top: px(l.at), width: px(l.to - l.from) });
320
+ Object.assign(label.style, { left: px(mid), top: px(l.at) });
321
+ }
322
+ else {
323
+ Object.assign(line.style, { left: px(l.at), top: px(l.from), height: px(l.to - l.from) });
324
+ Object.assign(label.style, { left: px(l.at), top: px(mid) });
325
+ }
326
+ parts.push(line, label);
327
+ }
328
+ measure.replaceChildren(...parts);
329
+ measure.hidden = false;
330
+ }
178
331
  function drawPicks() {
179
332
  for (const p of state.picks) {
180
333
  preview(elementOf(p), p.style ?? []);
@@ -185,6 +338,7 @@ function drawPicks() {
185
338
  boxes.replaceChildren(...state.picks.map((p, i) => {
186
339
  const box = framed(h('div', { class: 'box selected' }, [
187
340
  state.picks.length > 1 ? h('div', { class: 'badge' }, [String(i + 1)]) : null,
341
+ h('div', { class: 'size' }),
188
342
  ]), true);
189
343
  place(box, state.mode === 'select' ? elementOf(p) : null);
190
344
  return box;
@@ -203,6 +357,7 @@ function frame() {
203
357
  }
204
358
  if (hovered && (state.mode === 'select' || forced))
205
359
  place(hover, hovered);
360
+ drawMeasure();
206
361
  agent.frame();
207
362
  requestAnimationFrame(frame);
208
363
  }
@@ -272,12 +427,13 @@ function listen(target) {
272
427
  return;
273
428
  const mouse = event;
274
429
  const clicked = markedFrom(event);
275
- const el = mouse.altKey ? (clicked?.parentElement?.closest('[data-hz]') ?? clicked) : clicked;
430
+ const el = clicked;
276
431
  if (!el)
277
432
  return;
278
433
  select({ id: el.getAttribute('data-hz') ?? '', index: indexOf(el), visible: visibleOf(el) }, mouse.shiftKey);
279
434
  }, { capture: true, signal });
280
435
  target.addEventListener('pointermove', (event) => {
436
+ measuring = event.altKey;
281
437
  if (state.mode !== 'select')
282
438
  return;
283
439
  const el = ours(event) ? null : markedFrom(event);
@@ -285,30 +441,60 @@ function listen(target) {
285
441
  void showHover(el);
286
442
  }, { capture: true, passive: true, signal });
287
443
  target.addEventListener('keydown', onKey, { capture: true, signal });
444
+ target.addEventListener('keyup', (event) => {
445
+ measuring = event.altKey;
446
+ }, { capture: true, signal });
447
+ target.addEventListener('blur', () => {
448
+ measuring = false;
449
+ }, { signal });
288
450
  return stop;
289
451
  }
452
+ /** The marked parts directly inside `parent` (or the page), in document order. */
453
+ function partsIn(parent) {
454
+ const all = [...(parent ?? doc).querySelectorAll('[data-hz]')];
455
+ return all.filter((e) => (e.parentElement?.closest('[data-hz]') ?? null) === parent);
456
+ }
290
457
  function walk(direction) {
291
458
  const active = state.picks[state.active];
292
459
  if (!active)
293
460
  return;
294
461
  const el = elementOf(active);
295
462
  const own = el?.getAttribute('data-hz') === active.id;
463
+ const beside = () => {
464
+ if (!el || !own)
465
+ return null;
466
+ const list = partsIn(el.parentElement?.closest('[data-hz]') ?? null).filter((e) => {
467
+ const b = e.getBoundingClientRect();
468
+ return e === el || (b.width > 0 && b.height > 0);
469
+ });
470
+ const at = list.indexOf(el);
471
+ return list[(at + (direction === 'next' ? 1 : -1) + list.length) % list.length] ?? null;
472
+ };
296
473
  const next = direction === 'up'
297
474
  ? own
298
475
  ? el?.parentElement?.closest('[data-hz]')
299
476
  : el
300
- : own
301
- ? el?.querySelector('[data-hz]')
302
- : null;
303
- if (!next)
477
+ : direction === 'down'
478
+ ? own
479
+ ? el?.querySelector('[data-hz]')
480
+ : null
481
+ : beside();
482
+ if (!next || ((direction === 'next' || direction === 'previous') && next === el))
304
483
  return;
305
- state.picks[state.active] = {
306
- ...active,
307
- id: next.getAttribute('data-hz') ?? '',
308
- index: indexOf(next),
309
- visible: visibleOf(next),
310
- scope: null,
311
- };
484
+ const moved = { id: next.getAttribute('data-hz') ?? '', index: indexOf(next), visible: visibleOf(next) };
485
+ if (hasContent(active)) {
486
+ const existing = state.picks.findIndex((p) => p.id === moved.id && p.index === moved.index);
487
+ if (existing >= 0)
488
+ state.active = existing;
489
+ else {
490
+ state.picks.push({ ...moved, note: '', scope: null });
491
+ state.active = state.picks.length - 1;
492
+ }
493
+ }
494
+ else {
495
+ preview(el, []);
496
+ state.picks[state.active] = { ...moved, note: '', scope: null };
497
+ }
312
498
  persist();
313
499
  drawPicks();
314
500
  void renderPanel();
@@ -322,16 +508,19 @@ function setMode(mode) {
322
508
  persist();
323
509
  renderDock();
324
510
  }
511
+ const typingIn = (event) => event.composedPath().some((t) => ['TEXTAREA', 'INPUT', 'SELECT'].includes(t.tagName));
325
512
  function onKey(event) {
326
- const typing = event
327
- .composedPath()
328
- .some((t) => ['TEXTAREA', 'INPUT', 'SELECT'].includes(t.tagName));
513
+ const typing = typingIn(event);
514
+ if (event.key === 'Escape' && assets.isOpen())
515
+ return assets.close();
329
516
  if (event.altKey && event.shiftKey && event.code === 'KeyS') {
330
517
  event.preventDefault();
331
518
  return setMode(state.mode === 'select' ? 'browse' : 'select');
332
519
  }
333
520
  if (typing)
334
521
  return;
522
+ if (event.key === 'Escape' && bench.classList.contains('layers-open'))
523
+ return closeLayers();
335
524
  if (event.key === 'Escape' && state.mode === 'select')
336
525
  return setMode('browse');
337
526
  if (event.key === 'Escape' && state.panel) {
@@ -339,12 +528,27 @@ function onKey(event) {
339
528
  persist();
340
529
  return void renderPanel();
341
530
  }
342
- if (state.mode === 'select' &&
343
- (event.key === 'ArrowUp' || event.key === 'ArrowDown') &&
344
- state.picks.length) {
345
- event.preventDefault();
346
- walk(event.key === 'ArrowUp' ? 'up' : 'down');
531
+ if (event.key === 'Alt') {
532
+ measuring = true;
533
+ if (state.mode === 'select')
534
+ event.preventDefault();
535
+ return;
347
536
  }
537
+ if (state.mode !== 'select' || !state.picks.length || event.composedPath().includes(host))
538
+ return;
539
+ const step = event.key === 'ArrowUp' || (event.key === 'Enter' && event.shiftKey)
540
+ ? 'up'
541
+ : event.key === 'ArrowDown' || event.key === 'Enter'
542
+ ? 'down'
543
+ : event.key === 'Tab'
544
+ ? event.shiftKey
545
+ ? 'previous'
546
+ : 'next'
547
+ : null;
548
+ if (!step)
549
+ return;
550
+ event.preventDefault();
551
+ walk(step);
348
552
  }
349
553
  let listening = listen(window);
350
554
  const reload = () => win.location.reload();
@@ -439,6 +643,12 @@ function renderDock() {
439
643
  void renderPanel();
440
644
  },
441
645
  }, ['Layers']),
646
+ h('button', {
647
+ class: 'act',
648
+ type: 'button',
649
+ title: 'Every component of the app, its variants, previews and uses; and the design tokens',
650
+ onclick: () => void assets.open(),
651
+ }, ['Assets']),
442
652
  h('button', {
443
653
  class: 'act',
444
654
  type: 'button',
@@ -454,7 +664,7 @@ function renderDock() {
454
664
  state.panel = state.picks.length ? 'inspector' : null;
455
665
  openBench();
456
666
  },
457
- }, ['Workbench']),
667
+ }, ['Frame']),
458
668
  h('button', {
459
669
  class: 'act gear',
460
670
  type: 'button',
@@ -466,6 +676,17 @@ function renderDock() {
466
676
  void renderPanel();
467
677
  },
468
678
  }, ['⚙']),
679
+ screenOf()
680
+ ? h('button', {
681
+ class: 'previewing',
682
+ type: 'button',
683
+ title: 'Exit the preview from previews.ts',
684
+ onclick: () => {
685
+ setScreen(null);
686
+ win.location.reload();
687
+ },
688
+ }, [`Screen: ${screenOf().slice(screenOf().indexOf(':') + 1)}`, ' · Exit'])
689
+ : null,
469
690
  held()
470
691
  ? h('button', {
471
692
  class: 'previewing',
@@ -478,7 +699,8 @@ function renderDock() {
478
699
  ? h('div', { class: 'tip', role: 'note' }, [
479
700
  h('span', {}, [h('kbd', {}, ['Click']), ' a part to select it']),
480
701
  h('span', {}, [h('kbd', {}, ['Shift']), '+click adds another']),
481
- h('span', {}, [h('kbd', {}, ['Alt']), '+click picks its parent']),
702
+ h('span', {}, [h('kbd', {}, ['⇧ Enter']), ' picks its parent']),
703
+ h('span', {}, [h('kbd', {}, ['Alt']), ' measures']),
482
704
  h('span', {}, [h('kbd', {}, ['Esc']), ' stops']),
483
705
  ])
484
706
  : null,
@@ -978,8 +1200,13 @@ async function renderPanel() {
978
1200
  return renderSettings();
979
1201
  if (state.panel === 'notes')
980
1202
  return agent.render(panel);
981
- if (state.panel === 'layers')
982
- return renderLayers(panel, await tree(win.location.pathname), layersHost(() => open(state.picks.length ? 'inspector' : null)));
1203
+ if (state.panel === 'layers') {
1204
+ await renderLayers(panel, await tree(win.location.pathname), layersHost(() => open(state.picks.length ? 'inspector' : null)));
1205
+ const screens = await screensSection();
1206
+ if (screens)
1207
+ panel.append(screens);
1208
+ return;
1209
+ }
983
1210
  const active = state.picks[state.active];
984
1211
  if (state.panel !== 'inspector' || !active) {
985
1212
  panel.hidden = state.view !== 'workbench';
@@ -1064,7 +1291,7 @@ async function renderChanges() {
1064
1291
  const done = h('button', {
1065
1292
  class: 'link done',
1066
1293
  type: 'button',
1067
- title: 'Done or not needed: remove it',
1294
+ title: 'Resolved or not needed: remove it',
1068
1295
  onclick: async () => {
1069
1296
  if (!armed) {
1070
1297
  armed = true;
@@ -1074,7 +1301,7 @@ async function renderChanges() {
1074
1301
  await remove(r.number);
1075
1302
  void renderPanel();
1076
1303
  },
1077
- }, ['Done']);
1304
+ }, ['Resolve']);
1078
1305
  return h('div', { class: 'req' }, [
1079
1306
  h('span', { class: 'n' }, [r.number]),
1080
1307
  h('button', { class: 't open-req', type: 'button', onclick: () => open('changes', r.number, 'saved') }, [r.title]),
@@ -1087,7 +1314,7 @@ async function renderChanges() {
1087
1314
  message ? h('div', { class: 'notice' }, message) : null,
1088
1315
  ...(list.length
1089
1316
  ? [...list].reverse().map(row)
1090
- : [h('div', { class: 'empty' }, ['No open requests. Done ones are removed.'])]),
1317
+ : [h('div', { class: 'empty' }, ['No open requests. Resolved ones are removed.'])]),
1091
1318
  list.length
1092
1319
  ? h('div', { class: 'sec' }, [
1093
1320
  h('div', { class: 'choice' }, [
@@ -1199,7 +1426,7 @@ async function renderRequest(number) {
1199
1426
  renderDock();
1200
1427
  open('changes', null, 'saved');
1201
1428
  },
1202
- }, ['Done · remove']);
1429
+ }, ['Resolve · remove']);
1203
1430
  panel.replaceChildren(h('div', { class: 'head' }, [
1204
1431
  h('button', { class: 'back', type: 'button', onclick: () => open('changes', null, 'saved') }, [
1205
1432
  '← Saved',
@@ -1298,7 +1525,10 @@ function renderSettings() {
1298
1525
  ['Alt+Shift+S', 'Select on or off'],
1299
1526
  ['Click', 'Select'],
1300
1527
  ['Shift+click', 'Add to the request'],
1301
- ['Alt+click', 'Select the area around it'],
1528
+ ['Shift+Enter', 'Select the area around it'],
1529
+ ['Enter', 'Select inside it'],
1530
+ ['Tab / Shift+Tab', 'Next / previous part beside it'],
1531
+ ['Alt (hold)', 'Measure: W × H and the distance to the part under the pointer'],
1302
1532
  ['Double-click', 'Select a text'],
1303
1533
  ['↑ ↓', 'Around it / inside it'],
1304
1534
  ['Esc', 'Back to Browse'],
@@ -1315,6 +1545,10 @@ const left = h('div', { class: 'bench-left' });
1315
1545
  const stage = h('div', { class: 'stage' });
1316
1546
  const sizer = h('div', { class: 'sizer' });
1317
1547
  const bar = h('div', { class: 'bench-bar' });
1548
+ addEventListener('keydown', (event) => {
1549
+ if (state.view === 'workbench' && win !== window)
1550
+ onKey(event);
1551
+ }, true);
1318
1552
  function retarget(frame) {
1319
1553
  listening.abort();
1320
1554
  const w = frame?.contentWindow ?? null;
@@ -1375,6 +1609,15 @@ function renderBar() {
1375
1609
  bar.replaceChildren(...present([
1376
1610
  h('div', { class: 'grip' }, [h('img', { src: logo, alt: 'Hozu', width: '18', height: '18' })]),
1377
1611
  h('div', { class: 'seg' }, [mode('Browse', 'browse'), mode('Select', 'select')]),
1612
+ h('button', {
1613
+ class: 'act layers-toggle',
1614
+ type: 'button',
1615
+ 'aria-pressed': String(bench.classList.contains('layers-open')),
1616
+ onclick: () => {
1617
+ bench.classList.toggle('layers-open');
1618
+ renderBar();
1619
+ },
1620
+ }, ['Layers']),
1378
1621
  select,
1379
1622
  h('button', {
1380
1623
  class: 'act',
@@ -1408,11 +1651,21 @@ function renderBar() {
1408
1651
  'aria-pressed': api.isOpen() ? 'true' : 'false',
1409
1652
  onclick: () => api.toggle(),
1410
1653
  }, ['API']),
1411
- h('button', { class: 'act exit', type: 'button', onclick: () => closeBench() }, ['Exit workbench']),
1654
+ h('button', {
1655
+ class: 'act',
1656
+ type: 'button',
1657
+ title: 'Every component of the app, its variants, previews and uses; and the design tokens',
1658
+ onclick: () => void assets.open(),
1659
+ }, ['Assets']),
1660
+ h('button', { class: 'act exit', type: 'button', onclick: () => closeBench() }, ['Exit frame']),
1412
1661
  ]));
1413
1662
  }
1414
1663
  async function renderLeft() {
1415
- await renderLayers(left, await tree(win.location.pathname), layersHost(() => { }));
1664
+ await renderLayers(left, await tree(win.location.pathname), layersHost(closeLayers));
1665
+ }
1666
+ function closeLayers() {
1667
+ bench.classList.remove('layers-open');
1668
+ renderBar();
1416
1669
  }
1417
1670
  function openBench() {
1418
1671
  state.view = 'workbench';
@@ -1421,7 +1674,7 @@ function openBench() {
1421
1674
  bench.hidden = false;
1422
1675
  const frame = h('iframe', {
1423
1676
  src: location.href,
1424
- title: 'The page in Workbench',
1677
+ title: 'The page in Frame',
1425
1678
  'data-hozu-bench': true,
1426
1679
  });
1427
1680
  frame.addEventListener('load', () => {
@@ -1489,6 +1742,10 @@ function closeBench() {
1489
1742
  void renderPanel();
1490
1743
  }
1491
1744
  addEventListener('resize', layout);
1745
+ addEventListener('keydown', (event) => {
1746
+ if (event.key === 'Escape' && !typingIn(event) && bench.classList.contains('layers-open'))
1747
+ closeLayers();
1748
+ }, true);
1492
1749
  renderDock();
1493
1750
  drawPicks();
1494
1751
  void renderPanel();