@wildwinter/app-shell 0.1.0 → 0.4.1
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/chunk-KQT3XYRS.js +10 -0
- package/dist/chunk-KQT3XYRS.js.map +1 -0
- package/dist/index.cjs +367 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -1
- package/dist/index.d.ts +120 -1
- package/dist/index.js +353 -1
- package/dist/index.js.map +1 -1
- package/dist/menu.cjs +12 -0
- package/dist/menu.cjs.map +1 -0
- package/dist/menu.d.cts +13 -0
- package/dist/menu.d.ts +13 -0
- package/dist/menu.js +3 -0
- package/dist/menu.js.map +1 -0
- package/dist/pane-shell.css +114 -0
- package/dist/pane-shell.css.map +1 -0
- package/dist/pane-shell.d.cts +2 -0
- package/dist/pane-shell.d.ts +2 -0
- package/dist/settings.css +266 -0
- package/dist/settings.css.map +1 -0
- package/dist/settings.d.cts +2 -0
- package/dist/settings.d.ts +2 -0
- package/package.json +12 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { MenuLabel, PANE_MENU } from './menu.js';
|
|
2
|
+
|
|
1
3
|
type Child = Node | string | null | undefined;
|
|
2
4
|
interface ElProps {
|
|
3
5
|
className?: string;
|
|
@@ -6,6 +8,19 @@ interface ElProps {
|
|
|
6
8
|
onClick?: (event: MouseEvent) => void;
|
|
7
9
|
}
|
|
8
10
|
declare function el<K extends keyof HTMLElementTagNameMap>(tag: K, classNameOrProps?: string | ElProps, ...children: Child[]): HTMLElementTagNameMap[K];
|
|
11
|
+
/** A small square glyph button (the move/delete controls list editors share). */
|
|
12
|
+
declare function iconBtn(glyph: string, title: string, onClick: () => void, disabled?: boolean, danger?: boolean): HTMLButtonElement;
|
|
13
|
+
/** A captioned field: `<label class="shell-labelled"><span class="shell-fieldcap">…</span>control</label>`. */
|
|
14
|
+
declare function labelled(label: string, control: HTMLElement): HTMLElement;
|
|
15
|
+
/** Swap item `i` with its neighbour `i + delta` IN PLACE (the up/down reorder list editors share);
|
|
16
|
+
* a no-op when the target is out of range. Returns whether anything moved. */
|
|
17
|
+
declare function moveItem<T>(arr: T[], i: number, delta: number): boolean;
|
|
18
|
+
/** A tag-style editor for a string-list field (an enum's allowed values / flags): removable chips + an
|
|
19
|
+
* add input (Enter or "," commits; blank / duplicate ignored). Mutates `holder.values` in place (read
|
|
20
|
+
* back on save). `onChange` fires after any add / remove so callers can refresh a dependent control. */
|
|
21
|
+
declare function tagChips(holder: {
|
|
22
|
+
values?: string[];
|
|
23
|
+
}, onChange?: () => void): HTMLElement;
|
|
9
24
|
|
|
10
25
|
/** Curated colour slots; must match the `--char-*` set defined per theme. */
|
|
11
26
|
declare const PALETTE_SIZE = 12;
|
|
@@ -16,4 +31,108 @@ declare function colourIndex(name: string): number;
|
|
|
16
31
|
/** The theme-aware CSS variable reference for a name's slot. */
|
|
17
32
|
declare function colourFor(name: string): string;
|
|
18
33
|
|
|
19
|
-
|
|
34
|
+
type PaneSide = "nav" | "inspector";
|
|
35
|
+
interface PaneSideConfig {
|
|
36
|
+
/** The open width as a CSS length, e.g. "14rem" or "224px". */
|
|
37
|
+
defaultWidth: string;
|
|
38
|
+
/** Drag floor in px (default 160). */
|
|
39
|
+
minWidth?: number;
|
|
40
|
+
/** Drag ceiling in px from the window width (default w => min(640, w - 420)). */
|
|
41
|
+
maxWidth?: (innerWidth: number) => number;
|
|
42
|
+
/** Whether a drag seam is offered (default true). */
|
|
43
|
+
resizable?: boolean;
|
|
44
|
+
/** Noun for the toggle's tooltip/aria, e.g. "navigator" / "inspector". */
|
|
45
|
+
label?: string;
|
|
46
|
+
/** Shortcut hint appended to the tooltip, e.g. "Cmd+1". */
|
|
47
|
+
shortcutHint?: string;
|
|
48
|
+
}
|
|
49
|
+
interface PaneShellState {
|
|
50
|
+
open: Record<PaneSide, boolean>;
|
|
51
|
+
/** Dragged widths in px; absent = the config default. */
|
|
52
|
+
width: Partial<Record<PaneSide, number>>;
|
|
53
|
+
}
|
|
54
|
+
interface PaneShellOptions {
|
|
55
|
+
nav: PaneSideConfig;
|
|
56
|
+
inspector: PaneSideConfig;
|
|
57
|
+
/** Initial open + width state (from the host's store). */
|
|
58
|
+
initial?: Partial<PaneShellState>;
|
|
59
|
+
/** Persist state after any toggle or resize (host wires it to its store). */
|
|
60
|
+
onChange?: (state: PaneShellState) => void;
|
|
61
|
+
}
|
|
62
|
+
interface PaneShell {
|
|
63
|
+
/** The frame element (top bar + panes); mount it into the app container. */
|
|
64
|
+
readonly root: HTMLElement;
|
|
65
|
+
readonly topbar: HTMLElement;
|
|
66
|
+
/** Top-bar host slots: leading (title) and trailing (status), the host fills. */
|
|
67
|
+
readonly topbarLead: HTMLElement;
|
|
68
|
+
readonly topbarTrail: HTMLElement;
|
|
69
|
+
/** Pane bodies the host fills (the fixed-width inner that clips on collapse). */
|
|
70
|
+
readonly nav: HTMLElement;
|
|
71
|
+
readonly centre: HTMLElement;
|
|
72
|
+
readonly inspector: HTMLElement;
|
|
73
|
+
togglePane(side: PaneSide): void;
|
|
74
|
+
setPaneOpen(side: PaneSide, open: boolean): void;
|
|
75
|
+
isOpen(side: PaneSide): boolean;
|
|
76
|
+
/** Clear dragged widths back to the config defaults. */
|
|
77
|
+
resetWidths(): void;
|
|
78
|
+
/** A snapshot of the current state (open + widths). */
|
|
79
|
+
state(): PaneShellState;
|
|
80
|
+
destroy(): void;
|
|
81
|
+
}
|
|
82
|
+
declare function mountPaneShell(host: HTMLElement, opts: PaneShellOptions): PaneShell;
|
|
83
|
+
|
|
84
|
+
/** A compact list row: a single `.set-rowline` (inline controls) plus, when
|
|
85
|
+
* `details` are given, a disclosure that reveals a `.set-details` panel below,
|
|
86
|
+
* so a list scans like a table with secondary fields tucked away. */
|
|
87
|
+
declare function expandableRow(opts: {
|
|
88
|
+
line: HTMLElement[];
|
|
89
|
+
details?: HTMLElement[];
|
|
90
|
+
}): HTMLElement;
|
|
91
|
+
/** After a "+ Add" re-renders a list, bring the new (last) row into view and
|
|
92
|
+
* focus its name input (`.set-name`, else the first input). */
|
|
93
|
+
declare function focusNewRow(listEl: HTMLElement | null | undefined): void;
|
|
94
|
+
interface DupGuard {
|
|
95
|
+
/** Forget the previous render's inputs (call at the top of render). */
|
|
96
|
+
reset(): void;
|
|
97
|
+
/** Register a name input; `key` returns the comparison string (default the
|
|
98
|
+
* input's value). Re-checks live on input. `also` = extra inputs that change the key. */
|
|
99
|
+
track(input: HTMLInputElement, key?: () => string, also?: HTMLInputElement[]): void;
|
|
100
|
+
/** Re-paint duplicates; true if any two non-blank keys collide. */
|
|
101
|
+
check(): boolean;
|
|
102
|
+
/** The first input currently flagged (after a fresh check), or null. */
|
|
103
|
+
firstDuplicate(): HTMLInputElement | null;
|
|
104
|
+
}
|
|
105
|
+
/** Case-insensitive duplicate-name detection: paints clashing inputs `.invalid`
|
|
106
|
+
* and lets a Save gate block + jump via `firstDuplicate()`. */
|
|
107
|
+
declare function dupGuard(): DupGuard;
|
|
108
|
+
interface SettingsSectionHandle {
|
|
109
|
+
/** The first invalid control to jump to, blocking Save (e.g. a duplicate name). */
|
|
110
|
+
firstInvalid?(): HTMLElement | null;
|
|
111
|
+
}
|
|
112
|
+
interface SettingsSection {
|
|
113
|
+
id: string;
|
|
114
|
+
label: string;
|
|
115
|
+
/** Optional left-rail group heading above this tab. */
|
|
116
|
+
group?: string;
|
|
117
|
+
/** Build the section body into `host`; called fresh on each open so it reads
|
|
118
|
+
* the current data. Return a handle (for the Save gate). */
|
|
119
|
+
mount(host: HTMLElement): SettingsSectionHandle;
|
|
120
|
+
}
|
|
121
|
+
interface SettingsDialogOptions {
|
|
122
|
+
title: string;
|
|
123
|
+
sections: SettingsSection[];
|
|
124
|
+
/** Fired when Save is clicked and every section validates. The app reads its
|
|
125
|
+
* own section state and persists. May be async; the dialog stays open until
|
|
126
|
+
* it resolves, then closes. */
|
|
127
|
+
onSave: () => void | Promise<void>;
|
|
128
|
+
}
|
|
129
|
+
interface SettingsDialog {
|
|
130
|
+
/** Open (optionally on a given tab id). Sections re-mount from current data. */
|
|
131
|
+
open(tabId?: string): void;
|
|
132
|
+
destroy(): void;
|
|
133
|
+
}
|
|
134
|
+
/** Build a settings dialog. The frame owns the modal, the tab rail, save-on-Save
|
|
135
|
+
* (with a validation gate), and Cancel; the sections own their content + state. */
|
|
136
|
+
declare function mountSettingsDialog(opts: SettingsDialogOptions): SettingsDialog;
|
|
137
|
+
|
|
138
|
+
export { type Child, type DupGuard, type ElProps, PALETTE, PALETTE_SIZE, type PaneShell, type PaneShellOptions, type PaneShellState, type PaneSide, type PaneSideConfig, type SettingsDialog, type SettingsDialogOptions, type SettingsSection, type SettingsSectionHandle, colourFor, colourIndex, dupGuard, el, expandableRow, focusNewRow, iconBtn, labelled, mountPaneShell, mountSettingsDialog, moveItem, tagChips };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { PANE_MENU } from './chunk-KQT3XYRS.js';
|
|
2
|
+
|
|
1
3
|
// src/dom.ts
|
|
2
4
|
function el(tag, classNameOrProps, ...children) {
|
|
3
5
|
const node = document.createElement(tag);
|
|
@@ -12,6 +14,67 @@ function el(tag, classNameOrProps, ...children) {
|
|
|
12
14
|
}
|
|
13
15
|
return node;
|
|
14
16
|
}
|
|
17
|
+
function iconBtn(glyph, title, onClick, disabled = false, danger = false) {
|
|
18
|
+
const b = el("button", `shell-icon${danger ? " danger" : ""}`);
|
|
19
|
+
b.type = "button";
|
|
20
|
+
b.textContent = glyph;
|
|
21
|
+
b.title = title;
|
|
22
|
+
b.setAttribute("aria-label", title);
|
|
23
|
+
b.disabled = disabled;
|
|
24
|
+
b.addEventListener("click", onClick);
|
|
25
|
+
return b;
|
|
26
|
+
}
|
|
27
|
+
function labelled(label, control) {
|
|
28
|
+
const w = el("label", "shell-labelled");
|
|
29
|
+
w.append(el("span", "shell-fieldcap", label), control);
|
|
30
|
+
return w;
|
|
31
|
+
}
|
|
32
|
+
function moveItem(arr, i, delta) {
|
|
33
|
+
const j = i + delta;
|
|
34
|
+
if (j < 0 || j >= arr.length) return false;
|
|
35
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
function tagChips(holder, onChange) {
|
|
39
|
+
const wrap = el("div", "shell-tags");
|
|
40
|
+
const input = el("input", "shell-tag-input");
|
|
41
|
+
input.type = "text";
|
|
42
|
+
input.placeholder = "add value";
|
|
43
|
+
input.spellcheck = false;
|
|
44
|
+
const makeChip = (v) => {
|
|
45
|
+
const chip = el("span", "shell-tag", v);
|
|
46
|
+
const x = el("button", "shell-tag-x", "\u2715");
|
|
47
|
+
x.type = "button";
|
|
48
|
+
x.title = `remove ${v}`;
|
|
49
|
+
x.setAttribute("aria-label", `remove ${v}`);
|
|
50
|
+
x.addEventListener("click", () => {
|
|
51
|
+
holder.values = (holder.values ?? []).filter((o) => o !== v);
|
|
52
|
+
chip.remove();
|
|
53
|
+
onChange?.();
|
|
54
|
+
});
|
|
55
|
+
chip.append(x);
|
|
56
|
+
return chip;
|
|
57
|
+
};
|
|
58
|
+
const commit = () => {
|
|
59
|
+
const v = input.value.trim();
|
|
60
|
+
if (v && !(holder.values ?? []).includes(v)) {
|
|
61
|
+
(holder.values ??= []).push(v);
|
|
62
|
+
wrap.insertBefore(makeChip(v), input);
|
|
63
|
+
onChange?.();
|
|
64
|
+
}
|
|
65
|
+
input.value = "";
|
|
66
|
+
};
|
|
67
|
+
input.addEventListener("keydown", (e) => {
|
|
68
|
+
if (e.key === "Enter" || e.key === ",") {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
commit();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
input.addEventListener("blur", commit);
|
|
74
|
+
for (const v of holder.values ?? []) wrap.append(makeChip(v));
|
|
75
|
+
wrap.append(input);
|
|
76
|
+
return wrap;
|
|
77
|
+
}
|
|
15
78
|
|
|
16
79
|
// src/colour.ts
|
|
17
80
|
var PALETTE_SIZE = 12;
|
|
@@ -33,6 +96,295 @@ function colourFor(name) {
|
|
|
33
96
|
return PALETTE[colourIndex(name)];
|
|
34
97
|
}
|
|
35
98
|
|
|
36
|
-
|
|
99
|
+
// src/pane-shell.ts
|
|
100
|
+
var DEFAULT_MAX = (w) => Math.min(640, w - 420);
|
|
101
|
+
var openVar = { nav: "--nav-open-w", inspector: "--insp-open-w" };
|
|
102
|
+
var collapseClass = { nav: "no-nav", inspector: "no-inspector" };
|
|
103
|
+
function mountPaneShell(host, opts) {
|
|
104
|
+
const cfg = { nav: opts.nav, inspector: opts.inspector };
|
|
105
|
+
const open = { nav: opts.initial?.open?.nav ?? true, inspector: opts.initial?.open?.inspector ?? true };
|
|
106
|
+
const width = { ...opts.initial?.width ?? {} };
|
|
107
|
+
const el2 = (tag, cls) => {
|
|
108
|
+
const n = document.createElement(tag);
|
|
109
|
+
if (cls) n.className = cls;
|
|
110
|
+
return n;
|
|
111
|
+
};
|
|
112
|
+
const navToggle = el2("button", "pane-toggle");
|
|
113
|
+
navToggle.type = "button";
|
|
114
|
+
const inspToggle = el2("button", "pane-toggle");
|
|
115
|
+
inspToggle.type = "button";
|
|
116
|
+
const topbarLead = el2("div", "topbar-lead");
|
|
117
|
+
const topbarTrail = el2("div", "topbar-trail");
|
|
118
|
+
const spacer = el2("span", "topbar-spacer");
|
|
119
|
+
const topbar = el2("header", "topbar");
|
|
120
|
+
topbar.append(navToggle, topbarLead, spacer, topbarTrail, inspToggle);
|
|
121
|
+
const panes = el2("main", "panes");
|
|
122
|
+
const navPane = el2("nav", "pane pane-nav");
|
|
123
|
+
const navInner = el2("div", "pane-inner");
|
|
124
|
+
navPane.append(navInner);
|
|
125
|
+
const centre = el2("section", "pane-centre");
|
|
126
|
+
const inspPane = el2("aside", "pane pane-inspector");
|
|
127
|
+
const inspInner = el2("div", "pane-inner");
|
|
128
|
+
inspPane.append(inspInner);
|
|
129
|
+
const navResizer = el2("div", "pane-resizer pane-resizer-nav");
|
|
130
|
+
const inspResizer = el2("div", "pane-resizer pane-resizer-insp");
|
|
131
|
+
panes.append(navPane, centre, inspPane, navResizer, inspResizer);
|
|
132
|
+
const root = el2("div", "pane-shell");
|
|
133
|
+
root.append(topbar, panes);
|
|
134
|
+
host.append(root);
|
|
135
|
+
function applyToggleGlyph(side, button) {
|
|
136
|
+
const isOpen = open[side];
|
|
137
|
+
button.textContent = side === "nav" ? isOpen ? "\u2039" : "\u203A" : isOpen ? "\u203A" : "\u2039";
|
|
138
|
+
const noun = cfg[side].label ?? side;
|
|
139
|
+
const hint = cfg[side].shortcutHint ? ` (${cfg[side].shortcutHint})` : "";
|
|
140
|
+
button.title = `${isOpen ? "Hide" : "Show"} ${noun}${hint}`;
|
|
141
|
+
button.setAttribute("aria-label", button.title);
|
|
142
|
+
button.setAttribute("aria-pressed", String(isOpen));
|
|
143
|
+
}
|
|
144
|
+
function apply() {
|
|
145
|
+
panes.classList.toggle(collapseClass.nav, !open.nav);
|
|
146
|
+
panes.classList.toggle(collapseClass.inspector, !open.inspector);
|
|
147
|
+
panes.style.setProperty(openVar.nav, width.nav !== void 0 ? `${width.nav}px` : cfg.nav.defaultWidth);
|
|
148
|
+
panes.style.setProperty(openVar.inspector, width.inspector !== void 0 ? `${width.inspector}px` : cfg.inspector.defaultWidth);
|
|
149
|
+
applyToggleGlyph("nav", navToggle);
|
|
150
|
+
applyToggleGlyph("inspector", inspToggle);
|
|
151
|
+
navResizer.style.display = cfg.nav.resizable === false ? "none" : "";
|
|
152
|
+
inspResizer.style.display = cfg.inspector.resizable === false ? "none" : "";
|
|
153
|
+
}
|
|
154
|
+
const snapshot = () => ({ open: { ...open }, width: { ...width } });
|
|
155
|
+
const persist = () => opts.onChange?.(snapshot());
|
|
156
|
+
function setPaneOpen(side, next) {
|
|
157
|
+
if (open[side] === next) return;
|
|
158
|
+
open[side] = next;
|
|
159
|
+
apply();
|
|
160
|
+
persist();
|
|
161
|
+
}
|
|
162
|
+
function togglePane(side) {
|
|
163
|
+
setPaneOpen(side, !open[side]);
|
|
164
|
+
}
|
|
165
|
+
function resetWidths() {
|
|
166
|
+
delete width.nav;
|
|
167
|
+
delete width.inspector;
|
|
168
|
+
apply();
|
|
169
|
+
persist();
|
|
170
|
+
}
|
|
171
|
+
navToggle.addEventListener("click", () => togglePane("nav"));
|
|
172
|
+
inspToggle.addEventListener("click", () => togglePane("inspector"));
|
|
173
|
+
function beginResize(side, startEvent) {
|
|
174
|
+
if (cfg[side].resizable === false || !open[side]) return;
|
|
175
|
+
startEvent.preventDefault();
|
|
176
|
+
const pane = side === "nav" ? navPane : inspPane;
|
|
177
|
+
const min = cfg[side].minWidth ?? 160;
|
|
178
|
+
const max = cfg[side].maxWidth ?? DEFAULT_MAX;
|
|
179
|
+
const startX = startEvent.clientX;
|
|
180
|
+
const startW = pane.getBoundingClientRect().width;
|
|
181
|
+
document.body.classList.add("pane-resizing");
|
|
182
|
+
let latest = startW;
|
|
183
|
+
const onMove = (e) => {
|
|
184
|
+
const delta = side === "nav" ? e.clientX - startX : startX - e.clientX;
|
|
185
|
+
latest = Math.max(min, Math.min(max(window.innerWidth), startW + delta));
|
|
186
|
+
panes.style.setProperty(openVar[side], `${latest}px`);
|
|
187
|
+
};
|
|
188
|
+
const onUp = () => {
|
|
189
|
+
window.removeEventListener("pointermove", onMove);
|
|
190
|
+
window.removeEventListener("pointerup", onUp);
|
|
191
|
+
document.body.classList.remove("pane-resizing");
|
|
192
|
+
width[side] = latest;
|
|
193
|
+
persist();
|
|
194
|
+
};
|
|
195
|
+
window.addEventListener("pointermove", onMove);
|
|
196
|
+
window.addEventListener("pointerup", onUp);
|
|
197
|
+
}
|
|
198
|
+
navResizer.addEventListener("pointerdown", (e) => beginResize("nav", e));
|
|
199
|
+
inspResizer.addEventListener("pointerdown", (e) => beginResize("inspector", e));
|
|
200
|
+
navResizer.addEventListener("dblclick", () => {
|
|
201
|
+
delete width.nav;
|
|
202
|
+
apply();
|
|
203
|
+
persist();
|
|
204
|
+
});
|
|
205
|
+
inspResizer.addEventListener("dblclick", () => {
|
|
206
|
+
delete width.inspector;
|
|
207
|
+
apply();
|
|
208
|
+
persist();
|
|
209
|
+
});
|
|
210
|
+
apply();
|
|
211
|
+
return {
|
|
212
|
+
root,
|
|
213
|
+
topbar,
|
|
214
|
+
topbarLead,
|
|
215
|
+
topbarTrail,
|
|
216
|
+
nav: navInner,
|
|
217
|
+
centre,
|
|
218
|
+
inspector: inspInner,
|
|
219
|
+
togglePane,
|
|
220
|
+
setPaneOpen,
|
|
221
|
+
isOpen: (s) => open[s],
|
|
222
|
+
resetWidths,
|
|
223
|
+
state: snapshot,
|
|
224
|
+
destroy: () => {
|
|
225
|
+
root.remove();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/settings.ts
|
|
231
|
+
function expandableRow(opts) {
|
|
232
|
+
const row = el("div", "set-row");
|
|
233
|
+
const line = el("div", "set-rowline");
|
|
234
|
+
if (opts.details && opts.details.length) {
|
|
235
|
+
const details = el("div", "set-details");
|
|
236
|
+
details.hidden = true;
|
|
237
|
+
for (const d of opts.details) details.append(d);
|
|
238
|
+
const toggle = el("button", "set-expand");
|
|
239
|
+
toggle.type = "button";
|
|
240
|
+
toggle.title = "More";
|
|
241
|
+
toggle.setAttribute("aria-label", "More");
|
|
242
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
243
|
+
toggle.addEventListener("click", () => {
|
|
244
|
+
const open = details.hidden;
|
|
245
|
+
details.hidden = !open;
|
|
246
|
+
toggle.setAttribute("aria-expanded", String(open));
|
|
247
|
+
toggle.classList.toggle("open", open);
|
|
248
|
+
});
|
|
249
|
+
line.append(toggle);
|
|
250
|
+
opts.line.forEach((c) => line.append(c));
|
|
251
|
+
row.append(line, details);
|
|
252
|
+
} else {
|
|
253
|
+
opts.line.forEach((c) => line.append(c));
|
|
254
|
+
row.append(line);
|
|
255
|
+
}
|
|
256
|
+
return row;
|
|
257
|
+
}
|
|
258
|
+
function focusNewRow(listEl) {
|
|
259
|
+
const last = listEl?.lastElementChild;
|
|
260
|
+
if (!(last instanceof HTMLElement)) return;
|
|
261
|
+
last.scrollIntoView({ block: "nearest" });
|
|
262
|
+
const name = last.querySelector("input.set-name") ?? last.querySelector("input");
|
|
263
|
+
name?.focus();
|
|
264
|
+
}
|
|
265
|
+
function dupGuard() {
|
|
266
|
+
const items = [];
|
|
267
|
+
const check = () => {
|
|
268
|
+
const counts = /* @__PURE__ */ new Map();
|
|
269
|
+
for (const it of items) {
|
|
270
|
+
const k = it.key().trim().toLowerCase();
|
|
271
|
+
if (k) counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
272
|
+
}
|
|
273
|
+
let any = false;
|
|
274
|
+
for (const it of items) {
|
|
275
|
+
const k = it.key().trim().toLowerCase();
|
|
276
|
+
const dup = !!k && (counts.get(k) ?? 0) > 1;
|
|
277
|
+
it.input.classList.toggle("invalid", dup);
|
|
278
|
+
if (dup) {
|
|
279
|
+
it.input.title = "Duplicate name. Names must be unique.";
|
|
280
|
+
any = true;
|
|
281
|
+
} else if (it.input.title === "Duplicate name. Names must be unique.") it.input.removeAttribute("title");
|
|
282
|
+
}
|
|
283
|
+
return any;
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
reset() {
|
|
287
|
+
items.length = 0;
|
|
288
|
+
},
|
|
289
|
+
track(input, key = () => input.value, also) {
|
|
290
|
+
items.push({ input, key });
|
|
291
|
+
input.addEventListener("input", check);
|
|
292
|
+
for (const a of also ?? []) a.addEventListener("input", check);
|
|
293
|
+
},
|
|
294
|
+
check,
|
|
295
|
+
firstDuplicate() {
|
|
296
|
+
check();
|
|
297
|
+
return items.find((it) => it.input.classList.contains("invalid"))?.input ?? null;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function mountSettingsDialog(opts) {
|
|
302
|
+
const dialog = el("dialog", "settings-dialog");
|
|
303
|
+
const form = el("form", "settings-form");
|
|
304
|
+
form.method = "dialog";
|
|
305
|
+
const tabs = el("nav", "settings-tabs");
|
|
306
|
+
const panels = el("div", "settings-panels");
|
|
307
|
+
const error = el("p", "settings-error");
|
|
308
|
+
error.hidden = true;
|
|
309
|
+
const cancel = el("button", "settings-cancel", "Cancel");
|
|
310
|
+
cancel.type = "button";
|
|
311
|
+
cancel.value = "cancel";
|
|
312
|
+
const save = el("button", "settings-save primary", "Save");
|
|
313
|
+
save.value = "save";
|
|
314
|
+
const panelFor = /* @__PURE__ */ new Map();
|
|
315
|
+
const tabFor = /* @__PURE__ */ new Map();
|
|
316
|
+
const handles = /* @__PURE__ */ new Map();
|
|
317
|
+
let active = opts.sections[0]?.id ?? "";
|
|
318
|
+
let lastGroup;
|
|
319
|
+
for (const s of opts.sections) {
|
|
320
|
+
if (s.group && s.group !== lastGroup) {
|
|
321
|
+
tabs.append(el("div", "settings-group", s.group));
|
|
322
|
+
lastGroup = s.group;
|
|
323
|
+
}
|
|
324
|
+
const tab = el("button", "settings-tab", s.label);
|
|
325
|
+
tab.type = "button";
|
|
326
|
+
tab.dataset["tab"] = s.id;
|
|
327
|
+
tab.addEventListener("click", () => showTab(s.id));
|
|
328
|
+
tabs.append(tab);
|
|
329
|
+
tabFor.set(s.id, tab);
|
|
330
|
+
const panel = el("section", "settings-panel");
|
|
331
|
+
panel.dataset["panel"] = s.id;
|
|
332
|
+
panel.hidden = true;
|
|
333
|
+
panels.append(panel);
|
|
334
|
+
panelFor.set(s.id, panel);
|
|
335
|
+
}
|
|
336
|
+
function showTab(id) {
|
|
337
|
+
active = id;
|
|
338
|
+
for (const [sid, t] of tabFor) t.classList.toggle("active", sid === id);
|
|
339
|
+
for (const [sid, p] of panelFor) p.hidden = sid !== id;
|
|
340
|
+
}
|
|
341
|
+
form.append(
|
|
342
|
+
el("h2", "settings-title", opts.title),
|
|
343
|
+
el("div", "settings-body", tabs, panels),
|
|
344
|
+
el("div", "settings-actions", error, cancel, save)
|
|
345
|
+
);
|
|
346
|
+
dialog.append(form);
|
|
347
|
+
document.body.append(dialog);
|
|
348
|
+
form.addEventListener("keydown", (e) => {
|
|
349
|
+
if (e.key === "Enter" && e.target?.tagName === "INPUT") e.preventDefault();
|
|
350
|
+
});
|
|
351
|
+
cancel.addEventListener("click", () => dialog.close("cancel"));
|
|
352
|
+
save.addEventListener("click", (e) => {
|
|
353
|
+
e.preventDefault();
|
|
354
|
+
error.hidden = true;
|
|
355
|
+
for (const s of opts.sections) {
|
|
356
|
+
const bad = handles.get(s.id)?.firstInvalid?.();
|
|
357
|
+
if (bad) {
|
|
358
|
+
showTab(s.id);
|
|
359
|
+
bad.focus();
|
|
360
|
+
error.textContent = "Fix the highlighted fields first.";
|
|
361
|
+
error.hidden = false;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
void (async () => {
|
|
366
|
+
await opts.onSave();
|
|
367
|
+
dialog.close("save");
|
|
368
|
+
})();
|
|
369
|
+
});
|
|
370
|
+
return {
|
|
371
|
+
open(tabId) {
|
|
372
|
+
handles.clear();
|
|
373
|
+
for (const s of opts.sections) {
|
|
374
|
+
const panel = panelFor.get(s.id);
|
|
375
|
+
panel.replaceChildren();
|
|
376
|
+
handles.set(s.id, s.mount(panel));
|
|
377
|
+
}
|
|
378
|
+
error.hidden = true;
|
|
379
|
+
showTab(tabId ?? active ?? opts.sections[0]?.id ?? "");
|
|
380
|
+
dialog.showModal();
|
|
381
|
+
},
|
|
382
|
+
destroy() {
|
|
383
|
+
dialog.remove();
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export { PALETTE, PALETTE_SIZE, colourFor, colourIndex, dupGuard, el, expandableRow, focusNewRow, iconBtn, labelled, mountPaneShell, mountSettingsDialog, moveItem, tagChips };
|
|
37
389
|
//# sourceMappingURL=index.js.map
|
|
38
390
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/dom.ts","../src/colour.ts"],"names":[],"mappings":";AAqBO,SAAS,EAAA,CACd,GAAA,EACA,gBAAA,EAAA,GACG,QAAA,EACuB;AAC1B,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,aAAA,CAAc,GAAG,CAAA;AACvC,EAAA,MAAM,KAAA,GAAiB,OAAO,gBAAA,KAAqB,QAAA,GAC/C,EAAE,SAAA,EAAW,gBAAA,EAAiB,GAC9B,gBAAA,IAAoB,EAAC;AACzB,EAAA,IAAI,KAAA,CAAM,SAAA,EAAW,IAAA,CAAK,SAAA,GAAY,KAAA,CAAM,SAAA;AAC5C,EAAA,IAAI,KAAA,CAAM,SAAS,MAAA,IAAa,KAAA,CAAM,SAAS,IAAA,EAAM,IAAA,CAAK,cAAc,KAAA,CAAM,IAAA;AAC9E,EAAA,IAAI,KAAA,CAAM,KAAA,KAAU,MAAA,EAAW,IAAA,CAAK,QAAQ,KAAA,CAAM,KAAA;AAClD,EAAA,IAAI,MAAM,OAAA,EAAS,IAAA,CAAK,gBAAA,CAAiB,OAAA,EAAS,MAAM,OAAwB,CAAA;AAChF,EAAA,KAAA,MAAW,SAAS,QAAA,EAAU;AAC5B,IAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,KAAA,KAAU,MAAA,EAAW;AAC3C,IAAA,IAAA,CAAK,OAAO,KAAK,CAAA;AAAA,EACnB;AACA,EAAA,OAAO,IAAA;AACT;;;AC3BO,IAAM,YAAA,GAAe;AAErB,IAAM,OAAA,GAAU,KAAA,CAAM,IAAA,CAAK,EAAE,MAAA,EAAQ,YAAA,EAAa,EAAG,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,WAAA,EAAc,CAAC,CAAA,CAAA,CAAG;AAIjF,SAAS,YAAY,IAAA,EAAsB;AAChD,EAAA,IAAI,CAAA,GAAI,UAAA;AACR,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,QAAQ,CAAA,EAAA,EAAK;AAAE,IAAA,CAAA,IAAK,IAAA,CAAK,WAAW,CAAC,CAAA;AAAG,IAAA,CAAA,GAAI,IAAA,CAAK,IAAA,CAAK,CAAA,EAAG,QAAU,CAAA;AAAA,EAAG;AAC/F,EAAA,CAAA,IAAK,CAAA,KAAM,EAAA;AAAI,EAAA,CAAA,GAAI,IAAA,CAAK,IAAA,CAAK,CAAA,EAAG,UAAU,CAAA;AAAG,EAAA,CAAA,IAAK,CAAA,KAAM,EAAA;AAAI,EAAA,CAAA,GAAI,IAAA,CAAK,IAAA,CAAK,CAAA,EAAG,UAAU,CAAA;AAAG,EAAA,CAAA,IAAK,CAAA,KAAM,EAAA;AACrG,EAAA,OAAA,CAAQ,MAAM,CAAA,IAAK,YAAA;AACrB;AAGO,SAAS,UAAU,IAAA,EAAsB;AAC9C,EAAA,OAAO,OAAA,CAAQ,WAAA,CAAY,IAAI,CAAC,CAAA;AAClC","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\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\nexport type Child = Node | string | null | undefined;\n\nexport interface ElProps {\n className?: string;\n text?: string;\n title?: 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.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 return node;\n}\n","// ---------------------------------------------------------------------------\n// The per-entity colour model (design-language.md section 4): a stable hash of\n// a name selects an INDEX into the curated palette - repeatable, storage-free.\n// The hues live in the token layer as `--char-0 .. --char-(N-1)` per theme, so\n// a colour reference adapts to light/dark with zero JS. Each app defines the\n// `--char-*` values in its own palette; this module only picks the slot.\n//\n// Storylet Studio uses it for dimension-value / zone chips; Patterpad for\n// character cues.\n// ---------------------------------------------------------------------------\n\n/** Curated colour slots; must match the `--char-*` set defined per theme. */\nexport const PALETTE_SIZE = 12;\n\nexport const PALETTE = Array.from({ length: PALETTE_SIZE }, (_, i) => `var(--char-${i})`) as readonly string[];\n\n/** Hash a name to a palette slot: FNV-1a + an fmix32 avalanche, well\n * distributed even for short, similar names. */\nexport function colourIndex(name: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < name.length; i++) { h ^= name.charCodeAt(i); h = Math.imul(h, 0x01000193); }\n h ^= h >>> 16; h = Math.imul(h, 0x85ebca6b); h ^= h >>> 13; h = Math.imul(h, 0xc2b2ae35); h ^= h >>> 16;\n return (h >>> 0) % PALETTE_SIZE;\n}\n\n/** The theme-aware CSS variable reference for a name's slot. */\nexport function colourFor(name: string): string {\n return PALETTE[colourIndex(name)]!;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/dom.ts","../src/colour.ts","../src/pane-shell.ts","../src/settings.ts"],"names":["el"],"mappings":"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","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\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\nexport type Child = Node | string | null | undefined;\n\nexport interface ElProps {\n className?: string;\n text?: string;\n title?: 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.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 return node;\n}\n\n/** A small square glyph button (the move/delete controls list editors share). */\nexport function iconBtn(glyph: string, title: string, onClick: () => void, disabled = false, danger = false): HTMLButtonElement {\n const b = el(\"button\", `shell-icon${danger ? \" danger\" : \"\"}`);\n b.type = \"button\"; b.textContent = glyph; b.title = title; b.setAttribute(\"aria-label\", title);\n b.disabled = disabled;\n b.addEventListener(\"click\", onClick);\n return b;\n}\n\n/** A captioned field: `<label class=\"shell-labelled\"><span class=\"shell-fieldcap\">…</span>control</label>`. */\nexport function labelled(label: string, control: HTMLElement): HTMLElement {\n const w = el(\"label\", \"shell-labelled\");\n w.append(el(\"span\", \"shell-fieldcap\", label), control);\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/** 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 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\", \"✕\");\n x.type = \"button\"; x.title = `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// The per-entity colour model (design-language.md section 4): a stable hash of\n// a name selects an INDEX into the curated palette - repeatable, storage-free.\n// The hues live in the token layer as `--char-0 .. --char-(N-1)` per theme, so\n// a colour reference adapts to light/dark with zero JS. Each app defines the\n// `--char-*` values in its own palette; this module only picks the slot.\n//\n// Storylet Studio uses it for dimension-value / zone chips; Patterpad for\n// character cues.\n// ---------------------------------------------------------------------------\n\n/** Curated colour slots; must match the `--char-*` set defined per theme. */\nexport const PALETTE_SIZE = 12;\n\nexport const PALETTE = Array.from({ length: PALETTE_SIZE }, (_, i) => `var(--char-${i})`) as readonly string[];\n\n/** Hash a name to a palette slot: FNV-1a + an fmix32 avalanche, well\n * distributed even for short, similar names. */\nexport function colourIndex(name: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < name.length; i++) { h ^= name.charCodeAt(i); h = Math.imul(h, 0x01000193); }\n h ^= h >>> 16; h = Math.imul(h, 0x85ebca6b); h ^= h >>> 13; h = Math.imul(h, 0xc2b2ae35); h ^= h >>> 16;\n return (h >>> 0) % PALETTE_SIZE;\n}\n\n/** The theme-aware CSS variable reference for a name's slot. */\nexport function colourFor(name: string): string {\n return PALETTE[colourIndex(name)]!;\n}\n","// ---------------------------------------------------------------------------\n// mountPaneShell: the app-frame both editors share - a top bar with compact\n// pane toggles, a nav | centre | inspector grid whose side panes collapse and\n// resize, and width/open-state persistence via a host callback. The host fills\n// the pane bodies + top-bar slots and owns its own keyboard accelerators\n// (call togglePane); the frame owns layout, collapse, drag-resize, and state.\n//\n// The frame CSS ships alongside as pane-shell.css (import it once at the app\n// root). It reads the shared token grammar (tokens.css).\n// ---------------------------------------------------------------------------\n\nexport type PaneSide = \"nav\" | \"inspector\";\n\nexport interface PaneSideConfig {\n /** The open width as a CSS length, e.g. \"14rem\" or \"224px\". */\n defaultWidth: string;\n /** Drag floor in px (default 160). */\n minWidth?: number;\n /** Drag ceiling in px from the window width (default w => min(640, w - 420)). */\n maxWidth?: (innerWidth: number) => number;\n /** Whether a drag seam is offered (default true). */\n resizable?: boolean;\n /** Noun for the toggle's tooltip/aria, e.g. \"navigator\" / \"inspector\". */\n label?: string;\n /** Shortcut hint appended to the tooltip, e.g. \"Cmd+1\". */\n shortcutHint?: string;\n}\n\nexport interface PaneShellState {\n open: Record<PaneSide, boolean>;\n /** Dragged widths in px; absent = the config default. */\n width: Partial<Record<PaneSide, number>>;\n}\n\nexport interface PaneShellOptions {\n nav: PaneSideConfig;\n inspector: PaneSideConfig;\n /** Initial open + width state (from the host's store). */\n initial?: Partial<PaneShellState>;\n /** Persist state after any toggle or resize (host wires it to its store). */\n onChange?: (state: PaneShellState) => void;\n}\n\nexport interface PaneShell {\n /** The frame element (top bar + panes); mount it into the app container. */\n readonly root: HTMLElement;\n readonly topbar: HTMLElement;\n /** Top-bar host slots: leading (title) and trailing (status), the host fills. */\n readonly topbarLead: HTMLElement;\n readonly topbarTrail: HTMLElement;\n /** Pane bodies the host fills (the fixed-width inner that clips on collapse). */\n readonly nav: HTMLElement;\n readonly centre: HTMLElement;\n readonly inspector: HTMLElement;\n togglePane(side: PaneSide): void;\n setPaneOpen(side: PaneSide, open: boolean): void;\n isOpen(side: PaneSide): boolean;\n /** Clear dragged widths back to the config defaults. */\n resetWidths(): void;\n /** A snapshot of the current state (open + widths). */\n state(): PaneShellState;\n destroy(): void;\n}\n\nconst DEFAULT_MAX = (w: number): number => Math.min(640, w - 420);\nconst openVar: Record<PaneSide, string> = { nav: \"--nav-open-w\", inspector: \"--insp-open-w\" };\nconst collapseClass: Record<PaneSide, string> = { nav: \"no-nav\", inspector: \"no-inspector\" };\n\nexport function mountPaneShell(host: HTMLElement, opts: PaneShellOptions): PaneShell {\n const cfg: Record<PaneSide, PaneSideConfig> = { nav: opts.nav, inspector: opts.inspector };\n const open: Record<PaneSide, boolean> = { nav: opts.initial?.open?.nav ?? true, inspector: opts.initial?.open?.inspector ?? true };\n const width: Partial<Record<PaneSide, number>> = { ...(opts.initial?.width ?? {}) };\n\n const el = <K extends keyof HTMLElementTagNameMap>(tag: K, cls?: string): HTMLElementTagNameMap[K] => {\n const n = document.createElement(tag);\n if (cls) n.className = cls;\n return n;\n };\n\n // --- top bar ---------------------------------------------------------------\n const navToggle = el(\"button\", \"pane-toggle\");\n navToggle.type = \"button\";\n const inspToggle = el(\"button\", \"pane-toggle\");\n inspToggle.type = \"button\";\n const topbarLead = el(\"div\", \"topbar-lead\");\n const topbarTrail = el(\"div\", \"topbar-trail\");\n const spacer = el(\"span\", \"topbar-spacer\");\n const topbar = el(\"header\", \"topbar\");\n topbar.append(navToggle, topbarLead, spacer, topbarTrail, inspToggle);\n\n // --- panes -----------------------------------------------------------------\n const panes = el(\"main\", \"panes\");\n const navPane = el(\"nav\", \"pane pane-nav\");\n const navInner = el(\"div\", \"pane-inner\");\n navPane.append(navInner);\n const centre = el(\"section\", \"pane-centre\");\n const inspPane = el(\"aside\", \"pane pane-inspector\");\n const inspInner = el(\"div\", \"pane-inner\");\n inspPane.append(inspInner);\n const navResizer = el(\"div\", \"pane-resizer pane-resizer-nav\");\n const inspResizer = el(\"div\", \"pane-resizer pane-resizer-insp\");\n panes.append(navPane, centre, inspPane, navResizer, inspResizer);\n\n const root = el(\"div\", \"pane-shell\");\n root.append(topbar, panes);\n host.append(root);\n\n // --- state application -----------------------------------------------------\n function applyToggleGlyph(side: PaneSide, button: HTMLButtonElement): void {\n const isOpen = open[side];\n // Chevron points toward where the pane collapses to.\n button.textContent = side === \"nav\" ? (isOpen ? \"‹\" : \"›\") : (isOpen ? \"›\" : \"‹\");\n const noun = cfg[side].label ?? side;\n const hint = cfg[side].shortcutHint ? ` (${cfg[side].shortcutHint})` : \"\";\n button.title = `${isOpen ? \"Hide\" : \"Show\"} ${noun}${hint}`;\n button.setAttribute(\"aria-label\", button.title);\n button.setAttribute(\"aria-pressed\", String(isOpen));\n }\n\n function apply(): void {\n panes.classList.toggle(collapseClass.nav, !open.nav);\n panes.classList.toggle(collapseClass.inspector, !open.inspector);\n // The open widths live on the grid: the track and each .pane-inner read them.\n panes.style.setProperty(openVar.nav, width.nav !== undefined ? `${width.nav}px` : cfg.nav.defaultWidth);\n panes.style.setProperty(openVar.inspector, width.inspector !== undefined ? `${width.inspector}px` : cfg.inspector.defaultWidth);\n applyToggleGlyph(\"nav\", navToggle);\n applyToggleGlyph(\"inspector\", inspToggle);\n navResizer.style.display = cfg.nav.resizable === false ? \"none\" : \"\";\n inspResizer.style.display = cfg.inspector.resizable === false ? \"none\" : \"\";\n }\n\n const snapshot = (): PaneShellState => ({ open: { ...open }, width: { ...width } });\n const persist = (): void => opts.onChange?.(snapshot());\n\n function setPaneOpen(side: PaneSide, next: boolean): void {\n if (open[side] === next) return;\n open[side] = next;\n apply();\n persist();\n }\n function togglePane(side: PaneSide): void { setPaneOpen(side, !open[side]); }\n function resetWidths(): void { delete width.nav; delete width.inspector; apply(); persist(); }\n\n navToggle.addEventListener(\"click\", () => togglePane(\"nav\"));\n inspToggle.addEventListener(\"click\", () => togglePane(\"inspector\"));\n\n // --- drag resize -----------------------------------------------------------\n function beginResize(side: PaneSide, startEvent: PointerEvent): void {\n if (cfg[side].resizable === false || !open[side]) return;\n startEvent.preventDefault();\n const pane = side === \"nav\" ? navPane : inspPane;\n const min = cfg[side].minWidth ?? 160;\n const max = cfg[side].maxWidth ?? DEFAULT_MAX;\n const startX = startEvent.clientX;\n const startW = pane.getBoundingClientRect().width;\n document.body.classList.add(\"pane-resizing\");\n let latest = startW;\n const onMove = (e: PointerEvent): void => {\n const delta = side === \"nav\" ? e.clientX - startX : startX - e.clientX;\n latest = Math.max(min, Math.min(max(window.innerWidth), startW + delta));\n panes.style.setProperty(openVar[side], `${latest}px`);\n };\n const onUp = (): void => {\n window.removeEventListener(\"pointermove\", onMove);\n window.removeEventListener(\"pointerup\", onUp);\n document.body.classList.remove(\"pane-resizing\");\n width[side] = latest;\n persist();\n };\n window.addEventListener(\"pointermove\", onMove);\n window.addEventListener(\"pointerup\", onUp);\n }\n navResizer.addEventListener(\"pointerdown\", (e) => beginResize(\"nav\", e));\n inspResizer.addEventListener(\"pointerdown\", (e) => beginResize(\"inspector\", e));\n navResizer.addEventListener(\"dblclick\", () => { delete width.nav; apply(); persist(); });\n inspResizer.addEventListener(\"dblclick\", () => { delete width.inspector; apply(); persist(); });\n\n apply();\n\n return {\n root, topbar, topbarLead, topbarTrail,\n nav: navInner, centre, inspector: inspInner,\n togglePane, setPaneOpen, isOpen: (s) => open[s], resetWidths, state: snapshot,\n destroy: () => { root.remove(); },\n };\n}\n","// ---------------------------------------------------------------------------\n// The shared settings surface: a native <dialog> with a left-rail of grouped\n// tabs and one panel per section (mountSettingsDialog), plus the list-editing\n// core those sections lean on (expandableRow, focusNewRow, dupGuard). The app\n// supplies the sections; each holds its own state and reports the first invalid\n// control so the Save gate can block + jump. Framework here, content in the app.\n//\n// Lifted from Patterpad's Project Settings (settings-list.ts + the inlined\n// dialog logic in its renderer), with neutral class names. Styling ships as\n// settings.css.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\n\n// --- list-editing core --------------------------------------------------------\n\n/** A compact list row: a single `.set-rowline` (inline controls) plus, when\n * `details` are given, a disclosure that reveals a `.set-details` panel below,\n * so a list scans like a table with secondary fields tucked away. */\nexport function expandableRow(opts: { line: HTMLElement[]; details?: HTMLElement[] }): HTMLElement {\n const row = el(\"div\", \"set-row\");\n const line = el(\"div\", \"set-rowline\");\n if (opts.details && opts.details.length) {\n const details = el(\"div\", \"set-details\"); details.hidden = true;\n for (const d of opts.details) details.append(d);\n // The triangle is drawn by CSS off `.open` (a text glyph stays hairline-thin).\n const toggle = el(\"button\", \"set-expand\"); toggle.type = \"button\";\n toggle.title = \"More\"; toggle.setAttribute(\"aria-label\", \"More\");\n toggle.setAttribute(\"aria-expanded\", \"false\");\n toggle.addEventListener(\"click\", () => {\n const open = details.hidden;\n details.hidden = !open;\n toggle.setAttribute(\"aria-expanded\", String(open));\n toggle.classList.toggle(\"open\", open);\n });\n line.append(toggle);\n opts.line.forEach((c) => line.append(c));\n row.append(line, details);\n } else {\n opts.line.forEach((c) => line.append(c));\n row.append(line);\n }\n return row;\n}\n\n/** After a \"+ Add\" re-renders a list, bring the new (last) row into view and\n * focus its name input (`.set-name`, else the first input). */\nexport function focusNewRow(listEl: HTMLElement | null | undefined): void {\n const last = listEl?.lastElementChild;\n if (!(last instanceof HTMLElement)) return;\n last.scrollIntoView({ block: \"nearest\" });\n const name = last.querySelector<HTMLInputElement>(\"input.set-name\") ?? last.querySelector<HTMLInputElement>(\"input\");\n name?.focus();\n}\n\nexport interface DupGuard {\n /** Forget the previous render's inputs (call at the top of render). */\n reset(): void;\n /** Register a name input; `key` returns the comparison string (default the\n * input's value). Re-checks live on input. `also` = extra inputs that change the key. */\n track(input: HTMLInputElement, key?: () => string, also?: HTMLInputElement[]): void;\n /** Re-paint duplicates; true if any two non-blank keys collide. */\n check(): boolean;\n /** The first input currently flagged (after a fresh check), or null. */\n firstDuplicate(): HTMLInputElement | null;\n}\n\n/** Case-insensitive duplicate-name detection: paints clashing inputs `.invalid`\n * and lets a Save gate block + jump via `firstDuplicate()`. */\nexport function dupGuard(): DupGuard {\n const items: Array<{ input: HTMLInputElement; key: () => string }> = [];\n const check = (): boolean => {\n const counts = new Map<string, number>();\n for (const it of items) { const k = it.key().trim().toLowerCase(); if (k) counts.set(k, (counts.get(k) ?? 0) + 1); }\n let any = false;\n for (const it of items) {\n const k = it.key().trim().toLowerCase();\n const dup = !!k && (counts.get(k) ?? 0) > 1;\n it.input.classList.toggle(\"invalid\", dup);\n if (dup) { it.input.title = \"Duplicate name. Names must be unique.\"; any = true; }\n else if (it.input.title === \"Duplicate name. Names must be unique.\") it.input.removeAttribute(\"title\");\n }\n return any;\n };\n return {\n reset() { items.length = 0; },\n track(input, key = () => input.value, also) {\n items.push({ input, key });\n input.addEventListener(\"input\", check);\n for (const a of also ?? []) a.addEventListener(\"input\", check);\n },\n check,\n firstDuplicate() { check(); return items.find((it) => it.input.classList.contains(\"invalid\"))?.input ?? null; },\n };\n}\n\n// --- the settings dialog ------------------------------------------------------\n\nexport interface SettingsSectionHandle {\n /** The first invalid control to jump to, blocking Save (e.g. a duplicate name). */\n firstInvalid?(): HTMLElement | null;\n}\n\nexport interface SettingsSection {\n id: string;\n label: string;\n /** Optional left-rail group heading above this tab. */\n group?: string;\n /** Build the section body into `host`; called fresh on each open so it reads\n * the current data. Return a handle (for the Save gate). */\n mount(host: HTMLElement): SettingsSectionHandle;\n}\n\nexport interface SettingsDialogOptions {\n title: string;\n sections: SettingsSection[];\n /** Fired when Save is clicked and every section validates. The app reads its\n * own section state and persists. May be async; the dialog stays open until\n * it resolves, then closes. */\n onSave: () => void | Promise<void>;\n}\n\nexport interface SettingsDialog {\n /** Open (optionally on a given tab id). Sections re-mount from current data. */\n open(tabId?: string): void;\n destroy(): void;\n}\n\n/** Build a settings dialog. The frame owns the modal, the tab rail, save-on-Save\n * (with a validation gate), and Cancel; the sections own their content + state. */\nexport function mountSettingsDialog(opts: SettingsDialogOptions): SettingsDialog {\n const dialog = el(\"dialog\", \"settings-dialog\");\n const form = el(\"form\", \"settings-form\"); form.method = \"dialog\";\n const tabs = el(\"nav\", \"settings-tabs\");\n const panels = el(\"div\", \"settings-panels\");\n const error = el(\"p\", \"settings-error\"); error.hidden = true;\n const cancel = el(\"button\", \"settings-cancel\", \"Cancel\"); cancel.type = \"button\"; cancel.value = \"cancel\";\n const save = el(\"button\", \"settings-save primary\", \"Save\"); save.value = \"save\";\n\n const panelFor = new Map<string, HTMLElement>();\n const tabFor = new Map<string, HTMLButtonElement>();\n const handles = new Map<string, SettingsSectionHandle>();\n let active = opts.sections[0]?.id ?? \"\";\n\n let lastGroup: string | undefined;\n for (const s of opts.sections) {\n if (s.group && s.group !== lastGroup) { tabs.append(el(\"div\", \"settings-group\", s.group)); lastGroup = s.group; }\n const tab = el(\"button\", \"settings-tab\", s.label); tab.type = \"button\"; tab.dataset[\"tab\"] = s.id;\n tab.addEventListener(\"click\", () => showTab(s.id));\n tabs.append(tab); tabFor.set(s.id, tab);\n const panel = el(\"section\", \"settings-panel\"); panel.dataset[\"panel\"] = s.id; panel.hidden = true;\n panels.append(panel); panelFor.set(s.id, panel);\n }\n\n function showTab(id: string): void {\n active = id;\n for (const [sid, t] of tabFor) t.classList.toggle(\"active\", sid === id);\n for (const [sid, p] of panelFor) p.hidden = sid !== id;\n }\n\n form.append(\n el(\"h2\", \"settings-title\", opts.title),\n el(\"div\", \"settings-body\", tabs, panels),\n el(\"div\", \"settings-actions\", error, cancel, save),\n );\n dialog.append(form);\n document.body.append(dialog);\n\n // Enter should not submit while editing a field; only the Save button does.\n form.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" && (e.target as HTMLElement)?.tagName === \"INPUT\") e.preventDefault();\n });\n\n cancel.addEventListener(\"click\", () => dialog.close(\"cancel\"));\n save.addEventListener(\"click\", (e) => {\n e.preventDefault();\n error.hidden = true;\n for (const s of opts.sections) {\n const bad = handles.get(s.id)?.firstInvalid?.();\n if (bad) { showTab(s.id); bad.focus(); error.textContent = \"Fix the highlighted fields first.\"; error.hidden = false; return; }\n }\n void (async () => { await opts.onSave(); dialog.close(\"save\"); })();\n });\n\n return {\n open(tabId) {\n handles.clear();\n for (const s of opts.sections) {\n const panel = panelFor.get(s.id)!;\n panel.replaceChildren();\n handles.set(s.id, s.mount(panel));\n }\n error.hidden = true;\n showTab(tabId ?? active ?? opts.sections[0]?.id ?? \"\");\n dialog.showModal();\n },\n destroy() { dialog.remove(); },\n };\n}\n"]}
|
package/dist/menu.cjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/menu.ts
|
|
4
|
+
var PANE_MENU = {
|
|
5
|
+
showNav: { label: "Show Navigator", accelerator: "CmdOrCtrl+1" },
|
|
6
|
+
showInspector: { label: "Show Inspector", accelerator: "CmdOrCtrl+2" },
|
|
7
|
+
resetView: { label: "Reset View" }
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
exports.PANE_MENU = PANE_MENU;
|
|
11
|
+
//# sourceMappingURL=menu.cjs.map
|
|
12
|
+
//# sourceMappingURL=menu.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/menu.ts"],"names":[],"mappings":";;;AAeO,IAAM,SAAA,GAIT;AAAA,EACF,OAAA,EAAS,EAAE,KAAA,EAAO,gBAAA,EAAkB,aAAa,aAAA,EAAc;AAAA,EAC/D,aAAA,EAAe,EAAE,KAAA,EAAO,gBAAA,EAAkB,aAAa,aAAA,EAAc;AAAA,EACrE,SAAA,EAAW,EAAE,KAAA,EAAO,YAAA;AACtB","file":"menu.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// Shell-owned menu naming: the labels + accelerators for the pane-frame menu\n// items, single-sourced so every app in the family names them identically\n// (and a rename here flows to all of them). Pure data - no electron, no DOM -\n// so a main-process menu builder can import it. Each app builds its own\n// MenuItem from these and wires the click to its shell controller.\n// ---------------------------------------------------------------------------\n\nexport interface MenuLabel {\n label: string;\n /** Electron accelerator string, when the item has one. */\n accelerator?: string;\n}\n\n/** The pane-frame menu items (a View menu, conventionally). */\nexport const PANE_MENU: {\n showNav: MenuLabel;\n showInspector: MenuLabel;\n resetView: MenuLabel;\n} = {\n showNav: { label: \"Show Navigator\", accelerator: \"CmdOrCtrl+1\" },\n showInspector: { label: \"Show Inspector\", accelerator: \"CmdOrCtrl+2\" },\n resetView: { label: \"Reset View\" },\n};\n"]}
|
package/dist/menu.d.cts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface MenuLabel {
|
|
2
|
+
label: string;
|
|
3
|
+
/** Electron accelerator string, when the item has one. */
|
|
4
|
+
accelerator?: string;
|
|
5
|
+
}
|
|
6
|
+
/** The pane-frame menu items (a View menu, conventionally). */
|
|
7
|
+
declare const PANE_MENU: {
|
|
8
|
+
showNav: MenuLabel;
|
|
9
|
+
showInspector: MenuLabel;
|
|
10
|
+
resetView: MenuLabel;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { type MenuLabel, PANE_MENU };
|
package/dist/menu.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface MenuLabel {
|
|
2
|
+
label: string;
|
|
3
|
+
/** Electron accelerator string, when the item has one. */
|
|
4
|
+
accelerator?: string;
|
|
5
|
+
}
|
|
6
|
+
/** The pane-frame menu items (a View menu, conventionally). */
|
|
7
|
+
declare const PANE_MENU: {
|
|
8
|
+
showNav: MenuLabel;
|
|
9
|
+
showInspector: MenuLabel;
|
|
10
|
+
resetView: MenuLabel;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { type MenuLabel, PANE_MENU };
|