@lmfaole/basics 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +182 -33
  2. package/basic-styling/components/basic-accordion.css +65 -0
  3. package/basic-styling/components/basic-alert.css +27 -0
  4. package/basic-styling/components/basic-dialog.css +41 -0
  5. package/basic-styling/components/basic-popover.css +54 -0
  6. package/basic-styling/components/basic-summary-table.css +76 -0
  7. package/basic-styling/components/basic-table.css +48 -0
  8. package/basic-styling/components/basic-tabs.css +45 -0
  9. package/basic-styling/components/basic-toast.css +102 -0
  10. package/basic-styling/components/basic-toc.css +30 -0
  11. package/basic-styling/components.css +9 -0
  12. package/basic-styling/global.css +61 -0
  13. package/basic-styling/index.css +2 -0
  14. package/basic-styling/tokens/base.css +19 -0
  15. package/basic-styling/tokens/palette.css +117 -0
  16. package/basic-styling/tokens/palette.tokens.json +1019 -0
  17. package/components/basic-accordion/index.d.ts +5 -5
  18. package/components/basic-accordion/index.js +169 -165
  19. package/components/basic-alert/index.d.ts +53 -0
  20. package/components/basic-alert/index.js +189 -0
  21. package/components/basic-alert/register.d.ts +1 -0
  22. package/components/basic-alert/register.js +3 -0
  23. package/components/basic-summary-table/index.js +188 -42
  24. package/components/basic-table/index.js +203 -145
  25. package/components/basic-toast/index.d.ts +65 -0
  26. package/components/basic-toast/index.js +429 -0
  27. package/components/basic-toast/register.d.ts +1 -0
  28. package/components/basic-toast/register.js +3 -0
  29. package/index.d.ts +2 -0
  30. package/index.js +2 -0
  31. package/package.json +22 -57
@@ -0,0 +1,189 @@
1
+ const ElementBase = globalThis.Element ?? class {};
2
+ const HTMLElementBase = globalThis.HTMLElement ?? class {};
3
+ const HTMLButtonElementBase = globalThis.HTMLButtonElement ?? class {};
4
+
5
+ export const ALERT_TAG_NAME = "basic-alert";
6
+
7
+ const DEFAULT_LABEL = "Alert";
8
+ const DEFAULT_LIVE = "assertive";
9
+ const TITLE_SELECTOR = "[data-alert-title]";
10
+ const CLOSE_SELECTOR = "[data-alert-close]";
11
+ const MANAGED_LABEL_ATTRIBUTE = "data-basic-alert-managed-label";
12
+ const MANAGED_LABELLEDBY_ATTRIBUTE = "data-basic-alert-managed-labelledby";
13
+
14
+ let nextAlertInstanceId = 1;
15
+
16
+ function collectOwnedElements(root, scope, selector) {
17
+ return Array.from(scope.querySelectorAll(selector)).filter(
18
+ (element) => element instanceof HTMLElementBase && element.closest(ALERT_TAG_NAME) === root,
19
+ );
20
+ }
21
+
22
+ export function normalizeAlertLabel(value) {
23
+ return value?.trim() || DEFAULT_LABEL;
24
+ }
25
+
26
+ export function normalizeAlertLive(value) {
27
+ const normalized = value?.trim().toLowerCase();
28
+ return normalized === "polite" ? "polite" : DEFAULT_LIVE;
29
+ }
30
+
31
+ export function getAlertRoleForLive(value) {
32
+ return normalizeAlertLive(value) === "polite" ? "status" : "alert";
33
+ }
34
+
35
+ export function normalizeAlertOpen(value, hidden = false) {
36
+ if (hidden) {
37
+ return false;
38
+ }
39
+
40
+ if (value == null) {
41
+ return true;
42
+ }
43
+
44
+ const normalized = value.trim().toLowerCase();
45
+ return normalized === "" || normalized === "true" || normalized === "1";
46
+ }
47
+
48
+ export class AlertElement extends HTMLElementBase {
49
+ static observedAttributes = ["data-label", "data-live", "data-open", "hidden"];
50
+
51
+ #instanceId = `${ALERT_TAG_NAME}-${nextAlertInstanceId++}`;
52
+ #title = null;
53
+ #closeButtons = [];
54
+ #eventsBound = false;
55
+
56
+ connectedCallback() {
57
+ if (!this.#eventsBound) {
58
+ this.addEventListener("click", this.#handleClick);
59
+ this.#eventsBound = true;
60
+ }
61
+
62
+ this.#sync();
63
+ }
64
+
65
+ disconnectedCallback() {
66
+ if (!this.#eventsBound) {
67
+ return;
68
+ }
69
+
70
+ this.removeEventListener("click", this.#handleClick);
71
+ this.#eventsBound = false;
72
+ }
73
+
74
+ attributeChangedCallback() {
75
+ this.#sync();
76
+ }
77
+
78
+ show() {
79
+ this.hidden = false;
80
+ this.toggleAttribute("data-open", true);
81
+ this.#sync();
82
+ return true;
83
+ }
84
+
85
+ hide() {
86
+ this.hidden = true;
87
+ this.toggleAttribute("data-open", false);
88
+ this.#sync();
89
+ return true;
90
+ }
91
+
92
+ #handleClick = (event) => {
93
+ if (!(event.target instanceof ElementBase)) {
94
+ return;
95
+ }
96
+
97
+ const closeButton = event.target.closest(CLOSE_SELECTOR);
98
+
99
+ if (
100
+ closeButton instanceof HTMLElementBase
101
+ && closeButton.closest(ALERT_TAG_NAME) === this
102
+ ) {
103
+ event.preventDefault();
104
+ this.hide();
105
+ }
106
+ };
107
+
108
+ #sync() {
109
+ const nextTitle = collectOwnedElements(this, this, TITLE_SELECTOR)[0] ?? null;
110
+
111
+ this.#title = nextTitle instanceof HTMLElementBase ? nextTitle : null;
112
+ this.#closeButtons = collectOwnedElements(this, this, CLOSE_SELECTOR);
113
+ this.#applyState();
114
+ }
115
+
116
+ #applyState() {
117
+ for (const button of this.#closeButtons) {
118
+ if (button instanceof HTMLButtonElementBase && !button.hasAttribute("type")) {
119
+ button.type = "button";
120
+ }
121
+ }
122
+
123
+ const open = normalizeAlertOpen(this.getAttribute("data-open"), this.hidden);
124
+ const baseId = this.id || this.#instanceId;
125
+
126
+ if (this.#title instanceof HTMLElementBase && !this.#title.id) {
127
+ this.#title.id = `${baseId}-title`;
128
+ }
129
+
130
+ this.hidden = !open;
131
+ this.toggleAttribute("data-open", open);
132
+ this.setAttribute("role", getAlertRoleForLive(this.getAttribute("data-live")));
133
+ this.setAttribute("aria-live", normalizeAlertLive(this.getAttribute("data-live")));
134
+ this.setAttribute("aria-atomic", "true");
135
+ this.#syncAccessibleLabel();
136
+ }
137
+
138
+ #syncAccessibleLabel() {
139
+ const nextLabel = normalizeAlertLabel(this.getAttribute("data-label"));
140
+ const hasManagedLabel = this.hasAttribute(MANAGED_LABEL_ATTRIBUTE);
141
+ const hasManagedLabelledBy = this.hasAttribute(MANAGED_LABELLEDBY_ATTRIBUTE);
142
+
143
+ if (hasManagedLabel && this.getAttribute("aria-label") !== nextLabel) {
144
+ this.removeAttribute("aria-label");
145
+ this.removeAttribute(MANAGED_LABEL_ATTRIBUTE);
146
+ }
147
+
148
+ if (this.#title?.id) {
149
+ if (this.hasAttribute(MANAGED_LABEL_ATTRIBUTE)) {
150
+ this.removeAttribute("aria-label");
151
+ this.removeAttribute(MANAGED_LABEL_ATTRIBUTE);
152
+ }
153
+
154
+ if (!this.hasAttribute("aria-labelledby") || hasManagedLabelledBy) {
155
+ this.setAttribute("aria-labelledby", this.#title.id);
156
+ this.setAttribute(MANAGED_LABELLEDBY_ATTRIBUTE, "");
157
+ }
158
+
159
+ return;
160
+ }
161
+
162
+ if (hasManagedLabelledBy) {
163
+ this.removeAttribute("aria-labelledby");
164
+ this.removeAttribute(MANAGED_LABELLEDBY_ATTRIBUTE);
165
+ }
166
+
167
+ const hasOwnAriaLabel = this.hasAttribute("aria-label") && !this.hasAttribute(MANAGED_LABEL_ATTRIBUTE);
168
+ const hasOwnLabelledBy = this.hasAttribute("aria-labelledby");
169
+
170
+ if (hasOwnAriaLabel || hasOwnLabelledBy) {
171
+ return;
172
+ }
173
+
174
+ this.setAttribute("aria-label", nextLabel);
175
+ this.setAttribute(MANAGED_LABEL_ATTRIBUTE, "");
176
+ }
177
+ }
178
+
179
+ export function defineAlert(registry = globalThis.customElements) {
180
+ if (!registry?.get || !registry?.define) {
181
+ return AlertElement;
182
+ }
183
+
184
+ if (!registry.get(ALERT_TAG_NAME)) {
185
+ registry.define(ALERT_TAG_NAME, AlertElement);
186
+ }
187
+
188
+ return AlertElement;
189
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { defineAlert } from "./index.js";
2
+
3
+ defineAlert();
@@ -12,6 +12,11 @@ const DEFAULT_TOTAL_LABEL = "Totalt";
12
12
  const GENERATED_SUMMARY_ROW_ATTRIBUTE = "data-basic-summary-table-generated-row";
13
13
  const GENERATED_SUMMARY_CELL_ATTRIBUTE = "data-basic-summary-table-generated-cell";
14
14
  const GENERATED_SUMMARY_LABEL_ATTRIBUTE = "data-basic-summary-table-generated-label";
15
+ const SUMMARY_OBSERVER_OPTIONS = {
16
+ subtree: true,
17
+ attributes: true,
18
+ attributeFilter: ["data-value"],
19
+ };
15
20
 
16
21
  export function normalizeSummaryColumns(value) {
17
22
  if (!value?.trim()) {
@@ -36,8 +41,30 @@ export function normalizeSummaryLocale(value) {
36
41
  return value?.trim() || undefined;
37
42
  }
38
43
 
44
+ function findSummaryNumberMatch(value) {
45
+ const text = String(value ?? "").trim();
46
+
47
+ if (!text) {
48
+ return null;
49
+ }
50
+
51
+ const match = text.match(/(?:-\s*)?\d(?:[\d\s.,]*\d)?/);
52
+
53
+ if (!match || typeof match.index !== "number") {
54
+ return null;
55
+ }
56
+
57
+ return {
58
+ text,
59
+ numberText: match[0],
60
+ start: match.index,
61
+ end: match.index + match[0].length,
62
+ };
63
+ }
64
+
39
65
  function getDecimalSeparator(value) {
40
- const text = value.trim();
66
+ const numberMatch = findSummaryNumberMatch(value);
67
+ const text = numberMatch?.numberText.trim() ?? String(value ?? "").trim();
41
68
  const lastComma = text.lastIndexOf(",");
42
69
  const lastDot = text.lastIndexOf(".");
43
70
 
@@ -59,7 +86,8 @@ function getDecimalSeparator(value) {
59
86
  }
60
87
 
61
88
  function countFractionDigits(value) {
62
- const text = String(value ?? "").trim();
89
+ const numberMatch = findSummaryNumberMatch(value);
90
+ const text = numberMatch?.numberText.trim() ?? String(value ?? "").trim();
63
91
 
64
92
  if (!text) {
65
93
  return 0;
@@ -75,7 +103,8 @@ function countFractionDigits(value) {
75
103
  }
76
104
 
77
105
  export function parseSummaryNumber(value) {
78
- const text = String(value ?? "").trim();
106
+ const numberMatch = findSummaryNumberMatch(value);
107
+ const text = numberMatch?.numberText.trim() ?? String(value ?? "").trim();
79
108
 
80
109
  if (!text) {
81
110
  return null;
@@ -107,6 +136,28 @@ export function formatSummaryNumber(value, { locale, fractionDigits = 0 } = {})
107
136
  }).format(value);
108
137
  }
109
138
 
139
+ function getSummaryAffixes(value) {
140
+ const numberMatch = findSummaryNumberMatch(value);
141
+
142
+ if (!numberMatch) {
143
+ return null;
144
+ }
145
+
146
+ return {
147
+ prefix: numberMatch.text.slice(0, numberMatch.start),
148
+ suffix: numberMatch.text.slice(numberMatch.end),
149
+ };
150
+ }
151
+
152
+ function formatSummaryValue(value, {
153
+ locale,
154
+ fractionDigits = 0,
155
+ prefix = "",
156
+ suffix = "",
157
+ } = {}) {
158
+ return `${prefix}${formatSummaryNumber(value, { locale, fractionDigits })}${suffix}`;
159
+ }
160
+
110
161
  function findCellAtColumnIndex(row, targetColumnIndex) {
111
162
  let columnIndex = 0;
112
163
 
@@ -134,6 +185,10 @@ function getLogicalColumnCount(table) {
134
185
  let maxColumns = 0;
135
186
 
136
187
  for (const row of Array.from(table.rows)) {
188
+ if (row.hasAttribute(GENERATED_SUMMARY_ROW_ATTRIBUTE)) {
189
+ continue;
190
+ }
191
+
137
192
  let columnCount = 0;
138
193
 
139
194
  for (const cell of Array.from(row.cells)) {
@@ -192,6 +247,10 @@ function calculateSummaryTotals(table, summaryColumns) {
192
247
  totals.set(columnIndex, {
193
248
  total: 0,
194
249
  fractionDigits: 0,
250
+ prefix: "",
251
+ suffix: "",
252
+ affixInitialized: false,
253
+ affixConsistent: true,
195
254
  });
196
255
  }
197
256
 
@@ -213,6 +272,25 @@ function calculateSummaryTotals(table, summaryColumns) {
213
272
 
214
273
  current.total += parsedValue;
215
274
  current.fractionDigits = Math.max(current.fractionDigits, countFractionDigits(rawValue));
275
+
276
+ const affixes = getSummaryAffixes(cell?.textContent ?? "");
277
+
278
+ if (!affixes) {
279
+ continue;
280
+ }
281
+
282
+ if (!current.affixInitialized) {
283
+ current.prefix = affixes.prefix;
284
+ current.suffix = affixes.suffix;
285
+ current.affixInitialized = true;
286
+ continue;
287
+ }
288
+
289
+ if (current.prefix !== affixes.prefix || current.suffix !== affixes.suffix) {
290
+ current.affixConsistent = false;
291
+ current.prefix = "";
292
+ current.suffix = "";
293
+ }
216
294
  }
217
295
  }
218
296
 
@@ -238,6 +316,31 @@ function ensureGeneratedSummaryRow(table) {
238
316
  return row;
239
317
  }
240
318
 
319
+ function ensureSummaryRowCell(row, columnIndex, tagName) {
320
+ const expectedTagName = tagName.toUpperCase();
321
+ const existingCell = row.cells[columnIndex] ?? null;
322
+
323
+ if (existingCell?.tagName === expectedTagName) {
324
+ return existingCell;
325
+ }
326
+
327
+ const replacement = document.createElement(tagName);
328
+
329
+ if (existingCell) {
330
+ row.replaceChild(replacement, existingCell);
331
+ return replacement;
332
+ }
333
+
334
+ row.append(replacement);
335
+ return replacement;
336
+ }
337
+
338
+ function syncSummaryCellText(cell, text) {
339
+ if (cell.textContent !== text) {
340
+ cell.textContent = text;
341
+ }
342
+ }
343
+
241
344
  function removeGeneratedSummaryRow(table) {
242
345
  table.querySelector(`tr[${GENERATED_SUMMARY_ROW_ATTRIBUTE}]`)?.remove();
243
346
  }
@@ -259,34 +362,58 @@ function syncSummaryFooter(table, {
259
362
  const summaryColumnSet = new Set(summaryColumns.filter((column) => column < logicalColumnCount));
260
363
  const row = ensureGeneratedSummaryRow(table);
261
364
 
262
- row.replaceChildren();
263
-
264
365
  for (let columnIndex = 0; columnIndex < logicalColumnCount; columnIndex += 1) {
265
366
  if (columnIndex === effectiveLabelColumnIndex) {
266
- const labelCell = document.createElement("th");
267
- labelCell.scope = "row";
268
- labelCell.textContent = totalLabel;
367
+ const labelCell = ensureSummaryRowCell(row, columnIndex, "th");
368
+
369
+ if (labelCell.getAttribute("scope") !== "row") {
370
+ labelCell.setAttribute("scope", "row");
371
+ }
372
+
269
373
  labelCell.setAttribute(GENERATED_SUMMARY_LABEL_ATTRIBUTE, "");
270
- row.append(labelCell);
374
+ labelCell.removeAttribute(GENERATED_SUMMARY_CELL_ATTRIBUTE);
375
+ labelCell.removeAttribute("data-summary-total");
376
+ labelCell.removeAttribute("data-summary-empty");
377
+ delete labelCell.dataset.value;
378
+ syncSummaryCellText(labelCell, totalLabel);
271
379
  continue;
272
380
  }
273
381
 
274
- const valueCell = document.createElement("td");
382
+ const valueCell = ensureSummaryRowCell(row, columnIndex, "td");
275
383
  valueCell.setAttribute(GENERATED_SUMMARY_CELL_ATTRIBUTE, "");
384
+ valueCell.removeAttribute(GENERATED_SUMMARY_LABEL_ATTRIBUTE);
385
+ valueCell.removeAttribute("scope");
276
386
 
277
387
  if (summaryColumnSet.has(columnIndex)) {
278
- const summary = totals.get(columnIndex) ?? { total: 0, fractionDigits: 0 };
279
- valueCell.textContent = formatSummaryNumber(summary.total, {
388
+ const summary = totals.get(columnIndex) ?? {
389
+ total: 0,
390
+ fractionDigits: 0,
391
+ prefix: "",
392
+ suffix: "",
393
+ affixInitialized: false,
394
+ affixConsistent: true,
395
+ };
396
+ const formattedValue = formatSummaryValue(summary.total, {
280
397
  locale,
281
398
  fractionDigits: summary.fractionDigits,
399
+ prefix: summary.affixConsistent ? summary.prefix : "",
400
+ suffix: summary.affixConsistent ? summary.suffix : "",
282
401
  });
402
+
403
+ syncSummaryCellText(valueCell, formattedValue);
283
404
  valueCell.dataset.value = String(summary.total);
284
405
  valueCell.dataset.summaryTotal = "";
406
+ valueCell.removeAttribute("data-summary-empty");
285
407
  } else {
408
+ syncSummaryCellText(valueCell, "");
409
+ delete valueCell.dataset.value;
410
+ valueCell.removeAttribute("data-summary-total");
286
411
  valueCell.dataset.summaryEmpty = "";
287
412
  }
413
+ }
288
414
 
289
- row.append(valueCell);
415
+ while (row.cells.length > logicalColumnCount) {
416
+ row.deleteCell(-1);
290
417
  }
291
418
 
292
419
  return true;
@@ -301,6 +428,7 @@ export class SummaryTableElement extends TableElement {
301
428
  ];
302
429
 
303
430
  #summaryObserver = null;
431
+ #summaryObserving = false;
304
432
  #scheduledFrame = 0;
305
433
 
306
434
  connectedCallback() {
@@ -310,7 +438,7 @@ export class SummaryTableElement extends TableElement {
310
438
 
311
439
  disconnectedCallback() {
312
440
  super.disconnectedCallback();
313
- this.#summaryObserver?.disconnect();
441
+ this.#stopSummaryObserving();
314
442
  this.#summaryObserver = null;
315
443
 
316
444
  if (this.#scheduledFrame !== 0 && typeof window !== "undefined") {
@@ -320,33 +448,37 @@ export class SummaryTableElement extends TableElement {
320
448
  }
321
449
 
322
450
  refresh() {
323
- super.refresh();
451
+ this.#stopSummaryObserving();
324
452
 
325
- const table = getTable(this);
453
+ try {
454
+ const table = getTable(this);
326
455
 
327
- if (!(table instanceof HTMLTableElementBase)) {
328
- return;
329
- }
456
+ if (!(table instanceof HTMLTableElementBase)) {
457
+ return;
458
+ }
330
459
 
331
- const labelColumnIndex = normalizeTableRowHeaderColumn(
332
- this.getAttribute("data-row-header-column"),
333
- ) - 1;
334
- const summaryColumns = collectSummaryColumns(
335
- table,
336
- normalizeSummaryColumns(this.getAttribute("data-summary-columns")),
337
- labelColumnIndex,
338
- );
339
- const totals = calculateSummaryTotals(table, summaryColumns);
340
-
341
- syncSummaryFooter(table, {
342
- labelColumnIndex,
343
- locale: normalizeSummaryLocale(this.getAttribute("data-locale")),
344
- summaryColumns,
345
- totalLabel: normalizeSummaryTotalLabel(this.getAttribute("data-total-label")),
346
- totals,
347
- });
460
+ const labelColumnIndex = normalizeTableRowHeaderColumn(
461
+ this.getAttribute("data-row-header-column"),
462
+ ) - 1;
463
+ const summaryColumns = collectSummaryColumns(
464
+ table,
465
+ normalizeSummaryColumns(this.getAttribute("data-summary-columns")),
466
+ labelColumnIndex,
467
+ );
468
+ const totals = calculateSummaryTotals(table, summaryColumns);
469
+
470
+ syncSummaryFooter(table, {
471
+ labelColumnIndex,
472
+ locale: normalizeSummaryLocale(this.getAttribute("data-locale")),
473
+ summaryColumns,
474
+ totalLabel: normalizeSummaryTotalLabel(this.getAttribute("data-total-label")),
475
+ totals,
476
+ });
348
477
 
349
- super.refresh();
478
+ super.refresh();
479
+ } finally {
480
+ this.#startSummaryObserving();
481
+ }
350
482
  }
351
483
 
352
484
  #scheduleRefresh() {
@@ -369,11 +501,25 @@ export class SummaryTableElement extends TableElement {
369
501
  this.#scheduleRefresh();
370
502
  });
371
503
 
372
- this.#summaryObserver.observe(this, {
373
- subtree: true,
374
- attributes: true,
375
- attributeFilter: ["data-value"],
376
- });
504
+ this.#startSummaryObserving();
505
+ }
506
+
507
+ #startSummaryObserving() {
508
+ if (!this.#summaryObserver || this.#summaryObserving) {
509
+ return;
510
+ }
511
+
512
+ this.#summaryObserver.observe(this, SUMMARY_OBSERVER_OPTIONS);
513
+ this.#summaryObserving = true;
514
+ }
515
+
516
+ #stopSummaryObserving() {
517
+ if (!this.#summaryObserver || !this.#summaryObserving) {
518
+ return;
519
+ }
520
+
521
+ this.#summaryObserver.disconnect();
522
+ this.#summaryObserving = false;
377
523
  }
378
524
  }
379
525