@syware/jlib 0.5.1 → 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 (63) 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.d.ts +4 -1
  40. package/dist/molecules/jl-tooltip.js +27 -15
  41. package/dist/organisms/jl-command-palette.css_raw.js +1 -1
  42. package/dist/organisms/jl-command-palette.js +1 -1
  43. package/dist/organisms/jl-confirm.d.ts +2 -1
  44. package/dist/organisms/jl-confirm.js +16 -8
  45. package/dist/organisms/jl-flow.js +1 -1
  46. package/dist/organisms/jl-side-nav.js +1 -1
  47. package/dist/organisms/jl-tab-bar.js +1 -1
  48. package/dist/organisms/jl-table.js +1 -1
  49. package/dist/organisms/jl-toaster.css_raw.js +1 -1
  50. package/dist/organisms/jl-tree.js +1 -1
  51. package/dist/styles/tokens.css_raw.js +1 -1
  52. package/dist/templates/jl-app-shell.d.ts +6 -1
  53. package/dist/templates/jl-app-shell.js +27 -2
  54. package/dist/templates/jl-auth-layout.css_raw.js +3 -0
  55. package/dist/templates/jl-auth-layout.d.ts +35 -0
  56. package/dist/templates/jl-auth-layout.js +139 -0
  57. package/dist/templates/jl-page.css_raw.js +1 -1
  58. package/dist/templates/jl-split-view.css_raw.js +1 -1
  59. package/dist/templates/jl-split-view.d.ts +21 -1
  60. package/dist/templates/jl-split-view.js +33 -1
  61. package/dist/tokens.css +2 -0
  62. package/dist/types/jl-split-view.types.d.ts +2 -0
  63. 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";
@@ -41,10 +41,13 @@ export declare class JlTooltip extends JlElement {
41
41
  private isInteractive;
42
42
  private pressStart;
43
43
  private pressMove;
44
- /** The finger lifted, or the page took it to scroll: a bubble the press showed stays a moment, then goes. */
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. */
45
47
  private pressEnd;
46
48
  /** The system's own long press: the bubble shows now, and the menu it would open does not, but over a link. */
47
49
  private pressHeld;
50
+ /** The long press shows the bubble, and says whether there is one: without a text, the press stays a press. */
48
51
  private openFromPress;
49
52
  private swallowLongPress;
50
53
  private hideBubble;
@@ -51,6 +51,7 @@ var JlTooltip = class extends JlElement {
51
51
  hideTimer: 0,
52
52
  press: {
53
53
  down: false,
54
+ moved: false,
54
55
  opened: false,
55
56
  timer: 0,
56
57
  x: 0,
@@ -94,7 +95,7 @@ var JlTooltip = class extends JlElement {
94
95
  this.hideBubble();
95
96
  });
96
97
  this.on(this, "pointermove", (event) => this.pressMove(event));
97
- this.on(this, "pointerup", () => this.pressEnd());
98
+ this.on(this, "pointerup", () => this.pressUp());
98
99
  this.on(this, "pointercancel", () => this.pressEnd());
99
100
  this.on(this, "contextmenu", (event) => this.pressHeld(event));
100
101
  this.addEventListener("click", (event) => this.swallowLongPress(event), {
@@ -131,26 +132,34 @@ var JlTooltip = class extends JlElement {
131
132
  pressStart(event) {
132
133
  const press = this._state.press;
133
134
  window.clearTimeout(press.timer);
134
- press.opened = false;
135
- if (!this.isInteractive()) {
136
- if (this.isOpen()) {
137
- this.close();
138
- return;
139
- }
140
- this.open();
141
- return;
142
- }
143
135
  press.down = true;
136
+ press.moved = false;
137
+ press.opened = false;
144
138
  press.x = event.clientX;
145
139
  press.y = event.clientY;
146
- press.timer = window.setTimeout(() => this.openFromPress(), LONG_PRESS_MS);
140
+ if (this.isInteractive()) press.timer = window.setTimeout(() => this.openFromPress(), LONG_PRESS_MS);
147
141
  }
148
142
  pressMove(event) {
149
143
  const press = this._state.press;
150
144
  const moved = Math.hypot(event.clientX - press.x, event.clientY - press.y);
151
- if (press.down && moved > MOVE_TOLERANCE_PX) window.clearTimeout(press.timer);
145
+ if (press.down && moved > MOVE_TOLERANCE_PX) {
146
+ press.moved = true;
147
+ window.clearTimeout(press.timer);
148
+ }
152
149
  }
153
- /** The finger lifted, or the page took it to scroll: a bubble the press showed stays a moment, then goes. */
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. */
154
163
  pressEnd() {
155
164
  const press = this._state.press;
156
165
  window.clearTimeout(press.timer);
@@ -162,13 +171,15 @@ var JlTooltip = class extends JlElement {
162
171
  /** The system's own long press: the bubble shows now, and the menu it would open does not, but over a link. */
163
172
  pressHeld(event) {
164
173
  if (!this._state.press.down || !this.isInteractive()) return;
165
- this.openFromPress();
166
- if (!accessibleTarget(this.trigger()).matches("a[href]")) event.preventDefault();
174
+ if (this.openFromPress() && !accessibleTarget(this.trigger()).matches("a[href]")) event.preventDefault();
167
175
  }
176
+ /** The long press shows the bubble, and says whether there is one: without a text, the press stays a press. */
168
177
  openFromPress() {
169
178
  window.clearTimeout(this._state.press.timer);
179
+ if (this.text.get() === "") return false;
170
180
  this._state.press.opened = true;
171
181
  this.open();
182
+ return true;
172
183
  }
173
184
  swallowLongPress(event) {
174
185
  const press = this._state.press;
@@ -178,6 +189,7 @@ var JlTooltip = class extends JlElement {
178
189
  event.stopPropagation();
179
190
  }
180
191
  hideBubble() {
192
+ window.clearTimeout(this._state.press.timer);
181
193
  window.clearTimeout(this._state.showTimer);
182
194
  window.clearTimeout(this._state.hideTimer);
183
195
  this._state.following.abort();
@@ -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 };
@@ -0,0 +1,35 @@
1
+ import { JlElement } from '../base/jl-element';
2
+ /**
3
+ * The screen before an app, as every SyWare app shows it: on a dark panel, the app's brand (slot `brand`), a
4
+ * headline whose words between asterisks take the brand's blue ("Workflows that *ask* before they act."), a
5
+ * sentence, a few points, and where it comes from; beside it, the form (the default slot). On a phone, the panel
6
+ * becomes a band with the brand, above the form.
7
+ */
8
+ export declare class JlAuthLayout extends JlElement {
9
+ static readonly selector = "jl-auth-layout";
10
+ /** What the app does, in a few words; `*words*` take the brand's blue. */
11
+ readonly pitch: Readonly<Readonly<{
12
+ get(): string;
13
+ set(setter: string | ((prevState: string) => string)): void;
14
+ }>>;
15
+ /** One or two sentences under the headline. */
16
+ readonly text: Readonly<Readonly<{
17
+ get(): string;
18
+ set(setter: string | ((prevState: string) => string)): void;
19
+ }>>;
20
+ /** What the app is good at, three points at most; HTML writes them as JSON. */
21
+ readonly points: Readonly<Readonly<{
22
+ get(): string[];
23
+ set(setter: string[] | ((prevState: string[]) => string[])): void;
24
+ }>>;
25
+ /** Where the app comes from, at the foot of the panel. */
26
+ readonly origin: Readonly<Readonly<{
27
+ get(): string;
28
+ set(setter: string | ((prevState: string) => string)): void;
29
+ }>>;
30
+ private readonly _refs;
31
+ templateHtml(): Node;
32
+ templateCss(): string;
33
+ private renderPitch;
34
+ private renderPoints;
35
+ }