@syware/jlib 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/all.js +5 -3
  2. package/dist/atoms/jl-app-icon.css_raw.js +3 -0
  3. package/dist/atoms/jl-app-icon.d.ts +29 -0
  4. package/dist/atoms/jl-app-icon.js +64 -0
  5. package/dist/atoms/jl-avatar.js +1 -1
  6. package/dist/atoms/jl-badge.js +8 -1
  7. package/dist/atoms/jl-button.js +1 -1
  8. package/dist/atoms/jl-input.js +1 -1
  9. package/dist/atoms/jl-link.js +1 -1
  10. package/dist/atoms/jl-select.js +1 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +4 -2
  13. package/dist/lib/text.d.ts +10 -0
  14. package/dist/lib/text.js +13 -1
  15. package/dist/lib/time.d.ts +5 -3
  16. package/dist/lib/time.js +23 -15
  17. package/dist/lib/trigger.js +1 -1
  18. package/dist/molecules/jl-account-button.css_raw.js +1 -1
  19. package/dist/molecules/jl-account-button.d.ts +8 -1
  20. package/dist/molecules/jl-account-button.js +8 -1
  21. package/dist/molecules/jl-alert.js +1 -1
  22. package/dist/molecules/jl-calendar.js +1 -1
  23. package/dist/molecules/jl-chip-group.js +1 -1
  24. package/dist/molecules/jl-combobox.js +1 -1
  25. package/dist/molecules/jl-date-picker.js +1 -1
  26. package/dist/molecules/jl-empty.js +1 -1
  27. package/dist/molecules/jl-file-drop.js +1 -1
  28. package/dist/molecules/jl-list-item.js +1 -1
  29. package/dist/molecules/jl-load-state.d.ts +4 -1
  30. package/dist/molecules/jl-load-state.js +14 -5
  31. package/dist/molecules/jl-menu.js +1 -1
  32. package/dist/molecules/jl-number-input.js +1 -1
  33. package/dist/molecules/jl-search-button.js +1 -1
  34. package/dist/molecules/jl-search.js +1 -1
  35. package/dist/molecules/jl-segmented.js +1 -1
  36. package/dist/molecules/jl-tabs.js +1 -1
  37. package/dist/molecules/jl-tag-input.js +1 -1
  38. package/dist/molecules/jl-timeline.js +1 -1
  39. package/dist/molecules/jl-tooltip.css_raw.js +1 -1
  40. package/dist/molecules/jl-tooltip.d.ts +29 -1
  41. package/dist/molecules/jl-tooltip.js +144 -5
  42. package/dist/organisms/jl-command-palette.css_raw.js +1 -1
  43. package/dist/organisms/jl-command-palette.js +1 -1
  44. package/dist/organisms/jl-confirm.d.ts +2 -1
  45. package/dist/organisms/jl-confirm.js +16 -8
  46. package/dist/organisms/jl-flow.js +1 -1
  47. package/dist/organisms/jl-side-nav.js +1 -1
  48. package/dist/organisms/jl-tab-bar.js +1 -1
  49. package/dist/organisms/jl-table.js +1 -1
  50. package/dist/organisms/jl-toaster.css_raw.js +1 -1
  51. package/dist/organisms/jl-tree.js +1 -1
  52. package/dist/styles/tokens.css_raw.js +1 -1
  53. package/dist/templates/jl-app-shell.d.ts +6 -1
  54. package/dist/templates/jl-app-shell.js +27 -2
  55. package/dist/templates/jl-auth-layout.css_raw.js +3 -0
  56. package/dist/templates/jl-auth-layout.d.ts +35 -0
  57. package/dist/templates/jl-auth-layout.js +139 -0
  58. package/dist/templates/jl-page.css_raw.js +1 -1
  59. package/dist/templates/jl-split-view.css_raw.js +1 -1
  60. package/dist/templates/jl-split-view.d.ts +21 -1
  61. package/dist/templates/jl-split-view.js +33 -1
  62. package/dist/tokens.css +2 -0
  63. package/dist/types/jl-split-view.types.d.ts +2 -0
  64. package/package.json +1 -1
@@ -3,8 +3,8 @@ import { JlFormControl } from "../base/jl-form-control.js";
3
3
  import { oneOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
5
  import { CONTROL_SIZES } from "../lib/options.js";
6
- import { setOrRemove } from "../lib/dom.js";
7
6
  import { JlIcon } from "../atoms/jl-icon.js";
7
+ import { setOrRemove } from "../lib/dom.js";
8
8
  import { clamp, parseNumber, stepValue } from "../lib/numeric.js";
9
9
  import jl_number_input_default from "./jl-number-input.css_raw.js";
10
10
  import { css } from "@jadis/core";
@@ -1,8 +1,8 @@
1
1
  import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { registerIcons } from "../lib/icons.js";
4
- import { setOrRemove } from "../lib/dom.js";
5
4
  import { JlIcon } from "../atoms/jl-icon.js";
5
+ import { setOrRemove } from "../lib/dom.js";
6
6
  import { JlKbd } from "../atoms/jl-kbd.js";
7
7
  import { parseShortcut, shortcutAria } from "../lib/key-shortcut.js";
8
8
  import jl_search_button_default from "./jl-search-button.css_raw.js";
@@ -3,8 +3,8 @@ import { JlFormControl } from "../base/jl-form-control.js";
3
3
  import { oneOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
5
  import { CONTROL_SIZES } from "../lib/options.js";
6
- import { setOrRemove } from "../lib/dom.js";
7
6
  import { JlIcon } from "../atoms/jl-icon.js";
7
+ import { setOrRemove } from "../lib/dom.js";
8
8
  import { JlSpinner } from "../atoms/jl-spinner.js";
9
9
  import { hasCommandModifier, matchesShortcut, parseShortcut, shortcutAria, shortcutLabels } from "../lib/key-shortcut.js";
10
10
  import jl_search_default from "./jl-search.css_raw.js";
@@ -2,8 +2,8 @@ import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlFormControl } from "../base/jl-form-control.js";
3
3
  import { jsonOf, oneOf } from "../lib/attributes.js";
4
4
  import { CONTROL_SIZES } from "../lib/options.js";
5
- import { setOrRemove } from "../lib/dom.js";
6
5
  import { JlIcon } from "../atoms/jl-icon.js";
6
+ import { setOrRemove } from "../lib/dom.js";
7
7
  import { firstIndex, lastIndex, stepIndex } from "../lib/roving.js";
8
8
  import { moveIndicator } from "../lib/indicator.js";
9
9
  import jl_segmented_default from "./jl-segmented.css_raw.js";
@@ -2,8 +2,8 @@ import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { oneOf } from "../lib/attributes.js";
4
4
  import { define } from "../lib/define.js";
5
- import { setOrRemove, uniqueId } from "../lib/dom.js";
6
5
  import { JlIcon } from "../atoms/jl-icon.js";
6
+ import { setOrRemove, uniqueId } from "../lib/dom.js";
7
7
  import { firstIndex, lastIndex, stepIndex } from "../lib/roving.js";
8
8
  import { moveIndicator } from "../lib/indicator.js";
9
9
  import { JlTabPanel } from "./jl-tab-panel.js";
@@ -4,8 +4,8 @@ import { clampedNumber, jsonOf, oneOf } from "../lib/attributes.js";
4
4
  import { followAnchor } from "../lib/floating.js";
5
5
  import { registerIcons } from "../lib/icons.js";
6
6
  import { CONTROL_SIZES } from "../lib/options.js";
7
- import { setOrRemove, uniqueId } from "../lib/dom.js";
8
7
  import { JlIcon } from "../atoms/jl-icon.js";
8
+ import { setOrRemove, uniqueId } from "../lib/dom.js";
9
9
  import { keepInView } from "../lib/keep-in-view.js";
10
10
  import { filterOptions, matchParts, nextEnabledIndex } from "../lib/option-filter.js";
11
11
  import { addTags, hasTag, hasTagSeparator, splitTags } from "../lib/tags.js";
@@ -1,8 +1,8 @@
1
1
  import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { jsonOf } from "../lib/attributes.js";
4
- import { setOrRemove } from "../lib/dom.js";
5
4
  import { JlIcon } from "../atoms/jl-icon.js";
5
+ import { setOrRemove } from "../lib/dom.js";
6
6
  import jl_timeline_default from "./jl-timeline.css_raw.js";
7
7
  import { css } from "@jadis/core";
8
8
  import { jsx, jsxs } from "@jadis/core/jsx-runtime";
@@ -1,3 +1,3 @@
1
1
  // src/molecules/jl-tooltip.css?raw
2
- var jl_tooltip_default = ":host {\n display: inline-flex;\n vertical-align: middle;\n}\n\n/* Ink on the page's ground: dark on a light theme, light on a dark one. */\n.bubble {\n max-width: min(260px, calc(100vw - 16px));\n padding: 6px 9px;\n border-radius: var(--jl-radius-xs);\n background: var(--jl-text);\n color: var(--jl-bg);\n font-size: var(--jl-font-xs);\n font-weight: 550;\n line-height: 1.4;\n box-shadow: var(--jl-shadow);\n pointer-events: auto;\n}\n\n.arrow {\n position: absolute;\n width: 8px;\n height: 8px;\n border-radius: 1px;\n background: inherit;\n rotate: 45deg;\n}\n\n.bubble[data-placement^=\"top\"] .arrow {\n top: 100%;\n left: var(--arrow, 50%);\n translate: -50% -50%;\n}\n\n.bubble[data-placement^=\"bottom\"] .arrow {\n bottom: 100%;\n left: var(--arrow, 50%);\n translate: -50% 50%;\n}\n\n.bubble[data-placement^=\"left\"] .arrow {\n top: var(--arrow, 50%);\n left: 100%;\n translate: -50% -50%;\n}\n\n.bubble[data-placement^=\"right\"] .arrow {\n top: var(--arrow, 50%);\n right: 100%;\n translate: 50% -50%;\n}\n";
2
+ var jl_tooltip_default = ":host {\n display: inline-flex;\n vertical-align: middle;\n /* A long press shows the bubble: no callout over it, and (below) no text selected under the finger. */\n -webkit-touch-callout: none;\n}\n\n@media (pointer: coarse) {\n ::slotted(*) {\n -webkit-user-select: none;\n user-select: none;\n }\n}\n\n/* Ink on the page's ground: dark on a light theme, light on a dark one. */\n.bubble {\n max-width: min(260px, calc(100vw - 16px));\n padding: 6px 9px;\n border-radius: var(--jl-radius-xs);\n background: var(--jl-text);\n color: var(--jl-bg);\n font-size: var(--jl-font-xs);\n font-weight: 550;\n line-height: 1.4;\n box-shadow: var(--jl-shadow);\n pointer-events: auto;\n}\n\n.arrow {\n position: absolute;\n width: 8px;\n height: 8px;\n border-radius: 1px;\n background: inherit;\n rotate: 45deg;\n}\n\n.bubble[data-placement^=\"top\"] .arrow {\n top: 100%;\n left: var(--arrow, 50%);\n translate: -50% -50%;\n}\n\n.bubble[data-placement^=\"bottom\"] .arrow {\n bottom: 100%;\n left: var(--arrow, 50%);\n translate: -50% 50%;\n}\n\n.bubble[data-placement^=\"left\"] .arrow {\n top: var(--arrow, 50%);\n left: 100%;\n translate: -50% -50%;\n}\n\n.bubble[data-placement^=\"right\"] .arrow {\n top: var(--arrow, 50%);\n right: 100%;\n translate: 50% -50%;\n}\n";
3
3
  export { jl_tooltip_default as default };
@@ -2,7 +2,13 @@ import { JlElement } from '../base/jl-element';
2
2
  import type { Placement } from '../types/floating.types';
3
3
  /**
4
4
  * A short hint in a dark bubble by its trigger (the slotted element): it shows on hover and on keyboard focus after
5
- * a `delay`, and hides on leave, blur and Escape. The trigger gets the text as its accessible description.
5
+ * a `delay`, and hides on leave, blur and Escape. The trigger gets the text as its accessible description. While
6
+ * the pointer or the focus is on it, the trigger's own `title` (a button's `hint`) is set aside, so the browser's
7
+ * tooltip does not show over the bubble.
8
+ *
9
+ * Touch screens have no hover: pressing and holding a trigger shows the bubble (and the press does not act), and
10
+ * it stays a moment after the finger lifts; a trigger that does nothing on a tap (a word, an icon) shows or hides
11
+ * it on a tap.
6
12
  */
7
13
  export declare class JlTooltip extends JlElement {
8
14
  static readonly selector = "jl-tooltip";
@@ -31,6 +37,28 @@ export declare class JlTooltip extends JlElement {
31
37
  /** Hides the bubble now. */
32
38
  hide(): void;
33
39
  private close;
40
+ /** A trigger that acts on a tap (a button, a link, a field), as opposed to a word or an icon. */
41
+ private isInteractive;
42
+ private pressStart;
43
+ private pressMove;
44
+ /** The finger lifted. On a trigger that does nothing else on a tap, a tap (not a scroll) shows or hides the bubble. */
45
+ private pressUp;
46
+ /** The finger is gone: a bubble the long press showed stays a moment, then goes. */
47
+ private pressEnd;
48
+ /** The system's own long press: the bubble shows now, and the menu it would open does not, but over a link. */
49
+ private pressHeld;
50
+ /** The long press shows the bubble, and says whether there is one: without a text, the press stays a press. */
51
+ private openFromPress;
52
+ private swallowLongPress;
53
+ private hideBubble;
54
+ /**
55
+ * Sets aside the native `title` of the trigger and of its inner control: the bubble says it, and the browser's
56
+ * own tooltip would show over it a moment later. The control keeps its name for screen readers (a button's hint
57
+ * is its `aria-label` too).
58
+ */
59
+ private holdTitles;
60
+ /** Gives the titles back, unless the trigger wrote new ones meanwhile. */
61
+ private restoreTitles;
34
62
  private trigger;
35
63
  private isOpen;
36
64
  private scheduleShow;
@@ -14,10 +14,22 @@ var HIDE_GRACE_MS = 120;
14
14
  var GAP = 9;
15
15
  var ARROW_MARGIN = 10;
16
16
  var DESCRIPTION = "aria-description";
17
+ var TITLE = "title";
18
+ var TOUCH = "touch";
19
+ var LONG_PRESS_MS = 500;
20
+ var MOVE_TOLERANCE_PX = 10;
21
+ var TOUCH_LINGER_MS = 1500;
22
+ var INTERACTIVE = "a[href], button, input, select, textarea, summary, [role=\"button\"], [role=\"link\"]";
17
23
  var sideOf = (placement) => placement.split("-")[0];
18
24
  /**
19
25
  * A short hint in a dark bubble by its trigger (the slotted element): it shows on hover and on keyboard focus after
20
- * a `delay`, and hides on leave, blur and Escape. The trigger gets the text as its accessible description.
26
+ * a `delay`, and hides on leave, blur and Escape. The trigger gets the text as its accessible description. While
27
+ * the pointer or the focus is on it, the trigger's own `title` (a button's `hint`) is set aside, so the browser's
28
+ * tooltip does not show over the bubble.
29
+ *
30
+ * Touch screens have no hover: pressing and holding a trigger shows the bubble (and the press does not act), and
31
+ * it stays a moment after the finger lifts; a trigger that does nothing on a tap (a word, an icon) shows or hides
32
+ * it on a tap.
21
33
  */
22
34
  var JlTooltip = class extends JlElement {
23
35
  static selector = "jl-tooltip";
@@ -35,7 +47,16 @@ var JlTooltip = class extends JlElement {
35
47
  _state = {
36
48
  described: null,
37
49
  following: new AbortController(),
50
+ heldTitles: [],
38
51
  hideTimer: 0,
52
+ press: {
53
+ down: false,
54
+ moved: false,
55
+ opened: false,
56
+ timer: 0,
57
+ x: 0,
58
+ y: 0
59
+ },
39
60
  showTimer: 0
40
61
  };
41
62
  _refs = this.useRefs((ref) => ({
@@ -58,11 +79,34 @@ var JlTooltip = class extends JlElement {
58
79
  onConnect() {
59
80
  this.describeTrigger();
60
81
  this.on(this._refs.slot, "slotchange", () => this.describeTrigger());
61
- this.on(this, "pointerenter", () => this.scheduleShow());
62
- this.on(this, "pointerleave", () => this.scheduleHide());
63
- this.on(this, "pointerdown", () => this.close());
82
+ this.on(this, "pointerenter", (event) => {
83
+ if (event.pointerType === TOUCH) return;
84
+ this.holdTitles();
85
+ this.scheduleShow();
86
+ });
87
+ this.on(this, "pointerleave", (event) => {
88
+ if (event.pointerType !== TOUCH) this.scheduleHide();
89
+ });
90
+ this.on(this, "pointerdown", (event) => {
91
+ if (event.pointerType === TOUCH) {
92
+ this.pressStart(event);
93
+ return;
94
+ }
95
+ this.hideBubble();
96
+ });
97
+ this.on(this, "pointermove", (event) => this.pressMove(event));
98
+ this.on(this, "pointerup", () => this.pressUp());
99
+ this.on(this, "pointercancel", () => this.pressEnd());
100
+ this.on(this, "contextmenu", (event) => this.pressHeld(event));
101
+ this.addEventListener("click", (event) => this.swallowLongPress(event), {
102
+ capture: true,
103
+ signal: this.killSignal
104
+ });
64
105
  this.on(this, "focusin", () => {
65
- if (deepActiveElement(document)?.matches(":focus-visible") === true) this.scheduleShow();
106
+ if (deepActiveElement(document)?.matches(":focus-visible") === true) {
107
+ this.holdTitles();
108
+ this.scheduleShow();
109
+ }
66
110
  });
67
111
  this.on(this, "focusout", () => this.close());
68
112
  }
@@ -78,12 +122,104 @@ var JlTooltip = class extends JlElement {
78
122
  this.whenRendered(() => this.close());
79
123
  }
80
124
  close() {
125
+ this.hideBubble();
126
+ this.restoreTitles();
127
+ }
128
+ /** A trigger that acts on a tap (a button, a link, a field), as opposed to a word or an icon. */
129
+ isInteractive() {
130
+ return accessibleTarget(this.trigger()).matches(INTERACTIVE);
131
+ }
132
+ pressStart(event) {
133
+ const press = this._state.press;
134
+ window.clearTimeout(press.timer);
135
+ press.down = true;
136
+ press.moved = false;
137
+ press.opened = false;
138
+ press.x = event.clientX;
139
+ press.y = event.clientY;
140
+ if (this.isInteractive()) press.timer = window.setTimeout(() => this.openFromPress(), LONG_PRESS_MS);
141
+ }
142
+ pressMove(event) {
143
+ const press = this._state.press;
144
+ const moved = Math.hypot(event.clientX - press.x, event.clientY - press.y);
145
+ if (press.down && moved > MOVE_TOLERANCE_PX) {
146
+ press.moved = true;
147
+ window.clearTimeout(press.timer);
148
+ }
149
+ }
150
+ /** The finger lifted. On a trigger that does nothing else on a tap, a tap (not a scroll) shows or hides the bubble. */
151
+ pressUp() {
152
+ const press = this._state.press;
153
+ const tapped = press.down && !press.moved;
154
+ this.pressEnd();
155
+ if (!tapped || this.isInteractive()) return;
156
+ if (this.isOpen()) {
157
+ this.close();
158
+ return;
159
+ }
160
+ this.open();
161
+ }
162
+ /** The finger is gone: a bubble the long press showed stays a moment, then goes. */
163
+ pressEnd() {
164
+ const press = this._state.press;
165
+ window.clearTimeout(press.timer);
166
+ press.down = false;
167
+ if (!press.opened) return;
168
+ window.clearTimeout(this._state.hideTimer);
169
+ this._state.hideTimer = window.setTimeout(() => this.close(), TOUCH_LINGER_MS);
170
+ }
171
+ /** The system's own long press: the bubble shows now, and the menu it would open does not, but over a link. */
172
+ pressHeld(event) {
173
+ if (!this._state.press.down || !this.isInteractive()) return;
174
+ if (this.openFromPress() && !accessibleTarget(this.trigger()).matches("a[href]")) event.preventDefault();
175
+ }
176
+ /** The long press shows the bubble, and says whether there is one: without a text, the press stays a press. */
177
+ openFromPress() {
178
+ window.clearTimeout(this._state.press.timer);
179
+ if (this.text.get() === "") return false;
180
+ this._state.press.opened = true;
181
+ this.open();
182
+ return true;
183
+ }
184
+ swallowLongPress(event) {
185
+ const press = this._state.press;
186
+ if (!press.opened) return;
187
+ press.opened = false;
188
+ event.preventDefault();
189
+ event.stopPropagation();
190
+ }
191
+ hideBubble() {
192
+ window.clearTimeout(this._state.press.timer);
81
193
  window.clearTimeout(this._state.showTimer);
82
194
  window.clearTimeout(this._state.hideTimer);
83
195
  this._state.following.abort();
84
196
  const bubble = this._refs.bubble;
85
197
  if (bubble.matches(":popover-open")) bubble.hidePopover();
86
198
  }
199
+ /**
200
+ * Sets aside the native `title` of the trigger and of its inner control: the bubble says it, and the browser's
201
+ * own tooltip would show over it a moment later. The control keeps its name for screen readers (a button's hint
202
+ * is its `aria-label` too).
203
+ */
204
+ holdTitles() {
205
+ if (this.text.get() === "" || this._state.heldTitles.length > 0) return;
206
+ const trigger = this.trigger();
207
+ const elements = [.../* @__PURE__ */ new Set([trigger, accessibleTarget(trigger)])];
208
+ this._state.heldTitles = elements.flatMap((element) => {
209
+ const title = element.getAttribute(TITLE);
210
+ if (title === null) return [];
211
+ element.removeAttribute(TITLE);
212
+ return [{
213
+ element,
214
+ title
215
+ }];
216
+ });
217
+ }
218
+ /** Gives the titles back, unless the trigger wrote new ones meanwhile. */
219
+ restoreTitles() {
220
+ for (const { element, title } of this._state.heldTitles) if (!element.hasAttribute(TITLE)) element.setAttribute(TITLE, title);
221
+ this._state.heldTitles = [];
222
+ }
87
223
  trigger() {
88
224
  return this._refs.slot.assignedElements({ flatten: true })[0] ?? this;
89
225
  }
@@ -117,6 +253,9 @@ var JlTooltip = class extends JlElement {
117
253
  passive: true,
118
254
  signal: following.signal
119
255
  });
256
+ document.addEventListener("pointerdown", (event) => {
257
+ if (!event.composedPath().includes(this)) this.close();
258
+ }, { signal: following.signal });
120
259
  document.addEventListener("keydown", (event) => {
121
260
  if (event.key === "Escape") this.close();
122
261
  }, { signal: following.signal });
@@ -1,3 +1,3 @@
1
1
  // src/organisms/jl-command-palette.css?raw
2
- var jl_command_palette_default = ":host {\n display: contents;\n}\n\ndialog {\n flex-direction: column;\n width: min(620px, calc(100vw - 24px));\n max-width: none;\n max-height: min(520px, calc(100dvh - 24vh));\n margin: 12vh auto auto;\n padding: 0;\n overflow: hidden;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius-lg);\n background: var(--jl-surface);\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n box-shadow: var(--jl-shadow-lg);\n}\n\ndialog[open] {\n display: flex;\n animation: drop var(--jl-fast) var(--jl-ease);\n}\n\ndialog::backdrop {\n background: var(--jl-scrim);\n backdrop-filter: blur(2px);\n}\n\n.search {\n display: flex;\n flex: none;\n align-items: center;\n gap: 10px;\n height: 52px;\n padding: 0 14px 0 16px;\n border-bottom: 1px solid var(--jl-line);\n}\n\n.search-icon {\n color: var(--jl-faint);\n}\n\ninput {\n flex: 1;\n min-width: 0;\n height: 100%;\n padding: 0;\n border: none;\n outline: none;\n background: none;\n font-size: 15px;\n}\n\ninput::placeholder {\n color: var(--jl-faint);\n}\n\ninput:focus-visible {\n box-shadow: none;\n}\n\nkbd {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 20px;\n height: 20px;\n padding: 0 5px;\n border: 1px solid var(--jl-line-strong);\n border-bottom-width: 2px;\n border-radius: var(--jl-radius-xs);\n background: var(--jl-surface);\n color: var(--jl-muted);\n font-size: 11px;\n font-weight: 500;\n line-height: 1;\n}\n\n.results {\n flex: 1;\n min-height: 0;\n padding: 6px;\n overflow-y: auto;\n overscroll-behavior: contain;\n scroll-padding: 6px;\n}\n\n[role=\"group\"] + [role=\"group\"] {\n margin-top: 4px;\n padding-top: 4px;\n border-top: 1px solid var(--jl-line-soft);\n}\n\n.group-label {\n padding: 8px 10px 4px;\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n font-weight: 600;\n}\n\n.option {\n display: flex;\n align-items: center;\n gap: 10px;\n min-height: 38px;\n padding: 0 10px;\n border-radius: var(--jl-radius-sm);\n color: var(--jl-text-2);\n cursor: pointer;\n}\n\n.option[aria-selected=\"true\"] {\n background: var(--jl-accent-soft);\n color: var(--jl-text);\n}\n\n.option-icon {\n flex: none;\n width: 16px;\n color: var(--jl-muted);\n}\n\n.option[aria-selected=\"true\"] .option-icon {\n color: var(--jl-accent);\n}\n\n.option-label {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\nmark {\n background: none;\n color: var(--jl-accent);\n font-weight: 700;\n}\n\n.keys {\n display: inline-flex;\n flex: none;\n gap: 3px;\n}\n\n.empty {\n padding: 36px 16px;\n color: var(--jl-muted);\n text-align: center;\n}\n\nfooter {\n display: flex;\n flex: none;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px 16px;\n padding: 8px 16px;\n border-top: 1px solid var(--jl-line);\n background: var(--jl-surface-2);\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n}\n\n.hint,\n.hotkey-keys {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n}\n\n.hotkey-hint {\n margin-left: auto;\n}\n\n@media (width < 520px) {\n dialog {\n margin-top: 12px;\n max-height: calc(100dvh - 24px);\n }\n\n .hotkey-hint {\n display: none;\n }\n}\n\n@keyframes drop {\n from {\n opacity: 0;\n transform: translateY(-6px) scale(0.98);\n }\n}\n";
2
+ var jl_command_palette_default = ":host {\n display: contents;\n}\n\ndialog {\n flex-direction: column;\n width: min(620px, calc(100vw - 24px));\n max-width: none;\n max-height: min(520px, calc(100dvh - 24vh));\n margin: 12vh auto auto;\n padding: 0;\n overflow: hidden;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius-lg);\n background: var(--jl-surface);\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n box-shadow: var(--jl-shadow-lg);\n}\n\ndialog[open] {\n display: flex;\n animation: drop var(--jl-fast) var(--jl-ease);\n}\n\ndialog::backdrop {\n background: var(--jl-scrim);\n backdrop-filter: blur(2px);\n}\n\n.search {\n display: flex;\n flex: none;\n align-items: center;\n gap: 10px;\n height: 52px;\n padding: 0 14px 0 16px;\n border-bottom: 1px solid var(--jl-line);\n}\n\n.search-icon {\n color: var(--jl-faint);\n}\n\ninput {\n flex: 1;\n min-width: 0;\n height: 100%;\n padding: 0;\n border: none;\n outline: none;\n background: none;\n /* At least the text of fields: 16px on touch screens, where phones zoom into smaller text. */\n font-size: max(15px, var(--jl-control-font));\n}\n\ninput::placeholder {\n color: var(--jl-faint);\n}\n\ninput:focus-visible {\n box-shadow: none;\n}\n\nkbd {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 20px;\n height: 20px;\n padding: 0 5px;\n border: 1px solid var(--jl-line-strong);\n border-bottom-width: 2px;\n border-radius: var(--jl-radius-xs);\n background: var(--jl-surface);\n color: var(--jl-muted);\n font-size: 11px;\n font-weight: 500;\n line-height: 1;\n}\n\n.results {\n flex: 1;\n min-height: 0;\n padding: 6px;\n overflow-y: auto;\n overscroll-behavior: contain;\n scroll-padding: 6px;\n}\n\n[role=\"group\"] + [role=\"group\"] {\n margin-top: 4px;\n padding-top: 4px;\n border-top: 1px solid var(--jl-line-soft);\n}\n\n.group-label {\n padding: 8px 10px 4px;\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n font-weight: 600;\n}\n\n.option {\n display: flex;\n align-items: center;\n gap: 10px;\n min-height: 38px;\n padding: 0 10px;\n border-radius: var(--jl-radius-sm);\n color: var(--jl-text-2);\n cursor: pointer;\n}\n\n.option[aria-selected=\"true\"] {\n background: var(--jl-accent-soft);\n color: var(--jl-text);\n}\n\n.option-icon {\n flex: none;\n width: 16px;\n color: var(--jl-muted);\n}\n\n.option[aria-selected=\"true\"] .option-icon {\n color: var(--jl-accent);\n}\n\n.option-label {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\nmark {\n background: none;\n color: var(--jl-accent);\n font-weight: 700;\n}\n\n.keys {\n display: inline-flex;\n flex: none;\n gap: 3px;\n}\n\n.empty {\n padding: 36px 16px;\n color: var(--jl-muted);\n text-align: center;\n}\n\nfooter {\n display: flex;\n flex: none;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px 16px;\n padding: 8px 16px;\n border-top: 1px solid var(--jl-line);\n background: var(--jl-surface-2);\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n}\n\n.hint,\n.hotkey-keys {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n}\n\n.hotkey-hint {\n margin-left: auto;\n}\n\n@media (width < 520px) {\n dialog {\n margin-top: 12px;\n max-height: calc(100dvh - 24px);\n }\n\n .hotkey-hint {\n display: none;\n }\n}\n\n@keyframes drop {\n from {\n opacity: 0;\n transform: translateY(-6px) scale(0.98);\n }\n}\n";
3
3
  export { jl_command_palette_default as default };
@@ -2,8 +2,8 @@ import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { jsonOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
- import { setOrRemove } from "../lib/dom.js";
6
5
  import { JlIcon } from "../atoms/jl-icon.js";
6
+ import { setOrRemove } from "../lib/dom.js";
7
7
  import { highlightSegments } from "../lib/fuzzy.js";
8
8
  import { hotkeyLabels, isApplePlatform, matchesHotkey, parseHotkey } from "../lib/hotkey.js";
9
9
  import { groupMatches, matchCommands } from "../lib/palette.js";
@@ -57,6 +57,7 @@ export declare function confirmDialog(options: ConfirmOptions): Promise<boolean>
57
57
  /**
58
58
  * Guards a form or an editor against leaving with changes not saved. Closing or reloading the tab asks the
59
59
  * browser's own question; the function it returns asks in the page, for the app's router to call before it moves
60
- * on: `if (!(await canLeave())) return;`. It resolves `true` at once when nothing would be lost.
60
+ * on: `if (!(await canLeave())) return;`. It resolves `true` at once when nothing would be lost, or once `signal`
61
+ * ended the guard.
61
62
  */
62
63
  export declare function guardLeave(options: LeaveGuardOptions): () => Promise<boolean>;
@@ -136,18 +136,26 @@ var STAY_LABEL = "Keep editing";
136
136
  /**
137
137
  * Guards a form or an editor against leaving with changes not saved. Closing or reloading the tab asks the
138
138
  * browser's own question; the function it returns asks in the page, for the app's router to call before it moves
139
- * on: `if (!(await canLeave())) return;`. It resolves `true` at once when nothing would be lost.
139
+ * on: `if (!(await canLeave())) return;`. It resolves `true` at once when nothing would be lost, or once `signal`
140
+ * ended the guard.
140
141
  */
141
142
  function guardLeave(options) {
142
143
  window.addEventListener("beforeunload", (event) => {
143
144
  if (options.isDirty()) event.preventDefault();
144
145
  }, { signal: options.signal });
145
- return async () => !options.isDirty() || confirmDialog({
146
- cancelLabel: options.stayLabel ?? STAY_LABEL,
147
- confirmLabel: options.leaveLabel ?? LEAVE_LABEL,
148
- heading: options.heading ?? LEAVE_HEADING,
149
- text: options.text ?? LEAVE_TEXT,
150
- tone: "danger"
151
- });
146
+ const asking = { pending: null };
147
+ return () => {
148
+ if (options.signal.aborted || !options.isDirty()) return Promise.resolve(true);
149
+ asking.pending ??= confirmDialog({
150
+ cancelLabel: options.stayLabel ?? STAY_LABEL,
151
+ confirmLabel: options.leaveLabel ?? LEAVE_LABEL,
152
+ heading: options.heading ?? LEAVE_HEADING,
153
+ text: options.text ?? LEAVE_TEXT,
154
+ tone: "danger"
155
+ }).finally(() => {
156
+ asking.pending = null;
157
+ });
158
+ return asking.pending;
159
+ };
152
160
  }
153
161
  export { CONFIRM_TONES, JlConfirm, confirmDialog, guardLeave };
@@ -2,8 +2,8 @@ import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { jsonOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
- import { setOrRemove, svgElement } from "../lib/dom.js";
6
5
  import { JlIcon } from "../atoms/jl-icon.js";
6
+ import { setOrRemove, svgElement } from "../lib/dom.js";
7
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";
@@ -2,8 +2,8 @@ import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { jsonOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
- import { setOrRemove, uniqueId } from "../lib/dom.js";
6
5
  import { JlIcon } from "../atoms/jl-icon.js";
6
+ import { setOrRemove, uniqueId } from "../lib/dom.js";
7
7
  import { findItem, firstLeaf, pathInSections } from "../lib/nav-tree.js";
8
8
  import jl_side_nav_default from "./jl-side-nav.css_raw.js";
9
9
  import { css } from "@jadis/core";
@@ -2,8 +2,8 @@ import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { jsonOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
- import { setOrRemove } from "../lib/dom.js";
6
5
  import { JlIcon } from "../atoms/jl-icon.js";
6
+ import { setOrRemove } from "../lib/dom.js";
7
7
  import jl_tab_bar_default from "./jl-tab-bar.css_raw.js";
8
8
  import { css } from "@jadis/core";
9
9
  import { Ellipsis } from "lucide";
@@ -2,8 +2,8 @@ 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 { setOrRemove } from "../lib/dom.js";
6
5
  import { JlIcon } from "../atoms/jl-icon.js";
6
+ import { setOrRemove } from "../lib/dom.js";
7
7
  import { JlBadge } from "../atoms/jl-badge.js";
8
8
  import { columnAlign, columnWidth, formatCell, nextSortDirection, rowKeyOf, selectionState, sortRows, toggleAll, toggleKey } from "../lib/table.js";
9
9
  import jl_table_default from "./jl-table.css_raw.js";
@@ -1,3 +1,3 @@
1
1
  // src/organisms/jl-toaster.css?raw
2
- var jl_toaster_default = ":host {\n display: contents;\n --edge: 16px;\n --width: 380px;\n}\n\n.region {\n position: fixed;\n inset: auto;\n display: flex;\n flex-direction: column;\n gap: 10px;\n width: min(var(--width), calc(100vw - 2 * var(--edge)));\n margin: 0;\n padding: 0;\n overflow: visible;\n border: none;\n background: none;\n color: var(--jl-text);\n /* The empty space between toasts lets clicks through to the page. */\n pointer-events: none;\n}\n\n.region:not(:popover-open) {\n display: none;\n}\n\n::slotted(*) {\n pointer-events: auto;\n}\n\n::slotted([data-stacked]) {\n display: none;\n}\n\n:host([placement=\"bottom-right\"]) .region {\n right: var(--edge);\n bottom: var(--edge);\n}\n\n:host([placement=\"bottom-left\"]) .region {\n bottom: var(--edge);\n left: var(--edge);\n}\n\n:host([placement=\"bottom-center\"]) .region {\n bottom: var(--edge);\n left: 50%;\n translate: -50% 0;\n}\n\n/* At the top, the newest toast is the highest one, and toasts come in from above. */\n:host([placement^=\"top\"]) .region {\n top: var(--edge);\n flex-direction: column-reverse;\n}\n\n:host([placement^=\"top\"]) ::slotted(*) {\n --jl-toast-enter: -8px;\n}\n\n:host([placement=\"top-right\"]) .region {\n right: var(--edge);\n}\n\n:host([placement=\"top-center\"]) .region {\n left: 50%;\n translate: -50% 0;\n}\n";
2
+ var jl_toaster_default = ":host {\n display: contents;\n --edge: 16px;\n --width: 380px;\n}\n\n/* The bottom placements keep above the tab bar of an app shell (--jl-shell-bottom), or --edge-bottom when given. */\n.region {\n --bottom: var(--edge-bottom, calc(var(--edge) + var(--jl-shell-bottom, 0px)));\n}\n\n.region {\n position: fixed;\n inset: auto;\n display: flex;\n flex-direction: column;\n gap: 10px;\n width: min(var(--width), calc(100vw - 2 * var(--edge)));\n margin: 0;\n padding: 0;\n overflow: visible;\n border: none;\n background: none;\n color: var(--jl-text);\n /* The empty space between toasts lets clicks through to the page. */\n pointer-events: none;\n}\n\n.region:not(:popover-open) {\n display: none;\n}\n\n::slotted(*) {\n pointer-events: auto;\n}\n\n::slotted([data-stacked]) {\n display: none;\n}\n\n:host([placement=\"bottom-right\"]) .region {\n right: var(--edge);\n bottom: var(--bottom);\n}\n\n:host([placement=\"bottom-left\"]) .region {\n bottom: var(--bottom);\n left: var(--edge);\n}\n\n:host([placement=\"bottom-center\"]) .region {\n bottom: var(--bottom);\n left: 50%;\n translate: -50% 0;\n}\n\n/* At the top, the newest toast is the highest one, and toasts come in from above. */\n:host([placement^=\"top\"]) .region {\n top: var(--edge);\n flex-direction: column-reverse;\n}\n\n:host([placement^=\"top\"]) ::slotted(*) {\n --jl-toast-enter: -8px;\n}\n\n:host([placement=\"top-right\"]) .region {\n right: var(--edge);\n}\n\n:host([placement=\"top-center\"]) .region {\n left: 50%;\n translate: -50% 0;\n}\n";
3
3
  export { jl_toaster_default as default };
@@ -2,8 +2,8 @@ import { BASE_STYLES } from "../styles/shared.js";
2
2
  import { JlElement } from "../base/jl-element.js";
3
3
  import { jsonOf } from "../lib/attributes.js";
4
4
  import { registerIcons } from "../lib/icons.js";
5
- import { setOrRemove, uniqueId } from "../lib/dom.js";
6
5
  import { JlIcon } from "../atoms/jl-icon.js";
6
+ import { setOrRemove, uniqueId } from "../lib/dom.js";
7
7
  import { allItems, isFolder, treeKeyAction, withExpanded } from "../lib/tree.js";
8
8
  import jl_tree_default from "./jl-tree.css_raw.js";
9
9
  import { css } from "@jadis/core";
@@ -1,3 +1,3 @@
1
1
  // src/styles/tokens.css?raw
2
- var tokens_default = "/*\n * The design tokens, as swai and swcode have them: ink and white, flat surfaces separated by lines, and one blue\n * that marks what is live, selected or waiting for a person. Custom properties cross shadow roots, so every\n * component reads them. They sit in the `jlib` layer: any unlayered rule of the page overrides them.\n */\n@layer jlib {\n :root,\n [data-theme=\"light\"] {\n color-scheme: light;\n --jl-font-sans: \"Manrope Variable\", \"Manrope\", system-ui, -apple-system, \"Segoe UI\", sans-serif;\n --jl-font-mono: \"JetBrains Mono Variable\", \"JetBrains Mono\", ui-monospace, \"SF Mono\", Menlo, monospace;\n\n --jl-font-xs: 12px;\n --jl-font-sm: 13px;\n --jl-font-md: 14px;\n --jl-font-lg: 16px;\n --jl-font-xl: 20px;\n --jl-font-2xl: 28px;\n\n --jl-text: #0b0b0c;\n --jl-text-2: #3a3f47;\n /* Secondary text: 6.2:1 on white. */\n --jl-muted: #5b616b;\n /* Icons and decorations only: 3.2:1 on white is enough for them, not for text. */\n --jl-faint: #8a9099;\n\n --jl-bg: #ffffff;\n --jl-surface: #ffffff;\n --jl-surface-2: #f7f8f9;\n --jl-surface-3: #eef0f2;\n --jl-code-bg: #f0f2f4;\n --jl-line: #e6e8eb;\n --jl-line-soft: #eef0f2;\n --jl-line-strong: #d5d9de;\n\n /* The SyWare logo's blue: marks, focus rings, progress. 3.8:1 on white: enough for shapes, never for text. */\n --jl-brand: #0e88d3;\n /* The same hue, deep enough to carry text: buttons, links, the selection. 5.2:1 on white. */\n --jl-accent: #0c72b0;\n --jl-accent-strong: #095a8b;\n --jl-accent-soft: #e9f4fb;\n --jl-accent-line: #b7dbf2;\n --jl-on-accent: #ffffff;\n\n --jl-ok: #17784a;\n --jl-ok-soft: #ecf6f0;\n --jl-warn: #9a5b00;\n --jl-warn-soft: #fcf4e6;\n --jl-danger: #b42318;\n --jl-danger-soft: #fdf0ef;\n --jl-on-danger: #ffffff;\n --jl-info: #0c72b0;\n --jl-info-soft: #e9f4fb;\n\n /* Code: the accent for keywords, quiet colours from the same family for the rest. */\n --jl-code-keyword: #0c72b0;\n --jl-code-tag: #095a8b;\n --jl-code-attribute: #5b48c8;\n --jl-code-string: #17784a;\n --jl-code-number: #9a5b00;\n --jl-code-function: #3d5a80;\n --jl-code-property: #127a72;\n --jl-code-comment: #8a9099;\n\n --jl-radius-xs: 6px;\n --jl-radius-sm: 8px;\n --jl-radius: 10px;\n --jl-radius-lg: 12px;\n --jl-radius-xl: 20px;\n --jl-radius-pill: 999px;\n\n /* Spacing: a 4px grid, for the layouts pages build around the components. */\n --jl-space-1: 4px;\n --jl-space-2: 8px;\n --jl-space-3: 12px;\n --jl-space-4: 16px;\n --jl-space-5: 20px;\n --jl-space-6: 24px;\n --jl-space-8: 32px;\n --jl-space-10: 40px;\n --jl-space-12: 48px;\n --jl-space-16: 64px;\n\n --jl-control-sm: 30px;\n --jl-control-md: 36px;\n --jl-control-lg: 44px;\n /* The text people type in fields. */\n --jl-control-font: var(--jl-font-md);\n\n /* The widest a page gets: a form, most pages, and the tables and grids that need the room. */\n --jl-page-form: 720px;\n --jl-page-standard: 1120px;\n --jl-page-wide: 1400px;\n\n /* Flat: lines separate; only what floats (menus, dialogs, toasts) casts a shadow. */\n --jl-shadow-sm: 0 1px 2px rgb(11 11 12 / 0.05);\n --jl-shadow: 0 6px 16px -6px rgb(11 11 12 / 0.12);\n --jl-shadow-lg: 0 18px 44px -14px rgb(11 11 12 / 0.22);\n --jl-focus-ring: 0 0 0 3px color-mix(in srgb, var(--jl-brand) 35%, transparent);\n --jl-scrim: rgb(11 11 12 / 0.38);\n\n --jl-ease: cubic-bezier(0.2, 0.7, 0.2, 1);\n --jl-fast: 120ms;\n --jl-slow: 240ms;\n }\n\n @media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) {\n color-scheme: dark;\n --jl-text: #eceef1;\n --jl-text-2: #c3c8cf;\n --jl-muted: #959ca6;\n --jl-faint: #6b727c;\n --jl-bg: #0f1113;\n --jl-surface: #0f1113;\n --jl-surface-2: #16191c;\n --jl-surface-3: #1d2125;\n --jl-code-bg: #1d2125;\n --jl-line: #262a2f;\n --jl-line-soft: #1f2327;\n --jl-line-strong: #343a41;\n --jl-brand: #1a9be0;\n --jl-accent: #3ea6e8;\n --jl-accent-strong: #6cc0f2;\n --jl-accent-soft: #0f2a3b;\n --jl-accent-line: #1e4a66;\n --jl-on-accent: #04131c;\n --jl-ok: #4cc38a;\n --jl-ok-soft: #10261c;\n --jl-warn: #e5a54b;\n --jl-warn-soft: #2a2012;\n --jl-danger: #f0786c;\n --jl-danger-soft: #3a1a17;\n --jl-on-danger: #1c0604;\n --jl-info: #3ea6e8;\n --jl-info-soft: #0f2a3b;\n --jl-code-keyword: #3ea6e8;\n --jl-code-tag: #6cc0f2;\n --jl-code-attribute: #a99bf0;\n --jl-code-string: #4cc38a;\n --jl-code-number: #e5a54b;\n --jl-code-function: #8fb0d6;\n --jl-code-property: #4fc1b5;\n --jl-code-comment: #6b727c;\n --jl-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);\n --jl-shadow: 0 6px 18px -6px rgb(0 0 0 / 0.6);\n --jl-shadow-lg: 0 18px 44px -12px rgb(0 0 0 / 0.7);\n --jl-scrim: rgb(0 0 0 / 0.55);\n }\n }\n\n /* Dark: the same blue, lifted to read on a dark ground, with dark text on the buttons. */\n [data-theme=\"dark\"] {\n color-scheme: dark;\n --jl-text: #eceef1;\n --jl-text-2: #c3c8cf;\n --jl-muted: #959ca6;\n --jl-faint: #6b727c;\n --jl-bg: #0f1113;\n --jl-surface: #0f1113;\n --jl-surface-2: #16191c;\n --jl-surface-3: #1d2125;\n --jl-code-bg: #1d2125;\n --jl-line: #262a2f;\n --jl-line-soft: #1f2327;\n --jl-line-strong: #343a41;\n --jl-brand: #1a9be0;\n --jl-accent: #3ea6e8;\n --jl-accent-strong: #6cc0f2;\n --jl-accent-soft: #0f2a3b;\n --jl-accent-line: #1e4a66;\n --jl-on-accent: #04131c;\n --jl-ok: #4cc38a;\n --jl-ok-soft: #10261c;\n --jl-warn: #e5a54b;\n --jl-warn-soft: #2a2012;\n --jl-danger: #f0786c;\n --jl-danger-soft: #3a1a17;\n --jl-on-danger: #1c0604;\n --jl-info: #3ea6e8;\n --jl-info-soft: #0f2a3b;\n --jl-code-keyword: #3ea6e8;\n --jl-code-tag: #6cc0f2;\n --jl-code-attribute: #a99bf0;\n --jl-code-string: #4cc38a;\n --jl-code-number: #e5a54b;\n --jl-code-function: #8fb0d6;\n --jl-code-property: #4fc1b5;\n --jl-code-comment: #6b727c;\n --jl-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);\n --jl-shadow: 0 6px 18px -6px rgb(0 0 0 / 0.6);\n --jl-shadow-lg: 0 18px 44px -12px rgb(0 0 0 / 0.7);\n --jl-scrim: rgb(0 0 0 / 0.55);\n }\n\n /*\n * Touch screens: controls a finger can hit (44px for most), and field text phones do not zoom into when it takes\n * the focus (16px). Themed parts of a page re-declare the sizes, so they take these too.\n */\n @media (pointer: coarse) {\n :root,\n [data-theme=\"light\"],\n [data-theme=\"dark\"] {\n --jl-control-sm: 36px;\n --jl-control-md: 44px;\n --jl-control-lg: 48px;\n --jl-control-font: 16px;\n }\n }\n}\n";
2
+ var tokens_default = "/*\n * The design tokens, as swai and swcode have them: ink and white, flat surfaces separated by lines, and one blue\n * that marks what is live, selected or waiting for a person. Custom properties cross shadow roots, so every\n * component reads them. They sit in the `jlib` layer: any unlayered rule of the page overrides them.\n */\n@layer jlib {\n :root,\n [data-theme=\"light\"] {\n color-scheme: light;\n --jl-font-sans: \"Manrope Variable\", \"Manrope\", system-ui, -apple-system, \"Segoe UI\", sans-serif;\n --jl-font-mono: \"JetBrains Mono Variable\", \"JetBrains Mono\", ui-monospace, \"SF Mono\", Menlo, monospace;\n\n --jl-font-xs: 12px;\n --jl-font-sm: 13px;\n --jl-font-md: 14px;\n --jl-font-lg: 16px;\n --jl-font-xl: 20px;\n --jl-font-2xl: 28px;\n\n --jl-text: #0b0b0c;\n --jl-text-2: #3a3f47;\n /* Secondary text: 6.2:1 on white. */\n --jl-muted: #5b616b;\n /* Icons and decorations only: 3.2:1 on white is enough for them, not for text. */\n --jl-faint: #8a9099;\n\n --jl-bg: #ffffff;\n --jl-surface: #ffffff;\n --jl-surface-2: #f7f8f9;\n --jl-surface-3: #eef0f2;\n --jl-code-bg: #f0f2f4;\n --jl-line: #e6e8eb;\n --jl-line-soft: #eef0f2;\n --jl-line-strong: #d5d9de;\n\n /* The SyWare logo's blue: marks, focus rings, progress. 3.8:1 on white: enough for shapes, never for text. */\n --jl-brand: #0e88d3;\n /* The same hue, deep enough to carry text: buttons, links, the selection. 5.2:1 on white. */\n --jl-accent: #0c72b0;\n --jl-accent-strong: #095a8b;\n --jl-accent-soft: #e9f4fb;\n --jl-accent-line: #b7dbf2;\n --jl-on-accent: #ffffff;\n /* What is drawn on the brand's blue, such as an app's icon: white in both themes, as a favicon is. */\n --jl-on-brand: #ffffff;\n\n --jl-ok: #17784a;\n --jl-ok-soft: #ecf6f0;\n --jl-warn: #9a5b00;\n --jl-warn-soft: #fcf4e6;\n --jl-danger: #b42318;\n --jl-danger-soft: #fdf0ef;\n --jl-on-danger: #ffffff;\n --jl-info: #0c72b0;\n --jl-info-soft: #e9f4fb;\n\n /* Code: the accent for keywords, quiet colours from the same family for the rest. */\n --jl-code-keyword: #0c72b0;\n --jl-code-tag: #095a8b;\n --jl-code-attribute: #5b48c8;\n --jl-code-string: #17784a;\n --jl-code-number: #9a5b00;\n --jl-code-function: #3d5a80;\n --jl-code-property: #127a72;\n --jl-code-comment: #8a9099;\n\n --jl-radius-xs: 6px;\n --jl-radius-sm: 8px;\n --jl-radius: 10px;\n --jl-radius-lg: 12px;\n --jl-radius-xl: 20px;\n --jl-radius-pill: 999px;\n\n /* Spacing: a 4px grid, for the layouts pages build around the components. */\n --jl-space-1: 4px;\n --jl-space-2: 8px;\n --jl-space-3: 12px;\n --jl-space-4: 16px;\n --jl-space-5: 20px;\n --jl-space-6: 24px;\n --jl-space-8: 32px;\n --jl-space-10: 40px;\n --jl-space-12: 48px;\n --jl-space-16: 64px;\n\n --jl-control-sm: 30px;\n --jl-control-md: 36px;\n --jl-control-lg: 44px;\n /* The text people type in fields. */\n --jl-control-font: var(--jl-font-md);\n\n /* The widest a page gets: a form, most pages, and the tables and grids that need the room. */\n --jl-page-form: 720px;\n --jl-page-standard: 1120px;\n --jl-page-wide: 1400px;\n\n /* Flat: lines separate; only what floats (menus, dialogs, toasts) casts a shadow. */\n --jl-shadow-sm: 0 1px 2px rgb(11 11 12 / 0.05);\n --jl-shadow: 0 6px 16px -6px rgb(11 11 12 / 0.12);\n --jl-shadow-lg: 0 18px 44px -14px rgb(11 11 12 / 0.22);\n --jl-focus-ring: 0 0 0 3px color-mix(in srgb, var(--jl-brand) 35%, transparent);\n --jl-scrim: rgb(11 11 12 / 0.38);\n\n --jl-ease: cubic-bezier(0.2, 0.7, 0.2, 1);\n --jl-fast: 120ms;\n --jl-slow: 240ms;\n }\n\n @media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) {\n color-scheme: dark;\n --jl-text: #eceef1;\n --jl-text-2: #c3c8cf;\n --jl-muted: #959ca6;\n --jl-faint: #6b727c;\n --jl-bg: #0f1113;\n --jl-surface: #0f1113;\n --jl-surface-2: #16191c;\n --jl-surface-3: #1d2125;\n --jl-code-bg: #1d2125;\n --jl-line: #262a2f;\n --jl-line-soft: #1f2327;\n --jl-line-strong: #343a41;\n --jl-brand: #1a9be0;\n --jl-accent: #3ea6e8;\n --jl-accent-strong: #6cc0f2;\n --jl-accent-soft: #0f2a3b;\n --jl-accent-line: #1e4a66;\n --jl-on-accent: #04131c;\n --jl-ok: #4cc38a;\n --jl-ok-soft: #10261c;\n --jl-warn: #e5a54b;\n --jl-warn-soft: #2a2012;\n --jl-danger: #f0786c;\n --jl-danger-soft: #3a1a17;\n --jl-on-danger: #1c0604;\n --jl-info: #3ea6e8;\n --jl-info-soft: #0f2a3b;\n --jl-code-keyword: #3ea6e8;\n --jl-code-tag: #6cc0f2;\n --jl-code-attribute: #a99bf0;\n --jl-code-string: #4cc38a;\n --jl-code-number: #e5a54b;\n --jl-code-function: #8fb0d6;\n --jl-code-property: #4fc1b5;\n --jl-code-comment: #6b727c;\n --jl-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);\n --jl-shadow: 0 6px 18px -6px rgb(0 0 0 / 0.6);\n --jl-shadow-lg: 0 18px 44px -12px rgb(0 0 0 / 0.7);\n --jl-scrim: rgb(0 0 0 / 0.55);\n }\n }\n\n /* Dark: the same blue, lifted to read on a dark ground, with dark text on the buttons. */\n [data-theme=\"dark\"] {\n color-scheme: dark;\n --jl-text: #eceef1;\n --jl-text-2: #c3c8cf;\n --jl-muted: #959ca6;\n --jl-faint: #6b727c;\n --jl-bg: #0f1113;\n --jl-surface: #0f1113;\n --jl-surface-2: #16191c;\n --jl-surface-3: #1d2125;\n --jl-code-bg: #1d2125;\n --jl-line: #262a2f;\n --jl-line-soft: #1f2327;\n --jl-line-strong: #343a41;\n --jl-brand: #1a9be0;\n --jl-accent: #3ea6e8;\n --jl-accent-strong: #6cc0f2;\n --jl-accent-soft: #0f2a3b;\n --jl-accent-line: #1e4a66;\n --jl-on-accent: #04131c;\n --jl-ok: #4cc38a;\n --jl-ok-soft: #10261c;\n --jl-warn: #e5a54b;\n --jl-warn-soft: #2a2012;\n --jl-danger: #f0786c;\n --jl-danger-soft: #3a1a17;\n --jl-on-danger: #1c0604;\n --jl-info: #3ea6e8;\n --jl-info-soft: #0f2a3b;\n --jl-code-keyword: #3ea6e8;\n --jl-code-tag: #6cc0f2;\n --jl-code-attribute: #a99bf0;\n --jl-code-string: #4cc38a;\n --jl-code-number: #e5a54b;\n --jl-code-function: #8fb0d6;\n --jl-code-property: #4fc1b5;\n --jl-code-comment: #6b727c;\n --jl-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);\n --jl-shadow: 0 6px 18px -6px rgb(0 0 0 / 0.6);\n --jl-shadow-lg: 0 18px 44px -12px rgb(0 0 0 / 0.7);\n --jl-scrim: rgb(0 0 0 / 0.55);\n }\n\n /*\n * Touch screens: controls a finger can hit (44px for most), and field text phones do not zoom into when it takes\n * the focus (16px). Themed parts of a page re-declare the sizes, so they take these too.\n */\n @media (pointer: coarse) {\n :root,\n [data-theme=\"light\"],\n [data-theme=\"dark\"] {\n --jl-control-sm: 36px;\n --jl-control-md: 44px;\n --jl-control-lg: 48px;\n --jl-control-font: 16px;\n }\n }\n}\n";
3
3
  export { tokens_default as default };
@@ -4,7 +4,9 @@ import { JlElement } from '../base/jl-element';
4
4
  * on its own, and an optional panel on the right (`aside`). Narrower than 960 pixels, the sidebar becomes a drawer
5
5
  * over the content, opened by a menu button in the top bar (or `nav-open`, or `toggleNav()`), the panel hides, and
6
6
  * a bar along the bottom shows (`tabs`, such as a `jl-tab-bar`, whose `more` opens the drawer).
7
- * It takes the height of its container: give that one a height (`100dvh` for a whole page).
7
+ * It takes the height of its container: give that one a height (`100dvh` for a whole page). A shell that fills the
8
+ * window writes the height of its tab bar, while it shows, as `--jl-shell-bottom` on the document: toasts keep
9
+ * above it, and a page's floating bars can (`bottom: calc(var(--jl-shell-bottom, 0px) + 16px)`).
8
10
  */
9
11
  export declare class JlAppShell extends JlElement {
10
12
  static readonly selector = "jl-app-shell";
@@ -25,6 +27,9 @@ export declare class JlAppShell extends JlElement {
25
27
  templateCss(): string;
26
28
  onConnect(): void;
27
29
  onDisconnect(): void;
30
+ /** Tells the page how much the tab bar covers at the bottom of the window, while it shows. */
31
+ private renderShellBottom;
32
+ private withdrawShellBottom;
28
33
  /** Whether the shell is narrow: the menu button shows only then. */
29
34
  private isNarrow;
30
35
  private onMenu;
@@ -10,6 +10,7 @@ import { jsx, jsxs } from "@jadis/core/jsx-runtime";
10
10
  registerIcons({ menu: Menu });
11
11
  var MENU_ICON_SIZE = 18;
12
12
  var HIDDEN = "none";
13
+ var SHELL_BOTTOM = "--jl-shell-bottom";
13
14
  /** Whether a click inside the drawer followed a link: the page changes, so the drawer gets out of the way. */
14
15
  var followsLink = (event) => event.composedPath().some((node) => node instanceof HTMLAnchorElement && node.href !== "");
15
16
  /**
@@ -17,7 +18,9 @@ var followsLink = (event) => event.composedPath().some((node) => node instanceof
17
18
  * on its own, and an optional panel on the right (`aside`). Narrower than 960 pixels, the sidebar becomes a drawer
18
19
  * over the content, opened by a menu button in the top bar (or `nav-open`, or `toggleNav()`), the panel hides, and
19
20
  * a bar along the bottom shows (`tabs`, such as a `jl-tab-bar`, whose `more` opens the drawer).
20
- * It takes the height of its container: give that one a height (`100dvh` for a whole page).
21
+ * It takes the height of its container: give that one a height (`100dvh` for a whole page). A shell that fills the
22
+ * window writes the height of its tab bar, while it shows, as `--jl-shell-bottom` on the document: toasts keep
23
+ * above it, and a page's floating bars can (`bottom: calc(var(--jl-shell-bottom, 0px) + 16px)`).
21
24
  */
22
25
  var JlAppShell = class extends JlElement {
23
26
  static selector = "jl-app-shell";
@@ -26,9 +29,13 @@ var JlAppShell = class extends JlElement {
26
29
  navOpen = this.useChange(false, (value) => this.renderNavOpen(value), { attribute: "nav-open" });
27
30
  _state = {
28
31
  navListeners: new AbortController(),
32
+ publishedBottom: false,
29
33
  tabsListeners: new AbortController()
30
34
  };
31
- _resize = new ResizeObserver(() => this.renderInert());
35
+ _resize = new ResizeObserver(() => {
36
+ this.renderInert();
37
+ this.renderShellBottom();
38
+ });
32
39
  _refs = this.useRefs((ref) => ({
33
40
  aside: ref(".aside"),
34
41
  asideSlot: ref("slot[name=\"aside\"]"),
@@ -96,6 +103,7 @@ var JlAppShell = class extends JlElement {
96
103
  }
97
104
  onConnect() {
98
105
  this._resize.observe(this);
106
+ this._resize.observe(this._refs.tabs);
99
107
  this.on(this._refs.menu, "click", () => this.onMenu());
100
108
  this.on(this._refs.scrim, "click", () => this.setNavOpen(false));
101
109
  this.on(this._refs.nav, "click", (event) => {
@@ -114,6 +122,23 @@ var JlAppShell = class extends JlElement {
114
122
  this._resize.disconnect();
115
123
  this._state.navListeners.abort();
116
124
  this._state.tabsListeners.abort();
125
+ this.withdrawShellBottom();
126
+ }
127
+ /** Tells the page how much the tab bar covers at the bottom of the window, while it shows. */
128
+ renderShellBottom() {
129
+ const height = Math.round(this._refs.tabs.getBoundingClientRect().height);
130
+ const fillsWindow = this.getBoundingClientRect().bottom >= window.innerHeight - 1;
131
+ if (height === 0 || !fillsWindow) {
132
+ this.withdrawShellBottom();
133
+ return;
134
+ }
135
+ document.documentElement.style.setProperty(SHELL_BOTTOM, `${height}px`);
136
+ this._state.publishedBottom = true;
137
+ }
138
+ withdrawShellBottom() {
139
+ if (!this._state.publishedBottom) return;
140
+ document.documentElement.style.removeProperty(SHELL_BOTTOM);
141
+ this._state.publishedBottom = false;
117
142
  }
118
143
  /** Whether the shell is narrow: the menu button shows only then. */
119
144
  isNarrow() {
@@ -0,0 +1,3 @@
1
+ // src/templates/jl-auth-layout.css?raw
2
+ var jl_auth_layout_default = ":host {\n display: block;\n min-height: var(--min-height, 100dvh);\n}\n\n.screen {\n display: grid;\n grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);\n min-height: var(--min-height, 100dvh);\n}\n\n/* The brand's dark panel: the dark tokens apply inside it (data-theme), whatever the page's theme. */\n.story {\n position: relative;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n gap: 32px;\n overflow: hidden;\n padding: 40px 48px;\n background: var(--jl-bg);\n color: var(--jl-text);\n}\n\n.brand,\n.pitch,\n.points,\n.origin {\n position: relative;\n z-index: 1;\n}\n\n.pitch {\n display: grid;\n gap: 16px;\n max-width: 520px;\n}\n\n.headline {\n font-size: clamp(30px, 3.4vw, 46px);\n font-weight: 800;\n letter-spacing: -0.03em;\n line-height: 1.08;\n text-wrap: balance;\n}\n\n.accent {\n color: var(--jl-brand);\n}\n\n.text {\n color: var(--jl-muted);\n font-size: 15.5px;\n line-height: 1.6;\n}\n\n.points {\n display: grid;\n gap: 10px;\n margin: 0;\n padding: 0;\n list-style: none;\n font-size: var(--jl-font-md);\n}\n\n.points li {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.tick {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: var(--jl-brand);\n color: var(--jl-on-brand);\n}\n\n.origin {\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n font-weight: 650;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n}\n\n/* The SyWare mark, large and faint, leaning out of the corner. */\n.backdrop {\n position: absolute;\n right: -90px;\n bottom: -110px;\n width: 420px;\n height: 460px;\n opacity: 0.07;\n pointer-events: none;\n}\n\n.side {\n display: grid;\n place-items: center;\n padding: 32px 24px;\n background: var(--jl-bg);\n}\n\n/* A phone: the panel is a band with the brand, above the form. */\n@media (max-width: 860px) {\n .screen {\n grid-template-columns: minmax(0, 1fr);\n grid-template-rows: auto minmax(0, 1fr);\n }\n\n .story {\n padding: 18px 24px;\n }\n\n .pitch,\n .points,\n .origin,\n .backdrop {\n display: none;\n }\n}\n";
3
+ export { jl_auth_layout_default as default };