@syware/jlib 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/all.js +4 -2
- package/dist/atoms/jl-button.css_raw.js +1 -1
- package/dist/atoms/jl-button.d.ts +19 -0
- package/dist/atoms/jl-button.js +75 -23
- package/dist/atoms/jl-chip.css_raw.js +1 -1
- package/dist/atoms/jl-input.d.ts +5 -0
- package/dist/atoms/jl-input.js +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -1
- package/dist/lib/attributes.d.ts +5 -0
- package/dist/lib/attributes.js +13 -1
- package/dist/lib/tokens.d.ts +2 -1
- package/dist/lib/tokens.js +13 -4
- package/dist/molecules/jl-alert.d.ts +10 -3
- package/dist/molecules/jl-alert.js +23 -6
- package/dist/molecules/jl-chip-group.css_raw.js +3 -0
- package/dist/molecules/jl-chip-group.d.ts +81 -0
- package/dist/molecules/jl-chip-group.js +259 -0
- package/dist/molecules/jl-tag-input.css_raw.js +1 -1
- package/dist/organisms/jl-tab-bar.css_raw.js +3 -0
- package/dist/organisms/jl-tab-bar.d.ts +43 -0
- package/dist/organisms/jl-tab-bar.js +123 -0
- package/dist/templates/jl-app-shell.css_raw.js +1 -1
- package/dist/templates/jl-app-shell.d.ts +8 -2
- package/dist/templates/jl-app-shell.js +32 -5
- package/dist/types/jl-chip-group.types.d.ts +10 -0
- package/dist/types/jl-tab-bar.types.d.ts +11 -0
- package/package.json +1 -1
package/dist/all.js
CHANGED
|
@@ -34,6 +34,7 @@ import { JlAvatarGroup } from "./molecules/jl-avatar-group.js";
|
|
|
34
34
|
import { JlBreadcrumb } from "./molecules/jl-breadcrumb.js";
|
|
35
35
|
import { JlCalendar } from "./molecules/jl-calendar.js";
|
|
36
36
|
import { CARD_PADDINGS, JlCard } from "./molecules/jl-card.js";
|
|
37
|
+
import { JlChipGroup } from "./molecules/jl-chip-group.js";
|
|
37
38
|
import { JlCopyButton } from "./molecules/jl-copy-button.js";
|
|
38
39
|
import { CODE_LANGUAGES, JlCodeBlock } from "./molecules/jl-code-block.js";
|
|
39
40
|
import { JlCombobox } from "./molecules/jl-combobox.js";
|
|
@@ -65,6 +66,7 @@ import { JlFlow } from "./organisms/jl-flow.js";
|
|
|
65
66
|
import { JlNavbar } from "./organisms/jl-navbar.js";
|
|
66
67
|
import { JlPageHeader } from "./organisms/jl-page-header.js";
|
|
67
68
|
import { JlSideNav } from "./organisms/jl-side-nav.js";
|
|
69
|
+
import { JlTabBar } from "./organisms/jl-tab-bar.js";
|
|
68
70
|
import { JlTable, TABLE_SORT_DIRECTIONS } from "./organisms/jl-table.js";
|
|
69
71
|
import { JlToaster, TOASTER_PLACEMENTS, toast } from "./organisms/jl-toaster.js";
|
|
70
72
|
import { JlTree } from "./organisms/jl-tree.js";
|
|
@@ -72,5 +74,5 @@ import { JlAppShell } from "./templates/jl-app-shell.js";
|
|
|
72
74
|
import { JlSplitView, SPLIT_ORIENTATIONS } from "./templates/jl-split-view.js";
|
|
73
75
|
// src/all.ts
|
|
74
76
|
registerIcons(COMMON_ICONS);
|
|
75
|
-
define(JlAccordion, JlAccordionItem, JlAlert, JlAppShell, JlAvatar, JlAvatarGroup, JlBadge, JlBreadcrumb, JlButton, JlCalendar, JlCard, JlCheckbox, JlChip, JlCodeBlock, JlCombobox, JlCommandPalette, JlConfirm, JlCopyButton, JlDatePicker, JlDialog, JlDivider, JlDrawer, JlEmpty, JlField, JlFileDrop, JlFlow, JlIcon, JlInput, JlKbd, JlLink, JlListItem, JlMenu, JlNavbar, JlNumberInput, JlPageHeader, JlPagination, JlPopover, JlProgress, JlProgressRing, JlRadioGroup, JlRating, JlSearch, JlSegmented, JlSelect, JlSideNav, JlSkeleton, JlSlider, JlSpinner, JlSplitView, JlStat, JlSteps, JlSwitch, JlTable, JlTabPanel, JlTabs, JlTagInput, JlTextarea, JlTimeline, JlToast, JlToaster, JlTooltip, JlTree);
|
|
76
|
-
export { ACCORDION_VARIANTS, AVATAR_SHAPES, AVATAR_SIZES, AVATAR_STATUSES, BASE_STYLES, BUTTON_TYPES, BUTTON_VARIANTS, CARD_PADDINGS, CHIP_SIZES, CODE_LANGUAGES, COMMON_ICONS, CONFIRM_TONES, CONTROL_SIZES, CONTROL_STYLES, DIALOG_SIZES, DIVIDER_ALIGNS, DRAWER_PLACEMENTS, DRAWER_SIZES, EMPTY_SIZES, FIELD_ORIENTATIONS, FLOATING_STYLES, INPUT_TYPES, JlAccordion, JlAccordionItem, JlAlert, JlAppShell, JlAvatar, JlAvatarGroup, JlBadge, JlBreadcrumb, JlButton, JlCalendar, JlCard, JlCheckbox, JlChip, JlCodeBlock, JlCombobox, JlCommandPalette, JlConfirm, JlCopyButton, JlDatePicker, JlDialog, JlDivider, JlDrawer, JlElement, JlEmpty, JlField, JlFileDrop, JlFlow, JlFormControl, JlIcon, JlInput, JlKbd, JlLink, JlListItem, JlMenu, JlNavbar, JlNumberInput, JlPageHeader, JlPagination, JlPopover, JlProgress, JlProgressRing, JlRadioGroup, JlRating, JlSearch, JlSegmented, JlSelect, JlSideNav, JlSkeleton, JlSlider, JlSpinner, JlSplitView, JlStat, JlSteps, JlSwitch, JlTabPanel, JlTable, JlTabs, JlTagInput, JlTextarea, JlTimeline, JlToast, JlToaster, JlTooltip, JlTree, LINK_VARIANTS, PLACEMENTS, PROGRESS_SIZES, PROGRESS_TONES, RADIO_GROUP_ORIENTATIONS, RADIO_GROUP_VARIANTS, SKELETON_SHAPES, SPLIT_ORIENTATIONS, STAT_TRENDS, STEPS_ORIENTATIONS, SWITCH_SIZES, TABLE_SORT_DIRECTIONS, TAB_VARIANTS, TOASTER_PLACEMENTS, TOAST_TONES, TOKEN_STYLES, TONES, TONE_ICONS, TONE_ICON_FILLED, adoptTokens, clampedNumber, computePosition, confirmDialog, define, followAnchor, hasIcon, iconElement, iconNames, jsonOf, oneOf, placeFloating, registerIcons, toast };
|
|
77
|
+
define(JlAccordion, JlAccordionItem, JlAlert, JlAppShell, JlAvatar, JlAvatarGroup, JlBadge, JlBreadcrumb, JlButton, JlCalendar, JlCard, JlCheckbox, JlChip, JlChipGroup, JlCodeBlock, JlCombobox, JlCommandPalette, JlConfirm, JlCopyButton, JlDatePicker, JlDialog, JlDivider, JlDrawer, JlEmpty, JlField, JlFileDrop, JlFlow, JlIcon, JlInput, JlKbd, JlLink, JlListItem, JlMenu, JlNavbar, JlNumberInput, JlPageHeader, JlPagination, JlPopover, JlProgress, JlProgressRing, JlRadioGroup, JlRating, JlSearch, JlSegmented, JlSelect, JlSideNav, JlSkeleton, JlSlider, JlSpinner, JlSplitView, JlStat, JlSteps, JlSwitch, JlTabBar, JlTable, JlTabPanel, JlTabs, JlTagInput, JlTextarea, JlTimeline, JlToast, JlToaster, JlTooltip, JlTree);
|
|
78
|
+
export { ACCORDION_VARIANTS, AVATAR_SHAPES, AVATAR_SIZES, AVATAR_STATUSES, BASE_STYLES, BUTTON_TYPES, BUTTON_VARIANTS, CARD_PADDINGS, CHIP_SIZES, CODE_LANGUAGES, COMMON_ICONS, CONFIRM_TONES, CONTROL_SIZES, CONTROL_STYLES, DIALOG_SIZES, DIVIDER_ALIGNS, DRAWER_PLACEMENTS, DRAWER_SIZES, EMPTY_SIZES, FIELD_ORIENTATIONS, FLOATING_STYLES, INPUT_TYPES, JlAccordion, JlAccordionItem, JlAlert, JlAppShell, JlAvatar, JlAvatarGroup, JlBadge, JlBreadcrumb, JlButton, JlCalendar, JlCard, JlCheckbox, JlChip, JlChipGroup, JlCodeBlock, JlCombobox, JlCommandPalette, JlConfirm, JlCopyButton, JlDatePicker, JlDialog, JlDivider, JlDrawer, JlElement, JlEmpty, JlField, JlFileDrop, JlFlow, JlFormControl, JlIcon, JlInput, JlKbd, JlLink, JlListItem, JlMenu, JlNavbar, JlNumberInput, JlPageHeader, JlPagination, JlPopover, JlProgress, JlProgressRing, JlRadioGroup, JlRating, JlSearch, JlSegmented, JlSelect, JlSideNav, JlSkeleton, JlSlider, JlSpinner, JlSplitView, JlStat, JlSteps, JlSwitch, JlTabBar, JlTabPanel, JlTable, JlTabs, JlTagInput, JlTextarea, JlTimeline, JlToast, JlToaster, JlTooltip, JlTree, LINK_VARIANTS, PLACEMENTS, PROGRESS_SIZES, PROGRESS_TONES, RADIO_GROUP_ORIENTATIONS, RADIO_GROUP_VARIANTS, SKELETON_SHAPES, SPLIT_ORIENTATIONS, STAT_TRENDS, STEPS_ORIENTATIONS, SWITCH_SIZES, TABLE_SORT_DIRECTIONS, TAB_VARIANTS, TOASTER_PLACEMENTS, TOAST_TONES, TOKEN_STYLES, TONES, TONE_ICONS, TONE_ICON_FILLED, adoptTokens, clampedNumber, computePosition, confirmDialog, define, followAnchor, hasIcon, iconElement, iconNames, jsonOf, oneOf, placeFloating, registerIcons, toast };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// src/atoms/jl-button.css?raw
|
|
2
|
-
var jl_button_default = ":host {\n display: inline-flex;\n vertical-align: middle;\n --height: var(--jl-control-md);\n --pad: 14px;\n --font-size: var(--jl-font-md);\n --radius: var(--jl-radius-sm);\n}\n\n:host([size=\"sm\"]) {\n --height: var(--jl-control-sm);\n --pad: 11px;\n --font-size: var(--jl-font-xs);\n}\n\n:host([size=\"lg\"]) {\n --height: var(--jl-control-lg);\n --pad: 18px;\n --font-size: 15px;\n --radius: var(--jl-radius);\n}\n\n:host([block]) {\n display: flex;\n}\n\
|
|
2
|
+
var jl_button_default = ":host {\n display: inline-flex;\n vertical-align: middle;\n --height: var(--jl-control-md);\n --pad: 14px;\n --font-size: var(--jl-font-md);\n --radius: var(--jl-radius-sm);\n}\n\n:host([size=\"sm\"]) {\n --height: var(--jl-control-sm);\n --pad: 11px;\n --font-size: var(--jl-font-xs);\n}\n\n:host([size=\"lg\"]) {\n --height: var(--jl-control-lg);\n --pad: 18px;\n --font-size: 15px;\n --radius: var(--jl-radius);\n}\n\n:host([block]) {\n display: flex;\n}\n\n/* Moves between the button and the link: its children are laid out by whichever holds it. */\n.content {\n display: contents;\n}\n\n.base {\n display: inline-flex;\n flex: 1;\n align-items: center;\n justify-content: center;\n gap: 8px;\n height: var(--height);\n padding: 0 var(--pad);\n border: 1px solid var(--jl-line-strong);\n border-radius: var(--radius);\n background: var(--jl-surface);\n color: var(--jl-text);\n font-size: var(--font-size);\n font-weight: 600;\n line-height: 1;\n text-decoration: none;\n white-space: nowrap;\n cursor: pointer;\n transition:\n background var(--jl-fast) var(--jl-ease),\n border-color var(--jl-fast) var(--jl-ease),\n color var(--jl-fast) var(--jl-ease);\n}\n\n:host([icon-only]) .base {\n width: var(--height);\n padding: 0;\n}\n\n.base:hover {\n background: var(--jl-surface-2);\n}\n\n.base:active {\n background: var(--jl-surface-3);\n}\n\n:host([variant=\"primary\"]) .base {\n border-color: transparent;\n background: var(--jl-accent);\n color: var(--jl-on-accent);\n}\n\n:host([variant=\"primary\"]) .base:hover {\n background: var(--jl-accent-strong);\n}\n\n:host([variant=\"ghost\"]) .base {\n border-color: transparent;\n background: transparent;\n color: var(--jl-accent);\n}\n\n:host([variant=\"ghost\"]) .base:hover {\n background: var(--jl-accent-soft);\n}\n\n:host([variant=\"danger\"]) .base {\n border-color: transparent;\n background: var(--jl-danger);\n color: var(--jl-on-danger);\n}\n\n:host([variant=\"danger\"]) .base:hover {\n background: color-mix(in srgb, var(--jl-danger) 86%, var(--jl-text));\n}\n\n:host([loading]) .base {\n cursor: progress;\n}\n\n.base:disabled,\n.base[aria-disabled=\"true\"] {\n cursor: not-allowed;\n opacity: 0.5;\n}\n";
|
|
3
3
|
export { jl_button_default as default };
|
|
@@ -6,6 +6,7 @@ export declare const BUTTON_TYPES: readonly ButtonType[];
|
|
|
6
6
|
/**
|
|
7
7
|
* A button. Its text is the `label` (or the slotted content); with only an `icon`, its `hint` names it.
|
|
8
8
|
* Listen to `press`: it fires only while the button can be pressed. `type="submit"` submits the surrounding form.
|
|
9
|
+
* With an `href` it is a link that looks the same: it opens the address, and `press` still fires.
|
|
9
10
|
*/
|
|
10
11
|
export declare class JlButton extends JlElement {
|
|
11
12
|
static readonly selector = "jl-button";
|
|
@@ -56,12 +57,30 @@ export declare class JlButton extends JlElement {
|
|
|
56
57
|
get(): boolean;
|
|
57
58
|
set(setter: boolean | ((prevState: boolean) => boolean)): void;
|
|
58
59
|
}>>;
|
|
60
|
+
/** An address to open: the button becomes a link, for navigation that should open in a new tab when asked. */
|
|
61
|
+
readonly href: Readonly<Readonly<{
|
|
62
|
+
get(): string;
|
|
63
|
+
set(setter: string | ((prevState: string) => string)): void;
|
|
64
|
+
}>>;
|
|
65
|
+
/** Where the link opens (`_blank` for a new tab), with an `href`. */
|
|
66
|
+
readonly target: Readonly<Readonly<{
|
|
67
|
+
get(): string;
|
|
68
|
+
set(setter: string | ((prevState: string) => string)): void;
|
|
69
|
+
}>>;
|
|
59
70
|
private readonly _internals;
|
|
60
71
|
private readonly _refs;
|
|
61
72
|
focus(options?: FocusOptions): void;
|
|
62
73
|
templateHtml(): Node;
|
|
63
74
|
templateCss(): string;
|
|
64
75
|
onConnect(): void;
|
|
76
|
+
private canPress;
|
|
77
|
+
/** The element that takes focus and clicks: the link with an `href`, the button otherwise. */
|
|
78
|
+
private control;
|
|
79
|
+
/**
|
|
80
|
+
* Moves the content into the link or back into the button. A disabled link loses its address, so it can be
|
|
81
|
+
* neither followed nor focused, and says it is disabled.
|
|
82
|
+
*/
|
|
83
|
+
private renderLink;
|
|
65
84
|
private actOnForm;
|
|
66
85
|
private renderIcon;
|
|
67
86
|
private renderSize;
|
package/dist/atoms/jl-button.js
CHANGED
|
@@ -2,11 +2,12 @@ import { BASE_STYLES } from "../styles/shared.js";
|
|
|
2
2
|
import { JlElement } from "../base/jl-element.js";
|
|
3
3
|
import { oneOf } from "../lib/attributes.js";
|
|
4
4
|
import { CONTROL_SIZES } from "../lib/options.js";
|
|
5
|
+
import { setOrRemove } from "../lib/dom.js";
|
|
5
6
|
import { JlIcon } from "./jl-icon.js";
|
|
6
7
|
import jl_button_default from "./jl-button.css_raw.js";
|
|
7
8
|
import { JlSpinner } from "./jl-spinner.js";
|
|
8
9
|
import { css } from "@jadis/core";
|
|
9
|
-
import { jsx, jsxs } from "@jadis/core/jsx-runtime";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "@jadis/core/jsx-runtime";
|
|
10
11
|
// src/atoms/jl-button.tsx
|
|
11
12
|
var BUTTON_VARIANTS = [
|
|
12
13
|
"secondary",
|
|
@@ -24,9 +25,13 @@ var ICON_SIZES = {
|
|
|
24
25
|
md: 16,
|
|
25
26
|
sm: 14
|
|
26
27
|
};
|
|
28
|
+
var NEW_TAB = "_blank";
|
|
29
|
+
/** A page opened in a new tab gets no handle on this one. */
|
|
30
|
+
var SAFE_REL = "noopener noreferrer";
|
|
27
31
|
/**
|
|
28
32
|
* A button. Its text is the `label` (or the slotted content); with only an `icon`, its `hint` names it.
|
|
29
33
|
* Listen to `press`: it fires only while the button can be pressed. `type="submit"` submits the surrounding form.
|
|
34
|
+
* With an `href` it is a link that looks the same: it opens the address, and `press` still fires.
|
|
30
35
|
*/
|
|
31
36
|
var JlButton = class extends JlElement {
|
|
32
37
|
static selector = "jl-button";
|
|
@@ -49,6 +54,7 @@ var JlButton = class extends JlElement {
|
|
|
49
54
|
});
|
|
50
55
|
disabled = this.useChange(false, (value) => {
|
|
51
56
|
this._refs.button.disabled = value;
|
|
57
|
+
this.renderLink();
|
|
52
58
|
this.reflect("disabled", value);
|
|
53
59
|
}, { attribute: "disabled" });
|
|
54
60
|
/** Shows a spinner in place of the icon and ignores presses, while the work it started runs. */
|
|
@@ -56,42 +62,57 @@ var JlButton = class extends JlElement {
|
|
|
56
62
|
this._refs.spinner.hidden = !value;
|
|
57
63
|
this._refs.start.hidden = value || this.icon.get() === "";
|
|
58
64
|
this._refs.button.toggleAttribute("aria-busy", value);
|
|
65
|
+
this._refs.link.toggleAttribute("aria-busy", value);
|
|
59
66
|
this.reflect("loading", value);
|
|
60
67
|
}, { attribute: "loading" });
|
|
61
68
|
/** The tooltip; also the button's name for screen readers when it only has an icon. */
|
|
62
69
|
hint = this.useChange("", () => this.renderLabel(), { attribute: "hint" });
|
|
63
70
|
/** Takes the whole width of its container. */
|
|
64
71
|
block = this.useChange(false, (value) => this.reflect("block", value), { attribute: "block" });
|
|
72
|
+
/** An address to open: the button becomes a link, for navigation that should open in a new tab when asked. */
|
|
73
|
+
href = this.useChange("", () => this.renderLink(), { attribute: "href" });
|
|
74
|
+
/** Where the link opens (`_blank` for a new tab), with an `href`. */
|
|
75
|
+
target = this.useChange("", () => this.renderLink(), { attribute: "target" });
|
|
65
76
|
_internals = this.attachInternals();
|
|
66
77
|
_refs = this.useRefs((ref) => ({
|
|
67
78
|
button: ref("button"),
|
|
79
|
+
content: ref(".content"),
|
|
68
80
|
end: ref(".end"),
|
|
69
81
|
label: ref(".text"),
|
|
82
|
+
link: ref("a"),
|
|
70
83
|
slot: ref("slot"),
|
|
71
84
|
spinner: ref("jl-spinner"),
|
|
72
85
|
start: ref(".start")
|
|
73
86
|
}));
|
|
74
87
|
focus(options) {
|
|
75
|
-
this.
|
|
88
|
+
this.control().focus(options);
|
|
76
89
|
}
|
|
77
90
|
templateHtml() {
|
|
78
|
-
return /* @__PURE__ */ jsxs("button", {
|
|
91
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
|
|
92
|
+
class: "base",
|
|
79
93
|
type: "button",
|
|
80
94
|
attrs: { part: "base" },
|
|
81
|
-
children:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
96
|
+
class: "content",
|
|
97
|
+
children: [
|
|
98
|
+
/* @__PURE__ */ jsx(JlSpinner, { hidden: true }),
|
|
99
|
+
/* @__PURE__ */ jsx(JlIcon, {
|
|
100
|
+
class: "start",
|
|
101
|
+
hidden: true
|
|
102
|
+
}),
|
|
103
|
+
/* @__PURE__ */ jsx("span", { class: "text" }),
|
|
104
|
+
/* @__PURE__ */ jsx("slot", {}),
|
|
105
|
+
/* @__PURE__ */ jsx(JlIcon, {
|
|
106
|
+
class: "end",
|
|
107
|
+
hidden: true
|
|
108
|
+
})
|
|
109
|
+
]
|
|
110
|
+
})
|
|
111
|
+
}), /* @__PURE__ */ jsx("a", {
|
|
112
|
+
class: "base",
|
|
113
|
+
hidden: true,
|
|
114
|
+
attrs: { part: "base" }
|
|
115
|
+
})] });
|
|
95
116
|
}
|
|
96
117
|
templateCss() {
|
|
97
118
|
return css`${BASE_STYLES}${jl_button_default}`;
|
|
@@ -99,10 +120,43 @@ var JlButton = class extends JlElement {
|
|
|
99
120
|
onConnect() {
|
|
100
121
|
this.on(this._refs.slot, "slotchange", () => this.renderLabel());
|
|
101
122
|
this.on(this._refs.button, "click", () => {
|
|
102
|
-
if (this.
|
|
123
|
+
if (!this.canPress()) return;
|
|
103
124
|
this.events.emit("press");
|
|
104
125
|
this.actOnForm();
|
|
105
126
|
});
|
|
127
|
+
this.on(this._refs.link, "click", (event) => {
|
|
128
|
+
if (!this.canPress()) {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.events.emit("press");
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
canPress() {
|
|
136
|
+
return !(this.disabled.get() || this.loading.get());
|
|
137
|
+
}
|
|
138
|
+
/** The element that takes focus and clicks: the link with an `href`, the button otherwise. */
|
|
139
|
+
control() {
|
|
140
|
+
return this.href.get() === "" ? this._refs.button : this._refs.link;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Moves the content into the link or back into the button. A disabled link loses its address, so it can be
|
|
144
|
+
* neither followed nor focused, and says it is disabled.
|
|
145
|
+
*/
|
|
146
|
+
renderLink() {
|
|
147
|
+
const { button, content, link } = this._refs;
|
|
148
|
+
const href = this.href.get();
|
|
149
|
+
const isLink = href !== "";
|
|
150
|
+
const target = this.target.get();
|
|
151
|
+
const off = isLink && this.disabled.get();
|
|
152
|
+
this.control().append(content);
|
|
153
|
+
button.hidden = isLink;
|
|
154
|
+
link.hidden = !isLink;
|
|
155
|
+
setOrRemove(link, "href", isLink && !off ? href : "");
|
|
156
|
+
setOrRemove(link, "target", target);
|
|
157
|
+
setOrRemove(link, "rel", target === NEW_TAB ? SAFE_REL : "");
|
|
158
|
+
setOrRemove(link, "role", off ? "link" : "");
|
|
159
|
+
setOrRemove(link, "aria-disabled", off ? "true" : "");
|
|
106
160
|
}
|
|
107
161
|
actOnForm() {
|
|
108
162
|
const form = this._internals.form;
|
|
@@ -133,13 +187,11 @@ var JlButton = class extends JlElement {
|
|
|
133
187
|
const iconOnly = label === "" && !this.hasSlottedContent();
|
|
134
188
|
this._refs.label.textContent = label;
|
|
135
189
|
this._refs.label.hidden = label === "";
|
|
136
|
-
this._refs.button.title = hint;
|
|
137
190
|
this.reflect("icon-only", iconOnly);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
191
|
+
for (const control of [this._refs.button, this._refs.link]) {
|
|
192
|
+
setOrRemove(control, "title", hint);
|
|
193
|
+
setOrRemove(control, "aria-label", iconOnly ? hint : "");
|
|
141
194
|
}
|
|
142
|
-
this._refs.button.removeAttribute("aria-label");
|
|
143
195
|
}
|
|
144
196
|
};
|
|
145
197
|
JlButton.register();
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// src/atoms/jl-chip.css?raw
|
|
2
|
-
var jl_chip_default = ":host {\n display: inline-flex;\n max-width: 100%;\n vertical-align: middle;\n --height: 28px;\n --pad: 10px;\n --font-size: var(--jl-font-sm);\n --dismiss: 20px;\n}\n\n:host([size=\"sm\"]) {\n --height: 24px;\n --pad: 8px;\n --font-size: var(--jl-font-xs);\n --dismiss: 18px;\n}\n\n.chip {\n display: inline-flex;\n align-items: center;\n min-width: 0;\n height: var(--height);\n border: 1px solid var(--jl-line-strong);\n border-radius: var(--jl-radius-pill);\n background: var(--jl-surface);\n color: var(--jl-text-2);\n font-size: var(--font-size);\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n transition:\n background var(--jl-fast) var(--jl-ease),\n border-color var(--jl-fast) var(--jl-ease),\n color var(--jl-fast) var(--jl-ease);\n}\n\n.main {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n height: 100%;\n padding: 0 var(--pad);\n border-radius: inherit;\n}\n\n.label {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.label:empty {\n display: none;\n}\n\n:host([removable]) .main {\n padding-right: 4px;\n}\n\n:host([selectable]) .main {\n cursor: pointer;\n}\n\n:host([selectable]:not([disabled])) .chip:hover {\n background: var(--jl-surface-2);\n}\n\n:host([selected]) .chip,\n:host([selected][selectable]:not([disabled])) .chip:hover {\n border-color: var(--jl-accent-line);\n background: var(--jl-accent-soft);\n color: var(--jl-accent);\n}\n\n/* The ring goes around the whole chip, which the main part fills. */\n.main:focus-visible {\n box-shadow: none;\n}\n\n.chip:has(.main:focus-visible) {\n box-shadow: var(--jl-focus-ring);\n}\n\n.dismiss {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--dismiss);\n height: var(--dismiss);\n margin-right: 3px;\n padding: 0;\n border: 0;\n border-radius: 50%;\n background: transparent;\n color: var(--jl-muted);\n cursor: pointer;\n}\n\n.dismiss:hover {\n background: var(--jl-surface-3);\n color: var(--jl-text);\n}\n\n:host([selected]) .dismiss {\n color: var(--jl-accent);\n}\n\n:host([selected]) .dismiss:hover {\n background: color-mix(in srgb, var(--jl-accent) 14%, transparent);\n color: var(--jl-accent-strong);\n}\n\n:host([disabled]) .chip {\n opacity: 0.5;\n}\n\n:host([disabled]) .main,\n:host([disabled]) .dismiss {\n cursor: not-allowed;\n}\n\n:host([disabled]) .dismiss:hover {\n background: transparent;\n}\n";
|
|
2
|
+
var jl_chip_default = ":host {\n display: inline-flex;\n max-width: 100%;\n vertical-align: middle;\n --height: 28px;\n --pad: 10px;\n --font-size: var(--jl-font-sm);\n --dismiss: 20px;\n}\n\n:host([size=\"sm\"]) {\n --height: 24px;\n --pad: 8px;\n --font-size: var(--jl-font-xs);\n --dismiss: 18px;\n}\n\n.chip {\n display: inline-flex;\n align-items: center;\n min-width: 0;\n height: var(--height);\n border: 1px solid var(--jl-line-strong);\n border-radius: var(--jl-radius-pill);\n background: var(--jl-surface);\n color: var(--jl-text-2);\n font-size: var(--font-size);\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n transition:\n background var(--jl-fast) var(--jl-ease),\n border-color var(--jl-fast) var(--jl-ease),\n color var(--jl-fast) var(--jl-ease);\n}\n\n.main {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n height: 100%;\n padding: 0 var(--pad);\n border-radius: inherit;\n}\n\n/* A line box taller than the chip's own line-height of 1: the accents of É, Â, Ö stay inside what overflow clips. */\n.label {\n overflow: hidden;\n line-height: 1.4;\n text-overflow: ellipsis;\n}\n\n.label:empty {\n display: none;\n}\n\n:host([removable]) .main {\n padding-right: 4px;\n}\n\n:host([selectable]) .main {\n cursor: pointer;\n}\n\n:host([selectable]:not([disabled])) .chip:hover {\n background: var(--jl-surface-2);\n}\n\n:host([selected]) .chip,\n:host([selected][selectable]:not([disabled])) .chip:hover {\n border-color: var(--jl-accent-line);\n background: var(--jl-accent-soft);\n color: var(--jl-accent);\n}\n\n/* The ring goes around the whole chip, which the main part fills. */\n.main:focus-visible {\n box-shadow: none;\n}\n\n.chip:has(.main:focus-visible) {\n box-shadow: var(--jl-focus-ring);\n}\n\n.dismiss {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--dismiss);\n height: var(--dismiss);\n margin-right: 3px;\n padding: 0;\n border: 0;\n border-radius: 50%;\n background: transparent;\n color: var(--jl-muted);\n cursor: pointer;\n}\n\n.dismiss:hover {\n background: var(--jl-surface-3);\n color: var(--jl-text);\n}\n\n:host([selected]) .dismiss {\n color: var(--jl-accent);\n}\n\n:host([selected]) .dismiss:hover {\n background: color-mix(in srgb, var(--jl-accent) 14%, transparent);\n color: var(--jl-accent-strong);\n}\n\n:host([disabled]) .chip {\n opacity: 0.5;\n}\n\n:host([disabled]) .main,\n:host([disabled]) .dismiss {\n cursor: not-allowed;\n}\n\n:host([disabled]) .dismiss:hover {\n background: transparent;\n}\n";
|
|
3
3
|
export { jl_chip_default as default };
|
package/dist/atoms/jl-input.d.ts
CHANGED
|
@@ -34,6 +34,11 @@ export declare class JlInput extends JlFormControl {
|
|
|
34
34
|
get(): string;
|
|
35
35
|
set(setter: string | ((prevState: string) => string)): void;
|
|
36
36
|
}>>;
|
|
37
|
+
/** The keyboard a phone shows for the field (`numeric`, `decimal`, `tel`…), as a native input's `inputmode`. */
|
|
38
|
+
readonly inputmode: Readonly<Readonly<{
|
|
39
|
+
get(): string;
|
|
40
|
+
set(setter: string | ((prevState: string) => string)): void;
|
|
41
|
+
}>>;
|
|
37
42
|
readonly min: Readonly<Readonly<{
|
|
38
43
|
get(): string;
|
|
39
44
|
set(setter: string | ((prevState: string) => string)): void;
|
package/dist/atoms/jl-input.js
CHANGED
|
@@ -50,6 +50,8 @@ var JlInput = class extends JlFormControl {
|
|
|
50
50
|
/** The field's name for screen readers, when no visible label names it. */
|
|
51
51
|
label = this.useChange("", (value) => this.setNative("aria-label", value), { attribute: "label" });
|
|
52
52
|
autocomplete = this.useChange("", (value) => this.setNative("autocomplete", value), { attribute: "autocomplete" });
|
|
53
|
+
/** The keyboard a phone shows for the field (`numeric`, `decimal`, `tel`…), as a native input's `inputmode`. */
|
|
54
|
+
inputmode = this.useChange("", (value) => this.setNative("inputmode", value), { attribute: "inputmode" });
|
|
53
55
|
min = this.useChange("", (value) => this.setNative("min", value), { attribute: "min" });
|
|
54
56
|
max = this.useChange("", (value) => this.setNative("max", value), { attribute: "max" });
|
|
55
57
|
step = this.useChange("", (value) => this.setNative("step", value), { attribute: "step" });
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type * from './types/jl-avatar.types';
|
|
|
19
19
|
export type * from './types/jl-breadcrumb.types';
|
|
20
20
|
export type * from './types/jl-button.types';
|
|
21
21
|
export type * from './types/jl-card.types';
|
|
22
|
+
export type * from './types/jl-chip-group.types';
|
|
22
23
|
export type * from './types/jl-chip.types';
|
|
23
24
|
export type * from './types/jl-combobox.types';
|
|
24
25
|
export type * from './types/jl-command-palette.types';
|
|
@@ -44,6 +45,7 @@ export type * from './types/jl-split-view.types';
|
|
|
44
45
|
export type * from './types/jl-stat.types';
|
|
45
46
|
export type * from './types/jl-steps.types';
|
|
46
47
|
export type * from './types/jl-switch.types';
|
|
48
|
+
export type * from './types/jl-tab-bar.types';
|
|
47
49
|
export type * from './types/jl-table.types';
|
|
48
50
|
export type * from './types/jl-tabs.types';
|
|
49
51
|
export type * from './types/jl-timeline.types';
|
|
@@ -83,6 +85,7 @@ export * from './molecules/jl-avatar-group';
|
|
|
83
85
|
export * from './molecules/jl-breadcrumb';
|
|
84
86
|
export * from './molecules/jl-calendar';
|
|
85
87
|
export * from './molecules/jl-card';
|
|
88
|
+
export * from './molecules/jl-chip-group';
|
|
86
89
|
export * from './molecules/jl-code-block';
|
|
87
90
|
export * from './molecules/jl-combobox';
|
|
88
91
|
export * from './molecules/jl-copy-button';
|
|
@@ -114,6 +117,7 @@ export * from './organisms/jl-flow';
|
|
|
114
117
|
export * from './organisms/jl-navbar';
|
|
115
118
|
export * from './organisms/jl-page-header';
|
|
116
119
|
export * from './organisms/jl-side-nav';
|
|
120
|
+
export * from './organisms/jl-tab-bar';
|
|
117
121
|
export * from './organisms/jl-table';
|
|
118
122
|
export * from './organisms/jl-toaster';
|
|
119
123
|
export * from './organisms/jl-tree';
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { JlAvatarGroup } from "./molecules/jl-avatar-group.js";
|
|
|
34
34
|
import { JlBreadcrumb } from "./molecules/jl-breadcrumb.js";
|
|
35
35
|
import { JlCalendar } from "./molecules/jl-calendar.js";
|
|
36
36
|
import { CARD_PADDINGS, JlCard } from "./molecules/jl-card.js";
|
|
37
|
+
import { JlChipGroup } from "./molecules/jl-chip-group.js";
|
|
37
38
|
import { JlCopyButton } from "./molecules/jl-copy-button.js";
|
|
38
39
|
import { CODE_LANGUAGES, JlCodeBlock } from "./molecules/jl-code-block.js";
|
|
39
40
|
import { JlCombobox } from "./molecules/jl-combobox.js";
|
|
@@ -65,9 +66,10 @@ import { JlFlow } from "./organisms/jl-flow.js";
|
|
|
65
66
|
import { JlNavbar } from "./organisms/jl-navbar.js";
|
|
66
67
|
import { JlPageHeader } from "./organisms/jl-page-header.js";
|
|
67
68
|
import { JlSideNav } from "./organisms/jl-side-nav.js";
|
|
69
|
+
import { JlTabBar } from "./organisms/jl-tab-bar.js";
|
|
68
70
|
import { JlTable, TABLE_SORT_DIRECTIONS } from "./organisms/jl-table.js";
|
|
69
71
|
import { JlToaster, TOASTER_PLACEMENTS, toast } from "./organisms/jl-toaster.js";
|
|
70
72
|
import { JlTree } from "./organisms/jl-tree.js";
|
|
71
73
|
import { JlAppShell } from "./templates/jl-app-shell.js";
|
|
72
74
|
import { JlSplitView, SPLIT_ORIENTATIONS } from "./templates/jl-split-view.js";
|
|
73
|
-
export { ACCORDION_VARIANTS, AVATAR_SHAPES, AVATAR_SIZES, AVATAR_STATUSES, BASE_STYLES, BUTTON_TYPES, BUTTON_VARIANTS, CARD_PADDINGS, CHIP_SIZES, CODE_LANGUAGES, COMMON_ICONS, CONFIRM_TONES, CONTROL_SIZES, CONTROL_STYLES, DIALOG_SIZES, DIVIDER_ALIGNS, DRAWER_PLACEMENTS, DRAWER_SIZES, EMPTY_SIZES, FIELD_ORIENTATIONS, FLOATING_STYLES, INPUT_TYPES, JlAccordion, JlAccordionItem, JlAlert, JlAppShell, JlAvatar, JlAvatarGroup, JlBadge, JlBreadcrumb, JlButton, JlCalendar, JlCard, JlCheckbox, JlChip, JlCodeBlock, JlCombobox, JlCommandPalette, JlConfirm, JlCopyButton, JlDatePicker, JlDialog, JlDivider, JlDrawer, JlElement, JlEmpty, JlField, JlFileDrop, JlFlow, JlFormControl, JlIcon, JlInput, JlKbd, JlLink, JlListItem, JlMenu, JlNavbar, JlNumberInput, JlPageHeader, JlPagination, JlPopover, JlProgress, JlProgressRing, JlRadioGroup, JlRating, JlSearch, JlSegmented, JlSelect, JlSideNav, JlSkeleton, JlSlider, JlSpinner, JlSplitView, JlStat, JlSteps, JlSwitch, JlTabPanel, JlTable, JlTabs, JlTagInput, JlTextarea, JlTimeline, JlToast, JlToaster, JlTooltip, JlTree, LINK_VARIANTS, PLACEMENTS, PROGRESS_SIZES, PROGRESS_TONES, RADIO_GROUP_ORIENTATIONS, RADIO_GROUP_VARIANTS, SKELETON_SHAPES, SPLIT_ORIENTATIONS, STAT_TRENDS, STEPS_ORIENTATIONS, SWITCH_SIZES, TABLE_SORT_DIRECTIONS, TAB_VARIANTS, TOASTER_PLACEMENTS, TOAST_TONES, TOKEN_STYLES, TONES, TONE_ICONS, TONE_ICON_FILLED, adoptTokens, clampedNumber, computePosition, confirmDialog, define, followAnchor, hasIcon, iconElement, iconNames, jsonOf, oneOf, placeFloating, registerIcons, toast };
|
|
75
|
+
export { ACCORDION_VARIANTS, AVATAR_SHAPES, AVATAR_SIZES, AVATAR_STATUSES, BASE_STYLES, BUTTON_TYPES, BUTTON_VARIANTS, CARD_PADDINGS, CHIP_SIZES, CODE_LANGUAGES, COMMON_ICONS, CONFIRM_TONES, CONTROL_SIZES, CONTROL_STYLES, DIALOG_SIZES, DIVIDER_ALIGNS, DRAWER_PLACEMENTS, DRAWER_SIZES, EMPTY_SIZES, FIELD_ORIENTATIONS, FLOATING_STYLES, INPUT_TYPES, JlAccordion, JlAccordionItem, JlAlert, JlAppShell, JlAvatar, JlAvatarGroup, JlBadge, JlBreadcrumb, JlButton, JlCalendar, JlCard, JlCheckbox, JlChip, JlChipGroup, JlCodeBlock, JlCombobox, JlCommandPalette, JlConfirm, JlCopyButton, JlDatePicker, JlDialog, JlDivider, JlDrawer, JlElement, JlEmpty, JlField, JlFileDrop, JlFlow, JlFormControl, JlIcon, JlInput, JlKbd, JlLink, JlListItem, JlMenu, JlNavbar, JlNumberInput, JlPageHeader, JlPagination, JlPopover, JlProgress, JlProgressRing, JlRadioGroup, JlRating, JlSearch, JlSegmented, JlSelect, JlSideNav, JlSkeleton, JlSlider, JlSpinner, JlSplitView, JlStat, JlSteps, JlSwitch, JlTabBar, JlTabPanel, JlTable, JlTabs, JlTagInput, JlTextarea, JlTimeline, JlToast, JlToaster, JlTooltip, JlTree, LINK_VARIANTS, PLACEMENTS, PROGRESS_SIZES, PROGRESS_TONES, RADIO_GROUP_ORIENTATIONS, RADIO_GROUP_VARIANTS, SKELETON_SHAPES, SPLIT_ORIENTATIONS, STAT_TRENDS, STEPS_ORIENTATIONS, SWITCH_SIZES, TABLE_SORT_DIRECTIONS, TAB_VARIANTS, TOASTER_PLACEMENTS, TOAST_TONES, TOKEN_STYLES, TONES, TONE_ICONS, TONE_ICON_FILLED, adoptTokens, clampedNumber, computePosition, confirmDialog, define, followAnchor, hasIcon, iconElement, iconNames, jsonOf, oneOf, placeFloating, registerIcons, toast };
|
package/dist/lib/attributes.d.ts
CHANGED
|
@@ -7,3 +7,8 @@ export declare const oneOf: <Value extends string>(allowed: readonly Value[], fa
|
|
|
7
7
|
export declare const jsonOf: <Value>(fallback: Value) => (value: string | null) => Value;
|
|
8
8
|
/** A number attribute kept within bounds; a missing or unreadable one gives the fallback. */
|
|
9
9
|
export declare const clampedNumber: (fallback: number, min: number, max: number) => (value: string | null) => number;
|
|
10
|
+
/**
|
|
11
|
+
* An attribute holding a list of strings, as HTML writes it: a JSON list (`'["a","b"]'`), or one plain value
|
|
12
|
+
* (`open`) for the common case of a single one. Empty or unreadable gives an empty list.
|
|
13
|
+
*/
|
|
14
|
+
export declare const stringList: (value: string | null) => string[];
|
package/dist/lib/attributes.js
CHANGED
|
@@ -19,4 +19,16 @@ var clampedNumber = (fallback, min, max) => (value) => {
|
|
|
19
19
|
if (Number.isNaN(parsed)) return fallback;
|
|
20
20
|
return Math.min(max, Math.max(min, parsed));
|
|
21
21
|
};
|
|
22
|
-
|
|
22
|
+
var LIST_START = "[";
|
|
23
|
+
/**
|
|
24
|
+
* An attribute holding a list of strings, as HTML writes it: a JSON list (`'["a","b"]'`), or one plain value
|
|
25
|
+
* (`open`) for the common case of a single one. Empty or unreadable gives an empty list.
|
|
26
|
+
*/
|
|
27
|
+
var stringList = (value) => {
|
|
28
|
+
const text = value?.trim() ?? "";
|
|
29
|
+
if (text === "") return [];
|
|
30
|
+
if (!text.startsWith(LIST_START)) return [text];
|
|
31
|
+
const parsed = jsonOf([])(text);
|
|
32
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
33
|
+
};
|
|
34
|
+
export { clampedNumber, jsonOf, oneOf, stringList };
|
package/dist/lib/tokens.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Gives the document the design tokens, once. They live in the `jlib` cascade layer, so the page overrides any of
|
|
3
|
-
* them with a plain rule: `:root { --jl-accent: #7c3aed; }`.
|
|
3
|
+
* them with a plain rule: `:root { --jl-accent: #7c3aed; }`. Where the document takes no constructed stylesheet, a
|
|
4
|
+
* style element in its head holds them instead.
|
|
4
5
|
*/
|
|
5
6
|
export declare function adoptTokens(): void;
|
package/dist/lib/tokens.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { TOKEN_STYLES } from "../styles/shared.js";
|
|
2
2
|
// src/lib/tokens.ts
|
|
3
|
-
var adopted = {
|
|
3
|
+
var adopted = { done: false };
|
|
4
|
+
/** Whether the document takes constructed stylesheets: jsdom, which runs many apps' unit tests, does not. */
|
|
5
|
+
var takesSheets = () => typeof CSSStyleSheet !== "undefined" && typeof CSSStyleSheet.prototype.replaceSync === "function" && Array.isArray(document.adoptedStyleSheets);
|
|
4
6
|
/**
|
|
5
7
|
* Gives the document the design tokens, once. They live in the `jlib` cascade layer, so the page overrides any of
|
|
6
|
-
* them with a plain rule: `:root { --jl-accent: #7c3aed; }`.
|
|
8
|
+
* them with a plain rule: `:root { --jl-accent: #7c3aed; }`. Where the document takes no constructed stylesheet, a
|
|
9
|
+
* style element in its head holds them instead.
|
|
7
10
|
*/
|
|
8
11
|
function adoptTokens() {
|
|
9
|
-
if (adopted.
|
|
12
|
+
if (adopted.done || typeof document === "undefined") return;
|
|
13
|
+
adopted.done = true;
|
|
14
|
+
if (!takesSheets()) {
|
|
15
|
+
const style = document.createElement("style");
|
|
16
|
+
style.textContent = TOKEN_STYLES;
|
|
17
|
+
document.head.append(style);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
10
20
|
const sheet = new CSSStyleSheet();
|
|
11
21
|
sheet.replaceSync(TOKEN_STYLES);
|
|
12
22
|
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
|
13
|
-
adopted.sheet = sheet;
|
|
14
23
|
}
|
|
15
24
|
export { adoptTokens };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { JlElement } from '../base/jl-element';
|
|
2
2
|
import type { Tone } from '../types/options.types';
|
|
3
3
|
/**
|
|
4
|
-
* A message in the flow of the page, coloured by its tone: a tip, a success, a warning, an error. The text
|
|
5
|
-
* in the default slot, buttons in `slot="actions"`. A `dismissible`
|
|
4
|
+
* A message in the flow of the page, coloured by its tone: a tip, a success, a warning, an error. The text is the
|
|
5
|
+
* `description` or goes in the default slot (for links and markup), buttons in `slot="actions"`. A `dismissible`
|
|
6
|
+
* alert has an × that hides it.
|
|
6
7
|
*/
|
|
7
8
|
export declare class JlAlert extends JlElement {
|
|
8
9
|
static readonly selector = "jl-alert";
|
|
@@ -17,6 +18,11 @@ export declare class JlAlert extends JlElement {
|
|
|
17
18
|
get(): string;
|
|
18
19
|
set(setter: string | ((prevState: string) => string)): void;
|
|
19
20
|
}>>;
|
|
21
|
+
/** The message as plain text, for pages that set it from code; richer content goes in the default slot. */
|
|
22
|
+
readonly description: Readonly<Readonly<{
|
|
23
|
+
get(): string;
|
|
24
|
+
set(setter: string | ((prevState: string) => string)): void;
|
|
25
|
+
}>>;
|
|
20
26
|
/** An icon name in place of the tone's own; `none` shows no icon. */
|
|
21
27
|
readonly icon: Readonly<Readonly<{
|
|
22
28
|
get(): string;
|
|
@@ -33,7 +39,8 @@ export declare class JlAlert extends JlElement {
|
|
|
33
39
|
templateHtml(): Node;
|
|
34
40
|
templateCss(): string;
|
|
35
41
|
onConnect(): void;
|
|
36
|
-
|
|
42
|
+
/** Empty parts take no room: an alert with only a description has no blank line under it. */
|
|
43
|
+
private renderSlots;
|
|
37
44
|
/** The tone's own icon is filled, as a toast's is; an icon of the page's stays as it is drawn. */
|
|
38
45
|
private renderIcon;
|
|
39
46
|
}
|
|
@@ -31,8 +31,9 @@ var TONE_ROLES = {
|
|
|
31
31
|
warn: "alert"
|
|
32
32
|
};
|
|
33
33
|
/**
|
|
34
|
-
* A message in the flow of the page, coloured by its tone: a tip, a success, a warning, an error. The text
|
|
35
|
-
* in the default slot, buttons in `slot="actions"`. A `dismissible`
|
|
34
|
+
* A message in the flow of the page, coloured by its tone: a tip, a success, a warning, an error. The text is the
|
|
35
|
+
* `description` or goes in the default slot (for links and markup), buttons in `slot="actions"`. A `dismissible`
|
|
36
|
+
* alert has an × that hides it.
|
|
36
37
|
*/
|
|
37
38
|
var JlAlert = class extends JlElement {
|
|
38
39
|
static selector = "jl-alert";
|
|
@@ -49,6 +50,11 @@ var JlAlert = class extends JlElement {
|
|
|
49
50
|
this._refs.heading.textContent = value;
|
|
50
51
|
this._refs.heading.hidden = value === "";
|
|
51
52
|
}, { attribute: "heading" });
|
|
53
|
+
/** The message as plain text, for pages that set it from code; richer content goes in the default slot. */
|
|
54
|
+
description = this.useChange("", (value) => {
|
|
55
|
+
this._refs.description.textContent = value;
|
|
56
|
+
this._refs.description.hidden = value === "";
|
|
57
|
+
}, { attribute: "description" });
|
|
52
58
|
/** An icon name in place of the tone's own; `none` shows no icon. */
|
|
53
59
|
icon = this.useChange("", () => this.renderIcon(), { attribute: "icon" });
|
|
54
60
|
/** Shows an × that hides the alert and fires `dismiss`. */
|
|
@@ -61,8 +67,11 @@ var JlAlert = class extends JlElement {
|
|
|
61
67
|
actionsSlot: ref("slot[name=\"actions\"]"),
|
|
62
68
|
alert: ref(".alert"),
|
|
63
69
|
close: ref(".close"),
|
|
70
|
+
description: ref(".description"),
|
|
64
71
|
heading: ref(".heading"),
|
|
65
|
-
icon: ref(".icon")
|
|
72
|
+
icon: ref(".icon"),
|
|
73
|
+
text: ref(".text"),
|
|
74
|
+
textSlot: ref("slot:not([name])")
|
|
66
75
|
}));
|
|
67
76
|
/** Hides the alert as its × does, and fires `dismiss`. */
|
|
68
77
|
dismiss() {
|
|
@@ -86,8 +95,13 @@ var JlAlert = class extends JlElement {
|
|
|
86
95
|
class: "heading",
|
|
87
96
|
hidden: true
|
|
88
97
|
}),
|
|
98
|
+
/* @__PURE__ */ jsx("p", {
|
|
99
|
+
class: "description",
|
|
100
|
+
hidden: true
|
|
101
|
+
}),
|
|
89
102
|
/* @__PURE__ */ jsx("div", {
|
|
90
103
|
class: "text",
|
|
104
|
+
hidden: true,
|
|
91
105
|
children: /* @__PURE__ */ jsx("slot", {})
|
|
92
106
|
}),
|
|
93
107
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -113,10 +127,13 @@ var JlAlert = class extends JlElement {
|
|
|
113
127
|
}
|
|
114
128
|
onConnect() {
|
|
115
129
|
this._refs.close.events.register("press", () => this.dismiss());
|
|
116
|
-
this.
|
|
117
|
-
this.on(this._refs.actionsSlot, "slotchange", () => this.
|
|
130
|
+
this.renderSlots();
|
|
131
|
+
this.on(this._refs.actionsSlot, "slotchange", () => this.renderSlots());
|
|
132
|
+
this.on(this._refs.textSlot, "slotchange", () => this.renderSlots());
|
|
118
133
|
}
|
|
119
|
-
|
|
134
|
+
/** Empty parts take no room: an alert with only a description has no blank line under it. */
|
|
135
|
+
renderSlots() {
|
|
136
|
+
this._refs.text.hidden = !hasSlotted(this._refs.textSlot);
|
|
120
137
|
this._refs.actions.hidden = !hasSlotted(this._refs.actionsSlot);
|
|
121
138
|
}
|
|
122
139
|
/** The tone's own icon is filled, as a toast's is; an icon of the page's stays as it is drawn. */
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// src/molecules/jl-chip-group.css?raw
|
|
2
|
+
var jl_chip_group_default = ":host {\n display: block;\n min-width: 0;\n --height: 28px;\n --pad: 10px;\n --font-size: var(--jl-font-sm);\n}\n\n:host([size=\"sm\"]) {\n --height: 24px;\n --pad: 8px;\n --font-size: var(--jl-font-xs);\n}\n\n/* Where a thumb presses it. */\n:host([size=\"lg\"]) {\n --height: 36px;\n --pad: 14px;\n --font-size: var(--jl-font-md);\n}\n\n/*\n * One row that scrolls sideways on a narrow screen. The padding keeps the focus ring inside what the scroller\n * clips; the negative margin keeps the chips aligned with the content around.\n */\n.group {\n display: flex;\n gap: 8px;\n min-width: 0;\n margin: -3px;\n padding: 3px;\n overflow-x: auto;\n scrollbar-width: none;\n contain: inline-size;\n}\n\n.group::-webkit-scrollbar {\n display: none;\n}\n\n:host([wrap]) .group {\n flex-wrap: wrap;\n overflow: visible;\n contain: none;\n}\n\n.chip {\n display: inline-flex;\n flex: none;\n align-items: center;\n gap: 6px;\n max-width: 100%;\n height: var(--height);\n padding: 0 var(--pad);\n border: 1px solid var(--jl-line-strong);\n border-radius: var(--jl-radius-pill);\n background: var(--jl-surface);\n color: var(--jl-text-2);\n font-size: var(--font-size);\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n cursor: pointer;\n transition:\n background var(--jl-fast) var(--jl-ease),\n border-color var(--jl-fast) var(--jl-ease),\n color var(--jl-fast) var(--jl-ease);\n}\n\n.chip:hover:not(:disabled) {\n background: var(--jl-surface-2);\n}\n\n.chip.selected,\n.chip.selected:hover:not(:disabled) {\n border-color: var(--jl-accent-line);\n background: var(--jl-accent-soft);\n color: var(--jl-accent);\n}\n\n.chip:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n/* A line box taller than the chip's own line-height of 1: the accents of É, Â, Ö stay inside what overflow clips. */\n.label {\n overflow: hidden;\n line-height: 1.4;\n text-overflow: ellipsis;\n}\n\n.badge {\n min-width: 18px;\n margin-right: -4px;\n padding: 0 6px;\n border-radius: var(--jl-radius-pill);\n background: var(--jl-surface-3);\n color: var(--jl-muted);\n font-size: var(--jl-font-xs);\n font-variant-numeric: tabular-nums;\n font-weight: 650;\n line-height: 18px;\n text-align: center;\n}\n\n.chip.selected .badge {\n background: color-mix(in srgb, var(--jl-accent) 14%, transparent);\n color: var(--jl-accent);\n}\n";
|
|
3
|
+
export { jl_chip_group_default as default };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { JlFormControl } from '../base/jl-form-control';
|
|
2
|
+
import type { ChipGroupOption } from '../types/jl-chip-group.types';
|
|
3
|
+
import type { ControlSize } from '../types/options.types';
|
|
4
|
+
/**
|
|
5
|
+
* A row of chips to choose from: a status to filter by, a category, a reason. One choice by default, as radios
|
|
6
|
+
* (choosing the chosen chip again keeps it); `multiple` lets people turn each chip on and off. On a narrow screen
|
|
7
|
+
* the row scrolls sideways; `wrap` lets it go to the next line instead. `change` fires with the chosen values.
|
|
8
|
+
*/
|
|
9
|
+
export declare class JlChipGroup extends JlFormControl {
|
|
10
|
+
static readonly selector = "jl-chip-group";
|
|
11
|
+
readonly events: Readonly<import("@jadis/core").UseEventsHandler<{
|
|
12
|
+
change: string[];
|
|
13
|
+
}>>;
|
|
14
|
+
readonly options: Readonly<Readonly<{
|
|
15
|
+
get(): ChipGroupOption[];
|
|
16
|
+
set(setter: ChipGroupOption[] | ((prevState: ChipGroupOption[]) => ChipGroupOption[])): void;
|
|
17
|
+
}>>;
|
|
18
|
+
/** The chosen values, in the options' order; with one choice, a list of at most one. */
|
|
19
|
+
readonly value: Readonly<Readonly<{
|
|
20
|
+
get(): string[];
|
|
21
|
+
set(setter: string[] | ((prevState: string[]) => string[])): void;
|
|
22
|
+
}>>;
|
|
23
|
+
/** Several chips can be chosen, each turned on and off on its own. */
|
|
24
|
+
readonly multiple: Readonly<Readonly<{
|
|
25
|
+
get(): boolean;
|
|
26
|
+
set(setter: boolean | ((prevState: boolean) => boolean)): void;
|
|
27
|
+
}>>;
|
|
28
|
+
/** Goes to the next line when the chips do not fit, instead of scrolling sideways. */
|
|
29
|
+
readonly wrap: Readonly<Readonly<{
|
|
30
|
+
get(): boolean;
|
|
31
|
+
set(setter: boolean | ((prevState: boolean) => boolean)): void;
|
|
32
|
+
}>>;
|
|
33
|
+
readonly size: Readonly<Readonly<{
|
|
34
|
+
get(): ControlSize;
|
|
35
|
+
set(setter: ((prevState: ControlSize) => ControlSize) | ControlSize): void;
|
|
36
|
+
}>>;
|
|
37
|
+
/** The group's name for screen readers, when no visible label names it. */
|
|
38
|
+
readonly label: Readonly<Readonly<{
|
|
39
|
+
get(): string;
|
|
40
|
+
set(setter: string | ((prevState: string) => string)): void;
|
|
41
|
+
}>>;
|
|
42
|
+
/** A form refuses to submit while nothing is chosen. */
|
|
43
|
+
readonly required: Readonly<Readonly<{
|
|
44
|
+
get(): boolean;
|
|
45
|
+
set(setter: boolean | ((prevState: boolean) => boolean)): void;
|
|
46
|
+
}>>;
|
|
47
|
+
readonly disabled: Readonly<Readonly<{
|
|
48
|
+
get(): boolean;
|
|
49
|
+
set(setter: boolean | ((prevState: boolean) => boolean)): void;
|
|
50
|
+
}>>;
|
|
51
|
+
/** The chip that holds the Tab stop with several choices: the last one people reached. */
|
|
52
|
+
private readonly _state;
|
|
53
|
+
private readonly _nameWatch;
|
|
54
|
+
private readonly _refs;
|
|
55
|
+
/** Focuses the chip that holds the Tab stop. */
|
|
56
|
+
focus(options?: FocusOptions): void;
|
|
57
|
+
templateHtml(): Node;
|
|
58
|
+
templateCss(): string;
|
|
59
|
+
onConnect(): void;
|
|
60
|
+
onDisconnect(): void;
|
|
61
|
+
protected resetValue(): void;
|
|
62
|
+
/** The browser calls it for a disabled fieldset and for the element's own `disabled`, even before it renders. */
|
|
63
|
+
protected setDisabledByForm(disabled: boolean): void;
|
|
64
|
+
private chips;
|
|
65
|
+
/** What is chosen: with one choice, only the first value counts. */
|
|
66
|
+
private chosen;
|
|
67
|
+
private isDisabled;
|
|
68
|
+
/** One choice: the chip becomes it. Several: the chip turns on or off, the values keeping the options' order. */
|
|
69
|
+
private press;
|
|
70
|
+
private commit;
|
|
71
|
+
private onKeydown;
|
|
72
|
+
private renderOptions;
|
|
73
|
+
private chip;
|
|
74
|
+
/** Radios with one choice, toggle buttons with several; each chosen chip shows its icon or a check. */
|
|
75
|
+
private renderValue;
|
|
76
|
+
/** Tab reaches one chip: the chosen one with one choice, the last one reached with several, else the first. */
|
|
77
|
+
private renderTabStop;
|
|
78
|
+
/** One choice gives one entry; several give one entry each under the field's name, as checkboxes would. */
|
|
79
|
+
private renderFormValue;
|
|
80
|
+
private renderValidity;
|
|
81
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { BASE_STYLES } from "../styles/shared.js";
|
|
2
|
+
import { JlFormControl } from "../base/jl-form-control.js";
|
|
3
|
+
import { jsonOf, oneOf, stringList } from "../lib/attributes.js";
|
|
4
|
+
import { registerIcons } from "../lib/icons.js";
|
|
5
|
+
import { CONTROL_SIZES } from "../lib/options.js";
|
|
6
|
+
import { setOrRemove } from "../lib/dom.js";
|
|
7
|
+
import { JlIcon } from "../atoms/jl-icon.js";
|
|
8
|
+
import { rovingIndex } from "../lib/roving-index.js";
|
|
9
|
+
import jl_chip_group_default from "./jl-chip-group.css_raw.js";
|
|
10
|
+
import { css } from "@jadis/core";
|
|
11
|
+
import { Check } from "lucide";
|
|
12
|
+
import { jsx, jsxs } from "@jadis/core/jsx-runtime";
|
|
13
|
+
// src/molecules/jl-chip-group.tsx
|
|
14
|
+
registerIcons({ check: Check });
|
|
15
|
+
var ICON_SIZES = {
|
|
16
|
+
lg: 16,
|
|
17
|
+
md: 14,
|
|
18
|
+
sm: 12
|
|
19
|
+
};
|
|
20
|
+
var SELECTED_ICON = "check";
|
|
21
|
+
var CHIP_SELECTOR = ".chip";
|
|
22
|
+
var VALUE_MISSING = "Please select one of these options.";
|
|
23
|
+
/** The keys that move between the chips; with one choice, the choice moves too, as with native radios. */
|
|
24
|
+
var MOVES = /* @__PURE__ */ new Map([
|
|
25
|
+
["ArrowDown", "next"],
|
|
26
|
+
["ArrowLeft", "previous"],
|
|
27
|
+
["ArrowRight", "next"],
|
|
28
|
+
["ArrowUp", "previous"],
|
|
29
|
+
["End", "last"],
|
|
30
|
+
["Home", "first"]
|
|
31
|
+
]);
|
|
32
|
+
/** A JSON attribute that should hold a list of options; anything else gives none. */
|
|
33
|
+
var parseOptions = (value) => {
|
|
34
|
+
const parsed = jsonOf([])(value);
|
|
35
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* A row of chips to choose from: a status to filter by, a category, a reason. One choice by default, as radios
|
|
39
|
+
* (choosing the chosen chip again keeps it); `multiple` lets people turn each chip on and off. On a narrow screen
|
|
40
|
+
* the row scrolls sideways; `wrap` lets it go to the next line instead. `change` fires with the chosen values.
|
|
41
|
+
*/
|
|
42
|
+
var JlChipGroup = class extends JlFormControl {
|
|
43
|
+
static selector = "jl-chip-group";
|
|
44
|
+
events = this.useEvents();
|
|
45
|
+
options = this.useChange([], () => this.renderOptions(), {
|
|
46
|
+
attribute: "options",
|
|
47
|
+
parse: parseOptions
|
|
48
|
+
});
|
|
49
|
+
/** The chosen values, in the options' order; with one choice, a list of at most one. */
|
|
50
|
+
value = this.useChange([], () => this.renderValue(), {
|
|
51
|
+
attribute: "value",
|
|
52
|
+
parse: stringList
|
|
53
|
+
});
|
|
54
|
+
/** Several chips can be chosen, each turned on and off on its own. */
|
|
55
|
+
multiple = this.useChange(false, (value) => {
|
|
56
|
+
this.reflect("multiple", value);
|
|
57
|
+
this.renderValue();
|
|
58
|
+
}, { attribute: "multiple" });
|
|
59
|
+
/** Goes to the next line when the chips do not fit, instead of scrolling sideways. */
|
|
60
|
+
wrap = this.useChange(false, (value) => this.reflect("wrap", value), { attribute: "wrap" });
|
|
61
|
+
size = this.useChange("md", (value) => {
|
|
62
|
+
this.reflect("size", value);
|
|
63
|
+
this.renderOptions();
|
|
64
|
+
}, {
|
|
65
|
+
attribute: "size",
|
|
66
|
+
parse: oneOf(CONTROL_SIZES, "md")
|
|
67
|
+
});
|
|
68
|
+
/** The group's name for screen readers, when no visible label names it. */
|
|
69
|
+
label = this.useChange("", (value) => setOrRemove(this._refs.group, "aria-label", value), { attribute: "label" });
|
|
70
|
+
/** A form refuses to submit while nothing is chosen. */
|
|
71
|
+
required = this.useChange(false, (value) => {
|
|
72
|
+
this.reflect("required", value);
|
|
73
|
+
this._refs.group.toggleAttribute("aria-required", value);
|
|
74
|
+
this.renderValidity();
|
|
75
|
+
}, { attribute: "required" });
|
|
76
|
+
disabled = this.useChange(false, (value) => {
|
|
77
|
+
this.reflect("disabled", value);
|
|
78
|
+
this.renderValue();
|
|
79
|
+
}, { attribute: "disabled" });
|
|
80
|
+
/** The chip that holds the Tab stop with several choices: the last one people reached. */
|
|
81
|
+
_state = {
|
|
82
|
+
disabledByForm: false,
|
|
83
|
+
stop: ""
|
|
84
|
+
};
|
|
85
|
+
_nameWatch = new MutationObserver(() => this.renderFormValue());
|
|
86
|
+
_refs = this.useRefs((ref) => ({ group: ref(".group") }));
|
|
87
|
+
/** Focuses the chip that holds the Tab stop. */
|
|
88
|
+
focus(options) {
|
|
89
|
+
this.chips().find((chip) => chip.tabIndex === 0)?.focus(options);
|
|
90
|
+
}
|
|
91
|
+
templateHtml() {
|
|
92
|
+
return /* @__PURE__ */ jsx("div", {
|
|
93
|
+
class: "group",
|
|
94
|
+
role: "radiogroup",
|
|
95
|
+
attrs: { part: "base" }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
templateCss() {
|
|
99
|
+
return css`${BASE_STYLES}${jl_chip_group_default}`;
|
|
100
|
+
}
|
|
101
|
+
onConnect() {
|
|
102
|
+
this._nameWatch.observe(this, {
|
|
103
|
+
attributeFilter: ["name"],
|
|
104
|
+
attributes: true
|
|
105
|
+
});
|
|
106
|
+
this.renderFormValue();
|
|
107
|
+
this.on(this._refs.group, "click", (event) => {
|
|
108
|
+
const chip = event.target.closest(CHIP_SELECTOR);
|
|
109
|
+
if (chip !== null && !chip.disabled) this.press(chip);
|
|
110
|
+
});
|
|
111
|
+
this.on(this._refs.group, "keydown", (event) => this.onKeydown(event));
|
|
112
|
+
}
|
|
113
|
+
onDisconnect() {
|
|
114
|
+
this._nameWatch.disconnect();
|
|
115
|
+
}
|
|
116
|
+
resetValue() {
|
|
117
|
+
this.value.set(stringList(this.getAttribute("value")));
|
|
118
|
+
}
|
|
119
|
+
/** The browser calls it for a disabled fieldset and for the element's own `disabled`, even before it renders. */
|
|
120
|
+
setDisabledByForm(disabled) {
|
|
121
|
+
this._state.disabledByForm = disabled;
|
|
122
|
+
this.whenRendered(() => this.renderValue());
|
|
123
|
+
}
|
|
124
|
+
chips() {
|
|
125
|
+
return [...this._refs.group.querySelectorAll(CHIP_SELECTOR)];
|
|
126
|
+
}
|
|
127
|
+
/** What is chosen: with one choice, only the first value counts. */
|
|
128
|
+
chosen() {
|
|
129
|
+
const values = this.value.get();
|
|
130
|
+
return this.multiple.get() ? values : values.slice(0, 1);
|
|
131
|
+
}
|
|
132
|
+
isDisabled() {
|
|
133
|
+
return this.disabled.get() || this._state.disabledByForm;
|
|
134
|
+
}
|
|
135
|
+
/** One choice: the chip becomes it. Several: the chip turns on or off, the values keeping the options' order. */
|
|
136
|
+
press(chip) {
|
|
137
|
+
const value = chip.dataset.value ?? "";
|
|
138
|
+
this._state.stop = value;
|
|
139
|
+
const chosen = this.chosen();
|
|
140
|
+
if (!this.multiple.get()) {
|
|
141
|
+
if (chosen[0] === value) {
|
|
142
|
+
this.renderTabStop();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
this.commit([value]);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const next = new Set(chosen);
|
|
149
|
+
if (next.has(value)) next.delete(value);
|
|
150
|
+
else next.add(value);
|
|
151
|
+
this.commit(this.options.get().flatMap((option) => next.has(option.value) ? [option.value] : []));
|
|
152
|
+
}
|
|
153
|
+
commit(values) {
|
|
154
|
+
this.value.set(values);
|
|
155
|
+
this.events.emit("change", values);
|
|
156
|
+
}
|
|
157
|
+
onKeydown(event) {
|
|
158
|
+
const move = MOVES.get(event.key);
|
|
159
|
+
const chips = this.chips();
|
|
160
|
+
const from = chips.indexOf(event.target);
|
|
161
|
+
if (move === void 0 || from === -1) return;
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
const target = chips[rovingIndex(chips.map((chip) => chip.disabled), from, move)];
|
|
164
|
+
if (target === void 0) return;
|
|
165
|
+
target.focus();
|
|
166
|
+
if (this.multiple.get()) {
|
|
167
|
+
this._state.stop = target.dataset.value ?? "";
|
|
168
|
+
this.renderTabStop();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.press(target);
|
|
172
|
+
}
|
|
173
|
+
renderOptions() {
|
|
174
|
+
const iconSize = ICON_SIZES[this.size.get()];
|
|
175
|
+
this._refs.group.replaceChildren(...this.options.get().map((option) => this.chip(option, iconSize)));
|
|
176
|
+
this.renderValue();
|
|
177
|
+
}
|
|
178
|
+
chip(option, iconSize) {
|
|
179
|
+
const badge = option.badge === void 0 ? "" : String(option.badge);
|
|
180
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
181
|
+
type: "button",
|
|
182
|
+
class: "chip",
|
|
183
|
+
tabIndex: -1,
|
|
184
|
+
"data-value": option.value,
|
|
185
|
+
"data-icon": option.icon,
|
|
186
|
+
"data-disabled": option.disabled === true ? "" : void 0,
|
|
187
|
+
children: [
|
|
188
|
+
/* @__PURE__ */ jsx(JlIcon, {
|
|
189
|
+
class: "icon",
|
|
190
|
+
size: iconSize,
|
|
191
|
+
hidden: true
|
|
192
|
+
}),
|
|
193
|
+
/* @__PURE__ */ jsx("span", {
|
|
194
|
+
class: "label",
|
|
195
|
+
children: option.label
|
|
196
|
+
}),
|
|
197
|
+
badge === "" ? "" : /* @__PURE__ */ jsx("span", {
|
|
198
|
+
class: "badge",
|
|
199
|
+
children: badge
|
|
200
|
+
})
|
|
201
|
+
]
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/** Radios with one choice, toggle buttons with several; each chosen chip shows its icon or a check. */
|
|
205
|
+
renderValue() {
|
|
206
|
+
const chosen = new Set(this.chosen());
|
|
207
|
+
const multiple = this.multiple.get();
|
|
208
|
+
const groupDisabled = this.isDisabled();
|
|
209
|
+
this._refs.group.setAttribute("role", multiple ? "group" : "radiogroup");
|
|
210
|
+
this._refs.group.toggleAttribute("aria-disabled", groupDisabled);
|
|
211
|
+
for (const chip of this.chips()) {
|
|
212
|
+
const selected = chosen.has(chip.dataset.value ?? "");
|
|
213
|
+
chip.disabled = groupDisabled || chip.dataset.disabled !== void 0;
|
|
214
|
+
setOrRemove(chip, "role", multiple ? "" : "radio");
|
|
215
|
+
setOrRemove(chip, "aria-checked", multiple ? "" : String(selected));
|
|
216
|
+
setOrRemove(chip, "aria-pressed", multiple ? String(selected) : "");
|
|
217
|
+
chip.classList.toggle("selected", selected);
|
|
218
|
+
const icon = chip.querySelector(".icon");
|
|
219
|
+
const name = chip.dataset.icon ?? (selected ? SELECTED_ICON : "");
|
|
220
|
+
icon?.name.set(name);
|
|
221
|
+
if (icon !== null) icon.hidden = name === "";
|
|
222
|
+
}
|
|
223
|
+
this.renderTabStop();
|
|
224
|
+
this.renderFormValue();
|
|
225
|
+
this.renderValidity();
|
|
226
|
+
}
|
|
227
|
+
/** Tab reaches one chip: the chosen one with one choice, the last one reached with several, else the first. */
|
|
228
|
+
renderTabStop() {
|
|
229
|
+
const enabled = this.chips().filter((chip) => !chip.disabled);
|
|
230
|
+
const chosen = this.chosen();
|
|
231
|
+
const stop = (this.multiple.get() ? [this._state.stop, ...chosen] : chosen).map((value) => enabled.find((chip) => chip.dataset.value === value)).find((chip) => chip !== void 0) ?? enabled[0];
|
|
232
|
+
for (const chip of this.chips()) chip.tabIndex = chip === stop ? 0 : -1;
|
|
233
|
+
}
|
|
234
|
+
/** One choice gives one entry; several give one entry each under the field's name, as checkboxes would. */
|
|
235
|
+
renderFormValue() {
|
|
236
|
+
const chosen = this.chosen();
|
|
237
|
+
if (!this.multiple.get()) {
|
|
238
|
+
this.internals.setFormValue(chosen[0] ?? null);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const name = this.getAttribute("name") ?? "";
|
|
242
|
+
if (name === "") {
|
|
243
|
+
this.internals.setFormValue(null);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const data = new FormData();
|
|
247
|
+
for (const value of chosen) data.append(name, value);
|
|
248
|
+
this.internals.setFormValue(data);
|
|
249
|
+
}
|
|
250
|
+
renderValidity() {
|
|
251
|
+
if (!(this.required.get() && this.chosen().length === 0)) {
|
|
252
|
+
this.internals.setValidity({});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this.internals.setValidity({ valueMissing: true }, VALUE_MISSING, this.chips()[0] ?? this._refs.group);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
JlChipGroup.register();
|
|
259
|
+
export { JlChipGroup };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// src/molecules/jl-tag-input.css?raw
|
|
2
|
-
var jl_tag_input_default = ":host {\n display: block;\n /* As a grid or flex item, the field may be narrower than its text field would like. */\n min-width: 0;\n --chip-height: calc(var(--height, var(--jl-control-md)) - 12px);\n}\n\n.control {\n flex-wrap: wrap;\n gap: 4px;\n padding: 4px 6px;\n cursor: text;\n}\n\n/* The chips are items of the frame's own flow, so the field follows the last one on its line. */\n.tags {\n display: contents;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.tag {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n max-width: 100%;\n height: var(--chip-height);\n padding: 0 3px 0 8px;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius-xs);\n background: var(--jl-surface-2);\n color: var(--jl-text);\n font-size: var(--jl-font-sm);\n line-height: 1;\n cursor: default;\n}\n\n:host([size=\"sm\"]) .tag {\n padding-left: 6px;\n font-size: var(--jl-font-xs);\n}\n\n.tag.flash {\n animation: flash 600ms var(--jl-ease);\n}\n\n.tag-text {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.tag-remove {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n padding: 0;\n border: none;\n border-radius: 4px;\n background: none;\n color: var(--jl-faint);\n cursor: pointer;\n}\n\n.tag-remove:hover,\n.tag-remove:focus-visible {\n background: var(--jl-surface-3);\n color: var(--jl-text);\n}\n\n.native {\n flex: 1 0 80px;\n height: calc(var(--height, var(--jl-control-md)) - 10px);\n padding-left: 6px;\n}\n\n:host([disabled]) .tag {\n padding-right: 8px;\n color: var(--jl-muted);\n}\n\n.popup {\n min-width: 180px;\n padding: 4px;\n}\n\n.listbox {\n position: relative;\n max-height: 240px;\n margin: 0;\n padding: 0;\n overflow-y: auto;\n list-style: none;\n}\n\n.option {\n padding: 6px 10px;\n border-radius: var(--jl-radius-xs);\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n line-height: 1.4;\n white-space: nowrap;\n cursor: pointer;\n user-select: none;\n}\n\n.option.active,\n.option:hover {\n background: var(--jl-surface-3);\n}\n\nmark {\n background: none;\n color: var(--jl-accent);\n font-weight: 700;\n}\n\n@keyframes flash {\n 30% {\n border-color: var(--jl-accent-line);\n background: var(--jl-accent-soft);\n color: var(--jl-accent-strong);\n }\n}\n";
|
|
2
|
+
var jl_tag_input_default = ":host {\n display: block;\n /* As a grid or flex item, the field may be narrower than its text field would like. */\n min-width: 0;\n --chip-height: calc(var(--height, var(--jl-control-md)) - 12px);\n}\n\n.control {\n flex-wrap: wrap;\n gap: 4px;\n padding: 4px 6px;\n cursor: text;\n}\n\n/* The chips are items of the frame's own flow, so the field follows the last one on its line. */\n.tags {\n display: contents;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.tag {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n max-width: 100%;\n height: var(--chip-height);\n padding: 0 3px 0 8px;\n border: 1px solid var(--jl-line);\n border-radius: var(--jl-radius-xs);\n background: var(--jl-surface-2);\n color: var(--jl-text);\n font-size: var(--jl-font-sm);\n line-height: 1;\n cursor: default;\n}\n\n:host([size=\"sm\"]) .tag {\n padding-left: 6px;\n font-size: var(--jl-font-xs);\n}\n\n.tag.flash {\n animation: flash 600ms var(--jl-ease);\n}\n\n/* A line box taller than the tag's own line-height of 1: the accents of É, Â, Ö stay inside what overflow clips. */\n.tag-text {\n overflow: hidden;\n line-height: 1.4;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.tag-remove {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n padding: 0;\n border: none;\n border-radius: 4px;\n background: none;\n color: var(--jl-faint);\n cursor: pointer;\n}\n\n.tag-remove:hover,\n.tag-remove:focus-visible {\n background: var(--jl-surface-3);\n color: var(--jl-text);\n}\n\n.native {\n flex: 1 0 80px;\n height: calc(var(--height, var(--jl-control-md)) - 10px);\n padding-left: 6px;\n}\n\n:host([disabled]) .tag {\n padding-right: 8px;\n color: var(--jl-muted);\n}\n\n.popup {\n min-width: 180px;\n padding: 4px;\n}\n\n.listbox {\n position: relative;\n max-height: 240px;\n margin: 0;\n padding: 0;\n overflow-y: auto;\n list-style: none;\n}\n\n.option {\n padding: 6px 10px;\n border-radius: var(--jl-radius-xs);\n color: var(--jl-text);\n font-size: var(--jl-font-md);\n line-height: 1.4;\n white-space: nowrap;\n cursor: pointer;\n user-select: none;\n}\n\n.option.active,\n.option:hover {\n background: var(--jl-surface-3);\n}\n\nmark {\n background: none;\n color: var(--jl-accent);\n font-weight: 700;\n}\n\n@keyframes flash {\n 30% {\n border-color: var(--jl-accent-line);\n background: var(--jl-accent-soft);\n color: var(--jl-accent-strong);\n }\n}\n";
|
|
3
3
|
export { jl_tag_input_default as default };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// src/organisms/jl-tab-bar.css?raw
|
|
2
|
+
var jl_tab_bar_default = ":host {\n display: block;\n --height: 60px;\n}\n\nnav {\n display: grid;\n grid-auto-columns: minmax(0, 1fr);\n grid-auto-flow: column;\n min-height: var(--height);\n /* Clear of the home indicator on phones without a button. */\n padding-bottom: env(safe-area-inset-bottom);\n border-top: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n/* The items and the \"more\" button share the bar's columns. */\n.items {\n display: contents;\n}\n\n.item {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 2px;\n min-width: 0;\n padding: 6px 2px;\n border: none;\n background: none;\n color: var(--jl-muted);\n font-size: 11px;\n font-weight: 600;\n line-height: 1.4;\n text-decoration: none;\n cursor: pointer;\n transition: color var(--jl-fast) var(--jl-ease);\n}\n\n.item:hover {\n color: var(--jl-text);\n}\n\n.item[aria-current=\"page\"] {\n color: var(--jl-accent);\n}\n\n/* The ring goes round the icon's pill: round the whole column, it would touch the bar's edges. */\n.item:focus-visible {\n box-shadow: none;\n}\n\n.icon {\n position: relative;\n display: inline-grid;\n place-items: center;\n width: 52px;\n height: 28px;\n border-radius: var(--jl-radius-pill);\n transition: background var(--jl-fast) var(--jl-ease);\n}\n\n.item:focus-visible .icon {\n box-shadow: var(--jl-focus-ring);\n}\n\n/* The current item is told by a shape too, not by its colour alone. */\n.item[aria-current=\"page\"] .icon {\n background: var(--jl-accent-soft);\n}\n\n.label {\n max-width: 100%;\n padding: 0 2px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.badge {\n position: absolute;\n top: -4px;\n left: calc(50% + 4px);\n min-width: 18px;\n height: 18px;\n padding: 0 5px;\n border: 2px solid var(--jl-surface);\n border-radius: var(--jl-radius-pill);\n background: var(--jl-accent);\n color: var(--jl-on-accent);\n font-size: 10px;\n font-weight: 700;\n line-height: 14px;\n text-align: center;\n}\n";
|
|
3
|
+
export { jl_tab_bar_default as default };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { JlElement } from '../base/jl-element';
|
|
2
|
+
import type { TabBarItem } from '../types/jl-tab-bar.types';
|
|
3
|
+
/**
|
|
4
|
+
* The bar along the bottom of a phone, under the thumb: the three to five main places of an app, an icon over a
|
|
5
|
+
* label, one tap away. `value` is the current item. Items with an `href` are links; the others are buttons; both
|
|
6
|
+
* fire `select`. `more` adds a last button for everything else, which fires `more`: in a `jl-app-shell`, it opens
|
|
7
|
+
* the navigation drawer.
|
|
8
|
+
*/
|
|
9
|
+
export declare class JlTabBar extends JlElement {
|
|
10
|
+
static readonly selector = "jl-tab-bar";
|
|
11
|
+
readonly events: Readonly<import("@jadis/core").UseEventsHandler<{
|
|
12
|
+
select: string;
|
|
13
|
+
more: undefined;
|
|
14
|
+
}>>;
|
|
15
|
+
readonly items: Readonly<Readonly<{
|
|
16
|
+
get(): TabBarItem[];
|
|
17
|
+
set(setter: TabBarItem[] | ((prevState: TabBarItem[]) => TabBarItem[])): void;
|
|
18
|
+
}>>;
|
|
19
|
+
/** The id of the current item. */
|
|
20
|
+
readonly value: Readonly<Readonly<{
|
|
21
|
+
get(): string;
|
|
22
|
+
set(setter: string | ((prevState: string) => string)): void;
|
|
23
|
+
}>>;
|
|
24
|
+
/** The label of a last button for the other places (`More`); empty shows none. */
|
|
25
|
+
readonly more: Readonly<Readonly<{
|
|
26
|
+
get(): string;
|
|
27
|
+
set(setter: string | ((prevState: string) => string)): void;
|
|
28
|
+
}>>;
|
|
29
|
+
/** The name of the navigation for screen readers. */
|
|
30
|
+
readonly label: Readonly<Readonly<{
|
|
31
|
+
get(): string;
|
|
32
|
+
set(setter: string | ((prevState: string) => string)): void;
|
|
33
|
+
}>>;
|
|
34
|
+
private readonly _refs;
|
|
35
|
+
templateHtml(): Node;
|
|
36
|
+
templateCss(): string;
|
|
37
|
+
onConnect(): void;
|
|
38
|
+
private onClick;
|
|
39
|
+
private render;
|
|
40
|
+
private renderItem;
|
|
41
|
+
/** The current item says so: `aria-current="page"`, drawn in the accent. */
|
|
42
|
+
private renderCurrent;
|
|
43
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { BASE_STYLES } from "../styles/shared.js";
|
|
2
|
+
import { JlElement } from "../base/jl-element.js";
|
|
3
|
+
import { jsonOf } from "../lib/attributes.js";
|
|
4
|
+
import { registerIcons } from "../lib/icons.js";
|
|
5
|
+
import { setOrRemove } from "../lib/dom.js";
|
|
6
|
+
import { JlIcon } from "../atoms/jl-icon.js";
|
|
7
|
+
import jl_tab_bar_default from "./jl-tab-bar.css_raw.js";
|
|
8
|
+
import { css } from "@jadis/core";
|
|
9
|
+
import { Ellipsis } from "lucide";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "@jadis/core/jsx-runtime";
|
|
11
|
+
// src/organisms/jl-tab-bar.tsx
|
|
12
|
+
registerIcons({ ellipsis: Ellipsis });
|
|
13
|
+
var ICON_SIZE = 22;
|
|
14
|
+
var DEFAULT_LABEL = "Shortcuts";
|
|
15
|
+
var MAX_COUNT = 99;
|
|
16
|
+
var ITEM_SELECTOR = ".item";
|
|
17
|
+
/** A JSON attribute that should hold a list of items; anything else gives none. */
|
|
18
|
+
var parseItems = (value) => {
|
|
19
|
+
const parsed = jsonOf([])(value);
|
|
20
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
21
|
+
};
|
|
22
|
+
/** What the badge shows: counts are capped so they fit over the icon. */
|
|
23
|
+
var badgeText = (badge) => {
|
|
24
|
+
if (typeof badge === "number") return badge > MAX_COUNT ? `${MAX_COUNT}+` : String(badge);
|
|
25
|
+
return badge ?? "";
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The bar along the bottom of a phone, under the thumb: the three to five main places of an app, an icon over a
|
|
29
|
+
* label, one tap away. `value` is the current item. Items with an `href` are links; the others are buttons; both
|
|
30
|
+
* fire `select`. `more` adds a last button for everything else, which fires `more`: in a `jl-app-shell`, it opens
|
|
31
|
+
* the navigation drawer.
|
|
32
|
+
*/
|
|
33
|
+
var JlTabBar = class extends JlElement {
|
|
34
|
+
static selector = "jl-tab-bar";
|
|
35
|
+
events = this.useEvents();
|
|
36
|
+
items = this.useChange([], () => this.render(), {
|
|
37
|
+
attribute: "items",
|
|
38
|
+
parse: parseItems
|
|
39
|
+
});
|
|
40
|
+
/** The id of the current item. */
|
|
41
|
+
value = this.useChange("", (value) => this.renderCurrent(value), { attribute: "value" });
|
|
42
|
+
/** The label of a last button for the other places (`More`); empty shows none. */
|
|
43
|
+
more = this.useChange("", (value) => {
|
|
44
|
+
this._refs.moreLabel.textContent = value;
|
|
45
|
+
this._refs.more.hidden = value === "";
|
|
46
|
+
}, { attribute: "more" });
|
|
47
|
+
/** The name of the navigation for screen readers. */
|
|
48
|
+
label = this.useChange(DEFAULT_LABEL, (value) => this._refs.nav.setAttribute("aria-label", value === "" ? DEFAULT_LABEL : value), { attribute: "label" });
|
|
49
|
+
_refs = this.useRefs((ref) => ({
|
|
50
|
+
items: ref(".items"),
|
|
51
|
+
more: ref(".more"),
|
|
52
|
+
moreLabel: ref(".more .label"),
|
|
53
|
+
nav: ref("nav")
|
|
54
|
+
}));
|
|
55
|
+
templateHtml() {
|
|
56
|
+
return /* @__PURE__ */ jsxs("nav", {
|
|
57
|
+
attrs: { part: "base" },
|
|
58
|
+
children: [/* @__PURE__ */ jsx("div", { class: "items" }), /* @__PURE__ */ jsxs("button", {
|
|
59
|
+
class: "item more",
|
|
60
|
+
type: "button",
|
|
61
|
+
hidden: true,
|
|
62
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
63
|
+
class: "icon",
|
|
64
|
+
children: /* @__PURE__ */ jsx(JlIcon, {
|
|
65
|
+
name: "ellipsis",
|
|
66
|
+
size: ICON_SIZE
|
|
67
|
+
})
|
|
68
|
+
}), /* @__PURE__ */ jsx("span", { class: "label" })]
|
|
69
|
+
})]
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
templateCss() {
|
|
73
|
+
return css`${BASE_STYLES}${jl_tab_bar_default}`;
|
|
74
|
+
}
|
|
75
|
+
onConnect() {
|
|
76
|
+
this.on(this._refs.items, "click", (event) => this.onClick(event));
|
|
77
|
+
this.on(this._refs.more, "click", () => this.events.emit("more"));
|
|
78
|
+
}
|
|
79
|
+
onClick(event) {
|
|
80
|
+
const id = event.target.closest(ITEM_SELECTOR)?.dataset.id;
|
|
81
|
+
if (id === void 0) return;
|
|
82
|
+
this.value.set(id);
|
|
83
|
+
this.events.emit("select", id);
|
|
84
|
+
}
|
|
85
|
+
render() {
|
|
86
|
+
this._refs.items.replaceChildren(...this.items.get().map((item) => this.renderItem(item)));
|
|
87
|
+
this.renderCurrent(this.value.get());
|
|
88
|
+
}
|
|
89
|
+
renderItem(item) {
|
|
90
|
+
const badge = badgeText(item.badge);
|
|
91
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
92
|
+
class: "icon",
|
|
93
|
+
children: [/* @__PURE__ */ jsx(JlIcon, {
|
|
94
|
+
name: item.icon,
|
|
95
|
+
size: ICON_SIZE
|
|
96
|
+
}), badge === "" ? null : /* @__PURE__ */ jsx("span", {
|
|
97
|
+
class: "badge",
|
|
98
|
+
children: badge
|
|
99
|
+
})]
|
|
100
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
101
|
+
class: "label",
|
|
102
|
+
children: item.label
|
|
103
|
+
})] });
|
|
104
|
+
if (item.href === void 0) return /* @__PURE__ */ jsx("button", {
|
|
105
|
+
class: "item",
|
|
106
|
+
type: "button",
|
|
107
|
+
"data-id": item.id,
|
|
108
|
+
children: content
|
|
109
|
+
});
|
|
110
|
+
return /* @__PURE__ */ jsx("a", {
|
|
111
|
+
class: "item",
|
|
112
|
+
href: item.href,
|
|
113
|
+
"data-id": item.id,
|
|
114
|
+
children: content
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** The current item says so: `aria-current="page"`, drawn in the accent. */
|
|
118
|
+
renderCurrent(value) {
|
|
119
|
+
for (const item of this._refs.items.querySelectorAll(ITEM_SELECTOR)) setOrRemove(item, "aria-current", item.dataset.id === value ? "page" : "");
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
JlTabBar.register();
|
|
123
|
+
export { JlTabBar };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// src/templates/jl-app-shell.css?raw
|
|
2
|
-
var jl_app_shell_default = ":host {\n display: block;\n height: 100%;\n min-height: 0;\n container: jl-app-shell / inline-size;\n --header-height: 56px;\n --drawer-width: min(300px, 86%);\n}\n\n.shell {\n position: relative;\n display: grid;\n grid-template:\n \"top top top\" auto\n \"nav main aside\" minmax(0, 1fr)\n / auto minmax(0, 1fr) auto;\n height: 100%;\n overflow: hidden;\n background: var(--jl-bg);\n color: var(--jl-text);\n}\n\n.top {\n grid-area: top;\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: var(--header-height);\n padding: 0 16px;\n border-bottom: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n:host(:not([has-header])) .top {\n display: none;\n}\n\n::slotted([slot=\"header\"]) {\n flex: 1;\n min-width: 0;\n}\n\n.menu {\n display: none;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--jl-control-md);\n height: var(--jl-control-md);\n margin-left: -8px;\n padding: 0;\n border: none;\n border-radius: var(--jl-radius-sm);\n background: none;\n color: var(--jl-text-2);\n cursor: pointer;\n}\n\n.menu:hover {\n background: var(--jl-surface-3);\n color: var(--jl-text);\n}\n\n.nav {\n grid-area: nav;\n min-height: 0;\n overflow-y: auto;\n border-right: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n.nav:focus-visible {\n box-shadow: none;\n}\n\n:host(:not([has-nav])) .nav {\n display: none;\n}\n\n.main {\n grid-area: main;\n min-width: 0;\n min-height: 0;\n overflow: auto;\n}\n\n.aside {\n grid-area: aside;\n min-height: 0;\n overflow-y: auto;\n border-left: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n:host(:not([has-aside])) .aside {\n display: none;\n}\n\n.scrim {\n display: none;\n}\n\n/* Narrow: the sidebar slides over the content as a drawer, the menu button opens it, the panel steps aside. */\n@container jl-app-shell (width < 960px) {\n .shell {\n grid-template:\n \"top\" auto\n \"main\" minmax(0, 1fr)\n / minmax(0, 1fr);\n }\n\n :host([has-nav]) .top {\n display: flex;\n }\n\n :host([has-nav]) .menu {\n display: inline-flex;\n }\n\n .nav {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n z-index: 2;\n max-width: var(--drawer-width);\n box-shadow: var(--jl-shadow-lg);\n transform: translateX(-100%);\n visibility: hidden;\n transition:\n transform var(--jl-slow) var(--jl-ease),\n visibility 0s linear var(--jl-slow);\n }\n\n :host([nav-open]) .nav {\n transform: none;\n visibility: visible;\n transition: transform var(--jl-slow) var(--jl-ease);\n }\n\n .scrim {\n position: absolute;\n inset: 0;\n z-index: 1;\n display: block;\n background: var(--jl-scrim);\n opacity: 0;\n pointer-events: none;\n transition: opacity var(--jl-slow) var(--jl-ease);\n }\n\n :host([nav-open]) .scrim {\n opacity: 1;\n pointer-events: auto;\n }\n\n .aside {\n display: none;\n }\n}\n";
|
|
2
|
+
var jl_app_shell_default = ":host {\n display: block;\n height: 100%;\n min-height: 0;\n container: jl-app-shell / inline-size;\n --header-height: 56px;\n --drawer-width: min(300px, 86%);\n}\n\n.shell {\n position: relative;\n display: grid;\n grid-template:\n \"top top top\" auto\n \"nav main aside\" minmax(0, 1fr)\n / auto minmax(0, 1fr) auto;\n height: 100%;\n overflow: hidden;\n background: var(--jl-bg);\n color: var(--jl-text);\n}\n\n.top {\n grid-area: top;\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: var(--header-height);\n padding: 0 16px;\n border-bottom: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n:host(:not([has-header])) .top {\n display: none;\n}\n\n::slotted([slot=\"header\"]) {\n flex: 1;\n min-width: 0;\n}\n\n.menu {\n display: none;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--jl-control-md);\n height: var(--jl-control-md);\n margin-left: -8px;\n padding: 0;\n border: none;\n border-radius: var(--jl-radius-sm);\n background: none;\n color: var(--jl-text-2);\n cursor: pointer;\n}\n\n.menu:hover {\n background: var(--jl-surface-3);\n color: var(--jl-text);\n}\n\n.nav {\n grid-area: nav;\n min-height: 0;\n overflow-y: auto;\n border-right: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n.nav:focus-visible {\n box-shadow: none;\n}\n\n:host(:not([has-nav])) .nav {\n display: none;\n}\n\n/* Its own stacking context: what the page lifts with z-index (a sticky bar) stays under the drawer and its scrim. */\n.main {\n grid-area: main;\n min-width: 0;\n min-height: 0;\n overflow: auto;\n isolation: isolate;\n}\n\n.aside {\n grid-area: aside;\n min-height: 0;\n overflow-y: auto;\n border-left: 1px solid var(--jl-line);\n background: var(--jl-surface);\n}\n\n:host(:not([has-aside])) .aside {\n display: none;\n}\n\n/* The bottom bar is for narrow screens: wide ones have the sidebar. */\n.tabs {\n display: none;\n}\n\n.scrim {\n display: none;\n}\n\n/* Narrow: the sidebar slides over the content as a drawer, the menu button opens it, the panel steps aside. */\n@container jl-app-shell (width < 960px) {\n .shell {\n grid-template:\n \"top\" auto\n \"main\" minmax(0, 1fr)\n \"tabs\" auto\n / minmax(0, 1fr);\n }\n\n :host([has-tabs]) .tabs {\n display: block;\n grid-area: tabs;\n }\n\n :host([has-nav]) .top {\n display: flex;\n }\n\n :host([has-nav]) .menu {\n display: inline-flex;\n }\n\n .nav {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n z-index: 2;\n max-width: var(--drawer-width);\n box-shadow: var(--jl-shadow-lg);\n transform: translateX(-100%);\n visibility: hidden;\n transition:\n transform var(--jl-slow) var(--jl-ease),\n visibility 0s linear var(--jl-slow);\n }\n\n :host([nav-open]) .nav {\n transform: none;\n visibility: visible;\n transition: transform var(--jl-slow) var(--jl-ease);\n }\n\n .scrim {\n position: absolute;\n inset: 0;\n z-index: 1;\n display: block;\n background: var(--jl-scrim);\n opacity: 0;\n pointer-events: none;\n transition: opacity var(--jl-slow) var(--jl-ease);\n }\n\n :host([nav-open]) .scrim {\n opacity: 1;\n pointer-events: auto;\n }\n\n .aside {\n display: none;\n }\n}\n";
|
|
3
3
|
export { jl_app_shell_default as default };
|
|
@@ -2,7 +2,8 @@ import { JlElement } from '../base/jl-element';
|
|
|
2
2
|
/**
|
|
3
3
|
* The frame of an app: a top bar (`header`), a sidebar (`nav`), the main area (the default slot), which scrolls
|
|
4
4
|
* on its own, and an optional panel on the right (`aside`). Narrower than 960 pixels, the sidebar becomes a drawer
|
|
5
|
-
* over the content, opened by a menu button in the top bar (or `nav-open`, or `toggleNav()`),
|
|
5
|
+
* over the content, opened by a menu button in the top bar (or `nav-open`, or `toggleNav()`), the panel hides, and
|
|
6
|
+
* a bar along the bottom shows (`tabs`, such as a `jl-tab-bar`, whose `more` opens the drawer).
|
|
6
7
|
* It takes the height of its container: give that one a height (`100dvh` for a whole page).
|
|
7
8
|
*/
|
|
8
9
|
export declare class JlAppShell extends JlElement {
|
|
@@ -27,11 +28,16 @@ export declare class JlAppShell extends JlElement {
|
|
|
27
28
|
/** Whether the shell is narrow: the menu button shows only then. */
|
|
28
29
|
private isNarrow;
|
|
29
30
|
private onMenu;
|
|
31
|
+
/** The tab bar's "more": the other places are in the drawer. */
|
|
32
|
+
private onMore;
|
|
30
33
|
private onKey;
|
|
31
34
|
/** The drawer as people open and close it: says so. */
|
|
32
35
|
private setNavOpen;
|
|
33
36
|
private renderNavOpen;
|
|
34
37
|
private renderInert;
|
|
35
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* What was slotted: empty areas take no room, a `select` from the nav (a side nav's) closes the drawer, and a
|
|
40
|
+
* `more` from the tabs (a tab bar's) opens it.
|
|
41
|
+
*/
|
|
36
42
|
private renderSlots;
|
|
37
43
|
}
|
|
@@ -15,7 +15,8 @@ var followsLink = (event) => event.composedPath().some((node) => node instanceof
|
|
|
15
15
|
/**
|
|
16
16
|
* The frame of an app: a top bar (`header`), a sidebar (`nav`), the main area (the default slot), which scrolls
|
|
17
17
|
* on its own, and an optional panel on the right (`aside`). Narrower than 960 pixels, the sidebar becomes a drawer
|
|
18
|
-
* over the content, opened by a menu button in the top bar (or `nav-open`, or `toggleNav()`),
|
|
18
|
+
* over the content, opened by a menu button in the top bar (or `nav-open`, or `toggleNav()`), the panel hides, and
|
|
19
|
+
* a bar along the bottom shows (`tabs`, such as a `jl-tab-bar`, whose `more` opens the drawer).
|
|
19
20
|
* It takes the height of its container: give that one a height (`100dvh` for a whole page).
|
|
20
21
|
*/
|
|
21
22
|
var JlAppShell = class extends JlElement {
|
|
@@ -23,7 +24,10 @@ var JlAppShell = class extends JlElement {
|
|
|
23
24
|
events = this.useEvents();
|
|
24
25
|
/** Whether the drawer is open, on narrow screens; wide ones always show the sidebar. */
|
|
25
26
|
navOpen = this.useChange(false, (value) => this.renderNavOpen(value), { attribute: "nav-open" });
|
|
26
|
-
_state = {
|
|
27
|
+
_state = {
|
|
28
|
+
navListeners: new AbortController(),
|
|
29
|
+
tabsListeners: new AbortController()
|
|
30
|
+
};
|
|
27
31
|
_resize = new ResizeObserver(() => this.renderInert());
|
|
28
32
|
_refs = this.useRefs((ref) => ({
|
|
29
33
|
aside: ref(".aside"),
|
|
@@ -33,7 +37,9 @@ var JlAppShell = class extends JlElement {
|
|
|
33
37
|
menu: ref(".menu"),
|
|
34
38
|
nav: ref(".nav"),
|
|
35
39
|
navSlot: ref("slot[name=\"nav\"]"),
|
|
36
|
-
scrim: ref(".scrim")
|
|
40
|
+
scrim: ref(".scrim"),
|
|
41
|
+
tabs: ref(".tabs"),
|
|
42
|
+
tabsSlot: ref("slot[name=\"tabs\"]")
|
|
37
43
|
}));
|
|
38
44
|
/** Opens or closes the drawer; `force` sets the state instead of flipping it. */
|
|
39
45
|
toggleNav(force) {
|
|
@@ -76,6 +82,11 @@ var JlAppShell = class extends JlElement {
|
|
|
76
82
|
attrs: { part: "aside" },
|
|
77
83
|
children: /* @__PURE__ */ jsx("slot", { name: "aside" })
|
|
78
84
|
}),
|
|
85
|
+
/* @__PURE__ */ jsx("div", {
|
|
86
|
+
class: "tabs",
|
|
87
|
+
attrs: { part: "tabs" },
|
|
88
|
+
children: /* @__PURE__ */ jsx("slot", { name: "tabs" })
|
|
89
|
+
}),
|
|
79
90
|
/* @__PURE__ */ jsx("div", { class: "scrim" })
|
|
80
91
|
]
|
|
81
92
|
});
|
|
@@ -95,12 +106,14 @@ var JlAppShell = class extends JlElement {
|
|
|
95
106
|
for (const slot of [
|
|
96
107
|
this._refs.headerSlot,
|
|
97
108
|
this._refs.navSlot,
|
|
98
|
-
this._refs.asideSlot
|
|
109
|
+
this._refs.asideSlot,
|
|
110
|
+
this._refs.tabsSlot
|
|
99
111
|
]) this.on(slot, "slotchange", () => this.renderSlots());
|
|
100
112
|
}
|
|
101
113
|
onDisconnect() {
|
|
102
114
|
this._resize.disconnect();
|
|
103
115
|
this._state.navListeners.abort();
|
|
116
|
+
this._state.tabsListeners.abort();
|
|
104
117
|
}
|
|
105
118
|
/** Whether the shell is narrow: the menu button shows only then. */
|
|
106
119
|
isNarrow() {
|
|
@@ -111,6 +124,11 @@ var JlAppShell = class extends JlElement {
|
|
|
111
124
|
this.setNavOpen(opening);
|
|
112
125
|
if (opening) this._refs.nav.focus();
|
|
113
126
|
}
|
|
127
|
+
/** The tab bar's "more": the other places are in the drawer. */
|
|
128
|
+
onMore() {
|
|
129
|
+
this.setNavOpen(true);
|
|
130
|
+
this._refs.nav.focus();
|
|
131
|
+
}
|
|
114
132
|
onKey(event) {
|
|
115
133
|
if (event.key !== "Escape" || !this.navOpen.get() || !this.isNarrow()) return;
|
|
116
134
|
event.preventDefault();
|
|
@@ -132,11 +150,20 @@ var JlAppShell = class extends JlElement {
|
|
|
132
150
|
const covered = this.navOpen.get() && this.isNarrow();
|
|
133
151
|
this._refs.main.inert = covered;
|
|
134
152
|
this._refs.aside.inert = covered;
|
|
153
|
+
this._refs.tabs.inert = covered;
|
|
135
154
|
}
|
|
136
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* What was slotted: empty areas take no room, a `select` from the nav (a side nav's) closes the drawer, and a
|
|
157
|
+
* `more` from the tabs (a tab bar's) opens it.
|
|
158
|
+
*/
|
|
137
159
|
renderSlots() {
|
|
138
160
|
this.reflect("has-header", this._refs.headerSlot.assignedElements().length > 0);
|
|
139
161
|
this.reflect("has-aside", this._refs.asideSlot.assignedElements().length > 0);
|
|
162
|
+
const tabElements = this._refs.tabsSlot.assignedElements();
|
|
163
|
+
this.reflect("has-tabs", tabElements.length > 0);
|
|
164
|
+
this._state.tabsListeners.abort();
|
|
165
|
+
this._state.tabsListeners = new AbortController();
|
|
166
|
+
for (const element of tabElements) element.addEventListener("more", () => this.onMore(), { signal: this._state.tabsListeners.signal });
|
|
140
167
|
const navElements = this._refs.navSlot.assignedElements();
|
|
141
168
|
this.reflect("has-nav", navElements.length > 0);
|
|
142
169
|
this._state.navListeners.abort();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** One choice of a chip group. */
|
|
2
|
+
export type ChipGroupOption = {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** An icon name before the label; a chosen chip without one shows a check. */
|
|
6
|
+
icon?: string;
|
|
7
|
+
/** A count or a short word after the label: how many things the choice holds. */
|
|
8
|
+
badge?: string | number;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** One place in a bottom tab bar: an icon over a short label. */
|
|
2
|
+
export type TabBarItem = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** An icon name: the tab bar draws one over every label. */
|
|
6
|
+
icon: string;
|
|
7
|
+
/** A link to follow; without it the item is a button that only fires `select`. */
|
|
8
|
+
href?: string;
|
|
9
|
+
/** A count or a short word over the icon: `3`, `New`. Counts above 99 show as `99+`. */
|
|
10
|
+
badge?: string | number;
|
|
11
|
+
};
|