@urbicon-ui/blocks 6.26.3 → 6.27.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.
@@ -89,7 +89,14 @@ export const cardVariants = tv({
89
89
  },
90
90
  elementType: {
91
91
  button: {
92
- base: 'border-none font-inherit text-left cursor-pointer'
92
+ // `[font:inherit]` (arbitrary property), NOT `font-inherit`: Tailwind
93
+ // v4 has no `font-inherit` utility (no `--font-inherit` theme key), so
94
+ // that class emitted no rule and a clickable Card kept the UA button
95
+ // font instead of inheriting the surrounding type. Same utility gap as
96
+ // Button's `[gap:inherit]` (Codeberg #21); found ground-truthing the
97
+ // variants-lint theme-existence guard (`font-` itself stays unguarded —
98
+ // family keys are legitimately consumer-supplied).
99
+ base: 'border-none [font:inherit] text-left cursor-pointer'
93
100
  },
94
101
  a: {
95
102
  base: 'no-underline text-inherit'
@@ -35,6 +35,7 @@
35
35
  queryFn,
36
36
  debounceMs = 250,
37
37
  loadingText = 'Loading…',
38
+ onError,
38
39
  tier,
39
40
  variant = 'outlined',
40
41
  size = 'md',
@@ -154,12 +155,21 @@
154
155
  // The dev-warn is gated to *sync* mode: with `queryFn`, a pre-bound value that
155
156
  // isn't in the current result set is expected, not a bug (there is no API to
156
157
  // seed labels for it), so warning there would cry wolf on a legitimate pattern.
157
- // See technical-debt (async pre-selected labels + warn dedup).
158
+ // See technical-debt (async pre-selected labels).
159
+ //
160
+ // Warn dedup: `selectedTags` recomputes on every fresh `options` array a
161
+ // parent re-render passes in (the common `options={items.map(…)}` idiom), so
162
+ // an unguarded warn would re-fire per recompute. A plain Set — deliberately
163
+ // outside the reactive graph, so adding to it never invalidates the derived —
164
+ // makes it one warn per orphan value for the instance's lifetime (mirrors
165
+ // Select).
166
+ const warnedOrphanValues = new Set<T>();
158
167
  const selectedTags = $derived.by<ComboboxOption<T>[]>(() =>
159
168
  selectedValues.map((v) => {
160
169
  const found = allOptions.find((o) => o.value === v) ?? tagCache.get(v);
161
170
  if (found) return found;
162
- if (!queryFn && import.meta.env?.DEV) {
171
+ if (!queryFn && import.meta.env?.DEV && !warnedOrphanValues.has(v)) {
172
+ warnedOrphanValues.add(v);
163
173
  console.warn(
164
174
  `[Combobox] value ${JSON.stringify(v)} has no matching option — tag falls back to its raw value.`
165
175
  );
@@ -261,7 +271,18 @@
261
271
  .catch((err: unknown) => {
262
272
  if (controller.signal.aborted || (err as { name?: string })?.name === 'AbortError')
263
273
  return;
264
- if (import.meta.env?.DEV) console.warn('[Combobox] queryFn rejected:', err);
274
+ // Failure contract: the loading state ends (`finally`) and the
275
+ // previous — now stale — options stay in place (no result-set
276
+ // clobber; a UI error slot remains an open debt). The rejection is
277
+ // handed to `onError`; without one it is surfaced DEV-only instead
278
+ // of vanishing silently (ConfirmDialog onConfirm precedent). A
279
+ // throwing `onError` is a consumer bug and deliberately escapes
280
+ // (fail-loud, mirrors createCronRunner).
281
+ if (onError) {
282
+ onError(err);
283
+ } else if (import.meta.env?.DEV) {
284
+ console.warn('[Combobox] queryFn rejected:', err);
285
+ }
265
286
  })
266
287
  .finally(() => {
267
288
  if (!controller.signal.aborted) loading = false;
@@ -56,16 +56,25 @@ interface ComboboxBaseProps<T extends SelectValue = string> extends ComboboxVari
56
56
  * by {@link debounceMs} — on each query change, replacing the option list with
57
57
  * the resolved result. The `AbortSignal` is aborted when a newer query
58
58
  * supersedes an in-flight request, so a slow stale response never clobbers a
59
- * fresh one. Aborted rejections are swallowed; other rejections leave the
60
- * previous options in place (and warn in dev). `options`/`groups` are ignored
61
- * in this mode. The selected option's label is cached so it survives result
62
- * sets that no longer contain it.
59
+ * fresh one. Aborted rejections are swallowed; other rejections end the
60
+ * loading state, leave the previous options in place, and are reported via
61
+ * `onError`. `options`/`groups` are ignored in this mode. The selected
62
+ * option's label is cached so it survives result sets that no longer
63
+ * contain it.
63
64
  */
64
65
  queryFn?: (query: string, signal: AbortSignal) => Promise<ComboboxOption<T>[]>;
65
66
  /** Debounce applied to `queryFn` in milliseconds. @default 250 */
66
67
  debounceMs?: number;
67
68
  /** Text shown in the listbox while an async `queryFn` request is in flight. @default 'Loading…' */
68
69
  loadingText?: string;
70
+ /**
71
+ * Fired when `queryFn` rejects (aborted / superseded requests are ignored).
72
+ * The loading state ends and the previous option list stays in place — use
73
+ * this to surface the failure (toast, inline message). Without a handler
74
+ * the rejection is logged DEV-only (`console.warn`) and swallowed in
75
+ * production; it never escapes as an unhandled promise rejection.
76
+ */
77
+ onError?: (error: unknown) => void;
69
78
  /** Show a clear button when a value is selected. Click or press Escape to reset. @default false */
70
79
  clearable?: boolean;
71
80
  /** Disable the entire combobox. @default false */
@@ -20,7 +20,10 @@
20
20
  intent = 'primary',
21
21
  tier,
22
22
  visiblePages = 7,
23
- showFirstLast = true,
23
+ // Undefaulted so the DEV no-op warning below can tell an explicit
24
+ // `showFirstLast` from the default; `showFirstLast` itself (derived) keeps
25
+ // the documented `true` default for every render-path consumer.
26
+ showFirstLast: showFirstLastProp,
24
27
  showPreviousNext = true,
25
28
  showNumbers = true,
26
29
  showInfo = false,
@@ -52,6 +55,29 @@
52
55
  const blocksConfig = getBlocksConfig();
53
56
  const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
54
57
 
58
+ const showFirstLast = $derived(showFirstLastProp ?? true);
59
+
60
+ // DEV fail-loud: First/Last are redundancy-gated to the number window (they
61
+ // render only beside a start/end ellipsis), so with `showNumbers={false}` an
62
+ // explicitly-set `showFirstLast` is a silent no-op. Surface that once per
63
+ // instance — the coupling itself is a settled decision (see the
64
+ // `showFirstLast` JSDoc), only its silence was the bug. Plain flag, not
65
+ // `$state`: the warn must not feed back into the reactive graph.
66
+ let warnedFirstLastWithoutNumbers = false;
67
+ $effect(() => {
68
+ if (
69
+ import.meta.env?.DEV &&
70
+ !warnedFirstLastWithoutNumbers &&
71
+ showFirstLastProp === true &&
72
+ !showNumbers
73
+ ) {
74
+ warnedFirstLastWithoutNumbers = true;
75
+ console.warn(
76
+ '[Pagination] showFirstLast has no effect while showNumbers is false — First/Last only render beside the number window’s ellipsis. Drop showFirstLast or re-enable showNumbers.'
77
+ );
78
+ }
79
+ });
80
+
55
81
  const variantProps: PaginationVariants = $derived({
56
82
  layout,
57
83
  size,
@@ -68,7 +68,15 @@ export interface PaginationProps extends Omit<PaginationVariants, 'disabled' | '
68
68
  tier?: InteractiveTier;
69
69
  /** Maximum number of page buttons shown between the ellipsis indicators. */
70
70
  visiblePages?: number;
71
- /** Show "First" / "Last" boundary buttons when the current page is far from the edges. */
71
+ /**
72
+ * Show "First" / "Last" boundary buttons when the current page is far from
73
+ * the edges. Deliberately redundancy-gated to the number window: the buttons
74
+ * render only beside a start/end ellipsis, so without `showNumbers` there is
75
+ * no number window, no ellipsis — and no First/Last buttons. That coupling is
76
+ * intentional (a compact prev/next-only bar stays compact), not a bug.
77
+ * Setting `showFirstLast` explicitly while `showNumbers` is `false` warns
78
+ * once per instance in dev. @default true
79
+ */
72
80
  showFirstLast?: boolean;
73
81
  /** Show "Previous" / "Next" navigation buttons. */
74
82
  showPreviousNext?: boolean;
@@ -177,14 +177,23 @@
177
177
  * dev so the consumer notices the orphan. The orphan is also filtered out
178
178
  * of the hidden form input, so the submitted form stays consistent with
179
179
  * what the trigger shows.
180
+ *
181
+ * Warn dedup: `selectedOptions` recomputes on every fresh `options` array a
182
+ * parent re-render passes in (the common `options={items.map(…)}` idiom), so
183
+ * an unguarded warn would re-fire per recompute. A plain Set — deliberately
184
+ * outside the reactive graph, so adding to it never invalidates the derived —
185
+ * makes it one warn per orphan value for the instance's lifetime (mirrors
186
+ * Combobox).
180
187
  */
188
+ const warnedOrphanValues = new Set<unknown>();
181
189
  const selectedOptions = $derived.by((): SelectOption<T>[] => {
182
190
  if (multiple) {
183
191
  const values = Array.isArray(value) ? value : [];
184
192
  return values
185
193
  .map((v) => {
186
194
  const found = allOptions.find((o) => o.value === v);
187
- if (!found && import.meta.env?.DEV) {
195
+ if (!found && import.meta.env?.DEV && !warnedOrphanValues.has(v)) {
196
+ warnedOrphanValues.add(v);
188
197
  console.warn(
189
198
  `[Select] value ${JSON.stringify(v)} has no matching option — dropped from selection.`
190
199
  );
@@ -195,7 +204,8 @@
195
204
  }
196
205
  if (value === null || value === undefined) return [];
197
206
  const found = allOptions.find((o) => o.value === value);
198
- if (!found && import.meta.env?.DEV) {
207
+ if (!found && import.meta.env?.DEV && !warnedOrphanValues.has(value)) {
208
+ warnedOrphanValues.add(value);
199
209
  console.warn(
200
210
  `[Select] value ${JSON.stringify(value)} has no matching option — trigger will fall back to placeholder.`
201
211
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/blocks",
3
- "version": "6.26.3",
3
+ "version": "6.27.0",
4
4
  "description": "Svelte 5 UI component library with Tailwind CSS 4, OKLCH design tokens and zero runtime dependencies",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -92,8 +92,8 @@
92
92
  "@sveltejs/package": "^2.5.8",
93
93
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
94
94
  "@tailwindcss/vite": "^4.3.1",
95
- "@urbicon-ui/i18n": "6.26.3",
96
- "@urbicon-ui/shared-types": "6.26.3",
95
+ "@urbicon-ui/i18n": "6.27.0",
96
+ "@urbicon-ui/shared-types": "6.27.0",
97
97
  "prettier": "^3.8.4",
98
98
  "prettier-plugin-svelte": "^4.1.1",
99
99
  "prettier-plugin-tailwindcss": "^0.8.0",