@wildwinter/app-shell 0.37.0 → 0.39.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.
Files changed (144) hide show
  1. package/LICENSES.md +62 -0
  2. package/README.md +21 -0
  3. package/dist/about.css +13 -34
  4. package/dist/about.css.map +1 -1
  5. package/dist/anchored.css +5 -5
  6. package/dist/anchored.css.map +1 -1
  7. package/dist/app-store.cjs +15 -0
  8. package/dist/app-store.cjs.map +1 -1
  9. package/dist/app-store.d.cts +25 -1
  10. package/dist/app-store.d.ts +25 -1
  11. package/dist/app-store.js +14 -1
  12. package/dist/app-store.js.map +1 -1
  13. package/dist/chunk-2ECJUTSX.js +97 -0
  14. package/dist/chunk-2ECJUTSX.js.map +1 -0
  15. package/dist/chunk-2NMVIM54.js +55 -0
  16. package/dist/chunk-2NMVIM54.js.map +1 -0
  17. package/dist/chunk-C6UVQYKI.js +70 -0
  18. package/dist/chunk-C6UVQYKI.js.map +1 -0
  19. package/dist/chunk-IU7EY6BI.js +26 -0
  20. package/dist/chunk-IU7EY6BI.js.map +1 -0
  21. package/dist/chunk-K2AOLQW4.js +82 -0
  22. package/dist/chunk-K2AOLQW4.js.map +1 -0
  23. package/dist/chunk-KGAMP5LW.js +87 -0
  24. package/dist/chunk-KGAMP5LW.js.map +1 -0
  25. package/dist/chunk-L5HOBEKL.js +534 -0
  26. package/dist/chunk-L5HOBEKL.js.map +1 -0
  27. package/dist/chunk-P4G4ERG6.js +60 -0
  28. package/dist/chunk-P4G4ERG6.js.map +1 -0
  29. package/dist/chunk-P6WLAEKZ.js +71 -0
  30. package/dist/chunk-P6WLAEKZ.js.map +1 -0
  31. package/dist/chunk-S4KWQLYN.js +66 -0
  32. package/dist/chunk-S4KWQLYN.js.map +1 -0
  33. package/dist/chunk-XZYZXY7I.js +554 -0
  34. package/dist/chunk-XZYZXY7I.js.map +1 -0
  35. package/dist/comments.css +5 -3
  36. package/dist/comments.css.map +1 -1
  37. package/dist/confirm.css +1 -54
  38. package/dist/confirm.css.map +1 -1
  39. package/dist/context-menu.cjs +250 -0
  40. package/dist/context-menu.cjs.map +1 -0
  41. package/dist/context-menu.css +64 -0
  42. package/dist/context-menu.css.map +1 -0
  43. package/dist/context-menu.d.cts +25 -0
  44. package/dist/context-menu.d.ts +25 -0
  45. package/dist/context-menu.js +5 -0
  46. package/dist/context-menu.js.map +1 -0
  47. package/dist/controls.css +155 -0
  48. package/dist/controls.css.map +1 -0
  49. package/dist/dialog.cjs +222 -0
  50. package/dist/dialog.cjs.map +1 -0
  51. package/dist/dialog.css +55 -0
  52. package/dist/dialog.css.map +1 -0
  53. package/dist/dialog.d.cts +28 -0
  54. package/dist/dialog.d.ts +28 -0
  55. package/dist/dialog.js +5 -0
  56. package/dist/dialog.js.map +1 -0
  57. package/dist/floating.cjs +83 -0
  58. package/dist/floating.cjs.map +1 -0
  59. package/dist/floating.d.cts +25 -0
  60. package/dist/floating.d.ts +25 -0
  61. package/dist/floating.js +4 -0
  62. package/dist/floating.js.map +1 -0
  63. package/dist/identity.css +15 -50
  64. package/dist/identity.css.map +1 -1
  65. package/dist/index.cjs +1322 -258
  66. package/dist/index.cjs.map +1 -1
  67. package/dist/index.d.cts +349 -131
  68. package/dist/index.d.ts +349 -131
  69. package/dist/index.js +259 -808
  70. package/dist/index.js.map +1 -1
  71. package/dist/job.css +23 -0
  72. package/dist/job.css.map +1 -1
  73. package/dist/link-status.cjs +430 -0
  74. package/dist/link-status.cjs.map +1 -0
  75. package/dist/link-status.css +76 -0
  76. package/dist/link-status.css.map +1 -0
  77. package/dist/link-status.d.cts +53 -0
  78. package/dist/link-status.d.ts +53 -0
  79. package/dist/link-status.js +5 -0
  80. package/dist/link-status.js.map +1 -0
  81. package/dist/menu.cjs +62 -1
  82. package/dist/menu.cjs.map +1 -1
  83. package/dist/menu.d.cts +88 -1
  84. package/dist/menu.d.ts +88 -1
  85. package/dist/menu.js +1 -1
  86. package/dist/notes-editor.css +3 -46
  87. package/dist/notes-editor.css.map +1 -1
  88. package/dist/pane-shell.css +16 -3
  89. package/dist/pane-shell.css.map +1 -1
  90. package/dist/property-list-D3VZCJzi.d.cts +162 -0
  91. package/dist/property-list-D3VZCJzi.d.ts +162 -0
  92. package/dist/property-list.cjs +784 -0
  93. package/dist/property-list.cjs.map +1 -0
  94. package/dist/property-list.d.cts +1 -0
  95. package/dist/property-list.d.ts +1 -0
  96. package/dist/property-list.js +4 -0
  97. package/dist/property-list.js.map +1 -0
  98. package/dist/settings.css +63 -8
  99. package/dist/settings.css.map +1 -1
  100. package/dist/stale.css +3 -0
  101. package/dist/stale.css.map +1 -1
  102. package/dist/stepper.css +35 -0
  103. package/dist/stepper.css.map +1 -1
  104. package/dist/tokens.css +19 -0
  105. package/dist/tokens.css.map +1 -1
  106. package/dist/tool-window.cjs +63 -0
  107. package/dist/tool-window.cjs.map +1 -1
  108. package/dist/tool-window.css +6 -3
  109. package/dist/tool-window.css.map +1 -1
  110. package/dist/tool-window.d.cts +67 -1
  111. package/dist/tool-window.d.ts +67 -1
  112. package/dist/tool-window.js +63 -1
  113. package/dist/tool-window.js.map +1 -1
  114. package/dist/updater-view.cjs +283 -0
  115. package/dist/updater-view.cjs.map +1 -0
  116. package/dist/updater-view.d.cts +23 -0
  117. package/dist/updater-view.d.ts +23 -0
  118. package/dist/updater-view.js +6 -0
  119. package/dist/updater-view.js.map +1 -0
  120. package/dist/updater.cjs +12 -16
  121. package/dist/updater.cjs.map +1 -1
  122. package/dist/updater.css +44 -0
  123. package/dist/updater.css.map +1 -0
  124. package/dist/updater.js +12 -16
  125. package/dist/updater.js.map +1 -1
  126. package/dist/util.cjs +78 -0
  127. package/dist/util.cjs.map +1 -0
  128. package/dist/util.d.cts +37 -0
  129. package/dist/util.d.ts +37 -0
  130. package/dist/util.js +3 -0
  131. package/dist/util.js.map +1 -0
  132. package/dist/vc.css +3 -2
  133. package/dist/vc.css.map +1 -1
  134. package/dist/welcome.cjs +215 -0
  135. package/dist/welcome.cjs.map +1 -0
  136. package/dist/welcome.css +112 -0
  137. package/dist/welcome.css.map +1 -0
  138. package/dist/welcome.d.cts +55 -0
  139. package/dist/welcome.d.ts +55 -0
  140. package/dist/welcome.js +4 -0
  141. package/dist/welcome.js.map +1 -0
  142. package/package.json +58 -2
  143. package/dist/chunk-QDPFU4WX.js +0 -43
  144. package/dist/chunk-QDPFU4WX.js.map +0 -1
package/dist/vc.css CHANGED
@@ -1,7 +1,8 @@
1
1
  /* src/vc.css */
2
2
  .vc-badge {
3
+ display: inline-flex;
4
+ vertical-align: middle;
3
5
  margin-left: 6px;
4
- font-size: 0.78rem;
5
6
  line-height: 1;
6
7
  opacity: 0.85;
7
8
  }
@@ -16,7 +17,6 @@
16
17
  }
17
18
  .vc-badge.vc-dirty {
18
19
  color: var(--accent);
19
- font-size: 0.6rem;
20
20
  }
21
21
  .vc-badge.vc-new {
22
22
  color: var(--muted);
@@ -46,6 +46,7 @@
46
46
  color: var(--ink);
47
47
  }
48
48
  .vc-lock-glyph {
49
+ display: inline-flex;
49
50
  color: var(--danger);
50
51
  }
51
52
  /*# sourceMappingURL=vc.css.map */
package/dist/vc.css.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/vc.css"],"sourcesContent":["/* Version-control surfacing (vc-view.ts): the badge a row flies, and what a\n locked document looks like. Monochrome and themed - never colour emoji - so\n each glyph inherits its palette and reads the same in every theme. */\n.vc-badge { margin-left: 6px; font-size: 0.78rem; line-height: 1; opacity: 0.85; }\n.vc-badge.vc-locked { color: var(--danger); }\n.vc-badge.vc-stale { color: var(--warn); }\n/* The three \"this one is yours\" states. Accent rather than danger or warn:\n nothing is wrong with a file you are holding, so it must not read as a\n problem sitting in a list beside ones that are. */\n.vc-badge.vc-mine { color: var(--accent); }\n.vc-badge.vc-dirty { color: var(--accent); font-size: 0.6rem; }\n.vc-badge.vc-new { color: var(--muted); }\n.vc-badge.vc-frozen { color: var(--muted); opacity: 0.6; }\n\n/* A document somebody else holds: everything reads, nothing types. */\n.vc-readonly { position: relative; }\n.vc-off { opacity: 0.6; cursor: not-allowed; }\n.vc-lock {\n display: flex; align-items: center; gap: 8px;\n margin-bottom: 10px; padding: 7px 10px;\n border: 1px solid var(--line); border-left: 3px solid var(--danger);\n border-radius: var(--radius-sm);\n background: var(--card);\n font: 0.8rem/1.4 var(--font-ui); color: var(--ink);\n}\n.vc-lock-glyph { color: var(--danger); }\n"],"mappings":";AAGA,CAAC;AAAW,eAAa;AAAK,aAAW;AAAS,eAAa;AAAG,WAAS;AAAM;AACjF,CADC,QACQ,CAAC;AAAY,SAAO,IAAI;AAAW;AAC5C,CAFC,QAEQ,CAAC;AAAW,SAAO,IAAI;AAAS;AAIzC,CANC,QAMQ,CAAC;AAAU,SAAO,IAAI;AAAW;AAC1C,CAPC,QAOQ,CAAC;AAAW,SAAO,IAAI;AAAW,aAAW;AAAQ;AAC9D,CARC,QAQQ,CAAC;AAAS,SAAO,IAAI;AAAU;AACxC,CATC,QASQ,CAAC;AAAY,SAAO,IAAI;AAAU,WAAS;AAAK;AAGzD,CAAC;AAAc,YAAU;AAAU;AACnC,CAAC;AAAS,WAAS;AAAK,UAAQ;AAAa;AAC7C,CAAC;AACC,WAAS;AAAM,eAAa;AAAQ,OAAK;AACzC,iBAAe;AAAM,WAAS,IAAI;AAClC,UAAQ,IAAI,MAAM,IAAI;AAAS,eAAa,IAAI,MAAM,IAAI;AAC1D,iBAAe,IAAI;AACnB,cAAY,IAAI;AAChB,QAAM,MAAM,CAAC,IAAI,IAAI;AAAY,SAAO,IAAI;AAC9C;AACA,CAAC;AAAgB,SAAO,IAAI;AAAW;","names":[]}
1
+ {"version":3,"sources":["../src/vc.css"],"sourcesContent":["/* Version-control surfacing (vc-view.ts): the badge a row flies, and what a\n locked document looks like. Monochrome and themed - never colour emoji - so\n each drawing inherits its palette and reads the same in every theme. */\n.vc-badge { display: inline-flex; vertical-align: middle; margin-left: 6px; line-height: 1; opacity: 0.85; }\n.vc-badge.vc-locked { color: var(--danger); }\n.vc-badge.vc-stale { color: var(--warn); }\n/* The three \"this one is yours\" states. Accent rather than danger or warn:\n nothing is wrong with a file you are holding, so it must not read as a\n problem sitting in a list beside ones that are. */\n.vc-badge.vc-mine { color: var(--accent); }\n.vc-badge.vc-dirty { color: var(--accent); }\n.vc-badge.vc-new { color: var(--muted); }\n.vc-badge.vc-frozen { color: var(--muted); opacity: 0.6; }\n\n/* A document somebody else holds: everything reads, nothing types. */\n.vc-readonly { position: relative; }\n.vc-off { opacity: 0.6; cursor: not-allowed; }\n.vc-lock {\n display: flex; align-items: center; gap: 8px;\n margin-bottom: 10px; padding: 7px 10px;\n border: 1px solid var(--line); border-left: 3px solid var(--danger);\n border-radius: var(--radius-sm);\n background: var(--card);\n font: 0.8rem/1.4 var(--font-ui); color: var(--ink);\n}\n.vc-lock-glyph { display: inline-flex; color: var(--danger); }\n"],"mappings":";AAGA,CAAC;AAAW,WAAS;AAAa,kBAAgB;AAAQ,eAAa;AAAK,eAAa;AAAG,WAAS;AAAM;AAC3G,CADC,QACQ,CAAC;AAAY,SAAO,IAAI;AAAW;AAC5C,CAFC,QAEQ,CAAC;AAAW,SAAO,IAAI;AAAS;AAIzC,CANC,QAMQ,CAAC;AAAU,SAAO,IAAI;AAAW;AAC1C,CAPC,QAOQ,CAAC;AAAW,SAAO,IAAI;AAAW;AAC3C,CARC,QAQQ,CAAC;AAAS,SAAO,IAAI;AAAU;AACxC,CATC,QASQ,CAAC;AAAY,SAAO,IAAI;AAAU,WAAS;AAAK;AAGzD,CAAC;AAAc,YAAU;AAAU;AACnC,CAAC;AAAS,WAAS;AAAK,UAAQ;AAAa;AAC7C,CAAC;AACC,WAAS;AAAM,eAAa;AAAQ,OAAK;AACzC,iBAAe;AAAM,WAAS,IAAI;AAClC,UAAQ,IAAI,MAAM,IAAI;AAAS,eAAa,IAAI,MAAM,IAAI;AAC1D,iBAAe,IAAI;AACnB,cAAY,IAAI;AAChB,QAAM,MAAM,CAAC,IAAI,IAAI;AAAY,SAAO,IAAI;AAC9C;AACA,CAAC;AAAgB,WAAS;AAAa,SAAO,IAAI;AAAW;","names":[]}
@@ -0,0 +1,215 @@
1
+ 'use strict';
2
+
3
+ // src/tooltip.ts
4
+ function renderTip(host, text) {
5
+ host.textContent = "";
6
+ const re = /\u0002([^\u0003]*)\u0003/g;
7
+ let last = 0;
8
+ let m;
9
+ while ((m = re.exec(text)) !== null) {
10
+ if (m.index > last) host.append(text.slice(last, m.index));
11
+ const strong = document.createElement("strong");
12
+ strong.textContent = m[1] ?? "";
13
+ host.append(strong);
14
+ last = m.index + m[0].length;
15
+ }
16
+ if (last < text.length) host.append(text.slice(last));
17
+ }
18
+ var inited = false;
19
+ var checked = false;
20
+ var suppressed;
21
+ var SHOW_DELAY = 350;
22
+ var EDGE = 6;
23
+ var GAP = 7;
24
+ var tip = null;
25
+ var timer = 0;
26
+ var active = null;
27
+ function ensureEl() {
28
+ if (tip) return tip;
29
+ const el2 = document.createElement("div");
30
+ el2.className = "tooltip";
31
+ el2.setAttribute("role", "tooltip");
32
+ el2.hidden = true;
33
+ document.body.appendChild(el2);
34
+ tip = el2;
35
+ return el2;
36
+ }
37
+ var rectOf = (anchor) => anchor instanceof HTMLElement ? anchor.getBoundingClientRect() : anchor;
38
+ function place(anchor) {
39
+ const t = ensureEl();
40
+ const r = rectOf(anchor);
41
+ const tr = t.getBoundingClientRect();
42
+ let top = r.top - tr.height - GAP;
43
+ if (top < EDGE) top = r.top + r.height + GAP;
44
+ let left = r.left + r.width / 2 - tr.width / 2;
45
+ left = Math.max(EDGE, Math.min(left, window.innerWidth - tr.width - EDGE));
46
+ t.style.left = `${Math.round(left)}px`;
47
+ t.style.top = `${Math.round(top)}px`;
48
+ }
49
+ function show(anchor) {
50
+ const text = anchor.dataset["tip"];
51
+ if (!text) return;
52
+ paint(anchor, text);
53
+ }
54
+ function paint(anchor, text) {
55
+ if (suppressed?.()) return;
56
+ const t = ensureEl();
57
+ const host = (anchor instanceof HTMLElement ? anchor.closest("dialog[open]") : null) ?? document.body;
58
+ if (t.parentElement !== host) host.appendChild(t);
59
+ active = anchor instanceof HTMLElement ? anchor : null;
60
+ renderTip(t, text);
61
+ t.hidden = false;
62
+ t.classList.remove("show");
63
+ void t.offsetWidth;
64
+ place(anchor);
65
+ t.classList.add("show");
66
+ }
67
+ function hide() {
68
+ if (timer) {
69
+ window.clearTimeout(timer);
70
+ timer = 0;
71
+ }
72
+ active = null;
73
+ if (tip) {
74
+ tip.hidden = true;
75
+ tip.classList.remove("show");
76
+ }
77
+ }
78
+ function schedule(anchor) {
79
+ if (anchor === active) return;
80
+ if (timer) window.clearTimeout(timer);
81
+ timer = window.setTimeout(() => show(anchor), SHOW_DELAY);
82
+ }
83
+ function ensureTooltipHost() {
84
+ initTooltips();
85
+ }
86
+ function checkTooltipHost() {
87
+ if (checked) return;
88
+ checked = true;
89
+ setTimeout(() => {
90
+ if (inited || typeof document === "undefined") return;
91
+ if (document.querySelector("[data-tip]")) {
92
+ console.warn("app-shell: something set `data-tip` but no tooltip host is mounted, so those rollovers will not draw. Call initTooltips() once per window.");
93
+ }
94
+ }, 0);
95
+ }
96
+ var tipAncestor = (n) => n instanceof Element ? n.closest("[data-tip]") : null;
97
+ function initTooltips(opts = {}) {
98
+ if (opts.suppressed !== void 0) suppressed = opts.suppressed;
99
+ if (inited) return;
100
+ inited = true;
101
+ document.addEventListener("pointerover", (e) => {
102
+ const el2 = tipAncestor(e.target);
103
+ if (el2) schedule(el2);
104
+ });
105
+ document.addEventListener("pointerout", (e) => {
106
+ const el2 = tipAncestor(e.target);
107
+ if (!el2) return;
108
+ const to = e.relatedTarget;
109
+ if (to instanceof Node && el2.contains(to)) return;
110
+ hide();
111
+ });
112
+ document.addEventListener("focusin", (e) => {
113
+ const el2 = tipAncestor(e.target);
114
+ if (el2) schedule(el2);
115
+ });
116
+ document.addEventListener("focusout", () => hide());
117
+ document.addEventListener("pointerdown", () => hide(), true);
118
+ document.addEventListener("scroll", () => {
119
+ if (active) hide();
120
+ }, true);
121
+ document.addEventListener("keydown", (e) => {
122
+ if (e.key === "Escape") hide();
123
+ });
124
+ window.addEventListener("blur", () => hide());
125
+ }
126
+
127
+ // src/dom.ts
128
+ function el(tag, classNameOrProps, ...children) {
129
+ const node = document.createElement(tag);
130
+ const props = typeof classNameOrProps === "string" ? { className: classNameOrProps } : classNameOrProps ?? {};
131
+ if (props.className) node.className = props.className;
132
+ if (props.text !== void 0 && props.text !== null) node.textContent = props.text;
133
+ if (props.title !== void 0) node.title = props.title;
134
+ if (props.tip !== void 0) {
135
+ node.dataset["tip"] = props.tip;
136
+ node.setAttribute("aria-label", props.tip);
137
+ ensureTooltipHost();
138
+ }
139
+ if (props.onClick) node.addEventListener("click", props.onClick);
140
+ for (const child of children) {
141
+ if (child === null || child === void 0) continue;
142
+ node.append(child);
143
+ }
144
+ checkTooltipHost();
145
+ return node;
146
+ }
147
+
148
+ // src/welcome.ts
149
+ function mountWelcome(host, opts) {
150
+ const caption = (text) => el("h2", "welcome-caption", text);
151
+ const group = (label, ...body) => el("section", "welcome-group", caption(label), ...body);
152
+ const actions = el("div", "welcome-actions");
153
+ for (const a of opts.actions) {
154
+ const b = el("button", { className: `btn${a.primary ? " primary" : ""}`, text: a.label, onClick: a.onClick });
155
+ b.type = "button";
156
+ actions.append(b);
157
+ }
158
+ const groups = (opts.groups ?? []).map((g) => group(
159
+ g.caption,
160
+ g.note !== void 0 ? el("p", "welcome-note", g.note) : null,
161
+ el("div", "welcome-list", ...g.items.map((item) => {
162
+ const b = el(
163
+ "button",
164
+ { className: "welcome-item", onClick: item.onOpen },
165
+ el("span", "welcome-item-name", item.name),
166
+ item.hint !== void 0 ? el("span", "welcome-item-hint", item.hint) : null
167
+ );
168
+ b.type = "button";
169
+ return b;
170
+ }))
171
+ ));
172
+ const recentsHost = el("div", "welcome-recents-slot");
173
+ const drawRecents = (recents) => {
174
+ const shown = recents.slice(0, opts.maxRecents ?? 5);
175
+ recentsHost.replaceChildren();
176
+ if (shown.length === 0) return;
177
+ recentsHost.append(group(
178
+ opts.recentsCaption ?? "Recent",
179
+ el("div", "welcome-recents", ...shown.map((r) => {
180
+ const b = el(
181
+ "button",
182
+ { className: "welcome-recent", onClick: r.onOpen },
183
+ el("span", "welcome-recent-name", r.name),
184
+ el("span", "welcome-recent-path", r.path)
185
+ );
186
+ b.type = "button";
187
+ return b;
188
+ }))
189
+ ));
190
+ };
191
+ drawRecents(opts.recents);
192
+ const error = el("p", "welcome-error");
193
+ const setError = (text) => {
194
+ error.textContent = text ?? "";
195
+ error.hidden = text === void 0 || text === "";
196
+ };
197
+ setError(opts.error);
198
+ const root = el("div", "welcome", el(
199
+ "div",
200
+ "welcome-card",
201
+ el("h1", "welcome-title", opts.title),
202
+ el("p", "welcome-sub", opts.sub),
203
+ actions,
204
+ opts.tourLine ? el("div", "welcome-line", opts.tourLine) : null,
205
+ ...groups,
206
+ recentsHost,
207
+ error
208
+ ));
209
+ host.replaceChildren(root);
210
+ return { el: root, setRecents: drawRecents, setError };
211
+ }
212
+
213
+ exports.mountWelcome = mountWelcome;
214
+ //# sourceMappingURL=welcome.cjs.map
215
+ //# sourceMappingURL=welcome.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tooltip.ts","../src/dom.ts","../src/welcome.ts"],"names":["el"],"mappings":"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","file":"welcome.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// A single themed tooltip the family controls, replacing native `title`\n// rollovers: those are unstyled OS chrome and appear on the platform's slow\n// (~1s) delay, which is exactly the seam design-language section 4 (\"coherent\n// to the edges\") says not to leave showing.\n//\n// ONE floating bubble, document-DELEGATED: any element carrying a `data-tip`\n// attribute gets it, so a caller just sets `data-tip=\"...\"` anywhere and wires\n// nothing. The bubble anchors above the element (flipping below near the top\n// edge), uses our own snappy delay and the shared panel-enter motion.\n// `initTooltips()` is idempotent, so it is safe to call from more than one\n// place in an app that has several entry points.\n//\n// Lifted from Patterpad's patterpad-surface/web/tooltip.ts, which proved it,\n// with the one app-specific rule (suppressed in Writing View) generalised into\n// the `suppressed` option so the shell knows nothing about either app's modes.\n// ---------------------------------------------------------------------------\n\n// Bold markers: a producer wraps a span in these PRIVATE control chars (U+0002 STX / U+0003 ETX,\n// never present in user text) and the renderer turns it into a <strong>. Lets a data-tip bold part\n// of itself (a name, a \"VO:\" prefix) without any HTML, and each segment is still set via\n// textContent, so there is no injection risk from user content.\nconst B_OPEN = \"\\u0002\";\nconst B_CLOSE = \"\\u0003\";\n\n/** Wrap `s` so it renders bold inside a tooltip (see the marker note above). */\nexport function tipBold(s: string): string { return `${B_OPEN}${s}${B_CLOSE}`; }\n\n/** Render `text` into `host`, turning `\\u0002…\\u0003`-marked spans into <strong>; all set as text (safe). */\nfunction renderTip(host: HTMLElement, text: string): void {\n host.textContent = \"\";\n const re = /\\u0002([^\\u0003]*)\\u0003/g;\n let last = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > last) host.append(text.slice(last, m.index));\n const strong = document.createElement(\"strong\");\n strong.textContent = m[1] ?? \"\";\n host.append(strong);\n last = m.index + m[0].length;\n }\n if (last < text.length) host.append(text.slice(last));\n}\n\nexport interface TooltipOptions {\n /** Called before every show: return true to keep the bubble down. For a mode\n * that wants no chrome at all (Patterpad's Writing View). */\n suppressed?: () => boolean;\n}\n\nlet inited = false;\nlet checked = false; // the one-shot no-host warning below has been scheduled\nlet suppressed: (() => boolean) | undefined;\nconst SHOW_DELAY = 350; // ms - snappier than the OS title delay, slow enough not to flicker on pass-through\nconst EDGE = 6; // viewport inset so the bubble never touches the window edge\nconst GAP = 7; // px between the anchor and the bubble\n\nlet tip: HTMLDivElement | null = null;\nlet timer = 0;\nlet active: HTMLElement | null = null;\n/** The subject of a rect-anchored tip (see `tipAt`), when there is one. */\nlet activeKey: string | null = null;\n\nfunction ensureEl(): HTMLDivElement {\n if (tip) return tip;\n const el = document.createElement(\"div\");\n el.className = \"tooltip\";\n el.setAttribute(\"role\", \"tooltip\");\n el.hidden = true;\n document.body.appendChild(el);\n tip = el;\n return el;\n}\n\n/** A rectangle in VIEWPORT coordinates, for anchoring a tip to something that is\n * not a DOM element: a shape on a canvas, a run of text inside one. */\nexport interface TipRect { left: number; top: number; width: number; height: number }\n\nconst rectOf = (anchor: HTMLElement | TipRect): TipRect =>\n (anchor instanceof HTMLElement ? anchor.getBoundingClientRect() : anchor);\n\n/** Anchor the (already-visible, so measurable) bubble above the anchor, flipping below near the top. */\nfunction place(anchor: HTMLElement | TipRect): void {\n const t = ensureEl();\n const r = rectOf(anchor);\n const tr = t.getBoundingClientRect();\n let top = r.top - tr.height - GAP;\n if (top < EDGE) top = r.top + r.height + GAP; // not enough room above -> below\n let left = r.left + r.width / 2 - tr.width / 2;\n left = Math.max(EDGE, Math.min(left, window.innerWidth - tr.width - EDGE));\n t.style.left = `${Math.round(left)}px`;\n t.style.top = `${Math.round(top)}px`;\n}\n\nfunction show(anchor: HTMLElement): void {\n const text = anchor.dataset[\"tip\"];\n if (!text) return;\n paint(anchor, text);\n}\n\n/** Fill, place and reveal the bubble. The one path: both the delegated\n * `data-tip` route and `tipAt` end up here, so a canvas tip and a button tip\n * are the same bubble with the same look and the same motion. */\nfunction paint(anchor: HTMLElement | TipRect, text: string): void {\n if (suppressed?.()) return;\n const t = ensureEl();\n // A modal <dialog> (settings, a confirm) renders in the browser TOP LAYER, above every normal-flow\n // element whatever its z-index. If the anchor is inside one, move the bubble into that dialog so it\n // shares the top layer; otherwise keep it on <body>. Re-parenting per show also moves it back out\n // once the modal closes.\n const host: HTMLElement = (anchor instanceof HTMLElement ? anchor.closest<HTMLElement>(\"dialog[open]\") : null)\n ?? document.body;\n if (t.parentElement !== host) host.appendChild(t);\n active = anchor instanceof HTMLElement ? anchor : null;\n renderTip(t, text);\n t.hidden = false;\n t.classList.remove(\"show\"); void t.offsetWidth; // restart the enter animation\n place(anchor); // measure + position now it has content + is visible\n t.classList.add(\"show\");\n}\n\nfunction hide(): void {\n if (timer) { window.clearTimeout(timer); timer = 0; }\n active = null;\n activeKey = null;\n if (tip) { tip.hidden = true; tip.classList.remove(\"show\"); }\n}\n\nfunction schedule(anchor: HTMLElement): void {\n if (anchor === active) return;\n if (timer) window.clearTimeout(timer);\n timer = window.setTimeout(() => show(anchor), SHOW_DELAY);\n}\n\n/**\n * Show a tip for something that has no DOM node of its own: a card on a canvas,\n * a zone, a pin. The caller says WHERE (a viewport rectangle) and WHAT, and\n * identifies the subject with `key`.\n *\n * The key is what makes this usable from a canvas. Re-calling with the same key\n * re-places the bubble WITHOUT restarting the delay, so a tip can follow its\n * shape while the camera moves; calling with a different key starts the wait\n * again, so sweeping the pointer across a dense board does not strobe.\n *\n * The bubble is ordinary DOM, so it holds its size whatever the canvas is\n * zoomed to. That is the point of it: the label it stands in for is the one\n * thing on a canvas that must not shrink with everything else.\n */\nexport function tipAt(key: string, rect: TipRect, text: string): void {\n if (key === activeKey) {\n // Already up for this subject: move it, do not re-animate it.\n if (tip && !tip.hidden) { renderTip(tip, text); place(rect); }\n return;\n }\n // A different subject: down first, then wait again. Leaving the old bubble up\n // during the wait would put one card's name over another card, which is worse\n // than no name at all - and it matches the delegated route, where leaving an\n // element hides the tip before the next one is scheduled.\n hide();\n activeKey = key;\n timer = window.setTimeout(() => { timer = 0; paint(rect, text); }, SHOW_DELAY);\n}\n\n/** Take any tip down: the pointer has left, or a gesture has started. */\nexport function hideTip(): void { hide(); }\n\n/**\n * Mount the renderer because something is about to speak the grammar.\n *\n * The themed tooltip is not a feature a host turns on; it is the renderer for a grammar the shell's own\n * components already speak, and `data-tip` with nothing listening draws NOTHING - not the themed\n * bubble, not the platform one. That silence cost Patterpad two releases of a pin with no tooltip, and\n * cost it again when a `title` was converted to `data-tip` in a window that had never mounted this.\n *\n * So `el` calls this whenever it sets a tip. Mounting a delegated listener is the same class of act as\n * `confirmDialog` appending to `document.body`, which this package has always done.\n */\nexport function ensureTooltipHost(): void { initTooltips(); }\n\n/**\n * One deferred check that an app writing `data-tip` ITSELF has a host, warning if not.\n *\n * `ensureTooltipHost` covers anything built through `el`. It cannot cover a `data-tip` written straight\n * into markup or set by hand, which is the case that leaves a whole window of dead tips with no shell\n * component involved - so that one gets a warning rather than silence. Scheduled once, after the\n * current task, so it sees a rendered document rather than an empty one.\n */\nexport function checkTooltipHost(): void {\n if (checked) return;\n checked = true;\n setTimeout(() => {\n if (inited || typeof document === \"undefined\") return;\n if (document.querySelector(\"[data-tip]\")) {\n console.warn(\"app-shell: something set `data-tip` but no tooltip host is mounted, so those rollovers will not draw. Call initTooltips() once per window.\");\n }\n }, 0);\n}\n\nconst tipAncestor = (n: EventTarget | null): HTMLElement | null =>\n (n instanceof Element ? n.closest<HTMLElement>(\"[data-tip]\") : null);\n\n/**\n * Wire the one delegated controller. Mounting happens once; OPTIONS do not.\n *\n * An explicit call is always authoritative about its options, even after something else has already\n * mounted the listener. That ordering is not hypothetical, and Storyletter caught it before it shipped:\n * once components self-mount (see `ensureTooltipHost`), a host's own\n * `initTooltips({ suppressed })` could land second and be dropped by an early return, silently taking\n * a rule like \"no tooltips in Writing View\" with it. Nothing would fail and nothing would show.\n *\n * Components therefore call this with NO arguments: they mount the renderer without claiming the\n * options, and the host keeps the last word on behaviour.\n */\nexport function initTooltips(opts: TooltipOptions = {}): void {\n if (opts.suppressed !== undefined) suppressed = opts.suppressed; // an explicit call always wins\n if (inited) return;\n inited = true;\n\n document.addEventListener(\"pointerover\", (e) => {\n const el = tipAncestor(e.target);\n if (el) schedule(el);\n });\n document.addEventListener(\"pointerout\", (e) => {\n const el = tipAncestor(e.target);\n if (!el) return;\n const to = (e as PointerEvent).relatedTarget;\n if (to instanceof Node && el.contains(to)) return; // still inside the same anchor\n hide();\n });\n // Keyboard parity: reveal on focus, dismiss on blur.\n document.addEventListener(\"focusin\", (e) => { const el = tipAncestor(e.target); if (el) schedule(el); });\n document.addEventListener(\"focusout\", () => hide());\n // It's ANCHORED (not cursor-following), so anything that moves the layout or context dismisses it.\n document.addEventListener(\"pointerdown\", () => hide(), true);\n document.addEventListener(\"scroll\", () => { if (active) hide(); }, true);\n document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") hide(); });\n window.addEventListener(\"blur\", () => hide());\n}\n","// ---------------------------------------------------------------------------\n// `el`: the tiny tag-typed element factory both apps build their imperative,\n// idempotent views with.\n//\n// A SUPERSET of the two apps' prior signatures, so neither rewrites call sites:\n// - Patterpad: el(tag, cls?, text?) (positional)\n// - Storylet Studio: el(tag, { className, text, ... }, ...kids) (options bag)\n// A string second argument is the className; an object is the props bag; string\n// children become text nodes. So `el(\"div\", \"c\", \"t\")` and\n// `el(\"div\", { className: \"c\" }, \"t\")` produce identical DOM.\n// ---------------------------------------------------------------------------\n\nimport { ensureTooltipHost, checkTooltipHost } from \"./tooltip.js\";\nimport { iconNode, iconNameOfGlyph, isIconName, type IconName } from \"./icons.js\";\n\nexport type Child = Node | string | null | undefined;\n\nexport interface ElProps {\n className?: string;\n text?: string;\n /** A NATIVE rollover. Prefer `tip`: `title` is OS chrome on the platform's own\n * slow delay, which is the seam the themed tooltip exists to close. */\n title?: string;\n /** A themed rollover (`data-tip`, picked up by the delegated controller that\n * `initTooltips()` wires). Also the accessible name, since the elements that\n * want one are usually icon buttons with no text of their own. */\n tip?: string;\n onClick?: (event: MouseEvent) => void;\n}\n\nexport function el<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n classNameOrProps?: string | ElProps,\n ...children: Child[]\n): HTMLElementTagNameMap[K] {\n const node = document.createElement(tag);\n const props: ElProps = typeof classNameOrProps === \"string\"\n ? { className: classNameOrProps }\n : classNameOrProps ?? {};\n if (props.className) node.className = props.className;\n if (props.text !== undefined && props.text !== null) node.textContent = props.text;\n if (props.title !== undefined) node.title = props.title;\n if (props.tip !== undefined) {\n node.dataset[\"tip\"] = props.tip;\n node.setAttribute(\"aria-label\", props.tip);\n // Setting a tip mounts the renderer. Without this, `data-tip` draws nothing at all in a window whose\n // host never called `initTooltips()`, which is a silent failure rather than a degraded one. No\n // options passed: the host stays authoritative about behaviour (see tooltip.ts).\n ensureTooltipHost();\n }\n if (props.onClick) node.addEventListener(\"click\", props.onClick as EventListener);\n for (const child of children) {\n if (child === null || child === undefined) continue;\n node.append(child);\n }\n checkTooltipHost(); // one-shot: warns if the APP wrote data-tip and nothing mounted a host\n return node;\n}\n\n/** A small square icon button (the move/delete controls list editors share).\n * Its rollover is the THEMED one (`data-tip`), so an app that has called\n * `initTooltips()` gets our bubble rather than the platform's.\n *\n * `name` is a word from the vocabulary (\"up\", \"close\"), drawn at 14px. A\n * typed glyph from the deprecated `icon` table (\"↑\", \"✕\") is accepted for\n * now and drawn as the word it stood for, so an app that has not moved its\n * call sites yet still gets the drawn set; any other string is set as text,\n * which is the old behaviour and on its way out. */\nexport function iconBtn(name: IconName | (string & {}), title: string, onClick: () => void, disabled = false, danger = false): HTMLButtonElement {\n const b = el(\"button\", `shell-icon${danger ? \" danger\" : \"\"}`);\n b.type = \"button\"; b.dataset[\"tip\"] = title; b.setAttribute(\"aria-label\", title);\n const word = isIconName(name) ? name : iconNameOfGlyph(name);\n if (word) b.append(iconNode(word)); else b.textContent = name;\n b.disabled = disabled;\n b.addEventListener(\"click\", onClick);\n return b;\n}\n\n/**\n * A captioned field: `<label class=\"shell-labelled\"><span class=\"shell-fieldcap\">…</span>control</label>`.\n *\n * The label is pointed at the field it captions, EXPLICITLY, and never at a button. A `<label>` with no\n * `for` forwards a click to its first labelable descendant, and buttons are labelable - so a caption\n * wrapped around a control containing buttons (`tagChips`, whose every chip carries a remove button)\n * turns every click on the row's dead space into a press of the FIRST button in it. That shipped:\n * patterkit/patter#44, where clicking a Game Data list value anywhere but its ✕ deleted the first value\n * in the list. Reported as \"clicking a value removes the wrong one\", which is not a wrong-index bug at\n * all - the click never reached the chip.\n *\n * When the control holds no labelable field (a chips editor with the add input removed, say), this is a\n * plain `<div>`: no caption behaviour is better than a caption that presses something.\n */\nexport function labelled(label: string, control: HTMLElement): HTMLElement {\n const target = control.matches(\"input, select, textarea\")\n ? control\n : control.querySelector<HTMLElement>(\"input:not([type=button]):not([type=submit]), select, textarea\");\n const w = el(target ? \"label\" : \"div\", \"shell-labelled\");\n w.append(el(\"span\", \"shell-fieldcap\", label), control);\n if (target) {\n if (!target.id) target.id = `shell-f-${Math.random().toString(36).slice(2, 9)}`;\n (w as HTMLLabelElement).htmlFor = target.id;\n }\n return w;\n}\n\n/** Swap item `i` with its neighbour `i + delta` IN PLACE (the up/down reorder list editors share);\n * a no-op when the target is out of range. Returns whether anything moved. */\nexport function moveItem<T>(arr: T[], i: number, delta: number): boolean {\n const j = i + delta;\n if (j < 0 || j >= arr.length) return false;\n [arr[i], arr[j]] = [arr[j]!, arr[i]!];\n return true;\n}\n\n/** The icon size inside a chip's remove / move buttons: the chip is set at\n * 0.76rem, so its controls draw smaller than a toolbar button's 14px. */\nconst CHIP_ICON = 10;\n\n/** A tag-style editor for a string-list field (an enum's allowed values / flags): removable chips + an\n * add input (Enter or \",\" commits; blank / duplicate ignored). Mutates `holder.values` in place (read\n * back on save). `onChange` fires after any add / remove so callers can refresh a dependent control. */\nexport function tagChips(holder: { values?: string[] }, onChange?: () => void): HTMLElement {\n const wrap = el(\"div\", \"shell-tags\");\n const input = el(\"input\", \"shell-tag-input\");\n // Sentence-cased like every other hint (the microcopy ruling, 2026-08-28):\n // lowercase fragments read as adrift, and a placeholder is a hint speaking.\n input.type = \"text\"; input.placeholder = \"Add value\"; input.spellcheck = false;\n const makeChip = (v: string): HTMLElement => {\n const chip = el(\"span\", \"shell-tag\", v);\n const x = el(\"button\", \"shell-tag-x\", iconNode(\"close\", CHIP_ICON));\n x.type = \"button\"; x.dataset[\"tip\"] = `Remove ${v}`; x.setAttribute(\"aria-label\", `Remove ${v}`);\n x.addEventListener(\"click\", () => { holder.values = (holder.values ?? []).filter((o) => o !== v); chip.remove(); onChange?.(); });\n chip.append(x);\n return chip;\n };\n const commit = (): void => {\n const v = input.value.trim();\n if (v && !(holder.values ?? []).includes(v)) { (holder.values ??= []).push(v); wrap.insertBefore(makeChip(v), input); onChange?.(); }\n input.value = \"\";\n };\n input.addEventListener(\"keydown\", (e) => { if (e.key === \"Enter\" || e.key === \",\") { e.preventDefault(); commit(); } });\n input.addEventListener(\"blur\", commit);\n for (const v of holder.values ?? []) wrap.append(makeChip(v));\n wrap.append(input);\n return wrap;\n}\n\n/**\n * An ORDERED chips editor for a string-list field whose order means something\n * (a quality's stages: `advance()` walks them, so 2 comes after 1): numbered\n * chips, each with move-earlier / move-later / remove, plus an add input\n * (Enter or \",\" commits; blank / duplicate ignored). Mutates `holder.stages`\n * in place; `onChange` fires after any add / move / remove.\n *\n * Patterpad's, lifted (ui-review-2026-09, finding 3); Storyletter had been\n * faking it with `tagChips` over a `values` holder, which loses the order\n * controls. Same class family as `tagChips` (`.shell-tags`), so it is styled by\n * settings.css already.\n */\nexport function stageChips(holder: { stages?: string[] }, onChange?: () => void): HTMLElement {\n const wrap = el(\"div\", \"shell-tags shell-stages\");\n const input = el(\"input\", \"shell-tag-input\");\n input.type = \"text\"; input.placeholder = \"Add stage\"; input.spellcheck = false;\n const control = (name: IconName, tip: string, disabled: boolean, onClick: () => void): HTMLButtonElement => {\n const b = el(\"button\", \"shell-tag-x shell-tag-move\", iconNode(name, CHIP_ICON));\n b.type = \"button\"; b.dataset[\"tip\"] = tip; b.setAttribute(\"aria-label\", tip);\n b.disabled = disabled;\n b.addEventListener(\"click\", onClick);\n return b;\n };\n const rebuild = (): void => {\n for (const c of Array.from(wrap.children)) if (c !== input) c.remove();\n const stages = holder.stages ?? [];\n stages.forEach((v, i) => {\n const chip = el(\"span\", \"shell-tag\", `${i + 1}. ${v}`);\n chip.dataset[\"stage\"] = v;\n chip.append(\n control(\"back\", `Move ${v} earlier`, i === 0,\n () => { if (moveItem(stages, i, -1)) { rebuild(); onChange?.(); } }),\n control(\"forward\", `Move ${v} later`, i === stages.length - 1,\n () => { if (moveItem(stages, i, 1)) { rebuild(); onChange?.(); } }),\n );\n const x = el(\"button\", \"shell-tag-x\", iconNode(\"close\", CHIP_ICON));\n x.type = \"button\"; x.dataset[\"tip\"] = `Remove ${v}`; x.setAttribute(\"aria-label\", `Remove ${v}`);\n x.addEventListener(\"click\", () => { holder.stages = (holder.stages ?? []).filter((o) => o !== v); rebuild(); onChange?.(); });\n chip.append(x);\n wrap.insertBefore(chip, input);\n });\n };\n const commit = (): void => {\n const v = input.value.trim();\n if (v && !(holder.stages ?? []).includes(v)) { (holder.stages ??= []).push(v); rebuild(); onChange?.(); }\n input.value = \"\";\n };\n input.addEventListener(\"keydown\", (e) => { if (e.key === \"Enter\" || e.key === \",\") { e.preventDefault(); commit(); } });\n input.addEventListener(\"blur\", commit);\n wrap.append(input);\n rebuild();\n return wrap;\n}\n\n// --- drag-to-reorder ---------------------------------------------------------\n// One drag at a time per window, shared across every wired list, so the\n// midpoint test knows what is being carried without a dataTransfer round trip.\nlet reorderDragId: string | null = null;\nconst clearDropMarks = (host: ParentNode): void =>\n host.querySelectorAll(\".drop-before, .drop-after\").forEach((e) => e.classList.remove(\"drop-before\", \"drop-after\"));\n\n/**\n * Make `el` a drag source and a drop target for HTML5 drag-to-reorder among\n * its siblings. While carried it wears `.dragging`; a hovered target wears\n * `.drop-before` or `.drop-after` by the midpoint test on `axis` (\"y\" for\n * rows, \"x\" for a wrapping grid); a drop calls `onMove(draggedId, before,\n * targetId)` and the caller reorders its model and re-renders.\n *\n * Storyletter's `wireCardDrag`, lifted (ui-review-2026-09, finding 30);\n * Patterpad's `wireSceneDrag` is the same idea fixed on \"y\". The marks are\n * classes only: the app draws them (Patterpad's `box-shadow: 0 -2px 0 0\n * var(--accent)` pair is the family's look).\n */\nexport function wireReorder(el: HTMLElement, id: string, axis: \"x\" | \"y\", onMove: (id: string, before: boolean, targetId: string) => void): void {\n el.draggable = true;\n el.dataset[\"reorderId\"] = id;\n const host = (): ParentNode => el.parentElement ?? el;\n el.addEventListener(\"dragstart\", (e) => {\n reorderDragId = id; el.classList.add(\"dragging\");\n if (e.dataTransfer) { e.dataTransfer.effectAllowed = \"move\"; e.dataTransfer.setData(\"text/plain\", id); }\n });\n el.addEventListener(\"dragend\", () => { reorderDragId = null; el.classList.remove(\"dragging\"); clearDropMarks(host()); });\n el.addEventListener(\"dragover\", (e) => {\n if (!reorderDragId || reorderDragId === id) return;\n e.preventDefault();\n if (e.dataTransfer) e.dataTransfer.dropEffect = \"move\";\n const r = el.getBoundingClientRect();\n const before = axis === \"y\" ? e.clientY < r.top + r.height / 2 : e.clientX < r.left + r.width / 2;\n clearDropMarks(host());\n el.classList.add(before ? \"drop-before\" : \"drop-after\");\n });\n el.addEventListener(\"dragleave\", () => el.classList.remove(\"drop-before\", \"drop-after\"));\n el.addEventListener(\"drop\", (e) => {\n e.preventDefault();\n const before = el.classList.contains(\"drop-before\");\n clearDropMarks(host());\n if (reorderDragId && reorderDragId !== id) onMove(reorderDragId, before, id);\n });\n}\n","// ---------------------------------------------------------------------------\n// The welcome screen: what an app shows with no project open. Title, one line\n// under it, the actions (Open, New), an optional teaching line, optional\n// captioned groups (Storyletter's shipped examples), and the recents.\n//\n// Both apps drew this with the same twelve CSS rules under two class prefixes\n// (`.recent-name` / `.wel-recent-name`), four of them byte-identical. The CSS\n// here is Patterpad's; the captioned groups are Storyletter's, as an optional\n// slot, drawn as a plain list of rows (name + hint) rather than cards.\n//\n// The whole screen is a drag region (the window has no title bar to grab\n// here) and every control opts out. Styles ship as welcome.css.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\n\nexport interface WelcomeAction {\n label: string;\n onClick: () => void;\n /** The one accent button (Open a project…). */\n primary?: boolean;\n}\n\nexport interface WelcomeRecent {\n /** What the project calls itself, with the folder stem as the fallback. */\n name: string;\n /** Beside the name, because two projects may share a name and the folder\n * is how you tell them apart. */\n path: string;\n onOpen: () => void;\n}\n\nexport interface WelcomeGroupItem {\n name: string;\n /** What choosing it gets you, said before the click. */\n hint?: string;\n onOpen: () => void;\n}\n\nexport interface WelcomeGroup {\n /** Sentence-case label text (\"Learn from a finished project\"); never an overline. */\n caption: string;\n /** The consequence of a click, said before the click (\"Each opens as your\n * own copy, in a folder you choose.\"). */\n note?: string;\n items: WelcomeGroupItem[];\n}\n\nexport interface WelcomeOptions {\n title: string;\n sub: string;\n actions: WelcomeAction[];\n recents: WelcomeRecent[];\n groups?: WelcomeGroup[];\n /** The teaching door: a line under the actions, quieter than a button\n * (\"New to Patter? Take the interactive tour\"). The app builds and styles\n * it; a text action in it is a link, not bare muted text. */\n tourLine?: HTMLElement;\n /** The recents caption. Default \"Recent\". */\n recentsCaption?: string;\n /** How many recents to show. Default 5. */\n maxRecents?: number;\n /** A sentence about why the last open failed, under everything. */\n error?: string;\n}\n\nexport interface Welcome {\n el: HTMLElement;\n /** Redraw the recents (the list changes as projects open and close). */\n setRecents(recents: WelcomeRecent[]): void;\n setError(error: string | undefined): void;\n}\n\n/** Mount the welcome into `host` (replacing its children). */\nexport function mountWelcome(host: HTMLElement, opts: WelcomeOptions): Welcome {\n const caption = (text: string): HTMLElement => el(\"h2\", \"welcome-caption\", text);\n const group = (label: string, ...body: (Node | null)[]): HTMLElement =>\n el(\"section\", \"welcome-group\", caption(label), ...body);\n\n const actions = el(\"div\", \"welcome-actions\");\n for (const a of opts.actions) {\n const b = el(\"button\", { className: `btn${a.primary ? \" primary\" : \"\"}`, text: a.label, onClick: a.onClick });\n b.type = \"button\";\n actions.append(b);\n }\n\n const groups = (opts.groups ?? []).map((g) => group(g.caption,\n g.note !== undefined ? el(\"p\", \"welcome-note\", g.note) : null,\n el(\"div\", \"welcome-list\", ...g.items.map((item) => {\n const b = el(\"button\", { className: \"welcome-item\", onClick: item.onOpen },\n el(\"span\", \"welcome-item-name\", item.name),\n item.hint !== undefined ? el(\"span\", \"welcome-item-hint\", item.hint) : null);\n b.type = \"button\";\n return b;\n })),\n ));\n\n const recentsHost = el(\"div\", \"welcome-recents-slot\");\n const drawRecents = (recents: WelcomeRecent[]): void => {\n const shown = recents.slice(0, opts.maxRecents ?? 5);\n recentsHost.replaceChildren();\n if (shown.length === 0) return;\n recentsHost.append(group(opts.recentsCaption ?? \"Recent\",\n el(\"div\", \"welcome-recents\", ...shown.map((r) => {\n const b = el(\"button\", { className: \"welcome-recent\", onClick: r.onOpen },\n el(\"span\", \"welcome-recent-name\", r.name),\n el(\"span\", \"welcome-recent-path\", r.path));\n b.type = \"button\";\n return b;\n })),\n ));\n };\n drawRecents(opts.recents);\n\n const error = el(\"p\", \"welcome-error\");\n const setError = (text: string | undefined): void => {\n error.textContent = text ?? \"\";\n error.hidden = text === undefined || text === \"\";\n };\n setError(opts.error);\n\n const root = el(\"div\", \"welcome\", el(\"div\", \"welcome-card\",\n el(\"h1\", \"welcome-title\", opts.title),\n el(\"p\", \"welcome-sub\", opts.sub),\n actions,\n opts.tourLine ? el(\"div\", \"welcome-line\", opts.tourLine) : null,\n ...groups,\n recentsHost,\n error,\n ));\n host.replaceChildren(root);\n return { el: root, setRecents: drawRecents, setError };\n}\n"]}
@@ -0,0 +1,112 @@
1
+ /* src/welcome.css */
2
+ .welcome {
3
+ --welcome-width: 34rem;
4
+ flex: 1;
5
+ min-height: 0;
6
+ display: grid;
7
+ place-items: center;
8
+ padding: 2rem;
9
+ overflow: auto;
10
+ -webkit-app-region: drag;
11
+ animation: var(--anim-screen-in);
12
+ }
13
+ .welcome :is(button, input, select, a, .no-drag) {
14
+ -webkit-app-region: no-drag;
15
+ }
16
+ .welcome[hidden] {
17
+ display: none;
18
+ }
19
+ .welcome-card {
20
+ width: min(var(--welcome-width), 100%);
21
+ }
22
+ .welcome-title {
23
+ margin: 0 0 0.2rem;
24
+ font: 700 2.4rem/1.1 var(--font-read);
25
+ color: var(--ink);
26
+ text-align: center;
27
+ }
28
+ .welcome-sub {
29
+ margin: 0 0 1.8rem;
30
+ color: var(--muted);
31
+ font-style: italic;
32
+ text-align: center;
33
+ }
34
+ .welcome-actions {
35
+ display: flex;
36
+ gap: 0.6rem;
37
+ justify-content: center;
38
+ flex-wrap: wrap;
39
+ margin-bottom: 1rem;
40
+ }
41
+ .welcome-line {
42
+ margin: 0 0 1.8rem;
43
+ text-align: center;
44
+ color: var(--muted);
45
+ font: 0.82rem var(--font-ui);
46
+ }
47
+ .welcome-group {
48
+ margin-bottom: 1.6rem;
49
+ }
50
+ .welcome-caption {
51
+ margin: 0 0 0.5rem;
52
+ color: var(--muted);
53
+ font: 600 0.82rem/1.3 var(--font-ui);
54
+ }
55
+ .welcome-note {
56
+ margin: -0.25rem 0 0.5rem;
57
+ color: var(--muted);
58
+ font: 0.78rem/1.45 var(--font-ui);
59
+ }
60
+ .welcome-list,
61
+ .welcome-recents {
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: 0.2rem;
65
+ }
66
+ .welcome-item,
67
+ .welcome-recent {
68
+ display: flex;
69
+ flex-direction: column;
70
+ align-items: flex-start;
71
+ gap: 0.05rem;
72
+ width: 100%;
73
+ cursor: pointer;
74
+ background: none;
75
+ border: 1px solid transparent;
76
+ border-radius: var(--radius-md);
77
+ padding: 0.45rem 0.6rem;
78
+ text-align: left;
79
+ overflow-wrap: anywhere;
80
+ font: inherit;
81
+ color: inherit;
82
+ transition: background var(--dur-quick) var(--ease-standard), border-color var(--dur-quick) var(--ease-standard);
83
+ }
84
+ .welcome-item:hover,
85
+ .welcome-recent:hover {
86
+ background: color-mix(in oklab, var(--accent) 8%, transparent);
87
+ border-color: var(--line);
88
+ }
89
+ .welcome-item-name,
90
+ .welcome-recent-name {
91
+ color: var(--ink);
92
+ font: 600 0.9rem var(--font-ui);
93
+ }
94
+ .welcome-item-hint {
95
+ color: var(--muted);
96
+ font: 0.78rem/1.45 var(--font-ui);
97
+ }
98
+ .welcome-recent-path {
99
+ color: var(--muted);
100
+ font: 0.7rem var(--font-mono);
101
+ }
102
+ .welcome-error {
103
+ margin: 0;
104
+ color: var(--danger);
105
+ font: 0.85rem var(--font-ui);
106
+ max-width: 60ch;
107
+ text-align: center;
108
+ }
109
+ .welcome-error[hidden] {
110
+ display: none;
111
+ }
112
+ /*# sourceMappingURL=welcome.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/welcome.css"],"sourcesContent":["/* ---------------------------------------------------------------------------\n The welcome screen (welcome.ts). Patterpad's rules, generalised; consumes\n the host palette (--surface / --ink / --muted / --line / --accent /\n --danger / --font-ui / --font-read / --font-mono) over the shared tokens.\n The buttons are controls.css's `.btn`; import it beside this file.\n\n Width is a per-app decision: `--welcome-width` on `.welcome`.\n --------------------------------------------------------------------------- */\n\n/* The whole screen drags the window; anything clickable opts out. */\n.welcome {\n --welcome-width: 34rem;\n flex: 1; min-height: 0; display: grid; place-items: center; padding: 2rem; overflow: auto;\n -webkit-app-region: drag;\n animation: var(--anim-screen-in);\n}\n.welcome :is(button, input, select, a, .no-drag) { -webkit-app-region: no-drag; }\n.welcome[hidden] { display: none; }\n.welcome-card { width: min(var(--welcome-width), 100%); }\n\n.welcome-title { margin: 0 0 0.2rem; font: 700 2.4rem/1.1 var(--font-read); color: var(--ink); text-align: center; }\n.welcome-sub { margin: 0 0 1.8rem; color: var(--muted); font-style: italic; text-align: center; }\n.welcome-actions { display: flex; gap: 0.6rem; justify-content: center; flex-wrap: wrap; margin-bottom: 1rem; }\n/* The teaching line: quieter than the two actions above it. */\n.welcome-line { margin: 0 0 1.8rem; text-align: center; color: var(--muted); font: 0.82rem var(--font-ui); }\n\n/* One group per job (learn / return), captioned. A caption is sentence-case\n label text, weight 600, in the UI face: how a group box reads on the\n platform, never an overline. */\n.welcome-group { margin-bottom: 1.6rem; }\n.welcome-caption { margin: 0 0 0.5rem; color: var(--muted); font: 600 0.82rem/1.3 var(--font-ui); }\n/* The consequence of a click, said before the click. */\n.welcome-note { margin: -0.25rem 0 0.5rem; color: var(--muted); font: 0.78rem/1.45 var(--font-ui); }\n\n/* Rows, not cards: a name with a hint under it, the same well the recents\n use, so the two lists read as one kind of thing. */\n.welcome-list, .welcome-recents { display: flex; flex-direction: column; gap: 0.2rem; }\n.welcome-item, .welcome-recent {\n display: flex; flex-direction: column; align-items: flex-start; gap: 0.05rem; width: 100%; cursor: pointer;\n background: none; border: 1px solid transparent; border-radius: var(--radius-md); padding: 0.45rem 0.6rem;\n /* A button's UA default is centred text; a wrapping path would centre its\n second line. `anywhere` because a path has no spaces to break at. */\n text-align: left; overflow-wrap: anywhere; font: inherit; color: inherit;\n transition: background var(--dur-quick) var(--ease-standard), border-color var(--dur-quick) var(--ease-standard);\n}\n.welcome-item:hover, .welcome-recent:hover { background: color-mix(in oklab, var(--accent) 8%, transparent); border-color: var(--line); }\n.welcome-item-name, .welcome-recent-name { color: var(--ink); font: 600 0.9rem var(--font-ui); }\n.welcome-item-hint { color: var(--muted); font: 0.78rem/1.45 var(--font-ui); }\n.welcome-recent-path { color: var(--muted); font: 0.7rem var(--font-mono); }\n\n.welcome-error { margin: 0; color: var(--danger); font: 0.85rem var(--font-ui); max-width: 60ch; text-align: center; }\n.welcome-error[hidden] { display: none; }\n"],"mappings":";AAUA,CAAC;AACC,mBAAiB;AACjB,QAAM;AAAG,cAAY;AAAG,WAAS;AAAM,eAAa;AAAQ,WAAS;AAAM,YAAU;AACrF,sBAAoB;AACpB,aAAW,IAAI;AACjB;AACA,CANC,QAMQ,IAAI,QAAQ,OAAO,QAAQ,GAAG,CAAC;AAAW,sBAAoB;AAAS;AAChF,CAPC,OAOO,CAAC;AAAU,WAAS;AAAM;AAClC,CAAC;AAAe,SAAO,IAAI,IAAI,gBAAgB,EAAE;AAAO;AAExD,CAAC;AAAgB,UAAQ,EAAE,EAAE;AAAQ,QAAM,IAAI,MAAM,CAAC,IAAI,IAAI;AAAc,SAAO,IAAI;AAAQ,cAAY;AAAQ;AACnH,CAAC;AAAc,UAAQ,EAAE,EAAE;AAAQ,SAAO,IAAI;AAAU,cAAY;AAAQ,cAAY;AAAQ;AAChG,CAAC;AAAkB,WAAS;AAAM,OAAK;AAAQ,mBAAiB;AAAQ,aAAW;AAAM,iBAAe;AAAM;AAE9G,CAAC;AAAe,UAAQ,EAAE,EAAE;AAAQ,cAAY;AAAQ,SAAO,IAAI;AAAU,QAAM,QAAQ,IAAI;AAAY;AAK3G,CAAC;AAAgB,iBAAe;AAAQ;AACxC,CAAC;AAAkB,UAAQ,EAAE,EAAE;AAAQ,SAAO,IAAI;AAAU,QAAM,IAAI,OAAO,CAAC,IAAI,IAAI;AAAY;AAElG,CAAC;AAAe,UAAQ,SAAS,EAAE;AAAQ,SAAO,IAAI;AAAU,QAAM,OAAO,CAAC,KAAK,IAAI;AAAY;AAInG,CAAC;AAAc,CAAC;AAAkB,WAAS;AAAM,kBAAgB;AAAQ,OAAK;AAAQ;AACtF,CAAC;AAAc,CAAC;AACd,WAAS;AAAM,kBAAgB;AAAQ,eAAa;AAAY,OAAK;AAAS,SAAO;AAAM,UAAQ;AACnG,cAAY;AAAM,UAAQ,IAAI,MAAM;AAAa,iBAAe,IAAI;AAAc,WAAS,QAAQ;AAGnG,cAAY;AAAM,iBAAe;AAAU,QAAM;AAAS,SAAO;AACjE,cAAY,WAAW,IAAI,aAAa,IAAI,gBAAgB,EAAE,aAAa,IAAI,aAAa,IAAI;AAClG;AACA,CARC,YAQY;AAAQ,CARL,cAQoB;AAAS,cAAY,UAAU,GAAG,KAAK,EAAE,IAAI,UAAU,EAAE,EAAE;AAAc,gBAAc,IAAI;AAAS;AACxI,CAAC;AAAmB,CAAC;AAAsB,SAAO,IAAI;AAAQ,QAAM,IAAI,OAAO,IAAI;AAAY;AAC/F,CAAC;AAAoB,SAAO,IAAI;AAAU,QAAM,OAAO,CAAC,KAAK,IAAI;AAAY;AAC7E,CAAC;AAAsB,SAAO,IAAI;AAAU,QAAM,OAAO,IAAI;AAAc;AAE3E,CAAC;AAAgB,UAAQ;AAAG,SAAO,IAAI;AAAW,QAAM,QAAQ,IAAI;AAAY,aAAW;AAAM,cAAY;AAAQ;AACrH,CADC,aACa,CAAC;AAAU,WAAS;AAAM;","names":[]}
@@ -0,0 +1,55 @@
1
+ interface WelcomeAction {
2
+ label: string;
3
+ onClick: () => void;
4
+ /** The one accent button (Open a project…). */
5
+ primary?: boolean;
6
+ }
7
+ interface WelcomeRecent {
8
+ /** What the project calls itself, with the folder stem as the fallback. */
9
+ name: string;
10
+ /** Beside the name, because two projects may share a name and the folder
11
+ * is how you tell them apart. */
12
+ path: string;
13
+ onOpen: () => void;
14
+ }
15
+ interface WelcomeGroupItem {
16
+ name: string;
17
+ /** What choosing it gets you, said before the click. */
18
+ hint?: string;
19
+ onOpen: () => void;
20
+ }
21
+ interface WelcomeGroup {
22
+ /** Sentence-case label text ("Learn from a finished project"); never an overline. */
23
+ caption: string;
24
+ /** The consequence of a click, said before the click ("Each opens as your
25
+ * own copy, in a folder you choose."). */
26
+ note?: string;
27
+ items: WelcomeGroupItem[];
28
+ }
29
+ interface WelcomeOptions {
30
+ title: string;
31
+ sub: string;
32
+ actions: WelcomeAction[];
33
+ recents: WelcomeRecent[];
34
+ groups?: WelcomeGroup[];
35
+ /** The teaching door: a line under the actions, quieter than a button
36
+ * ("New to Patter? Take the interactive tour"). The app builds and styles
37
+ * it; a text action in it is a link, not bare muted text. */
38
+ tourLine?: HTMLElement;
39
+ /** The recents caption. Default "Recent". */
40
+ recentsCaption?: string;
41
+ /** How many recents to show. Default 5. */
42
+ maxRecents?: number;
43
+ /** A sentence about why the last open failed, under everything. */
44
+ error?: string;
45
+ }
46
+ interface Welcome {
47
+ el: HTMLElement;
48
+ /** Redraw the recents (the list changes as projects open and close). */
49
+ setRecents(recents: WelcomeRecent[]): void;
50
+ setError(error: string | undefined): void;
51
+ }
52
+ /** Mount the welcome into `host` (replacing its children). */
53
+ declare function mountWelcome(host: HTMLElement, opts: WelcomeOptions): Welcome;
54
+
55
+ export { type Welcome, type WelcomeAction, type WelcomeGroup, type WelcomeGroupItem, type WelcomeOptions, type WelcomeRecent, mountWelcome };
@@ -0,0 +1,55 @@
1
+ interface WelcomeAction {
2
+ label: string;
3
+ onClick: () => void;
4
+ /** The one accent button (Open a project…). */
5
+ primary?: boolean;
6
+ }
7
+ interface WelcomeRecent {
8
+ /** What the project calls itself, with the folder stem as the fallback. */
9
+ name: string;
10
+ /** Beside the name, because two projects may share a name and the folder
11
+ * is how you tell them apart. */
12
+ path: string;
13
+ onOpen: () => void;
14
+ }
15
+ interface WelcomeGroupItem {
16
+ name: string;
17
+ /** What choosing it gets you, said before the click. */
18
+ hint?: string;
19
+ onOpen: () => void;
20
+ }
21
+ interface WelcomeGroup {
22
+ /** Sentence-case label text ("Learn from a finished project"); never an overline. */
23
+ caption: string;
24
+ /** The consequence of a click, said before the click ("Each opens as your
25
+ * own copy, in a folder you choose."). */
26
+ note?: string;
27
+ items: WelcomeGroupItem[];
28
+ }
29
+ interface WelcomeOptions {
30
+ title: string;
31
+ sub: string;
32
+ actions: WelcomeAction[];
33
+ recents: WelcomeRecent[];
34
+ groups?: WelcomeGroup[];
35
+ /** The teaching door: a line under the actions, quieter than a button
36
+ * ("New to Patter? Take the interactive tour"). The app builds and styles
37
+ * it; a text action in it is a link, not bare muted text. */
38
+ tourLine?: HTMLElement;
39
+ /** The recents caption. Default "Recent". */
40
+ recentsCaption?: string;
41
+ /** How many recents to show. Default 5. */
42
+ maxRecents?: number;
43
+ /** A sentence about why the last open failed, under everything. */
44
+ error?: string;
45
+ }
46
+ interface Welcome {
47
+ el: HTMLElement;
48
+ /** Redraw the recents (the list changes as projects open and close). */
49
+ setRecents(recents: WelcomeRecent[]): void;
50
+ setError(error: string | undefined): void;
51
+ }
52
+ /** Mount the welcome into `host` (replacing its children). */
53
+ declare function mountWelcome(host: HTMLElement, opts: WelcomeOptions): Welcome;
54
+
55
+ export { type Welcome, type WelcomeAction, type WelcomeGroup, type WelcomeGroupItem, type WelcomeOptions, type WelcomeRecent, mountWelcome };
@@ -0,0 +1,4 @@
1
+ export { mountWelcome } from './chunk-C6UVQYKI.js';
2
+ import './chunk-XZYZXY7I.js';
3
+ //# sourceMappingURL=welcome.js.map
4
+ //# sourceMappingURL=welcome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"welcome.js"}