@fluixi/devtools 0.2.0-alpha.2 → 0.2.0-alpha.4

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 (158) hide show
  1. package/README.md +80 -2
  2. package/dist/callsite.cjs +72 -0
  3. package/dist/callsite.d.ts +22 -0
  4. package/dist/callsite.d.ts.map +1 -0
  5. package/dist/callsite.js +82 -0
  6. package/dist/callsite.mjs +51 -0
  7. package/dist/change-view.cjs +134 -0
  8. package/dist/change-view.d.ts +10 -0
  9. package/dist/change-view.d.ts.map +1 -0
  10. package/dist/change-view.js +102 -0
  11. package/dist/change-view.mjs +111 -0
  12. package/dist/copy.cjs +63 -0
  13. package/dist/copy.d.ts +9 -0
  14. package/dist/copy.d.ts.map +1 -0
  15. package/dist/copy.js +52 -0
  16. package/dist/copy.mjs +42 -0
  17. package/dist/describe.cjs +55 -0
  18. package/dist/describe.d.ts +3 -0
  19. package/dist/describe.d.ts.map +1 -0
  20. package/dist/describe.js +47 -0
  21. package/dist/describe.mjs +34 -0
  22. package/dist/detail-panel.cjs +307 -0
  23. package/dist/detail-panel.d.ts +17 -0
  24. package/dist/detail-panel.d.ts.map +1 -0
  25. package/dist/detail-panel.js +217 -0
  26. package/dist/detail-panel.mjs +284 -0
  27. package/dist/diff.cjs +225 -0
  28. package/dist/diff.d.ts +31 -0
  29. package/dist/diff.d.ts.map +1 -0
  30. package/dist/diff.js +132 -0
  31. package/dist/diff.mjs +202 -0
  32. package/dist/dom.cjs +204 -0
  33. package/dist/dom.d.ts +81 -0
  34. package/dist/dom.d.ts.map +1 -0
  35. package/dist/dom.js +269 -0
  36. package/dist/dom.mjs +183 -0
  37. package/dist/export.cjs +221 -0
  38. package/dist/export.d.ts +55 -0
  39. package/dist/export.d.ts.map +1 -0
  40. package/dist/export.js +90 -0
  41. package/dist/export.mjs +209 -0
  42. package/dist/float.cjs +232 -0
  43. package/dist/float.d.ts +54 -0
  44. package/dist/float.d.ts.map +1 -0
  45. package/dist/float.js +256 -0
  46. package/dist/float.mjs +211 -0
  47. package/dist/format.cjs +175 -0
  48. package/dist/format.d.ts +16 -0
  49. package/dist/format.d.ts.map +1 -0
  50. package/dist/format.js +188 -0
  51. package/dist/format.mjs +154 -0
  52. package/dist/graph-view.cjs +893 -0
  53. package/dist/graph-view.d.ts +79 -2
  54. package/dist/graph-view.d.ts.map +1 -1
  55. package/dist/graph-view.js +866 -50
  56. package/dist/graph-view.mjs +870 -0
  57. package/dist/highlight.cjs +140 -0
  58. package/dist/highlight.d.ts +56 -0
  59. package/dist/highlight.d.ts.map +1 -0
  60. package/dist/highlight.js +154 -0
  61. package/dist/highlight.mjs +119 -0
  62. package/dist/history.cjs +95 -0
  63. package/dist/history.d.ts +59 -0
  64. package/dist/history.d.ts.map +1 -0
  65. package/dist/history.js +85 -0
  66. package/dist/history.mjs +74 -0
  67. package/dist/hook.cjs +1562 -0
  68. package/dist/hook.d.ts +60 -0
  69. package/dist/hook.d.ts.map +1 -0
  70. package/dist/hook.js +142 -0
  71. package/dist/hook.mjs +1553 -0
  72. package/dist/index.cjs +4552 -0
  73. package/dist/index.d.ts +44 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +25 -1
  76. package/dist/index.mjs +4544 -0
  77. package/dist/inspector.cjs +688 -0
  78. package/dist/inspector.d.ts +60 -0
  79. package/dist/inspector.d.ts.map +1 -0
  80. package/dist/inspector.js +415 -0
  81. package/dist/inspector.mjs +676 -0
  82. package/dist/instrument.cjs +497 -0
  83. package/dist/instrument.d.ts +147 -5
  84. package/dist/instrument.d.ts.map +1 -1
  85. package/dist/instrument.js +164 -44
  86. package/dist/instrument.mjs +487 -0
  87. package/dist/kind-filter.cjs +172 -0
  88. package/dist/kind-filter.d.ts +39 -0
  89. package/dist/kind-filter.d.ts.map +1 -0
  90. package/dist/kind-filter.js +151 -0
  91. package/dist/kind-filter.mjs +151 -0
  92. package/dist/menu.cjs +141 -0
  93. package/dist/menu.d.ts +37 -0
  94. package/dist/menu.d.ts.map +1 -0
  95. package/dist/menu.js +125 -0
  96. package/dist/menu.mjs +120 -0
  97. package/dist/observe.cjs +1352 -0
  98. package/dist/observe.d.ts +80 -0
  99. package/dist/observe.d.ts.map +1 -0
  100. package/dist/observe.js +893 -0
  101. package/dist/observe.mjs +1345 -0
  102. package/dist/palette.cjs +52 -0
  103. package/dist/palette.d.ts +11 -0
  104. package/dist/palette.d.ts.map +1 -0
  105. package/dist/palette.js +27 -0
  106. package/dist/palette.mjs +31 -0
  107. package/dist/path.cjs +45 -0
  108. package/dist/path.d.ts +19 -0
  109. package/dist/path.d.ts.map +1 -0
  110. package/dist/path.js +49 -0
  111. package/dist/path.mjs +24 -0
  112. package/dist/plugin.cjs +87 -0
  113. package/dist/plugin.d.ts +121 -0
  114. package/dist/plugin.d.ts.map +1 -0
  115. package/dist/plugin.js +74 -0
  116. package/dist/plugin.mjs +66 -0
  117. package/dist/plugins/context.cjs +98 -0
  118. package/dist/plugins/context.d.ts +10 -0
  119. package/dist/plugins/context.d.ts.map +1 -0
  120. package/dist/plugins/context.js +80 -0
  121. package/dist/plugins/context.mjs +75 -0
  122. package/dist/plugins/directives.cjs +98 -0
  123. package/dist/plugins/directives.d.ts +25 -0
  124. package/dist/plugins/directives.d.ts.map +1 -0
  125. package/dist/plugins/directives.js +78 -0
  126. package/dist/plugins/directives.mjs +75 -0
  127. package/dist/plugins/index.cjs +162 -0
  128. package/dist/plugins/index.d.ts +7 -0
  129. package/dist/plugins/index.d.ts.map +1 -0
  130. package/dist/plugins/index.js +9 -0
  131. package/dist/plugins/index.mjs +140 -0
  132. package/dist/source.cjs +111 -0
  133. package/dist/source.d.ts +72 -0
  134. package/dist/source.d.ts.map +1 -0
  135. package/dist/source.js +153 -0
  136. package/dist/source.mjs +90 -0
  137. package/dist/timeline.cjs +350 -0
  138. package/dist/timeline.d.ts +30 -0
  139. package/dist/timeline.d.ts.map +1 -0
  140. package/dist/timeline.js +271 -0
  141. package/dist/timeline.mjs +327 -0
  142. package/dist/tree.cjs +574 -0
  143. package/dist/tree.d.ts +128 -0
  144. package/dist/tree.d.ts.map +1 -0
  145. package/dist/tree.js +645 -0
  146. package/dist/tree.mjs +551 -0
  147. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  148. package/dist/value-pane.cjs +223 -0
  149. package/dist/value-pane.d.ts +10 -0
  150. package/dist/value-pane.d.ts.map +1 -0
  151. package/dist/value-pane.js +61 -0
  152. package/dist/value-pane.mjs +200 -0
  153. package/dist/wire.cjs +109 -0
  154. package/dist/wire.d.ts +100 -0
  155. package/dist/wire.d.ts.map +1 -0
  156. package/dist/wire.js +93 -0
  157. package/dist/wire.mjs +89 -0
  158. package/package.json +17 -5
@@ -0,0 +1,151 @@
1
+ /** Every kind a graph can hold, in the order they are offered. */
2
+ export const FILTERABLE_KINDS = [
3
+ 'state', 'memo', 'effect', 'store', 'resource', 'component', 'provider', 'control', 'router', 'handler',
4
+ 'context', 'directive',
5
+ ];
6
+ /** Only `handler` needs renaming; the other kinds read fine as they are. */
7
+ const LABEL = { handler: 'route' };
8
+ /**
9
+ * Builds the control inside `host`. Call `destroy` to remove it — it holds a document
10
+ * listener for closing the panel on an outside click.
11
+ */
12
+ export function createKindFilter(host, options) {
13
+ const doc = host.ownerDocument;
14
+ const kinds = options.kinds ?? FILTERABLE_KINDS;
15
+ const on = new Set(kinds);
16
+ const root = doc.createElement('div');
17
+ root.className = 'fx-kinds';
18
+ const button = doc.createElement('button');
19
+ button.type = 'button';
20
+ button.className = 'fx-kinds-button fx-dropdown-btn';
21
+ button.setAttribute('aria-haspopup', 'true');
22
+ button.setAttribute('aria-expanded', 'false');
23
+ button.title = 'Choose which kinds of node the graph draws';
24
+ const caret = doc.createElement('div');
25
+ caret.className = 'fx-dropdown-caret';
26
+ const panel = doc.createElement('div');
27
+ panel.className = 'fx-kinds-panel';
28
+ if (options.panelClass) {
29
+ panel.classList.add(...options.panelClass.split(/\s+/).filter((c) => c));
30
+ }
31
+ panel.hidden = true;
32
+ const all = doc.createElement('input');
33
+ all.type = 'checkbox';
34
+ const allRow = doc.createElement('label');
35
+ allRow.className = 'fx-kinds-all';
36
+ allRow.append(all, doc.createTextNode('all'));
37
+ const boxes = new Map();
38
+ for (const kind of kinds) {
39
+ const box = doc.createElement('input');
40
+ box.type = 'checkbox';
41
+ box.checked = true;
42
+ const row = doc.createElement('label');
43
+ row.append(box, doc.createTextNode(LABEL[kind] ?? kind));
44
+ panel.append(row);
45
+ boxes.set(kind, box);
46
+ }
47
+ panel.prepend(allRow);
48
+ root.append(button, panel);
49
+ host.append(root);
50
+ const everything = () => on.size === kinds.length;
51
+ // Read back in the order they are offered: a Set re-orders as entries come and go.
52
+ const chosen = () => kinds.filter((kind) => on.has(kind));
53
+ function paint() {
54
+ const name = options.label ?? 'Kinds';
55
+ button.textContent = everything() ? name : `${name} · ${on.size}`;
56
+ button.classList.toggle('on', !everything());
57
+ all.checked = everything();
58
+ button.appendChild(caret);
59
+ // Partly selected: show the box as indeterminate rather than guessing a state.
60
+ all.indeterminate = on.size > 0 && !everything();
61
+ for (const [kind, box] of boxes)
62
+ box.checked = on.has(kind);
63
+ }
64
+ function report() {
65
+ options.onChange(everything() ? null : chosen());
66
+ }
67
+ for (const [kind, box] of boxes) {
68
+ box.addEventListener('change', () => {
69
+ if (box.checked)
70
+ on.add(kind);
71
+ else
72
+ on.delete(kind);
73
+ paint();
74
+ report();
75
+ });
76
+ }
77
+ all.addEventListener('change', () => {
78
+ // Unticking would select nothing, leaving an empty graph with no obvious way back,
79
+ // so either direction turns everything on.
80
+ for (const kind of kinds)
81
+ on.add(kind);
82
+ paint();
83
+ report();
84
+ });
85
+ const close = () => {
86
+ panel.hidden = true;
87
+ button.setAttribute('aria-expanded', 'false');
88
+ };
89
+ button.addEventListener('click', (event) => {
90
+ event.stopPropagation();
91
+ panel.hidden = !panel.hidden;
92
+ button.setAttribute('aria-expanded', String(!panel.hidden));
93
+ });
94
+ // Stop clicks inside the panel reaching the document listener, which would close it.
95
+ panel.addEventListener('click', (event) => event.stopPropagation());
96
+ const away = () => close();
97
+ const key = (event) => {
98
+ if (event.key === 'Escape')
99
+ close();
100
+ };
101
+ doc.addEventListener('click', away);
102
+ doc.addEventListener('keydown', key);
103
+ paint();
104
+ return {
105
+ get selected() {
106
+ return chosen();
107
+ },
108
+ select(next) {
109
+ on.clear();
110
+ for (const kind of next ?? kinds)
111
+ on.add(kind);
112
+ paint();
113
+ },
114
+ destroy() {
115
+ doc.removeEventListener('click', away);
116
+ doc.removeEventListener('keydown', key);
117
+ root.remove();
118
+ },
119
+ };
120
+ }
121
+ export const KIND_FILTER_CSS = `
122
+ .fx-kinds { position: relative; display: inline-flex; }
123
+ .fx-kinds-button { white-space: nowrap; }
124
+ /* The hidden attribute is display:none from the user agent, which a class setting display outranks. */
125
+ .fx-kinds-panel[hidden] { display: none; }
126
+ .fx-kinds-panel {
127
+ position: absolute; top: calc(100% + 4px); z-index: 40;
128
+ display: grid; gap: 2px; padding: 6px 8px; min-width: 132px;
129
+ background: var(--fx-panel, #14161c);
130
+ border: 1px solid var(--fx-line, #262a35); border-radius: 6px;
131
+ box-shadow: 0 6px 18px rgba(0, 0, 0, .38);
132
+ }
133
+ .fx-kinds-panel:not(.fx-panel-left){ right: 0; }
134
+ .fx-kinds-panel.fx-panel-left { left: 0; }
135
+ .fx-kinds-panel label {
136
+ display: flex; align-items: center; gap: 6px;
137
+ font-size: 11px; color: var(--fx-muted, #8a90a2); cursor: pointer; user-select: none;
138
+ }
139
+ .fx-kinds-panel label:hover { color: var(--fx-label, #e7e9ee); }
140
+ .fx-kinds-panel input { width: 11px; height: 11px; margin: 0; accent-color: var(--fx-accent, #5eead4); }
141
+ .fx-kinds-all {
142
+ padding-bottom: 4px; margin-bottom: 2px;
143
+ border-bottom: 1px solid var(--fx-line, #262a35);
144
+ color: var(--fx-label, #e7e9ee) !important;
145
+ }
146
+ `;
147
+ export const DROPDOWN_FILTER_CSS = `
148
+ .fx-dropdown-btn { background-color: transparent; appearance: none; border: 1px solid var(--line-strong); outline: none; border-radius: .25rem; padding-block: .25rem; padding-inline: .875rem; font-size: .75rem; cursor: pointer }
149
+ .fx-dropdown-btn:hover { border-color: color-mix(in oklab, var(--accent) 50%, transparent); background-color: var(--bg); }
150
+ .fx-dropdown-caret { width: 0;height: 0;border-left: .15rem solid transparent; border-right: .15rem solid transparent; border-top: .15rem solid currentColor; transition: transform 0.2s ease; right: .4rem; top: 50%; color: color-mix(in oklab, var(--muted) 50%, transparent); position: absolute; }
151
+ `;
@@ -0,0 +1,151 @@
1
+ // src/kind-filter.ts
2
+ var FILTERABLE_KINDS = [
3
+ "state",
4
+ "memo",
5
+ "effect",
6
+ "store",
7
+ "resource",
8
+ "component",
9
+ "provider",
10
+ "control",
11
+ "router",
12
+ "handler",
13
+ "context",
14
+ "directive"
15
+ ];
16
+ var LABEL = { handler: "route" };
17
+ function createKindFilter(host, options) {
18
+ const doc = host.ownerDocument;
19
+ const kinds = options.kinds ?? FILTERABLE_KINDS;
20
+ const on = new Set(kinds);
21
+ const root = doc.createElement("div");
22
+ root.className = "fx-kinds";
23
+ const button = doc.createElement("button");
24
+ button.type = "button";
25
+ button.className = "fx-kinds-button fx-dropdown-btn";
26
+ button.setAttribute("aria-haspopup", "true");
27
+ button.setAttribute("aria-expanded", "false");
28
+ button.title = "Choose which kinds of node the graph draws";
29
+ const caret = doc.createElement("div");
30
+ caret.className = "fx-dropdown-caret";
31
+ const panel = doc.createElement("div");
32
+ panel.className = "fx-kinds-panel";
33
+ if (options.panelClass) {
34
+ panel.classList.add(...options.panelClass.split(/\s+/).filter((c) => c));
35
+ }
36
+ panel.hidden = true;
37
+ const all = doc.createElement("input");
38
+ all.type = "checkbox";
39
+ const allRow = doc.createElement("label");
40
+ allRow.className = "fx-kinds-all";
41
+ allRow.append(all, doc.createTextNode("all"));
42
+ const boxes = /* @__PURE__ */ new Map();
43
+ for (const kind of kinds) {
44
+ const box = doc.createElement("input");
45
+ box.type = "checkbox";
46
+ box.checked = true;
47
+ const row = doc.createElement("label");
48
+ row.append(box, doc.createTextNode(LABEL[kind] ?? kind));
49
+ panel.append(row);
50
+ boxes.set(kind, box);
51
+ }
52
+ panel.prepend(allRow);
53
+ root.append(button, panel);
54
+ host.append(root);
55
+ const everything = () => on.size === kinds.length;
56
+ const chosen = () => kinds.filter((kind) => on.has(kind));
57
+ function paint() {
58
+ const name = options.label ?? "Kinds";
59
+ button.textContent = everything() ? name : `${name} · ${on.size}`;
60
+ button.classList.toggle("on", !everything());
61
+ all.checked = everything();
62
+ button.appendChild(caret);
63
+ all.indeterminate = on.size > 0 && !everything();
64
+ for (const [kind, box] of boxes) box.checked = on.has(kind);
65
+ }
66
+ function report() {
67
+ options.onChange(everything() ? null : chosen());
68
+ }
69
+ for (const [kind, box] of boxes) {
70
+ box.addEventListener("change", () => {
71
+ if (box.checked) on.add(kind);
72
+ else on.delete(kind);
73
+ paint();
74
+ report();
75
+ });
76
+ }
77
+ all.addEventListener("change", () => {
78
+ for (const kind of kinds) on.add(kind);
79
+ paint();
80
+ report();
81
+ });
82
+ const close = () => {
83
+ panel.hidden = true;
84
+ button.setAttribute("aria-expanded", "false");
85
+ };
86
+ button.addEventListener("click", (event) => {
87
+ event.stopPropagation();
88
+ panel.hidden = !panel.hidden;
89
+ button.setAttribute("aria-expanded", String(!panel.hidden));
90
+ });
91
+ panel.addEventListener("click", (event) => event.stopPropagation());
92
+ const away = () => close();
93
+ const key = (event) => {
94
+ if (event.key === "Escape") close();
95
+ };
96
+ doc.addEventListener("click", away);
97
+ doc.addEventListener("keydown", key);
98
+ paint();
99
+ return {
100
+ get selected() {
101
+ return chosen();
102
+ },
103
+ select(next) {
104
+ on.clear();
105
+ for (const kind of next ?? kinds) on.add(kind);
106
+ paint();
107
+ },
108
+ destroy() {
109
+ doc.removeEventListener("click", away);
110
+ doc.removeEventListener("keydown", key);
111
+ root.remove();
112
+ }
113
+ };
114
+ }
115
+ var KIND_FILTER_CSS = `
116
+ .fx-kinds { position: relative; display: inline-flex; }
117
+ .fx-kinds-button { white-space: nowrap; }
118
+ /* The hidden attribute is display:none from the user agent, which a class setting display outranks. */
119
+ .fx-kinds-panel[hidden] { display: none; }
120
+ .fx-kinds-panel {
121
+ position: absolute; top: calc(100% + 4px); z-index: 40;
122
+ display: grid; gap: 2px; padding: 6px 8px; min-width: 132px;
123
+ background: var(--fx-panel, #14161c);
124
+ border: 1px solid var(--fx-line, #262a35); border-radius: 6px;
125
+ box-shadow: 0 6px 18px rgba(0, 0, 0, .38);
126
+ }
127
+ .fx-kinds-panel:not(.fx-panel-left){ right: 0; }
128
+ .fx-kinds-panel.fx-panel-left { left: 0; }
129
+ .fx-kinds-panel label {
130
+ display: flex; align-items: center; gap: 6px;
131
+ font-size: 11px; color: var(--fx-muted, #8a90a2); cursor: pointer; user-select: none;
132
+ }
133
+ .fx-kinds-panel label:hover { color: var(--fx-label, #e7e9ee); }
134
+ .fx-kinds-panel input { width: 11px; height: 11px; margin: 0; accent-color: var(--fx-accent, #5eead4); }
135
+ .fx-kinds-all {
136
+ padding-bottom: 4px; margin-bottom: 2px;
137
+ border-bottom: 1px solid var(--fx-line, #262a35);
138
+ color: var(--fx-label, #e7e9ee) !important;
139
+ }
140
+ `;
141
+ var DROPDOWN_FILTER_CSS = `
142
+ .fx-dropdown-btn { background-color: transparent; appearance: none; border: 1px solid var(--line-strong); outline: none; border-radius: .25rem; padding-block: .25rem; padding-inline: .875rem; font-size: .75rem; cursor: pointer }
143
+ .fx-dropdown-btn:hover { border-color: color-mix(in oklab, var(--accent) 50%, transparent); background-color: var(--bg); }
144
+ .fx-dropdown-caret { width: 0;height: 0;border-left: .15rem solid transparent; border-right: .15rem solid transparent; border-top: .15rem solid currentColor; transition: transform 0.2s ease; right: .4rem; top: 50%; color: color-mix(in oklab, var(--muted) 50%, transparent); position: absolute; }
145
+ `;
146
+ export {
147
+ DROPDOWN_FILTER_CSS,
148
+ FILTERABLE_KINDS,
149
+ KIND_FILTER_CSS,
150
+ createKindFilter
151
+ };
package/dist/menu.cjs ADDED
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/menu.ts
21
+ var menu_exports = {};
22
+ __export(menu_exports, {
23
+ MENU_CSS: () => MENU_CSS,
24
+ createMenu: () => createMenu
25
+ });
26
+ module.exports = __toCommonJS(menu_exports);
27
+ function createMenu(host, options) {
28
+ const doc = host.ownerDocument;
29
+ const root = doc.createElement("div");
30
+ root.className = "fx-menu";
31
+ const button = doc.createElement("button");
32
+ button.type = "button";
33
+ button.className = "fx-menu-button fx-dropdown-btn";
34
+ button.setAttribute("aria-haspopup", "true");
35
+ button.setAttribute("aria-expanded", "false");
36
+ if (options.hint) button.title = options.hint;
37
+ const caret = doc.createElement("div");
38
+ caret.className = "fx-dropdown-caret";
39
+ const panel = doc.createElement("div");
40
+ panel.className = "fx-menu-panel";
41
+ if (options.menuClass) {
42
+ panel.classList.add(...options.menuClass.split(/\s+/).filter((c) => c));
43
+ }
44
+ panel.hidden = true;
45
+ const boxes = /* @__PURE__ */ new Map();
46
+ for (const item of options.items) {
47
+ if (item.type === "action") {
48
+ const run = doc.createElement("button");
49
+ run.type = "button";
50
+ run.className = "fx-menu-action";
51
+ run.textContent = item.label;
52
+ if (item.hint) run.title = item.hint;
53
+ run.addEventListener("click", () => {
54
+ close();
55
+ item.onRun();
56
+ });
57
+ panel.append(run);
58
+ continue;
59
+ }
60
+ const box = doc.createElement("input");
61
+ box.type = "checkbox";
62
+ box.checked = item.on === true;
63
+ const row = doc.createElement("label");
64
+ if (item.hint) row.title = item.hint;
65
+ row.append(box, doc.createTextNode(item.label));
66
+ box.addEventListener("change", () => {
67
+ paint();
68
+ item.onChange(box.checked);
69
+ });
70
+ panel.append(row);
71
+ boxes.set(item.label, box);
72
+ }
73
+ root.append(button, panel);
74
+ host.append(root);
75
+ function paint() {
76
+ let on = 0;
77
+ for (const box of boxes.values()) if (box.checked) on++;
78
+ button.textContent = on ? `${options.label} · ${on}` : options.label;
79
+ button.appendChild(caret);
80
+ button.classList.toggle("on", on > 0);
81
+ }
82
+ function close() {
83
+ panel.hidden = true;
84
+ button.setAttribute("aria-expanded", "false");
85
+ }
86
+ button.addEventListener("click", (event) => {
87
+ event.stopPropagation();
88
+ panel.hidden = !panel.hidden;
89
+ button.setAttribute("aria-expanded", String(!panel.hidden));
90
+ });
91
+ panel.addEventListener("click", (event) => event.stopPropagation());
92
+ const away = () => close();
93
+ const key = (event) => {
94
+ if (event.key === "Escape") close();
95
+ };
96
+ doc.addEventListener("click", away);
97
+ doc.addEventListener("keydown", key);
98
+ paint();
99
+ return {
100
+ set(label, on) {
101
+ const box = boxes.get(label);
102
+ if (!box) return;
103
+ box.checked = on;
104
+ paint();
105
+ },
106
+ destroy() {
107
+ doc.removeEventListener("click", away);
108
+ doc.removeEventListener("keydown", key);
109
+ root.remove();
110
+ }
111
+ };
112
+ }
113
+ var MENU_CSS = `
114
+ /* The button itself is styled by DROPDOWN_FILTER_CSS, shared with the kind filter. */
115
+ .fx-menu { position: relative; display: inline-flex; }
116
+ .fx-menu-button { white-space: nowrap; position: relative; padding-inline-end: 1.1rem; }
117
+ /* hidden is display:none from the user agent, which a class setting display outranks. */
118
+ .fx-menu-panel[hidden] { display: none; }
119
+ .fx-menu-panel {
120
+ position: absolute; top: calc(100% + 4px); z-index: 40;
121
+ display: grid; gap: 2px; padding: 6px 8px; min-width: 150px;
122
+ background: var(--fx-panel, #14161c);
123
+ border: 1px solid var(--fx-line, #262a35); border-radius: 6px;
124
+ box-shadow: 0 6px 18px rgba(0, 0, 0, .38);
125
+ }
126
+ .fx-menu-panel:not(.fx-panel-left){ right: 0; }
127
+ .fx-menu-panel.fx-panel-left { left: 0; }
128
+ .fx-menu-panel label {
129
+ display: flex; align-items: center; gap: 6px;
130
+ font-size: 11px; color: var(--fx-muted, #8a90a2); cursor: pointer; user-select: none;
131
+ }
132
+ .fx-menu-panel label:hover { color: var(--fx-label, #e7e9ee); }
133
+ .fx-menu-panel input { width: 11px; height: 11px; margin: 0; accent-color: var(--fx-accent, #5eead4); }
134
+ .fx-menu-action {
135
+ text-align: left; padding: 2px 0; border: 0; background: none; cursor: pointer;
136
+ font: inherit; font-size: 11px; color: var(--fx-muted, #8a90a2);
137
+ /* A button does not inherit this, so an action read lowercase beside uppercase toggles. */
138
+ text-transform: inherit; letter-spacing: inherit;
139
+ }
140
+ .fx-menu-action:hover { color: var(--fx-accent, #5eead4); }
141
+ `;
package/dist/menu.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A labelled dropdown of toggles and actions.
3
+ *
4
+ * The graph toolbar had grown to nine bare glyphs. Every one carried a `title`, so the only
5
+ * way to learn what `⇥` did was to hover it and wait, and a reader who was not the author had
6
+ * no chance. This is the same shape as the kind filter, which already solved that for kinds:
7
+ * a named button, and the detail behind it.
8
+ */
9
+ export type MenuItem = {
10
+ type: 'toggle';
11
+ label: string;
12
+ /** Starting state. Read once. */
13
+ on?: boolean;
14
+ hint?: string;
15
+ onChange: (on: boolean) => void;
16
+ } | {
17
+ type: 'action';
18
+ label: string;
19
+ hint?: string;
20
+ onRun: () => void;
21
+ };
22
+ export interface MenuOptions {
23
+ /** What the button says when nothing is on. */
24
+ label: string;
25
+ items: readonly MenuItem[];
26
+ /** The button's own tooltip. */
27
+ hint?: string;
28
+ menuClass?: string;
29
+ }
30
+ export interface Menu {
31
+ /** Set a toggle from outside, by label, for a host restoring what was last used. */
32
+ set(label: string, on: boolean): void;
33
+ destroy(): void;
34
+ }
35
+ export declare function createMenu(host: HTMLElement, options: MenuOptions): Menu;
36
+ export declare const MENU_CSS = "\n/* The button itself is styled by DROPDOWN_FILTER_CSS, shared with the kind filter. */\n.fx-menu { position: relative; display: inline-flex; }\n.fx-menu-button { white-space: nowrap; position: relative; padding-inline-end: 1.1rem; }\n/* hidden is display:none from the user agent, which a class setting display outranks. */\n.fx-menu-panel[hidden] { display: none; }\n.fx-menu-panel {\n position: absolute; top: calc(100% + 4px); z-index: 40;\n display: grid; gap: 2px; padding: 6px 8px; min-width: 150px;\n background: var(--fx-panel, #14161c);\n border: 1px solid var(--fx-line, #262a35); border-radius: 6px;\n box-shadow: 0 6px 18px rgba(0, 0, 0, .38);\n}\n.fx-menu-panel:not(.fx-panel-left){ right: 0; }\n.fx-menu-panel.fx-panel-left { left: 0; }\n.fx-menu-panel label {\n display: flex; align-items: center; gap: 6px;\n font-size: 11px; color: var(--fx-muted, #8a90a2); cursor: pointer; user-select: none;\n}\n.fx-menu-panel label:hover { color: var(--fx-label, #e7e9ee); }\n.fx-menu-panel input { width: 11px; height: 11px; margin: 0; accent-color: var(--fx-accent, #5eead4); }\n.fx-menu-action {\n text-align: left; padding: 2px 0; border: 0; background: none; cursor: pointer;\n font: inherit; font-size: 11px; color: var(--fx-muted, #8a90a2);\n /* A button does not inherit this, so an action read lowercase beside uppercase toggles. */\n text-transform: inherit; letter-spacing: inherit;\n}\n.fx-menu-action:hover { color: var(--fx-accent, #5eead4); }\n";
37
+ //# sourceMappingURL=menu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../src/menu.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,MAAM,QAAQ,GAChB;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,IAAI,CAAC;CACjC,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEN,MAAM,WAAW,WAAW;IAC1B,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC3B,gCAAgC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,IAAI;IACnB,oFAAoF;IACpF,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,GAAG,IAAI,CAAC;IACtC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAmGxE;AAED,eAAO,MAAM,QAAQ,y8CA4BpB,CAAC"}
package/dist/menu.js ADDED
@@ -0,0 +1,125 @@
1
+ export function createMenu(host, options) {
2
+ const doc = host.ownerDocument;
3
+ const root = doc.createElement('div');
4
+ root.className = 'fx-menu';
5
+ const button = doc.createElement('button');
6
+ button.type = 'button';
7
+ button.className = 'fx-menu-button fx-dropdown-btn';
8
+ button.setAttribute('aria-haspopup', 'true');
9
+ button.setAttribute('aria-expanded', 'false');
10
+ if (options.hint)
11
+ button.title = options.hint;
12
+ const caret = doc.createElement('div');
13
+ caret.className = 'fx-dropdown-caret';
14
+ const panel = doc.createElement('div');
15
+ panel.className = 'fx-menu-panel';
16
+ if (options.menuClass) {
17
+ panel.classList.add(...options.menuClass.split(/\s+/).filter((c) => c));
18
+ }
19
+ panel.hidden = true;
20
+ const boxes = new Map();
21
+ for (const item of options.items) {
22
+ if (item.type === 'action') {
23
+ const run = doc.createElement('button');
24
+ run.type = 'button';
25
+ run.className = 'fx-menu-action';
26
+ run.textContent = item.label;
27
+ if (item.hint)
28
+ run.title = item.hint;
29
+ run.addEventListener('click', () => {
30
+ close();
31
+ item.onRun();
32
+ });
33
+ panel.append(run);
34
+ continue;
35
+ }
36
+ const box = doc.createElement('input');
37
+ box.type = 'checkbox';
38
+ box.checked = item.on === true;
39
+ const row = doc.createElement('label');
40
+ if (item.hint)
41
+ row.title = item.hint;
42
+ row.append(box, doc.createTextNode(item.label));
43
+ box.addEventListener('change', () => {
44
+ paint();
45
+ item.onChange(box.checked);
46
+ });
47
+ panel.append(row);
48
+ boxes.set(item.label, box);
49
+ }
50
+ root.append(button, panel);
51
+ host.append(root);
52
+ /** The count is what makes a folded menu honest: something is on in here. */
53
+ function paint() {
54
+ let on = 0;
55
+ for (const box of boxes.values())
56
+ if (box.checked)
57
+ on++;
58
+ button.textContent = on ? `${options.label} · ${on}` : options.label;
59
+ // textContent clears it, so the caret goes back after every repaint.
60
+ button.appendChild(caret);
61
+ button.classList.toggle('on', on > 0);
62
+ }
63
+ function close() {
64
+ panel.hidden = true;
65
+ button.setAttribute('aria-expanded', 'false');
66
+ }
67
+ button.addEventListener('click', (event) => {
68
+ event.stopPropagation();
69
+ panel.hidden = !panel.hidden;
70
+ button.setAttribute('aria-expanded', String(!panel.hidden));
71
+ });
72
+ // Clicks inside must not reach the document listener, which closes it.
73
+ panel.addEventListener('click', (event) => event.stopPropagation());
74
+ const away = () => close();
75
+ const key = (event) => {
76
+ if (event.key === 'Escape')
77
+ close();
78
+ };
79
+ doc.addEventListener('click', away);
80
+ doc.addEventListener('keydown', key);
81
+ paint();
82
+ return {
83
+ set(label, on) {
84
+ const box = boxes.get(label);
85
+ if (!box)
86
+ return;
87
+ box.checked = on;
88
+ paint();
89
+ },
90
+ destroy() {
91
+ doc.removeEventListener('click', away);
92
+ doc.removeEventListener('keydown', key);
93
+ root.remove();
94
+ },
95
+ };
96
+ }
97
+ export const MENU_CSS = `
98
+ /* The button itself is styled by DROPDOWN_FILTER_CSS, shared with the kind filter. */
99
+ .fx-menu { position: relative; display: inline-flex; }
100
+ .fx-menu-button { white-space: nowrap; position: relative; padding-inline-end: 1.1rem; }
101
+ /* hidden is display:none from the user agent, which a class setting display outranks. */
102
+ .fx-menu-panel[hidden] { display: none; }
103
+ .fx-menu-panel {
104
+ position: absolute; top: calc(100% + 4px); z-index: 40;
105
+ display: grid; gap: 2px; padding: 6px 8px; min-width: 150px;
106
+ background: var(--fx-panel, #14161c);
107
+ border: 1px solid var(--fx-line, #262a35); border-radius: 6px;
108
+ box-shadow: 0 6px 18px rgba(0, 0, 0, .38);
109
+ }
110
+ .fx-menu-panel:not(.fx-panel-left){ right: 0; }
111
+ .fx-menu-panel.fx-panel-left { left: 0; }
112
+ .fx-menu-panel label {
113
+ display: flex; align-items: center; gap: 6px;
114
+ font-size: 11px; color: var(--fx-muted, #8a90a2); cursor: pointer; user-select: none;
115
+ }
116
+ .fx-menu-panel label:hover { color: var(--fx-label, #e7e9ee); }
117
+ .fx-menu-panel input { width: 11px; height: 11px; margin: 0; accent-color: var(--fx-accent, #5eead4); }
118
+ .fx-menu-action {
119
+ text-align: left; padding: 2px 0; border: 0; background: none; cursor: pointer;
120
+ font: inherit; font-size: 11px; color: var(--fx-muted, #8a90a2);
121
+ /* A button does not inherit this, so an action read lowercase beside uppercase toggles. */
122
+ text-transform: inherit; letter-spacing: inherit;
123
+ }
124
+ .fx-menu-action:hover { color: var(--fx-accent, #5eead4); }
125
+ `;