@syware/jlib 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -107,6 +107,7 @@ git push --follow-tags
107
107
 
108
108
  The tag stages the version on npm and creates the GitHub release; a maintainer then approves it with
109
109
  `npm stage approve`. Every step, pre-releases and what to do when something fails: [RELEASING.md](RELEASING.md).
110
+ What changed in each version: [CHANGELOG.md](CHANGELOG.md).
110
111
 
111
112
  ## Made by SyWare
112
113
 
package/dist/all.js CHANGED
@@ -1,4 +1,4 @@
1
- import { TOKEN_STYLES } from "./styles/shared.js";
1
+ import { BASE_STYLES, CONTROL_STYLES, FLOATING_STYLES, TOKEN_STYLES } from "./styles/shared.js";
2
2
  import { adoptTokens } from "./lib/tokens.js";
3
3
  import { JlElement } from "./base/jl-element.js";
4
4
  import { JlFormControl } from "./base/jl-form-control.js";
@@ -73,4 +73,4 @@ import { JlSplitView, SPLIT_ORIENTATIONS } from "./templates/jl-split-view.js";
73
73
  // src/all.ts
74
74
  registerIcons(COMMON_ICONS);
75
75
  define(JlAccordion, JlAccordionItem, JlAlert, JlAppShell, JlAvatar, JlAvatarGroup, JlBadge, JlBreadcrumb, JlButton, JlCalendar, JlCard, JlCheckbox, JlChip, JlCodeBlock, JlCombobox, JlCommandPalette, JlConfirm, JlCopyButton, JlDatePicker, JlDialog, JlDivider, JlDrawer, JlEmpty, JlField, JlFileDrop, JlFlow, JlIcon, JlInput, JlKbd, JlLink, JlListItem, JlMenu, JlNavbar, JlNumberInput, JlPageHeader, JlPagination, JlPopover, JlProgress, JlProgressRing, JlRadioGroup, JlRating, JlSearch, JlSegmented, JlSelect, JlSideNav, JlSkeleton, JlSlider, JlSpinner, JlSplitView, JlStat, JlSteps, JlSwitch, JlTable, JlTabPanel, JlTabs, JlTagInput, JlTextarea, JlTimeline, JlToast, JlToaster, JlTooltip, JlTree);
76
- export { ACCORDION_VARIANTS, AVATAR_SHAPES, AVATAR_SIZES, AVATAR_STATUSES, BUTTON_TYPES, BUTTON_VARIANTS, CARD_PADDINGS, CHIP_SIZES, CODE_LANGUAGES, COMMON_ICONS, CONFIRM_TONES, CONTROL_SIZES, DIALOG_SIZES, DIVIDER_ALIGNS, DRAWER_PLACEMENTS, DRAWER_SIZES, EMPTY_SIZES, FIELD_ORIENTATIONS, INPUT_TYPES, JlAccordion, JlAccordionItem, JlAlert, JlAppShell, JlAvatar, JlAvatarGroup, JlBadge, JlBreadcrumb, JlButton, JlCalendar, JlCard, JlCheckbox, JlChip, JlCodeBlock, JlCombobox, JlCommandPalette, JlConfirm, JlCopyButton, JlDatePicker, JlDialog, JlDivider, JlDrawer, JlElement, JlEmpty, JlField, JlFileDrop, JlFlow, JlFormControl, JlIcon, JlInput, JlKbd, JlLink, JlListItem, JlMenu, JlNavbar, JlNumberInput, JlPageHeader, JlPagination, JlPopover, JlProgress, JlProgressRing, JlRadioGroup, JlRating, JlSearch, JlSegmented, JlSelect, JlSideNav, JlSkeleton, JlSlider, JlSpinner, JlSplitView, JlStat, JlSteps, JlSwitch, JlTabPanel, JlTable, JlTabs, JlTagInput, JlTextarea, JlTimeline, JlToast, JlToaster, JlTooltip, JlTree, LINK_VARIANTS, PLACEMENTS, PROGRESS_SIZES, PROGRESS_TONES, RADIO_GROUP_ORIENTATIONS, RADIO_GROUP_VARIANTS, SKELETON_SHAPES, SPLIT_ORIENTATIONS, STAT_TRENDS, STEPS_ORIENTATIONS, SWITCH_SIZES, TABLE_SORT_DIRECTIONS, TAB_VARIANTS, TOASTER_PLACEMENTS, TOAST_TONES, TOKEN_STYLES, TONES, TONE_ICONS, TONE_ICON_FILLED, adoptTokens, clampedNumber, computePosition, confirmDialog, define, followAnchor, hasIcon, iconElement, iconNames, jsonOf, oneOf, placeFloating, registerIcons, toast };
76
+ export { ACCORDION_VARIANTS, AVATAR_SHAPES, AVATAR_SIZES, AVATAR_STATUSES, BASE_STYLES, BUTTON_TYPES, BUTTON_VARIANTS, CARD_PADDINGS, CHIP_SIZES, CODE_LANGUAGES, COMMON_ICONS, CONFIRM_TONES, CONTROL_SIZES, CONTROL_STYLES, DIALOG_SIZES, DIVIDER_ALIGNS, DRAWER_PLACEMENTS, DRAWER_SIZES, EMPTY_SIZES, FIELD_ORIENTATIONS, FLOATING_STYLES, INPUT_TYPES, JlAccordion, JlAccordionItem, JlAlert, JlAppShell, JlAvatar, JlAvatarGroup, JlBadge, JlBreadcrumb, JlButton, JlCalendar, JlCard, JlCheckbox, JlChip, JlCodeBlock, JlCombobox, JlCommandPalette, JlConfirm, JlCopyButton, JlDatePicker, JlDialog, JlDivider, JlDrawer, JlElement, JlEmpty, JlField, JlFileDrop, JlFlow, JlFormControl, JlIcon, JlInput, JlKbd, JlLink, JlListItem, JlMenu, JlNavbar, JlNumberInput, JlPageHeader, JlPagination, JlPopover, JlProgress, JlProgressRing, JlRadioGroup, JlRating, JlSearch, JlSegmented, JlSelect, JlSideNav, JlSkeleton, JlSlider, JlSpinner, JlSplitView, JlStat, JlSteps, JlSwitch, JlTabPanel, JlTable, JlTabs, JlTagInput, JlTextarea, JlTimeline, JlToast, JlToaster, JlTooltip, JlTree, LINK_VARIANTS, PLACEMENTS, PROGRESS_SIZES, PROGRESS_TONES, RADIO_GROUP_ORIENTATIONS, RADIO_GROUP_VARIANTS, SKELETON_SHAPES, SPLIT_ORIENTATIONS, STAT_TRENDS, STEPS_ORIENTATIONS, SWITCH_SIZES, TABLE_SORT_DIRECTIONS, TAB_VARIANTS, TOASTER_PLACEMENTS, TOAST_TONES, TOKEN_STYLES, TONES, TONE_ICONS, TONE_ICON_FILLED, adoptTokens, clampedNumber, computePosition, confirmDialog, define, followAnchor, hasIcon, iconElement, iconNames, jsonOf, oneOf, placeFloating, registerIcons, toast };
@@ -11,6 +11,7 @@ export declare class JlInput extends JlFormControl {
11
11
  readonly events: Readonly<import("@jadis/core").UseEventsHandler<{
12
12
  input: string;
13
13
  change: string;
14
+ enter: string;
14
15
  }>>;
15
16
  readonly value: Readonly<Readonly<{
16
17
  get(): string;
@@ -33,6 +33,7 @@ var PASSWORD = "password";
33
33
  var REVEALED = "text";
34
34
  var ICON_SIZE = 16;
35
35
  var ACTION_ICON_SIZE = 14;
36
+ var ENTER = "Enter";
36
37
  /**
37
38
  * A single-line text field that takes part in forms. `value` is the text; `input` fires as people type and
38
39
  * `change` when they commit. Slots `start` and `end` hold adornments such as a unit or a prefix.
@@ -137,6 +138,7 @@ var JlInput = class extends JlFormControl {
137
138
  this.events.emit("input", native.value);
138
139
  });
139
140
  this.on(native, "change", () => this.events.emit("change", native.value));
141
+ this.on(native, "keydown", (event) => event.key === ENTER && this.events.emit("enter", native.value));
140
142
  this.on(this._refs.clear, "click", () => this.clearValue());
141
143
  this.on(this._refs.reveal, "click", () => this.toggleReveal());
142
144
  }
@@ -15,6 +15,7 @@ export declare class JlSelect extends JlFormControl {
15
15
  get(): SelectOption[];
16
16
  set(setter: SelectOption[] | ((prevState: SelectOption[]) => SelectOption[])): void;
17
17
  }>>;
18
+ /** The chosen value. One that no option has is kept, as an option of its own, rather than replaced. */
18
19
  readonly value: Readonly<Readonly<{
19
20
  get(): string;
20
21
  set(setter: string | ((prevState: string) => string)): void;
@@ -60,6 +61,11 @@ export declare class JlSelect extends JlFormControl {
60
61
  private optionNodes;
61
62
  private placeholderNodes;
62
63
  private renderOptions;
64
+ /**
65
+ * A value no option has (a choice since removed, options still loading) shows as an option of its own, first, so
66
+ * the field shows what is saved and the form keeps it, rather than another choice.
67
+ */
68
+ private keepUnknownValue;
63
69
  /** Shows the chosen option; the form gets what is shown. */
64
70
  private renderValue;
65
71
  /**
@@ -40,7 +40,8 @@ var JlSelect = class extends JlFormControl {
40
40
  attribute: "options",
41
41
  parse: jsonOf([])
42
42
  });
43
- value = this.useChange(NO_VALUE, () => this.renderValue(), { attribute: "value" });
43
+ /** The chosen value. One that no option has is kept, as an option of its own, rather than replaced. */
44
+ value = this.useChange(NO_VALUE, () => this.renderOptions(), { attribute: "value" });
44
45
  /** Shown while nothing is chosen; it cannot be picked back from the list. */
45
46
  placeholder = this.useChange("", () => this.renderOptions(), { attribute: "placeholder" });
46
47
  size = this.useChange("md", (value) => this.reflect("size", value), {
@@ -120,8 +121,24 @@ var JlSelect = class extends JlFormControl {
120
121
  }
121
122
  renderOptions() {
122
123
  this._refs.native.replaceChildren(...this.placeholderNodes(), ...this.optionNodes());
124
+ this.keepUnknownValue();
123
125
  this.renderValue();
124
126
  }
127
+ /**
128
+ * A value no option has (a choice since removed, options still loading) shows as an option of its own, first, so
129
+ * the field shows what is saved and the form keeps it, rather than another choice.
130
+ */
131
+ keepUnknownValue() {
132
+ const native = this._refs.native;
133
+ const value = this.value.get();
134
+ const known = [...native.options].some((option) => option.value === value);
135
+ if (value === NO_VALUE || known) return;
136
+ const afterPlaceholder = this.placeholder.get() === "" ? 0 : 1;
137
+ native.add(/* @__PURE__ */ jsx("option", {
138
+ value,
139
+ children: value
140
+ }), afterPlaceholder);
141
+ }
125
142
  /** Shows the chosen option; the form gets what is shown. */
126
143
  renderValue() {
127
144
  const native = this._refs.native;
@@ -24,6 +24,14 @@ export declare class JlSwitch extends JlFormControl {
24
24
  get(): string;
25
25
  set(setter: string | ((prevState: string) => string)): void;
26
26
  }>>;
27
+ /**
28
+ * Keeps the label for screen readers only, for a switch whose meaning the page already shows beside it (an "On"
29
+ * or "Off" next to the track).
30
+ */
31
+ readonly hideLabel: Readonly<Readonly<{
32
+ get(): boolean;
33
+ set(setter: boolean | ((prevState: boolean) => boolean)): void;
34
+ }>>;
27
35
  readonly size: Readonly<Readonly<{
28
36
  get(): SwitchSize;
29
37
  set(setter: ((prevState: SwitchSize) => SwitchSize) | SwitchSize): void;
@@ -52,6 +60,9 @@ export declare class JlSwitch extends JlFormControl {
52
60
  private commit;
53
61
  private renderChecked;
54
62
  private renderFormValue;
55
- /** Hides the text column when there is nothing to show, so a lone switch has no gap beside it. */
63
+ /**
64
+ * Hides the text column when there is nothing to show, so a lone switch has no gap beside it; a hidden label stays
65
+ * for screen readers, out of the layout.
66
+ */
56
67
  private renderText;
57
68
  }
@@ -7,6 +7,7 @@ import { jsx, jsxs } from "@jadis/core/jsx-runtime";
7
7
  // src/atoms/jl-switch.tsx
8
8
  var SWITCH_SIZES = ["md", "sm"];
9
9
  var DEFAULT_VALUE = "on";
10
+ var SCREEN_READER_ONLY = "sr-only";
10
11
  /**
11
12
  * An on/off setting that applies at once, such as "Dark mode" or "Email me". `checked` is the state; the form gets
12
13
  * `value` while it is on. For a choice confirmed later with a button, prefer a checkbox.
@@ -27,6 +28,11 @@ var JlSwitch = class extends JlFormControl {
27
28
  this._refs.description.hidden = value === "";
28
29
  this.renderText();
29
30
  }, { attribute: "description" });
31
+ /**
32
+ * Keeps the label for screen readers only, for a switch whose meaning the page already shows beside it (an "On"
33
+ * or "Off" next to the track).
34
+ */
35
+ hideLabel = this.useChange(false, () => this.renderText(), { attribute: "hide-label" });
30
36
  size = this.useChange("md", (value) => this.reflect("size", value), {
31
37
  attribute: "size",
32
38
  parse: oneOf(SWITCH_SIZES, "md")
@@ -115,10 +121,14 @@ var JlSwitch = class extends JlFormControl {
115
121
  renderFormValue() {
116
122
  this.internals.setFormValue(this.checked.get() ? this.value.get() : null);
117
123
  }
118
- /** Hides the text column when there is nothing to show, so a lone switch has no gap beside it. */
124
+ /**
125
+ * Hides the text column when there is nothing to show, so a lone switch has no gap beside it; a hidden label stays
126
+ * for screen readers, out of the layout.
127
+ */
119
128
  renderText() {
120
129
  const slotted = this._refs.slot.assignedNodes().some((node) => node instanceof Element || node.textContent?.trim() !== "");
121
130
  this._refs.text.hidden = this.label.get() === "" && this.description.get() === "" && !slotted;
131
+ this._refs.text.classList.toggle(SCREEN_READER_ONLY, this.hideLabel.get());
122
132
  }
123
133
  };
124
134
  JlSwitch.register();
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { COMMON_ICONS } from './lib/icon-set';
7
7
  export { hasIcon, iconElement, iconNames, registerIcons } from './lib/icons';
8
8
  export { CONTROL_SIZES, TONE_ICON_FILLED, TONE_ICONS, TONES } from './lib/options';
9
9
  export { adoptTokens } from './lib/tokens';
10
- export { TOKEN_STYLES } from './styles/shared';
10
+ export { BASE_STYLES, CONTROL_STYLES, FLOATING_STYLES, TOKEN_STYLES } from './styles/shared';
11
11
  export type * from './types/calendar.types';
12
12
  export type * from './types/files.types';
13
13
  export type * from './types/floating.types';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { TOKEN_STYLES } from "./styles/shared.js";
1
+ import { BASE_STYLES, CONTROL_STYLES, FLOATING_STYLES, TOKEN_STYLES } from "./styles/shared.js";
2
2
  import { adoptTokens } from "./lib/tokens.js";
3
3
  import { JlElement } from "./base/jl-element.js";
4
4
  import { JlFormControl } from "./base/jl-form-control.js";
@@ -70,4 +70,4 @@ import { JlToaster, TOASTER_PLACEMENTS, toast } from "./organisms/jl-toaster.js"
70
70
  import { JlTree } from "./organisms/jl-tree.js";
71
71
  import { JlAppShell } from "./templates/jl-app-shell.js";
72
72
  import { JlSplitView, SPLIT_ORIENTATIONS } from "./templates/jl-split-view.js";
73
- export { ACCORDION_VARIANTS, AVATAR_SHAPES, AVATAR_SIZES, AVATAR_STATUSES, BUTTON_TYPES, BUTTON_VARIANTS, CARD_PADDINGS, CHIP_SIZES, CODE_LANGUAGES, COMMON_ICONS, CONFIRM_TONES, CONTROL_SIZES, DIALOG_SIZES, DIVIDER_ALIGNS, DRAWER_PLACEMENTS, DRAWER_SIZES, EMPTY_SIZES, FIELD_ORIENTATIONS, INPUT_TYPES, JlAccordion, JlAccordionItem, JlAlert, JlAppShell, JlAvatar, JlAvatarGroup, JlBadge, JlBreadcrumb, JlButton, JlCalendar, JlCard, JlCheckbox, JlChip, JlCodeBlock, JlCombobox, JlCommandPalette, JlConfirm, JlCopyButton, JlDatePicker, JlDialog, JlDivider, JlDrawer, JlElement, JlEmpty, JlField, JlFileDrop, JlFlow, JlFormControl, JlIcon, JlInput, JlKbd, JlLink, JlListItem, JlMenu, JlNavbar, JlNumberInput, JlPageHeader, JlPagination, JlPopover, JlProgress, JlProgressRing, JlRadioGroup, JlRating, JlSearch, JlSegmented, JlSelect, JlSideNav, JlSkeleton, JlSlider, JlSpinner, JlSplitView, JlStat, JlSteps, JlSwitch, JlTabPanel, JlTable, JlTabs, JlTagInput, JlTextarea, JlTimeline, JlToast, JlToaster, JlTooltip, JlTree, LINK_VARIANTS, PLACEMENTS, PROGRESS_SIZES, PROGRESS_TONES, RADIO_GROUP_ORIENTATIONS, RADIO_GROUP_VARIANTS, SKELETON_SHAPES, SPLIT_ORIENTATIONS, STAT_TRENDS, STEPS_ORIENTATIONS, SWITCH_SIZES, TABLE_SORT_DIRECTIONS, TAB_VARIANTS, TOASTER_PLACEMENTS, TOAST_TONES, TOKEN_STYLES, TONES, TONE_ICONS, TONE_ICON_FILLED, adoptTokens, clampedNumber, computePosition, confirmDialog, define, followAnchor, hasIcon, iconElement, iconNames, jsonOf, oneOf, placeFloating, registerIcons, toast };
73
+ export { ACCORDION_VARIANTS, AVATAR_SHAPES, AVATAR_SIZES, AVATAR_STATUSES, BASE_STYLES, BUTTON_TYPES, BUTTON_VARIANTS, CARD_PADDINGS, CHIP_SIZES, CODE_LANGUAGES, COMMON_ICONS, CONFIRM_TONES, CONTROL_SIZES, CONTROL_STYLES, DIALOG_SIZES, DIVIDER_ALIGNS, DRAWER_PLACEMENTS, DRAWER_SIZES, EMPTY_SIZES, FIELD_ORIENTATIONS, FLOATING_STYLES, INPUT_TYPES, JlAccordion, JlAccordionItem, JlAlert, JlAppShell, JlAvatar, JlAvatarGroup, JlBadge, JlBreadcrumb, JlButton, JlCalendar, JlCard, JlCheckbox, JlChip, JlCodeBlock, JlCombobox, JlCommandPalette, JlConfirm, JlCopyButton, JlDatePicker, JlDialog, JlDivider, JlDrawer, JlElement, JlEmpty, JlField, JlFileDrop, JlFlow, JlFormControl, JlIcon, JlInput, JlKbd, JlLink, JlListItem, JlMenu, JlNavbar, JlNumberInput, JlPageHeader, JlPagination, JlPopover, JlProgress, JlProgressRing, JlRadioGroup, JlRating, JlSearch, JlSegmented, JlSelect, JlSideNav, JlSkeleton, JlSlider, JlSpinner, JlSplitView, JlStat, JlSteps, JlSwitch, JlTabPanel, JlTable, JlTabs, JlTagInput, JlTextarea, JlTimeline, JlToast, JlToaster, JlTooltip, JlTree, LINK_VARIANTS, PLACEMENTS, PROGRESS_SIZES, PROGRESS_TONES, RADIO_GROUP_ORIENTATIONS, RADIO_GROUP_VARIANTS, SKELETON_SHAPES, SPLIT_ORIENTATIONS, STAT_TRENDS, STEPS_ORIENTATIONS, SWITCH_SIZES, TABLE_SORT_DIRECTIONS, TAB_VARIANTS, TOASTER_PLACEMENTS, TOAST_TONES, TOKEN_STYLES, TONES, TONE_ICONS, TONE_ICON_FILLED, adoptTokens, clampedNumber, computePosition, confirmDialog, define, followAnchor, hasIcon, iconElement, iconNames, jsonOf, oneOf, placeFloating, registerIcons, toast };
@@ -1,7 +1,11 @@
1
1
  import type { FlowEdge, FlowEdgeEnds, FlowGraph, FlowNode, FlowPoint, FlowPort, FlowViewport } from '../types/jl-flow.types';
2
- /** A node's box: fixed, so that every anchor and edge is computed without measuring the page. */
2
+ /**
3
+ * A node's box: fixed, so that every anchor and edge is computed without measuring the page. A node with a detail
4
+ * line is taller.
5
+ */
3
6
  export declare const FLOW_NODE: {
4
7
  readonly height: 64;
8
+ readonly heightWithDetail: 96;
5
9
  readonly width: 240;
6
10
  };
7
11
  export declare const FLOW_ZOOM: {
@@ -12,6 +16,7 @@ export declare const FLOW_ZOOM: {
12
16
  /** The exit of a node that names none. */
13
17
  export declare const DEFAULT_PORT: FlowPort;
14
18
  export declare const portsOf: (node: FlowNode) => FlowPort[];
19
+ export declare const nodeHeight: (node: FlowNode) => number;
15
20
  /** The exit an edge leaves by: its own, or its node's first. */
16
21
  export declare const portOfEdge: (node: FlowNode, edge: FlowEdge) => string;
17
22
  export declare const snap: (point: FlowPoint) => FlowPoint;
@@ -31,6 +36,10 @@ export declare const edgePath: (from: FlowPoint, to: FlowPoint) => string;
31
36
  export declare const edgeMidpoint: (from: FlowPoint, to: FlowPoint) => FlowPoint;
32
37
  /** Where an edge starts and ends, or nothing when one of its ends is gone. */
33
38
  export declare function edgeEnds(graph: FlowGraph, edge: FlowEdge): FlowEdgeEnds | undefined;
39
+ /** A run went this way: its node left by the edge's exit, and the node it leads to ran too. */
40
+ export declare function isEdgeTaken(graph: FlowGraph, edge: FlowEdge): boolean;
41
+ /** The position of a node centred on a point: where something dropped there lands. */
42
+ export declare const centeredAt: (point: FlowPoint) => FlowPoint;
34
43
  /** The node under a point, other than `except`. */
35
44
  export declare const nodeAt: (graph: FlowGraph, point: FlowPoint, except?: string) => FlowNode | undefined;
36
45
  export declare const isPortFree: (graph: FlowGraph, node: FlowNode, portId: string) => boolean;
package/dist/lib/flow.js CHANGED
@@ -1,7 +1,11 @@
1
1
  // src/lib/flow.ts
2
- /** A node's box: fixed, so that every anchor and edge is computed without measuring the page. */
2
+ /**
3
+ * A node's box: fixed, so that every anchor and edge is computed without measuring the page. A node with a detail
4
+ * line is taller.
5
+ */
3
6
  var FLOW_NODE = {
4
7
  height: 64,
8
+ heightWithDetail: 96,
5
9
  width: 240
6
10
  };
7
11
  var FLOW_ZOOM = {
@@ -20,6 +24,7 @@ var REVEAL_MARGIN = 48;
20
24
  var ROW_GAP = 72;
21
25
  var COLUMN_GAP = 32;
22
26
  var portsOf = (node) => node.ports ?? [DEFAULT_PORT];
27
+ var nodeHeight = (node) => node.detail ? FLOW_NODE.heightWithDetail : FLOW_NODE.height;
23
28
  /** The exit an edge leaves by: its own, or its node's first. */
24
29
  var portOfEdge = (node, edge) => edge.port ?? portsOf(node)[0]?.id ?? "";
25
30
  var nodeById = (graph, id) => graph.nodes.find((node) => node.id === id);
@@ -36,7 +41,7 @@ function outputAnchor(node, portId) {
36
41
  if (index < 0) return;
37
42
  return {
38
43
  x: node.position.x + portX(index, ports.length),
39
- y: node.position.y + FLOW_NODE.height + PORT_DROP
44
+ y: node.position.y + nodeHeight(node) + PORT_DROP
40
45
  };
41
46
  }
42
47
  var inputAnchor = (node) => ({
@@ -152,7 +157,18 @@ function edgeEnds(graph, edge) {
152
157
  tone
153
158
  };
154
159
  }
155
- var isInside = (point, node) => point.x >= node.position.x && point.x <= node.position.x + FLOW_NODE.width && point.y >= node.position.y && point.y <= node.position.y + FLOW_NODE.height;
160
+ var isInside = (point, node) => point.x >= node.position.x && point.x <= node.position.x + FLOW_NODE.width && point.y >= node.position.y && point.y <= node.position.y + nodeHeight(node);
161
+ /** A run went this way: its node left by the edge's exit, and the node it leads to ran too. */
162
+ function isEdgeTaken(graph, edge) {
163
+ const source = nodeById(graph, edge.from);
164
+ const target = nodeById(graph, edge.to);
165
+ return source?.taken !== void 0 && source.taken === portOfEdge(source, edge) && target?.status !== void 0;
166
+ }
167
+ /** The position of a node centred on a point: where something dropped there lands. */
168
+ var centeredAt = (point) => snap({
169
+ x: point.x - FLOW_NODE.width / 2,
170
+ y: point.y - FLOW_NODE.height / 2
171
+ });
156
172
  /** The node under a point, other than `except`. */
157
173
  var nodeAt = (graph, point, except = "") => graph.nodes.find((node) => node.id !== except && isInside(point, node));
158
174
  var isPortFree = (graph, node, portId) => !graph.edges.some((edge) => edge.from === node.id && portOfEdge(node, edge) === portId);
@@ -188,8 +204,8 @@ var removeEdge = (graph, id) => ({
188
204
  ...graph,
189
205
  edges: graph.edges.filter((edge) => edge.id !== id)
190
206
  });
191
- var ROW = FLOW_NODE.height + ROW_GAP;
192
207
  var COLUMN = FLOW_NODE.width + COLUMN_GAP;
208
+ var rowOfFlow = (graph) => Math.max(FLOW_NODE.height, ...graph.nodes.map(nodeHeight)) + ROW_GAP;
193
209
  /** How far across a node hangs from its parent's exit: the exits share the row out, side by side, around it. */
194
210
  function portShift(node, portId) {
195
211
  const ports = portsOf(node);
@@ -204,7 +220,7 @@ function positionAfter(graph, from, portId) {
204
220
  };
205
221
  return snap({
206
222
  x: source.position.x + portShift(source, portId),
207
- y: source.position.y + ROW
223
+ y: source.position.y + nodeHeight(source) + ROW_GAP
208
224
  });
209
225
  }
210
226
  /** The edges that close a loop, found walking from the starts: the layout leaves them out, or a loop has no top. */
@@ -267,6 +283,7 @@ function layoutFlow(graph) {
267
283
  const loops = loopEdges(graph);
268
284
  const edges = graph.edges.filter((edge) => !loops.has(edge.id) && ids.has(edge.from) && ids.has(edge.to));
269
285
  const rows = rowsOf(graph, edges);
286
+ const row = rowOfFlow(graph);
270
287
  const placed = /* @__PURE__ */ new Map();
271
288
  for (const row of [...new Set(rows.values())].sort((a, b) => a - b)) placeRow(graph.nodes.filter((node) => rows.get(node.id) === row).map((node) => ({
272
289
  id: node.id,
@@ -278,7 +295,7 @@ function layoutFlow(graph) {
278
295
  ...node,
279
296
  position: snap({
280
297
  x: placed.get(node.id) ?? 0,
281
- y: (rows.get(node.id) ?? 0) * ROW
298
+ y: (rows.get(node.id) ?? 0) * row
282
299
  })
283
300
  }))
284
301
  };
@@ -309,7 +326,7 @@ function fitView(nodes, width, height) {
309
326
  const left = Math.min(...nodes.map((node) => node.position.x));
310
327
  const top = Math.min(...nodes.map((node) => node.position.y));
311
328
  const right = Math.max(...nodes.map((node) => node.position.x + FLOW_NODE.width));
312
- const bottom = Math.max(...nodes.map((node) => node.position.y + FLOW_NODE.height + PORT_DROP));
329
+ const bottom = Math.max(...nodes.map((node) => node.position.y + nodeHeight(node) + PORT_DROP));
313
330
  const zoom = clampZoom(Math.min(1, (width - 96) / (right - left), (height - 96) / (bottom - top)));
314
331
  return {
315
332
  x: width / 2 - (left + right) / 2 * zoom,
@@ -329,8 +346,8 @@ function revealIn(viewport, node, width, height) {
329
346
  const top = viewport.y + node.position.y * zoom;
330
347
  return {
331
348
  x: viewport.x + shiftInto(left, left + FLOW_NODE.width * zoom, width),
332
- y: viewport.y + shiftInto(top, top + (FLOW_NODE.height + PORT_DROP) * zoom, height),
349
+ y: viewport.y + shiftInto(top, top + (nodeHeight(node) + PORT_DROP) * zoom, height),
333
350
  zoom
334
351
  };
335
352
  }
336
- export { DEFAULT_PORT, FLOW_NODE, FLOW_ZOOM, connect, edgeEnds, edgeMidpoint, edgePath, edgeRoute, fitView, inputAnchor, isPortFree, layoutFlow, moveNode, nodeAt, outputAnchor, portOfEdge, portX, portsOf, positionAfter, removeEdge, removeNode, revealIn, roundedPath, snap, toWorld, zoomAt };
353
+ export { DEFAULT_PORT, FLOW_NODE, FLOW_ZOOM, centeredAt, connect, edgeEnds, edgeMidpoint, edgePath, edgeRoute, fitView, inputAnchor, isEdgeTaken, isPortFree, layoutFlow, moveNode, nodeAt, nodeHeight, outputAnchor, portOfEdge, portX, portsOf, positionAfter, removeEdge, removeNode, revealIn, roundedPath, snap, toWorld, zoomAt };
@@ -1,3 +1,3 @@
1
1
  // src/molecules/jl-accordion-item.css?raw
2
- var jl_accordion_item_default = ":host {\n display: block;\n border-bottom: 1px solid var(--jl-line);\n /* Lets the content's height animate from 0 to auto. */\n interpolate-size: allow-keywords;\n --pad-x: 0px;\n}\n\nsummary {\n display: flex;\n align-items: center;\n gap: 10px;\n min-height: 48px;\n padding: 12px var(--pad-x);\n border-radius: var(--jl-radius-xs);\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n font-weight: 600;\n line-height: 1.4;\n list-style: none;\n cursor: pointer;\n user-select: none;\n}\n\nsummary::-webkit-details-marker {\n display: none;\n}\n\n.heading {\n flex: 1;\n min-width: 0;\n}\n\n.icon {\n color: var(--jl-muted);\n}\n\n.chevron {\n color: var(--jl-faint);\n transition: rotate var(--jl-slow) var(--jl-ease);\n}\n\nsummary:hover .chevron {\n color: var(--jl-text);\n}\n\ndetails[open] .chevron {\n rotate: 180deg;\n}\n\nsummary:focus-visible {\n box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--jl-brand) 60%, transparent);\n}\n\n/*\n * The content box is a pseudo-element BASE_STYLES' \"* { box-sizing: inherit }\" does not reach: without this, what\n * it holds (and the slotted fields, which inherit through it) would fall back to content-box.\n */\ndetails::details-content {\n box-sizing: inherit;\n block-size: 0;\n overflow: clip;\n transition:\n block-size var(--jl-slow) var(--jl-ease),\n content-visibility var(--jl-slow) allow-discrete;\n}\n\ndetails[open]::details-content {\n block-size: auto;\n}\n\n.content {\n padding: 0 var(--pad-x) 16px;\n color: var(--jl-text-2);\n font-size: var(--jl-font-md);\n line-height: 1.6;\n}\n\n:host([disabled]) summary {\n color: var(--jl-faint);\n cursor: not-allowed;\n}\n\n:host([disabled]) summary:hover .chevron {\n color: var(--jl-faint);\n}\n";
2
+ var jl_accordion_item_default = ":host {\n display: block;\n border-bottom: 1px solid var(--jl-line);\n /* Lets the content's height animate from 0 to auto. */\n interpolate-size: allow-keywords;\n --pad-x: 0px;\n}\n\nsummary {\n display: flex;\n align-items: center;\n gap: 10px;\n min-height: 48px;\n padding: 12px var(--pad-x);\n border-radius: var(--jl-radius-xs);\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n font-weight: 600;\n line-height: 1.4;\n list-style: none;\n cursor: pointer;\n user-select: none;\n}\n\nsummary::-webkit-details-marker {\n display: none;\n}\n\n.titles {\n display: grid;\n flex: 1;\n min-width: 0;\n}\n\n.description {\n overflow: hidden;\n color: var(--jl-muted);\n font-size: var(--jl-font-sm);\n font-weight: 400;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.icon {\n color: var(--jl-muted);\n}\n\n.chevron {\n color: var(--jl-faint);\n transition: rotate var(--jl-slow) var(--jl-ease);\n}\n\nsummary:hover .chevron {\n color: var(--jl-text);\n}\n\ndetails[open] .chevron {\n rotate: 180deg;\n}\n\nsummary:focus-visible {\n box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--jl-brand) 60%, transparent);\n}\n\n/*\n * The content box is a pseudo-element BASE_STYLES' \"* { box-sizing: inherit }\" does not reach: without this, what\n * it holds (and the slotted fields, which inherit through it) would fall back to content-box.\n */\ndetails::details-content {\n box-sizing: inherit;\n block-size: 0;\n overflow: clip;\n transition:\n block-size var(--jl-slow) var(--jl-ease),\n content-visibility var(--jl-slow) allow-discrete;\n}\n\ndetails[open]::details-content {\n block-size: auto;\n}\n\n.content {\n padding: 0 var(--pad-x) 16px;\n color: var(--jl-text-2);\n font-size: var(--jl-font-md);\n line-height: 1.6;\n}\n\n:host([disabled]) summary {\n color: var(--jl-faint);\n cursor: not-allowed;\n}\n\n:host([disabled]) summary:hover .chevron {\n color: var(--jl-faint);\n}\n";
3
3
  export { jl_accordion_item_default as default };
@@ -12,11 +12,16 @@ export declare class JlAccordionItem extends JlElement {
12
12
  get(): string;
13
13
  set(setter: string | ((prevState: string) => string)): void;
14
14
  }>>;
15
+ /** A quieter line under the heading, shown open or closed: what the section holds ("3 fields", "2 pages"). */
16
+ readonly description: Readonly<Readonly<{
17
+ get(): string;
18
+ set(setter: string | ((prevState: string) => string)): void;
19
+ }>>;
15
20
  readonly open: Readonly<Readonly<{
16
21
  get(): boolean;
17
22
  set(setter: boolean | ((prevState: boolean) => boolean)): void;
18
23
  }>>;
19
- /** An icon before the heading. */
24
+ /** An icon before the heading; the `icon` slot takes a richer one instead. */
20
25
  readonly icon: Readonly<Readonly<{
21
26
  get(): string;
22
27
  set(setter: string | ((prevState: string) => string)): void;
@@ -19,8 +19,13 @@ var JlAccordionItem = class extends JlElement {
19
19
  heading = this.useChange("", (value) => {
20
20
  this._refs.heading.textContent = value;
21
21
  }, { attribute: "heading" });
22
+ /** A quieter line under the heading, shown open or closed: what the section holds ("3 fields", "2 pages"). */
23
+ description = this.useChange("", (value) => {
24
+ this._refs.description.textContent = value;
25
+ this._refs.description.hidden = value === "";
26
+ }, { attribute: "description" });
22
27
  open = this.useChange(false, (value) => this.renderOpen(value), { attribute: "open" });
23
- /** An icon before the heading. */
28
+ /** An icon before the heading; the `icon` slot takes a richer one instead. */
24
29
  icon = this.useChange("", (name) => {
25
30
  this._refs.icon.name.set(name);
26
31
  this._refs.icon.hidden = name === "";
@@ -32,6 +37,7 @@ var JlAccordionItem = class extends JlElement {
32
37
  this._refs.summary.tabIndex = value ? -1 : 0;
33
38
  }, { attribute: "disabled" });
34
39
  _refs = this.useRefs((ref) => ({
40
+ description: ref(".description"),
35
41
  details: ref("details"),
36
42
  heading: ref(".heading"),
37
43
  icon: ref(".icon"),
@@ -41,12 +47,21 @@ var JlAccordionItem = class extends JlElement {
41
47
  return /* @__PURE__ */ jsxs("details", {
42
48
  attrs: { part: "base" },
43
49
  children: [/* @__PURE__ */ jsxs("summary", { children: [
44
- /* @__PURE__ */ jsx(JlIcon, {
45
- class: "icon",
46
- size: ICON_SIZE,
47
- hidden: true
50
+ /* @__PURE__ */ jsx("slot", {
51
+ name: "icon",
52
+ children: /* @__PURE__ */ jsx(JlIcon, {
53
+ class: "icon",
54
+ size: ICON_SIZE,
55
+ hidden: true
56
+ })
57
+ }),
58
+ /* @__PURE__ */ jsxs("span", {
59
+ class: "titles",
60
+ children: [/* @__PURE__ */ jsx("span", { class: "heading" }), /* @__PURE__ */ jsx("span", {
61
+ class: "description",
62
+ hidden: true
63
+ })]
48
64
  }),
49
- /* @__PURE__ */ jsx("span", { class: "heading" }),
50
65
  /* @__PURE__ */ jsx(JlIcon, {
51
66
  class: "chevron",
52
67
  name: "chevron-down",
@@ -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/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;
@@ -4,13 +4,17 @@ import { jsonOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
5
  import { setOrRemove, svgElement } from "../lib/dom.js";
6
6
  import { JlIcon } from "../atoms/jl-icon.js";
7
- import { FLOW_NODE, FLOW_ZOOM, connect, edgeEnds, edgeMidpoint, edgePath, fitView, isPortFree, layoutFlow, moveNode, nodeAt, outputAnchor, portX, portsOf, positionAfter, removeEdge, removeNode, revealIn, snap, toWorld, zoomAt } from "../lib/flow.js";
7
+ import { FLOW_NODE, FLOW_ZOOM, centeredAt, connect, edgeEnds, edgeMidpoint, edgePath, fitView, isEdgeTaken, isPortFree, layoutFlow, moveNode, nodeAt, nodeHeight, outputAnchor, portX, portsOf, positionAfter, removeEdge, removeNode, revealIn, snap, toWorld, zoomAt } from "../lib/flow.js";
8
8
  import jl_flow_default from "./jl-flow.css_raw.js";
9
9
  import { css } from "@jadis/core";
10
- import { Maximize, Minus, Plus, WandSparkles, X } from "lucide";
10
+ import { Check, CircleAlert, Hourglass, LoaderCircle, Maximize, Minus, Plus, WandSparkles, X } from "lucide";
11
11
  import { Fragment, jsx, jsxs } from "@jadis/core/jsx-runtime";
12
12
  // src/organisms/jl-flow.tsx
13
13
  registerIcons({
14
+ check: Check,
15
+ "circle-alert": CircleAlert,
16
+ hourglass: Hourglass,
17
+ "loader-circle": LoaderCircle,
14
18
  maximize: Maximize,
15
19
  minus: Minus,
16
20
  plus: Plus,
@@ -32,6 +36,7 @@ var PERCENT = 100;
32
36
  var ICON_SIZE = 16;
33
37
  var ADD_ICON_SIZE = 12;
34
38
  var CONTROL_ICON_SIZE = 14;
39
+ var STATUS_ICON_SIZE = 12;
35
40
  var PORT_GAP = 6;
36
41
  var ID_LENGTH = 8;
37
42
  var DELETE_KEYS = /* @__PURE__ */ new Set(["Delete", "Backspace"]);
@@ -53,6 +58,20 @@ var ARROWS = /* @__PURE__ */ new Map([
53
58
  y: -1
54
59
  }]
55
60
  ]);
61
+ var STATUS_ICONS = {
62
+ done: "check",
63
+ failed: "x",
64
+ running: "loader-circle",
65
+ waiting: "hourglass"
66
+ };
67
+ var STATUS_LABELS = {
68
+ done: "Done",
69
+ failed: "Failed",
70
+ running: "Running",
71
+ waiting: "Waiting"
72
+ };
73
+ /** What screen readers hear after a node's label: how it did in the run, and what to fix. */
74
+ var noteOf = (node) => [node.status ? STATUS_LABELS[node.status] : "", node.problem ?? ""].filter((part) => part !== "").join(". ");
56
75
  var markOf = (element, name) => element?.closest(`[${name}]`)?.getAttribute(name) ?? void 0;
57
76
  var isControl = (event) => event.composedPath().some((element) => element instanceof Element && element.hasAttribute("data-control"));
58
77
  function targetOf(event) {
@@ -71,7 +90,8 @@ var addLabel = (node, port) => port.label ? `Add after ${node.label}, ${port.lab
71
90
  /**
72
91
  * A flow of nodes joined by edges, to edit: people move the nodes, draw an edge from a node's exit to another node,
73
92
  * 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
74
- * (`change`); the page adds nodes itself, when `add` asks.
93
+ * (`change`); the page adds nodes itself, when `add` or `drop` asks. Nodes can show how a run went (`status`,
94
+ * `taken`) and what to fix (`problem`).
75
95
  */
76
96
  var JlFlow = class extends JlElement {
77
97
  static selector = "jl-flow";
@@ -90,6 +110,11 @@ var JlFlow = class extends JlElement {
90
110
  this.reflect("readonly", value);
91
111
  this.render();
92
112
  }, { attribute: "readonly" });
113
+ /**
114
+ * The drag-and-drop type the canvas takes (`application/x-step`): something dragged in carrying it asks for a node
115
+ * where it is dropped, with `drop`.
116
+ */
117
+ dropType = this.useChange("", () => void 0, { attribute: "drop-type" });
93
118
  /** The canvas's name for screen readers. */
94
119
  label = this.useChange("", (value) => this._refs.viewport.setAttribute("aria-label", value || DEFAULT_LABEL), { attribute: "label" });
95
120
  _state = {
@@ -133,6 +158,27 @@ var JlFlow = class extends JlElement {
133
158
  this.setViewport(fitView(this._state.graph.nodes, width, height));
134
159
  });
135
160
  }
161
+ /** Moves the view, when needed, so that a node is in sight: one just added, or moved with the keys. */
162
+ reveal(id) {
163
+ this.whenRendered(() => {
164
+ const node = this.nodeOf(id);
165
+ const { width, height } = this._refs.viewport.getBoundingClientRect();
166
+ if (!node || width === 0) return;
167
+ this.setViewport(revealIn(this._state.viewport, node, width, height));
168
+ });
169
+ }
170
+ /** Where a node added after an exit goes, as + puts it: a row below, under that exit. */
171
+ positionAfter(from, port) {
172
+ return positionAfter(this._state.graph, from, port);
173
+ }
174
+ /** Where a node added without a place goes: in the middle of the view (its top left corner). */
175
+ centerPosition() {
176
+ const { width, height } = this._refs.viewport.getBoundingClientRect();
177
+ return centeredAt(toWorld(this._state.viewport, {
178
+ x: width / 2,
179
+ y: height / 2
180
+ }));
181
+ }
136
182
  /** Lays the nodes out again, in rows from the starts down (`change` gives the new positions), and shows them all. */
137
183
  layout() {
138
184
  this.whenRendered(() => {
@@ -236,6 +282,8 @@ var JlFlow = class extends JlElement {
236
282
  this.on(viewport, "wheel", (event) => this.wheel(event));
237
283
  this.on(viewport, "keydown", (event) => this.key(event));
238
284
  this.on(viewport, "click", (event) => this.pressAdd(event));
285
+ this.on(viewport, "dragover", (event) => this.dragOver(event));
286
+ this.on(viewport, "drop", (event) => this.dropOn(event));
239
287
  this.on(viewport, "focusin", (event) => {
240
288
  const node = targetOf(event).node;
241
289
  if (node !== void 0) this.select(node);
@@ -424,6 +472,24 @@ var JlFlow = class extends JlElement {
424
472
  position: positionAfter(this._state.graph, node, add)
425
473
  });
426
474
  }
475
+ /** Whether a drag carries what the canvas takes: then it may be dropped here. */
476
+ carriesDrop(event) {
477
+ const type = this.dropType.get();
478
+ return type !== "" && !this.readonly.get() && (event.dataTransfer?.types.includes(type) ?? false);
479
+ }
480
+ dragOver(event) {
481
+ if (this.carriesDrop(event)) event.preventDefault();
482
+ }
483
+ dropOn(event) {
484
+ if (!this.carriesDrop(event)) return;
485
+ event.preventDefault();
486
+ event.stopPropagation();
487
+ const data = event.dataTransfer?.getData(this.dropType.get()) ?? "";
488
+ this.events.emit("drop", {
489
+ data,
490
+ position: centeredAt(this.worldPoint(event))
491
+ });
492
+ }
427
493
  key(event) {
428
494
  const target = targetOf(event);
429
495
  if (target.control || target.add !== void 0) return;
@@ -453,13 +519,6 @@ var JlFlow = class extends JlElement {
453
519
  this.commit(moveNode(this._state.graph, node.id, position));
454
520
  this.reveal(node.id);
455
521
  }
456
- /** Moves the view, when needed, so that a node is in sight: one just added, or moved with the keys. */
457
- reveal(id) {
458
- const node = this.nodeOf(id);
459
- const { width, height } = this._refs.viewport.getBoundingClientRect();
460
- if (!node || width === 0) return;
461
- this.setViewport(revealIn(this._state.viewport, node, width, height));
462
- }
463
522
  /** Two fingers or the wheel move the view; with Ctrl or ⌘ (a pinch on a trackpad), they zoom. */
464
523
  wheel(event) {
465
524
  event.preventDefault();
@@ -535,30 +594,63 @@ var JlFlow = class extends JlElement {
535
594
  drawNode(element, node) {
536
595
  const ports = portsOf(node);
537
596
  element.setAttribute("aria-label", node.label);
597
+ setOrRemove(element, "aria-description", noteOf(node));
538
598
  element.part.toggle("start", node.start === true);
539
599
  setOrRemove(element, "data-tone", node.tone ?? "");
600
+ setOrRemove(element, "data-status", node.status ?? "");
601
+ element.style.height = `${nodeHeight(node)}px`;
602
+ this.paintIcon(element, node.color ?? "");
540
603
  element.replaceChildren(/* @__PURE__ */ jsxs(Fragment, { children: [
541
604
  node.start === true ? null : /* @__PURE__ */ jsx("span", { class: "input" }),
542
- node.icon ? /* @__PURE__ */ jsx("span", {
543
- class: "icon",
544
- attrs: { part: "icon" },
545
- children: /* @__PURE__ */ jsx(JlIcon, {
546
- name: node.icon,
547
- size: ICON_SIZE
548
- })
549
- }) : null,
550
605
  /* @__PURE__ */ jsxs("span", {
551
- class: "text",
552
- children: [/* @__PURE__ */ jsx("span", {
553
- class: "label",
554
- attrs: { part: "label" },
555
- children: node.label
556
- }), node.description ? /* @__PURE__ */ jsx("span", {
557
- class: "description",
558
- attrs: { part: "description" },
559
- children: node.description
560
- }) : null]
606
+ class: "head",
607
+ children: [
608
+ node.icon ? /* @__PURE__ */ jsx("span", {
609
+ class: "icon",
610
+ attrs: { part: "icon" },
611
+ children: /* @__PURE__ */ jsx(JlIcon, {
612
+ name: node.icon,
613
+ size: ICON_SIZE
614
+ })
615
+ }) : null,
616
+ /* @__PURE__ */ jsxs("span", {
617
+ class: "text",
618
+ children: [/* @__PURE__ */ jsx("span", {
619
+ class: "label",
620
+ attrs: { part: "label" },
621
+ children: node.label
622
+ }), node.description ? /* @__PURE__ */ jsx("span", {
623
+ class: "description",
624
+ attrs: { part: "description" },
625
+ children: node.description
626
+ }) : null]
627
+ }),
628
+ node.problem ? /* @__PURE__ */ jsx("span", {
629
+ class: "problem",
630
+ title: node.problem,
631
+ attrs: { part: "problem" },
632
+ children: /* @__PURE__ */ jsx(JlIcon, {
633
+ name: "circle-alert",
634
+ size: ICON_SIZE
635
+ })
636
+ }) : null,
637
+ node.status ? /* @__PURE__ */ jsx("span", {
638
+ class: "status",
639
+ title: STATUS_LABELS[node.status],
640
+ attrs: { part: "status" },
641
+ children: /* @__PURE__ */ jsx(JlIcon, {
642
+ name: STATUS_ICONS[node.status],
643
+ size: STATUS_ICON_SIZE
644
+ })
645
+ }) : null
646
+ ]
561
647
  }),
648
+ node.detail ? /* @__PURE__ */ jsx("span", {
649
+ class: "detail",
650
+ title: node.detail,
651
+ attrs: { part: "detail" },
652
+ children: node.detail
653
+ }) : null,
562
654
  /* @__PURE__ */ jsx("span", {
563
655
  class: "ports",
564
656
  children: ports.map((port, index) => this.portElement(node, port, index, ports.length))
@@ -566,6 +658,14 @@ var JlFlow = class extends JlElement {
566
658
  ] }));
567
659
  this.place(element, node.position);
568
660
  }
661
+ /** A colour of the page's own for the icon wins over the tone's. */
662
+ paintIcon(element, color) {
663
+ if (color === "") {
664
+ element.style.removeProperty("--tone");
665
+ return;
666
+ }
667
+ element.style.setProperty("--tone", color);
668
+ }
569
669
  portElement(node, port, index, count) {
570
670
  const free = !this.readonly.get() && isPortFree(this._state.graph, node, port.id);
571
671
  const element = /* @__PURE__ */ jsxs("span", {
@@ -594,6 +694,7 @@ var JlFlow = class extends JlElement {
594
694
  element.style.left = `${portX(index, count)}px`;
595
695
  element.style.maxWidth = `${FLOW_NODE.width / count - PORT_GAP}px`;
596
696
  setOrRemove(element, "data-tone", port.tone ?? "");
697
+ element.toggleAttribute("data-taken", node.taken === port.id);
597
698
  return element;
598
699
  }
599
700
  place(element, position) {
@@ -611,16 +712,20 @@ var JlFlow = class extends JlElement {
611
712
  const { edge: selected, graph } = this._state;
612
713
  const groups = graph.edges.flatMap((edge) => {
613
714
  const ends = edgeEnds(graph, edge);
614
- return ends ? [this.edgeGroup(edge.id, ends, edge.id === selected)] : [];
715
+ return ends ? [this.edgeGroup(edge.id, ends, edge.id === selected, isEdgeTaken(graph, edge))] : [];
615
716
  });
616
717
  this._svg.root.replaceChildren(...groups, this._svg.draft);
617
718
  this.renderEdgeTool();
618
719
  }
619
- /** An edge, with a wide invisible twin that catches the pointer: a thin line is still easy to press. */
620
- edgeGroup(id, ends, selected) {
720
+ /**
721
+ * An edge, with a wide invisible twin that catches the pointer: a thin line is still easy to press. One a run took
722
+ * stands out.
723
+ */
724
+ edgeGroup(id, ends, selected, taken) {
621
725
  const d = edgePath(ends.from, ends.to);
726
+ const states = [selected ? "selected" : "", taken ? "taken" : ""].filter((state) => state !== "");
622
727
  const group = svgElement("g", {
623
- class: selected ? "edge-group selected" : "edge-group",
728
+ class: ["edge-group", ...states].join(" "),
624
729
  "data-edge": id
625
730
  });
626
731
  group.append(svgElement("path", {
@@ -630,7 +735,7 @@ var JlFlow = class extends JlElement {
630
735
  class: "edge",
631
736
  d,
632
737
  "data-tone": ends.tone,
633
- part: selected ? "edge selected" : "edge"
738
+ part: ["edge", ...states].join(" ")
634
739
  }));
635
740
  return group;
636
741
  }
@@ -10,6 +10,8 @@ export type FlowPort = {
10
10
  label?: string;
11
11
  tone?: Tone;
12
12
  };
13
+ /** How a node did in a run, shown on it: at work, finished, waiting for someone, or failed. */
14
+ export type FlowNodeStatus = 'running' | 'done' | 'waiting' | 'failed';
13
15
  export type FlowNode = {
14
16
  id: string;
15
17
  label: string;
@@ -17,9 +19,19 @@ export type FlowNode = {
17
19
  position: FlowPoint;
18
20
  /** A second, quieter line under the label. */
19
21
  description?: string;
22
+ /** A third line, under a dashed rule: what the node is set to do. The node is taller with it. */
23
+ detail?: string;
20
24
  icon?: string;
21
25
  /** The colour of the icon: `accent` by default. */
22
26
  tone?: Tone;
27
+ /** The icon's colour as any CSS colour, for kinds the tones do not cover (`var(--my-ai)`); wins over `tone`. */
28
+ color?: string;
29
+ /** How it did in a run. */
30
+ status?: FlowNodeStatus;
31
+ /** The exit a run left it by: that exit, and its edges to nodes with a status, stand out. */
32
+ taken?: string;
33
+ /** Something to fix before the flow can run: a mark on the node, with this text as its explanation. */
34
+ problem?: string;
23
35
  /** The exits. Absent, the node has one without a name; empty, it has none (it ends the flow). */
24
36
  ports?: FlowPort[];
25
37
  /** A start takes no incoming edge. */
@@ -42,6 +54,11 @@ export type FlowAddRequest = {
42
54
  port: string;
43
55
  position: FlowPoint;
44
56
  };
57
+ /** Asks for a node where something was dropped: what it carried, and where to put the node. */
58
+ export type FlowDropRequest = {
59
+ data: string;
60
+ position: FlowPoint;
61
+ };
45
62
  /** How far the view is moved, and how much it is zoomed. */
46
63
  export type FlowViewport = {
47
64
  x: number;
@@ -3,5 +3,7 @@ export type SegmentedOption = {
3
3
  value: string;
4
4
  label?: string;
5
5
  icon?: string;
6
+ /** A count or a short word after the label: what waits behind the choice. */
7
+ badge?: string | number;
6
8
  disabled?: boolean;
7
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syware/jlib",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A tree-shakeable component library on Jadis and native Web Components, organised in atomic design",
5
5
  "keywords": [
6
6
  "web-components",