@syware/jlib 0.2.0 → 0.4.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 (50) hide show
  1. package/README.md +1 -0
  2. package/dist/all.js +5 -3
  3. package/dist/atoms/jl-button.css_raw.js +1 -1
  4. package/dist/atoms/jl-button.d.ts +19 -0
  5. package/dist/atoms/jl-button.js +75 -23
  6. package/dist/atoms/jl-chip.css_raw.js +1 -1
  7. package/dist/atoms/jl-input.d.ts +6 -0
  8. package/dist/atoms/jl-input.js +4 -0
  9. package/dist/atoms/jl-select.d.ts +6 -0
  10. package/dist/atoms/jl-select.js +18 -1
  11. package/dist/atoms/jl-switch.d.ts +12 -1
  12. package/dist/atoms/jl-switch.js +11 -1
  13. package/dist/index.d.ts +5 -1
  14. package/dist/index.js +4 -2
  15. package/dist/lib/attributes.d.ts +5 -0
  16. package/dist/lib/attributes.js +13 -1
  17. package/dist/lib/flow.d.ts +10 -1
  18. package/dist/lib/flow.js +26 -9
  19. package/dist/lib/tokens.d.ts +2 -1
  20. package/dist/lib/tokens.js +13 -4
  21. package/dist/molecules/jl-accordion-item.css_raw.js +1 -1
  22. package/dist/molecules/jl-accordion-item.d.ts +6 -1
  23. package/dist/molecules/jl-accordion-item.js +21 -6
  24. package/dist/molecules/jl-alert.d.ts +10 -3
  25. package/dist/molecules/jl-alert.js +23 -6
  26. package/dist/molecules/jl-chip-group.css_raw.js +3 -0
  27. package/dist/molecules/jl-chip-group.d.ts +81 -0
  28. package/dist/molecules/jl-chip-group.js +259 -0
  29. package/dist/molecules/jl-search.d.ts +3 -1
  30. package/dist/molecules/jl-search.js +8 -6
  31. package/dist/molecules/jl-segmented.css_raw.js +1 -1
  32. package/dist/molecules/jl-segmented.js +14 -7
  33. package/dist/molecules/jl-stat.css_raw.js +1 -1
  34. package/dist/molecules/jl-stat.d.ts +6 -0
  35. package/dist/molecules/jl-stat.js +6 -0
  36. package/dist/molecules/jl-tag-input.css_raw.js +1 -1
  37. package/dist/organisms/jl-flow.css_raw.js +1 -1
  38. package/dist/organisms/jl-flow.d.ts +28 -5
  39. package/dist/organisms/jl-flow.js +138 -33
  40. package/dist/organisms/jl-tab-bar.css_raw.js +3 -0
  41. package/dist/organisms/jl-tab-bar.d.ts +43 -0
  42. package/dist/organisms/jl-tab-bar.js +123 -0
  43. package/dist/templates/jl-app-shell.css_raw.js +1 -1
  44. package/dist/templates/jl-app-shell.d.ts +8 -2
  45. package/dist/templates/jl-app-shell.js +32 -5
  46. package/dist/types/jl-chip-group.types.d.ts +10 -0
  47. package/dist/types/jl-flow.types.d.ts +17 -0
  48. package/dist/types/jl-segmented.types.d.ts +2 -0
  49. package/dist/types/jl-tab-bar.types.d.ts +11 -0
  50. package/package.json +1 -1
@@ -0,0 +1,259 @@
1
+ import { BASE_STYLES } from "../styles/shared.js";
2
+ import { JlFormControl } from "../base/jl-form-control.js";
3
+ import { jsonOf, oneOf, stringList } from "../lib/attributes.js";
4
+ import { registerIcons } from "../lib/icons.js";
5
+ import { CONTROL_SIZES } from "../lib/options.js";
6
+ import { setOrRemove } from "../lib/dom.js";
7
+ import { JlIcon } from "../atoms/jl-icon.js";
8
+ import { rovingIndex } from "../lib/roving-index.js";
9
+ import jl_chip_group_default from "./jl-chip-group.css_raw.js";
10
+ import { css } from "@jadis/core";
11
+ import { Check } from "lucide";
12
+ import { jsx, jsxs } from "@jadis/core/jsx-runtime";
13
+ // src/molecules/jl-chip-group.tsx
14
+ registerIcons({ check: Check });
15
+ var ICON_SIZES = {
16
+ lg: 16,
17
+ md: 14,
18
+ sm: 12
19
+ };
20
+ var SELECTED_ICON = "check";
21
+ var CHIP_SELECTOR = ".chip";
22
+ var VALUE_MISSING = "Please select one of these options.";
23
+ /** The keys that move between the chips; with one choice, the choice moves too, as with native radios. */
24
+ var MOVES = /* @__PURE__ */ new Map([
25
+ ["ArrowDown", "next"],
26
+ ["ArrowLeft", "previous"],
27
+ ["ArrowRight", "next"],
28
+ ["ArrowUp", "previous"],
29
+ ["End", "last"],
30
+ ["Home", "first"]
31
+ ]);
32
+ /** A JSON attribute that should hold a list of options; anything else gives none. */
33
+ var parseOptions = (value) => {
34
+ const parsed = jsonOf([])(value);
35
+ return Array.isArray(parsed) ? parsed : [];
36
+ };
37
+ /**
38
+ * A row of chips to choose from: a status to filter by, a category, a reason. One choice by default, as radios
39
+ * (choosing the chosen chip again keeps it); `multiple` lets people turn each chip on and off. On a narrow screen
40
+ * the row scrolls sideways; `wrap` lets it go to the next line instead. `change` fires with the chosen values.
41
+ */
42
+ var JlChipGroup = class extends JlFormControl {
43
+ static selector = "jl-chip-group";
44
+ events = this.useEvents();
45
+ options = this.useChange([], () => this.renderOptions(), {
46
+ attribute: "options",
47
+ parse: parseOptions
48
+ });
49
+ /** The chosen values, in the options' order; with one choice, a list of at most one. */
50
+ value = this.useChange([], () => this.renderValue(), {
51
+ attribute: "value",
52
+ parse: stringList
53
+ });
54
+ /** Several chips can be chosen, each turned on and off on its own. */
55
+ multiple = this.useChange(false, (value) => {
56
+ this.reflect("multiple", value);
57
+ this.renderValue();
58
+ }, { attribute: "multiple" });
59
+ /** Goes to the next line when the chips do not fit, instead of scrolling sideways. */
60
+ wrap = this.useChange(false, (value) => this.reflect("wrap", value), { attribute: "wrap" });
61
+ size = this.useChange("md", (value) => {
62
+ this.reflect("size", value);
63
+ this.renderOptions();
64
+ }, {
65
+ attribute: "size",
66
+ parse: oneOf(CONTROL_SIZES, "md")
67
+ });
68
+ /** The group's name for screen readers, when no visible label names it. */
69
+ label = this.useChange("", (value) => setOrRemove(this._refs.group, "aria-label", value), { attribute: "label" });
70
+ /** A form refuses to submit while nothing is chosen. */
71
+ required = this.useChange(false, (value) => {
72
+ this.reflect("required", value);
73
+ this._refs.group.toggleAttribute("aria-required", value);
74
+ this.renderValidity();
75
+ }, { attribute: "required" });
76
+ disabled = this.useChange(false, (value) => {
77
+ this.reflect("disabled", value);
78
+ this.renderValue();
79
+ }, { attribute: "disabled" });
80
+ /** The chip that holds the Tab stop with several choices: the last one people reached. */
81
+ _state = {
82
+ disabledByForm: false,
83
+ stop: ""
84
+ };
85
+ _nameWatch = new MutationObserver(() => this.renderFormValue());
86
+ _refs = this.useRefs((ref) => ({ group: ref(".group") }));
87
+ /** Focuses the chip that holds the Tab stop. */
88
+ focus(options) {
89
+ this.chips().find((chip) => chip.tabIndex === 0)?.focus(options);
90
+ }
91
+ templateHtml() {
92
+ return /* @__PURE__ */ jsx("div", {
93
+ class: "group",
94
+ role: "radiogroup",
95
+ attrs: { part: "base" }
96
+ });
97
+ }
98
+ templateCss() {
99
+ return css`${BASE_STYLES}${jl_chip_group_default}`;
100
+ }
101
+ onConnect() {
102
+ this._nameWatch.observe(this, {
103
+ attributeFilter: ["name"],
104
+ attributes: true
105
+ });
106
+ this.renderFormValue();
107
+ this.on(this._refs.group, "click", (event) => {
108
+ const chip = event.target.closest(CHIP_SELECTOR);
109
+ if (chip !== null && !chip.disabled) this.press(chip);
110
+ });
111
+ this.on(this._refs.group, "keydown", (event) => this.onKeydown(event));
112
+ }
113
+ onDisconnect() {
114
+ this._nameWatch.disconnect();
115
+ }
116
+ resetValue() {
117
+ this.value.set(stringList(this.getAttribute("value")));
118
+ }
119
+ /** The browser calls it for a disabled fieldset and for the element's own `disabled`, even before it renders. */
120
+ setDisabledByForm(disabled) {
121
+ this._state.disabledByForm = disabled;
122
+ this.whenRendered(() => this.renderValue());
123
+ }
124
+ chips() {
125
+ return [...this._refs.group.querySelectorAll(CHIP_SELECTOR)];
126
+ }
127
+ /** What is chosen: with one choice, only the first value counts. */
128
+ chosen() {
129
+ const values = this.value.get();
130
+ return this.multiple.get() ? values : values.slice(0, 1);
131
+ }
132
+ isDisabled() {
133
+ return this.disabled.get() || this._state.disabledByForm;
134
+ }
135
+ /** One choice: the chip becomes it. Several: the chip turns on or off, the values keeping the options' order. */
136
+ press(chip) {
137
+ const value = chip.dataset.value ?? "";
138
+ this._state.stop = value;
139
+ const chosen = this.chosen();
140
+ if (!this.multiple.get()) {
141
+ if (chosen[0] === value) {
142
+ this.renderTabStop();
143
+ return;
144
+ }
145
+ this.commit([value]);
146
+ return;
147
+ }
148
+ const next = new Set(chosen);
149
+ if (next.has(value)) next.delete(value);
150
+ else next.add(value);
151
+ this.commit(this.options.get().flatMap((option) => next.has(option.value) ? [option.value] : []));
152
+ }
153
+ commit(values) {
154
+ this.value.set(values);
155
+ this.events.emit("change", values);
156
+ }
157
+ onKeydown(event) {
158
+ const move = MOVES.get(event.key);
159
+ const chips = this.chips();
160
+ const from = chips.indexOf(event.target);
161
+ if (move === void 0 || from === -1) return;
162
+ event.preventDefault();
163
+ const target = chips[rovingIndex(chips.map((chip) => chip.disabled), from, move)];
164
+ if (target === void 0) return;
165
+ target.focus();
166
+ if (this.multiple.get()) {
167
+ this._state.stop = target.dataset.value ?? "";
168
+ this.renderTabStop();
169
+ return;
170
+ }
171
+ this.press(target);
172
+ }
173
+ renderOptions() {
174
+ const iconSize = ICON_SIZES[this.size.get()];
175
+ this._refs.group.replaceChildren(...this.options.get().map((option) => this.chip(option, iconSize)));
176
+ this.renderValue();
177
+ }
178
+ chip(option, iconSize) {
179
+ const badge = option.badge === void 0 ? "" : String(option.badge);
180
+ return /* @__PURE__ */ jsxs("button", {
181
+ type: "button",
182
+ class: "chip",
183
+ tabIndex: -1,
184
+ "data-value": option.value,
185
+ "data-icon": option.icon,
186
+ "data-disabled": option.disabled === true ? "" : void 0,
187
+ children: [
188
+ /* @__PURE__ */ jsx(JlIcon, {
189
+ class: "icon",
190
+ size: iconSize,
191
+ hidden: true
192
+ }),
193
+ /* @__PURE__ */ jsx("span", {
194
+ class: "label",
195
+ children: option.label
196
+ }),
197
+ badge === "" ? "" : /* @__PURE__ */ jsx("span", {
198
+ class: "badge",
199
+ children: badge
200
+ })
201
+ ]
202
+ });
203
+ }
204
+ /** Radios with one choice, toggle buttons with several; each chosen chip shows its icon or a check. */
205
+ renderValue() {
206
+ const chosen = new Set(this.chosen());
207
+ const multiple = this.multiple.get();
208
+ const groupDisabled = this.isDisabled();
209
+ this._refs.group.setAttribute("role", multiple ? "group" : "radiogroup");
210
+ this._refs.group.toggleAttribute("aria-disabled", groupDisabled);
211
+ for (const chip of this.chips()) {
212
+ const selected = chosen.has(chip.dataset.value ?? "");
213
+ chip.disabled = groupDisabled || chip.dataset.disabled !== void 0;
214
+ setOrRemove(chip, "role", multiple ? "" : "radio");
215
+ setOrRemove(chip, "aria-checked", multiple ? "" : String(selected));
216
+ setOrRemove(chip, "aria-pressed", multiple ? String(selected) : "");
217
+ chip.classList.toggle("selected", selected);
218
+ const icon = chip.querySelector(".icon");
219
+ const name = chip.dataset.icon ?? (selected ? SELECTED_ICON : "");
220
+ icon?.name.set(name);
221
+ if (icon !== null) icon.hidden = name === "";
222
+ }
223
+ this.renderTabStop();
224
+ this.renderFormValue();
225
+ this.renderValidity();
226
+ }
227
+ /** Tab reaches one chip: the chosen one with one choice, the last one reached with several, else the first. */
228
+ renderTabStop() {
229
+ const enabled = this.chips().filter((chip) => !chip.disabled);
230
+ const chosen = this.chosen();
231
+ const stop = (this.multiple.get() ? [this._state.stop, ...chosen] : chosen).map((value) => enabled.find((chip) => chip.dataset.value === value)).find((chip) => chip !== void 0) ?? enabled[0];
232
+ for (const chip of this.chips()) chip.tabIndex = chip === stop ? 0 : -1;
233
+ }
234
+ /** One choice gives one entry; several give one entry each under the field's name, as checkboxes would. */
235
+ renderFormValue() {
236
+ const chosen = this.chosen();
237
+ if (!this.multiple.get()) {
238
+ this.internals.setFormValue(chosen[0] ?? null);
239
+ return;
240
+ }
241
+ const name = this.getAttribute("name") ?? "";
242
+ if (name === "") {
243
+ this.internals.setFormValue(null);
244
+ return;
245
+ }
246
+ const data = new FormData();
247
+ for (const value of chosen) data.append(name, value);
248
+ this.internals.setFormValue(data);
249
+ }
250
+ renderValidity() {
251
+ if (!(this.required.get() && this.chosen().length === 0)) {
252
+ this.internals.setValidity({});
253
+ return;
254
+ }
255
+ this.internals.setValidity({ valueMissing: true }, VALUE_MISSING, this.chips()[0] ?? this._refs.group);
256
+ }
257
+ };
258
+ JlChipGroup.register();
259
+ export { JlChipGroup };
@@ -9,6 +9,7 @@ export declare class JlSearch extends JlFormControl {
9
9
  readonly events: Readonly<import("@jadis/core").UseEventsHandler<{
10
10
  input: string;
11
11
  search: string;
12
+ enter: string;
12
13
  }>>;
13
14
  readonly value: Readonly<Readonly<{
14
15
  get(): string;
@@ -52,6 +53,8 @@ export declare class JlSearch extends JlFormControl {
52
53
  focus(options?: FocusOptions): void;
53
54
  /** Selects the whole text, ready to be replaced. */
54
55
  select(): void;
56
+ /** Empties the field, as its clear button does: `input` and `search` fire with `''`, and it takes the focus. */
57
+ clear(): void;
55
58
  templateHtml(): Node;
56
59
  templateCss(): string;
57
60
  onConnect(): void;
@@ -64,7 +67,6 @@ export declare class JlSearch extends JlFormControl {
64
67
  private onPageKey;
65
68
  private scheduleSearch;
66
69
  private search;
67
- private clear;
68
70
  private renderValue;
69
71
  private renderActions;
70
72
  private renderShortcut;
@@ -77,6 +77,13 @@ var JlSearch = class extends JlFormControl {
77
77
  select() {
78
78
  this._refs.native.select();
79
79
  }
80
+ /** Empties the field, as its clear button does: `input` and `search` fire with `''`, and it takes the focus. */
81
+ clear() {
82
+ this.value.set("");
83
+ this.events.emit("input", "");
84
+ this.search("");
85
+ this.focus();
86
+ }
80
87
  templateHtml() {
81
88
  return /* @__PURE__ */ jsxs("div", {
82
89
  class: "control",
@@ -151,6 +158,7 @@ var JlSearch = class extends JlFormControl {
151
158
  if (event.key === "Enter") {
152
159
  event.preventDefault();
153
160
  this.search(this.value.get());
161
+ this.events.emit("enter", this.value.get());
154
162
  return;
155
163
  }
156
164
  if (event.key === "Escape" && this.value.get() !== "") {
@@ -178,12 +186,6 @@ var JlSearch = class extends JlFormControl {
178
186
  this._state.lastSearch = query;
179
187
  this.events.emit("search", query);
180
188
  }
181
- clear() {
182
- this.value.set("");
183
- this.events.emit("input", "");
184
- this.search("");
185
- this.focus();
186
- }
187
189
  renderValue(value) {
188
190
  const native = this._refs.native;
189
191
  if (native.value !== value) native.value = value;
@@ -1,3 +1,3 @@
1
1
  // src/molecules/jl-segmented.css?raw
2
- var jl_segmented_default = ":host {\n display: inline-flex;\n vertical-align: middle;\n /* Its own width, even as a grid or flex item that would stretch it: block asks for the whole width. */\n justify-self: start;\n max-width: 100%;\n --height: var(--jl-control-md);\n --pad: 12px;\n --font-size: var(--jl-font-sm);\n}\n\n:host([size=\"sm\"]) {\n --height: var(--jl-control-sm);\n --pad: 10px;\n --font-size: var(--jl-font-xs);\n}\n\n:host([size=\"lg\"]) {\n --height: var(--jl-control-lg);\n --pad: 16px;\n --font-size: var(--jl-font-md);\n}\n\n:host([block]) {\n display: flex;\n justify-self: stretch;\n}\n\n.group {\n position: relative;\n display: flex;\n flex: 1;\n overflow-x: auto;\n scrollbar-width: none;\n min-width: 0;\n height: var(--height);\n padding: 2px;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius-sm);\n background: var(--jl-surface-2);\n}\n\n.segment {\n position: relative;\n z-index: 1;\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n gap: 6px;\n min-width: calc(var(--height) - 6px);\n padding: 0 var(--pad);\n border: none;\n border-radius: calc(var(--jl-radius-sm) - 3px);\n background: none;\n color: var(--jl-muted);\n font-size: var(--font-size);\n font-weight: 600;\n white-space: nowrap;\n cursor: pointer;\n transition: color var(--jl-fast) var(--jl-ease);\n}\n\n.segment:hover:not(:disabled) {\n color: var(--jl-text);\n}\n\n.segment[aria-checked=\"true\"] {\n color: var(--jl-text);\n}\n\n.segment:disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n:host([block]) .segment {\n flex: 1 1 0;\n}\n\n/* The chosen option's raised surface, slid under it. */\n.thumb {\n position: absolute;\n top: 0;\n left: 0;\n width: var(--w);\n height: var(--h);\n border: 1px solid var(--jl-line-strong);\n border-radius: calc(var(--jl-radius-sm) - 3px);\n /* Raised over the track: white on light grey, a lighter grey on a dark ground. */\n background: light-dark(var(--jl-surface), var(--jl-surface-3));\n box-shadow: var(--jl-shadow-sm);\n transform: translate(var(--x), var(--y));\n pointer-events: none;\n}\n\n.thumb[data-placed] {\n transition:\n transform var(--jl-slow) var(--jl-ease),\n width var(--jl-slow) var(--jl-ease);\n}\n\n.group[aria-disabled] {\n opacity: 0.55;\n}\n\n.group[aria-disabled] .segment {\n opacity: 1;\n}\n\n/* The track scrolls when the options do not fit: a ring drawn outside the segment would be cut. */\n.segment:focus-visible {\n box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--jl-brand) 60%, transparent);\n}\n";
2
+ var jl_segmented_default = ":host {\n display: inline-flex;\n vertical-align: middle;\n /* Its own width, even as a grid or flex item that would stretch it: block asks for the whole width. */\n justify-self: start;\n max-width: 100%;\n --height: var(--jl-control-md);\n --pad: 12px;\n --font-size: var(--jl-font-sm);\n}\n\n:host([size=\"sm\"]) {\n --height: var(--jl-control-sm);\n --pad: 10px;\n --font-size: var(--jl-font-xs);\n}\n\n:host([size=\"lg\"]) {\n --height: var(--jl-control-lg);\n --pad: 16px;\n --font-size: var(--jl-font-md);\n}\n\n:host([block]) {\n display: flex;\n justify-self: stretch;\n}\n\n.group {\n position: relative;\n display: flex;\n flex: 1;\n overflow-x: auto;\n scrollbar-width: none;\n min-width: 0;\n height: var(--height);\n padding: 2px;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius-sm);\n background: var(--jl-surface-2);\n}\n\n.segment {\n position: relative;\n z-index: 1;\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n gap: 6px;\n min-width: calc(var(--height) - 6px);\n padding: 0 var(--pad);\n border: none;\n border-radius: calc(var(--jl-radius-sm) - 3px);\n background: none;\n color: var(--jl-muted);\n font-size: var(--font-size);\n font-weight: 600;\n white-space: nowrap;\n cursor: pointer;\n transition: color var(--jl-fast) var(--jl-ease);\n}\n\n.segment:hover:not(:disabled) {\n color: var(--jl-text);\n}\n\n.segment[aria-checked=\"true\"] {\n color: var(--jl-text);\n}\n\n.badge {\n min-width: 18px;\n padding: 0 6px;\n border-radius: var(--jl-radius-pill);\n background: var(--jl-surface-3);\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n font-weight: 650;\n line-height: 18px;\n text-align: center;\n}\n\n.segment[aria-checked=\"true\"] .badge {\n background: var(--jl-accent-soft);\n color: var(--jl-accent);\n}\n\n.segment:disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n:host([block]) .segment {\n flex: 1 1 0;\n}\n\n/* The chosen option's raised surface, slid under it. */\n.thumb {\n position: absolute;\n top: 0;\n left: 0;\n width: var(--w);\n height: var(--h);\n border: 1px solid var(--jl-line-strong);\n border-radius: calc(var(--jl-radius-sm) - 3px);\n /* Raised over the track: white on light grey, a lighter grey on a dark ground. */\n background: light-dark(var(--jl-surface), var(--jl-surface-3));\n box-shadow: var(--jl-shadow-sm);\n transform: translate(var(--x), var(--y));\n pointer-events: none;\n}\n\n.thumb[data-placed] {\n transition:\n transform var(--jl-slow) var(--jl-ease),\n width var(--jl-slow) var(--jl-ease);\n}\n\n.group[aria-disabled] {\n opacity: 0.55;\n}\n\n.group[aria-disabled] .segment {\n opacity: 1;\n}\n\n/* The track scrolls when the options do not fit: a ring drawn outside the segment would be cut. */\n.segment:focus-visible {\n box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--jl-brand) 60%, transparent);\n}\n";
3
3
  export { jl_segmented_default as default };
@@ -126,13 +126,20 @@ var JlSegmented = class extends JlFormControl {
126
126
  class: "segment",
127
127
  "data-value": option.value,
128
128
  "data-disabled": option.disabled === true ? "" : void 0,
129
- children: [option.icon === void 0 ? "" : /* @__PURE__ */ jsx(JlIcon, {
130
- name: option.icon,
131
- size: iconSize
132
- }), label === "" ? "" : /* @__PURE__ */ jsx("span", {
133
- class: "label",
134
- children: label
135
- })]
129
+ children: [
130
+ option.icon === void 0 ? "" : /* @__PURE__ */ jsx(JlIcon, {
131
+ name: option.icon,
132
+ size: iconSize
133
+ }),
134
+ label === "" ? "" : /* @__PURE__ */ jsx("span", {
135
+ class: "label",
136
+ children: label
137
+ }),
138
+ option.badge === void 0 ? "" : /* @__PURE__ */ jsx("span", {
139
+ class: "badge",
140
+ children: String(option.badge)
141
+ })
142
+ ]
136
143
  });
137
144
  }
138
145
  /** The chosen option's index: the one `value` names, or else the first that can be chosen. */
@@ -1,3 +1,3 @@
1
1
  // src/molecules/jl-stat.css?raw
2
- var jl_stat_default = ":host {\n display: block;\n --mood: var(--jl-muted);\n}\n\n:host([mood=\"good\"]) {\n --mood: var(--jl-ok);\n}\n\n:host([mood=\"bad\"]) {\n --mood: var(--jl-danger);\n}\n\n.stat {\n display: grid;\n gap: 4px;\n min-width: 0;\n}\n\n.top {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.label {\n flex: 1;\n min-width: 0;\n color: var(--jl-muted);\n font-size: var(--jl-font-sm);\n font-weight: 600;\n line-height: 1.4;\n}\n\n.icon {\n color: var(--jl-faint);\n}\n\n.figure {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n column-gap: 10px;\n}\n\n.value {\n color: var(--jl-text);\n font-size: var(--jl-font-2xl);\n font-variant-numeric: tabular-nums;\n font-weight: 700;\n letter-spacing: -0.02em;\n line-height: 1.2;\n}\n\n.delta {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n color: var(--mood);\n font-size: var(--jl-font-sm);\n font-variant-numeric: tabular-nums;\n font-weight: 650;\n white-space: nowrap;\n}\n\n.caption {\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n line-height: 1.4;\n}\n\n.spark {\n height: 36px;\n margin-top: 8px;\n}\n\n.spark svg {\n display: block;\n width: 100%;\n height: 100%;\n overflow: visible;\n}\n\n.line {\n fill: none;\n stroke: var(--jl-accent);\n stroke-linecap: round;\n stroke-linejoin: round;\n stroke-width: 2;\n vector-effect: non-scaling-stroke;\n}\n\n.area {\n fill: color-mix(in srgb, var(--jl-accent) 10%, transparent);\n stroke: none;\n}\n";
2
+ var jl_stat_default = ":host {\n display: block;\n --mood: var(--jl-muted);\n}\n\n:host([mood=\"good\"]) {\n --mood: var(--jl-ok);\n}\n\n:host([mood=\"bad\"]) {\n --mood: var(--jl-danger);\n}\n\n.stat {\n display: grid;\n gap: 4px;\n min-width: 0;\n}\n\n.top {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.label {\n flex: 1;\n min-width: 0;\n color: var(--jl-muted);\n font-size: var(--jl-font-sm);\n font-weight: 600;\n line-height: 1.4;\n}\n\n.icon {\n color: var(--jl-faint);\n}\n\n:host([tone=\"accent\"]) .icon {\n color: var(--jl-accent);\n}\n\n:host([tone=\"ok\"]) .icon {\n color: var(--jl-ok);\n}\n\n:host([tone=\"warn\"]) .icon {\n color: var(--jl-warn);\n}\n\n:host([tone=\"danger\"]) .icon {\n color: var(--jl-danger);\n}\n\n:host([tone=\"info\"]) .icon {\n color: var(--jl-info);\n}\n\n.figure {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n column-gap: 10px;\n}\n\n.value {\n color: var(--jl-text);\n font-size: var(--jl-font-2xl);\n font-variant-numeric: tabular-nums;\n font-weight: 700;\n letter-spacing: -0.02em;\n line-height: 1.2;\n}\n\n.delta {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n color: var(--mood);\n font-size: var(--jl-font-sm);\n font-variant-numeric: tabular-nums;\n font-weight: 650;\n white-space: nowrap;\n}\n\n.caption {\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n line-height: 1.4;\n}\n\n.spark {\n height: 36px;\n margin-top: 8px;\n}\n\n.spark svg {\n display: block;\n width: 100%;\n height: 100%;\n overflow: visible;\n}\n\n.line {\n fill: none;\n stroke: var(--jl-accent);\n stroke-linecap: round;\n stroke-linejoin: round;\n stroke-width: 2;\n vector-effect: non-scaling-stroke;\n}\n\n.area {\n fill: color-mix(in srgb, var(--jl-accent) 10%, transparent);\n stroke: none;\n}\n";
3
3
  export { jl_stat_default as default };
@@ -1,5 +1,6 @@
1
1
  import { JlElement } from '../base/jl-element';
2
2
  import type { StatTrend } from '../types/jl-stat.types';
3
+ import type { Tone } from '../types/options.types';
3
4
  export declare const STAT_TRENDS: readonly StatTrend[];
4
5
  /**
5
6
  * A key figure: what it measures, its value, how it moved (`delta`, coloured by its trend) and an optional
@@ -36,6 +37,11 @@ export declare class JlStat extends JlElement {
36
37
  get(): string;
37
38
  set(setter: string | ((prevState: string) => string)): void;
38
39
  }>>;
40
+ /** The colour of the icon: neutral (quiet) by default, or what the figure is about (`ok`, `info`…). */
41
+ readonly tone: Readonly<Readonly<{
42
+ get(): Tone;
43
+ set(setter: ((prevState: Tone) => Tone) | Tone): void;
44
+ }>>;
39
45
  /** A note under the figure: "vs. last month". */
40
46
  readonly caption: Readonly<Readonly<{
41
47
  get(): string;
@@ -2,6 +2,7 @@ import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { jsonOf, oneOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
+ import { TONES } from "../lib/options.js";
5
6
  import { JlIcon } from "../atoms/jl-icon.js";
6
7
  import { areaAttribute, pointsAttribute, sparklinePoints } from "../lib/sparkline.js";
7
8
  import { trendOfDelta } from "../lib/trend.js";
@@ -71,6 +72,11 @@ var JlStat = class extends JlElement {
71
72
  this._refs.icon.name.set(value);
72
73
  this._refs.icon.hidden = value === "";
73
74
  }, { attribute: "icon" });
75
+ /** The colour of the icon: neutral (quiet) by default, or what the figure is about (`ok`, `info`…). */
76
+ tone = this.useChange("neutral", (value) => this.reflect("tone", value), {
77
+ attribute: "tone",
78
+ parse: oneOf(TONES, "neutral")
79
+ });
74
80
  /** A note under the figure: "vs. last month". */
75
81
  caption = this.useChange("", (value) => {
76
82
  this._refs.caption.textContent = value;
@@ -1,3 +1,3 @@
1
1
  // src/molecules/jl-tag-input.css?raw
2
- var jl_tag_input_default = ":host {\n display: block;\n /* As a grid or flex item, the field may be narrower than its text field would like. */\n min-width: 0;\n --chip-height: calc(var(--height, var(--jl-control-md)) - 12px);\n}\n\n.control {\n flex-wrap: wrap;\n gap: 4px;\n padding: 4px 6px;\n cursor: text;\n}\n\n/* The chips are items of the frame's own flow, so the field follows the last one on its line. */\n.tags {\n display: contents;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.tag {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n max-width: 100%;\n height: var(--chip-height);\n padding: 0 3px 0 8px;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius-xs);\n background: var(--jl-surface-2);\n color: var(--jl-text);\n font-size: var(--jl-font-sm);\n line-height: 1;\n cursor: default;\n}\n\n:host([size=\"sm\"]) .tag {\n padding-left: 6px;\n font-size: var(--jl-font-xs);\n}\n\n.tag.flash {\n animation: flash 600ms var(--jl-ease);\n}\n\n.tag-text {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.tag-remove {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n padding: 0;\n border: none;\n border-radius: 4px;\n background: none;\n color: var(--jl-faint);\n cursor: pointer;\n}\n\n.tag-remove:hover,\n.tag-remove:focus-visible {\n background: var(--jl-surface-3);\n color: var(--jl-text);\n}\n\n.native {\n flex: 1 0 80px;\n height: calc(var(--height, var(--jl-control-md)) - 10px);\n padding-left: 6px;\n}\n\n:host([disabled]) .tag {\n padding-right: 8px;\n color: var(--jl-muted);\n}\n\n.popup {\n min-width: 180px;\n padding: 4px;\n}\n\n.listbox {\n position: relative;\n max-height: 240px;\n margin: 0;\n padding: 0;\n overflow-y: auto;\n list-style: none;\n}\n\n.option {\n padding: 6px 10px;\n border-radius: var(--jl-radius-xs);\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n line-height: 1.4;\n white-space: nowrap;\n cursor: pointer;\n user-select: none;\n}\n\n.option.active,\n.option:hover {\n background: var(--jl-surface-3);\n}\n\nmark {\n background: none;\n color: var(--jl-accent);\n font-weight: 700;\n}\n\n@keyframes flash {\n 30% {\n border-color: var(--jl-accent-line);\n background: var(--jl-accent-soft);\n color: var(--jl-accent-strong);\n }\n}\n";
2
+ var jl_tag_input_default = ":host {\n display: block;\n /* As a grid or flex item, the field may be narrower than its text field would like. */\n min-width: 0;\n --chip-height: calc(var(--height, var(--jl-control-md)) - 12px);\n}\n\n.control {\n flex-wrap: wrap;\n gap: 4px;\n padding: 4px 6px;\n cursor: text;\n}\n\n/* The chips are items of the frame's own flow, so the field follows the last one on its line. */\n.tags {\n display: contents;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.tag {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n max-width: 100%;\n height: var(--chip-height);\n padding: 0 3px 0 8px;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius-xs);\n background: var(--jl-surface-2);\n color: var(--jl-text);\n font-size: var(--jl-font-sm);\n line-height: 1;\n cursor: default;\n}\n\n:host([size=\"sm\"]) .tag {\n padding-left: 6px;\n font-size: var(--jl-font-xs);\n}\n\n.tag.flash {\n animation: flash 600ms var(--jl-ease);\n}\n\n/* A line box taller than the tag's own line-height of 1: the accents of É, Â, Ö stay inside what overflow clips. */\n.tag-text {\n overflow: hidden;\n line-height: 1.4;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.tag-remove {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n padding: 0;\n border: none;\n border-radius: 4px;\n background: none;\n color: var(--jl-faint);\n cursor: pointer;\n}\n\n.tag-remove:hover,\n.tag-remove:focus-visible {\n background: var(--jl-surface-3);\n color: var(--jl-text);\n}\n\n.native {\n flex: 1 0 80px;\n height: calc(var(--height, var(--jl-control-md)) - 10px);\n padding-left: 6px;\n}\n\n:host([disabled]) .tag {\n padding-right: 8px;\n color: var(--jl-muted);\n}\n\n.popup {\n min-width: 180px;\n padding: 4px;\n}\n\n.listbox {\n position: relative;\n max-height: 240px;\n margin: 0;\n padding: 0;\n overflow-y: auto;\n list-style: none;\n}\n\n.option {\n padding: 6px 10px;\n border-radius: var(--jl-radius-xs);\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n line-height: 1.4;\n white-space: nowrap;\n cursor: pointer;\n user-select: none;\n}\n\n.option.active,\n.option:hover {\n background: var(--jl-surface-3);\n}\n\nmark {\n background: none;\n color: var(--jl-accent);\n font-weight: 700;\n}\n\n@keyframes flash {\n 30% {\n border-color: var(--jl-accent-line);\n background: var(--jl-accent-soft);\n color: var(--jl-accent-strong);\n }\n}\n";
3
3
  export { jl_tag_input_default as default };
@@ -1,3 +1,3 @@
1
1
  // src/organisms/jl-flow.css?raw
2
- var jl_flow_default = ":host {\n display: block;\n height: 420px;\n min-height: 0;\n}\n\n.viewport {\n position: relative;\n height: 100%;\n overflow: clip;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius-lg);\n background-color: var(--jl-surface-2);\n background-image: radial-gradient(circle, var(--jl-line-strong) 1px, transparent 1.2px);\n outline: none;\n touch-action: none;\n user-select: none;\n}\n\n.viewport:focus-visible {\n box-shadow: var(--jl-focus-ring);\n}\n\n.world,\n.nodes,\n.edges {\n position: absolute;\n top: 0;\n left: 0;\n}\n\n.world {\n transform-origin: 0 0;\n}\n\n.edges {\n overflow: visible;\n pointer-events: none;\n}\n\n.edge {\n fill: none;\n stroke: color-mix(in srgb, var(--tone, var(--jl-muted)) 60%, var(--jl-line-strong));\n stroke-width: 2;\n stroke-linecap: round;\n}\n\n.edge[data-tone=\"danger\"] {\n stroke-dasharray: 6 5;\n}\n\n.edge-hit {\n fill: none;\n stroke: transparent;\n stroke-width: 14;\n pointer-events: stroke;\n cursor: pointer;\n}\n\n.edge-group:hover .edge {\n stroke: var(--tone, var(--jl-muted));\n}\n\n.edge-group.selected .edge {\n stroke: var(--jl-accent);\n stroke-width: 3;\n}\n\n.draft {\n stroke: var(--jl-accent);\n stroke-dasharray: 5 5;\n}\n\n.node {\n --tone: var(--jl-accent);\n position: absolute;\n top: 0;\n left: 0;\n display: flex;\n align-items: center;\n gap: var(--jl-space-3);\n width: 240px;\n height: 64px;\n padding: 0 var(--jl-space-3) var(--jl-space-1);\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius-lg);\n background: var(--jl-surface);\n box-shadow: var(--jl-shadow-sm);\n cursor: grab;\n transition:\n border-color var(--jl-fast) var(--jl-ease),\n box-shadow var(--jl-fast) var(--jl-ease);\n}\n\n.node:hover {\n border-color: var(--jl-line-strong);\n}\n\n.node:focus-visible {\n outline: none;\n box-shadow: var(--jl-focus-ring);\n}\n\n.node.selected {\n border-color: var(--jl-accent);\n box-shadow: 0 0 0 3px var(--jl-accent-soft);\n}\n\n.input {\n position: absolute;\n top: -5px;\n left: 50%;\n width: 10px;\n height: 10px;\n border: 2px solid var(--jl-surface);\n border-radius: 50%;\n background: var(--jl-line-strong);\n transform: translateX(-50%);\n}\n\n.icon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border-radius: var(--jl-radius);\n background: color-mix(in srgb, var(--tone) 14%, var(--jl-surface));\n color: var(--tone);\n}\n\n.text {\n display: grid;\n min-width: 0;\n}\n\n.label,\n.description {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.label {\n color: var(--jl-text);\n font-size: var(--jl-font-sm);\n font-weight: 600;\n}\n\n.description {\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n}\n\n.ports {\n position: absolute;\n right: 0;\n bottom: -11px;\n left: 0;\n height: 22px;\n}\n\n.port {\n --tone: var(--jl-muted);\n position: absolute;\n top: 0;\n display: inline-flex;\n align-items: center;\n gap: var(--jl-space-1);\n height: 22px;\n padding: 0 7px;\n border: 1px solid color-mix(in srgb, var(--tone) 35%, var(--jl-line));\n border-radius: var(--jl-radius-pill);\n background: var(--jl-surface);\n color: color-mix(in srgb, var(--tone) 80%, var(--jl-text));\n font-size: var(--jl-font-xs);\n font-weight: 600;\n white-space: nowrap;\n transform: translateX(-50%);\n cursor: crosshair;\n}\n\n.port:has(.add) {\n padding-right: 3px;\n}\n\n.dot {\n flex: none;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--tone);\n}\n\n.port-label {\n overflow: hidden;\n min-width: 0;\n text-overflow: ellipsis;\n}\n\n.add,\n.remove-edge {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border-radius: 50%;\n cursor: pointer;\n}\n\n.add {\n width: 16px;\n height: 16px;\n border: none;\n background: color-mix(in srgb, var(--tone) 16%, var(--jl-surface));\n color: var(--tone);\n}\n\n.add:hover {\n background: var(--tone);\n color: var(--jl-surface);\n}\n\n.remove-edge {\n position: absolute;\n top: 0;\n left: 0;\n width: 24px;\n height: 24px;\n border: 1px solid var(--jl-line);\n background: var(--jl-surface);\n color: var(--jl-danger);\n}\n\n.remove-edge:hover {\n border-color: var(--jl-danger);\n background: var(--jl-danger);\n color: var(--jl-on-danger);\n}\n\n.empty {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n padding: var(--jl-space-6);\n color: var(--jl-muted);\n text-align: center;\n pointer-events: none;\n}\n\n.empty[hidden] {\n display: none;\n}\n\n.empty ::slotted(*) {\n pointer-events: auto;\n}\n\n.controls {\n position: absolute;\n bottom: var(--jl-space-3);\n left: var(--jl-space-3);\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 2px;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius);\n background: var(--jl-surface);\n}\n\n/* The zoom buttons: small and quiet, as ghost buttons. */\n.control {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--jl-control-sm);\n height: var(--jl-control-sm);\n padding: 0;\n border: none;\n border-radius: var(--jl-radius-sm);\n background: transparent;\n color: var(--jl-accent);\n cursor: pointer;\n transition: background var(--jl-fast) var(--jl-ease);\n}\n\n.control:hover {\n background: var(--jl-accent-soft);\n}\n\n.zoom-label {\n min-width: 40px;\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n font-variant-numeric: tabular-nums;\n text-align: center;\n}\n\n/* The tones of icons, exits and edges: after .node and .port, whose default tones they replace. */\n[data-tone=\"neutral\"] {\n --tone: var(--jl-muted);\n}\n\n[data-tone=\"accent\"] {\n --tone: var(--jl-accent);\n}\n\n[data-tone=\"ok\"] {\n --tone: var(--jl-ok);\n}\n\n[data-tone=\"warn\"] {\n --tone: var(--jl-warn);\n}\n\n[data-tone=\"danger\"] {\n --tone: var(--jl-danger);\n}\n\n[data-tone=\"info\"] {\n --tone: var(--jl-info);\n}\n\n/* View only: nodes are pressed to be selected, not dragged, and their exits do not connect. */\n:host([readonly]) .node,\n:host([readonly]) .port {\n cursor: pointer;\n}\n\n/* View only: the layout stays as it was given. */\n:host([readonly]) .tidy {\n display: none;\n}\n";
2
+ var jl_flow_default = ":host {\n display: block;\n height: 420px;\n min-height: 0;\n}\n\n.viewport {\n position: relative;\n height: 100%;\n overflow: clip;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius-lg);\n background-color: var(--jl-surface-2);\n background-image: radial-gradient(circle, var(--jl-line-strong) 1px, transparent 1.2px);\n outline: none;\n touch-action: none;\n user-select: none;\n}\n\n.viewport:focus-visible {\n box-shadow: var(--jl-focus-ring);\n}\n\n.world,\n.nodes,\n.edges {\n position: absolute;\n top: 0;\n left: 0;\n}\n\n.world {\n transform-origin: 0 0;\n}\n\n.edges {\n overflow: visible;\n pointer-events: none;\n}\n\n.edge {\n fill: none;\n stroke: color-mix(in srgb, var(--tone, var(--jl-muted)) 60%, var(--jl-line-strong));\n stroke-width: 2;\n stroke-linecap: round;\n}\n\n.edge[data-tone=\"danger\"] {\n stroke-dasharray: 6 5;\n}\n\n.edge-hit {\n fill: none;\n stroke: transparent;\n stroke-width: 14;\n pointer-events: stroke;\n cursor: pointer;\n}\n\n.edge-group:hover .edge {\n stroke: var(--tone, var(--jl-muted));\n}\n\n.edge-group.taken .edge {\n stroke: var(--tone, var(--jl-muted));\n stroke-width: 3;\n}\n\n.edge-group.selected .edge {\n stroke: var(--jl-accent);\n stroke-width: 3;\n}\n\n.draft {\n stroke: var(--jl-accent);\n stroke-dasharray: 5 5;\n}\n\n/* The box's size is the one lib/flow.ts computes the edges with: 240 wide, and the height the script sets (64, or 96\n with a detail line). */\n.node {\n --tone: var(--jl-accent);\n position: absolute;\n top: 0;\n left: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n width: 240px;\n height: 64px;\n padding: 0 var(--jl-space-3) var(--jl-space-1);\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius-lg);\n background: var(--jl-surface);\n box-shadow: var(--jl-shadow-sm);\n cursor: grab;\n transition:\n border-color var(--jl-fast) var(--jl-ease),\n box-shadow var(--jl-fast) var(--jl-ease);\n}\n\n.node:hover {\n border-color: var(--jl-line-strong);\n}\n\n.node:focus-visible {\n outline: none;\n box-shadow: var(--jl-focus-ring);\n}\n\n/* How a node did in a run: a ring while it works or waits for someone, a border once it is done or failed. */\n.node[data-status=\"running\"] {\n --state: var(--jl-info);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--jl-info) 30%, transparent);\n}\n\n.node[data-status=\"done\"] {\n --state: var(--jl-ok);\n border-color: color-mix(in srgb, var(--jl-ok) 55%, var(--jl-line));\n}\n\n.node[data-status=\"waiting\"] {\n --state: var(--jl-brand);\n border-color: var(--jl-brand);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--jl-brand) 30%, transparent);\n}\n\n.node[data-status=\"failed\"] {\n --state: var(--jl-danger);\n border-color: var(--jl-danger);\n}\n\n.node.selected {\n border-color: var(--jl-accent);\n box-shadow: 0 0 0 3px var(--jl-accent-soft);\n}\n\n.head {\n display: flex;\n align-items: center;\n gap: var(--jl-space-3);\n min-width: 0;\n}\n\n/* With a detail line, the head sits at the top and the detail under it, clear of the exits below. */\n.node:has(.detail) {\n justify-content: flex-start;\n padding-top: 10px;\n}\n\n.detail {\n overflow: hidden;\n margin-top: 6px;\n padding-top: 5px;\n line-height: 16px;\n border-top: 1px dashed var(--jl-line);\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.problem {\n display: inline-flex;\n flex: none;\n color: var(--jl-danger);\n}\n\n.status {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: var(--state);\n color: var(--jl-on-accent);\n}\n\n.node[data-status=\"running\"] .status jl-icon {\n animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n to {\n rotate: 360deg;\n }\n}\n\n.input {\n position: absolute;\n top: -5px;\n left: 50%;\n width: 10px;\n height: 10px;\n border: 2px solid var(--jl-surface);\n border-radius: 50%;\n background: var(--jl-line-strong);\n transform: translateX(-50%);\n}\n\n.icon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border-radius: var(--jl-radius);\n background: color-mix(in srgb, var(--tone) 14%, var(--jl-surface));\n color: var(--tone);\n}\n\n.text {\n display: grid;\n flex: 1;\n min-width: 0;\n}\n\n.label,\n.description {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.label {\n color: var(--jl-text);\n font-size: var(--jl-font-sm);\n font-weight: 600;\n}\n\n.description {\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n}\n\n.ports {\n position: absolute;\n right: 0;\n bottom: -11px;\n left: 0;\n height: 22px;\n}\n\n.port {\n --tone: var(--jl-muted);\n position: absolute;\n top: 0;\n display: inline-flex;\n align-items: center;\n gap: var(--jl-space-1);\n height: 22px;\n padding: 0 7px;\n border: 1px solid color-mix(in srgb, var(--tone) 35%, var(--jl-line));\n border-radius: var(--jl-radius-pill);\n background: var(--jl-surface);\n color: color-mix(in srgb, var(--tone) 80%, var(--jl-text));\n font-size: var(--jl-font-xs);\n font-weight: 600;\n white-space: nowrap;\n transform: translateX(-50%);\n cursor: crosshair;\n}\n\n.port:has(.add) {\n padding-right: 3px;\n}\n\n/* The exit a run left by. */\n.port[data-taken] {\n border-color: var(--tone);\n background: color-mix(in srgb, var(--tone) 14%, var(--jl-surface));\n}\n\n.dot {\n flex: none;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--tone);\n}\n\n.port-label {\n overflow: hidden;\n min-width: 0;\n text-overflow: ellipsis;\n}\n\n.add,\n.remove-edge {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border-radius: 50%;\n cursor: pointer;\n}\n\n.add {\n width: 16px;\n height: 16px;\n border: none;\n background: color-mix(in srgb, var(--tone) 16%, var(--jl-surface));\n color: var(--tone);\n}\n\n.add:hover {\n background: var(--tone);\n color: var(--jl-surface);\n}\n\n.remove-edge {\n position: absolute;\n top: 0;\n left: 0;\n width: 24px;\n height: 24px;\n border: 1px solid var(--jl-line);\n background: var(--jl-surface);\n color: var(--jl-danger);\n}\n\n.remove-edge:hover {\n border-color: var(--jl-danger);\n background: var(--jl-danger);\n color: var(--jl-on-danger);\n}\n\n.empty {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n padding: var(--jl-space-6);\n color: var(--jl-muted);\n text-align: center;\n pointer-events: none;\n}\n\n.empty[hidden] {\n display: none;\n}\n\n.empty ::slotted(*) {\n pointer-events: auto;\n}\n\n.controls {\n position: absolute;\n bottom: var(--jl-space-3);\n left: var(--jl-space-3);\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 2px;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius);\n background: var(--jl-surface);\n}\n\n/* The zoom buttons: small and quiet, as ghost buttons. */\n.control {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--jl-control-sm);\n height: var(--jl-control-sm);\n padding: 0;\n border: none;\n border-radius: var(--jl-radius-sm);\n background: transparent;\n color: var(--jl-accent);\n cursor: pointer;\n transition: background var(--jl-fast) var(--jl-ease);\n}\n\n.control:hover {\n background: var(--jl-accent-soft);\n}\n\n.zoom-label {\n min-width: 40px;\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n font-variant-numeric: tabular-nums;\n text-align: center;\n}\n\n/* The tones of icons, exits and edges: after .node and .port, whose default tones they replace. */\n[data-tone=\"neutral\"] {\n --tone: var(--jl-muted);\n}\n\n[data-tone=\"accent\"] {\n --tone: var(--jl-accent);\n}\n\n[data-tone=\"ok\"] {\n --tone: var(--jl-ok);\n}\n\n[data-tone=\"warn\"] {\n --tone: var(--jl-warn);\n}\n\n[data-tone=\"danger\"] {\n --tone: var(--jl-danger);\n}\n\n[data-tone=\"info\"] {\n --tone: var(--jl-info);\n}\n\n/* View only: nodes are pressed to be selected, not dragged, and their exits do not connect. */\n:host([readonly]) .node,\n:host([readonly]) .port {\n cursor: pointer;\n}\n\n/* View only: the layout stays as it was given. */\n:host([readonly]) .tidy {\n display: none;\n}\n";
3
3
  export { jl_flow_default as default };
@@ -1,9 +1,10 @@
1
1
  import { JlElement } from '../base/jl-element';
2
- import type { FlowAddRequest, FlowGraph } from '../types/jl-flow.types';
2
+ import type { FlowAddRequest, FlowDropRequest, FlowGraph, FlowPoint } from '../types/jl-flow.types';
3
3
  /**
4
4
  * A flow of nodes joined by edges, to edit: people move the nodes, draw an edge from a node's exit to another node,
5
5
  * press + on a free exit to ask for the next node, and move and zoom the view. Every edit goes out as a new graph
6
- * (`change`); the page adds nodes itself, when `add` asks.
6
+ * (`change`); the page adds nodes itself, when `add` or `drop` asks. Nodes can show how a run went (`status`,
7
+ * `taken`) and what to fix (`problem`).
7
8
  */
8
9
  export declare class JlFlow extends JlElement {
9
10
  static readonly selector = "jl-flow";
@@ -11,6 +12,7 @@ export declare class JlFlow extends JlElement {
11
12
  change: FlowGraph;
12
13
  select: string;
13
14
  add: FlowAddRequest;
15
+ drop: FlowDropRequest;
14
16
  }>>;
15
17
  readonly graph: Readonly<Readonly<{
16
18
  get(): FlowGraph;
@@ -26,6 +28,14 @@ export declare class JlFlow extends JlElement {
26
28
  get(): boolean;
27
29
  set(setter: boolean | ((prevState: boolean) => boolean)): void;
28
30
  }>>;
31
+ /**
32
+ * The drag-and-drop type the canvas takes (`application/x-step`): something dragged in carrying it asks for a node
33
+ * where it is dropped, with `drop`.
34
+ */
35
+ readonly dropType: Readonly<Readonly<{
36
+ get(): string;
37
+ set(setter: string | ((prevState: string) => string)): void;
38
+ }>>;
29
39
  /** The canvas's name for screen readers. */
30
40
  readonly label: Readonly<Readonly<{
31
41
  get(): string;
@@ -37,6 +47,12 @@ export declare class JlFlow extends JlElement {
37
47
  private readonly _refs;
38
48
  /** Moves and zooms the view to show every node, never larger than life. */
39
49
  fit(): void;
50
+ /** Moves the view, when needed, so that a node is in sight: one just added, or moved with the keys. */
51
+ reveal(id: string): void;
52
+ /** Where a node added after an exit goes, as + puts it: a row below, under that exit. */
53
+ positionAfter(from: string, port: string): FlowPoint;
54
+ /** Where a node added without a place goes: in the middle of the view (its top left corner). */
55
+ centerPosition(): FlowPoint;
40
56
  /** Lays the nodes out again, in rows from the starts down (`change` gives the new positions), and shows them all. */
41
57
  layout(): void;
42
58
  templateHtml(): Node;
@@ -58,11 +74,13 @@ export declare class JlFlow extends JlElement {
58
74
  /** An edge dropped on a node joins it; dropped on empty space, it asks for a node there. */
59
75
  private connectAt;
60
76
  private pressAdd;
77
+ /** Whether a drag carries what the canvas takes: then it may be dropped here. */
78
+ private carriesDrop;
79
+ private dragOver;
80
+ private dropOn;
61
81
  private key;
62
82
  /** The arrows move the selected node; with Shift, further. */
63
83
  private nudge;
64
- /** Moves the view, when needed, so that a node is in sight: one just added, or moved with the keys. */
65
- private reveal;
66
84
  /** Two fingers or the wheel move the view; with Ctrl or ⌘ (a pinch on a trackpad), they zoom. */
67
85
  private wheel;
68
86
  private zoomBy;
@@ -75,11 +93,16 @@ export declare class JlFlow extends JlElement {
75
93
  private renderNodes;
76
94
  private createNode;
77
95
  private drawNode;
96
+ /** A colour of the page's own for the icon wins over the tone's. */
97
+ private paintIcon;
78
98
  private portElement;
79
99
  private place;
80
100
  private renderSelection;
81
101
  private renderEdges;
82
- /** An edge, with a wide invisible twin that catches the pointer: a thin line is still easy to press. */
102
+ /**
103
+ * An edge, with a wide invisible twin that catches the pointer: a thin line is still easy to press. One a run took
104
+ * stands out.
105
+ */
83
106
  private edgeGroup;
84
107
  /** The button that removes the selected edge, in its middle. */
85
108
  private renderEdgeTool;