jsl-ui 9.28.7 → 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.
Files changed (166) hide show
  1. package/docs/AutoSuggestComponent.md +17 -0
  2. package/docs/ButtonComponent.md +14 -0
  3. package/docs/CheckBoxComponent.md +12 -0
  4. package/docs/DateComponent.md +12 -0
  5. package/docs/DateRangeComponent.md +13 -0
  6. package/docs/DateTimeComponent.md +13 -0
  7. package/docs/DialogComponent.md +15 -0
  8. package/docs/ExpanderComponent.md +13 -0
  9. package/docs/FormComponent.md +15 -0
  10. package/docs/FormFieldComponent.md +15 -0
  11. package/docs/GridComponent.md +19 -0
  12. package/docs/InputComponent.md +15 -0
  13. package/docs/IntervalComponent.md +13 -0
  14. package/docs/LoadingComponent.md +12 -0
  15. package/docs/NavigationComponent.md +17 -0
  16. package/docs/Notification.md +14 -0
  17. package/docs/NotificationComponent.md +13 -0
  18. package/docs/NumberComponent.md +12 -0
  19. package/docs/OverlayComponent.md +12 -0
  20. package/docs/ProgressComponent.md +12 -0
  21. package/docs/README.md +24 -0
  22. package/docs/RibbonMenuComponent.md +14 -0
  23. package/docs/SelectComponent.md +15 -0
  24. package/docs/SplitterComponent.md +14 -0
  25. package/docs/StackPanelComponent.md +13 -0
  26. package/docs/TabsComponent.md +16 -0
  27. package/docs/TimeComponent.md +13 -0
  28. package/docs/TreeViewComponent.md +16 -0
  29. package/examples/showcase/checks-and-expanders.ts +163 -0
  30. package/examples/showcase/cultures.ts +39 -0
  31. package/examples/showcase/dates.ts +182 -0
  32. package/examples/showcase/dialogs.ts +184 -0
  33. package/examples/showcase/feedback-and-navigation.ts +174 -0
  34. package/examples/showcase/fields.ts +178 -0
  35. package/examples/showcase/fonts/Jost-Variable.ttf +0 -0
  36. package/examples/showcase/fonts/OFL-Jost.txt +93 -0
  37. package/examples/showcase/grids.ts +150 -0
  38. package/examples/showcase/index.html +60 -0
  39. package/examples/showcase/layout-and-navigation.ts +47 -0
  40. package/examples/showcase/nivona.css +98 -0
  41. package/examples/showcase/selects.ts +158 -0
  42. package/examples/showcase/showcase.css +231 -0
  43. package/examples/showcase/showcase.ts +214 -0
  44. package/examples/showcase/tabs.ts +161 -0
  45. package/examples/showcase/themes.css +83 -0
  46. package/examples/showcase/themes.ts +54 -0
  47. package/lib/components/IntervalComponent.d.ts +5 -0
  48. package/lib/components/IntervalComponent.js +29 -13
  49. package/lib/components/auto-suggest/AutoSuggestComponent.d.ts +11 -9
  50. package/lib/components/auto-suggest/AutoSuggestComponent.js +25 -38
  51. package/lib/components/button/ButtonComponent.d.ts +30 -1
  52. package/lib/components/button/ButtonComponent.js +85 -4
  53. package/lib/components/checkbox/CheckBoxComponent.d.ts +11 -1
  54. package/lib/components/checkbox/CheckBoxComponent.js +66 -56
  55. package/lib/components/checkbox/CheckBoxStyle.scss +72 -85
  56. package/lib/components/date/DateComponent.d.ts +10 -0
  57. package/lib/components/date/DateComponent.js +92 -44
  58. package/lib/components/date-range/DateRangeComponent.d.ts +14 -3
  59. package/lib/components/date-range/DateRangeComponent.js +96 -86
  60. package/lib/components/date-range/DateRangeStyle.scss +35 -11
  61. package/lib/components/date-time/DateTimeComponent.d.ts +8 -0
  62. package/lib/components/date-time/DateTimeComponent.js +64 -23
  63. package/lib/components/date-time/DateTimeStyle.scss +10 -7
  64. package/lib/components/dialog/DialogComponent.d.ts +21 -7
  65. package/lib/components/dialog/DialogComponent.js +179 -122
  66. package/lib/components/expander/ExpanderComponent.d.ts +15 -1
  67. package/lib/components/expander/ExpanderComponent.js +85 -64
  68. package/lib/components/expander/ExpanderStyle.scss +61 -47
  69. package/lib/components/form-field/FormFieldComponent.d.ts +45 -15
  70. package/lib/components/form-field/FormFieldComponent.js +249 -96
  71. package/lib/components/form-field/FormFieldStyle.scss +36 -87
  72. package/lib/components/form-field/create-control.d.ts +7 -0
  73. package/lib/components/form-field/create-control.js +48 -0
  74. package/lib/components/input/InputComponent.d.ts +18 -1
  75. package/lib/components/input/InputComponent.js +68 -93
  76. package/lib/components/loading/LoadingComponent.d.ts +9 -2
  77. package/lib/components/loading/LoadingComponent.js +13 -12
  78. package/lib/components/loading/LoadingStyle.css +19 -29
  79. package/lib/components/navigation/NavigationComponent.d.ts +34 -10
  80. package/lib/components/navigation/NavigationComponent.js +261 -33
  81. package/lib/components/navigation/NavigationStyle.scss +62 -226
  82. package/lib/components/notification/Notification.d.ts +37 -4
  83. package/lib/components/notification/Notification.js +160 -41
  84. package/lib/components/notification/Notification.scss +103 -19
  85. package/lib/components/number/NumberComponent.d.ts +16 -2
  86. package/lib/components/number/NumberComponent.js +54 -53
  87. package/lib/components/overlay/OverlayComponent.d.ts +13 -7
  88. package/lib/components/overlay/OverlayComponent.js +46 -114
  89. package/lib/components/overlay/overlay.css +18 -57
  90. package/lib/components/progress/ProgressComponent.d.ts +1 -0
  91. package/lib/components/progress/ProgressComponent.js +6 -6
  92. package/lib/components/progress/ProgressStyle.scss +5 -15
  93. package/lib/components/ribbon-menu/RibbonMenuComponent.js +2 -2
  94. package/lib/components/ribbon-menu/RibbonMenuStyle.scss +6 -76
  95. package/lib/components/select/SelectComponent.d.ts +33 -3
  96. package/lib/components/select/SelectComponent.js +312 -278
  97. package/lib/components/select/SelectDDAreaComponent.d.ts +13 -7
  98. package/lib/components/select/SelectDDAreaComponent.js +215 -256
  99. package/lib/components/select/SelectStyle.scss +204 -186
  100. package/lib/components/slider/nouislider.css +40 -54
  101. package/lib/components/stack-panel/SplitterComponent.d.ts +19 -3
  102. package/lib/components/stack-panel/SplitterComponent.js +94 -111
  103. package/lib/components/stack-panel/SplitterStyle.scss +9 -45
  104. package/lib/components/stack-panel/StackPanelComponent.js +3 -3
  105. package/lib/components/stack-panel/StackPanelStyle.scss +10 -52
  106. package/lib/components/tabs/TabsComponent.d.ts +17 -1
  107. package/lib/components/tabs/TabsComponent.js +247 -123
  108. package/lib/components/tabs/TabsStyle.scss +138 -75
  109. package/lib/components/time/TimeComponent.d.ts +5 -0
  110. package/lib/components/time/TimeComponent.js +61 -69
  111. package/lib/components/time/TimeComponent.scss +4 -4
  112. package/lib/components/tree-view/TreeViewComponent.d.ts +10 -4
  113. package/lib/components/tree-view/TreeViewComponent.js +65 -32
  114. package/lib/components/tree-view/TreeViewStyle.scss +12 -96
  115. package/lib/core/date.d.ts +2 -0
  116. package/lib/core/date.js +6 -0
  117. package/lib/core/jq.js +11 -8
  118. package/lib/grid/slick-default-theme.css +107 -31
  119. package/lib/grid/slick.creator.js +23 -5
  120. package/lib/grid/slick.formatters.js +4 -3
  121. package/lib/grid/slick.grid.css +38 -26
  122. package/lib/grid/slick.grid.d.ts +1 -0
  123. package/lib/grid/slick.grid.js +2 -3
  124. package/lib/grid.css +3413 -454
  125. package/lib/index.css +2500 -1031
  126. package/lib/index.css.map +1 -1
  127. package/lib/index.d.ts +1 -0
  128. package/lib/index.js +1 -1
  129. package/lib/index.js.map +1 -1
  130. package/lib/interaction/moveable.d.ts +1 -0
  131. package/lib/interaction/moveable.js +8 -0
  132. package/lib/interaction/resizable.d.ts +1 -0
  133. package/lib/interaction/resizable.js +9 -0
  134. package/lib/ui/auto.suggest.css +31 -14
  135. package/lib/ui/auto.suggest.d.ts +1 -0
  136. package/lib/ui/auto.suggest.js +88 -18
  137. package/lib/ui/base.css +2 -48
  138. package/lib/ui/button.css +134 -0
  139. package/lib/ui/date-input.css +11 -0
  140. package/lib/ui/date.picker/css/tiny-date-picker.css +124 -325
  141. package/lib/ui/date.picker/date-picker-options.js +4 -3
  142. package/lib/ui/date.picker/lib/date-manip.js +9 -0
  143. package/lib/ui/date.picker/mode/base-mode.d.ts +2 -1
  144. package/lib/ui/date.picker/mode/base-mode.js +112 -22
  145. package/lib/ui/date.picker/mode/dropdown-mode.d.ts +2 -4
  146. package/lib/ui/date.picker/mode/dropdown-mode.js +29 -42
  147. package/lib/ui/date.picker/mode/index.d.ts +2 -1
  148. package/lib/ui/date.picker/mode/modal-mode.d.ts +2 -1
  149. package/lib/ui/date.picker/mode/modal-mode.js +4 -2
  150. package/lib/ui/date.picker/mode/permanent-mode.d.ts +2 -1
  151. package/lib/ui/date.picker/views/day-picker.js +24 -15
  152. package/lib/ui/date.picker/views/month-picker.js +1 -1
  153. package/lib/ui/date.picker/views/year-picker.js +3 -2
  154. package/lib/ui/date.picker.d.ts +2 -1
  155. package/lib/ui/dialog.css +144 -108
  156. package/lib/ui/dialog.d.ts +19 -1
  157. package/lib/ui/dialog.js +178 -60
  158. package/lib/ui/input.css +35 -0
  159. package/lib/ui/number.input.js +50 -37
  160. package/lib/ui/theme.css +28 -0
  161. package/package.json +12 -8
  162. package/lib/components/drop-down/DropDownComponent.d.ts +0 -27
  163. package/lib/components/drop-down/DropDownComponent.js +0 -60
  164. package/lib/components/expander/OldExpanderStyle.scss +0 -37
  165. package/lib/ui/drop.down.d.ts +0 -52
  166. package/lib/ui/drop.down.js +0 -2112
@@ -0,0 +1,214 @@
1
+ import "../../ui/base.css";
2
+ import "./cultures";
3
+ import { gridShowcase } from "./grids";
4
+ import "./themes";
5
+ import { dialogShowcase } from "./dialogs";
6
+ import { tabsShowcase } from "./tabs";
7
+ import { selectShowcase } from "./selects";
8
+ import { checkboxShowcase, expanderShowcase } from "./checks-and-expanders";
9
+ import { inputShowcase, numberShowcase, formFieldShowcase } from "./fields";
10
+ import { notificationShowcase, loadingShowcase, navigationShowcase } from "./feedback-and-navigation";
11
+ import { progressShowcase, stackShowcase, treeShowcase, overlayShowcase, ribbonShowcase } from "./layout-and-navigation";
12
+ import { sliderShowcase, dateShowcase, dateRangeShowcase, dateTimeShowcase } from "./dates";
13
+ import { IJSLComponent, IJSLVNode, JSLRender } from "jsl-render";
14
+ import { ButtonComponent, IButtonOptions } from "../../components/button/ButtonComponent";
15
+
16
+ // Add each redesigned component as a section below, with its own navigation entry.
17
+ type Content = IJSLVNode | IJSLComponent;
18
+ const node = (tag: string, content: string, className?: string): IJSLVNode => ({
19
+ tag, content, attr: { class: className }
20
+ });
21
+ const group = (children: Content[], className = "sample-row"): IJSLVNode => ({
22
+ tag: "div", attr: { class: className }, children
23
+ });
24
+ const button = (options: IButtonOptions) => new ButtonComponent({ type: "button", ...options });
25
+
26
+ // Self-contained sample icons. Applications can supply their own VNodes, images,
27
+ // icon components, or plain strings; ButtonComponent has no icon-library dependency.
28
+ const paths = {
29
+ plus: '<path d="M12 5v14M5 12h14"/>',
30
+ arrow: '<path d="M5 12h14m-6-6 6 6-6 6"/>',
31
+ download: '<path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/>',
32
+ refresh: '<path d="M20 7v5h-5M4 17v-5h5M6 7a7 7 0 0 1 12-1l2 6M4 12l2 6a7 7 0 0 0 12-1"/>',
33
+ more: '<circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/>',
34
+ bookmark: '<path d="M6 3h12v18l-6-4-6 4z"/>',
35
+ check: '<path d="m5 12 4 4L19 6"/>'
36
+ };
37
+ function icon(name: keyof typeof paths): IJSLVNode {
38
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' + paths[name] + '</svg>';
39
+ const url = 'url("data:image/svg+xml,' + encodeURIComponent(svg) + '")';
40
+ return { tag: "span", attr: { class: "showcase-icon", style: "mask-image:" + url + ";-webkit-mask-image:" + url } };
41
+ }
42
+
43
+ function card(id: string, number: string, title: string, description: string, children: Content[]): IJSLVNode {
44
+ return {
45
+ tag: "article", attr: { class: "sample-card", id, "aria-labelledby": id + "-title" },
46
+ children: [
47
+ group([node("span", number, "sample-number"), { tag: "h2", content: title, attr: { id: id + "-title" } }], "card-heading"),
48
+ node("p", description, "card-description"),
49
+ ...children
50
+ ]
51
+ };
52
+ }
53
+
54
+ const actionStatus = node("p", "Choose an action to try it.", "sample-status");
55
+ actionStatus.attr.role = "status";
56
+ const action = (text: string) => () => { actionStatus.content = text; };
57
+
58
+ const toggleStatus = node("p", "No formatting selected.", "sample-status");
59
+ toggleStatus.attr.role = "status";
60
+ const formatting: ButtonComponent[] = [];
61
+ const labels = ["Bold", "Italic", "Underline"];
62
+ const updateFormatting = () => {
63
+ const selected = labels.filter((_, index) => formatting[index].pressed);
64
+ toggleStatus.content = selected.length ? "Selected: " + selected.join(", ") + "." : "No formatting selected.";
65
+ };
66
+ labels.forEach((label, index) => formatting.push(button({
67
+ content: label,
68
+ icon: { tag: "span", content: label[0], attr: { class: ["format-bold", "format-italic", "format-underline"][index] } },
69
+ iconOnly: true, outlined: true, title: label, toggle: true, onToggle: updateFormatting
70
+ })));
71
+
72
+ const bookmarkStatus = node("p", "Save this view for later.", "sample-status");
73
+ bookmarkStatus.attr.role = "status";
74
+ const bookmark = button({
75
+ content: "Save view", icon: icon("bookmark"), variant: "outline", toggle: true,
76
+ onToggle: pressed => { bookmarkStatus.content = pressed ? "View saved. Click again to remove it." : "Save this view for later."; }
77
+ });
78
+
79
+ const saveStatus = node("p", "Your changes are ready to save.", "context-copy");
80
+ saveStatus.attr.role = "status";
81
+
82
+ const cards = [
83
+ card("hierarchy", "01", "A clear hierarchy", "Three treatments for primary, secondary, and quieter actions.", [
84
+ group([
85
+ button({ content: "Save changes", click: action("Changes saved.") }),
86
+ button({ content: "Cancel", outlined: true, click: action("Action cancelled.") }),
87
+ button({ content: "Learn more", variant: "ghost", click: action("Choose solid for the main action, outline for alternatives, and ghost for quieter actions.") })
88
+ ]), actionStatus,
89
+ node("pre", 'new ButtonComponent({ content: "Save changes" })', "sample-code")
90
+ ]),
91
+ card("icons", "02", "Icons, with or without text", "Bring your own icons. Place them before or after the label.", [
92
+ group([
93
+ button({ content: "Create project", icon: icon("plus"), click: action("Project created.") }),
94
+ button({ content: "Download", icon: icon("download"), outlined: true, click: action("Download action clicked.") }),
95
+ button({ content: "Continue", icon: icon("arrow"), iconPosition: "end", variant: "ghost", click: action("Continue action clicked.") })
96
+ ]),
97
+ group([
98
+ button({ icon: icon("plus"), ariaLabel: "Add item", title: "Add item", click: action("Item added.") }),
99
+ button({ icon: icon("refresh"), ariaLabel: "Refresh", title: "Refresh", outlined: true, click: action("View refreshed.") }),
100
+ button({ icon: icon("more"), ariaLabel: "More options", title: "More options", variant: "ghost", click: action("More options action clicked.") }),
101
+ node("span", "Icon-only buttons have accessible names.", "inline-note")
102
+ ]),
103
+ node("pre", '{ icon: myIcon, ariaLabel: "Add item" }', "sample-code")
104
+ ]),
105
+ card("toggles", "03", "Actions that stay selected", "Toggle buttons expose their state to assistive technology. Try a few.", [
106
+ { tag: "div", attr: { class: "sample-row", role: "group", "aria-label": "Text formatting" }, children: [
107
+ ...formatting,
108
+ node("span", "", "sample-divider"),
109
+ button({ content: "Reset", variant: "ghost", click: () => {
110
+ formatting.forEach(item => { item.pressed = false; });
111
+ updateFormatting();
112
+ } })
113
+ ] },
114
+ toggleStatus,
115
+ group([bookmark]), bookmarkStatus,
116
+ node("pre", '{ toggle: true, pressed: false, onToggle: pressed => { … } }', "sample-code")
117
+ ]),
118
+ card("sizes", "04", "The right amount of space", "Compact controls for toolbars, comfortable actions for forms.", [
119
+ group([
120
+ group([button({ content: "Small", icon: icon("plus"), size: "small" }), node("span", "28 px", "size-caption")], "size-sample"),
121
+ group([button({ content: "Medium", icon: icon("plus") }), node("span", "32 px · default", "size-caption")], "size-sample"),
122
+ group([button({ content: "Large", icon: icon("plus"), size: "large" }), node("span", "40 px", "size-caption")], "size-sample")
123
+ ], "sample-row size-row"),
124
+ group([
125
+ button({ icon: icon("plus"), ariaLabel: "Small add", title: "Small add", size: "small", outlined: true }),
126
+ button({ icon: icon("plus"), ariaLabel: "Medium add", title: "Medium add", outlined: true }),
127
+ button({ icon: icon("plus"), ariaLabel: "Large add", title: "Large add", size: "large", outlined: true })
128
+ ]),
129
+ node("pre", '{ size: "small" | "medium" | "large" }', "sample-code")
130
+ ]),
131
+ card("states", "05", "Every state considered", "Native disabled behavior, a visible keyboard focus ring, and pressed states.", [
132
+ group([
133
+ button({ content: "Available" }),
134
+ button({ content: "Unavailable", enabled: false }),
135
+ button({ content: "Unavailable", outlined: true, enabled: false })
136
+ ]),
137
+ group([
138
+ button({ content: "Selected", icon: icon("check"), outlined: true, toggle: true, pressed: true }),
139
+ button({ content: "Selected", icon: icon("check"), outlined: true, toggle: true, pressed: true, enabled: false }),
140
+ button({ icon: icon("plus"), ariaLabel: "Unavailable add", title: "Unavailable add", variant: "ghost", enabled: false })
141
+ ]),
142
+ node("p", "Use Tab to explore keyboard focus. Enter and Space activate a focused button.", "inline-note"),
143
+ node("pre", '{ enabled: false }', "sample-code")
144
+ ]),
145
+ card("context", "06", "At home in an application", "Consistent actions, whether they appear on their own or in a footer.", [
146
+ group([
147
+ group([node("span", "PROJECT SETTINGS", "context-eyebrow"), node("h3", "Keep everything up to date"), saveStatus], "context-body"),
148
+ group([
149
+ button({ content: "Discard", outlined: true, click: () => { saveStatus.content = "Changes discarded."; } }),
150
+ button({ content: "Save changes", icon: icon("check"), click: () => { saveStatus.content = "All changes saved."; } })
151
+ ], "context-footer")
152
+ ], "context-panel"),
153
+ node("p", "The same styles also apply to vanilla buttons using .jsl-button.", "inline-note")
154
+ ]),
155
+ card("customization", "07", "Make it yours", "Custom content and application colors still fit the same button API.", [
156
+ group([
157
+ button({ content: { tag: "span", children: [node("span", "Inbox"), node("span", "8", "count-badge")] }, outlined: true }),
158
+ button({ content: "Publish", icon: icon("arrow"), iconPosition: "end", class: "brand-teal" })
159
+ ]),
160
+ node("pre", '.brand-teal {\n --jsl-button-primary: #0f766e;\n --jsl-button-on-primary: #fff;\n --jsl-button-primary-hover: #115e59;\n --jsl-button-primary-active: #134e4a;\n --jsl-button-focus: #0f766e;\n}', "sample-code")
161
+ ])
162
+ ];
163
+
164
+ const usage: IJSLVNode = {
165
+ tag: "details", attr: { class: "usage" }, children: [
166
+ node("summary", "Usage & compatibility"),
167
+ node("p", "Existing content, enabled, class, outlined, style, and click options continue to work. variant takes precedence over outlined when both are supplied."),
168
+ node("pre", 'import { ButtonComponent } from "jsl-ui";\n\nnew ButtonComponent({\n content: "Save view",\n icon: myBookmarkIcon,\n outlined: true,\n toggle: true,\n pressed: false,\n type: "button",\n onToggle: pressed => console.log(pressed),\n click: event => console.log(event)\n});', "sample-code"),
169
+ node("p", "For an icon-only button, omit content and supply ariaLabel, or set iconOnly: true to visually hide the content while keeping its accessible name. Icons can be strings, VNodes, or IJSLComponents. Keep icons decorative; place interactive elements outside the button."),
170
+ node("p", "The pressed option sets the initial toggle state. Read or assign button.pressed to update it later without firing events. User activation updates pressed, then raises onToggle and the existing click event. Toggle state survives jsl-render refreshes, including when a parent creates a new ButtonComponent instance during render."),
171
+ node("p", "The native button type is unchanged when type is omitted, including form submission. Use type: 'button' for actions inside a form that should not submit it. Disabled buttons are now natively disabled and skipped during keyboard navigation."),
172
+ node("p", "The neutral base palette lives in ui/theme.css. Override --jsl-color-* CSS variables on a container to theme components together, or --jsl-button-* variables to customize just buttons. ButtonComponent imports its own CSS. Vanilla controls receive the same styles through ui/base.css.")
173
+ ]
174
+ };
175
+
176
+ JSLRender.PrintRenderTime = false;
177
+ new JSLRender(document.getElementById("showcase"), true).render({
178
+ tag: "div", children: [{
179
+ tag: "section", attr: { id: "buttons", "aria-labelledby": "buttons-title" }, children: [
180
+ group([
181
+ group([node("p", "THE ESSENTIALS / 01", "eyebrow"), { tag: "h1", content: "Buttons", attr: { id: "buttons-title" } }, node("p", "A clear next step. A quiet alternative. An action that feels right.", "hero-description")], "hero-copy"),
182
+ group([node("span", "INTERACTIVE PREVIEW", "preview-caption"), button({ content: "Create something", icon: icon("plus"), size: "large", click: action("Something created. Your next step starts here.") }), node("span", "Simple by default. Flexible by design.", "preview-note")], "hero-preview")
183
+ ], "hero"),
184
+ group([node("span", "Overview", "overview-label"), node("span", "3 variants", "overview-stat"), node("span", "3 sizes", "overview-stat"), node("span", "Keyboard accessible", "overview-stat")], "overview-bar"),
185
+ group(cards, "sample-grid"), usage
186
+ ]
187
+ }, dialogShowcase, tabsShowcase, sliderShowcase, dateShowcase, dateRangeShowcase, dateTimeShowcase, selectShowcase, checkboxShowcase, expanderShowcase, inputShowcase, numberShowcase, formFieldShowcase, notificationShowcase, loadingShowcase, navigationShowcase, progressShowcase, stackShowcase, treeShowcase, overlayShowcase, ribbonShowcase, gridShowcase]
188
+ });
189
+
190
+ const navigation = Array.from(document.querySelectorAll<HTMLAnchorElement>(".nav-item"));
191
+ let navigationScheduled = false;
192
+ function updateNavigation() {
193
+ navigationScheduled = false;
194
+ let current = navigation[0];
195
+ for (const link of navigation) {
196
+ if (document.querySelector(link.hash).getBoundingClientRect().top <= window.innerHeight * 0.35) {
197
+ current = link;
198
+ }
199
+ }
200
+ for (const link of navigation) {
201
+ if (link === current) {
202
+ link.setAttribute("aria-current", "location");
203
+ } else {
204
+ link.removeAttribute("aria-current");
205
+ }
206
+ }
207
+ }
208
+ window.addEventListener("scroll", () => {
209
+ if (!navigationScheduled) {
210
+ navigationScheduled = true;
211
+ requestAnimationFrame(updateNavigation);
212
+ }
213
+ }, { passive: true });
214
+ updateNavigation();
@@ -0,0 +1,161 @@
1
+ import { IJSLComponent, IJSLVNode } from "jsl-render";
2
+ import { TabsComponent, ITabItemOption } from "../../components/tabs/TabsComponent";
3
+ import { ButtonComponent } from "../../components/button/ButtonComponent";
4
+
5
+ type Content = IJSLVNode | IJSLComponent;
6
+ const text = (tag: string, content: string, className?: string): IJSLVNode => ({ tag, content, attr: { class: className } });
7
+ const group = (children: Content[], className?: string): IJSLVNode => ({ tag: "div", attr: { class: className }, children });
8
+ const status = (content: string): IJSLVNode => ({ tag: "p", content, attr: { class: "sample-status", role: "status" } });
9
+ const action = (content: string, click: () => void) => new ButtonComponent({ content, type: "button", outlined: true, size: "small", click });
10
+ const copy = (title: string, content: string) => group([text("h4", title), text("p", content)], "tabs-example-copy");
11
+
12
+ const basicStatus = status("Overview is selected.");
13
+ const basicNames = ["Overview", "Activity", "Settings"];
14
+ const basic = new TabsComponent({
15
+ ariaLabel: "Project views",
16
+ tabs: [
17
+ { header: "Overview", content: copy("Everything in one place", "A clear view of your project, its progress, and what needs your attention.") },
18
+ { header: "Activity", content: copy("The latest updates", "Your team created a project, shared a draft, and reviewed the next release.") },
19
+ { header: "Settings", content: copy("Make it your own", "Manage project preferences, access, and notifications from a single view.") }
20
+ ],
21
+ onSelect: index => { basicStatus.content = basicNames[index] + " is selected."; }
22
+ });
23
+
24
+ const preserved = new TabsComponent({
25
+ ariaLabel: "Preserved project draft", preserveContent: true,
26
+ tabs: [
27
+ { header: "Draft", content: group([
28
+ { tag: "label", content: "Project name", attr: { for: "tabs-project-draft" } },
29
+ { tag: "input", attr: { id: "tabs-project-draft", class: "tabs-draft-input", type: "text", placeholder: "Type something, then switch tabs" } },
30
+ text("p", "Your unfinished work stays exactly where you left it.")
31
+ ], "tabs-example-copy") },
32
+ { header: "Preview", content: copy("Your draft is still there", "Switch back to Draft. The input keeps its value and its DOM element.") }
33
+ ]
34
+ });
35
+
36
+ const lazyStatus = status("No detail panels have been created yet.");
37
+ let createdPanels = 0;
38
+ const lazyTab = (header: string): ITabItemOption => ({
39
+ header,
40
+ onCreate: () => {
41
+ createdPanels++;
42
+ lazyStatus.content = createdPanels + (createdPanels === 1 ? " detail panel created." : " detail panels created.");
43
+ return copy(header, "This content was created on the first visit. Revisit this tab and the creation count stays the same.");
44
+ }
45
+ });
46
+ const lazy = new TabsComponent({
47
+ ariaLabel: "Lazy project details", preserveContent: true,
48
+ tabs: [
49
+ { header: "Summary", content: copy("Ready when you are", "Details are created only when you select their tab. Try Reports or History.") },
50
+ lazyTab("Reports"), lazyTab("History")
51
+ ]
52
+ });
53
+
54
+ const documentStatus = status("Add a draft, close one, or try closing the protected tab.");
55
+ let allowProtectedClose = false;
56
+ let nextDocument = 2;
57
+ const documents = new TabsComponent({
58
+ ariaLabel: "Open documents", preserveContent: true,
59
+ tabs: [
60
+ { header: "Overview", content: copy("Your workspace", "Keep a few documents open while you work.") },
61
+ { id: "showcase-tabs-draft-1", header: "Draft 1", canClose: true, content: copy("Draft 1", "A document that can be closed and replaced with another.") },
62
+ { id: "showcase-tabs-protected", header: "Protected", canClose: true,
63
+ content: copy("A little protection", "Allow this tab to close using the checkbox below, or keep it open with onClosing."),
64
+ onClosing: () => {
65
+ if (!allowProtectedClose) {
66
+ documentStatus.content = "The protected tab is still open. Allow closing it using the checkbox.";
67
+ return false;
68
+ }
69
+ return true;
70
+ }
71
+ }
72
+ ],
73
+ onSelect: index => { documentStatus.content = index < 0 ? "No document selected." : "Selected tab " + (index + 1) + "."; }
74
+ });
75
+
76
+ const disabledStatus = status("Audit is unavailable. Keyboard navigation skips it.");
77
+ const disabled = new TabsComponent({
78
+ ariaLabel: "Workspace sections",
79
+ tabs: [
80
+ { header: "Workspace", content: copy("Choose where to work", "Tabs can remain visible while an action or section is unavailable.") },
81
+ { header: "Audit", enabled: false, content: copy("Audit trail", "This section is now enabled and ready to use.") },
82
+ { header: "Team", content: copy("Work together", "Arrow keys move between available tabs, skipping disabled ones.") }
83
+ ]
84
+ });
85
+
86
+ const overflow = new TabsComponent({
87
+ ariaLabel: "Workspace information", maxHeight: "10rem",
88
+ tabs: [
89
+ { header: "Overview", content: copy("A place for every view", "When space gets tight, the header scrolls horizontally. Focused tabs stay in view.") },
90
+ { header: group([text("span", "Release notes"), { tag: "span", content: "3", attr: { class: "tabs-header-badge", "aria-hidden": "true" } }], "tabs-rich-header"),
91
+ ariaLabel: "Release notes, 3 updates",
92
+ content: group(Array.from({ length: 8 }, (_, index) => text("p", "Release " + (index + 1) + ": improvements to everyday work and collaboration.")), "tabs-example-copy") },
93
+ { header: "Deployment history", content: copy("Recent deployments", "The latest deployment completed successfully.") },
94
+ { header: "Team permissions", content: copy("Who can do what", "Keep access and responsibilities clear across the workspace.") },
95
+ { header: "Integrations", content: copy("Connected tools", "Connect your team's existing tools to its daily workflow.") },
96
+ { header: "Audit log", content: copy("A record of changes", "Track what changed, when it changed, and who made the update.") }
97
+ ]
98
+ });
99
+
100
+ const preview = new TabsComponent({
101
+ ariaLabel: "Tabs preview", class: "tabs-hero-demo",
102
+ tabs: [
103
+ { header: "Preview", content: text("p", "A clear view of the task at hand.") },
104
+ { header: "Details", content: text("p", "Another view, one selection away.") }
105
+ ]
106
+ });
107
+
108
+ function card(id: string, number: string, title: string, description: string, children: Content[]): IJSLVNode {
109
+ return { tag: "article", attr: { class: "sample-card", id, "aria-labelledby": id + "-title" }, children: [
110
+ group([text("span", number, "sample-number"), { tag: "h3", content: title, attr: { id: id + "-title" } }], "card-heading"),
111
+ text("p", description, "card-description"), ...children
112
+ ] };
113
+ }
114
+
115
+ export const tabsShowcase: IJSLVNode = {
116
+ tag: "section", attr: { id: "tabs", class: "component-section", "aria-labelledby": "tabs-title" }, children: [
117
+ group([
118
+ group([
119
+ text("p", "THE ESSENTIALS / 03", "eyebrow"),
120
+ { tag: "h2", content: "Tabs", attr: { id: "tabs-title", class: "section-title" } },
121
+ text("p", "Related views, one clear selection. Keep the context and make room for the details.", "hero-description")
122
+ ], "hero-copy"),
123
+ group([text("span", "INTERACTIVE PREVIEW", "preview-caption"), preview, text("span", "Use the arrow keys to explore.", "preview-note")], "hero-preview")
124
+ ], "hero"),
125
+ group([text("span", "Overview", "overview-label"), text("span", "Lazy content", "overview-stat"), text("span", "Closable tabs", "overview-stat"), text("span", "Keyboard accessible", "overview-stat")], "overview-bar"),
126
+ group([
127
+ card("tabs-basic", "01", "One view at a time", "An understated active indicator, comfortable labels, and linked content panels.", [basic, basicStatus]),
128
+ card("tabs-preserved", "02", "Keep your work", "Preserved panels keep draft input and component state when you switch views.", [preserved, text("pre", "{ preserveContent: true }", "sample-code")]),
129
+ card("tabs-lazy", "03", "Create only when needed", "Load content on its first selection, then keep the result for future visits.", [lazy, lazyStatus]),
130
+ card("tabs-documents", "04", "Tabs that come and go", "Add and close documents. Use Delete on a closable tab, or its close button.", [
131
+ documents,
132
+ group([action("Add draft", () => {
133
+ const number = nextDocument++;
134
+ const index = documents.AddTab({ header: "Draft " + number, canClose: true, content: copy("Draft " + number, "A new document, ready for your next idea.") });
135
+ documents.SetSelectedTab(index);
136
+ documentStatus.content = "Draft " + number + " added.";
137
+ })], "sample-row"),
138
+ { tag: "label", attr: { class: "tabs-example-check" }, children: [
139
+ { tag: "input", attr: { type: "checkbox", id: "tabs-allow-close", change: (event: Event) => { allowProtectedClose = (event.target as HTMLInputElement).checked; } } },
140
+ text("span", "Allow closing the protected tab")
141
+ ] }, documentStatus
142
+ ]),
143
+ card("tabs-disabled", "05", "Available when you need it", "Disabled tabs stay visible and are skipped by pointer and keyboard activation.", [
144
+ disabled,
145
+ group([
146
+ action("Enable all", () => { disabled.EnableAll(); disabledStatus.content = "All sections are available."; }),
147
+ action("Disable all", () => { disabled.DisableAll(); disabledStatus.content = "All sections are disabled. Current content stays visible."; })
148
+ ], "sample-row"), disabledStatus
149
+ ]),
150
+ card("tabs-overflow", "06", "Labels with room to grow", "Rich headers and long lists fit a scrolling tab bar. Content height can be limited independently.", [overflow, text("pre", '{ maxHeight: "10rem" }', "sample-code")])
151
+ ], "sample-grid tabs-sample-grid"),
152
+ { tag: "details", attr: { class: "usage" }, children: [
153
+ text("summary", "Tabs usage & compatibility"),
154
+ text("pre", 'new TabsComponent({\n tabs: [\n { id: "overview", header: "Overview", content: "Project overview" },\n { id: "reports", header: "Reports", onCreate: () => createReports(), canClose: true }\n ],\n preserveContent: true,\n onSelect: index => { /* Return false to veto user selection. */ }\n});', "sample-code"),
155
+ text("p", "Existing options and AddTab, SetSelectedTab, getActiveTabIndex, GetByID, closeTabIndex, EnableAll, and DisableAll methods are preserved. SetSelectedTab selects without raising onSelect; -1 clears selection. Invalid indices are ignored."),
156
+ text("p", "Arrow Left/Right, Home, and End focus and select enabled tabs. Enter and Space activate a focused tab. Delete closes a closable tab. onClosing can veto closure; closeTabIndex(index, true) also emits the resulting selection."),
157
+ text("p", "onCreate runs once per tab, on its first selection. By default, inactive content is unmounted; preserveContent keeps it mounted and hidden. Closing a tab keeps the current document selected when possible, or selects the next enabled neighbor. Empty tab sets are supported."),
158
+ text("p", "Tab IDs must be unique. Each tab is linked to a panel with accessible roles and selection state. Use ariaLabel on the tab set or individual items, and closeButtonLabel to translate a close control. Custom headers should contain decorative content, keeping interactive controls in the panel.")
159
+ ] }
160
+ ]
161
+ };
@@ -0,0 +1,83 @@
1
+ /* Showcase-only examples of overriding the shared library palette. */
2
+ :root { color-scheme: light; }
3
+
4
+ :root[data-showcase-theme="ocean"] {
5
+ --jsl-color-canvas: #f5f8fc;
6
+ --jsl-color-surface-subtle: #f8fafd;
7
+ --jsl-color-surface-hover: #edf2f9;
8
+ --jsl-color-surface-selected: #e3edfc;
9
+ --jsl-color-border: #c7d4e8;
10
+ --jsl-color-border-subtle: #e0e7f1;
11
+ --jsl-color-text: #24344a;
12
+ --jsl-color-text-secondary: #4b5e78;
13
+ --jsl-color-text-muted: #62748a;
14
+ --jsl-color-accent: #245fc7;
15
+ --jsl-color-accent-hover: #1d4fa8;
16
+ --jsl-color-accent-active: #184187;
17
+ --jsl-color-focus: #245fc7;
18
+ --jsl-color-disabled-surface: #edf2f7;
19
+ --jsl-color-disabled-text: #62748a;
20
+ }
21
+
22
+ :root[data-showcase-theme="forest"] {
23
+ --jsl-color-canvas: #f5f8f5;
24
+ --jsl-color-surface-subtle: #f9fbf8;
25
+ --jsl-color-surface-hover: #eef4ed;
26
+ --jsl-color-surface-selected: #dfeddf;
27
+ --jsl-color-border: #c6d6c5;
28
+ --jsl-color-border-subtle: #dfe8dd;
29
+ --jsl-color-text: #293b2b;
30
+ --jsl-color-text-secondary: #4c6250;
31
+ --jsl-color-text-muted: #647667;
32
+ --jsl-color-accent: #356b45;
33
+ --jsl-color-accent-hover: #295436;
34
+ --jsl-color-accent-active: #203f29;
35
+ --jsl-color-focus: #356b45;
36
+ --jsl-color-disabled-surface: #eef3ed;
37
+ --jsl-color-disabled-text: #647667;
38
+ }
39
+
40
+ :root[data-showcase-theme="plum"] {
41
+ --jsl-color-canvas: #faf6fa;
42
+ --jsl-color-surface-subtle: #fcf9fc;
43
+ --jsl-color-surface-hover: #f4edf5;
44
+ --jsl-color-surface-selected: #efdef0;
45
+ --jsl-color-border: #dac8dd;
46
+ --jsl-color-border-subtle: #ebdfed;
47
+ --jsl-color-text: #412b46;
48
+ --jsl-color-text-secondary: #6c5271;
49
+ --jsl-color-text-muted: #7b667f;
50
+ --jsl-color-accent: #80458b;
51
+ --jsl-color-accent-hover: #693872;
52
+ --jsl-color-accent-active: #542d5c;
53
+ --jsl-color-focus: #80458b;
54
+ --jsl-color-disabled-surface: #f3edf4;
55
+ --jsl-color-disabled-text: #7b667f;
56
+ }
57
+
58
+ :root[data-showcase-theme="midnight"] {
59
+ --jsl-color-success: #86efac;
60
+ --jsl-color-warning: #fcd34d;
61
+ --jsl-color-info: #93c5fd;
62
+ --jsl-color-danger: #fda4af;
63
+ color-scheme: dark;
64
+ --jsl-color-overlay: rgba(0, 0, 0, 0.65);
65
+ --jsl-shadow-dialog: 0 24px 80px rgba(0, 0, 0, 0.55);
66
+ --jsl-color-canvas: #18181b;
67
+ --jsl-color-surface: #232326;
68
+ --jsl-color-surface-subtle: #1e1e21;
69
+ --jsl-color-surface-hover: #303035;
70
+ --jsl-color-surface-selected: #3b3b43;
71
+ --jsl-color-border: #62626d;
72
+ --jsl-color-border-subtle: #3d3d45;
73
+ --jsl-color-text: #f4f4f5;
74
+ --jsl-color-text-secondary: #d4d4d8;
75
+ --jsl-color-text-muted: #a1a1aa;
76
+ --jsl-color-accent: #e4e4e7;
77
+ --jsl-color-accent-hover: #ffffff;
78
+ --jsl-color-accent-active: #a1a1aa;
79
+ --jsl-color-on-accent: #18181b;
80
+ --jsl-color-focus: #a1a1aa;
81
+ --jsl-color-disabled-surface: #2b2b30;
82
+ --jsl-color-disabled-text: #91919c;
83
+ }
@@ -0,0 +1,54 @@
1
+ import "./themes.css";
2
+ import "./nivona.css";
3
+
4
+ export const showcaseThemes = [
5
+ { id: "default", label: "Default (neutral)" },
6
+ { id: "nivona", label: "Nivona" },
7
+ { id: "ocean", label: "Ocean" },
8
+ { id: "forest", label: "Forest" },
9
+ { id: "plum", label: "Plum" },
10
+ { id: "midnight", label: "Midnight (dark)" }
11
+ ] as const;
12
+
13
+ export type ShowcaseTheme = typeof showcaseThemes[number]["id"];
14
+
15
+ const storageKey = "jsl-showcase-theme";
16
+ const themePicker = document.getElementById("showcase-theme") as HTMLSelectElement;
17
+
18
+ /** Switch the showcase palette without recreating components or resetting their state. */
19
+ export function changeTheme(themeId: string): ShowcaseTheme {
20
+ const theme = showcaseThemes.find(item => item.id === themeId)?.id ?? "default";
21
+ if (theme === "default") {
22
+ // Removing the override restores the library's actual built-in palette.
23
+ document.documentElement.removeAttribute("data-showcase-theme");
24
+ } else {
25
+ document.documentElement.setAttribute("data-showcase-theme", theme);
26
+ }
27
+ themePicker.value = theme;
28
+ try {
29
+ localStorage.setItem(storageKey, theme);
30
+ } catch {
31
+ // Theme switching also works when browser storage is unavailable.
32
+ }
33
+ return theme;
34
+ }
35
+
36
+ themePicker.replaceChildren(...showcaseThemes.map(theme => {
37
+ const option = document.createElement("option");
38
+ option.value = theme.id;
39
+ option.textContent = theme.label;
40
+ return option;
41
+ }));
42
+ themePicker.addEventListener("change", () => changeTheme(themePicker.value));
43
+
44
+ let savedTheme = "default";
45
+ try {
46
+ savedTheme = localStorage.getItem(storageKey) ?? "default";
47
+ } catch {
48
+ // Use the built-in palette when browser storage is unavailable.
49
+ }
50
+ // Share a particular palette with ?theme=nivona (or any registered theme id).
51
+ // A valid URL choice takes precedence over the saved preference, including default.
52
+ const urlTheme = new URLSearchParams(window.location.search).get("theme");
53
+ const initialTheme = showcaseThemes.some(theme => theme.id === urlTheme) ? urlTheme : savedTheme;
54
+ changeTheme(initialTheme);
@@ -7,6 +7,11 @@ export interface IIntervalOptions {
7
7
  export declare class IntervalComponent implements IJSLComponent {
8
8
  private fnc;
9
9
  private options;
10
+ State: {
11
+ run?: {
12
+ active: boolean;
13
+ };
14
+ };
10
15
  private renderer;
11
16
  constructor(fnc: any, options: IIntervalOptions);
12
17
  isEqual(node: IJSLComponent): boolean;
@@ -2,6 +2,8 @@ export class IntervalComponent {
2
2
  constructor(fnc, options) {
3
3
  this.fnc = fnc;
4
4
  this.options = options;
5
+ // Shared by jsl-render when a refresh replaces the component instance.
6
+ this.State = {};
5
7
  }
6
8
  isEqual(node) {
7
9
  return this.__proto__ === node.__proto__;
@@ -13,12 +15,31 @@ export class IntervalComponent {
13
15
  this.renderer = render;
14
16
  }
15
17
  onCreate(node) {
18
+ if (this.State.run) {
19
+ this.State.run.active = false;
20
+ }
21
+ const run = this.State.run = { active: true };
16
22
  const timeoutRef = node.dom.value;
17
23
  if (timeoutRef && timeoutRef !== "0") {
18
24
  clearTimeout(parseInt(timeoutRef, 10));
19
25
  node.dom.value = "";
20
26
  }
27
+ const complete = () => {
28
+ if (!run.active) {
29
+ return;
30
+ }
31
+ if (!this.options.noRefresh) {
32
+ this.renderer.refresh();
33
+ }
34
+ // Refreshing may synchronously remove this component.
35
+ if (run.active) {
36
+ node.dom.value = setTimeout(f, this.options.interval).toString();
37
+ }
38
+ };
21
39
  const f = () => {
40
+ if (!run.active) {
41
+ return;
42
+ }
22
43
  let result;
23
44
  try {
24
45
  result = this.fnc();
@@ -31,31 +52,26 @@ export class IntervalComponent {
31
52
  }
32
53
  const isPromise = result && typeof result.then === "function" && typeof result.catch === "function";
33
54
  if (isPromise) {
34
- result.then(() => {
35
- if (!this.options.noRefresh) {
36
- this.renderer.refresh();
55
+ result.then(complete).catch((e) => {
56
+ if (!run.active) {
57
+ return;
37
58
  }
38
- node.dom.value = setTimeout(f, this.options.interval).toString();
39
- }).catch((e) => {
40
59
  console.error(e);
41
60
  if (!this.options.stopOnError) {
42
- if (!this.options.noRefresh) {
43
- this.renderer.refresh();
44
- }
45
- node.dom.value = setTimeout(f, this.options.interval).toString();
61
+ complete();
46
62
  }
47
63
  });
48
64
  }
49
65
  else {
50
- if (!this.options.noRefresh) {
51
- this.renderer.refresh();
52
- }
53
- node.dom.value = setTimeout(f, this.options.interval).toString();
66
+ complete();
54
67
  }
55
68
  };
56
69
  f();
57
70
  }
58
71
  onRemove(node) {
72
+ if (this.State.run) {
73
+ this.State.run.active = false;
74
+ }
59
75
  const timeoutRef = node.dom.value;
60
76
  if (timeoutRef && timeoutRef !== "0") {
61
77
  clearTimeout(parseInt(timeoutRef, 10));
@@ -1,22 +1,24 @@
1
- import { IJSLComponent, IJSLVNode } from "jsl-render";
1
+ import { IJSLVNode } from "jsl-render";
2
2
  import { IAutoSuggestOptions, IAutoSuggest } from "../../ui/auto.suggest";
3
+ import { InputComponent } from "../input/InputComponent";
3
4
  export interface IAutoSuggestComponentOptions extends IAutoSuggestOptions {
5
+ id?: string;
4
6
  value?: string;
5
7
  class?: string;
6
8
  style?: string;
7
9
  placeholder?: string;
10
+ enabled?: boolean;
11
+ readOnly?: boolean;
12
+ unstyled?: boolean;
13
+ ariaLabel?: string;
14
+ maxLength?: number;
8
15
  }
9
- export declare class AutoSuggestComponent implements IJSLComponent {
10
- private options;
16
+ export declare class AutoSuggestComponent extends InputComponent {
11
17
  private ref;
12
- private dom;
18
+ private input;
19
+ private suggestOptions;
13
20
  constructor(options: IAutoSuggestComponentOptions);
14
- render(): IJSLVNode;
15
21
  getRef(): IAutoSuggest;
16
- getValue(): string;
17
- setValue(val: string): void;
18
- focus(): void;
19
22
  onCreate(node: IJSLVNode): void;
20
- onUpdate(node: IJSLVNode): void;
21
23
  onRemove(): void;
22
24
  }