jsl-ui 9.29.0 → 10.0.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/docs/AutoSuggestComponent.md +17 -0
- package/docs/ButtonComponent.md +14 -0
- package/docs/CheckBoxComponent.md +12 -0
- package/docs/DateComponent.md +12 -0
- package/docs/DateRangeComponent.md +13 -0
- package/docs/DateTimeComponent.md +13 -0
- package/docs/DialogComponent.md +15 -0
- package/docs/ExpanderComponent.md +13 -0
- package/docs/FormComponent.md +15 -0
- package/docs/FormFieldComponent.md +15 -0
- package/docs/GridComponent.md +19 -0
- package/docs/InputComponent.md +15 -0
- package/docs/IntervalComponent.md +13 -0
- package/docs/LoadingComponent.md +12 -0
- package/docs/NavigationComponent.md +17 -0
- package/docs/Notification.md +14 -0
- package/docs/NotificationComponent.md +13 -0
- package/docs/NumberComponent.md +12 -0
- package/docs/OverlayComponent.md +12 -0
- package/docs/ProgressComponent.md +12 -0
- package/docs/README.md +24 -0
- package/docs/RibbonMenuComponent.md +14 -0
- package/docs/SelectComponent.md +15 -0
- package/docs/SplitterComponent.md +14 -0
- package/docs/StackPanelComponent.md +13 -0
- package/docs/TabsComponent.md +16 -0
- package/docs/TimeComponent.md +13 -0
- package/docs/TreeViewComponent.md +16 -0
- package/examples/showcase/checks-and-expanders.ts +163 -0
- package/examples/showcase/cultures.ts +39 -0
- package/examples/showcase/dates.ts +182 -0
- package/examples/showcase/dialogs.ts +184 -0
- package/examples/showcase/feedback-and-navigation.ts +174 -0
- package/examples/showcase/fields.ts +178 -0
- package/examples/showcase/fonts/Jost-Variable.ttf +0 -0
- package/examples/showcase/fonts/OFL-Jost.txt +93 -0
- package/examples/showcase/grids.ts +150 -0
- package/examples/showcase/index.html +60 -0
- package/examples/showcase/layout-and-navigation.ts +47 -0
- package/examples/showcase/nivona.css +98 -0
- package/examples/showcase/selects.ts +158 -0
- package/examples/showcase/showcase.css +231 -0
- package/examples/showcase/showcase.ts +214 -0
- package/examples/showcase/tabs.ts +161 -0
- package/examples/showcase/themes.css +83 -0
- package/examples/showcase/themes.ts +54 -0
- package/lib/components/IntervalComponent.d.ts +5 -0
- package/lib/components/IntervalComponent.js +29 -13
- package/lib/components/auto-suggest/AutoSuggestComponent.d.ts +11 -9
- package/lib/components/auto-suggest/AutoSuggestComponent.js +25 -38
- package/lib/components/button/ButtonComponent.d.ts +30 -1
- package/lib/components/button/ButtonComponent.js +85 -4
- package/lib/components/checkbox/CheckBoxComponent.d.ts +11 -1
- package/lib/components/checkbox/CheckBoxComponent.js +66 -56
- package/lib/components/checkbox/CheckBoxStyle.scss +72 -85
- package/lib/components/date/DateComponent.d.ts +10 -0
- package/lib/components/date/DateComponent.js +92 -44
- package/lib/components/date-range/DateRangeComponent.d.ts +14 -3
- package/lib/components/date-range/DateRangeComponent.js +96 -86
- package/lib/components/date-range/DateRangeStyle.scss +35 -11
- package/lib/components/date-time/DateTimeComponent.d.ts +8 -0
- package/lib/components/date-time/DateTimeComponent.js +64 -23
- package/lib/components/date-time/DateTimeStyle.scss +10 -7
- package/lib/components/dialog/DialogComponent.d.ts +21 -7
- package/lib/components/dialog/DialogComponent.js +179 -122
- package/lib/components/expander/ExpanderComponent.d.ts +15 -1
- package/lib/components/expander/ExpanderComponent.js +85 -64
- package/lib/components/expander/ExpanderStyle.scss +61 -47
- package/lib/components/form-field/FormFieldComponent.d.ts +45 -15
- package/lib/components/form-field/FormFieldComponent.js +249 -96
- package/lib/components/form-field/FormFieldStyle.scss +36 -87
- package/lib/components/form-field/create-control.d.ts +7 -0
- package/lib/components/form-field/create-control.js +48 -0
- package/lib/components/input/InputComponent.d.ts +18 -1
- package/lib/components/input/InputComponent.js +68 -93
- package/lib/components/loading/LoadingComponent.d.ts +9 -2
- package/lib/components/loading/LoadingComponent.js +13 -12
- package/lib/components/loading/LoadingStyle.css +19 -29
- package/lib/components/navigation/NavigationComponent.d.ts +34 -10
- package/lib/components/navigation/NavigationComponent.js +261 -33
- package/lib/components/navigation/NavigationStyle.scss +62 -226
- package/lib/components/notification/Notification.d.ts +37 -4
- package/lib/components/notification/Notification.js +160 -41
- package/lib/components/notification/Notification.scss +103 -19
- package/lib/components/number/NumberComponent.d.ts +16 -2
- package/lib/components/number/NumberComponent.js +54 -53
- package/lib/components/overlay/OverlayComponent.d.ts +13 -7
- package/lib/components/overlay/OverlayComponent.js +46 -114
- package/lib/components/overlay/overlay.css +18 -57
- package/lib/components/progress/ProgressComponent.d.ts +1 -0
- package/lib/components/progress/ProgressComponent.js +6 -6
- package/lib/components/progress/ProgressStyle.scss +5 -15
- package/lib/components/ribbon-menu/RibbonMenuComponent.js +2 -2
- package/lib/components/ribbon-menu/RibbonMenuStyle.scss +6 -76
- package/lib/components/select/SelectComponent.d.ts +33 -3
- package/lib/components/select/SelectComponent.js +312 -278
- package/lib/components/select/SelectDDAreaComponent.d.ts +13 -7
- package/lib/components/select/SelectDDAreaComponent.js +215 -256
- package/lib/components/select/SelectStyle.scss +204 -186
- package/lib/components/slider/nouislider.css +40 -54
- package/lib/components/stack-panel/SplitterComponent.d.ts +19 -3
- package/lib/components/stack-panel/SplitterComponent.js +94 -111
- package/lib/components/stack-panel/SplitterStyle.scss +9 -45
- package/lib/components/stack-panel/StackPanelComponent.js +3 -3
- package/lib/components/stack-panel/StackPanelStyle.scss +10 -52
- package/lib/components/tabs/TabsComponent.d.ts +17 -1
- package/lib/components/tabs/TabsComponent.js +247 -123
- package/lib/components/tabs/TabsStyle.scss +138 -75
- package/lib/components/time/TimeComponent.d.ts +5 -0
- package/lib/components/time/TimeComponent.js +61 -69
- package/lib/components/time/TimeComponent.scss +4 -4
- package/lib/components/tree-view/TreeViewComponent.d.ts +10 -4
- package/lib/components/tree-view/TreeViewComponent.js +65 -32
- package/lib/components/tree-view/TreeViewStyle.scss +12 -96
- package/lib/core/jq.js +11 -8
- package/lib/grid/slick-default-theme.css +107 -31
- package/lib/grid/slick.creator.js +23 -5
- package/lib/grid/slick.formatters.js +4 -3
- package/lib/grid/slick.grid.css +38 -26
- package/lib/grid/slick.grid.d.ts +1 -0
- package/lib/grid/slick.grid.js +2 -3
- package/lib/grid.css +3413 -454
- package/lib/index.css +2500 -1031
- package/lib/index.css.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/interaction/moveable.d.ts +1 -0
- package/lib/interaction/moveable.js +8 -0
- package/lib/interaction/resizable.d.ts +1 -0
- package/lib/interaction/resizable.js +9 -0
- package/lib/ui/auto.suggest.css +31 -14
- package/lib/ui/auto.suggest.d.ts +1 -0
- package/lib/ui/auto.suggest.js +88 -18
- package/lib/ui/base.css +2 -48
- package/lib/ui/button.css +134 -0
- package/lib/ui/date-input.css +11 -0
- package/lib/ui/date.picker/css/tiny-date-picker.css +124 -325
- package/lib/ui/date.picker/date-picker-options.js +4 -3
- package/lib/ui/date.picker/lib/date-manip.js +9 -0
- package/lib/ui/date.picker/mode/base-mode.d.ts +2 -1
- package/lib/ui/date.picker/mode/base-mode.js +112 -22
- package/lib/ui/date.picker/mode/dropdown-mode.d.ts +2 -4
- package/lib/ui/date.picker/mode/dropdown-mode.js +29 -42
- package/lib/ui/date.picker/mode/index.d.ts +2 -1
- package/lib/ui/date.picker/mode/modal-mode.d.ts +2 -1
- package/lib/ui/date.picker/mode/modal-mode.js +4 -2
- package/lib/ui/date.picker/mode/permanent-mode.d.ts +2 -1
- package/lib/ui/date.picker/views/day-picker.js +24 -15
- package/lib/ui/date.picker/views/month-picker.js +1 -1
- package/lib/ui/date.picker/views/year-picker.js +3 -2
- package/lib/ui/date.picker.d.ts +2 -1
- package/lib/ui/dialog.css +144 -108
- package/lib/ui/dialog.d.ts +19 -1
- package/lib/ui/dialog.js +178 -60
- package/lib/ui/input.css +35 -0
- package/lib/ui/number.input.js +50 -37
- package/lib/ui/theme.css +28 -0
- package/package.json +12 -8
- package/lib/components/drop-down/DropDownComponent.d.ts +0 -27
- package/lib/components/drop-down/DropDownComponent.js +0 -60
- package/lib/components/expander/OldExpanderStyle.scss +0 -37
- package/lib/ui/drop.down.d.ts +0 -52
- package/lib/ui/drop.down.js +0 -2112
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import culture from "../../core/culture";
|
|
2
|
+
import { date } from "../../core/date";
|
|
3
|
+
import { numeric } from "../../core/numeric";
|
|
4
|
+
|
|
5
|
+
export const showcaseCultures = [
|
|
6
|
+
{ id: "", label: "Default (ISO)" },
|
|
7
|
+
{ id: "de", label: "Deutsch (de)" },
|
|
8
|
+
{ id: "en-us", label: "English (en-us)" },
|
|
9
|
+
{ id: "en-gb", label: "English (en-gb)" }
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
const storageKey = "jsl-showcase-culture";
|
|
13
|
+
const picker = document.getElementById("showcase-culture") as HTMLSelectElement;
|
|
14
|
+
const preview = document.getElementById("showcase-format-preview");
|
|
15
|
+
const normalize = (id: string) => showcaseCultures.find(item => item.id === id?.toLowerCase())?.id ?? "";
|
|
16
|
+
let saved = "";
|
|
17
|
+
try { saved = localStorage.getItem(storageKey) ?? ""; } catch { /* URL selection works without storage. */ }
|
|
18
|
+
const selected = normalize(new URL(location.href).searchParams.get("culture") ?? saved);
|
|
19
|
+
|
|
20
|
+
// This module runs before sample modules construct controls with formatted values.
|
|
21
|
+
culture.set(selected);
|
|
22
|
+
picker.replaceChildren(...showcaseCultures.map(item => {
|
|
23
|
+
const option = document.createElement("option");
|
|
24
|
+
option.value = item.id; option.textContent = item.label;
|
|
25
|
+
return option;
|
|
26
|
+
}));
|
|
27
|
+
picker.value = selected;
|
|
28
|
+
preview.textContent = date.format(new Date(2026, 9, 23)) + " · " + numeric.format(1234.56, "#,0.00");
|
|
29
|
+
|
|
30
|
+
/** Reinitialize the examples so existing input strings aren't parsed under a new culture. */
|
|
31
|
+
export function changeCulture(id: string): void {
|
|
32
|
+
const next = normalize(id);
|
|
33
|
+
if (next === selected) { return; }
|
|
34
|
+
try { localStorage.setItem(storageKey, next); } catch { /* Keep selection in the URL instead. */ }
|
|
35
|
+
const url = new URL(location.href);
|
|
36
|
+
url.searchParams.set("culture", next);
|
|
37
|
+
location.assign(url.href);
|
|
38
|
+
}
|
|
39
|
+
picker.addEventListener("change", () => changeCulture(picker.value));
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { IJSLComponent, IJSLVNode } from "jsl-render";
|
|
2
|
+
import { noUiSlider } from "../../components/slider/nouislider";
|
|
3
|
+
import { DateComponent } from "../../components/date/DateComponent";
|
|
4
|
+
import { DateRangeComponent } from "../../components/date-range/DateRangeComponent";
|
|
5
|
+
import { DateTimeComponent } from "../../components/date-time/DateTimeComponent";
|
|
6
|
+
import { ButtonComponent } from "../../components/button/ButtonComponent";
|
|
7
|
+
import { DialogComponent } from "../../components/dialog/DialogComponent";
|
|
8
|
+
import { DatePicker, IDatePicker } from "../../ui/date.picker";
|
|
9
|
+
import { date } from "../../core/date";
|
|
10
|
+
|
|
11
|
+
type Content = IJSLVNode | IJSLComponent;
|
|
12
|
+
const text = (tag: string, content: string, className?: string): IJSLVNode => ({ tag, content, attr: { class: className } });
|
|
13
|
+
const group = (children: Content[], className?: string): IJSLVNode => ({ tag: "div", attr: { class: className }, children });
|
|
14
|
+
const status = (content: string) => ({ tag: "p", content, attr: { class: "sample-status", role: "status" } });
|
|
15
|
+
const format = (value: Date) => value ? date.format(value) : "No date selected";
|
|
16
|
+
const day = (n: number, hours = 9, minutes = 30, seconds = 0) => new Date(2026, 9, n, hours, minutes, seconds);
|
|
17
|
+
const code = (content: string) => text("pre", content, "sample-code");
|
|
18
|
+
const action = (content: string, click: () => void) => new ButtonComponent({ content, click, type: "button", outlined: true, size: "small" });
|
|
19
|
+
const field = (id: string, label: string, control: Content) => group([
|
|
20
|
+
{ tag: "label", content: label, attr: { for: id } }, control
|
|
21
|
+
], "date-showcase-field");
|
|
22
|
+
|
|
23
|
+
class SliderSample implements IJSLComponent {
|
|
24
|
+
private slider: any;
|
|
25
|
+
constructor(private options: { start: number[]; disabled?: boolean; vertical?: boolean; tooltips?: boolean; }) {}
|
|
26
|
+
render(): IJSLVNode {
|
|
27
|
+
return { tag: "div", attr: {
|
|
28
|
+
class: "showcase-slider" + (this.options.vertical ? " showcase-slider-vertical" : ""),
|
|
29
|
+
disabled: this.options.disabled ? "" : undefined
|
|
30
|
+
} };
|
|
31
|
+
}
|
|
32
|
+
onCreate(node: IJSLVNode) {
|
|
33
|
+
const count = this.options.start.length;
|
|
34
|
+
this.slider = noUiSlider.create(node.dom, {
|
|
35
|
+
start: this.options.start, range: { min: 0, max: 100 }, step: 1,
|
|
36
|
+
connect: count === 2 ? true : [true, false],
|
|
37
|
+
orientation: this.options.vertical ? "vertical" : "horizontal",
|
|
38
|
+
direction: this.options.vertical ? "rtl" : "ltr",
|
|
39
|
+
tooltips: this.options.tooltips ? { to: value => Math.round(value) + "%" } : false
|
|
40
|
+
});
|
|
41
|
+
node.dom.querySelectorAll<HTMLElement>("[role=slider]").forEach((handle, index) => {
|
|
42
|
+
handle.setAttribute("aria-label", count === 2 ? (index === 0 ? "Lower value" : "Upper value") : "Value");
|
|
43
|
+
if (this.options.disabled) {
|
|
44
|
+
handle.setAttribute("tabindex", "-1");
|
|
45
|
+
handle.setAttribute("aria-disabled", "true");
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// Sibling nodes are available after the renderer finishes mounting.
|
|
49
|
+
Promise.resolve().then(() => this.slider?.on("update", values => {
|
|
50
|
+
const readout = node.dom.parentElement?.querySelector<HTMLElement>(".slider-readout");
|
|
51
|
+
if (readout) { readout.textContent = values.map(value => Math.round(+value) + "%").join(" – "); }
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
onRemove() { this.slider?.destroy(); this.slider = null; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class CalendarSample implements IJSLComponent {
|
|
58
|
+
private picker: IDatePicker;
|
|
59
|
+
render(): IJSLVNode { return group([], "showcase-calendar"); }
|
|
60
|
+
onCreate(node: IJSLVNode) {
|
|
61
|
+
this.picker = DatePicker(node.dom, { mode: "dp-permanent", hilightedDate: day(15) });
|
|
62
|
+
}
|
|
63
|
+
onRemove() { this.picker?.destroy(); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function card(id: string, number: string, title: string, description: string, children: Content[]): IJSLVNode {
|
|
67
|
+
return { tag: "article", attr: { class: "sample-card", id }, children: [
|
|
68
|
+
group([text("span", number, "sample-number"), text("h3", title)], "card-heading"),
|
|
69
|
+
text("p", description, "card-description"), ...children
|
|
70
|
+
] };
|
|
71
|
+
}
|
|
72
|
+
function section(id: string, number: string, title: string, description: string, preview: Content, cards: IJSLVNode[], usage: Content[]): IJSLVNode {
|
|
73
|
+
return { tag: "section", attr: { id, class: "component-section", "aria-labelledby": id + "-title" }, children: [
|
|
74
|
+
group([
|
|
75
|
+
group([text("p", "THE ESSENTIALS / " + number, "eyebrow"),
|
|
76
|
+
{ tag: "h2", content: title, attr: { id: id + "-title", class: "section-title" } },
|
|
77
|
+
text("p", description, "hero-description")], "hero-copy"),
|
|
78
|
+
group([text("span", "INTERACTIVE PREVIEW", "preview-caption"), preview], "hero-preview date-hero-preview")
|
|
79
|
+
], "hero"),
|
|
80
|
+
group(cards, "sample-grid date-sample-grid"),
|
|
81
|
+
{ tag: "details", attr: { class: "usage" }, children: [text("summary", title + " usage & compatibility"), ...usage] }
|
|
82
|
+
] };
|
|
83
|
+
}
|
|
84
|
+
const sliderPreview = group([new SliderSample({ start: [30, 75] }), text("span", "", "slider-readout")], "slider-sample");
|
|
85
|
+
export const sliderShowcase = section("sliders", "04", "Sliders",
|
|
86
|
+
"A lighter track, a precise handle. Move a value or narrow a range.", sliderPreview, [
|
|
87
|
+
card("slider-single", "01", "A single value", "Click the track, drag the handle, or use the arrow keys. Home and End reach the limits.", [
|
|
88
|
+
group([new SliderSample({ start: [45], tooltips: true }), text("span", "", "slider-readout")], "slider-sample slider-with-tooltip")
|
|
89
|
+
]),
|
|
90
|
+
card("slider-states", "02", "Different directions", "The same design supports vertical sliders and disabled controls.", [
|
|
91
|
+
group([new SliderSample({ start: [65], vertical: true }),
|
|
92
|
+
group([text("p", "Unavailable range", "inline-note"), new SliderSample({ start: [20, 80], disabled: true })], "slider-disabled-example")
|
|
93
|
+
], "slider-orientations")
|
|
94
|
+
])
|
|
95
|
+
], [
|
|
96
|
+
code('noUiSlider.create(element, {\n start: [25, 75], connect: true, step: 1,\n range: { min: 0, max: 100 }\n});'),
|
|
97
|
+
text("p", "The bundled noUiSlider API is preserved. Its CSS now uses the shared theme, including handles, tooltips, pips, and disabled states. Use aria-label on handles to describe their meaning. Override --jsl-slider-track and --jsl-slider-connect for a particular slider.")
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
const dateStatus = status("Choose a day or enter a date.");
|
|
101
|
+
const customStatus = status("Uses day/month/year formatting.");
|
|
102
|
+
export const dateShowcase = section("dates", "05", "Dates",
|
|
103
|
+
"Type a date or pick a day. A calmer calendar for everyday planning.", new CalendarSample(), [
|
|
104
|
+
card("date-basic", "01", "A date, simply", "Focus the field to open the calendar. Arrow Down moves into it; Escape returns to the field.", [
|
|
105
|
+
field("showcase-date-basic", "Due date", new DateComponent({ id: "showcase-date-basic", value: day(15), onChange: value => { dateStatus.content = format(value); } })),
|
|
106
|
+
dateStatus,
|
|
107
|
+
field("showcase-date-disabled", "Unavailable date", new DateComponent({ id: "showcase-date-disabled", value: day(20), enabled: false }))
|
|
108
|
+
]),
|
|
109
|
+
card("date-custom", "02", "Your format, your availability", "A custom formatter and parser, with October weekdays available for selection.", [
|
|
110
|
+
field("showcase-date-custom", "Appointment date (DD/MM/YYYY)", new DateComponent({
|
|
111
|
+
id: "showcase-date-custom", value: day(19), min: day(1), max: day(31),
|
|
112
|
+
format: value => date.format(value, "dd/mm/yy"),
|
|
113
|
+
parse: value => date.parse(value, "dd/mm/yy"),
|
|
114
|
+
inRange: value => value.getDay() !== 0 && value.getDay() !== 6,
|
|
115
|
+
onChange: value => { customStatus.content = value ? date.format(value, "dd/mm/yy") : "No date selected"; }
|
|
116
|
+
})), customStatus,
|
|
117
|
+
text("p", "Invalid or unavailable dates stay in the field for correction and do not raise onChange.", "inline-note")
|
|
118
|
+
])
|
|
119
|
+
], [
|
|
120
|
+
code('new DateComponent({\n value: new Date(2026, 9, 15),\n min: new Date(2026, 9, 1), max: new Date(2026, 9, 31),\n onChange: value => console.log(value)\n});'),
|
|
121
|
+
text("p", "Existing formatting, parsing, localization, dateClass, inRange, binding, and getRef options remain available. Empty fields return null. setValue(date) updates the field and calendar silently. getRef().setState(state) retains its selection events; pass true as a second argument for a silent update."),
|
|
122
|
+
text("p", "Calendar controls support Tab, Enter, Space, arrow keys, and Escape. Use ariaLabel or an associated label for the field. Popups within dialogs stay inside the dialog; appendTo allows an explicit popup container.")
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
const rangeStatus = status("October 8 – October 22, 2026");
|
|
126
|
+
const mainRange = new DateRangeComponent({
|
|
127
|
+
min: day(1), max: day(31), minValue: day(8), maxValue: day(22), increment: "days",
|
|
128
|
+
onChange: value => { rangeStatus.content = format(value.minValue) + " – " + format(value.maxValue); }
|
|
129
|
+
});
|
|
130
|
+
export const dateRangeShowcase = section("date-ranges", "06", "Date ranges",
|
|
131
|
+
"Two dates, one selection. Fine-tune either endpoint or move the handles.", new DateRangeComponent({
|
|
132
|
+
min: day(1), max: day(31), minValue: day(10), maxValue: day(24), increment: "days"
|
|
133
|
+
}), [
|
|
134
|
+
card("range-basic", "01", "Choose a window", "The fields and slider stay in sync. Endpoints stay ordered and within the available dates.", [mainRange, rangeStatus]),
|
|
135
|
+
card("range-disabled", "02", "Fixed and unavailable", "Disabled inputs and handles preserve a range. A single available day is supported too.", [
|
|
136
|
+
new DateRangeComponent({ min: day(1), max: day(31), minValue: day(8), maxValue: day(22), increment: "days", enabled: false }),
|
|
137
|
+
text("p", "One available day", "inline-note"),
|
|
138
|
+
new DateRangeComponent({ min: day(15), max: day(15), increment: "days", ariaLabel: "Single available day" })
|
|
139
|
+
])
|
|
140
|
+
], [
|
|
141
|
+
code('new DateRangeComponent({\n min, max, minValue: start, maxValue: end,\n increment: "days",\n onChange: ({ minValue, maxValue }) => { /* Selected dates */ }\n});'),
|
|
142
|
+
text("p", "getMin(), getMax(), and the existing range options are preserved. Daily increments remain the supported mode. onChange reports both endpoints once for each changed range, and is silent during initialization. enabled, ariaLabel, startLabel, and endLabel are optional additions.")
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
const timeStatus = status("Pick a date and enter a time.");
|
|
146
|
+
const dateTimeModel = { appointment: day(21, 14, 45) };
|
|
147
|
+
const boundStatus = status("October 21 at 14:45");
|
|
148
|
+
const boundDateTime = new DateTimeComponent({
|
|
149
|
+
id: "showcase-datetime-bound", bindTo: dateTimeModel, property: "appointment", bindMode: "both",
|
|
150
|
+
onChange: value => { boundStatus.content = value ? format(value) + " at " + date.formatTime(value, "HourMinute") : "Appointment cleared"; }
|
|
151
|
+
});
|
|
152
|
+
export const dateTimeShowcase = section("date-times", "07", "Date & time",
|
|
153
|
+
"A day and a moment, together. Familiar fields with room for precision.", new DateTimeComponent({
|
|
154
|
+
id: "showcase-datetime-preview", value: day(15, 10, 30), ariaLabel: "Preview appointment"
|
|
155
|
+
}), [
|
|
156
|
+
card("datetime-basic", "01", "Plan a precise moment", "Use hours and minutes, or include seconds when the details matter.", [
|
|
157
|
+
field("showcase-datetime-seconds", "Scheduled start", new DateTimeComponent({
|
|
158
|
+
id: "showcase-datetime-seconds", inputMode: "HourMinuteSecond", value: day(15, 9, 30, 15),
|
|
159
|
+
onChange: value => { timeStatus.content = value ? format(value) + " at " + date.formatTime(value, "HourMinuteSecond") : "No complete date and time"; }
|
|
160
|
+
})), timeStatus,
|
|
161
|
+
field("showcase-datetime-disabled", "Unavailable schedule", new DateTimeComponent({ id: "showcase-datetime-disabled", value: day(15), enabled: false }))
|
|
162
|
+
]),
|
|
163
|
+
card("datetime-model", "02", "Connected to your application", "Changes update the model. A model change refreshes both fields.", [
|
|
164
|
+
field("showcase-datetime-bound", "Appointment", boundDateTime),
|
|
165
|
+
group([action("Move to October 26", () => {
|
|
166
|
+
dateTimeModel.appointment = day(26, 11, 15);
|
|
167
|
+
boundStatus.content = "Model updated to October 26 at 11:15.";
|
|
168
|
+
}), action("Open in a dialog", () => {
|
|
169
|
+
DialogComponent.show({
|
|
170
|
+
header: "Schedule an appointment", width: "28rem",
|
|
171
|
+
content: group([
|
|
172
|
+
text("p", "Choose a date and time. Escape closes the calendar first.", "card-description"),
|
|
173
|
+
field("showcase-dialog-date", "Appointment", new DateTimeComponent({ id: "showcase-dialog-date", value: day(21, 14, 45) }))
|
|
174
|
+
]), footer: group([DialogComponent.Cancel(), DialogComponent.Ok()], "sample-row")
|
|
175
|
+
});
|
|
176
|
+
})], "sample-row"), boundStatus
|
|
177
|
+
])
|
|
178
|
+
], [
|
|
179
|
+
code('new DateTimeComponent({\n value: new Date(2026, 9, 15, 9, 30),\n inputMode: "HourMinute", // Or "HourMinuteSecond"\n bindTo: model, property: "appointment", bindMode: "both",\n onChange: value => console.log(value)\n});'),
|
|
180
|
+
text("p", "The existing Date-valued callbacks, converters, getValue(), and getValueAsDate() remain available. toModel, fromModel, and both binding modes are supported. An incomplete value returns null; malformed dates and times do not update the model. setValue(date) silently updates both fields."),
|
|
181
|
+
text("p", "The supplied id belongs to the date field; the time field gets an id with '-time' appended. ariaLabel names the group, and dateAriaLabel and timeAriaLabel can name its fields. Date-only formatting and parsing options apply to the date field.")
|
|
182
|
+
]);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { IJSLVNode } from "jsl-render";
|
|
2
|
+
import { DialogComponent } from "../../components/dialog/DialogComponent";
|
|
3
|
+
import { ButtonComponent } from "../../components/button/ButtonComponent";
|
|
4
|
+
|
|
5
|
+
const text = (tag: string, content: string, className?: string): IJSLVNode => ({ tag, content, attr: { class: className } });
|
|
6
|
+
const actions = (...children: IJSLVNode[]): IJSLVNode => ({ tag: "div", children });
|
|
7
|
+
const message = (content: string): IJSLVNode => ({ tag: "p", content, attr: { class: "sample-status", role: "status" } });
|
|
8
|
+
const launch = (content: string, id: string, click: () => void) => new ButtonComponent({ content, class: id, type: "button", outlined: true, click });
|
|
9
|
+
|
|
10
|
+
const confirmationStatus = message("Choose an action inside the dialog.");
|
|
11
|
+
function showConfirmation() {
|
|
12
|
+
DialogComponent.show({
|
|
13
|
+
header: "Save your changes?",
|
|
14
|
+
content: { tag: "div", attr: { class: "showcase-dialog-copy" }, children: [
|
|
15
|
+
text("p", "Your workspace has been updated. Save your changes to keep everything in sync."),
|
|
16
|
+
{ tag: "div", attr: { class: "dialog-summary" }, children: [
|
|
17
|
+
text("span", "Workspace", "dialog-summary-label"), text("strong", "Product design"),
|
|
18
|
+
text("span", "Changes", "dialog-summary-label"), text("span", "Settings and permissions")
|
|
19
|
+
] }
|
|
20
|
+
] },
|
|
21
|
+
footer: actions(DialogComponent.Cancel(), DialogComponent.Button({
|
|
22
|
+
content: "Save changes", class: "dlg-ok-btn",
|
|
23
|
+
onClick: () => { confirmationStatus.content = "Changes saved."; }
|
|
24
|
+
})),
|
|
25
|
+
width: "29rem", showCloseButton: true
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const formStatus = message("Create a project without leaving the page.");
|
|
30
|
+
function showForm() {
|
|
31
|
+
const nameId = "showcase-project-name";
|
|
32
|
+
const formId = "showcase-project-form";
|
|
33
|
+
DialogComponent.show({
|
|
34
|
+
header: "Create a project",
|
|
35
|
+
content: { tag: "form", attr: { id: formId, class: "showcase-dialog-copy", submit: (event: Event) => event.preventDefault() }, children: [
|
|
36
|
+
text("p", "Give your team a place to organize its next idea."),
|
|
37
|
+
{ tag: "div", attr: { class: "dialog-form-field" }, children: [
|
|
38
|
+
{ tag: "label", content: "Project name", attr: { for: nameId } },
|
|
39
|
+
{ tag: "input", attr: { id: nameId, name: "name", type: "text", required: "required", placeholder: "e.g. Website refresh", maxlength: 80 } }
|
|
40
|
+
] },
|
|
41
|
+
{ tag: "div", attr: { class: "dialog-form-field" }, children: [
|
|
42
|
+
{ tag: "label", content: "Description (optional)", attr: { for: "showcase-project-notes" } },
|
|
43
|
+
{ tag: "textarea", attr: { id: "showcase-project-notes", name: "notes", rows: 3, placeholder: "What are you working on?" } }
|
|
44
|
+
] }
|
|
45
|
+
] },
|
|
46
|
+
footer: actions(DialogComponent.Cancel(), DialogComponent.Button({
|
|
47
|
+
content: "Create project", class: "dlg-ok-btn", onClick: () => {
|
|
48
|
+
const form = document.getElementById(formId) as HTMLFormElement;
|
|
49
|
+
const name = document.getElementById(nameId) as HTMLInputElement;
|
|
50
|
+
if (!form.reportValidity() || !name.value.trim()) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
formStatus.content = 'Project "' + name.value.trim() + '" created.';
|
|
54
|
+
}
|
|
55
|
+
})),
|
|
56
|
+
showCloseButton: true, autoFocus: "#" + nameId, OkOnEnter: true
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function showLongContent() {
|
|
61
|
+
DialogComponent.show({
|
|
62
|
+
header: "Workspace access",
|
|
63
|
+
content: { tag: "div", attr: { class: "showcase-dialog-copy" }, children: [
|
|
64
|
+
text("p", "Review who can access this workspace. The actions stay in view while this list scrolls."),
|
|
65
|
+
...Array.from({ length: 18 }, (_, index): IJSLVNode => ({
|
|
66
|
+
tag: "div", attr: { class: "dialog-member" }, children: [
|
|
67
|
+
text("span", ("0" + (index + 1)).slice(-2), "dialog-member-avatar"),
|
|
68
|
+
{ tag: "div", children: [text("strong", "Team member " + (index + 1)), text("span", "member" + (index + 1) + "@example.com")] },
|
|
69
|
+
text("span", index === 0 ? "Owner" : "Member", "dialog-member-role")
|
|
70
|
+
]
|
|
71
|
+
}))
|
|
72
|
+
] },
|
|
73
|
+
footer: actions(DialogComponent.Button({ content: "Done", class: "dlg-ok-btn" })),
|
|
74
|
+
width: "36rem", height: "22rem", showCloseButton: true
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function showWorkspace(modal = true) {
|
|
79
|
+
DialogComponent.show({
|
|
80
|
+
header: "Workspace details",
|
|
81
|
+
content: { tag: "div", attr: { class: "showcase-dialog-copy" }, children: [
|
|
82
|
+
text("p", "Drag the title bar to move this dialog. Use the handle in the bottom-right corner to resize it."),
|
|
83
|
+
{ tag: "div", attr: { class: "dialog-summary" }, children: [
|
|
84
|
+
text("span", "Workspace", "dialog-summary-label"), text("strong", "Product design"),
|
|
85
|
+
text("span", "Visibility", "dialog-summary-label"), text("span", "Private"),
|
|
86
|
+
text("span", "Members", "dialog-summary-label"), text("span", "18 people")
|
|
87
|
+
] },
|
|
88
|
+
text("p", modal ? "The background stays still while you work." : "This is a non-modal dialog. You can still use the showcase and change its theme.", "dialog-help")
|
|
89
|
+
] },
|
|
90
|
+
footer: actions(DialogComponent.Button({ content: "Done" })),
|
|
91
|
+
width: "36rem", height: "15rem", showCloseButton: true,
|
|
92
|
+
allowMove: true, allowResize: true, modal,
|
|
93
|
+
resizeOptions: { minWidth: 280, minHeight: 240, maxWidth: 960, maxHeight: 760 }
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const nestedStatus = message("Open a second dialog from the first.");
|
|
98
|
+
function showNested() {
|
|
99
|
+
DialogComponent.show({
|
|
100
|
+
header: "Manage workspace",
|
|
101
|
+
content: { tag: "div", attr: { class: "showcase-dialog-copy" }, children: [
|
|
102
|
+
text("p", "Review a permission before making changes. Closing the second dialog returns you here."),
|
|
103
|
+
new ButtonComponent({ content: "Review permissions", outlined: true, type: "button", class: "dialog-open-nested", click: () => {
|
|
104
|
+
DialogComponent.show({
|
|
105
|
+
header: "Confirm access",
|
|
106
|
+
content: "Allow team members to invite others to this workspace?",
|
|
107
|
+
footer: actions(DialogComponent.Cancel(), DialogComponent.Ok(() => { nestedStatus.content = "Permission confirmed."; })),
|
|
108
|
+
width: "27rem", showCloseButton: true
|
|
109
|
+
});
|
|
110
|
+
} })
|
|
111
|
+
] },
|
|
112
|
+
footer: actions(DialogComponent.Cancel(), DialogComponent.Button({ content: "Done" })),
|
|
113
|
+
width: "30rem", showCloseButton: true
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const asyncStatus = message("Try saving, or simulate a failed save.");
|
|
118
|
+
function showAsync() {
|
|
119
|
+
const notice = message("Your changes are ready to save.");
|
|
120
|
+
DialogComponent.show({
|
|
121
|
+
header: "Save workspace",
|
|
122
|
+
content: { tag: "div", attr: { class: "showcase-dialog-copy" }, children: [
|
|
123
|
+
text("p", "The save action waits for the result before closing. A failed save keeps the dialog open so you can try again."),
|
|
124
|
+
{ tag: "label", attr: { class: "dialog-check" }, children: [
|
|
125
|
+
{ tag: "input", attr: { id: "showcase-failed-save", type: "checkbox" } }, text("span", "Simulate a failed save")
|
|
126
|
+
] }, notice
|
|
127
|
+
] },
|
|
128
|
+
footer: actions(DialogComponent.Cancel(), DialogComponent.Button({
|
|
129
|
+
content: "Save changes", class: "dlg-ok-btn", onClick: async () => {
|
|
130
|
+
const fail = (document.getElementById("showcase-failed-save") as HTMLInputElement).checked;
|
|
131
|
+
notice.content = "Saving changes...";
|
|
132
|
+
await new Promise(resolve => setTimeout(resolve, 750));
|
|
133
|
+
if (fail) {
|
|
134
|
+
notice.content = "Could not save. Uncheck the option and try again.";
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
asyncStatus.content = "Workspace saved successfully.";
|
|
138
|
+
}
|
|
139
|
+
})),
|
|
140
|
+
showCloseButton: true, closeOnOverlayClick: false
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function card(id: string, number: string, title: string, description: string, children: Array<IJSLVNode | ButtonComponent>): IJSLVNode {
|
|
145
|
+
return { tag: "article", attr: { class: "sample-card", id, "aria-labelledby": id + "-title" }, children: [
|
|
146
|
+
{ tag: "div", attr: { class: "card-heading" }, children: [text("span", number, "sample-number"), { tag: "h3", content: title, attr: { id: id + "-title" } }] },
|
|
147
|
+
text("p", description, "card-description"), ...children
|
|
148
|
+
] };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const dialogShowcase: IJSLVNode = {
|
|
152
|
+
tag: "section", attr: { id: "dialogs", class: "component-section", "aria-labelledby": "dialogs-title" }, children: [
|
|
153
|
+
{ tag: "div", attr: { class: "hero" }, children: [
|
|
154
|
+
{ tag: "div", attr: { class: "hero-copy" }, children: [
|
|
155
|
+
text("p", "THE ESSENTIALS / 02", "eyebrow"),
|
|
156
|
+
{ tag: "h2", content: "Dialogs", attr: { id: "dialogs-title", class: "section-title" } },
|
|
157
|
+
text("p", "A little focus for the task at hand. Clear content, accessible controls, and room to work.", "hero-description")
|
|
158
|
+
] },
|
|
159
|
+
{ tag: "div", attr: { class: "hero-preview" }, children: [
|
|
160
|
+
text("span", "INTERACTIVE PREVIEW", "preview-caption"),
|
|
161
|
+
new ButtonComponent({ content: "Open a dialog", size: "large", type: "button", class: "dialog-launch-hero", click: showConfirmation }),
|
|
162
|
+
text("span", "Try it in any theme.", "preview-note")
|
|
163
|
+
] }
|
|
164
|
+
] },
|
|
165
|
+
{ tag: "div", attr: { class: "overview-bar" }, children: [text("span", "Overview", "overview-label"), text("span", "Responsive layout", "overview-stat"), text("span", "Keyboard accessible", "overview-stat")] },
|
|
166
|
+
{ tag: "div", attr: { class: "sample-grid dialog-sample-grid" }, children: [
|
|
167
|
+
card("dialogs-basic", "01", "Focused decisions", "A title, a short explanation, and a clear next step.", [launch("Open confirmation", "dialog-launch-basic", showConfirmation), confirmationStatus]),
|
|
168
|
+
card("dialogs-form", "02", "Collect a little more", "Autofocus, native validation, and Enter to confirm. Textareas keep their line breaks.", [launch("Open project form", "dialog-launch-form", showForm), formStatus]),
|
|
169
|
+
card("dialogs-scroll", "03", "Room for more", "Long content scrolls within the dialog while the title and actions stay visible.", [launch("Open long content", "dialog-launch-scroll", showLongContent)]),
|
|
170
|
+
card("dialogs-resize", "04", "Make room", "Move and resize a dialog, or keep the page interactive with a non-modal version.", [
|
|
171
|
+
{ tag: "div", attr: { class: "sample-row" }, children: [launch("Move & resize", "dialog-launch-resize", () => showWorkspace()), launch("Non-modal", "dialog-launch-nonmodal", () => showWorkspace(false))] }
|
|
172
|
+
]),
|
|
173
|
+
card("dialogs-nested", "05", "One decision at a time", "Nested dialogs keep keyboard focus and Escape scoped to the active dialog.", [launch("Open nested dialogs", "dialog-launch-nested", showNested), nestedStatus]),
|
|
174
|
+
card("dialogs-async", "06", "Wait for the result", "Async actions prevent duplicate submissions. Return false to keep the dialog open.", [launch("Try async save", "dialog-launch-async", showAsync), asyncStatus])
|
|
175
|
+
] },
|
|
176
|
+
{ tag: "details", attr: { class: "usage" }, children: [
|
|
177
|
+
text("summary", "Dialog usage & compatibility"),
|
|
178
|
+
text("pre", 'DialogComponent.show({\n header: "Save your changes?",\n content: "Your workspace has been updated.",\n footer: { tag: "div", children: [\n DialogComponent.Cancel(),\n DialogComponent.Ok(async () => {\n // Await your save. Return false to keep the dialog open.\n })\n ] },\n showCloseButton: true\n});', "sample-code"),
|
|
179
|
+
text("p", "Existing options, getRef().open()/close(), and Button/Ok/Cancel helpers are preserved. Width defaults to 32rem and is constrained to the viewport. Height options still size the content area. Empty footers collapse without reserving space."),
|
|
180
|
+
text("p", "Modal dialogs contain Tab navigation, prevent background scrolling, and restore focus to the opener. Non-modal dialogs leave the background interactive. Escape closes only the top dialog. onClose returning false cancels every close route."),
|
|
181
|
+
text("p", "The close control is a native button. Use closeButtonLabel to translate its accessible label, or ariaLabel when the header does not provide a suitable dialog name. Dragging and resizing are optional and retain moveOptions and resizeOptions callbacks.")
|
|
182
|
+
] }
|
|
183
|
+
]
|
|
184
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { IJSLComponent, IJSLVNode, JSLRender } from "jsl-render";
|
|
2
|
+
import { ButtonComponent } from "../../components/button/ButtonComponent";
|
|
3
|
+
import { Notification, NotificationComponent, INotificationOptions } from "../../components/notification/Notification";
|
|
4
|
+
import { LoadingComponent } from "../../components/loading/LoadingComponent";
|
|
5
|
+
import { NavigationComponent } from "../../components/navigation/NavigationComponent";
|
|
6
|
+
|
|
7
|
+
type Content = IJSLVNode | IJSLComponent;
|
|
8
|
+
const text = (tag: string, content: string, className?: string): IJSLVNode => ({ tag, content, attr: { class: className } });
|
|
9
|
+
const group = (children: Content[], className?: string): IJSLVNode => ({ tag: "div", attr: { class: className }, children });
|
|
10
|
+
const status = (content: string): IJSLVNode => ({ tag: "p", content, attr: { class: "sample-status", role: "status" } });
|
|
11
|
+
const action = (content: string, click: () => void) => new ButtonComponent({ content, click, type: "button", outlined: true, size: "small" });
|
|
12
|
+
function card(id: string, number: string, title: string, description: string, children: Content[]): IJSLVNode {
|
|
13
|
+
return { tag: "article", attr: { id, class: "sample-card" }, children: [
|
|
14
|
+
group([text("span", number, "sample-number"), text("h3", title)], "card-heading"),
|
|
15
|
+
text("p", description, "card-description"), ...children
|
|
16
|
+
] };
|
|
17
|
+
}
|
|
18
|
+
function section(id: string, number: string, title: string, description: string, preview: Content, cards: Content[], usage: Content[]): IJSLVNode {
|
|
19
|
+
return { tag: "section", attr: { id, class: "component-section", "aria-labelledby": id + "-title" }, children: [
|
|
20
|
+
group([
|
|
21
|
+
group([text("p", "THE ESSENTIALS / " + number, "eyebrow"),
|
|
22
|
+
{ tag: "h2", content: title, attr: { id: id + "-title", class: "section-title" } },
|
|
23
|
+
text("p", description, "hero-description")], "hero-copy"),
|
|
24
|
+
group([text("span", "INTERACTIVE PREVIEW", "preview-caption"), preview], "hero-preview feedback-preview")
|
|
25
|
+
], "hero"),
|
|
26
|
+
group(cards, "sample-grid feedback-sample-grid"),
|
|
27
|
+
{ tag: "details", attr: { class: "usage" }, children: [text("summary", title + " usage & compatibility"), ...usage] }
|
|
28
|
+
] };
|
|
29
|
+
}
|
|
30
|
+
const messageStatus = status("Notifications appear in the top-right corner.");
|
|
31
|
+
let latestMessage: string;
|
|
32
|
+
const notify = (options: INotificationOptions) => {
|
|
33
|
+
latestMessage = Notification.show({ ...options, onClose: () => {
|
|
34
|
+
messageStatus.content = "Notification dismissed.";
|
|
35
|
+
options.onClose?.();
|
|
36
|
+
JSLRender.lastCreatedRenderer?.refresh();
|
|
37
|
+
} });
|
|
38
|
+
messageStatus.content = "Notification shown.";
|
|
39
|
+
};
|
|
40
|
+
const variants: INotificationOptions["variant"][] = ["info", "success", "warning", "error"];
|
|
41
|
+
const messages = ["Your export is ready to download.", "All changes have been saved.", "Your session will expire soon.", "We couldn't save your changes. Please try again."];
|
|
42
|
+
export const notificationShowcase = section("notifications", "14", "Notifications",
|
|
43
|
+
"A little feedback, clearly delivered. Quiet surfaces with a distinct tone for each message.",
|
|
44
|
+
new NotificationComponent({ title: "Changes saved", content: "Your team is up to date.", variant: "success", closeOnClick: false, showCloseButton: false }), [
|
|
45
|
+
card("notification-tones", "01", "Feedback with a purpose", "Color supports the message; titles and text explain what happened. Try each live notification.", [
|
|
46
|
+
group(variants.map((variant, index) => new NotificationComponent({
|
|
47
|
+
title: ["For your information", "All done", "A quick heads-up", "Something went wrong"][index],
|
|
48
|
+
content: messages[index], variant, closeOnClick: false, showCloseButton: false
|
|
49
|
+
})), "notification-samples"),
|
|
50
|
+
group(variants.map((variant, index) => action("Show " + variant, () => notify({ content: messages[index], variant }))), "sample-row")
|
|
51
|
+
]),
|
|
52
|
+
card("notification-actions", "02", "A useful next step", "Keep a rich message open while its controls are used, or close a simple message with a click.", [
|
|
53
|
+
group([
|
|
54
|
+
action("Show message", () => notify({ title: "A new update", content: "Click this message or its close button to dismiss it." })),
|
|
55
|
+
action("Show action", () => {
|
|
56
|
+
const id = Notification.show({
|
|
57
|
+
title: "Item archived", closeOnClick: false, content: group([
|
|
58
|
+
text("p", "You can restore the item if you change your mind."),
|
|
59
|
+
action("Undo archive", () => { Notification.clearNotification(id); messageStatus.content = "Item restored."; JSLRender.lastCreatedRenderer?.refresh(); })
|
|
60
|
+
], "notification-action-copy")
|
|
61
|
+
});
|
|
62
|
+
latestMessage = id;
|
|
63
|
+
}),
|
|
64
|
+
action("Dismiss latest", () => Notification.clearNotification(latestMessage)),
|
|
65
|
+
action("Dismiss all", () => Notification.clearAll())
|
|
66
|
+
], "sample-row"), messageStatus,
|
|
67
|
+
text("pre", 'const id = Notification.show({\n title: "Changes saved", content: "Your team is up to date.",\n variant: "success", closeOnClick: false\n});\nNotification.clearNotification(id);', "sample-code")
|
|
68
|
+
]),
|
|
69
|
+
card("notification-timing", "03", "Time to read", "A countdown bar shows the remaining time. Both the bar and timer pause while hovered or focused. Persistent messages stay until dismissed.", [
|
|
70
|
+
group([
|
|
71
|
+
action("Show for 3 seconds", () => notify({ title: "Export complete", content: "Hover or focus this message to pause its timer.", timeOut: 3000, variant: "info" })),
|
|
72
|
+
action("Show persistent message", () => notify({ title: "Review needed", content: "This message stays open until you dismiss it.", closeOnClick: false, variant: "warning" }))
|
|
73
|
+
], "sample-row")
|
|
74
|
+
])
|
|
75
|
+
], [
|
|
76
|
+
text("p", "Notification.show(), clearNotification(), clearAll(), content, timeOut, closeOnClick, class, style, and onClick remain available. Messages still close when clicked by default. timeOut defaults to zero (persistent)."),
|
|
77
|
+
text("p", "Add title, variant, showCloseButton, closeButtonLabel, or onClose. NotificationComponent uses the same surface inline. Informational messages use role=status; warning and error messages use role=alert. Motion follows the user's reduced-motion preference.")
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
class LoadingDemo implements IJSLComponent {
|
|
81
|
+
public State = { busy: false, complete: false };
|
|
82
|
+
private renderer: JSLRender;
|
|
83
|
+
private timer: ReturnType<typeof setTimeout>;
|
|
84
|
+
public onInit(renderer: JSLRender) { this.renderer = renderer; }
|
|
85
|
+
public onRemove() { clearTimeout(this.timer); }
|
|
86
|
+
public render(): IJSLVNode {
|
|
87
|
+
return group([
|
|
88
|
+
new ButtonComponent({ content: this.State.busy ? "Loading report…" : "Load report", enabled: !this.State.busy, type: "button", click: () => {
|
|
89
|
+
this.State.busy = true; this.State.complete = false;
|
|
90
|
+
this.timer = setTimeout(() => { this.State.busy = false; this.State.complete = true; this.renderer.refresh(); }, 1800);
|
|
91
|
+
} }),
|
|
92
|
+
{ tag: "div", attr: { class: "loading-demo-panel", "aria-busy": String(this.State.busy) }, children: [
|
|
93
|
+
this.State.busy ? new LoadingComponent({ sizeInRem: 1.25, label: "Fetching your report…", showLabel: true }) :
|
|
94
|
+
text("p", this.State.complete ? "Your report is ready. 24 projects, all up to date." : "Load a report to try a brief loading state.")
|
|
95
|
+
] }
|
|
96
|
+
], "loading-demo");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export const loadingShowcase = section("loading", "15", "Loading",
|
|
100
|
+
"A small sign that work is happening. Theme-aware indicators with room for helpful context.",
|
|
101
|
+
new LoadingComponent({ sizeInRem: 1.5, label: "Getting things ready…", showLabel: true }), [
|
|
102
|
+
card("loading-sizes", "01", "From inline to centre stage", "The original numeric constructor still sets the ring's size in rem.", [
|
|
103
|
+
group([1, 2, 3].map(size => group([new LoadingComponent(size), text("span", size + " rem", "size-caption")], "size-sample")), "sample-row size-row"),
|
|
104
|
+
text("pre", 'new LoadingComponent(1);\nnew LoadingComponent(3);', "sample-code")
|
|
105
|
+
]),
|
|
106
|
+
card("loading-labels", "02", "Say what's happening", "Provide a useful label when a task takes time. Labels remain accessible when visually hidden.", [
|
|
107
|
+
group([
|
|
108
|
+
new LoadingComponent({ sizeInRem: 1, label: "Saving changes…", showLabel: true }),
|
|
109
|
+
new LoadingComponent({ sizeInRem: 1.5, label: "Syncing files…", showLabel: true, style: "--jsl-loading-color:var(--jsl-color-success)" })
|
|
110
|
+
], "loading-label-samples"),
|
|
111
|
+
text("pre", '{ sizeInRem: 1, label: "Saving changes…", showLabel: true }', "sample-code")
|
|
112
|
+
]),
|
|
113
|
+
card("loading-workflow", "03", "A moment between states", "Disable a repeated action while it runs, and mark the affected region as busy.", [new LoadingDemo()])
|
|
114
|
+
], [
|
|
115
|
+
text("p", "The numeric constructor and its original 6 rem default remain available. An options object adds sizeInRem, label, showLabel, class, and style. The ring uses the current theme's accent; override --jsl-loading-color for a particular context."),
|
|
116
|
+
text("p", "Each indicator has an accessible status label. Use aria-busy on the region being updated. Reduced-motion mode keeps the ring visible without rotating it.")
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const navStatus = status("Choose a destination or an action.");
|
|
120
|
+
const selected = (name: string) => () => { navStatus.content = "Selected: " + name + "."; };
|
|
121
|
+
const wideNavigation = new NavigationComponent({
|
|
122
|
+
title: "Workspace", position: "default", alignment: "right", ariaLabel: "Workspace example",
|
|
123
|
+
data: [
|
|
124
|
+
{ content: "Overview", link: "#navigation", click: selected("Overview") },
|
|
125
|
+
{ content: "Projects", link: "#navigation/projects", click: selected("Projects"), children: [
|
|
126
|
+
{ content: "All projects", link: "#navigation/projects/all", click: selected("All projects") },
|
|
127
|
+
{ content: "Create project", click: selected("Create project") },
|
|
128
|
+
{ content: "Templates", children: [
|
|
129
|
+
{ content: "Design review", click: selected("Design review") },
|
|
130
|
+
{ content: "Sprint planning", click: selected("Sprint planning") }
|
|
131
|
+
] }
|
|
132
|
+
] },
|
|
133
|
+
{ content: "Resources", children: [
|
|
134
|
+
{ content: "Documentation", link: "https://example.com", target: "_blank" },
|
|
135
|
+
{ content: "What's new", click: selected("What's new") }
|
|
136
|
+
] },
|
|
137
|
+
{ content: "Admin", enabled: false, click: selected("Admin") }
|
|
138
|
+
]
|
|
139
|
+
});
|
|
140
|
+
const compactNavigation = new NavigationComponent({ title: "Team space", position: "static", ariaLabel: "Compact example", data: [
|
|
141
|
+
{ content: "Activity", active: true, click: selected("Activity") },
|
|
142
|
+
{ content: "People", click: selected("People") },
|
|
143
|
+
{ content: "Settings", children: [
|
|
144
|
+
{ content: "Profile", click: selected("Profile") },
|
|
145
|
+
{ content: "Notifications", click: selected("Notification settings") }
|
|
146
|
+
] }
|
|
147
|
+
] });
|
|
148
|
+
export const navigationShowcase = section("navigation", "16", "Navigation",
|
|
149
|
+
"A clear way through. Familiar links, purposeful actions, and menus that work at any size.",
|
|
150
|
+
group([new NavigationComponent({ title: "Studio", position: "default", ariaLabel: "Navigation preview", data: [
|
|
151
|
+
{ content: "Projects", active: true, click: selected("Projects") },
|
|
152
|
+
{ content: "Team", click: selected("Team") }
|
|
153
|
+
] })], "navigation-preview"), [
|
|
154
|
+
card("navigation-bar", "01", "A place for everything", "Follow a link, open a submenu, or run an action. Project links and their adjacent submenu buttons work independently.", [
|
|
155
|
+
wideNavigation, navStatus,
|
|
156
|
+
text("p", "Try Tab, Enter or Space, arrow keys, and Escape. Click outside a menu to close it.", "inline-note")
|
|
157
|
+
]),
|
|
158
|
+
card("navigation-compact", "02", "Comfortable in a smaller space", "Navigation adapts to its container as well as the viewport. Open the menu to explore its destinations.", [
|
|
159
|
+
group([compactNavigation], "navigation-compact-sample"),
|
|
160
|
+
text("pre", '{ title: "Team space", position: "static", data }', "sample-code")
|
|
161
|
+
]),
|
|
162
|
+
card("navigation-markup", "03", "Your content, your destinations", "A brand and each item can contain a VNode. Native anchors preserve standard browser behavior, including opening links in another tab.", [
|
|
163
|
+
new NavigationComponent({
|
|
164
|
+
title: group([text("span", "S", "navigation-brand-mark"), text("span", "Studio")], "navigation-rich-brand"),
|
|
165
|
+
titleLink: "#navigation", position: "default", ariaLabel: "Rich content example",
|
|
166
|
+
data: [{ content: "Home", link: "#navigation" }, { content: "Help", click: selected("Help") }]
|
|
167
|
+
})
|
|
168
|
+
])
|
|
169
|
+
], [
|
|
170
|
+
text("pre", 'new NavigationComponent({\n title: "Workspace", position: "default", alignment: "right",\n data: [\n { content: "Overview", link: "#overview" },\n { content: "Projects", children: [\n { content: "Create project", click: () => createProject() }\n ] }\n ]\n});', "sample-code"),
|
|
171
|
+
text("p", "The existing title, titleLink, data, link, target, click, alignment, position, class, style, and id options remain. New options include ariaLabel, menuButtonLabel, activeLink, and item active, enabled, submenuLabel, and nested children."),
|
|
172
|
+
text("p", "Navigation is rebuilt without Luxbar. Custom CSS that targets old Luxbar classes must be updated. id now identifies the header; menu and submenu IDs are derived from it (or generated uniquely). Items without links use buttons, and the title is plain text unless titleLink is provided."),
|
|
173
|
+
text("p", "default and static participate in normal document flow. fixed (the original default) and fixed-bottom attach to the viewport. Active links follow hashchange and popstate, with path boundaries respected; activeLink or item.active can override route detection. Menus close when focus leaves, on outside interaction, on selection, or with Escape.")
|
|
174
|
+
]);
|