@lyeve-labs/ui-kit 0.19.0 → 0.21.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.
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { safeHref } from '../internal/href.js';
3
+ import { formatCount } from '../internal/number.js';
3
4
 
4
5
  interface Base {
5
6
  page: number;
@@ -21,6 +22,31 @@
21
22
  * back.
22
23
  */
23
24
  hasNext?: boolean;
25
+ /**
26
+ * How many rows the page on screen actually holds.
27
+ *
28
+ * Only an uncounted list needs it, and only for the sentence: without a
29
+ * total the component knows where the page starts but not where it ends,
30
+ * because the last page is short and nothing here says how short. Given it,
31
+ * the summary states a range instead of naming the page.
32
+ *
33
+ * It is the row count of what was rendered, not a page size. A caller that
34
+ * asked for 50 and received 12 passes 12.
35
+ */
36
+ count?: number;
37
+ /**
38
+ * What the list holds, in the plural: 'files', 'jobs', 'webhooks'.
39
+ *
40
+ * The summary names it, because a console that stacks three lists under one
41
+ * page title prints '51 to 100' three times and says of what nowhere. Left
42
+ * out, the summary reads exactly as it did before this existed.
43
+ *
44
+ * Plural at every size, including a list of one. The noun labels the
45
+ * collection here rather than agreeing with a figure in the sentence, the
46
+ * same way the column header above it does, and a singular form would have
47
+ * to pick which of the three figures governs it.
48
+ */
49
+ noun?: string;
24
50
  class?: string;
25
51
  }
26
52
 
@@ -47,6 +73,8 @@
47
73
  total = undefined,
48
74
  perPage = 20,
49
75
  hasNext = false,
76
+ count = undefined,
77
+ noun = '',
50
78
  class: cls = '',
51
79
  onchange,
52
80
  href,
@@ -64,6 +92,15 @@
64
92
  let from = $derived(Math.min((safePage - 1) * perPage + 1, safeTotal));
65
93
  let to = $derived(Math.min(safePage * perPage, safeTotal));
66
94
 
95
+ /**
96
+ * The rows on screen, when the caller states them. Zero and a negative are
97
+ * both "nothing to describe": a page of no rows has no range, and the summary
98
+ * falls back to naming the page rather than printing an inverted one.
99
+ */
100
+ let safeCount = $derived(
101
+ typeof count === 'number' && isFinite(count) && count > 0 ? Math.floor(count) : undefined,
102
+ );
103
+
67
104
  let canPrev = $derived(safePage > 1);
68
105
  let canNext = $derived(counted ? safePage < totalPages : hasNext);
69
106
  let hasControls = $derived(counted ? totalPages > 1 : canPrev || canNext);
@@ -102,20 +139,40 @@
102
139
 
103
140
  let nums = $derived(counted ? pageNumbers(safePage, totalPages) : []);
104
141
 
142
+ /**
143
+ * The noun as it will be read.
144
+ *
145
+ * Trimmed, and empty means absent: a caller composing the word from data can
146
+ * hand over ' ' as easily as 'files', and a summary that opens with a stray
147
+ * space is worse than one that names nothing.
148
+ */
149
+ let label = $derived(noun.trim());
150
+
105
151
  /**
106
152
  * The count, when there is one to state.
107
153
  *
108
154
  * An uncounted list says which page it is on and nothing else. It cannot say
109
155
  * "1 to 20 of 400" without the total, and it cannot say "1 to 20" either: the
110
- * last page is short and this component is never told how short.
156
+ * last page is short and this component is never told how short. It still
157
+ * groups the page number: `href` mode puts that number in the URL, so a link
158
+ * or a reader can land on page 5,000 without stepping there.
159
+ *
160
+ * Every figure is grouped, because a pager sits under a table that groups its
161
+ * own figures and the pager is where the largest number on the screen is.
111
162
  */
112
- let summary = $derived(
113
- counted
114
- ? safeTotal === 0
115
- ? 'No results'
116
- : `${from} to ${to} of ${safeTotal}`
117
- : `Page ${safePage}`,
118
- );
163
+ let summary = $derived.by(() => {
164
+ if (!counted) {
165
+ if (safeCount !== undefined) {
166
+ const start = (safePage - 1) * perPage + 1;
167
+ const span = `${formatCount(start)} to ${formatCount(start + safeCount - 1)}`;
168
+ return label ? `${label} ${span}` : span;
169
+ }
170
+ return label ? `${label}, page ${formatCount(safePage)}` : `Page ${formatCount(safePage)}`;
171
+ }
172
+ if (safeTotal === 0) return label ? `No ${label}` : 'No results';
173
+ const range = `${formatCount(from)} to ${formatCount(to)} of ${formatCount(safeTotal)}`;
174
+ return label ? `${label} ${range}` : range;
175
+ });
119
176
 
120
177
  /** The target of a control, sanitized, or nothing when the step is unavailable. */
121
178
  function linkTo(target: number, enabled: boolean): string | undefined {
@@ -18,6 +18,31 @@ interface Base {
18
18
  * back.
19
19
  */
20
20
  hasNext?: boolean;
21
+ /**
22
+ * How many rows the page on screen actually holds.
23
+ *
24
+ * Only an uncounted list needs it, and only for the sentence: without a
25
+ * total the component knows where the page starts but not where it ends,
26
+ * because the last page is short and nothing here says how short. Given it,
27
+ * the summary states a range instead of naming the page.
28
+ *
29
+ * It is the row count of what was rendered, not a page size. A caller that
30
+ * asked for 50 and received 12 passes 12.
31
+ */
32
+ count?: number;
33
+ /**
34
+ * What the list holds, in the plural: 'files', 'jobs', 'webhooks'.
35
+ *
36
+ * The summary names it, because a console that stacks three lists under one
37
+ * page title prints '51 to 100' three times and says of what nowhere. Left
38
+ * out, the summary reads exactly as it did before this existed.
39
+ *
40
+ * Plural at every size, including a list of one. The noun labels the
41
+ * collection here rather than agreeing with a figure in the sentence, the
42
+ * same way the column header above it does, and a singular form would have
43
+ * to pick which of the three figures governs it.
44
+ */
45
+ noun?: string;
21
46
  class?: string;
22
47
  }
23
48
  /**
package/dist/index.d.ts CHANGED
@@ -89,4 +89,4 @@ export type { ConfirmOptions } from './components/dialog/dialog-manager.svelte.j
89
89
  export type { DialogOptions, DialogEntry, DialogSize } from './components/dialog/types.js';
90
90
  export { cn, type ClassValue } from './utils/cn.js';
91
91
  export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, toggleTheme, watchSystemTheme, THEME_PREFERENCES, type Theme, type ThemePreference, } from './utils/theme.js';
92
- export declare const VERSION = "0.19.0";
92
+ export declare const VERSION = "0.21.0";
package/dist/index.js CHANGED
@@ -96,4 +96,4 @@ export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setThe
96
96
  // ── Version ────────────────────────────────────────────────────────────────
97
97
  // Generated from package.json by `pnpm version:sync`. Bump package.json, never
98
98
  // this line; the build and the test suite fail when the two disagree.
99
- export const VERSION = '0.19.0';
99
+ export const VERSION = '0.21.0';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Digit grouping for a figure a reader is meant to take in at a glance.
3
+ *
4
+ * The locale is fixed rather than the runtime's. `toLocaleString()` with no
5
+ * argument formats by whatever locale the environment reports, and the server
6
+ * that renders a page and the browser that hydrates it do not have to report
7
+ * the same one: the same figure then arrives as `4,210` in the HTML and as
8
+ * `4.210` or `4210` after hydration, which is a mismatch the framework warns
9
+ * about and a number the reader watches change under them. A component library
10
+ * has no way to know which locale a host has configured either, so the
11
+ * separator has to be a decision the kit makes once.
12
+ *
13
+ * `en-US` because that is the language every string in the kit is written in,
14
+ * and an English sentence carrying a German separator is the worse of the two
15
+ * inconsistencies.
16
+ *
17
+ * Not exported from the package entry point - this is an implementation detail.
18
+ */
19
+ /**
20
+ * A count, grouped in threes.
21
+ *
22
+ * A value that is not a finite number reads as zero rather than reaching the
23
+ * formatter, which spells infinity as a glyph no summary line should carry and
24
+ * NaN as a word no reader can act on.
25
+ */
26
+ export declare function formatCount(n: number): string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Digit grouping for a figure a reader is meant to take in at a glance.
3
+ *
4
+ * The locale is fixed rather than the runtime's. `toLocaleString()` with no
5
+ * argument formats by whatever locale the environment reports, and the server
6
+ * that renders a page and the browser that hydrates it do not have to report
7
+ * the same one: the same figure then arrives as `4,210` in the HTML and as
8
+ * `4.210` or `4210` after hydration, which is a mismatch the framework warns
9
+ * about and a number the reader watches change under them. A component library
10
+ * has no way to know which locale a host has configured either, so the
11
+ * separator has to be a decision the kit makes once.
12
+ *
13
+ * `en-US` because that is the language every string in the kit is written in,
14
+ * and an English sentence carrying a German separator is the worse of the two
15
+ * inconsistencies.
16
+ *
17
+ * Not exported from the package entry point - this is an implementation detail.
18
+ */
19
+ /**
20
+ * Built at module load, not per call.
21
+ *
22
+ * Constructing an `Intl` formatter is the expensive half of using one, and a
23
+ * pager re-renders on every keystroke that filters the list behind it.
24
+ */
25
+ const GROUPED = new Intl.NumberFormat('en-US');
26
+ /**
27
+ * A count, grouped in threes.
28
+ *
29
+ * A value that is not a finite number reads as zero rather than reaching the
30
+ * formatter, which spells infinity as a glyph no summary line should carry and
31
+ * NaN as a word no reader can act on.
32
+ */
33
+ export function formatCount(n) {
34
+ return Number.isFinite(n) ? GROUPED.format(n) : '0';
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyeve-labs/ui-kit",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "A clean, accessible, palette-aware Svelte 5 component library. The design system behind LyEve.",
5
5
  "license": "MIT",
6
6
  "author": "LyEve Labs <hello@lyeve.com>",