@marianmeres/stuic 3.162.0 → 3.164.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.
@@ -4,35 +4,8 @@
4
4
  import type { PagingCalcResult } from "@marianmeres/paging-store";
5
5
  import type { THC } from "../Thc/index.js";
6
6
  import type { TranslateFn } from "../../types.js";
7
- import { isPlainObject } from "../../utils/is-plain-object.js";
8
- import { replaceMap } from "../../utils/replace-map.js";
9
-
10
- // i18n ready
11
- function t_default(
12
- k: string,
13
- values: false | null | undefined | Record<string, string | number> = null,
14
- fallback: string | boolean = "",
15
- _i18nSpanWrap: boolean = true
16
- ) {
17
- const m: Record<string, string> = {
18
- previous_page: "Prev",
19
- next_page: "Next",
20
- page_x_of_y: "Page {page} of {pageCount}",
21
- no_data: "No data",
22
- select_all_rows: "Select all rows on this page",
23
- select_row: "Select row",
24
- select_all_on_page_x: "All {count} on this page selected.",
25
- select_all_results: "Select all {totalCount} results",
26
- all_results_selected: "All {totalCount} results selected.",
27
- clear_selection: "Clear selection",
28
- };
29
- let out = m[k] ?? fallback ?? k;
30
- return isPlainObject(values)
31
- ? replaceMap(out, values as any, {
32
- preSearchKeyTransform: (k) => `{${k}}`,
33
- })
34
- : out;
35
- }
7
+ // i18n ready -- see ./i18n.ts (english, built-in) and ./i18n-sk.ts (opt-in slovak)
8
+ import { DATA_TABLE_MESSAGES_EN, t_default } from "./i18n.js";
36
9
 
37
10
  export interface DataTableColumn<T = Record<string, any>> {
38
11
  /** Property key to extract from row data (supports dot-notation: "data.name") */
@@ -95,6 +68,20 @@
95
68
  /** Set of row IDs explicitly deselected while in all-pages mode (bindable) */
96
69
  excluded?: Set<string | number>;
97
70
 
71
+ /**
72
+ * Keep the selection bar -- the strip above the table holding the idle status,
73
+ * `batchActions`, and the select-all-across-pages offer -- in the layout at all
74
+ * times, so checking the first row does not shove the table down. While nothing
75
+ * is selected it reads `t("no_rows_selected")`.
76
+ *
77
+ * Only meaningful when `selectable` is on AND there is something to reserve room
78
+ * for (a `batchActions` snippet or `allowSelectAllPages`); with neither, no bar
79
+ * is rendered at all.
80
+ *
81
+ * Set `false` for the pop-in behaviour: the bar appears only once it has content.
82
+ */
83
+ reserveBatchBar?: boolean;
84
+
98
85
  /** Callback when a row is clicked */
99
86
  onRowClick?: (row: T, index: number) => void;
100
87
 
@@ -173,7 +160,8 @@
173
160
  ]
174
161
  >;
175
162
  /**
176
- * Batch actions bar snippet (shown when items are selected).
163
+ * Content of the selection bar while something is selected. Replaces the default
164
+ * "{count} selected" status, so render your own count alongside the buttons.
177
165
  *
178
166
  * Note: in all-pages mode (`selectedAll === true`) `selectedRows` only contains
179
167
  * rows from the current page that aren't excluded. Off-page rows are not
@@ -195,8 +183,8 @@
195
183
  ]
196
184
  >;
197
185
  /**
198
- * Custom "select all results across pages" banner. When omitted, a default
199
- * banner is rendered.
186
+ * Custom "select all results across pages" offer, rendered on the trailing edge
187
+ * of the selection bar. When omitted, a default one is rendered.
200
188
  */
201
189
  selectAllBanner?: Snippet<
202
190
  [
@@ -250,6 +238,7 @@
250
238
  allowSelectAllPages = false,
251
239
  selectedAll = $bindable(false),
252
240
  excluded = $bindable(new Set()),
241
+ reserveBatchBar = true,
253
242
  onRowClick,
254
243
  rowHref,
255
244
  rowHrefColumn,
@@ -395,6 +384,30 @@
395
384
  return allOnPageSelected;
396
385
  });
397
386
 
387
+ // The default selection status, shown when there is no `batchActions` snippet to take
388
+ // its place. Both keys post-date the component, so a consumer `t` written against an
389
+ // older version does not know them -- and one that answers an unknown key with ""
390
+ // would leave the reserved bar visibly blank. Hence the explicit english fallbacks
391
+ // (pre-interpolated, since a `t` may hand a fallback straight back untouched), which
392
+ // every other, older key can do without.
393
+ let statusText = $derived(
394
+ effectiveCount > 0
395
+ ? t(
396
+ "x_rows_selected",
397
+ { count: effectiveCount },
398
+ DATA_TABLE_MESSAGES_EN.x_rows_selected.replace("{count}", `${effectiveCount}`)
399
+ )
400
+ : t("no_rows_selected", null, DATA_TABLE_MESSAGES_EN.no_rows_selected)
401
+ );
402
+
403
+ // The bar only exists if something can appear in it; `reserveBatchBar` then decides
404
+ // whether it holds its place while that something is absent.
405
+ let hasBatchBar = $derived(selectable && (!!batchActions || allowSelectAllPages));
406
+ let showBatchBar = $derived(
407
+ hasBatchBar &&
408
+ (reserveBatchBar || showSelectAllBanner || (effectiveCount > 0 && !!batchActions))
409
+ );
410
+
398
411
  // --- Row click / activation ---
399
412
 
400
413
  // A row is a big target that legitimately contains its own controls. Anything that
@@ -489,47 +502,62 @@
489
502
  {/if}
490
503
  {/snippet}
491
504
 
492
- <!-- Batch action bar -->
493
- {#if selectable && effectiveCount > 0 && batchActions}
494
- <div class={!unstyled ? "stuic-data-table-batch" : undefined}>
495
- {@render batchActions({
496
- selected,
497
- selectedRows,
498
- selectedAll,
499
- excluded,
500
- effectiveCount,
501
- totalCount,
502
- clearSelection: clearAllSelection,
503
- })}
504
- </div>
505
- {/if}
506
-
507
- <!-- Select-all-across-pages banner -->
508
- {#if showSelectAllBanner && paging}
509
- {#if selectAllBanner}
510
- {@render selectAllBanner({
511
- selectedAll,
512
- effectiveCount,
513
- totalCount: paging.total,
514
- pageCount: data.length,
515
- selectAll: enterSelectAll,
516
- clearSelection: clearAllSelection,
517
- })}
518
- {:else}
519
- <div class={!unstyled ? "stuic-data-table-select-all-banner" : undefined}>
520
- {#if selectedAll}
521
- <span>{t("all_results_selected", { totalCount: paging.total })}</span>
522
- <Button variant="ghost" size="sm" onclick={clearAllSelection}>
523
- {t("clear_selection")}
524
- </Button>
505
+ <!--
506
+ Selection bar -- one strip carrying the selection status, the consumer's
507
+ `batchActions`, and the select-all-across-pages offer. Deliberately a single element
508
+ rather than a stack of independent conditionals: each of those appearing on its own
509
+ reflows everything below it, which is exactly the jump `reserveBatchBar` exists to
510
+ prevent. Sits outside the root container so the loading overlay doesn't cover it.
511
+ -->
512
+ {#if showBatchBar}
513
+ <div
514
+ class={!unstyled ? "stuic-data-table-batch" : undefined}
515
+ data-idle={!unstyled && effectiveCount === 0 ? "true" : undefined}
516
+ data-select-all={!unstyled && showSelectAllBanner ? "true" : undefined}
517
+ >
518
+ <div class={!unstyled ? "stuic-data-table-batch-main" : undefined}>
519
+ {#if effectiveCount > 0 && batchActions}
520
+ {@render batchActions({
521
+ selected,
522
+ selectedRows,
523
+ selectedAll,
524
+ excluded,
525
+ effectiveCount,
526
+ totalCount,
527
+ clearSelection: clearAllSelection,
528
+ })}
525
529
  {:else}
526
- <span>{t("select_all_on_page_x", { count: data.length })}</span>
527
- <Button variant="ghost" size="sm" onclick={enterSelectAll}>
528
- {t("select_all_results", { totalCount: paging.total })}
529
- </Button>
530
+ <span class={!unstyled ? "stuic-data-table-batch-status" : undefined}>
531
+ {statusText}
532
+ </span>
530
533
  {/if}
531
534
  </div>
532
- {/if}
535
+
536
+ {#if showSelectAllBanner && paging}
537
+ <div class={!unstyled ? "stuic-data-table-batch-select-all" : undefined}>
538
+ {#if selectAllBanner}
539
+ {@render selectAllBanner({
540
+ selectedAll,
541
+ effectiveCount,
542
+ totalCount: paging.total,
543
+ pageCount: data.length,
544
+ selectAll: enterSelectAll,
545
+ clearSelection: clearAllSelection,
546
+ })}
547
+ {:else if selectedAll}
548
+ <span>{t("all_results_selected", { totalCount: paging.total })}</span>
549
+ <Button variant="ghost" size="sm" onclick={clearAllSelection}>
550
+ {t("clear_selection")}
551
+ </Button>
552
+ {:else}
553
+ <span>{t("select_all_on_page_x", { count: data.length })}</span>
554
+ <Button variant="ghost" size="sm" onclick={enterSelectAll}>
555
+ {t("select_all_results", { totalCount: paging.total })}
556
+ </Button>
557
+ {/if}
558
+ </div>
559
+ {/if}
560
+ </div>
533
561
  {/if}
534
562
 
535
563
  <!-- Root container -->
@@ -56,6 +56,19 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
56
56
  selectedAll?: boolean;
57
57
  /** Set of row IDs explicitly deselected while in all-pages mode (bindable) */
58
58
  excluded?: Set<string | number>;
59
+ /**
60
+ * Keep the selection bar -- the strip above the table holding the idle status,
61
+ * `batchActions`, and the select-all-across-pages offer -- in the layout at all
62
+ * times, so checking the first row does not shove the table down. While nothing
63
+ * is selected it reads `t("no_rows_selected")`.
64
+ *
65
+ * Only meaningful when `selectable` is on AND there is something to reserve room
66
+ * for (a `batchActions` snippet or `allowSelectAllPages`); with neither, no bar
67
+ * is rendered at all.
68
+ *
69
+ * Set `false` for the pop-in behaviour: the bar appears only once it has content.
70
+ */
71
+ reserveBatchBar?: boolean;
59
72
  /** Callback when a row is clicked */
60
73
  onRowClick?: (row: T, index: number) => void;
61
74
  /**
@@ -126,7 +139,8 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
126
139
  }
127
140
  ]>;
128
141
  /**
129
- * Batch actions bar snippet (shown when items are selected).
142
+ * Content of the selection bar while something is selected. Replaces the default
143
+ * "{count} selected" status, so render your own count alongside the buttons.
130
144
  *
131
145
  * Note: in all-pages mode (`selectedAll === true`) `selectedRows` only contains
132
146
  * rows from the current page that aren't excluded. Off-page rows are not
@@ -146,8 +160,8 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
146
160
  }
147
161
  ]>;
148
162
  /**
149
- * Custom "select all results across pages" banner. When omitted, a default
150
- * banner is rendered.
163
+ * Custom "select all results across pages" offer, rendered on the trailing edge
164
+ * of the selection bar. When omitted, a default one is rendered.
151
165
  */
152
166
  selectAllBanner?: Snippet<[
153
167
  {
@@ -64,6 +64,30 @@ DataTable integrates with [`@marianmeres/paging-store`](https://github.com/maria
64
64
 
65
65
  Shift+click a checkbox to toggle a range of rows from the last clicked checkbox to the current one. Disabled rows in the range are skipped. The anchor resets when `data` changes (e.g. on page navigation).
66
66
 
67
+ #### The selection bar holds its place
68
+
69
+ Everything selection-related lives in one strip above the table: the status while nothing
70
+ is selected, your `batchActions` while something is, and the select-all-across-pages offer
71
+ on its trailing edge. That strip is rendered from the start and keeps a constant height
72
+ (`reserveBatchBar`, on by default), so checking the first row swaps its contents instead
73
+ of pushing the table down.
74
+
75
+ - The height comes from `--stuic-data-table-batch-min-height`, sized to a `size="sm"`
76
+ Button. Batch actions built from bigger controls need that token raised, or the bar
77
+ grows when they appear — which is the jump all over again.
78
+ - Idle, the bar is transparent (`--stuic-data-table-batch-bg-idle`) so a permanently
79
+ reserved strip doesn't shout; it takes on `--stuic-data-table-batch-bg` once something
80
+ is selected and `--stuic-data-table-select-all-bg` while the select-all offer is up.
81
+ - The bar only exists when there is something to reserve room for — `selectable` plus
82
+ either a `batchActions` snippet or `allowSelectAllPages`. `selectable` on its own
83
+ renders no bar.
84
+ - `reserveBatchBar={false}` restores the old behaviour: the bar appears only once it has
85
+ content.
86
+
87
+ Without a `batchActions` snippet the bar still reports the count itself
88
+ (`t("x_rows_selected")` / `t("no_rows_selected")`). With one, the snippet replaces that
89
+ status entirely — render your own count next to the buttons.
90
+
67
91
  ### Select All Across Pages
68
92
 
69
93
  When your data is paged, DataTable can offer a "select all results" affordance that expands selection beyond the current page. Enable it with `allowSelectAllPages`. When the user opts in, selection flips to **all-pages mode**: every row is implicitly selected, and `excluded` holds IDs the user has explicitly deselected.
@@ -115,7 +139,7 @@ function deleteSelection({ selectedAll, excluded }) {
115
139
 
116
140
  New records inserted while all-pages mode is active are implicitly selected (they aren't in `excluded`). This matches the conventional intent — "delete everything matching X" should include matches that arrive before the operation runs. If you need snapshot-at-click semantics, capture a timestamp or ID list in your consumer.
117
141
 
118
- **Customising the banner:** the default banner uses the built-in `t()` keys `select_all_on_page_x`, `select_all_results`, `all_results_selected`, and `clear_selection`. Override markup entirely with the `selectAllBanner` snippet:
142
+ **Customising the offer:** the default offer renders on the trailing edge of the selection bar and uses the built-in `t()` keys `select_all_on_page_x`, `select_all_results`, `all_results_selected`, and `clear_selection`. Override its markup entirely with the `selectAllBanner` snippet (which renders in the same slot — it is no longer a standalone block below the batch bar):
119
143
 
120
144
  ```svelte
121
145
  <DataTable
@@ -301,6 +325,7 @@ Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable d
301
325
  | `allowSelectAllPages` | `boolean` | `false` | Show a banner offering "select all results" across paged data |
302
326
  | `selectedAll` | `boolean` | `false` | All-pages mode flag (bindable). In this mode `excluded` drives selection |
303
327
  | `excluded` | `Set<string \| number>` | `new Set()` | Deselected row IDs while in all-pages mode (bindable) |
328
+ | `reserveBatchBar` | `boolean` | `true` | Keep the selection bar in the layout while nothing is selected |
304
329
  | `onRowClick` | `(row, index) => void` | - | Row click callback |
305
330
  | `rowHref` | `(row, index) => string \| undefined` | - | Wrap the lead cell's content in an `<a href>` (keyboard/⌘-click reachable) |
306
331
  | `rowHrefColumn` | `string` | first column | Which column is the lead cell for `rowHref` |
@@ -313,8 +338,8 @@ Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable d
313
338
  | `cell` | `Snippet` | - | Custom cell renderer (desktop + mobile) |
314
339
  | `row` | `Snippet` | - | Custom desktop `<tr>` renderer (overrides default row) |
315
340
  | `mobileRow` | `Snippet` | - | Custom mobile card renderer |
316
- | `batchActions` | `Snippet` | - | Batch action bar content |
317
- | `selectAllBanner` | `Snippet` | - | Override default "select all across pages" banner |
341
+ | `batchActions` | `Snippet` | - | Selection bar content while something is selected |
342
+ | `selectAllBanner` | `Snippet` | - | Override default "select all across pages" offer |
318
343
  | `empty` | `Snippet` | - | Custom empty state |
319
344
  | `unstyled` | `boolean` | `false` | Skip default styling |
320
345
  | `class` | `string` | - | Additional CSS classes |
@@ -348,36 +373,83 @@ Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable d
348
373
  | `hideOnMobile` | `boolean` | `false` | Hide in mobile view |
349
374
  | `renderValue` | `(value, row) => string` | - | Value formatter |
350
375
 
376
+ ## i18n
377
+
378
+ All built-in texts go through the `t` prop. English is the built-in default; Slovak ships
379
+ bundled and opt-in (importing it is what pulls it into your bundle — English-only
380
+ consumers pay nothing).
381
+
382
+ ```svelte
383
+ <script>
384
+ import {
385
+ DataTable,
386
+ createDataTableT,
387
+ DATA_TABLE_MESSAGES_SK,
388
+ } from "@marianmeres/stuic";
389
+
390
+ const t = createDataTableT(DATA_TABLE_MESSAGES_SK);
391
+ </script>
392
+
393
+ <DataTable {columns} {data} {t} />
394
+ ```
395
+
396
+ `createDataTableT(messages, fallbackMessages?)` falls back to `DATA_TABLE_MESSAGES_EN` for
397
+ any key the catalog does not define, so a partial catalog is fine and a raw key is never
398
+ rendered — pass your own object to translate into a language that is not bundled, or to
399
+ override individual bundled texts:
400
+
401
+ ```ts
402
+ const t = createDataTableT({ ...DATA_TABLE_MESSAGES_SK, no_data: "Nič tu nie je" });
403
+ ```
404
+
405
+ | Key | English | Used by |
406
+ | ---------------------- | ---------------------------------- | ------------------------------------ |
407
+ | `previous_page` | Prev | Paging |
408
+ | `next_page` | Next | Paging |
409
+ | `page_x_of_y` | Page {page} of {pageCount} | Paging |
410
+ | `no_data` | No data | Empty state (no `empty` snippet) |
411
+ | `select_all_rows` | Select all rows on this page | Header checkbox `aria-label` |
412
+ | `select_row` | Select row | Row checkbox `aria-label` |
413
+ | `no_rows_selected` | No rows selected | Idle selection bar |
414
+ | `x_rows_selected` | {count} selected | Selection bar without `batchActions` |
415
+ | `select_all_on_page_x` | All {count} on this page selected. | Select-all offer |
416
+ | `select_all_results` | Select all {totalCount} results | Select-all offer |
417
+ | `all_results_selected` | All {totalCount} results selected. | Select-all offer, all-pages mode |
418
+ | `clear_selection` | Clear selection | Select-all offer, all-pages mode |
419
+
351
420
  ## CSS Variables
352
421
 
353
- | Variable | Default | Description |
354
- | ---------------------------------------------- | ------------------------------------- | ---------------------------- |
355
- | `--stuic-data-table-radius` | `var(--radius-md)` | Border radius |
356
- | `--stuic-data-table-border-color` | `var(--stuic-color-border)` | Border color |
357
- | `--stuic-data-table-header-bg` | `var(--stuic-color-muted)` | Header background |
358
- | `--stuic-data-table-header-color` | `var(--stuic-color-muted-foreground)` | Header text |
359
- | `--stuic-data-table-header-font-size` | `0.875rem` | Header font size |
360
- | `--stuic-data-table-header-font-weight` | `var(--font-weight-semibold)` | Header font weight |
361
- | `--stuic-data-table-header-padding-x` | `0.75rem` | Header horizontal padding |
362
- | `--stuic-data-table-header-padding-y` | `0.5rem` | Header vertical padding |
363
- | `--stuic-data-table-row-bg` | `transparent` | Row background |
364
- | `--stuic-data-table-row-bg-hover` | `var(--stuic-color-muted)` | Row hover background |
365
- | `--stuic-data-table-row-bg-selected` | `color-mix(primary 10%)` | Selected row background |
366
- | `--stuic-data-table-row-border-color` | `var(--stuic-color-border)` | Row border color |
367
- | `--stuic-data-table-row-link-color` | `inherit` | `rowHref` anchor color |
368
- | `--stuic-data-table-row-link-decoration` | `none` | `rowHref` anchor decoration |
369
- | `--stuic-data-table-row-link-decoration-hover` | `underline` | …on hover |
370
- | `--stuic-data-table-row-ring-width` | `3px` | Activatable row focus ring |
371
- | `--stuic-data-table-row-ring-color` | `var(--stuic-color-ring)` | Activatable row ring color |
372
- | `--stuic-data-table-cell-padding-x` | `0.75rem` | Cell horizontal padding |
373
- | `--stuic-data-table-cell-padding-y` | `0.75rem` | Cell vertical padding |
374
- | `--stuic-data-table-cell-font-size` | `0.875rem` | Cell font size |
375
- | `--stuic-data-table-loading-opacity` | `0.5` | Loading state opacity |
376
- | `--stuic-data-table-card-bg` | `var(--stuic-color-background)` | Mobile card background |
377
- | `--stuic-data-table-card-border-color` | `var(--stuic-color-border)` | Mobile card border |
378
- | `--stuic-data-table-card-radius` | `var(--radius-md)` | Mobile card radius |
379
- | `--stuic-data-table-card-padding` | `0.75rem` | Mobile card padding |
380
- | `--stuic-data-table-card-gap` | `0.5rem` | Gap between mobile cards |
381
- | `--stuic-data-table-select-all-bg` | `color-mix(primary 10%)` | Select-all banner background |
382
- | `--stuic-data-table-select-all-padding-x` | `0.75rem` | Banner horizontal padding |
383
- | `--stuic-data-table-select-all-padding-y` | `0.5rem` | Banner vertical padding |
422
+ | Variable | Default | Description |
423
+ | ---------------------------------------------- | ------------------------------------- | --------------------------------- |
424
+ | `--stuic-data-table-radius` | `var(--radius-md)` | Border radius |
425
+ | `--stuic-data-table-border-color` | `var(--stuic-color-border)` | Border color |
426
+ | `--stuic-data-table-header-bg` | `var(--stuic-color-muted)` | Header background |
427
+ | `--stuic-data-table-header-color` | `var(--stuic-color-muted-foreground)` | Header text |
428
+ | `--stuic-data-table-header-font-size` | `0.875rem` | Header font size |
429
+ | `--stuic-data-table-header-font-weight` | `var(--font-weight-semibold)` | Header font weight |
430
+ | `--stuic-data-table-header-padding-x` | `0.75rem` | Header horizontal padding |
431
+ | `--stuic-data-table-header-padding-y` | `0.5rem` | Header vertical padding |
432
+ | `--stuic-data-table-row-bg` | `transparent` | Row background |
433
+ | `--stuic-data-table-row-bg-hover` | `var(--stuic-color-muted)` | Row hover background |
434
+ | `--stuic-data-table-row-bg-selected` | `color-mix(primary 10%)` | Selected row background |
435
+ | `--stuic-data-table-row-border-color` | `var(--stuic-color-border)` | Row border color |
436
+ | `--stuic-data-table-row-link-color` | `inherit` | `rowHref` anchor color |
437
+ | `--stuic-data-table-row-link-decoration` | `none` | `rowHref` anchor decoration |
438
+ | `--stuic-data-table-row-link-decoration-hover` | `underline` | …on hover |
439
+ | `--stuic-data-table-row-ring-width` | `3px` | Activatable row focus ring |
440
+ | `--stuic-data-table-row-ring-color` | `var(--stuic-color-ring)` | Activatable row ring color |
441
+ | `--stuic-data-table-cell-padding-x` | `0.75rem` | Cell horizontal padding |
442
+ | `--stuic-data-table-cell-padding-y` | `0.75rem` | Cell vertical padding |
443
+ | `--stuic-data-table-cell-font-size` | `0.875rem` | Cell font size |
444
+ | `--stuic-data-table-loading-opacity` | `0.5` | Loading state opacity |
445
+ | `--stuic-data-table-card-bg` | `var(--stuic-color-background)` | Mobile card background |
446
+ | `--stuic-data-table-card-border-color` | `var(--stuic-color-border)` | Mobile card border |
447
+ | `--stuic-data-table-card-radius` | `var(--radius-md)` | Mobile card radius |
448
+ | `--stuic-data-table-card-padding` | `0.75rem` | Mobile card padding |
449
+ | `--stuic-data-table-card-gap` | `0.5rem` | Gap between mobile cards |
450
+ | `--stuic-data-table-batch-bg` | `var(--stuic-color-muted)` | Selection bar background |
451
+ | `--stuic-data-table-batch-bg-idle` | `transparent` | …while nothing is selected |
452
+ | `--stuic-data-table-batch-padding-x` | `0.75rem` | Selection bar h. padding |
453
+ | `--stuic-data-table-batch-padding-y` | `0.5rem` | Selection bar v. padding |
454
+ | `--stuic-data-table-batch-min-height` | `sm Button + padding` | Reserved selection bar height |
455
+ | `--stuic-data-table-select-all-bg` | `color-mix(primary 10%)` | …while the select-all offer is up |
@@ -0,0 +1,17 @@
1
+ import type { DataTableMessages } from "./i18n.js";
2
+ /**
3
+ * Slovak message catalog for `DataTable`. Opt-in — English stays the built-in default,
4
+ * and this module is only pulled into a bundle when it is actually imported (the
5
+ * component itself never references it).
6
+ *
7
+ * @example
8
+ * ```svelte
9
+ * <script>
10
+ * import { DataTable, createDataTableT, DATA_TABLE_MESSAGES_SK } from "@marianmeres/stuic";
11
+ * const t = createDataTableT(DATA_TABLE_MESSAGES_SK);
12
+ * </script>
13
+ *
14
+ * <DataTable {columns} {data} {t} />
15
+ * ```
16
+ */
17
+ export declare const DATA_TABLE_MESSAGES_SK: DataTableMessages;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Slovak message catalog for `DataTable`. Opt-in — English stays the built-in default,
3
+ * and this module is only pulled into a bundle when it is actually imported (the
4
+ * component itself never references it).
5
+ *
6
+ * @example
7
+ * ```svelte
8
+ * <script>
9
+ * import { DataTable, createDataTableT, DATA_TABLE_MESSAGES_SK } from "@marianmeres/stuic";
10
+ * const t = createDataTableT(DATA_TABLE_MESSAGES_SK);
11
+ * </script>
12
+ *
13
+ * <DataTable {columns} {data} {t} />
14
+ * ```
15
+ */
16
+ export const DATA_TABLE_MESSAGES_SK = {
17
+ previous_page: "Späť",
18
+ next_page: "Ďalej",
19
+ page_x_of_y: "Strana {page} z {pageCount}",
20
+ no_data: "Žiadne údaje",
21
+ select_all_rows: "Vybrať všetky riadky na tejto strane",
22
+ select_row: "Vybrať riadok",
23
+ select_all_on_page_x: "Vybrané všetky na tejto strane ({count}).",
24
+ select_all_results: "Vybrať všetky výsledky ({totalCount})",
25
+ all_results_selected: "Vybrané všetky výsledky ({totalCount}).",
26
+ clear_selection: "Zrušiť výber",
27
+ no_rows_selected: "Nič nie je vybrané",
28
+ x_rows_selected: "Vybrané: {count}",
29
+ };
@@ -0,0 +1,43 @@
1
+ import type { TranslateFn } from "../../types.js";
2
+ /**
3
+ * The built-in (English) message catalog of `DataTable`. Also the fallback of every
4
+ * other bundled locale, so a locale missing a key still renders text.
5
+ *
6
+ * Placeholders are single-brace (`{page}`, `{totalCount}`, ...).
7
+ */
8
+ export declare const DATA_TABLE_MESSAGES_EN: {
9
+ previous_page: string;
10
+ next_page: string;
11
+ page_x_of_y: string;
12
+ no_data: string;
13
+ select_all_rows: string;
14
+ select_row: string;
15
+ select_all_on_page_x: string;
16
+ select_all_results: string;
17
+ all_results_selected: string;
18
+ clear_selection: string;
19
+ no_rows_selected: string;
20
+ x_rows_selected: string;
21
+ };
22
+ /** Every message key `DataTable` may look up. */
23
+ export type DataTableMessageKey = keyof typeof DATA_TABLE_MESSAGES_EN;
24
+ /** A (possibly partial) catalog for one locale. */
25
+ export type DataTableMessages = Record<DataTableMessageKey, string>;
26
+ /**
27
+ * Builds the `t` prop of `DataTable` from a message catalog. Unknown or untranslated
28
+ * keys fall back to `fallbackMessages` (English by default), so a catalog may safely be
29
+ * partial and never renders a raw key.
30
+ *
31
+ * @example
32
+ * ```svelte
33
+ * <script>
34
+ * import { DataTable, createDataTableT, DATA_TABLE_MESSAGES_SK } from "@marianmeres/stuic";
35
+ * const t = createDataTableT(DATA_TABLE_MESSAGES_SK);
36
+ * </script>
37
+ *
38
+ * <DataTable {columns} {data} {t} />
39
+ * ```
40
+ */
41
+ export declare function createDataTableT(messages: Partial<DataTableMessages> | Record<string, string>, fallbackMessages?: Partial<DataTableMessages> | Record<string, string>): TranslateFn;
42
+ /** The component's built-in English `t`. */
43
+ export declare const t_default: TranslateFn;
@@ -0,0 +1,51 @@
1
+ import { isPlainObject } from "../../utils/is-plain-object.js";
2
+ import { replaceMap } from "../../utils/replace-map.js";
3
+ /**
4
+ * The built-in (English) message catalog of `DataTable`. Also the fallback of every
5
+ * other bundled locale, so a locale missing a key still renders text.
6
+ *
7
+ * Placeholders are single-brace (`{page}`, `{totalCount}`, ...).
8
+ */
9
+ export const DATA_TABLE_MESSAGES_EN = {
10
+ previous_page: "Prev",
11
+ next_page: "Next",
12
+ page_x_of_y: "Page {page} of {pageCount}",
13
+ no_data: "No data",
14
+ select_all_rows: "Select all rows on this page",
15
+ select_row: "Select row",
16
+ select_all_on_page_x: "All {count} on this page selected.",
17
+ select_all_results: "Select all {totalCount} results",
18
+ all_results_selected: "All {totalCount} results selected.",
19
+ clear_selection: "Clear selection",
20
+ no_rows_selected: "No rows selected",
21
+ x_rows_selected: "{count} selected",
22
+ };
23
+ /**
24
+ * Builds the `t` prop of `DataTable` from a message catalog. Unknown or untranslated
25
+ * keys fall back to `fallbackMessages` (English by default), so a catalog may safely be
26
+ * partial and never renders a raw key.
27
+ *
28
+ * @example
29
+ * ```svelte
30
+ * <script>
31
+ * import { DataTable, createDataTableT, DATA_TABLE_MESSAGES_SK } from "@marianmeres/stuic";
32
+ * const t = createDataTableT(DATA_TABLE_MESSAGES_SK);
33
+ * </script>
34
+ *
35
+ * <DataTable {columns} {data} {t} />
36
+ * ```
37
+ */
38
+ export function createDataTableT(messages, fallbackMessages = DATA_TABLE_MESSAGES_EN) {
39
+ return (k, values = null, fallback = "") => {
40
+ const out = messages[k] ??
41
+ fallbackMessages[k] ??
42
+ (typeof fallback === "string" ? fallback : k);
43
+ return isPlainObject(values)
44
+ ? replaceMap(out, values, {
45
+ preSearchKeyTransform: (k) => `{${k}}`,
46
+ })
47
+ : out;
48
+ };
49
+ }
50
+ /** The component's built-in English `t`. */
51
+ export const t_default = createDataTableT(DATA_TABLE_MESSAGES_EN);
@@ -42,15 +42,19 @@
42
42
  --stuic-data-table-paging-gap: 0.5rem;
43
43
  --stuic-data-table-paging-padding-y: 0.75rem;
44
44
 
45
- /* Batch action bar */
45
+ /* Selection (batch action) bar */
46
46
  --stuic-data-table-batch-bg: var(--stuic-color-muted);
47
+ /* Nothing selected -- the bar is only holding its place, so it stays quiet. */
48
+ --stuic-data-table-batch-bg-idle: var(--stuic-color-muted);
47
49
  --stuic-data-table-batch-padding-x: 0.75rem;
48
- --stuic-data-table-batch-padding-y: 0.5rem;
50
+ --stuic-data-table-batch-padding-y: 0.25rem;
51
+ /* Sized to a `size="sm"` Button so the idle and active states are the same height. */
52
+ --stuic-data-table-batch-min-height: calc(var(--stuic-button-min-height-sm, 2.75rem) + var(--stuic-data-table-batch-padding-y) * 2);
53
+ /* */
54
+ --stuic-data-table-batch-margin: 0 0 0.125rem 0;
49
55
 
50
- /* Select-all-across-pages banner */
56
+ /* Select-all-across-pages offer (rendered inside the selection bar) */
51
57
  --stuic-data-table-select-all-bg: color-mix(in srgb, var(--stuic-color-primary) 10%, var(--stuic-color-background));
52
- --stuic-data-table-select-all-padding-x: 0.75rem;
53
- --stuic-data-table-select-all-padding-y: 0.5rem;
54
58
 
55
59
  /* Checkbox column */
56
60
  --stuic-data-table-checkbox-width: 3rem;
@@ -238,34 +242,52 @@
238
242
  BATCH ACTION BAR
239
243
  ============================================================================ */
240
244
 
245
+ /*
246
+ The min-height is the whole point of the reserved bar: an idle line of text is
247
+ shorter than a row of buttons, so without it the table would still shift -- just
248
+ by less. Keep it in sync with whatever button size the batch actions use.
249
+ */
241
250
  .stuic-data-table-batch {
242
251
  display: flex;
243
252
  align-items: center;
253
+ justify-content: space-between;
254
+ flex-wrap: wrap;
244
255
  gap: var(--stuic-data-table-paging-gap);
256
+ min-height: var(--stuic-data-table-batch-min-height);
245
257
  padding: var(--stuic-data-table-batch-padding-y)
246
258
  var(--stuic-data-table-batch-padding-x);
247
259
  background: var(--stuic-data-table-batch-bg);
248
260
  border-radius: var(--stuic-data-table-radius, var(--stuic-radius));
249
- margin-bottom: 0.5rem;
261
+ margin: var(--stuic-data-table-batch-margin, 0);
262
+ transition: background var(--stuic-data-table-transition, var(--stuic-transition));
250
263
  }
251
264
 
252
- /* ============================================================================
253
- SELECT-ALL-ACROSS-PAGES BANNER
254
- ============================================================================ */
265
+ .stuic-data-table-batch[data-idle="true"] {
266
+ background: var(--stuic-data-table-batch-bg-idle);
267
+ }
268
+
269
+ /* The select-all offer is the one state worth actively drawing attention to. */
270
+ .stuic-data-table-batch[data-select-all="true"] {
271
+ background: var(--stuic-data-table-select-all-bg);
272
+ }
255
273
 
256
- .stuic-data-table-select-all-banner {
274
+ .stuic-data-table-batch-main,
275
+ .stuic-data-table-batch-select-all {
257
276
  display: flex;
258
277
  align-items: center;
259
- justify-content: center;
278
+ flex-wrap: wrap;
260
279
  gap: var(--stuic-data-table-paging-gap);
261
- padding: var(--stuic-data-table-select-all-padding-y)
262
- var(--stuic-data-table-select-all-padding-x);
263
- background: var(--stuic-data-table-select-all-bg);
264
- border-radius: var(--stuic-data-table-radius, var(--stuic-radius));
265
- margin-bottom: 0.5rem;
280
+ }
281
+
282
+ .stuic-data-table-batch-select-all {
283
+ margin-left: auto;
284
+ font-size: var(--stuic-data-table-cell-font-size);
285
+ color: var(--stuic-data-table-header-color);
286
+ }
287
+
288
+ .stuic-data-table-batch-status {
266
289
  font-size: var(--stuic-data-table-cell-font-size);
267
290
  color: var(--stuic-data-table-header-color);
268
- text-align: center;
269
291
  }
270
292
 
271
293
  /* ============================================================================
@@ -1 +1,3 @@
1
1
  export { default as DataTable, type Props as DataTableProps, type DataTableColumn, } from "./DataTable.svelte";
2
+ export { createDataTableT, DATA_TABLE_MESSAGES_EN, type DataTableMessageKey, type DataTableMessages, } from "./i18n.js";
3
+ export { DATA_TABLE_MESSAGES_SK } from "./i18n-sk.js";
@@ -1 +1,3 @@
1
1
  export { default as DataTable, } from "./DataTable.svelte";
2
+ export { createDataTableT, DATA_TABLE_MESSAGES_EN, } from "./i18n.js";
3
+ export { DATA_TABLE_MESSAGES_SK } from "./i18n-sk.js";
@@ -180,14 +180,13 @@
180
180
  {#if !expanded || !_hasMultiple}
181
181
  {@render langInput(_defaultLanguage, true)}
182
182
  {:else}
183
- <div class="expanded-wrap">
184
- {#each sortedLanguages as lang, idx (lang)}
185
- <div
186
- class={twMerge(
187
- "flex-1 flex gap-2 items-center pl-2",
188
- idx > 0 && "entry-divider"
189
- )}
190
- >
183
+ <!-- no `.expanded-wrap`/`.entry-divider` chrome here (unlike
184
+ FieldInputLocalized): these inputs carry their own border, so a
185
+ divider line and the wrap's right edge would only add noise —
186
+ plain spacing separates the rows instead -->
187
+ <div class="flex flex-col gap-1.5">
188
+ {#each sortedLanguages as lang (lang)}
189
+ <div class="flex-1 flex gap-2 items-center pl-2">
191
190
  <div
192
191
  class={twMerge(
193
192
  "lang-label",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.162.0",
3
+ "version": "3.164.0",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -19,8 +19,8 @@
19
19
  "test:ui": "vitest --ui",
20
20
  "svelte-check": "svelte-check",
21
21
  "svelte-package": "svelte-package",
22
- "rp": "pnpm run build && deno run -A jsr:@marianmeres/release patch --no-push && npm publish --access public && git push --follow-tags",
23
- "rpm": "pnpm run build && deno run -A jsr:@marianmeres/release minor --no-push && npm publish --access public && git push --follow-tags",
22
+ "rp": "pnpm run build && deno run -A jsr:@marianmeres/release patch --yes --no-push && npm publish --access public && git push --follow-tags",
23
+ "rpm": "pnpm run build && deno run -A jsr:@marianmeres/release minor --yes --no-push && npm publish --access public && git push --follow-tags",
24
24
  "release": "deno run -A jsr:@marianmeres/release"
25
25
  },
26
26
  "files": [