@lmfaole/basics 0.3.0 → 0.5.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -350
  3. package/basic-components/basic-accordion/README.md +53 -0
  4. package/{components → basic-components}/basic-accordion/index.d.ts +5 -5
  5. package/basic-components/basic-accordion/index.js +413 -0
  6. package/basic-components/basic-alert/README.md +48 -0
  7. package/basic-components/basic-alert/index.d.ts +53 -0
  8. package/basic-components/basic-alert/index.js +189 -0
  9. package/basic-components/basic-alert/register.js +3 -0
  10. package/basic-components/basic-carousel/README.md +108 -0
  11. package/basic-components/basic-carousel/index.d.ts +73 -0
  12. package/basic-components/basic-carousel/index.js +255 -0
  13. package/basic-components/basic-carousel/register.js +3 -0
  14. package/basic-components/basic-dialog/README.md +57 -0
  15. package/basic-components/basic-popover/README.md +56 -0
  16. package/basic-components/basic-summary-table/README.md +93 -0
  17. package/{components → basic-components}/basic-summary-table/index.js +188 -42
  18. package/basic-components/basic-table/README.md +89 -0
  19. package/{components → basic-components}/basic-table/index.js +203 -145
  20. package/basic-components/basic-tabs/README.md +63 -0
  21. package/basic-components/basic-tabs/register.d.ts +1 -0
  22. package/basic-components/basic-toast/README.md +62 -0
  23. package/basic-components/basic-toast/index.d.ts +68 -0
  24. package/basic-components/basic-toast/index.js +690 -0
  25. package/basic-components/basic-toast/register.d.ts +1 -0
  26. package/basic-components/basic-toast/register.js +3 -0
  27. package/basic-components/basic-toc/README.md +43 -0
  28. package/basic-components/basic-toc/register.d.ts +1 -0
  29. package/basic-styling/components/basic-accordion.css +99 -0
  30. package/basic-styling/components/basic-alert.css +27 -0
  31. package/basic-styling/components/basic-carousel.css +183 -0
  32. package/basic-styling/components/basic-dialog.css +41 -0
  33. package/basic-styling/components/basic-popover.css +52 -0
  34. package/basic-styling/components/basic-summary-table.css +98 -0
  35. package/basic-styling/components/basic-table.css +66 -0
  36. package/basic-styling/components/basic-tabs.css +61 -0
  37. package/basic-styling/components/basic-toast.css +102 -0
  38. package/basic-styling/components/basic-toc.css +30 -0
  39. package/basic-styling/components.css +11 -0
  40. package/basic-styling/forms.css +55 -0
  41. package/basic-styling/global.css +62 -0
  42. package/basic-styling/index.css +2 -0
  43. package/basic-styling/interaction.css +90 -0
  44. package/basic-styling/tokens/base.css +19 -0
  45. package/basic-styling/tokens/palette.css +229 -0
  46. package/basic-styling/tokens/palette.tokens.json +1787 -0
  47. package/index.d.ts +10 -7
  48. package/index.js +10 -7
  49. package/package.json +61 -76
  50. package/components/basic-accordion/index.js +0 -387
  51. package/readme.mdx +0 -6
  52. /package/{components → basic-components}/basic-accordion/register.d.ts +0 -0
  53. /package/{components → basic-components}/basic-accordion/register.js +0 -0
  54. /package/{components/basic-dialog → basic-components/basic-alert}/register.d.ts +0 -0
  55. /package/{components/basic-popover → basic-components/basic-carousel}/register.d.ts +0 -0
  56. /package/{components → basic-components}/basic-dialog/index.d.ts +0 -0
  57. /package/{components → basic-components}/basic-dialog/index.js +0 -0
  58. /package/{components/basic-summary-table → basic-components/basic-dialog}/register.d.ts +0 -0
  59. /package/{components → basic-components}/basic-dialog/register.js +0 -0
  60. /package/{components → basic-components}/basic-popover/index.d.ts +0 -0
  61. /package/{components → basic-components}/basic-popover/index.js +0 -0
  62. /package/{components/basic-table → basic-components/basic-popover}/register.d.ts +0 -0
  63. /package/{components → basic-components}/basic-popover/register.js +0 -0
  64. /package/{components → basic-components}/basic-summary-table/index.d.ts +0 -0
  65. /package/{components/basic-tabs → basic-components/basic-summary-table}/register.d.ts +0 -0
  66. /package/{components → basic-components}/basic-summary-table/register.js +0 -0
  67. /package/{components → basic-components}/basic-table/index.d.ts +0 -0
  68. /package/{components/basic-toc → basic-components/basic-table}/register.d.ts +0 -0
  69. /package/{components → basic-components}/basic-table/register.js +0 -0
  70. /package/{components → basic-components}/basic-tabs/index.d.ts +0 -0
  71. /package/{components → basic-components}/basic-tabs/index.js +0 -0
  72. /package/{components → basic-components}/basic-tabs/register.js +0 -0
  73. /package/{components → basic-components}/basic-toc/index.d.ts +0 -0
  74. /package/{components → basic-components}/basic-toc/index.js +0 -0
  75. /package/{components → basic-components}/basic-toc/register.js +0 -0
@@ -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
 
@@ -0,0 +1,89 @@
1
+ # `basic-table`
2
+
3
+ Accessible naming and header relationships for regular tables.
4
+
5
+ ## Register
6
+
7
+ ```js
8
+ import "@lmfaole/basics/basic-components/basic-table/register";
9
+ ```
10
+
11
+ ## Example
12
+
13
+ ```html
14
+ <basic-table
15
+ data-caption="Bemanning per sprint"
16
+ data-description="Viser team, lokasjon og ledig kapasitet per sprint."
17
+ data-row-headers
18
+ data-row-header-column="2"
19
+ >
20
+ <table>
21
+ <thead>
22
+ <tr>
23
+ <th>Statuskode</th>
24
+ <th>Team</th>
25
+ <th>Lokasjon</th>
26
+ <th>Sprint</th>
27
+ <th>Ledige timer</th>
28
+ </tr>
29
+ </thead>
30
+ <tbody>
31
+ <tr>
32
+ <td>A1</td>
33
+ <td>Plattform</td>
34
+ <td>Oslo</td>
35
+ <td>14</td>
36
+ <td>18</td>
37
+ </tr>
38
+ <tr>
39
+ <td>B4</td>
40
+ <td>Designsystem</td>
41
+ <td>Trondheim</td>
42
+ <td>14</td>
43
+ <td>10</td>
44
+ </tr>
45
+ <tr>
46
+ <td>C2</td>
47
+ <td>Innsikt</td>
48
+ <td>Bergen</td>
49
+ <td>15</td>
50
+ <td>26</td>
51
+ </tr>
52
+ </tbody>
53
+ </table>
54
+ </basic-table>
55
+ ```
56
+
57
+ ## Props
58
+
59
+ | Prop | Description | Type | Default | Options |
60
+ | --- | --- | --- | --- | --- |
61
+ | `data-caption` | Generates a visible `<caption>` when the table does not already define one. | string | none | any string |
62
+ | `data-column-headers` | Promotes the first row to column headers when the author provides a plain table without a header row. | boolean attribute | off | `present`, `omitted` |
63
+ | `data-description` | Generates hidden helper text and connects it with `aria-describedby`. | string | none | any string |
64
+ | `data-label` | Fallback accessible name when the table has no caption, `aria-label`, or `aria-labelledby`. | string | `Tabell` | any string |
65
+ | `data-row-header-column` | One-based body column that should become the generated row header. | positive integer | `1` | positive integer |
66
+ | `data-row-headers` | Enables generated row headers in body rows. This is also enabled automatically when `data-row-header-column` is present. | boolean attribute | off | `present`, `omitted` |
67
+
68
+ ## Starter Styling Prop
69
+
70
+ | Prop | Description | Type | Default | Options |
71
+ | --- | --- | --- | --- | --- |
72
+ | `data-zebra` | Optional starter-CSS hook that adds alternating body-row backgrounds when you import `basic-styling`. | boolean attribute | off | `present`, `omitted` |
73
+ | `data-separators` | Optional starter-CSS hook that chooses whether interior dividers appear between rows, columns, or both when you import `basic-styling`. | enum string | `rows` | `rows`, `columns`, `both` |
74
+
75
+ ## Behavior
76
+
77
+ - Preserves author-provided captions and only generates one when needed
78
+ - Can generate hidden helper text for extra context
79
+ - Can promote a plain first row to column headers
80
+ - Assigns missing header ids and populates `headers` on data cells
81
+ - Re-runs automatically when the wrapped table changes
82
+
83
+ ## Markup Contract
84
+
85
+ - Provide one descendant `<table>`
86
+ - Use real table sections and header cells where possible
87
+ - Add `data-row-headers` when one body column identifies each row
88
+ - Add `data-column-headers` only when you want the component to promote a plain first row
89
+ - Keep layout and styling outside the package