flexdesk 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2261 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/css/base.css +1834 -75
  6. package/css/flexdesk.css +3373 -238
  7. package/css/overrides.css +16 -0
  8. package/css/reset.css +0 -0
  9. package/css/tokens.css +0 -0
  10. package/dist/canvas.js +6 -239
  11. package/dist/canvas.js.map +3 -3
  12. package/dist/charts.js +709 -18
  13. package/dist/charts.js.map +4 -4
  14. package/dist/chunk-3PHCPZHT.js +0 -0
  15. package/dist/chunk-3PHCPZHT.js.map +0 -0
  16. package/dist/chunk-4A7SJC3T.js +503 -0
  17. package/dist/chunk-4A7SJC3T.js.map +7 -0
  18. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  19. package/dist/chunk-CVMCZZX2.js.map +7 -0
  20. package/dist/chunk-D3COLZZ2.js +1311 -0
  21. package/dist/chunk-D3COLZZ2.js.map +7 -0
  22. package/dist/chunk-FOOS3T5L.js +0 -0
  23. package/dist/chunk-FOOS3T5L.js.map +0 -0
  24. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  25. package/dist/chunk-GOQLR5WR.js.map +7 -0
  26. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  27. package/dist/chunk-HNU6MM2B.js.map +7 -0
  28. package/dist/chunk-HYJYYEPK.js +375 -0
  29. package/dist/chunk-HYJYYEPK.js.map +7 -0
  30. package/dist/chunk-JKZLNKG6.js +2041 -0
  31. package/dist/chunk-JKZLNKG6.js.map +7 -0
  32. package/dist/chunk-JYWURG5T.js +0 -0
  33. package/dist/chunk-JYWURG5T.js.map +0 -0
  34. package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
  35. package/dist/chunk-MCIXFNVF.js.map +7 -0
  36. package/dist/chunk-N7HC3ADV.js +30 -0
  37. package/dist/chunk-N7HC3ADV.js.map +7 -0
  38. package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
  39. package/dist/chunk-Q36EPT2R.js.map +7 -0
  40. package/dist/chunk-QWQJUDPD.js +610 -0
  41. package/dist/chunk-QWQJUDPD.js.map +7 -0
  42. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  43. package/dist/chunk-UURZ37SD.js.map +7 -0
  44. package/dist/chunk-UWO43BZU.js +111 -0
  45. package/dist/chunk-UWO43BZU.js.map +7 -0
  46. package/dist/chunk-WVFGV5FT.js +0 -0
  47. package/dist/chunk-WVFGV5FT.js.map +0 -0
  48. package/dist/core.js +170 -118
  49. package/dist/core.js.map +2 -2
  50. package/dist/editor.js +156 -70
  51. package/dist/editor.js.map +2 -2
  52. package/dist/flexdesk.css +3373 -238
  53. package/dist/flow.js +2789 -306
  54. package/dist/flow.js.map +4 -4
  55. package/dist/host.js +109 -3
  56. package/dist/host.js.map +3 -3
  57. package/dist/reset.css +0 -0
  58. package/dist/tile_registry-6WZPTUZV.js +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  60. package/dist/tiles.js +1131 -253
  61. package/dist/tiles.js.map +4 -4
  62. package/dist/tokens.css +0 -0
  63. package/dist/widgets.js +3254 -2112
  64. package/dist/widgets.js.map +4 -4
  65. package/dist/wm.js +1227 -345
  66. package/dist/wm.js.map +4 -4
  67. package/package.json +3 -1
  68. package/src/canvas/canvas_adapter.js +5 -0
  69. package/src/canvas/edge_router.js +0 -0
  70. package/src/canvas/graph/connector_router.js +0 -0
  71. package/src/canvas/graph/history_service.js +0 -0
  72. package/src/canvas/graph/node_platform.js +0 -0
  73. package/src/canvas/index.js +1 -0
  74. package/src/canvas/layered_layout.js +244 -0
  75. package/src/charts/chart_types.js +0 -0
  76. package/src/charts/data_series_plot_window.js +0 -0
  77. package/src/charts/downsample.js +2 -2
  78. package/src/charts/plot_config.js +53 -29
  79. package/src/charts/plot_config_editor.js +627 -0
  80. package/src/charts/plot_grid.js +449 -0
  81. package/src/charts/plot_popout_window.js +103 -317
  82. package/src/charts/plotly_wrapper.js +22 -2
  83. package/src/charts/series_transforms.js +206 -0
  84. package/src/core/event_bus.js +0 -0
  85. package/src/core/logging.js +1 -1
  86. package/src/core/settings.js +105 -19
  87. package/src/core/state_guard.js +93 -51
  88. package/src/core/state_guard_config.js +85 -74
  89. package/src/core/state_machine.js +0 -0
  90. package/src/editor/editor_pane.js +3 -3
  91. package/src/editor/monaco_editor_factory.js +124 -60
  92. package/src/editor/monaco_loader.js +103 -28
  93. package/src/editor/notebook_search.js +0 -0
  94. package/src/editor/notebook_tab_bar.js +38 -52
  95. package/src/editor/notebook_undo_manager.js +0 -0
  96. package/src/editor/split_pane_container.js +6 -3
  97. package/src/flow/canvas_editor/connections.js +90 -0
  98. package/src/flow/canvas_editor/editor.js +2351 -153
  99. package/src/flow/canvas_editor/index.js +2 -1
  100. package/src/flow/canvas_editor/layout.js +119 -12
  101. package/src/flow/canvas_editor/strings.js +80 -0
  102. package/src/flow/kit/always_before.js +0 -0
  103. package/src/flow/kit/catalogue.js +97 -3
  104. package/src/flow/kit/chip_input.js +0 -0
  105. package/src/flow/kit/dom.js +0 -0
  106. package/src/flow/kit/findings.js +0 -0
  107. package/src/flow/kit/history.js +0 -0
  108. package/src/flow/kit/index.js +3 -3
  109. package/src/flow/kit/keys.js +7 -1
  110. package/src/flow/kit/logic_graph.js +97 -15
  111. package/src/flow/kit/references.js +0 -0
  112. package/src/flow/kit/settings_panel.js +0 -0
  113. package/src/flow/kit/settings_schema.js +2 -1
  114. package/src/flow/kit/step_picker.js +0 -0
  115. package/src/flow/kit/strings.js +3 -1
  116. package/src/flow/kit/value_picker.js +0 -0
  117. package/src/flow/kit/widgets.js +86 -109
  118. package/src/flow/lanes/editor.js +136 -14
  119. package/src/flow/lanes/index.js +3 -1
  120. package/src/flow/lanes/layout.js +0 -0
  121. package/src/flow/lanes/pipeline.js +44 -2
  122. package/src/flow/lanes/preview.js +0 -0
  123. package/src/flow/lanes/strings.js +1 -0
  124. package/src/flow/outline/editor.js +0 -0
  125. package/src/flow/outline/index.js +0 -0
  126. package/src/flow/outline/mapping.js +0 -0
  127. package/src/flow/outline/menu.js +0 -0
  128. package/src/flow/outline/operations.js +0 -0
  129. package/src/flow/outline/recognise.js +0 -0
  130. package/src/flow/outline/strings.js +0 -0
  131. package/src/help/help_modal.js +168 -42
  132. package/src/help/help_registry.js +12 -1
  133. package/src/host/external_links.js +0 -0
  134. package/src/host/file_picker.js +117 -0
  135. package/src/host/host.js +10 -2
  136. package/src/host/photino_host.js +9 -0
  137. package/src/host/photino_host.md +21 -3
  138. package/src/host/pywebview_host.js +10 -0
  139. package/src/tiles/chart_tiles.js +53 -245
  140. package/src/tiles/config_schema.js +8 -0
  141. package/src/tiles/figure_tile.js +322 -0
  142. package/src/tiles/layout_persistence.js +3 -3
  143. package/src/tiles/list_tiles.js +422 -0
  144. package/src/tiles/scope_picker.js +96 -0
  145. package/src/tiles/tile_base.js +12 -1
  146. package/src/tiles/tile_config_form.js +442 -0
  147. package/src/tiles/tile_grid.js +38 -1
  148. package/src/tiles/tile_registry.js +0 -0
  149. package/src/tiling/bottom_panel_tabs.js +67 -99
  150. package/src/tiling/command_palette.js +64 -19
  151. package/src/tiling/content_registry.js +4 -1
  152. package/src/tiling/desktops.js +11 -1
  153. package/src/tiling/entity_sources.js +0 -0
  154. package/src/tiling/keymap.js +403 -128
  155. package/src/tiling/kind_taxonomy.js +78 -0
  156. package/src/tiling/landing_table.js +0 -0
  157. package/src/tiling/loading_overlay.js +0 -0
  158. package/src/tiling/nav_panel.js +59 -10
  159. package/src/tiling/page_factory.js +5 -5
  160. package/src/tiling/panel_availability.js +49 -0
  161. package/src/tiling/panel_keys.js +0 -0
  162. package/src/tiling/shell.js +114 -12
  163. package/src/tiling/tab_strip.js +0 -0
  164. package/src/tiling/tile_breadcrumb.js +0 -0
  165. package/src/tiling/tile_renderer.js +105 -9
  166. package/src/tiling/tile_tab_menu.js +0 -0
  167. package/src/tiling/tile_tree.js +0 -0
  168. package/src/tiling/wm.js +562 -30
  169. package/src/tiling/zoom.js +0 -0
  170. package/src/ui/base/component_base.js +0 -0
  171. package/src/ui/base/controller_base.js +0 -0
  172. package/src/ui/base/page_base.js +0 -0
  173. package/src/ui/components/about_dialog.js +0 -0
  174. package/src/ui/components/action_dropdown.js +30 -2
  175. package/src/ui/components/alert_log.js +0 -0
  176. package/src/ui/components/alerts_pane.js +0 -0
  177. package/src/ui/components/attribute_list_editor.js +0 -0
  178. package/src/ui/components/autocomplete_field.js +0 -0
  179. package/src/ui/components/cell_editor_host.js +211 -0
  180. package/src/ui/components/cell_grid.js +648 -0
  181. package/src/ui/components/cell_selection.js +220 -0
  182. package/src/ui/components/choice_controls.js +351 -0
  183. package/src/ui/components/color_field.js +211 -0
  184. package/src/ui/components/color_list_field.js +210 -0
  185. package/src/ui/components/column_fit.js +0 -0
  186. package/src/ui/components/column_groups.js +314 -0
  187. package/src/ui/components/column_order.js +142 -0
  188. package/src/ui/components/computing_status_window.js +98 -25
  189. package/src/ui/components/confirm_dialog.js +81 -38
  190. package/src/ui/components/context_menu.js +2 -2
  191. package/src/ui/components/conversation_view.js +440 -0
  192. package/src/ui/components/crosstab.js +289 -0
  193. package/src/ui/components/data_table.js +601 -62
  194. package/src/ui/components/detail_header.js +22 -11
  195. package/src/ui/components/document_outline.js +397 -0
  196. package/src/ui/components/drag_reorder.js +0 -0
  197. package/src/ui/components/fact_table.js +0 -0
  198. package/src/ui/components/fill_handle.js +322 -0
  199. package/src/ui/components/gallery_picker.js +19 -8
  200. package/src/ui/components/header_actions.js +55 -0
  201. package/src/ui/components/inline_renamer.js +0 -0
  202. package/src/ui/components/job_progress.js +51 -0
  203. package/src/ui/components/managed_window.js +27 -4
  204. package/src/ui/components/menu_bar.js +745 -0
  205. package/src/ui/components/modal.js +449 -75
  206. package/src/ui/components/notification_history.js +3 -3
  207. package/src/ui/components/progress.js +483 -0
  208. package/src/ui/components/record_list.js +0 -0
  209. package/src/ui/components/row_activation.js +0 -0
  210. package/src/ui/components/section_stack.js +298 -0
  211. package/src/ui/components/series.js +173 -0
  212. package/src/ui/components/settings_view.js +634 -0
  213. package/src/ui/components/slide_out_panel.js +43 -6
  214. package/src/ui/components/slider_field.js +275 -18
  215. package/src/ui/components/sortable_list.js +26 -4
  216. package/src/ui/components/staged_edits.js +343 -0
  217. package/src/ui/components/staged_history.js +172 -0
  218. package/src/ui/components/table_filter_chips.js +0 -0
  219. package/src/ui/components/table_state_store.js +0 -0
  220. package/src/ui/components/table_structure.js +182 -0
  221. package/src/ui/components/table_values.js +0 -0
  222. package/src/ui/components/tabs.js +497 -0
  223. package/src/ui/components/tag_input.js +215 -0
  224. package/src/ui/components/toast.js +25 -8
  225. package/src/ui/components/toggle_field.js +175 -0
  226. package/src/ui/components/tree_view.js +55 -34
  227. package/src/ui/components/window_taskbar.js +0 -0
  228. package/src/ui/components/wizard.js +563 -0
  229. package/src/ui/controllers/window_chrome_controller.js +2 -2
  230. package/src/ui/notification_center.js +375 -119
  231. package/src/ui/utils/auto_scrollbars.js +0 -0
  232. package/src/ui/utils/markdown.js +709 -0
  233. package/src/ui/utils/native_context_menu.js +0 -0
  234. package/src/ui/utils/overlay_scrollbar.js +16 -12
  235. package/src/ui/utils/raf_resize_observer.js +0 -0
  236. package/src/ui/utils/scroll_lock.js +0 -0
  237. package/src/ui/utils/strip_scroll.js +55 -0
  238. package/src/ui/utils/tooltip_service.js +0 -8
  239. package/dist/chunk-6JSOVNID.js.map +0 -7
  240. package/dist/chunk-CWPMLX67.js +0 -475
  241. package/dist/chunk-CWPMLX67.js.map +0 -7
  242. package/dist/chunk-FBGP5KN5.js.map +0 -7
  243. package/dist/chunk-FL5KFNQH.js.map +0 -7
  244. package/dist/chunk-NLTOY57M.js.map +0 -7
  245. package/dist/chunk-NNFTETK3.js +0 -633
  246. package/dist/chunk-NNFTETK3.js.map +0 -7
  247. package/dist/chunk-O5OHMWBB.js +0 -284
  248. package/dist/chunk-O5OHMWBB.js.map +0 -7
  249. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  250. package/dist/chunk-SN4LSOAR.js.map +0 -7
  251. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -0,0 +1,211 @@
1
+ /**
2
+ * ColorField — one colour: a swatch that opens the native picker, a hex text
3
+ * box kept in step with it, and an optional row of preset swatches.
4
+ *
5
+ * const field = new ColorField({
6
+ * value: 'rgb(31, 119, 180)', // shown as #1f77b4
7
+ * presets: ['#1f77b4', '#ff7f0e', '#2ca02c'],
8
+ * onChange: (hex, { commit }) => { ... },
9
+ * });
10
+ * host.appendChild(field.render());
11
+ * field.getValue(); // '#1f77b4' | '' | null
12
+ *
13
+ * THE HEX BOX IS THE CONTROL. Picking in the swatch writes the box and fires
14
+ * the box's own `input` (and, when the picker commits, `change`); a preset
15
+ * does the same. So there is one path from any gesture to the value, and a
16
+ * host listening on the box — `openForm` does, by its `name` — hears every
17
+ * change whatever made it. Typing a valid hex moves the swatch; typing an
18
+ * invalid one leaves the swatch on the last good colour and marks the field
19
+ * `.twm-color-field--invalid`.
20
+ *
21
+ * VALUES. `getValue()` is the colour as lowercase `#rrggbb`, `''` when the box
22
+ * is empty, and `null` when what is typed is not a hex colour (`#rgb` and
23
+ * `#rrggbb` are). An `rgb()` / `rgba()` value handed IN — a default, a preset,
24
+ * `setValue` — is converted to its hex equivalent first; alpha is dropped,
25
+ * because the native picker has no alpha channel.
26
+ *
27
+ * `onChange(value, { commit })` runs on every edit with `commit: false` and on
28
+ * the box's `change` (blur, Enter, a preset, the picker closing) with
29
+ * `commit: true`.
30
+ */
31
+
32
+ const HEX_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
33
+ const RGB_RE = /^rgba?\(\s*([^)]*)\)$/i;
34
+
35
+ /** True for `#rgb` and `#rrggbb` (any case), and nothing else. */
36
+ export function isHexColor(value) {
37
+ return typeof value === 'string' && HEX_RE.test(value.trim());
38
+ }
39
+
40
+ /**
41
+ * A colour as lowercase `#rrggbb`, or null when `value` is not one.
42
+ * Accepts `#rgb`, `#rrggbb` and `rgb()` / `rgba()` in comma or space syntax,
43
+ * with channels as 0–255 or percentages; an alpha channel is dropped.
44
+ */
45
+ export function normalizeHex(value) {
46
+ if (typeof value !== 'string') return null;
47
+ const s = value.trim();
48
+ if (HEX_RE.test(s)) {
49
+ const h = s.slice(1).toLowerCase();
50
+ return h.length === 3 ? `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}` : `#${h}`;
51
+ }
52
+ const m = RGB_RE.exec(s);
53
+ if (!m) return null;
54
+ const parts = m[1].split(/\s*[,/]\s*|\s+/).filter(Boolean);
55
+ if (parts.length < 3 || parts.length > 4) return null;
56
+ const channels = [];
57
+ for (const p of parts.slice(0, 3)) {
58
+ const pct = p.endsWith('%');
59
+ const n = Number(pct ? p.slice(0, -1) : p);
60
+ if (!Number.isFinite(n)) return null;
61
+ const v = pct ? (n / 100) * 255 : n;
62
+ if (v < 0 || v > 255) return null;
63
+ channels.push(Math.round(v));
64
+ }
65
+ return `#${channels.map((c) => c.toString(16).padStart(2, '0')).join('')}`;
66
+ }
67
+
68
+ /** What the box shows for a value handed in: its hex if it is a colour, else as given. */
69
+ function displayText(value) {
70
+ if (value == null) return '';
71
+ return normalizeHex(String(value)) ?? String(value);
72
+ }
73
+
74
+ export class ColorField {
75
+ /**
76
+ * @param {Object} [o]
77
+ * @param {string} [o.value] initial colour (hex or rgb()); '' for none
78
+ * @param {string[]} [o.presets] preset colours drawn as buttons; ones that
79
+ * are not colours are left out
80
+ * @param {string} [o.name] `name` of the hex box, for a host form
81
+ * @param {string} [o.label] accessible name of the swatch and the box
82
+ * @param {Function} [o.onChange] `(value, { commit })`, value as `getValue()`
83
+ */
84
+ constructor({ value = '', presets = [], name = null, label = null, onChange = null } = {}) {
85
+ this.initial = displayText(value);
86
+ this.presets = (Array.isArray(presets) ? presets : []).map(normalizeHex).filter(Boolean);
87
+ this.name = name;
88
+ this.label = label;
89
+ this.onChange = typeof onChange === 'function' ? onChange : null;
90
+
91
+ this.root = null;
92
+ this.swatch = null;
93
+ this.hex = null;
94
+ this._disposers = [];
95
+ }
96
+
97
+ render() {
98
+ if (this.root) return this.root;
99
+ const root = document.createElement('div');
100
+ root.className = 'twm-color-field';
101
+
102
+ const swatch = document.createElement('input');
103
+ swatch.type = 'color';
104
+ swatch.className = 'twm-color-field__swatch';
105
+ swatch.value = normalizeHex(this.initial) ?? '#000000';
106
+ if (this.label) swatch.title = this.label;
107
+
108
+ const hex = document.createElement('input');
109
+ hex.type = 'text';
110
+ hex.className = 'twm-color-field__hex';
111
+ hex.value = this.initial;
112
+ hex.placeholder = '#rrggbb';
113
+ hex.spellcheck = false;
114
+ hex.autocomplete = 'off';
115
+ hex.maxLength = 7;
116
+ if (this.name) hex.name = this.name;
117
+ if (this.label) hex.setAttribute('aria-label', this.label);
118
+
119
+ root.append(swatch, hex);
120
+ this.root = root;
121
+ this.swatch = swatch;
122
+ this.hex = hex;
123
+
124
+ if (this.presets.length) {
125
+ const strip = document.createElement('div');
126
+ strip.className = 'twm-color-field__presets';
127
+ for (const color of this.presets) {
128
+ const btn = document.createElement('button');
129
+ btn.type = 'button';
130
+ btn.className = 'twm-color-field__preset';
131
+ btn.style.backgroundColor = color;
132
+ btn.title = color;
133
+ btn.setAttribute('aria-label', color);
134
+ this.#listen(btn, 'click', () => this.#writeBox(color, true));
135
+ strip.appendChild(btn);
136
+ }
137
+ root.appendChild(strip);
138
+ }
139
+
140
+ // Every gesture lands on the box; the box's listeners do the rest.
141
+ this.#listen(swatch, 'input', () => this.#writeBox(swatch.value, false));
142
+ this.#listen(swatch, 'change', () => this.#writeBox(swatch.value, true));
143
+ this.#listen(hex, 'input', () => this.#sync(false));
144
+ this.#listen(hex, 'change', () => {
145
+ const v = this.getValue();
146
+ if (v) hex.value = v; // canonical form once committed
147
+ this.#sync(true);
148
+ });
149
+
150
+ this.#sync(null);
151
+ return root;
152
+ }
153
+
154
+ /** `#rrggbb`, `''` when empty, or `null` when the box holds no hex colour. */
155
+ getValue() {
156
+ const text = this.getText().trim();
157
+ if (text === '') return '';
158
+ return isHexColor(text) ? normalizeHex(text) : null;
159
+ }
160
+
161
+ /** The box as it stands. */
162
+ getText() {
163
+ return this.hex ? this.hex.value : this.initial;
164
+ }
165
+
166
+ isValid() {
167
+ return this.getValue() !== null;
168
+ }
169
+
170
+ /** Write a colour (hex or rgb()) without firing `onChange`. */
171
+ setValue(value) {
172
+ const text = displayText(value);
173
+ if (!this.hex) { this.initial = text; return; }
174
+ this.hex.value = text;
175
+ this.#sync(null);
176
+ }
177
+
178
+ focus() {
179
+ this.hex?.focus();
180
+ }
181
+
182
+ dispose() {
183
+ for (const d of this._disposers) {
184
+ try { d(); } catch { /* listener already gone */ }
185
+ }
186
+ this._disposers = [];
187
+ this.root = null;
188
+ this.swatch = null;
189
+ this.hex = null;
190
+ }
191
+
192
+ #listen(el, type, fn) {
193
+ el.addEventListener(type, fn);
194
+ this._disposers.push(() => el.removeEventListener(type, fn));
195
+ }
196
+
197
+ /** Put `text` in the box and fire the box's own events, as typing would. */
198
+ #writeBox(text, commit) {
199
+ this.hex.value = text;
200
+ this.hex.dispatchEvent(new Event('input', { bubbles: true }));
201
+ if (commit) this.hex.dispatchEvent(new Event('change', { bubbles: true }));
202
+ }
203
+
204
+ /** Swatch and invalid mark follow the box; `commit === null` is silent. */
205
+ #sync(commit) {
206
+ const v = this.getValue();
207
+ if (v) this.swatch.value = v;
208
+ this.root.classList.toggle('twm-color-field--invalid', v === null);
209
+ if (commit !== null) this.onChange?.(v, { commit });
210
+ }
211
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * ColorListField — an ordered list of colours: add, remove, and reorder by
3
+ * the grip (drag) or the up / down buttons, each row a `ColorField`.
4
+ *
5
+ * const list = new ColorListField({
6
+ * value: ['#1f77b4', 'rgb(255, 127, 14)'],
7
+ * onChange: (colors, { commit }) => { ... },
8
+ * });
9
+ * host.appendChild(list.render());
10
+ * list.getValue(); // ['#1f77b4', '#ff7f0e'] — or null, see below
11
+ *
12
+ * The list is a `SortableList` (the same add / remove / drag-reorder every
13
+ * list in the library uses) whose rows render a `ColorField` and a pair of
14
+ * move buttons, so the order can be changed without a pointer.
15
+ *
16
+ * `getValue()` is the colours in their current order as lowercase `#rrggbb`,
17
+ * or `null` while any row holds something that is not a hex colour —
18
+ * `invalidIndexes()` says which. Empty rows count as invalid: a list entry
19
+ * with no colour is not a colour.
20
+ *
21
+ * Given a `name`, the list also carries a hidden `<input name>` whose value is
22
+ * the rows as JSON, and fires `input` on it for every edit and `change` for
23
+ * every committed one (a row's `change`, add, remove, move). That is the
24
+ * surface a host form listens on; `openForm` reads the list through
25
+ * `getValue()`, not through that input.
26
+ */
27
+
28
+ import { SortableList } from './sortable_list.js';
29
+ import { ColorField, isHexColor, normalizeHex } from './color_field.js';
30
+
31
+ let _seq = 0;
32
+
33
+ /** A row's text as `#rrggbb`, or null when it is not a hex colour. */
34
+ const hexOf = (text) => {
35
+ const t = String(text ?? '').trim();
36
+ return isHexColor(t) ? normalizeHex(t) : null;
37
+ };
38
+
39
+ export class ColorListField {
40
+ /**
41
+ * @param {Object} [o]
42
+ * @param {string[]} [o.value] initial colours (hex or rgb())
43
+ * @param {string[]} [o.presets] preset colours offered on every row
44
+ * @param {string} [o.newColor] colour a newly added row starts on
45
+ * @param {string} [o.addLabel] text of the add button
46
+ * @param {string} [o.name] `name` of the hidden input, for a host form
47
+ * @param {Function} [o.onChange] `(colors, { commit })`, colors as `getValue()`
48
+ */
49
+ constructor({
50
+ value = [], presets = [], newColor = '#000000', addLabel = 'Add colour',
51
+ name = null, onChange = null,
52
+ } = {}) {
53
+ this.presets = Array.isArray(presets) ? presets : [];
54
+ // Converted once, as #toItems does, so an added row is what it shows.
55
+ this.newColor = normalizeHex(String(newColor ?? '')) ?? String(newColor ?? '');
56
+ this.addLabel = addLabel;
57
+ this.name = name;
58
+ this.onChange = typeof onChange === 'function' ? onChange : null;
59
+
60
+ /** Row index -> its ColorField; always exactly the rendered rows. */
61
+ this._fields = [];
62
+ /** Whether the SortableList change being reported is a committed one. */
63
+ this._commit = true;
64
+ /** True while a row's own edit is written into the list's items. */
65
+ this._rowEdit = false;
66
+ this.list = new SortableList({
67
+ items: this.#toItems(value),
68
+ addButtonText: addLabel,
69
+ emptyMessage: 'No colours',
70
+ createItem: () => ({ id: ++_seq, text: this.newColor }),
71
+ renderItem: (container, item, index, callbacks) =>
72
+ this.#renderRow(container, item, index, callbacks),
73
+ // Every SortableList change but a row's own edit (add, remove,
74
+ // move, drag) is followed by a refresh that rebuilds all rows —
75
+ // down to none — so the old fields go first.
76
+ onChange: () => {
77
+ if (!this._rowEdit) this.#dropFields();
78
+ this.#emit(this._commit);
79
+ },
80
+ });
81
+ this.root = null;
82
+ this.input = null;
83
+ }
84
+
85
+ render() {
86
+ if (this.root) return this.root;
87
+ const root = document.createElement('div');
88
+ root.className = 'twm-color-list';
89
+ if (this.name) {
90
+ const input = document.createElement('input');
91
+ input.type = 'hidden';
92
+ input.name = this.name;
93
+ root.appendChild(input);
94
+ this.input = input;
95
+ }
96
+ root.appendChild(this.list.render());
97
+ this.root = root;
98
+ this.#writeInput();
99
+ return root;
100
+ }
101
+
102
+ /** The colours in order as `#rrggbb`, or null while any row is not one. */
103
+ getValue() {
104
+ const out = this.getTexts().map(hexOf);
105
+ return out.includes(null) ? null : out;
106
+ }
107
+
108
+ /** The rows' text as it stands, valid or not. */
109
+ getTexts() {
110
+ return this.list.getItems().map((item) => String(item.text ?? ''));
111
+ }
112
+
113
+ /** Indexes of the rows that do not hold a hex colour. */
114
+ invalidIndexes() {
115
+ const bad = [];
116
+ this.getTexts().forEach((text, i) => { if (!hexOf(text)) bad.push(i); });
117
+ return bad;
118
+ }
119
+
120
+ /** Replace the colours without firing `onChange`. */
121
+ setValue(colors) {
122
+ this.#dropFields();
123
+ this.list.setItems(this.#toItems(colors));
124
+ this.#writeInput();
125
+ }
126
+
127
+ /** Focus the first invalid row, else the first row, else the add button. */
128
+ focus() {
129
+ const bad = this.invalidIndexes()[0];
130
+ const field = this._fields[bad ?? 0];
131
+ if (field) { field.focus(); return; }
132
+ this.root?.querySelector('.twm-sortable-list__add')?.focus();
133
+ }
134
+
135
+ dispose() {
136
+ this.#dropFields();
137
+ this.list.dispose();
138
+ this.root = null;
139
+ this.input = null;
140
+ }
141
+
142
+ #toItems(colors) {
143
+ // A colour that converts is kept as its hex; anything else is kept as
144
+ // typed, so it shows — and is reported — rather than vanishing.
145
+ return (Array.isArray(colors) ? colors : []).map((c) => ({
146
+ id: ++_seq,
147
+ text: normalizeHex(String(c ?? '')) ?? String(c ?? ''),
148
+ }));
149
+ }
150
+
151
+ /** Dispose every row's field: the rows are about to be rebuilt. */
152
+ #dropFields() {
153
+ for (const f of this._fields) f?.dispose();
154
+ this._fields = [];
155
+ }
156
+
157
+ #renderRow(container, item, index, callbacks) {
158
+ const count = this.list.getItems().length;
159
+
160
+ const row = document.createElement('div');
161
+ row.className = 'twm-color-list__row';
162
+ const field = new ColorField({
163
+ value: item.text,
164
+ presets: this.presets,
165
+ label: `Colour ${index + 1}`,
166
+ // Every edit goes into the list's items, so the list never holds
167
+ // a stale row; `_commit` tells its onChange which kind it was.
168
+ onChange: (_v, { commit }) => {
169
+ this._commit = commit;
170
+ this._rowEdit = true;
171
+ try { callbacks.update({ text: field.getText() }); }
172
+ finally { this._commit = true; this._rowEdit = false; }
173
+ },
174
+ });
175
+ this._fields[index] = field;
176
+ row.appendChild(field.render());
177
+
178
+ const move = (icon, label, disabled, fn) => {
179
+ const btn = document.createElement('button');
180
+ btn.type = 'button';
181
+ btn.className = 'twm-color-list__move';
182
+ btn.title = label;
183
+ btn.setAttribute('aria-label', label);
184
+ btn.disabled = disabled;
185
+ const glyph = document.createElement('span');
186
+ glyph.className = 'material-symbols-outlined';
187
+ glyph.setAttribute('aria-hidden', 'true');
188
+ glyph.textContent = icon;
189
+ btn.appendChild(glyph);
190
+ btn.addEventListener('click', fn);
191
+ row.appendChild(btn);
192
+ };
193
+ move('arrow_upward', 'Move up', index === 0, callbacks.moveUp);
194
+ move('arrow_downward', 'Move down', index === count - 1, callbacks.moveDown);
195
+ container.appendChild(row);
196
+ }
197
+
198
+ #writeInput() {
199
+ if (this.input) this.input.value = JSON.stringify(this.getTexts());
200
+ }
201
+
202
+ #emit(commit) {
203
+ this.#writeInput();
204
+ if (this.input) {
205
+ this.input.dispatchEvent(new Event('input', { bubbles: true }));
206
+ if (commit) this.input.dispatchEvent(new Event('change', { bubbles: true }));
207
+ }
208
+ this.onChange?.(this.getValue(), { commit });
209
+ }
210
+ }
File without changes