@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.
- package/dist/all.js +11 -4
- package/dist/atoms/jl-app-icon.css_raw.js +3 -0
- package/dist/atoms/jl-app-icon.d.ts +34 -0
- package/dist/atoms/jl-app-icon.js +86 -0
- package/dist/atoms/jl-avatar.js +1 -1
- package/dist/atoms/jl-badge.js +9 -2
- package/dist/atoms/jl-button.d.ts +5 -0
- package/dist/atoms/jl-button.js +5 -2
- package/dist/atoms/jl-chip.js +3 -2
- package/dist/atoms/jl-input.js +5 -4
- package/dist/atoms/jl-link.js +2 -2
- package/dist/atoms/jl-rating.js +4 -5
- package/dist/atoms/jl-select.css_raw.js +1 -1
- package/dist/atoms/jl-select.d.ts +3 -2
- package/dist/atoms/jl-select.js +6 -5
- package/dist/atoms/jl-spinner.js +2 -2
- package/dist/base/jl-form-control.js +2 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -3
- package/dist/lib/app-icon.d.ts +8 -0
- package/dist/lib/app-icon.js +37 -0
- package/dist/lib/dom.d.ts +2 -0
- package/dist/lib/dom.js +3 -1
- package/dist/lib/download.d.ts +10 -0
- package/dist/lib/download.js +62 -0
- package/dist/lib/icons.d.ts +4 -0
- package/dist/lib/icons.js +7 -4
- package/dist/lib/input-list.d.ts +12 -0
- package/dist/lib/input-list.js +14 -0
- package/dist/lib/internals.d.ts +5 -0
- package/dist/lib/internals.js +18 -0
- package/dist/lib/palette.js +6 -0
- package/dist/lib/resize.d.ts +2 -0
- package/dist/lib/resize.js +10 -0
- package/dist/lib/table.d.ts +6 -1
- package/dist/lib/table.js +16 -1
- package/dist/lib/text.d.ts +12 -0
- package/dist/lib/text.js +16 -1
- package/dist/lib/texts.d.ts +97 -0
- package/dist/lib/texts.js +102 -0
- package/dist/lib/time.d.ts +5 -3
- package/dist/lib/time.js +23 -15
- package/dist/lib/tree.d.ts +10 -1
- package/dist/lib/tree.js +37 -3
- package/dist/lib/trigger.js +1 -1
- package/dist/molecules/jl-account-button.css_raw.js +1 -1
- package/dist/molecules/jl-account-button.d.ts +8 -1
- package/dist/molecules/jl-account-button.js +9 -2
- package/dist/molecules/jl-alert.js +3 -2
- package/dist/molecules/jl-breadcrumb.js +3 -3
- package/dist/molecules/jl-calendar.js +5 -5
- package/dist/molecules/jl-chip-group.js +3 -3
- package/dist/molecules/jl-code-block.css_raw.js +1 -1
- package/dist/molecules/jl-code-block.d.ts +5 -0
- package/dist/molecules/jl-code-block.js +5 -2
- package/dist/molecules/jl-combobox.js +6 -7
- package/dist/molecules/jl-copy-button.js +5 -4
- package/dist/molecules/jl-date-picker.js +7 -8
- package/dist/molecules/jl-empty.js +2 -2
- package/dist/molecules/jl-facts.css_raw.js +3 -0
- package/dist/molecules/jl-facts.d.ts +35 -0
- package/dist/molecules/jl-facts.js +64 -0
- package/dist/molecules/jl-file-drop.d.ts +5 -0
- package/dist/molecules/jl-file-drop.js +18 -15
- package/dist/molecules/jl-input-list.css_raw.js +3 -0
- package/dist/molecules/jl-input-list.d.ts +79 -0
- package/dist/molecules/jl-input-list.js +177 -0
- package/dist/molecules/jl-list-item.js +1 -1
- package/dist/molecules/jl-load-more.css_raw.js +3 -0
- package/dist/molecules/jl-load-more.d.ts +41 -0
- package/dist/molecules/jl-load-more.js +62 -0
- package/dist/molecules/jl-load-state.d.ts +4 -1
- package/dist/molecules/jl-load-state.js +20 -12
- package/dist/molecules/jl-menu.js +1 -1
- package/dist/molecules/jl-number-input.js +5 -5
- package/dist/molecules/jl-pagination.js +5 -5
- package/dist/molecules/jl-radio-group.js +2 -2
- package/dist/molecules/jl-search-button.js +5 -5
- package/dist/molecules/jl-search.js +5 -5
- package/dist/molecules/jl-segmented.js +3 -2
- package/dist/molecules/jl-steps.css_raw.js +1 -1
- package/dist/molecules/jl-steps.d.ts +2 -0
- package/dist/molecules/jl-steps.js +30 -14
- package/dist/molecules/jl-tabs.js +3 -2
- package/dist/molecules/jl-tag-input.js +3 -3
- package/dist/molecules/jl-timeline.js +1 -1
- package/dist/molecules/jl-toast.css_raw.js +1 -1
- package/dist/molecules/jl-toast.d.ts +13 -1
- package/dist/molecules/jl-toast.js +23 -3
- package/dist/molecules/jl-tooltip.d.ts +4 -1
- package/dist/molecules/jl-tooltip.js +27 -15
- package/dist/organisms/jl-command-palette.css_raw.js +1 -1
- package/dist/organisms/jl-command-palette.d.ts +14 -0
- package/dist/organisms/jl-command-palette.js +40 -10
- package/dist/organisms/jl-confirm.d.ts +2 -1
- package/dist/organisms/jl-confirm.js +18 -12
- package/dist/organisms/jl-dialog.js +2 -1
- package/dist/organisms/jl-drawer.js +2 -1
- package/dist/organisms/jl-flow.js +13 -13
- package/dist/organisms/jl-navbar.js +6 -6
- package/dist/organisms/jl-side-nav.css_raw.js +1 -1
- package/dist/organisms/jl-side-nav.d.ts +33 -1
- package/dist/organisms/jl-side-nav.js +71 -14
- package/dist/organisms/jl-tab-bar.js +3 -3
- package/dist/organisms/jl-table.css_raw.js +1 -1
- package/dist/organisms/jl-table.d.ts +9 -0
- package/dist/organisms/jl-table.js +36 -6
- package/dist/organisms/jl-toaster.css_raw.js +1 -1
- package/dist/organisms/jl-toaster.d.ts +9 -1
- package/dist/organisms/jl-toaster.js +148 -7
- package/dist/organisms/jl-tree.css_raw.js +1 -1
- package/dist/organisms/jl-tree.d.ts +71 -4
- package/dist/organisms/jl-tree.js +303 -40
- package/dist/styles/tokens.css_raw.js +1 -1
- package/dist/templates/jl-app-shell.css_raw.js +1 -1
- package/dist/templates/jl-app-shell.d.ts +16 -1
- package/dist/templates/jl-app-shell.js +42 -3
- package/dist/templates/jl-auth-layout.css_raw.js +3 -0
- package/dist/templates/jl-auth-layout.d.ts +35 -0
- package/dist/templates/jl-auth-layout.js +139 -0
- package/dist/templates/jl-page.css_raw.js +1 -1
- package/dist/templates/jl-page.js +3 -3
- package/dist/templates/jl-split-view.css_raw.js +1 -1
- package/dist/templates/jl-split-view.d.ts +21 -1
- package/dist/templates/jl-split-view.js +37 -4
- package/dist/tokens.css +2 -0
- package/dist/types/jl-app-icon.types.d.ts +5 -0
- package/dist/types/jl-command-palette.types.d.ts +7 -0
- package/dist/types/jl-facts.types.d.ts +17 -0
- package/dist/types/jl-side-nav.types.d.ts +2 -0
- package/dist/types/jl-split-view.types.d.ts +2 -0
- package/dist/types/jl-steps.types.d.ts +2 -0
- package/dist/types/jl-table.types.d.ts +9 -2
- package/dist/types/jl-toaster.types.d.ts +42 -1
- package/dist/types/jl-tree.types.d.ts +26 -0
- package/dist/types/texts.types.d.ts +5 -0
- 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.
|
|
6
|
-
*
|
|
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
|
-
/**
|
|
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 {
|
|
5
|
+
import { jlText } from "../lib/texts.js";
|
|
6
6
|
import { JlIcon } from "../atoms/jl-icon.js";
|
|
7
|
-
import {
|
|
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.
|
|
27
|
-
*
|
|
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([], () =>
|
|
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
|
-
/**
|
|
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(
|
|
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
|
|
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.
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
121
|
-
this.
|
|
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:
|
|
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
|
}
|