@aiaiai-pt/design-system 0.8.1 → 0.8.3

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.
@@ -28,6 +28,13 @@
28
28
  @example Loading
29
29
  <DataTable {columns} rows={[]} loading />
30
30
 
31
+ @example Responsive (default)
32
+ Tables card-stack below `card_breakpoint` (default 768px) so columns
33
+ don't scroll off-screen on phones/tablets. The first column becomes the
34
+ card title; the rest render as label:value rows. Opt out with
35
+ `responsive={false}`, or raise the breakpoint for wide tables.
36
+ <DataTable {columns} {rows} card_breakpoint={1024} />
37
+
31
38
  @example Custom cell rendering (badges, chips, components)
32
39
  Pass a `cell` snippet to override the default per-cell text rendering.
33
40
  The default behavior (using `column.render` to produce a string) is
@@ -70,6 +77,21 @@
70
77
  selected_rows = $bindable(new Set()),
71
78
  /** @type {string} */
72
79
  row_key = 'id',
80
+ /**
81
+ * Responsive card mode. When true (the default), the table restyles
82
+ * into a stack of cards below `card_breakpoint` so its columns don't
83
+ * scroll off-screen on narrow viewports. Set false to keep the table
84
+ * layout at every width (e.g. tables that are already narrow).
85
+ * @type {boolean}
86
+ */
87
+ responsive = true,
88
+ /**
89
+ * Viewport width (px) at/below which `responsive` switches to cards.
90
+ * A prop rather than a fixed media query so consumers with wider
91
+ * tables (many columns) can raise it.
92
+ * @type {number}
93
+ */
94
+ card_breakpoint = 768,
73
95
  /** @type {string} */
74
96
  empty_heading = 'No data',
75
97
  /** @type {string} */
@@ -106,6 +128,22 @@
106
128
 
107
129
  const SKELETON_ROWS = 5;
108
130
 
131
+ // Responsive card mode is toggled by a CLASS driven by matchMedia, not
132
+ // a static @media query — that keeps `card_breakpoint` a runtime prop
133
+ // while still rendering a SINGLE table DOM (the cells are restyled, not
134
+ // duplicated). SSR renders the table (is_card=false); the effect flips
135
+ // it after mount, so the initial client render matches SSR (no
136
+ // hydration mismatch) and narrow viewports get a CSS-only restyle.
137
+ let is_card = $state(false);
138
+ $effect(() => {
139
+ if (!responsive || typeof window === 'undefined' || !window.matchMedia) return;
140
+ const mq = window.matchMedia(`(max-width: ${card_breakpoint}px)`);
141
+ const sync = () => (is_card = mq.matches);
142
+ sync();
143
+ mq.addEventListener('change', sync);
144
+ return () => mq.removeEventListener('change', sync);
145
+ });
146
+
109
147
  const all_selected = $derived(
110
148
  rows.length > 0 &&
111
149
  rows.every((row) => selected_rows.has(String(row[row_key])))
@@ -176,7 +214,7 @@
176
214
  }
177
215
  </script>
178
216
 
179
- <div class="table-wrap {className}" {...rest}>
217
+ <div class="table-wrap {className}" class:is-card={responsive && is_card} {...rest}>
180
218
  {#if children}
181
219
  <div class="table-toolbar">
182
220
  {@render children()}
@@ -288,8 +326,12 @@
288
326
  />
289
327
  </td>
290
328
  {/if}
291
- {#each columns as col}
292
- <td class="table-td">
329
+ {#each columns as col, col_index}
330
+ <td
331
+ class="table-td"
332
+ class:table-td-card-title={col_index === 0}
333
+ data-label={col.label}
334
+ >
293
335
  {#if cell}
294
336
  {@render cell({ row, column: col, value: row[col.key] })}
295
337
  {:else}
@@ -453,4 +495,83 @@
453
495
  cursor: pointer;
454
496
  display: block;
455
497
  }
498
+
499
+ /* ─── Responsive card mode ───
500
+ Toggled by `.is-card` (set via matchMedia against `card_breakpoint`,
501
+ not a static @media query, so the breakpoint stays a prop). The SAME
502
+ table is restyled into a stack of cards — one DOM, no duplicate text
503
+ nodes — so getByText / the a11y tree see a single set of cells. Each
504
+ row becomes a card; each cell a `label : value` line whose label is
505
+ the column header surfaced via `data-label`; the first column renders
506
+ as the card title. */
507
+ .table-wrap.is-card {
508
+ border: none;
509
+ border-radius: 0;
510
+ overflow: visible;
511
+ }
512
+
513
+ .table-wrap.is-card .table-scroll {
514
+ overflow: visible;
515
+ }
516
+
517
+ .table-wrap.is-card .table,
518
+ .table-wrap.is-card .table-body {
519
+ display: block;
520
+ }
521
+
522
+ .table-wrap.is-card .table-head {
523
+ display: none;
524
+ }
525
+
526
+ .table-wrap.is-card .table-row {
527
+ display: block;
528
+ padding: var(--space-md);
529
+ border: var(--elevation-border);
530
+ border-radius: var(--radius-md);
531
+ background: var(--color-surface);
532
+ margin-bottom: var(--space-sm);
533
+ }
534
+
535
+ /* Cards are uniform — drop the table's zebra striping. */
536
+ .table-wrap.is-card .table-row-even {
537
+ background: var(--color-surface);
538
+ }
539
+
540
+ .table-wrap.is-card .table-td {
541
+ display: flex;
542
+ align-items: center;
543
+ justify-content: space-between;
544
+ gap: var(--space-md);
545
+ padding: var(--space-2xs) 0;
546
+ border-bottom: none;
547
+ }
548
+
549
+ .table-wrap.is-card .table-td::before {
550
+ content: attr(data-label);
551
+ flex-shrink: 0;
552
+ font-family: var(--type-label-font);
553
+ font-size: var(--type-label-size);
554
+ letter-spacing: var(--type-label-tracking);
555
+ color: var(--color-text-secondary);
556
+ }
557
+
558
+ /* First column → full-width card heading, no label prefix. */
559
+ .table-wrap.is-card .table-td-card-title {
560
+ display: block;
561
+ padding: 0 0 var(--space-2xs);
562
+ font-weight: var(--raw-font-weight-medium, 500);
563
+ }
564
+
565
+ .table-wrap.is-card .table-td-card-title::before {
566
+ content: none;
567
+ }
568
+
569
+ /* Selection checkbox keeps its own line; no empty label pseudo. */
570
+ .table-wrap.is-card .table-td-check {
571
+ justify-content: flex-start;
572
+ }
573
+
574
+ .table-wrap.is-card .table-td-check::before {
575
+ content: none;
576
+ }
456
577
  </style>
@@ -44,6 +44,8 @@ declare const DataTable: import("svelte").Component<
44
44
  selectable?: boolean;
45
45
  selected_rows?: Set<string>;
46
46
  row_key?: string;
47
+ responsive?: boolean;
48
+ card_breakpoint?: number;
47
49
  empty_heading?: string;
48
50
  empty_body?: string;
49
51
  on_sort?: any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -97,11 +97,18 @@
97
97
  --type-body-sm-tracking: var(--raw-tracking-micro);
98
98
  --type-body-sm-font: var(--font-sans);
99
99
 
100
+ /* Form labels (#63 typography). Was uppercase-monospace-light-wider —
101
+ the dev-tool look the action-editor walk flagged as the
102
+ highest-leverage single change. Now sentence-case body sans, medium
103
+ weight, normal tracking. Monospace is reserved for code tokens
104
+ (keys, layout codes, template placeholders), NOT labels. Consumers
105
+ should pass labels as `"Field name"`, not `"FIELD NAME"`. See
106
+ dev_docs/specs/typography-form-labels.md (ADR). */
100
107
  --type-label-size: var(--raw-font-size-12);
101
- --type-label-weight: var(--raw-font-weight-regular);
108
+ --type-label-weight: var(--raw-font-weight-medium);
102
109
  --type-label-leading: var(--raw-line-height-heading);
103
- --type-label-tracking: var(--raw-tracking-wider);
104
- --type-label-font: var(--font-mono);
110
+ --type-label-tracking: var(--raw-tracking-normal);
111
+ --type-label-font: var(--font-sans);
105
112
 
106
113
  --type-data-size: var(--raw-font-size-14);
107
114
  --type-data-weight: var(--raw-font-weight-regular);