@syware/jlib 0.5.1 → 0.7.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 (137) hide show
  1. package/dist/all.js +11 -4
  2. package/dist/atoms/jl-app-icon.css_raw.js +3 -0
  3. package/dist/atoms/jl-app-icon.d.ts +34 -0
  4. package/dist/atoms/jl-app-icon.js +86 -0
  5. package/dist/atoms/jl-avatar.js +1 -1
  6. package/dist/atoms/jl-badge.js +9 -2
  7. package/dist/atoms/jl-button.d.ts +5 -0
  8. package/dist/atoms/jl-button.js +5 -2
  9. package/dist/atoms/jl-chip.js +3 -2
  10. package/dist/atoms/jl-input.js +5 -4
  11. package/dist/atoms/jl-link.js +2 -2
  12. package/dist/atoms/jl-rating.js +4 -5
  13. package/dist/atoms/jl-select.css_raw.js +1 -1
  14. package/dist/atoms/jl-select.d.ts +3 -2
  15. package/dist/atoms/jl-select.js +6 -5
  16. package/dist/atoms/jl-spinner.js +2 -2
  17. package/dist/base/jl-form-control.js +2 -1
  18. package/dist/index.d.ts +10 -0
  19. package/dist/index.js +10 -3
  20. package/dist/lib/app-icon.d.ts +8 -0
  21. package/dist/lib/app-icon.js +37 -0
  22. package/dist/lib/dom.d.ts +2 -0
  23. package/dist/lib/dom.js +3 -1
  24. package/dist/lib/download.d.ts +10 -0
  25. package/dist/lib/download.js +62 -0
  26. package/dist/lib/icons.d.ts +4 -0
  27. package/dist/lib/icons.js +7 -4
  28. package/dist/lib/input-list.d.ts +12 -0
  29. package/dist/lib/input-list.js +14 -0
  30. package/dist/lib/internals.d.ts +5 -0
  31. package/dist/lib/internals.js +18 -0
  32. package/dist/lib/palette.js +6 -0
  33. package/dist/lib/resize.d.ts +2 -0
  34. package/dist/lib/resize.js +10 -0
  35. package/dist/lib/table.d.ts +6 -1
  36. package/dist/lib/table.js +16 -1
  37. package/dist/lib/text.d.ts +12 -0
  38. package/dist/lib/text.js +16 -1
  39. package/dist/lib/texts.d.ts +97 -0
  40. package/dist/lib/texts.js +102 -0
  41. package/dist/lib/time.d.ts +5 -3
  42. package/dist/lib/time.js +23 -15
  43. package/dist/lib/tree.d.ts +10 -1
  44. package/dist/lib/tree.js +37 -3
  45. package/dist/lib/trigger.js +1 -1
  46. package/dist/molecules/jl-account-button.css_raw.js +1 -1
  47. package/dist/molecules/jl-account-button.d.ts +8 -1
  48. package/dist/molecules/jl-account-button.js +9 -2
  49. package/dist/molecules/jl-alert.js +3 -2
  50. package/dist/molecules/jl-breadcrumb.js +3 -3
  51. package/dist/molecules/jl-calendar.js +5 -5
  52. package/dist/molecules/jl-chip-group.js +3 -3
  53. package/dist/molecules/jl-code-block.css_raw.js +1 -1
  54. package/dist/molecules/jl-code-block.d.ts +5 -0
  55. package/dist/molecules/jl-code-block.js +5 -2
  56. package/dist/molecules/jl-combobox.js +6 -7
  57. package/dist/molecules/jl-copy-button.js +5 -4
  58. package/dist/molecules/jl-date-picker.js +7 -8
  59. package/dist/molecules/jl-empty.js +2 -2
  60. package/dist/molecules/jl-facts.css_raw.js +3 -0
  61. package/dist/molecules/jl-facts.d.ts +35 -0
  62. package/dist/molecules/jl-facts.js +64 -0
  63. package/dist/molecules/jl-file-drop.d.ts +5 -0
  64. package/dist/molecules/jl-file-drop.js +18 -15
  65. package/dist/molecules/jl-input-list.css_raw.js +3 -0
  66. package/dist/molecules/jl-input-list.d.ts +79 -0
  67. package/dist/molecules/jl-input-list.js +177 -0
  68. package/dist/molecules/jl-list-item.js +1 -1
  69. package/dist/molecules/jl-load-more.css_raw.js +3 -0
  70. package/dist/molecules/jl-load-more.d.ts +41 -0
  71. package/dist/molecules/jl-load-more.js +62 -0
  72. package/dist/molecules/jl-load-state.d.ts +4 -1
  73. package/dist/molecules/jl-load-state.js +20 -12
  74. package/dist/molecules/jl-menu.js +1 -1
  75. package/dist/molecules/jl-number-input.js +5 -5
  76. package/dist/molecules/jl-pagination.js +5 -5
  77. package/dist/molecules/jl-radio-group.js +2 -2
  78. package/dist/molecules/jl-search-button.js +5 -5
  79. package/dist/molecules/jl-search.js +5 -5
  80. package/dist/molecules/jl-segmented.js +3 -2
  81. package/dist/molecules/jl-steps.css_raw.js +1 -1
  82. package/dist/molecules/jl-steps.d.ts +2 -0
  83. package/dist/molecules/jl-steps.js +30 -14
  84. package/dist/molecules/jl-tabs.js +3 -2
  85. package/dist/molecules/jl-tag-input.js +3 -3
  86. package/dist/molecules/jl-timeline.js +1 -1
  87. package/dist/molecules/jl-toast.css_raw.js +1 -1
  88. package/dist/molecules/jl-toast.d.ts +13 -1
  89. package/dist/molecules/jl-toast.js +23 -3
  90. package/dist/molecules/jl-tooltip.d.ts +4 -1
  91. package/dist/molecules/jl-tooltip.js +27 -15
  92. package/dist/organisms/jl-command-palette.css_raw.js +1 -1
  93. package/dist/organisms/jl-command-palette.d.ts +14 -0
  94. package/dist/organisms/jl-command-palette.js +40 -10
  95. package/dist/organisms/jl-confirm.d.ts +2 -1
  96. package/dist/organisms/jl-confirm.js +18 -12
  97. package/dist/organisms/jl-dialog.js +2 -1
  98. package/dist/organisms/jl-drawer.js +2 -1
  99. package/dist/organisms/jl-flow.js +13 -13
  100. package/dist/organisms/jl-navbar.js +6 -6
  101. package/dist/organisms/jl-side-nav.css_raw.js +1 -1
  102. package/dist/organisms/jl-side-nav.d.ts +33 -1
  103. package/dist/organisms/jl-side-nav.js +71 -14
  104. package/dist/organisms/jl-tab-bar.js +3 -3
  105. package/dist/organisms/jl-table.css_raw.js +1 -1
  106. package/dist/organisms/jl-table.d.ts +9 -0
  107. package/dist/organisms/jl-table.js +36 -6
  108. package/dist/organisms/jl-toaster.css_raw.js +1 -1
  109. package/dist/organisms/jl-toaster.d.ts +9 -1
  110. package/dist/organisms/jl-toaster.js +148 -7
  111. package/dist/organisms/jl-tree.css_raw.js +1 -1
  112. package/dist/organisms/jl-tree.d.ts +71 -4
  113. package/dist/organisms/jl-tree.js +303 -40
  114. package/dist/styles/tokens.css_raw.js +1 -1
  115. package/dist/templates/jl-app-shell.css_raw.js +1 -1
  116. package/dist/templates/jl-app-shell.d.ts +16 -1
  117. package/dist/templates/jl-app-shell.js +42 -3
  118. package/dist/templates/jl-auth-layout.css_raw.js +3 -0
  119. package/dist/templates/jl-auth-layout.d.ts +35 -0
  120. package/dist/templates/jl-auth-layout.js +139 -0
  121. package/dist/templates/jl-page.css_raw.js +1 -1
  122. package/dist/templates/jl-page.js +3 -3
  123. package/dist/templates/jl-split-view.css_raw.js +1 -1
  124. package/dist/templates/jl-split-view.d.ts +21 -1
  125. package/dist/templates/jl-split-view.js +37 -4
  126. package/dist/tokens.css +2 -0
  127. package/dist/types/jl-app-icon.types.d.ts +5 -0
  128. package/dist/types/jl-command-palette.types.d.ts +7 -0
  129. package/dist/types/jl-facts.types.d.ts +17 -0
  130. package/dist/types/jl-side-nav.types.d.ts +2 -0
  131. package/dist/types/jl-split-view.types.d.ts +2 -0
  132. package/dist/types/jl-steps.types.d.ts +2 -0
  133. package/dist/types/jl-table.types.d.ts +9 -2
  134. package/dist/types/jl-toaster.types.d.ts +42 -1
  135. package/dist/types/jl-tree.types.d.ts +26 -0
  136. package/dist/types/texts.types.d.ts +5 -0
  137. package/package.json +3 -3
@@ -1,9 +1,12 @@
1
1
  import { JlElement } from '../base/jl-element';
2
- import type { TreeItem } from '../types/jl-tree.types';
2
+ import type { TreeItem, TreeLoader } from '../types/jl-tree.types';
3
3
  /**
4
4
  * Nested items that open and close, as files in folders: `items` holds them, `expanded` lists the open folders and
5
- * `value` is the selected item. The keyboard follows the tree pattern: Up and Down move, Right opens or enters a
6
- * folder, Left closes it or goes to the parent, Home and End jump, Enter selects. Emits `select` and `toggle`.
5
+ * `value` is the selected item. With a `loader`, the tree loads itself a level at a time and a page at a time, so a
6
+ * large archive opens without loading everything: the roots first, a `lazy` folder's children when it opens, and
7
+ * the next page of a level from the button that ends it. The keyboard follows the tree pattern: Up and Down move,
8
+ * Right opens or enters a folder, Left closes it or goes to the parent, Home and End jump, Enter selects. Emits
9
+ * `select` and `toggle`; an item with an `href` is a link instead.
7
10
  */
8
11
  export declare class JlTree extends JlElement {
9
12
  static readonly selector = "jl-tree";
@@ -33,24 +36,88 @@ export declare class JlTree extends JlElement {
33
36
  get(): string;
34
37
  set(setter: string | ((prevState: string) => string)): void;
35
38
  }>>;
39
+ /** What an empty tree says, such as "No object yet"; none by default. */
40
+ readonly emptyText: Readonly<Readonly<{
41
+ get(): string;
42
+ set(setter: string | ((prevState: string) => string)): void;
43
+ }>>;
44
+ /** Said while a page loads. */
45
+ readonly loadingText: Readonly<Readonly<{
46
+ get(): string;
47
+ set(setter: string | ((prevState: string) => string)): void;
48
+ }>>;
49
+ /** The button that loads a level's next page; `{count}` is how many items are left. */
50
+ readonly moreText: Readonly<Readonly<{
51
+ get(): string;
52
+ set(setter: string | ((prevState: string) => string)): void;
53
+ }>>;
54
+ /** Said where a page could not be loaded. */
55
+ readonly errorText: Readonly<Readonly<{
56
+ get(): string;
57
+ set(setter: string | ((prevState: string) => string)): void;
58
+ }>>;
59
+ /** The action that loads it again. */
60
+ readonly retryLabel: Readonly<Readonly<{
61
+ get(): string;
62
+ set(setter: string | ((prevState: string) => string)): void;
63
+ }>>;
64
+ private readonly _source;
36
65
  private readonly _rows;
37
66
  private readonly _focus;
67
+ private readonly _loading;
38
68
  private readonly _refs;
69
+ /** Loads the items a page at a time; setting it forgets `items` and loads the roots. */
70
+ get loader(): TreeLoader | null;
71
+ set loader(loader: TreeLoader | null);
39
72
  /** Opens or closes a folder, as a press on its chevron would. */
40
73
  toggle(id: string, open: boolean): void;
74
+ /** Forgets what the loader gave and loads the roots again. */
75
+ reload(): Promise<void>;
76
+ /**
77
+ * Shows an item given the ids from its root down to it: opens the folders on the way, loading their levels and
78
+ * pages as needed, then selects it, without firing `select`. False when an id is not found where the path says.
79
+ */
80
+ reveal(path: readonly string[]): Promise<boolean>;
41
81
  templateHtml(): Node;
42
82
  templateCss(): string;
43
83
  onConnect(): void;
44
84
  private idOf;
45
- /** A press selects the item; on a folder it also opens or closes it, and the chevron only does that. */
85
+ /**
86
+ * A press selects the item; on a folder it also opens or closes it, and the chevron only does that. A link's row
87
+ * leaves a modified click (a new tab) to the browser.
88
+ */
46
89
  private press;
47
90
  private handleKey;
91
+ /** Enter on an item: the row that ends a level loads its page, a link is followed, an item is selected. */
92
+ private activate;
93
+ /** Marks the item; one that is not a link says so with `select`. */
48
94
  private select;
49
95
  private findItem;
50
96
  /** One item takes the Tab stop: the focused one, else the selected one, else the first. */
51
97
  private moveFocus;
98
+ private hasFocusInside;
99
+ private scrollToRow;
100
+ /** Loads a page of a level, once at a time: a second call while it loads waits for the same page. */
101
+ private loadLevel;
102
+ private loadNextPage;
103
+ private addPage;
104
+ private failLevel;
105
+ /** The open folders whose children the loader has not given yet: their first page. */
106
+ private loadOpenFolders;
107
+ /** The item of an id in a level, loading the level's next pages until it shows or none is left. */
108
+ private findInLevel;
109
+ private openFolder;
110
+ private levelEnd;
111
+ private hasLevelEndRow;
52
112
  private renderItems;
53
113
  private itemNode;
114
+ /** The row that ends a level, if it has one. */
115
+ private levelEndNodes;
116
+ private levelEndRow;
117
+ private loadingNode;
118
+ private moreNode;
119
+ private errorNode;
120
+ private noteNode;
54
121
  private renderExpanded;
55
122
  private isVisible;
56
123
  /** The item itself when it shows, else the nearest folder around it that does: the focus goes there. */
@@ -2,16 +2,20 @@ 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";
5
+ import { jlText } from "../lib/texts.js";
6
6
  import { JlIcon } from "../atoms/jl-icon.js";
7
- import { allItems, isFolder, treeKeyAction, withExpanded } from "../lib/tree.js";
7
+ import { isModifiedClick, setOrRemove, uniqueId } from "../lib/dom.js";
8
+ import { JlSpinner } from "../atoms/jl-spinner.js";
9
+ import { pageLocale } from "../lib/locale.js";
10
+ import { allItems, childrenOf, isFolder, treeKeyAction, withExpanded, withExtraRows, withPage } from "../lib/tree.js";
8
11
  import jl_tree_default from "./jl-tree.css_raw.js";
9
12
  import { css } from "@jadis/core";
10
- import { ChevronRight, File, Folder, FolderOpen } from "lucide";
13
+ import { ChevronRight, CircleAlert, File, Folder, FolderOpen } from "lucide";
11
14
  import { jsx, jsxs } from "@jadis/core/jsx-runtime";
12
15
  // src/organisms/jl-tree.tsx
13
16
  registerIcons({
14
17
  "chevron-right": ChevronRight,
18
+ "circle-alert": CircleAlert,
15
19
  file: File,
16
20
  folder: Folder,
17
21
  "folder-open": FolderOpen
@@ -21,10 +25,20 @@ var CHEVRON_SIZE = 14;
21
25
  var FOLDER_ICON = "folder";
22
26
  var OPEN_FOLDER_ICON = "folder-open";
23
27
  var FILE_ICON = "file";
28
+ var ERROR_ICON = "circle-alert";
29
+ var LEVEL_END_PREFIX = "jl-tree-level-end:";
30
+ var COUNT_PLACEHOLDER = "{count}";
31
+ var levelEndId = (parent) => `${LEVEL_END_PREFIX}${parent ?? ""}`;
32
+ var isLevelEnd = (id) => id.startsWith(LEVEL_END_PREFIX);
33
+ var parentOfLevelEnd = (id) => id.slice(18) || null;
34
+ var messageOf = (error) => error instanceof Error ? error.message : String(error);
24
35
  /**
25
36
  * Nested items that open and close, as files in folders: `items` holds them, `expanded` lists the open folders and
26
- * `value` is the selected item. The keyboard follows the tree pattern: Up and Down move, Right opens or enters a
27
- * folder, Left closes it or goes to the parent, Home and End jump, Enter selects. Emits `select` and `toggle`.
37
+ * `value` is the selected item. With a `loader`, the tree loads itself a level at a time and a page at a time, so a
38
+ * large archive opens without loading everything: the roots first, a `lazy` folder's children when it opens, and
39
+ * the next page of a level from the button that ends it. The keyboard follows the tree pattern: Up and Down move,
40
+ * Right opens or enters a folder, Left closes it or goes to the parent, Home and End jump, Enter selects. Emits
41
+ * `select` and `toggle`; an item with an `href` is a link instead.
28
42
  */
29
43
  var JlTree = class extends JlElement {
30
44
  static selector = "jl-tree";
@@ -34,7 +48,10 @@ var JlTree = class extends JlElement {
34
48
  parse: jsonOf([])
35
49
  });
36
50
  /** The ids of the open folders. */
37
- expanded = this.useChange([], () => this.renderExpanded(), {
51
+ expanded = this.useChange([], () => {
52
+ this.renderExpanded();
53
+ this.loadOpenFolders();
54
+ }, {
38
55
  attribute: "expanded",
39
56
  parse: jsonOf([])
40
57
  });
@@ -42,9 +59,32 @@ var JlTree = class extends JlElement {
42
59
  value = this.useChange("", () => this.renderSelection(), { attribute: "value" });
43
60
  /** The tree's name for screen readers. */
44
61
  label = this.useChange("", (value) => setOrRemove(this._refs.tree, "aria-label", value), { attribute: "label" });
62
+ /** What an empty tree says, such as "No object yet"; none by default. */
63
+ emptyText = this.useChange("", () => this.renderItems(), { attribute: "empty-text" });
64
+ /** Said while a page loads. */
65
+ loadingText = this.useChange(jlText("loadingMore"), () => this.renderItems(), { attribute: "loading-text" });
66
+ /** The button that loads a level's next page; `{count}` is how many items are left. */
67
+ moreText = this.useChange(jlText("showMoreCount"), () => this.renderItems(), { attribute: "more-text" });
68
+ /** Said where a page could not be loaded. */
69
+ errorText = this.useChange(jlText("loadError"), () => this.renderItems(), { attribute: "error-text" });
70
+ /** The action that loads it again. */
71
+ retryLabel = this.useChange(jlText("retry"), () => this.renderItems(), { attribute: "retry-label" });
72
+ _source = { loader: null };
45
73
  _rows = /* @__PURE__ */ new Map();
46
74
  _focus = { id: "" };
75
+ _loading = {
76
+ generation: 0,
77
+ levels: /* @__PURE__ */ new Map()
78
+ };
47
79
  _refs = this.useRefs((ref) => ({ tree: ref(".tree") }));
80
+ /** Loads the items a page at a time; setting it forgets `items` and loads the roots. */
81
+ get loader() {
82
+ return this._source.loader;
83
+ }
84
+ set loader(loader) {
85
+ this._source.loader = loader;
86
+ this.whenRendered(() => void this.reload());
87
+ }
48
88
  /** Opens or closes a folder, as a press on its chevron would. */
49
89
  toggle(id, open) {
50
90
  this.expanded.set(withExpanded(this.expanded.get(), id, open));
@@ -53,6 +93,30 @@ var JlTree = class extends JlElement {
53
93
  open
54
94
  });
55
95
  }
96
+ /** Forgets what the loader gave and loads the roots again. */
97
+ async reload() {
98
+ if (this.loader === null) return;
99
+ this._loading.generation += 1;
100
+ this._loading.levels.clear();
101
+ this.items.set([]);
102
+ await this.loadLevel(null, 0);
103
+ }
104
+ /**
105
+ * Shows an item given the ids from its root down to it: opens the folders on the way, loading their levels and
106
+ * pages as needed, then selects it, without firing `select`. False when an id is not found where the path says.
107
+ */
108
+ async reveal(path) {
109
+ const target = path.at(-1);
110
+ if (target === void 0) return false;
111
+ for (const [index, id] of path.entries()) {
112
+ if (!await this.findInLevel(index === 0 ? null : path[index - 1] ?? null, id)) return false;
113
+ if (index < path.length - 1) await this.openFolder(id);
114
+ }
115
+ this.value.set(target);
116
+ this.moveFocus(target, false);
117
+ this.scrollToRow(target);
118
+ return true;
119
+ }
56
120
  templateHtml() {
57
121
  return /* @__PURE__ */ jsx("div", {
58
122
  class: "tree",
@@ -75,30 +139,53 @@ var JlTree = class extends JlElement {
75
139
  idOf(target) {
76
140
  return (target instanceof Element ? target.closest("[role=\"treeitem\"]") : null)?.dataset.id ?? null;
77
141
  }
78
- /** A press selects the item; on a folder it also opens or closes it, and the chevron only does that. */
142
+ /**
143
+ * A press selects the item; on a folder it also opens or closes it, and the chevron only does that. A link's row
144
+ * leaves a modified click (a new tab) to the browser.
145
+ */
79
146
  press(event) {
80
147
  const id = this.idOf(event.target);
148
+ if (id !== null && isLevelEnd(id)) {
149
+ this.loadNextPage(parentOfLevelEnd(id));
150
+ return;
151
+ }
81
152
  const item = id === null ? void 0 : this.findItem(id);
82
153
  if (id === null || item === void 0) return;
83
154
  const onChevron = event.target instanceof Element && event.target.closest(".chevron") !== null;
155
+ if (onChevron) event.preventDefault();
156
+ if (item.href !== void 0 && !onChevron && isModifiedClick(event)) return;
84
157
  if (isFolder(item)) this.toggle(id, !this.expanded.get().includes(id));
85
- if (!onChevron) this.select(id);
158
+ if (!onChevron) this.select(item);
86
159
  this.moveFocus(id, true);
87
160
  }
88
161
  handleKey(event) {
89
162
  const focused = this._focus.id;
90
- const action = treeKeyAction(this.items.get(), new Set(this.expanded.get()), focused, event.key);
163
+ const keyboardItems = withExtraRows(this.items.get(), (parent) => this.hasLevelEndRow(parent) ? levelEndId(parent) : null);
164
+ const action = treeKeyAction(keyboardItems, new Set(this.expanded.get()), focused, event.key);
91
165
  if (action === null) return;
92
166
  event.preventDefault();
93
167
  ({
94
168
  focus: () => this.moveFocus(action.id, true),
95
- select: () => this.select(action.id),
169
+ select: () => this.activate(action.id),
96
170
  toggle: () => this.toggle(action.id, action.kind === "toggle" && action.open)
97
171
  })[action.kind]();
98
172
  }
99
- select(id) {
100
- this.value.set(id);
101
- this.events.emit("select", id);
173
+ /** Enter on an item: the row that ends a level loads its page, a link is followed, an item is selected. */
174
+ activate(id) {
175
+ if (isLevelEnd(id)) {
176
+ this.loadNextPage(parentOfLevelEnd(id));
177
+ return;
178
+ }
179
+ const item = this.findItem(id);
180
+ if (item === void 0) return;
181
+ this.select(item);
182
+ const link = this._rows.get(id)?.querySelector(":scope > a.row");
183
+ if (link) location.assign(link.href);
184
+ }
185
+ /** Marks the item; one that is not a link says so with `select`. */
186
+ select(item) {
187
+ this.value.set(item.id);
188
+ if (item.href === void 0) this.events.emit("select", item.id);
102
189
  }
103
190
  findItem(id) {
104
191
  return allItems(this.items.get()).find((entry) => entry.item.id === id)?.item;
@@ -112,18 +199,133 @@ var JlTree = class extends JlElement {
112
199
  row.setAttribute("tabindex", "0");
113
200
  if (focus) row.focus();
114
201
  }
202
+ hasFocusInside() {
203
+ const active = this.shadowRoot?.activeElement;
204
+ return active instanceof Element && this._refs.tree.contains(active);
205
+ }
206
+ scrollToRow(id) {
207
+ const row = this._rows.get(id);
208
+ if (row !== void 0 && typeof row.scrollIntoView === "function") row.scrollIntoView({ block: "nearest" });
209
+ }
210
+ /** Loads a page of a level, once at a time: a second call while it loads waits for the same page. */
211
+ loadLevel(parent, offset) {
212
+ const loader = this.loader;
213
+ const level = this._loading.levels.get(parent);
214
+ if (loader === null) return Promise.resolve();
215
+ if (level !== void 0 && level.pending !== null) return level.pending;
216
+ const generation = this._loading.generation;
217
+ const pending = Promise.resolve().then(() => loader(parent, offset)).then((page) => this.addPage(parent, page, generation), (error) => this.failLevel(parent, error, generation));
218
+ this._loading.levels.set(parent, {
219
+ error: "",
220
+ pending,
221
+ total: level?.total ?? 0
222
+ });
223
+ this.whenRendered(() => this.renderItems());
224
+ return pending;
225
+ }
226
+ loadNextPage(parent) {
227
+ this.loadLevel(parent, childrenOf(this.items.get(), parent).length);
228
+ }
229
+ addPage(parent, page, generation) {
230
+ if (generation !== this._loading.generation) return;
231
+ const focusFirst = this._focus.id === levelEndId(parent) && this.hasFocusInside();
232
+ const loaded = childrenOf(this.items.get(), parent).length + page.items.length;
233
+ const total = page.items.length === 0 ? loaded : page.total;
234
+ this._loading.levels.set(parent, {
235
+ error: "",
236
+ pending: null,
237
+ total
238
+ });
239
+ this.items.set(withPage(this.items.get(), parent, page.items));
240
+ this.loadOpenFolders();
241
+ const first = page.items[0];
242
+ if (focusFirst && first !== void 0) this.moveFocus(first.id, true);
243
+ }
244
+ failLevel(parent, error, generation) {
245
+ if (generation !== this._loading.generation) return;
246
+ const total = this._loading.levels.get(parent)?.total ?? 0;
247
+ this._loading.levels.set(parent, {
248
+ error: messageOf(error) || this.errorText.get(),
249
+ pending: null,
250
+ total
251
+ });
252
+ this.whenRendered(() => this.renderItems());
253
+ }
254
+ /** The open folders whose children the loader has not given yet: their first page. */
255
+ loadOpenFolders() {
256
+ for (const id of this.expanded.get()) if (this.findItem(id)?.lazy === true && !this._loading.levels.has(id)) this.loadLevel(id, 0);
257
+ }
258
+ /** The item of an id in a level, loading the level's next pages until it shows or none is left. */
259
+ async findInLevel(parent, id) {
260
+ const isThere = () => childrenOf(this.items.get(), parent).some((item) => item.id === id);
261
+ await this._loading.levels.get(parent)?.pending;
262
+ while (!isThere() && this.levelEnd(parent) === "more") await this.loadLevel(parent, childrenOf(this.items.get(), parent).length);
263
+ return isThere();
264
+ }
265
+ async openFolder(id) {
266
+ if (!this.expanded.get().includes(id)) this.expanded.set(withExpanded(this.expanded.get(), id, true));
267
+ await this._loading.levels.get(id)?.pending;
268
+ }
269
+ levelEnd(parent) {
270
+ const level = this._loading.levels.get(parent);
271
+ if (level !== void 0 && level.pending !== null) return "loading";
272
+ if (level !== void 0 && level.error !== "") return "error";
273
+ if (level !== void 0 && level.total > childrenOf(this.items.get(), parent).length) return "more";
274
+ return parent === null && this.items.get().length === 0 && this.emptyText.get() !== "" ? "empty" : "none";
275
+ }
276
+ hasLevelEndRow(parent) {
277
+ const end = this.levelEnd(parent);
278
+ return end !== "none" && end !== "empty";
279
+ }
115
280
  renderItems() {
281
+ const hadFocus = this.hasFocusInside();
116
282
  this._rows.clear();
117
- this._refs.tree.replaceChildren(...this.items.get().map((item) => this.itemNode(item, 1)));
283
+ this._refs.tree.replaceChildren(...this.items.get().map((item) => this.itemNode(item, 1)), ...this.levelEndNodes(null, 1));
118
284
  this.renderExpanded();
119
285
  this.renderSelection();
120
- const start = this._rows.has(this.value.get()) ? this.value.get() : this.items.get()[0]?.id ?? "";
121
- this.moveFocus(this._rows.has(this._focus.id) ? this._focus.id : start, false);
286
+ const first = this._rows.keys().next().value ?? "";
287
+ const start = this._rows.has(this.value.get()) ? this.value.get() : first;
288
+ this.moveFocus(this._rows.has(this._focus.id) ? this._focus.id : start, hadFocus);
122
289
  }
123
290
  itemNode(item, level) {
124
291
  const folder = isFolder(item);
125
292
  const labelId = uniqueId("tree-label");
126
293
  const iconClass = item.icon === void 0 && folder ? "icon auto" : "icon";
294
+ const content = [
295
+ /* @__PURE__ */ jsx("span", {
296
+ class: "chevron",
297
+ "aria-hidden": "true",
298
+ children: folder ? /* @__PURE__ */ jsx(JlIcon, {
299
+ name: "chevron-right",
300
+ size: CHEVRON_SIZE
301
+ }) : null
302
+ }),
303
+ /* @__PURE__ */ jsx(JlIcon, {
304
+ class: iconClass,
305
+ size: ICON_SIZE,
306
+ name: item.icon ?? (folder ? FOLDER_ICON : FILE_ICON)
307
+ }),
308
+ /* @__PURE__ */ jsx("span", {
309
+ class: "label",
310
+ id: labelId,
311
+ children: item.label
312
+ }),
313
+ item.badge === void 0 ? null : /* @__PURE__ */ jsx("span", {
314
+ class: "badge",
315
+ children: String(item.badge)
316
+ })
317
+ ];
318
+ const row = item.href === void 0 ? /* @__PURE__ */ jsx("div", {
319
+ class: "row",
320
+ title: item.hint,
321
+ children: content
322
+ }) : /* @__PURE__ */ jsx("a", {
323
+ class: "row",
324
+ href: item.href,
325
+ title: item.hint,
326
+ tabIndex: -1,
327
+ children: content
328
+ });
127
329
  const node = /* @__PURE__ */ jsx("div", {
128
330
  class: "item",
129
331
  role: "treeitem",
@@ -131,29 +333,7 @@ var JlTree = class extends JlElement {
131
333
  tabIndex: -1,
132
334
  "aria-selected": "false",
133
335
  "aria-labelledby": labelId,
134
- children: /* @__PURE__ */ jsxs("div", {
135
- class: "row",
136
- children: [
137
- /* @__PURE__ */ jsx("span", {
138
- class: "chevron",
139
- "aria-hidden": "true",
140
- children: folder ? /* @__PURE__ */ jsx(JlIcon, {
141
- name: "chevron-right",
142
- size: CHEVRON_SIZE
143
- }) : null
144
- }),
145
- /* @__PURE__ */ jsx(JlIcon, {
146
- class: iconClass,
147
- size: ICON_SIZE,
148
- name: item.icon ?? (folder ? FOLDER_ICON : FILE_ICON)
149
- }),
150
- /* @__PURE__ */ jsx("span", {
151
- class: "label",
152
- id: labelId,
153
- children: item.label
154
- })
155
- ]
156
- })
336
+ children: row
157
337
  });
158
338
  node.style.setProperty("--level", String(level - 1));
159
339
  this._rows.set(item.id, node);
@@ -163,11 +343,94 @@ var JlTree = class extends JlElement {
163
343
  role: "group"
164
344
  });
165
345
  group.style.setProperty("--level", String(level - 1));
166
- group.replaceChildren(...(item.children ?? []).map((child) => this.itemNode(child, level + 1)));
346
+ group.replaceChildren(...(item.children ?? []).map((child) => this.itemNode(child, level + 1)), ...this.levelEndNodes(item.id, level + 1));
167
347
  node.append(group);
168
348
  }
169
349
  return node;
170
350
  }
351
+ /** The row that ends a level, if it has one. */
352
+ levelEndNodes(parent, level) {
353
+ return {
354
+ empty: () => [this.noteNode(this.emptyText.get(), level)],
355
+ error: () => [this.errorNode(parent, level)],
356
+ loading: () => [this.loadingNode(parent, level)],
357
+ more: () => [this.moreNode(parent, level)],
358
+ none: () => []
359
+ }[this.levelEnd(parent)]();
360
+ }
361
+ levelEndRow(parent, level, content, disabled) {
362
+ const id = levelEndId(parent);
363
+ const node = /* @__PURE__ */ jsx("div", {
364
+ class: "item level-end",
365
+ role: "treeitem",
366
+ "data-id": id,
367
+ tabIndex: -1,
368
+ "aria-disabled": disabled ? "true" : void 0,
369
+ children: /* @__PURE__ */ jsxs("div", {
370
+ class: "row",
371
+ children: [/* @__PURE__ */ jsx("span", {
372
+ class: "chevron",
373
+ "aria-hidden": "true"
374
+ }), content]
375
+ })
376
+ });
377
+ node.style.setProperty("--level", String(level - 1));
378
+ this._rows.set(id, node);
379
+ return node;
380
+ }
381
+ loadingNode(parent, level) {
382
+ const text = this.loadingText.get();
383
+ return this.levelEndRow(parent, level, [/* @__PURE__ */ jsx(JlSpinner, {
384
+ class: "spinner",
385
+ size: ICON_SIZE,
386
+ label: text
387
+ }), /* @__PURE__ */ jsx("span", {
388
+ class: "label",
389
+ "aria-hidden": "true",
390
+ children: text
391
+ })], true);
392
+ }
393
+ moreNode(parent, level) {
394
+ const left = (this._loading.levels.get(parent)?.total ?? 0) - childrenOf(this.items.get(), parent).length;
395
+ const text = this.moreText.get().replace(COUNT_PLACEHOLDER, left.toLocaleString(pageLocale()));
396
+ return this.levelEndRow(parent, level, [/* @__PURE__ */ jsx("span", {
397
+ class: "label more",
398
+ children: text
399
+ })], false);
400
+ }
401
+ errorNode(parent, level) {
402
+ const node = this.levelEndRow(parent, level, [
403
+ /* @__PURE__ */ jsx(JlIcon, {
404
+ class: "icon error",
405
+ size: ICON_SIZE,
406
+ name: ERROR_ICON
407
+ }),
408
+ /* @__PURE__ */ jsx("span", {
409
+ class: "label",
410
+ children: this.errorText.get()
411
+ }),
412
+ /* @__PURE__ */ jsx("span", {
413
+ class: "retry",
414
+ children: this.retryLabel.get()
415
+ })
416
+ ], false);
417
+ setOrRemove(node, "title", this._loading.levels.get(parent)?.error ?? "");
418
+ return node;
419
+ }
420
+ noteNode(text, level) {
421
+ const node = /* @__PURE__ */ jsxs("div", {
422
+ class: "note",
423
+ children: [/* @__PURE__ */ jsx("span", {
424
+ class: "chevron",
425
+ "aria-hidden": "true"
426
+ }), /* @__PURE__ */ jsx("span", {
427
+ class: "label",
428
+ children: text
429
+ })]
430
+ });
431
+ node.style.setProperty("--level", String(level - 1));
432
+ return node;
433
+ }
171
434
  renderExpanded() {
172
435
  const expanded = new Set(this.expanded.get());
173
436
  for (const [id, row] of this._rows) {
@@ -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 };
@@ -1,3 +1,3 @@
1
1
  // src/templates/jl-app-shell.css?raw
2
- var jl_app_shell_default = ":host {\n display: block;\n height: 100%;\n min-height: 0;\n container: jl-app-shell / inline-size;\n --header-height: 56px;\n --drawer-width: min(300px, 86%);\n}\n\n.shell {\n position: relative;\n display: grid;\n grid-template:\n \"top top top\" auto\n \"nav main aside\" minmax(0, 1fr)\n / auto minmax(0, 1fr) auto;\n height: 100%;\n overflow: hidden;\n background: var(--jl-bg);\n color: var(--jl-text);\n}\n\n.top {\n grid-area: top;\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: var(--header-height);\n padding: 0 16px;\n border-bottom: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n:host(:not([has-header])) .top {\n display: none;\n}\n\n::slotted([slot=\"header\"]) {\n flex: 1;\n min-width: 0;\n}\n\n.menu {\n display: none;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--jl-control-md);\n height: var(--jl-control-md);\n margin-left: -8px;\n padding: 0;\n border: none;\n border-radius: var(--jl-radius-sm);\n background: none;\n color: var(--jl-text-2);\n cursor: pointer;\n}\n\n.menu:hover {\n background: var(--jl-surface-3);\n color: var(--jl-text);\n}\n\n.nav {\n grid-area: nav;\n min-height: 0;\n overflow-y: auto;\n border-right: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n.nav:focus-visible {\n box-shadow: none;\n}\n\n:host(:not([has-nav])) .nav {\n display: none;\n}\n\n/* Its own stacking context: what the page lifts with z-index (a sticky bar) stays under the drawer and its scrim. */\n.main {\n grid-area: main;\n min-width: 0;\n min-height: 0;\n overflow: auto;\n isolation: isolate;\n}\n\n.aside {\n grid-area: aside;\n min-height: 0;\n overflow-y: auto;\n border-left: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n:host(:not([has-aside])) .aside {\n display: none;\n}\n\n/* The bottom bar is for narrow screens: wide ones have the sidebar. */\n.tabs {\n display: none;\n}\n\n.scrim {\n display: none;\n}\n\n/* Narrow: the sidebar slides over the content as a drawer, the menu button opens it, the panel steps aside. */\n@container jl-app-shell (width < 960px) {\n .shell {\n grid-template:\n \"top\" auto\n \"main\" minmax(0, 1fr)\n \"tabs\" auto\n / minmax(0, 1fr);\n }\n\n :host([has-tabs]) .tabs {\n display: block;\n grid-area: tabs;\n }\n\n :host([has-nav]) .top {\n display: flex;\n }\n\n :host([has-nav]) .menu {\n display: inline-flex;\n }\n\n .nav {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n z-index: 2;\n max-width: var(--drawer-width);\n box-shadow: var(--jl-shadow-lg);\n transform: translateX(-100%);\n visibility: hidden;\n transition:\n transform var(--jl-slow) var(--jl-ease),\n visibility 0s linear var(--jl-slow);\n }\n\n :host([nav-open]) .nav {\n transform: none;\n visibility: visible;\n transition: transform var(--jl-slow) var(--jl-ease);\n }\n\n .scrim {\n position: absolute;\n inset: 0;\n z-index: 1;\n display: block;\n background: var(--jl-scrim);\n opacity: 0;\n pointer-events: none;\n transition: opacity var(--jl-slow) var(--jl-ease);\n }\n\n :host([nav-open]) .scrim {\n opacity: 1;\n pointer-events: auto;\n }\n\n .aside {\n display: none;\n }\n}\n";
2
+ var jl_app_shell_default = ":host {\n display: block;\n height: 100%;\n min-height: 0;\n container: jl-app-shell / inline-size;\n --header-height: 56px;\n --drawer-width: min(300px, 86%);\n}\n\n.shell {\n position: relative;\n display: grid;\n grid-template:\n \"top top top\" auto\n \"nav main aside\" minmax(0, 1fr)\n / auto minmax(0, 1fr) auto;\n height: 100%;\n overflow: hidden;\n background: var(--jl-bg);\n color: var(--jl-text);\n}\n\n.top {\n grid-area: top;\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: var(--header-height);\n padding: 0 16px;\n border-bottom: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n:host(:not([has-header])) .top,\n:host([narrow-header]) .top {\n display: none;\n}\n\n::slotted([slot=\"header\"]) {\n flex: 1;\n min-width: 0;\n}\n\n.menu {\n display: none;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--jl-control-md);\n height: var(--jl-control-md);\n margin-left: -8px;\n padding: 0;\n border: none;\n border-radius: var(--jl-radius-sm);\n background: none;\n color: var(--jl-text-2);\n cursor: pointer;\n}\n\n.menu:hover {\n background: var(--jl-surface-3);\n color: var(--jl-text);\n}\n\n.nav {\n grid-area: nav;\n min-height: 0;\n overflow-y: auto;\n border-right: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n.nav:focus-visible {\n box-shadow: none;\n}\n\n:host(:not([has-nav])) .nav {\n display: none;\n}\n\n/* Its own stacking context: what the page lifts with z-index (a sticky bar) stays under the drawer and its scrim. */\n.main {\n grid-area: main;\n min-width: 0;\n min-height: 0;\n overflow: auto;\n isolation: isolate;\n}\n\n.aside {\n grid-area: aside;\n min-height: 0;\n overflow-y: auto;\n border-left: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n:host(:not([has-aside])) .aside {\n display: none;\n}\n\n/* The bottom bar is for narrow screens: wide ones have the sidebar. */\n.tabs {\n display: none;\n}\n\n.scrim {\n display: none;\n}\n\n/* Narrow: the sidebar slides over the content as a drawer, the menu button opens it, the panel steps aside. */\n@container jl-app-shell (width < 960px) {\n .shell {\n grid-template:\n \"top\" auto\n \"main\" minmax(0, 1fr)\n \"tabs\" auto\n / minmax(0, 1fr);\n }\n\n :host([has-tabs]) .tabs {\n display: block;\n grid-area: tabs;\n }\n\n :host([has-nav]) .top,\n :host([narrow-header][has-header]) .top {\n display: flex;\n }\n\n :host([has-nav]) .menu {\n display: inline-flex;\n }\n\n .nav {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n z-index: 2;\n max-width: var(--drawer-width);\n box-shadow: var(--jl-shadow-lg);\n transform: translateX(-100%);\n visibility: hidden;\n transition:\n transform var(--jl-slow) var(--jl-ease),\n visibility 0s linear var(--jl-slow);\n }\n\n :host([nav-open]) .nav {\n transform: none;\n visibility: visible;\n transition: transform var(--jl-slow) var(--jl-ease);\n }\n\n .scrim {\n position: absolute;\n inset: 0;\n z-index: 1;\n display: block;\n background: var(--jl-scrim);\n opacity: 0;\n pointer-events: none;\n transition: opacity var(--jl-slow) var(--jl-ease);\n }\n\n :host([nav-open]) .scrim {\n opacity: 1;\n pointer-events: auto;\n }\n\n .aside {\n display: none;\n }\n}\n";
3
3
  export { jl_app_shell_default as default };
@@ -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";
@@ -16,6 +18,14 @@ export declare class JlAppShell extends JlElement {
16
18
  get(): boolean;
17
19
  set(setter: boolean | ((prevState: boolean) => boolean)): void;
18
20
  }>>;
21
+ /**
22
+ * The top bar only on narrow screens, as the navigation pattern lays it out: there it holds the menu button and
23
+ * the app's name; wide, the side nav's own header names the app.
24
+ */
25
+ readonly narrowHeader: Readonly<Readonly<{
26
+ get(): boolean;
27
+ set(setter: boolean | ((prevState: boolean) => boolean)): void;
28
+ }>>;
19
29
  private readonly _state;
20
30
  private readonly _resize;
21
31
  private readonly _refs;
@@ -25,6 +35,9 @@ export declare class JlAppShell extends JlElement {
25
35
  templateCss(): string;
26
36
  onConnect(): void;
27
37
  onDisconnect(): void;
38
+ /** Tells the page how much the tab bar covers at the bottom of the window, while it shows. */
39
+ private renderShellBottom;
40
+ private withdrawShellBottom;
28
41
  /** Whether the shell is narrow: the menu button shows only then. */
29
42
  private isNarrow;
30
43
  private onMenu;
@@ -40,4 +53,6 @@ export declare class JlAppShell extends JlElement {
40
53
  * `more` from the tabs (a tab bar's) opens it.
41
54
  */
42
55
  private renderSlots;
56
+ /** Narrow, the nav is a drawer: a side nav shows whole there, whether it is folded on a wide screen or not. */
57
+ private renderDrawer;
43
58
  }