@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.
- package/README.md +1 -0
- package/dist/all.js +5 -3
- package/dist/atoms/jl-button.css_raw.js +1 -1
- package/dist/atoms/jl-button.d.ts +19 -0
- package/dist/atoms/jl-button.js +75 -23
- package/dist/atoms/jl-chip.css_raw.js +1 -1
- package/dist/atoms/jl-input.d.ts +6 -0
- package/dist/atoms/jl-input.js +4 -0
- package/dist/atoms/jl-select.d.ts +6 -0
- package/dist/atoms/jl-select.js +18 -1
- package/dist/atoms/jl-switch.d.ts +12 -1
- package/dist/atoms/jl-switch.js +11 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -2
- package/dist/lib/attributes.d.ts +5 -0
- package/dist/lib/attributes.js +13 -1
- package/dist/lib/flow.d.ts +10 -1
- package/dist/lib/flow.js +26 -9
- package/dist/lib/tokens.d.ts +2 -1
- package/dist/lib/tokens.js +13 -4
- package/dist/molecules/jl-accordion-item.css_raw.js +1 -1
- package/dist/molecules/jl-accordion-item.d.ts +6 -1
- package/dist/molecules/jl-accordion-item.js +21 -6
- package/dist/molecules/jl-alert.d.ts +10 -3
- package/dist/molecules/jl-alert.js +23 -6
- package/dist/molecules/jl-chip-group.css_raw.js +3 -0
- package/dist/molecules/jl-chip-group.d.ts +81 -0
- package/dist/molecules/jl-chip-group.js +259 -0
- package/dist/molecules/jl-search.d.ts +3 -1
- package/dist/molecules/jl-search.js +8 -6
- package/dist/molecules/jl-segmented.css_raw.js +1 -1
- package/dist/molecules/jl-segmented.js +14 -7
- package/dist/molecules/jl-stat.css_raw.js +1 -1
- package/dist/molecules/jl-stat.d.ts +6 -0
- package/dist/molecules/jl-stat.js +6 -0
- package/dist/molecules/jl-tag-input.css_raw.js +1 -1
- package/dist/organisms/jl-flow.css_raw.js +1 -1
- package/dist/organisms/jl-flow.d.ts +28 -5
- package/dist/organisms/jl-flow.js +138 -33
- package/dist/organisms/jl-tab-bar.css_raw.js +3 -0
- package/dist/organisms/jl-tab-bar.d.ts +43 -0
- package/dist/organisms/jl-tab-bar.js +123 -0
- package/dist/templates/jl-app-shell.css_raw.js +1 -1
- package/dist/templates/jl-app-shell.d.ts +8 -2
- package/dist/templates/jl-app-shell.js +32 -5
- package/dist/types/jl-chip-group.types.d.ts +10 -0
- package/dist/types/jl-flow.types.d.ts +17 -0
- package/dist/types/jl-segmented.types.d.ts +2 -0
- package/dist/types/jl-tab-bar.types.d.ts +11 -0
- package/package.json +1 -1
|
@@ -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: "
|
|
552
|
-
children: [
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
/**
|
|
620
|
-
|
|
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:
|
|
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:
|
|
738
|
+
part: ["edge", ...states].join(" ")
|
|
634
739
|
}));
|
|
635
740
|
return group;
|
|
636
741
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// src/organisms/jl-tab-bar.css?raw
|
|
2
|
+
var jl_tab_bar_default = ":host {\n display: block;\n --height: 60px;\n}\n\nnav {\n display: grid;\n grid-auto-columns: minmax(0, 1fr);\n grid-auto-flow: column;\n min-height: var(--height);\n /* Clear of the home indicator on phones without a button. */\n padding-bottom: env(safe-area-inset-bottom);\n border-top: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n/* The items and the \"more\" button share the bar's columns. */\n.items {\n display: contents;\n}\n\n.item {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 2px;\n min-width: 0;\n padding: 6px 2px;\n border: none;\n background: none;\n color: var(--jl-muted);\n font-size: 11px;\n font-weight: 600;\n line-height: 1.4;\n text-decoration: none;\n cursor: pointer;\n transition: color var(--jl-fast) var(--jl-ease);\n}\n\n.item:hover {\n color: var(--jl-text);\n}\n\n.item[aria-current=\"page\"] {\n color: var(--jl-accent);\n}\n\n/* The ring goes round the icon's pill: round the whole column, it would touch the bar's edges. */\n.item:focus-visible {\n box-shadow: none;\n}\n\n.icon {\n position: relative;\n display: inline-grid;\n place-items: center;\n width: 52px;\n height: 28px;\n border-radius: var(--jl-radius-pill);\n transition: background var(--jl-fast) var(--jl-ease);\n}\n\n.item:focus-visible .icon {\n box-shadow: var(--jl-focus-ring);\n}\n\n/* The current item is told by a shape too, not by its colour alone. */\n.item[aria-current=\"page\"] .icon {\n background: var(--jl-accent-soft);\n}\n\n.label {\n max-width: 100%;\n padding: 0 2px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.badge {\n position: absolute;\n top: -4px;\n left: calc(50% + 4px);\n min-width: 18px;\n height: 18px;\n padding: 0 5px;\n border: 2px solid var(--jl-surface);\n border-radius: var(--jl-radius-pill);\n background: var(--jl-accent);\n color: var(--jl-on-accent);\n font-size: 10px;\n font-weight: 700;\n line-height: 14px;\n text-align: center;\n}\n";
|
|
3
|
+
export { jl_tab_bar_default as default };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { JlElement } from '../base/jl-element';
|
|
2
|
+
import type { TabBarItem } from '../types/jl-tab-bar.types';
|
|
3
|
+
/**
|
|
4
|
+
* The bar along the bottom of a phone, under the thumb: the three to five main places of an app, an icon over a
|
|
5
|
+
* label, one tap away. `value` is the current item. Items with an `href` are links; the others are buttons; both
|
|
6
|
+
* fire `select`. `more` adds a last button for everything else, which fires `more`: in a `jl-app-shell`, it opens
|
|
7
|
+
* the navigation drawer.
|
|
8
|
+
*/
|
|
9
|
+
export declare class JlTabBar extends JlElement {
|
|
10
|
+
static readonly selector = "jl-tab-bar";
|
|
11
|
+
readonly events: Readonly<import("@jadis/core").UseEventsHandler<{
|
|
12
|
+
select: string;
|
|
13
|
+
more: undefined;
|
|
14
|
+
}>>;
|
|
15
|
+
readonly items: Readonly<Readonly<{
|
|
16
|
+
get(): TabBarItem[];
|
|
17
|
+
set(setter: TabBarItem[] | ((prevState: TabBarItem[]) => TabBarItem[])): void;
|
|
18
|
+
}>>;
|
|
19
|
+
/** The id of the current item. */
|
|
20
|
+
readonly value: Readonly<Readonly<{
|
|
21
|
+
get(): string;
|
|
22
|
+
set(setter: string | ((prevState: string) => string)): void;
|
|
23
|
+
}>>;
|
|
24
|
+
/** The label of a last button for the other places (`More`); empty shows none. */
|
|
25
|
+
readonly more: Readonly<Readonly<{
|
|
26
|
+
get(): string;
|
|
27
|
+
set(setter: string | ((prevState: string) => string)): void;
|
|
28
|
+
}>>;
|
|
29
|
+
/** The name of the navigation for screen readers. */
|
|
30
|
+
readonly label: Readonly<Readonly<{
|
|
31
|
+
get(): string;
|
|
32
|
+
set(setter: string | ((prevState: string) => string)): void;
|
|
33
|
+
}>>;
|
|
34
|
+
private readonly _refs;
|
|
35
|
+
templateHtml(): Node;
|
|
36
|
+
templateCss(): string;
|
|
37
|
+
onConnect(): void;
|
|
38
|
+
private onClick;
|
|
39
|
+
private render;
|
|
40
|
+
private renderItem;
|
|
41
|
+
/** The current item says so: `aria-current="page"`, drawn in the accent. */
|
|
42
|
+
private renderCurrent;
|
|
43
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { BASE_STYLES } from "../styles/shared.js";
|
|
2
|
+
import { JlElement } from "../base/jl-element.js";
|
|
3
|
+
import { jsonOf } from "../lib/attributes.js";
|
|
4
|
+
import { registerIcons } from "../lib/icons.js";
|
|
5
|
+
import { setOrRemove } from "../lib/dom.js";
|
|
6
|
+
import { JlIcon } from "../atoms/jl-icon.js";
|
|
7
|
+
import jl_tab_bar_default from "./jl-tab-bar.css_raw.js";
|
|
8
|
+
import { css } from "@jadis/core";
|
|
9
|
+
import { Ellipsis } from "lucide";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "@jadis/core/jsx-runtime";
|
|
11
|
+
// src/organisms/jl-tab-bar.tsx
|
|
12
|
+
registerIcons({ ellipsis: Ellipsis });
|
|
13
|
+
var ICON_SIZE = 22;
|
|
14
|
+
var DEFAULT_LABEL = "Shortcuts";
|
|
15
|
+
var MAX_COUNT = 99;
|
|
16
|
+
var ITEM_SELECTOR = ".item";
|
|
17
|
+
/** A JSON attribute that should hold a list of items; anything else gives none. */
|
|
18
|
+
var parseItems = (value) => {
|
|
19
|
+
const parsed = jsonOf([])(value);
|
|
20
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
21
|
+
};
|
|
22
|
+
/** What the badge shows: counts are capped so they fit over the icon. */
|
|
23
|
+
var badgeText = (badge) => {
|
|
24
|
+
if (typeof badge === "number") return badge > MAX_COUNT ? `${MAX_COUNT}+` : String(badge);
|
|
25
|
+
return badge ?? "";
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The bar along the bottom of a phone, under the thumb: the three to five main places of an app, an icon over a
|
|
29
|
+
* label, one tap away. `value` is the current item. Items with an `href` are links; the others are buttons; both
|
|
30
|
+
* fire `select`. `more` adds a last button for everything else, which fires `more`: in a `jl-app-shell`, it opens
|
|
31
|
+
* the navigation drawer.
|
|
32
|
+
*/
|
|
33
|
+
var JlTabBar = class extends JlElement {
|
|
34
|
+
static selector = "jl-tab-bar";
|
|
35
|
+
events = this.useEvents();
|
|
36
|
+
items = this.useChange([], () => this.render(), {
|
|
37
|
+
attribute: "items",
|
|
38
|
+
parse: parseItems
|
|
39
|
+
});
|
|
40
|
+
/** The id of the current item. */
|
|
41
|
+
value = this.useChange("", (value) => this.renderCurrent(value), { attribute: "value" });
|
|
42
|
+
/** The label of a last button for the other places (`More`); empty shows none. */
|
|
43
|
+
more = this.useChange("", (value) => {
|
|
44
|
+
this._refs.moreLabel.textContent = value;
|
|
45
|
+
this._refs.more.hidden = value === "";
|
|
46
|
+
}, { attribute: "more" });
|
|
47
|
+
/** The name of the navigation for screen readers. */
|
|
48
|
+
label = this.useChange(DEFAULT_LABEL, (value) => this._refs.nav.setAttribute("aria-label", value === "" ? DEFAULT_LABEL : value), { attribute: "label" });
|
|
49
|
+
_refs = this.useRefs((ref) => ({
|
|
50
|
+
items: ref(".items"),
|
|
51
|
+
more: ref(".more"),
|
|
52
|
+
moreLabel: ref(".more .label"),
|
|
53
|
+
nav: ref("nav")
|
|
54
|
+
}));
|
|
55
|
+
templateHtml() {
|
|
56
|
+
return /* @__PURE__ */ jsxs("nav", {
|
|
57
|
+
attrs: { part: "base" },
|
|
58
|
+
children: [/* @__PURE__ */ jsx("div", { class: "items" }), /* @__PURE__ */ jsxs("button", {
|
|
59
|
+
class: "item more",
|
|
60
|
+
type: "button",
|
|
61
|
+
hidden: true,
|
|
62
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
63
|
+
class: "icon",
|
|
64
|
+
children: /* @__PURE__ */ jsx(JlIcon, {
|
|
65
|
+
name: "ellipsis",
|
|
66
|
+
size: ICON_SIZE
|
|
67
|
+
})
|
|
68
|
+
}), /* @__PURE__ */ jsx("span", { class: "label" })]
|
|
69
|
+
})]
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
templateCss() {
|
|
73
|
+
return css`${BASE_STYLES}${jl_tab_bar_default}`;
|
|
74
|
+
}
|
|
75
|
+
onConnect() {
|
|
76
|
+
this.on(this._refs.items, "click", (event) => this.onClick(event));
|
|
77
|
+
this.on(this._refs.more, "click", () => this.events.emit("more"));
|
|
78
|
+
}
|
|
79
|
+
onClick(event) {
|
|
80
|
+
const id = event.target.closest(ITEM_SELECTOR)?.dataset.id;
|
|
81
|
+
if (id === void 0) return;
|
|
82
|
+
this.value.set(id);
|
|
83
|
+
this.events.emit("select", id);
|
|
84
|
+
}
|
|
85
|
+
render() {
|
|
86
|
+
this._refs.items.replaceChildren(...this.items.get().map((item) => this.renderItem(item)));
|
|
87
|
+
this.renderCurrent(this.value.get());
|
|
88
|
+
}
|
|
89
|
+
renderItem(item) {
|
|
90
|
+
const badge = badgeText(item.badge);
|
|
91
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
92
|
+
class: "icon",
|
|
93
|
+
children: [/* @__PURE__ */ jsx(JlIcon, {
|
|
94
|
+
name: item.icon,
|
|
95
|
+
size: ICON_SIZE
|
|
96
|
+
}), badge === "" ? null : /* @__PURE__ */ jsx("span", {
|
|
97
|
+
class: "badge",
|
|
98
|
+
children: badge
|
|
99
|
+
})]
|
|
100
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
101
|
+
class: "label",
|
|
102
|
+
children: item.label
|
|
103
|
+
})] });
|
|
104
|
+
if (item.href === void 0) return /* @__PURE__ */ jsx("button", {
|
|
105
|
+
class: "item",
|
|
106
|
+
type: "button",
|
|
107
|
+
"data-id": item.id,
|
|
108
|
+
children: content
|
|
109
|
+
});
|
|
110
|
+
return /* @__PURE__ */ jsx("a", {
|
|
111
|
+
class: "item",
|
|
112
|
+
href: item.href,
|
|
113
|
+
"data-id": item.id,
|
|
114
|
+
children: content
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** The current item says so: `aria-current="page"`, drawn in the accent. */
|
|
118
|
+
renderCurrent(value) {
|
|
119
|
+
for (const item of this._refs.items.querySelectorAll(ITEM_SELECTOR)) setOrRemove(item, "aria-current", item.dataset.id === value ? "page" : "");
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
JlTabBar.register();
|
|
123
|
+
export { JlTabBar };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// src/templates/jl-app-shell.css?raw
|
|
2
|
-
var jl_app_shell_default = ":host {\n display: block;\n height: 100%;\n min-height: 0;\n container: jl-app-shell / inline-size;\n --header-height: 56px;\n --drawer-width: min(300px, 86%);\n}\n\n.shell {\n position: relative;\n display: grid;\n grid-template:\n \"top top top\" auto\n \"nav main aside\" minmax(0, 1fr)\n / auto minmax(0, 1fr) auto;\n height: 100%;\n overflow: hidden;\n background: var(--jl-bg);\n color: var(--jl-text);\n}\n\n.top {\n grid-area: top;\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: var(--header-height);\n padding: 0 16px;\n border-bottom: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n:host(:not([has-header])) .top {\n display: none;\n}\n\n::slotted([slot=\"header\"]) {\n flex: 1;\n min-width: 0;\n}\n\n.menu {\n display: none;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--jl-control-md);\n height: var(--jl-control-md);\n margin-left: -8px;\n padding: 0;\n border: none;\n border-radius: var(--jl-radius-sm);\n background: none;\n color: var(--jl-text-2);\n cursor: pointer;\n}\n\n.menu:hover {\n background: var(--jl-surface-3);\n color: var(--jl-text);\n}\n\n.nav {\n grid-area: nav;\n min-height: 0;\n overflow-y: auto;\n border-right: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n.nav:focus-visible {\n box-shadow: none;\n}\n\n:host(:not([has-nav])) .nav {\n display: none;\n}\n\n.main {\n grid-area: main;\n min-width: 0;\n min-height: 0;\n overflow: auto;\n}\n\n.aside {\n grid-area: aside;\n min-height: 0;\n overflow-y: auto;\n border-left: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n:host(:not([has-aside])) .aside {\n display: none;\n}\n\n.scrim {\n display: none;\n}\n\n/* Narrow: the sidebar slides over the content as a drawer, the menu button opens it, the panel steps aside. */\n@container jl-app-shell (width < 960px) {\n .shell {\n grid-template:\n \"top\" auto\n \"main\" minmax(0, 1fr)\n / minmax(0, 1fr);\n }\n\n :host([has-nav]) .top {\n display: flex;\n }\n\n :host([has-nav]) .menu {\n display: inline-flex;\n }\n\n .nav {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n z-index: 2;\n max-width: var(--drawer-width);\n box-shadow: var(--jl-shadow-lg);\n transform: translateX(-100%);\n visibility: hidden;\n transition:\n transform var(--jl-slow) var(--jl-ease),\n visibility 0s linear var(--jl-slow);\n }\n\n :host([nav-open]) .nav {\n transform: none;\n visibility: visible;\n transition: transform var(--jl-slow) var(--jl-ease);\n }\n\n .scrim {\n position: absolute;\n inset: 0;\n z-index: 1;\n display: block;\n background: var(--jl-scrim);\n opacity: 0;\n pointer-events: none;\n transition: opacity var(--jl-slow) var(--jl-ease);\n }\n\n :host([nav-open]) .scrim {\n opacity: 1;\n pointer-events: auto;\n }\n\n .aside {\n display: none;\n }\n}\n";
|
|
2
|
+
var jl_app_shell_default = ":host {\n display: block;\n height: 100%;\n min-height: 0;\n container: jl-app-shell / inline-size;\n --header-height: 56px;\n --drawer-width: min(300px, 86%);\n}\n\n.shell {\n position: relative;\n display: grid;\n grid-template:\n \"top top top\" auto\n \"nav main aside\" minmax(0, 1fr)\n / auto minmax(0, 1fr) auto;\n height: 100%;\n overflow: hidden;\n background: var(--jl-bg);\n color: var(--jl-text);\n}\n\n.top {\n grid-area: top;\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: var(--header-height);\n padding: 0 16px;\n border-bottom: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n:host(:not([has-header])) .top {\n display: none;\n}\n\n::slotted([slot=\"header\"]) {\n flex: 1;\n min-width: 0;\n}\n\n.menu {\n display: none;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--jl-control-md);\n height: var(--jl-control-md);\n margin-left: -8px;\n padding: 0;\n border: none;\n border-radius: var(--jl-radius-sm);\n background: none;\n color: var(--jl-text-2);\n cursor: pointer;\n}\n\n.menu:hover {\n background: var(--jl-surface-3);\n color: var(--jl-text);\n}\n\n.nav {\n grid-area: nav;\n min-height: 0;\n overflow-y: auto;\n border-right: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n.nav:focus-visible {\n box-shadow: none;\n}\n\n:host(:not([has-nav])) .nav {\n display: none;\n}\n\n/* Its own stacking context: what the page lifts with z-index (a sticky bar) stays under the drawer and its scrim. */\n.main {\n grid-area: main;\n min-width: 0;\n min-height: 0;\n overflow: auto;\n isolation: isolate;\n}\n\n.aside {\n grid-area: aside;\n min-height: 0;\n overflow-y: auto;\n border-left: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n:host(:not([has-aside])) .aside {\n display: none;\n}\n\n/* The bottom bar is for narrow screens: wide ones have the sidebar. */\n.tabs {\n display: none;\n}\n\n.scrim {\n display: none;\n}\n\n/* Narrow: the sidebar slides over the content as a drawer, the menu button opens it, the panel steps aside. */\n@container jl-app-shell (width < 960px) {\n .shell {\n grid-template:\n \"top\" auto\n \"main\" minmax(0, 1fr)\n \"tabs\" auto\n / minmax(0, 1fr);\n }\n\n :host([has-tabs]) .tabs {\n display: block;\n grid-area: tabs;\n }\n\n :host([has-nav]) .top {\n display: flex;\n }\n\n :host([has-nav]) .menu {\n display: inline-flex;\n }\n\n .nav {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n z-index: 2;\n max-width: var(--drawer-width);\n box-shadow: var(--jl-shadow-lg);\n transform: translateX(-100%);\n visibility: hidden;\n transition:\n transform var(--jl-slow) var(--jl-ease),\n visibility 0s linear var(--jl-slow);\n }\n\n :host([nav-open]) .nav {\n transform: none;\n visibility: visible;\n transition: transform var(--jl-slow) var(--jl-ease);\n }\n\n .scrim {\n position: absolute;\n inset: 0;\n z-index: 1;\n display: block;\n background: var(--jl-scrim);\n opacity: 0;\n pointer-events: none;\n transition: opacity var(--jl-slow) var(--jl-ease);\n }\n\n :host([nav-open]) .scrim {\n opacity: 1;\n pointer-events: auto;\n }\n\n .aside {\n display: none;\n }\n}\n";
|
|
3
3
|
export { jl_app_shell_default as default };
|
|
@@ -2,7 +2,8 @@ import { JlElement } from '../base/jl-element';
|
|
|
2
2
|
/**
|
|
3
3
|
* The frame of an app: a top bar (`header`), a sidebar (`nav`), the main area (the default slot), which scrolls
|
|
4
4
|
* on its own, and an optional panel on the right (`aside`). Narrower than 960 pixels, the sidebar becomes a drawer
|
|
5
|
-
* over the content, opened by a menu button in the top bar (or `nav-open`, or `toggleNav()`),
|
|
5
|
+
* over the content, opened by a menu button in the top bar (or `nav-open`, or `toggleNav()`), the panel hides, and
|
|
6
|
+
* a bar along the bottom shows (`tabs`, such as a `jl-tab-bar`, whose `more` opens the drawer).
|
|
6
7
|
* It takes the height of its container: give that one a height (`100dvh` for a whole page).
|
|
7
8
|
*/
|
|
8
9
|
export declare class JlAppShell extends JlElement {
|
|
@@ -27,11 +28,16 @@ export declare class JlAppShell extends JlElement {
|
|
|
27
28
|
/** Whether the shell is narrow: the menu button shows only then. */
|
|
28
29
|
private isNarrow;
|
|
29
30
|
private onMenu;
|
|
31
|
+
/** The tab bar's "more": the other places are in the drawer. */
|
|
32
|
+
private onMore;
|
|
30
33
|
private onKey;
|
|
31
34
|
/** The drawer as people open and close it: says so. */
|
|
32
35
|
private setNavOpen;
|
|
33
36
|
private renderNavOpen;
|
|
34
37
|
private renderInert;
|
|
35
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* What was slotted: empty areas take no room, a `select` from the nav (a side nav's) closes the drawer, and a
|
|
40
|
+
* `more` from the tabs (a tab bar's) opens it.
|
|
41
|
+
*/
|
|
36
42
|
private renderSlots;
|
|
37
43
|
}
|
|
@@ -15,7 +15,8 @@ var followsLink = (event) => event.composedPath().some((node) => node instanceof
|
|
|
15
15
|
/**
|
|
16
16
|
* The frame of an app: a top bar (`header`), a sidebar (`nav`), the main area (the default slot), which scrolls
|
|
17
17
|
* on its own, and an optional panel on the right (`aside`). Narrower than 960 pixels, the sidebar becomes a drawer
|
|
18
|
-
* over the content, opened by a menu button in the top bar (or `nav-open`, or `toggleNav()`),
|
|
18
|
+
* over the content, opened by a menu button in the top bar (or `nav-open`, or `toggleNav()`), the panel hides, and
|
|
19
|
+
* a bar along the bottom shows (`tabs`, such as a `jl-tab-bar`, whose `more` opens the drawer).
|
|
19
20
|
* It takes the height of its container: give that one a height (`100dvh` for a whole page).
|
|
20
21
|
*/
|
|
21
22
|
var JlAppShell = class extends JlElement {
|
|
@@ -23,7 +24,10 @@ var JlAppShell = class extends JlElement {
|
|
|
23
24
|
events = this.useEvents();
|
|
24
25
|
/** Whether the drawer is open, on narrow screens; wide ones always show the sidebar. */
|
|
25
26
|
navOpen = this.useChange(false, (value) => this.renderNavOpen(value), { attribute: "nav-open" });
|
|
26
|
-
_state = {
|
|
27
|
+
_state = {
|
|
28
|
+
navListeners: new AbortController(),
|
|
29
|
+
tabsListeners: new AbortController()
|
|
30
|
+
};
|
|
27
31
|
_resize = new ResizeObserver(() => this.renderInert());
|
|
28
32
|
_refs = this.useRefs((ref) => ({
|
|
29
33
|
aside: ref(".aside"),
|
|
@@ -33,7 +37,9 @@ var JlAppShell = class extends JlElement {
|
|
|
33
37
|
menu: ref(".menu"),
|
|
34
38
|
nav: ref(".nav"),
|
|
35
39
|
navSlot: ref("slot[name=\"nav\"]"),
|
|
36
|
-
scrim: ref(".scrim")
|
|
40
|
+
scrim: ref(".scrim"),
|
|
41
|
+
tabs: ref(".tabs"),
|
|
42
|
+
tabsSlot: ref("slot[name=\"tabs\"]")
|
|
37
43
|
}));
|
|
38
44
|
/** Opens or closes the drawer; `force` sets the state instead of flipping it. */
|
|
39
45
|
toggleNav(force) {
|
|
@@ -76,6 +82,11 @@ var JlAppShell = class extends JlElement {
|
|
|
76
82
|
attrs: { part: "aside" },
|
|
77
83
|
children: /* @__PURE__ */ jsx("slot", { name: "aside" })
|
|
78
84
|
}),
|
|
85
|
+
/* @__PURE__ */ jsx("div", {
|
|
86
|
+
class: "tabs",
|
|
87
|
+
attrs: { part: "tabs" },
|
|
88
|
+
children: /* @__PURE__ */ jsx("slot", { name: "tabs" })
|
|
89
|
+
}),
|
|
79
90
|
/* @__PURE__ */ jsx("div", { class: "scrim" })
|
|
80
91
|
]
|
|
81
92
|
});
|
|
@@ -95,12 +106,14 @@ var JlAppShell = class extends JlElement {
|
|
|
95
106
|
for (const slot of [
|
|
96
107
|
this._refs.headerSlot,
|
|
97
108
|
this._refs.navSlot,
|
|
98
|
-
this._refs.asideSlot
|
|
109
|
+
this._refs.asideSlot,
|
|
110
|
+
this._refs.tabsSlot
|
|
99
111
|
]) this.on(slot, "slotchange", () => this.renderSlots());
|
|
100
112
|
}
|
|
101
113
|
onDisconnect() {
|
|
102
114
|
this._resize.disconnect();
|
|
103
115
|
this._state.navListeners.abort();
|
|
116
|
+
this._state.tabsListeners.abort();
|
|
104
117
|
}
|
|
105
118
|
/** Whether the shell is narrow: the menu button shows only then. */
|
|
106
119
|
isNarrow() {
|
|
@@ -111,6 +124,11 @@ var JlAppShell = class extends JlElement {
|
|
|
111
124
|
this.setNavOpen(opening);
|
|
112
125
|
if (opening) this._refs.nav.focus();
|
|
113
126
|
}
|
|
127
|
+
/** The tab bar's "more": the other places are in the drawer. */
|
|
128
|
+
onMore() {
|
|
129
|
+
this.setNavOpen(true);
|
|
130
|
+
this._refs.nav.focus();
|
|
131
|
+
}
|
|
114
132
|
onKey(event) {
|
|
115
133
|
if (event.key !== "Escape" || !this.navOpen.get() || !this.isNarrow()) return;
|
|
116
134
|
event.preventDefault();
|
|
@@ -132,11 +150,20 @@ var JlAppShell = class extends JlElement {
|
|
|
132
150
|
const covered = this.navOpen.get() && this.isNarrow();
|
|
133
151
|
this._refs.main.inert = covered;
|
|
134
152
|
this._refs.aside.inert = covered;
|
|
153
|
+
this._refs.tabs.inert = covered;
|
|
135
154
|
}
|
|
136
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* What was slotted: empty areas take no room, a `select` from the nav (a side nav's) closes the drawer, and a
|
|
157
|
+
* `more` from the tabs (a tab bar's) opens it.
|
|
158
|
+
*/
|
|
137
159
|
renderSlots() {
|
|
138
160
|
this.reflect("has-header", this._refs.headerSlot.assignedElements().length > 0);
|
|
139
161
|
this.reflect("has-aside", this._refs.asideSlot.assignedElements().length > 0);
|
|
162
|
+
const tabElements = this._refs.tabsSlot.assignedElements();
|
|
163
|
+
this.reflect("has-tabs", tabElements.length > 0);
|
|
164
|
+
this._state.tabsListeners.abort();
|
|
165
|
+
this._state.tabsListeners = new AbortController();
|
|
166
|
+
for (const element of tabElements) element.addEventListener("more", () => this.onMore(), { signal: this._state.tabsListeners.signal });
|
|
140
167
|
const navElements = this._refs.navSlot.assignedElements();
|
|
141
168
|
this.reflect("has-nav", navElements.length > 0);
|
|
142
169
|
this._state.navListeners.abort();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** One choice of a chip group. */
|
|
2
|
+
export type ChipGroupOption = {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** An icon name before the label; a chosen chip without one shows a check. */
|
|
6
|
+
icon?: string;
|
|
7
|
+
/** A count or a short word after the label: how many things the choice holds. */
|
|
8
|
+
badge?: string | number;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
};
|
|
@@ -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;
|