@incursa/ui-kit 1.0.1 → 1.2.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 (45) hide show
  1. package/AI-AGENT-INSTRUCTIONS.md +41 -28
  2. package/LLMS.txt +55 -41
  3. package/README.md +181 -68
  4. package/dist/inc-design-language.css +413 -239
  5. package/dist/inc-design-language.css.map +1 -1
  6. package/dist/inc-design-language.js +520 -0
  7. package/dist/inc-design-language.min.css +1 -1
  8. package/dist/inc-design-language.min.css.map +1 -1
  9. package/dist/web-components/README.md +92 -0
  10. package/dist/web-components/RUNTIME-NOTES.md +40 -0
  11. package/dist/web-components/base-element.js +193 -0
  12. package/dist/web-components/components/feedback.js +1074 -0
  13. package/dist/web-components/components/forms.js +979 -0
  14. package/dist/web-components/components/layout.js +408 -0
  15. package/dist/web-components/components/navigation.js +854 -0
  16. package/dist/web-components/components/overlays.js +634 -0
  17. package/dist/web-components/controllers/focus.js +101 -0
  18. package/dist/web-components/controllers/overlay.js +128 -0
  19. package/dist/web-components/controllers/selection.js +145 -0
  20. package/dist/web-components/controllers/theme.js +173 -0
  21. package/dist/web-components/index.js +886 -0
  22. package/dist/web-components/package.json +3 -0
  23. package/dist/web-components/registry.js +74 -0
  24. package/dist/web-components/shared.js +186 -0
  25. package/dist/web-components/style.css +6 -0
  26. package/package.json +11 -2
  27. package/src/inc-design-language.js +520 -0
  28. package/src/inc-design-language.scss +434 -246
  29. package/src/web-components/README.md +92 -0
  30. package/src/web-components/RUNTIME-NOTES.md +40 -0
  31. package/src/web-components/base-element.js +193 -0
  32. package/src/web-components/components/feedback.js +1074 -0
  33. package/src/web-components/components/forms.js +979 -0
  34. package/src/web-components/components/layout.js +408 -0
  35. package/src/web-components/components/navigation.js +854 -0
  36. package/src/web-components/components/overlays.js +634 -0
  37. package/src/web-components/controllers/focus.js +101 -0
  38. package/src/web-components/controllers/overlay.js +128 -0
  39. package/src/web-components/controllers/selection.js +145 -0
  40. package/src/web-components/controllers/theme.js +173 -0
  41. package/src/web-components/index.js +886 -0
  42. package/src/web-components/package.json +3 -0
  43. package/src/web-components/registry.js +74 -0
  44. package/src/web-components/shared.js +186 -0
  45. package/src/web-components/style.css +6 -0
@@ -6,6 +6,11 @@
6
6
  menu: ".inc-dropdown__menu",
7
7
  collapseToggle: '[data-inc-toggle="collapse"]',
8
8
  tabToggle: '[data-inc-toggle="tab"]',
9
+ themeMode: "[data-inc-theme-mode]:not(html)",
10
+ themeToggle: "[data-inc-theme-toggle]",
11
+ themeSelect: "[data-inc-theme-select]",
12
+ themeLabel: "[data-inc-theme-label]",
13
+ themeSwitcher: "[data-inc-theme-switcher], details.inc-theme-switcher",
9
14
  nativeDialogOpen: "[data-inc-native-dialog-open]",
10
15
  autoRefresh: "[data-inc-auto-refresh]",
11
16
  autoRefreshToggle: '[data-inc-action="auto-refresh-toggle"]',
@@ -30,6 +35,411 @@
30
35
 
31
36
  const autoRefreshControllers = [];
32
37
  let autoRefreshReloadScheduled = false;
38
+ const themeModes = ["light", "dark", "system"];
39
+ const themeDescriptions = {
40
+ light: "Use the brighter application palette.",
41
+ dark: "Use the darker application palette.",
42
+ system: "Match the device preference automatically.",
43
+ };
44
+ const themeStorageKey = "inc-theme-mode";
45
+ const themeState = {
46
+ mode: "system",
47
+ resolved: "light",
48
+ };
49
+ let themeMediaQuery = null;
50
+ let themeMediaListenerBound = false;
51
+ let themeStorageListenerBound = false;
52
+ let themeInitialized = false;
53
+
54
+ function isThemeMode(value) {
55
+ return themeModes.includes(value);
56
+ }
57
+
58
+ function getThemeLabel(mode) {
59
+ if (!isThemeMode(mode)) {
60
+ return "System";
61
+ }
62
+
63
+ return mode.charAt(0).toUpperCase() + mode.slice(1);
64
+ }
65
+
66
+ function getThemeStatusLabel(mode = themeState.mode, resolved = themeState.resolved) {
67
+ return mode === "system"
68
+ ? `${getThemeLabel(mode)} (${getThemeLabel(resolved)})`
69
+ : getThemeLabel(mode);
70
+ }
71
+
72
+ function getStoredThemeMode() {
73
+ try {
74
+ const stored = window.localStorage.getItem(themeStorageKey);
75
+ return isThemeMode(stored) ? stored : null;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ function getConfiguredThemeMode() {
82
+ const root = document.documentElement;
83
+
84
+ return root.getAttribute("data-inc-theme-mode")
85
+ || root.dataset.incThemeMode
86
+ || root.getAttribute("data-bs-theme")
87
+ || "system";
88
+ }
89
+
90
+ function persistThemeMode(mode) {
91
+ try {
92
+ if (mode === "system") {
93
+ window.localStorage.removeItem(themeStorageKey);
94
+ return;
95
+ }
96
+
97
+ window.localStorage.setItem(themeStorageKey, mode);
98
+ } catch {
99
+ // Ignore storage failures in private mode or restricted contexts.
100
+ }
101
+ }
102
+
103
+ function getSystemTheme() {
104
+ if (!window.matchMedia) {
105
+ return "light";
106
+ }
107
+
108
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
109
+ }
110
+
111
+ function resolveThemeMode(mode) {
112
+ return mode === "system" ? getSystemTheme() : mode;
113
+ }
114
+
115
+ function syncThemeControls(mode, resolved) {
116
+ document.querySelectorAll(selectors.themeMode).forEach((control) => {
117
+ if (!(control instanceof HTMLElement)) {
118
+ return;
119
+ }
120
+
121
+ const controlMode = control.getAttribute("data-inc-theme-mode");
122
+ const isSelected = controlMode === mode;
123
+ const role = control.getAttribute("role");
124
+
125
+ control.classList.toggle("active", isSelected);
126
+ control.classList.toggle("is-selected", isSelected);
127
+
128
+ if (role === "menuitemradio" || role === "radio") {
129
+ control.setAttribute("aria-checked", isSelected ? "true" : "false");
130
+ } else if (control.tagName === "BUTTON" || control.tagName === "A") {
131
+ control.setAttribute("aria-pressed", isSelected ? "true" : "false");
132
+ }
133
+
134
+ if (control.tagName === "INPUT" && (control.type === "radio" || control.type === "checkbox")) {
135
+ control.checked = isSelected;
136
+ }
137
+
138
+ if (control.tagName === "OPTION") {
139
+ control.selected = isSelected;
140
+ }
141
+ });
142
+
143
+ document.querySelectorAll(selectors.themeSelect).forEach((control) => {
144
+ if (control instanceof HTMLSelectElement) {
145
+ control.value = mode;
146
+ }
147
+ });
148
+
149
+ document.querySelectorAll(selectors.themeLabel).forEach((label) => {
150
+ if (!(label instanceof HTMLElement)) {
151
+ return;
152
+ }
153
+
154
+ const labelType = label.getAttribute("data-inc-theme-label") || "status";
155
+
156
+ if (labelType === "resolved") {
157
+ label.textContent = getThemeLabel(resolved);
158
+ return;
159
+ }
160
+
161
+ if (labelType === "mode") {
162
+ label.textContent = getThemeLabel(mode);
163
+ return;
164
+ }
165
+
166
+ label.textContent = getThemeStatusLabel(mode, resolved);
167
+ });
168
+
169
+ document.querySelectorAll(selectors.themeSwitcher).forEach((switcher) => {
170
+ if (!(switcher instanceof HTMLElement)) {
171
+ return;
172
+ }
173
+
174
+ switcher.dataset.incThemeModeState = mode;
175
+ switcher.dataset.incThemeResolved = resolved;
176
+ });
177
+ }
178
+
179
+ function publishThemeChange() {
180
+ document.documentElement.dispatchEvent(new CustomEvent("inc-theme-change", {
181
+ bubbles: true,
182
+ composed: true,
183
+ detail: {
184
+ mode: themeState.mode,
185
+ resolved: themeState.resolved,
186
+ },
187
+ }));
188
+ }
189
+
190
+ function applyThemeMode(mode, options = {}) {
191
+ const nextMode = isThemeMode(mode) ? mode : "system";
192
+ const resolved = resolveThemeMode(nextMode);
193
+ const root = document.documentElement;
194
+
195
+ themeState.mode = nextMode;
196
+ themeState.resolved = resolved;
197
+
198
+ root.setAttribute("data-inc-theme-mode", nextMode);
199
+ root.setAttribute("data-bs-theme", resolved);
200
+ root.style.colorScheme = resolved;
201
+ root.dataset.incThemeModeState = nextMode;
202
+ root.dataset.incThemeResolved = resolved;
203
+
204
+ if (options.persist !== false) {
205
+ persistThemeMode(nextMode);
206
+ }
207
+
208
+ if (options.syncControls !== false) {
209
+ syncThemeControls(nextMode, resolved);
210
+ }
211
+
212
+ if (options.dispatch !== false) {
213
+ publishThemeChange();
214
+ }
215
+
216
+ return themeState;
217
+ }
218
+
219
+ function cycleThemeMode() {
220
+ const currentIndex = themeModes.indexOf(themeState.mode);
221
+ const nextMode = themeModes[(currentIndex + 1) % themeModes.length];
222
+
223
+ return applyThemeMode(nextMode);
224
+ }
225
+
226
+ function createThemeSwitcherOption(mode) {
227
+ const button = document.createElement("button");
228
+ const body = document.createElement("span");
229
+ const label = document.createElement("span");
230
+ const detail = document.createElement("span");
231
+
232
+ button.type = "button";
233
+ button.className = "inc-theme-switcher__option";
234
+ button.setAttribute("data-inc-theme-mode", mode);
235
+ button.setAttribute("role", "menuitemradio");
236
+
237
+ body.className = "inc-theme-switcher__option-body";
238
+ label.className = "inc-theme-switcher__option-label";
239
+ label.textContent = getThemeLabel(mode);
240
+ detail.className = "inc-theme-switcher__option-detail";
241
+ detail.textContent = themeDescriptions[mode];
242
+
243
+ body.append(label, detail);
244
+ button.append(body);
245
+
246
+ return button;
247
+ }
248
+
249
+ function createThemeSwitcher(options = {}) {
250
+ const switcher = document.createElement("details");
251
+ const summary = document.createElement("summary");
252
+ const meta = document.createElement("span");
253
+ const label = document.createElement("span");
254
+ const status = document.createElement("span");
255
+ const panel = document.createElement("div");
256
+ const header = document.createElement("div");
257
+
258
+ switcher.className = "inc-native-menu inc-theme-switcher";
259
+
260
+ if (options.variant === "navbar") {
261
+ switcher.classList.add("inc-native-menu--navbar");
262
+ }
263
+
264
+ if (options.block) {
265
+ switcher.classList.add("inc-native-menu--block");
266
+ }
267
+
268
+ summary.className = "inc-native-menu__summary inc-theme-switcher__summary";
269
+ meta.className = "inc-theme-switcher__meta";
270
+ label.className = "inc-theme-switcher__label";
271
+ label.textContent = options.label || "Theme";
272
+ status.className = "inc-theme-switcher__status";
273
+ status.setAttribute("data-inc-theme-label", "status");
274
+ status.textContent = getThemeStatusLabel();
275
+ meta.append(label, status);
276
+ summary.append(meta);
277
+
278
+ panel.className = "inc-native-menu__panel inc-theme-switcher__panel";
279
+ panel.setAttribute("role", "menu");
280
+ panel.setAttribute("aria-label", options.menuLabel || "Theme");
281
+
282
+ header.className = "inc-native-menu__header";
283
+ header.textContent = options.heading || "Choose appearance";
284
+ panel.append(header);
285
+
286
+ themeModes.forEach((mode) => {
287
+ panel.append(createThemeSwitcherOption(mode));
288
+ });
289
+
290
+ switcher.append(summary, panel);
291
+ syncThemeControls(themeState.mode, themeState.resolved);
292
+
293
+ return switcher;
294
+ }
295
+
296
+ function mountThemeSwitcher(target, options = {}) {
297
+ let host = target;
298
+
299
+ if (typeof target === "string") {
300
+ host = document.querySelector(target);
301
+ }
302
+
303
+ if (!(host instanceof HTMLElement)) {
304
+ return null;
305
+ }
306
+
307
+ const switcher = createThemeSwitcher(options);
308
+ host.replaceChildren(switcher);
309
+ syncThemeControls(themeState.mode, themeState.resolved);
310
+
311
+ return switcher;
312
+ }
313
+
314
+ function getThemeSwitcherOptions(control) {
315
+ const panel = control.closest(".inc-theme-switcher__panel");
316
+
317
+ if (!panel) {
318
+ return [];
319
+ }
320
+
321
+ return Array.from(panel.querySelectorAll(selectors.themeMode)).filter((option) => option.closest(".inc-theme-switcher__panel") === panel);
322
+ }
323
+
324
+ function focusThemeSwitcherOption(control, direction) {
325
+ const options = getThemeSwitcherOptions(control);
326
+
327
+ if (!options.length) {
328
+ return;
329
+ }
330
+
331
+ const activeIndex = options.findIndex((option) => option === control);
332
+
333
+ if (direction === "first") {
334
+ options[0]?.focus();
335
+ return;
336
+ }
337
+
338
+ if (direction === "last") {
339
+ options[options.length - 1]?.focus();
340
+ return;
341
+ }
342
+
343
+ const delta = direction === "next" ? 1 : -1;
344
+ const startIndex = activeIndex === -1 ? 0 : activeIndex;
345
+ const nextIndex = (startIndex + delta + options.length) % options.length;
346
+ options[nextIndex]?.focus();
347
+ }
348
+
349
+ function initializeThemeSwitchers() {
350
+ document.querySelectorAll(selectors.themeSwitcher).forEach((switcher) => {
351
+ if (!(switcher instanceof HTMLElement) || switcher.dataset.incThemeSwitcherInitialized === "true") {
352
+ return;
353
+ }
354
+
355
+ switcher.dataset.incThemeSwitcherInitialized = "true";
356
+
357
+ if (switcher.matches("details.inc-theme-switcher")) {
358
+ syncThemeControls(themeState.mode, themeState.resolved);
359
+ return;
360
+ }
361
+
362
+ if (switcher.querySelector(selectors.themeMode)) {
363
+ syncThemeControls(themeState.mode, themeState.resolved);
364
+ return;
365
+ }
366
+
367
+ mountThemeSwitcher(switcher, {
368
+ variant: switcher.getAttribute("data-inc-theme-switcher-variant")
369
+ || (switcher.closest(".inc-navbar, .inc-navbar__utilities") ? "navbar" : undefined),
370
+ block: switcher.hasAttribute("data-inc-theme-switcher-block"),
371
+ label: switcher.getAttribute("data-inc-theme-switcher-label") || "Theme",
372
+ menuLabel: switcher.getAttribute("data-inc-theme-switcher-menu-label") || "Theme",
373
+ heading: switcher.getAttribute("data-inc-theme-switcher-heading") || "Choose appearance",
374
+ });
375
+ });
376
+ }
377
+
378
+ function bindThemeMediaListener() {
379
+ if (themeMediaListenerBound || !window.matchMedia) {
380
+ return;
381
+ }
382
+
383
+ themeMediaListenerBound = true;
384
+ themeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
385
+
386
+ const handleThemePreferenceChange = () => {
387
+ if (themeState.mode === "system") {
388
+ applyThemeMode("system", { persist: false });
389
+ }
390
+ };
391
+
392
+ if (typeof themeMediaQuery.addEventListener === "function") {
393
+ themeMediaQuery.addEventListener("change", handleThemePreferenceChange);
394
+ } else if (typeof themeMediaQuery.addListener === "function") {
395
+ themeMediaQuery.addListener(handleThemePreferenceChange);
396
+ }
397
+ }
398
+
399
+ function bindThemeStorageListener() {
400
+ if (themeStorageListenerBound) {
401
+ return;
402
+ }
403
+
404
+ themeStorageListenerBound = true;
405
+
406
+ window.addEventListener("storage", (event) => {
407
+ if (event.key !== themeStorageKey) {
408
+ return;
409
+ }
410
+
411
+ applyThemeMode(getStoredThemeMode() || getConfiguredThemeMode(), {
412
+ persist: false,
413
+ });
414
+ });
415
+ }
416
+
417
+ function initializeTheme() {
418
+ if (themeInitialized) {
419
+ syncThemeControls(themeState.mode, themeState.resolved);
420
+ initializeThemeSwitchers();
421
+ return themeState;
422
+ }
423
+
424
+ themeInitialized = true;
425
+
426
+ applyThemeMode(getStoredThemeMode() || getConfiguredThemeMode(), {
427
+ persist: false,
428
+ });
429
+
430
+ bindThemeMediaListener();
431
+ bindThemeStorageListener();
432
+ initializeThemeSwitchers();
433
+ syncThemeControls(themeState.mode, themeState.resolved);
434
+
435
+ return themeState;
436
+ }
437
+
438
+ applyThemeMode(getStoredThemeMode() || getConfiguredThemeMode(), {
439
+ dispatch: false,
440
+ persist: false,
441
+ syncControls: false,
442
+ });
33
443
 
34
444
  function getTarget(trigger) {
35
445
  const rawTarget = trigger.getAttribute("data-inc-target")
@@ -766,6 +1176,34 @@
766
1176
 
767
1177
  function attachEventHandlers() {
768
1178
  document.addEventListener("click", (event) => {
1179
+ const themeToggle = event.target.closest(selectors.themeToggle);
1180
+
1181
+ if (themeToggle) {
1182
+ event.preventDefault();
1183
+ cycleThemeMode();
1184
+ return;
1185
+ }
1186
+
1187
+ const themeModeControl = event.target.closest(selectors.themeMode);
1188
+
1189
+ if (themeModeControl && themeModeControl.tagName !== "INPUT") {
1190
+ event.preventDefault();
1191
+ applyThemeMode(themeModeControl.getAttribute("data-inc-theme-mode"));
1192
+
1193
+ const owningSwitcher = themeModeControl.closest("details.inc-theme-switcher");
1194
+ const switcherSummary = owningSwitcher?.querySelector("summary");
1195
+
1196
+ if (owningSwitcher instanceof HTMLDetailsElement) {
1197
+ owningSwitcher.open = false;
1198
+ }
1199
+
1200
+ if (switcherSummary instanceof HTMLElement) {
1201
+ switcherSummary.focus();
1202
+ }
1203
+
1204
+ return;
1205
+ }
1206
+
769
1207
  const autoRefreshToggle = event.target.closest(selectors.autoRefreshToggle);
770
1208
 
771
1209
  if (autoRefreshToggle) {
@@ -879,12 +1317,69 @@
879
1317
  }
880
1318
  });
881
1319
 
1320
+ document.addEventListener("change", (event) => {
1321
+ const themeModeControl = event.target.closest(selectors.themeMode);
1322
+
1323
+ if (themeModeControl) {
1324
+ applyThemeMode(themeModeControl.getAttribute("data-inc-theme-mode"));
1325
+ return;
1326
+ }
1327
+
1328
+ const themeSelect = event.target.closest(selectors.themeSelect);
1329
+
1330
+ if (themeSelect) {
1331
+ applyThemeMode(themeSelect.value);
1332
+ }
1333
+ });
1334
+
882
1335
  document.addEventListener("keydown", (event) => {
883
1336
  const menuToggle = event.target.closest(selectors.menuToggle);
884
1337
  const menu = event.target.closest(selectors.menu);
885
1338
  const tabToggle = event.target.closest(selectors.tabToggle);
1339
+ const themeModeControl = event.target.closest(selectors.themeMode);
886
1340
  const openOverlay = getTopOpenOverlay();
887
1341
 
1342
+ if (themeModeControl && themeModeControl.closest(".inc-theme-switcher__panel")) {
1343
+ if (event.key === "ArrowDown") {
1344
+ event.preventDefault();
1345
+ focusThemeSwitcherOption(themeModeControl, "next");
1346
+ return;
1347
+ }
1348
+
1349
+ if (event.key === "ArrowUp") {
1350
+ event.preventDefault();
1351
+ focusThemeSwitcherOption(themeModeControl, "previous");
1352
+ return;
1353
+ }
1354
+
1355
+ if (event.key === "Home") {
1356
+ event.preventDefault();
1357
+ focusThemeSwitcherOption(themeModeControl, "first");
1358
+ return;
1359
+ }
1360
+
1361
+ if (event.key === "End") {
1362
+ event.preventDefault();
1363
+ focusThemeSwitcherOption(themeModeControl, "last");
1364
+ return;
1365
+ }
1366
+
1367
+ if (event.key === "Escape") {
1368
+ const owningSwitcher = themeModeControl.closest("details.inc-theme-switcher");
1369
+ const switcherSummary = owningSwitcher?.querySelector("summary");
1370
+
1371
+ if (owningSwitcher instanceof HTMLDetailsElement) {
1372
+ owningSwitcher.open = false;
1373
+ }
1374
+
1375
+ if (switcherSummary instanceof HTMLElement) {
1376
+ switcherSummary.focus();
1377
+ }
1378
+
1379
+ return;
1380
+ }
1381
+ }
1382
+
888
1383
  if (menuToggle) {
889
1384
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
890
1385
  event.preventDefault();
@@ -1000,7 +1495,32 @@
1000
1495
  });
1001
1496
  }
1002
1497
 
1498
+ window.IncTheme = {
1499
+ getMode() {
1500
+ return themeState.mode;
1501
+ },
1502
+ getResolvedTheme() {
1503
+ return themeState.resolved;
1504
+ },
1505
+ setMode(mode) {
1506
+ return applyThemeMode(mode);
1507
+ },
1508
+ cycleMode() {
1509
+ return cycleThemeMode();
1510
+ },
1511
+ createSwitcher(options = {}) {
1512
+ return createThemeSwitcher(options);
1513
+ },
1514
+ mountSwitcher(target, options = {}) {
1515
+ return mountThemeSwitcher(target, options);
1516
+ },
1517
+ init() {
1518
+ return initializeTheme();
1519
+ },
1520
+ };
1521
+
1003
1522
  function initialize() {
1523
+ initializeTheme();
1004
1524
  initializeMenus();
1005
1525
  initializeCollapses();
1006
1526
  initializeTabs();