@shadow-js/ui 1.0.0 → 1.0.2

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,4 +1,3 @@
1
- //#region src/cli/schema.d.ts
2
1
  /**
3
2
  * The shapes the CLI reads: the registry index (`registry.json`) and the project's
4
3
  * `components.json`, with validators that turn untrusted JSON into typed values or a clear error.
@@ -69,5 +68,4 @@ export interface RegistryIndex {
69
68
  /** Every installable item, in registry order. */
70
69
  readonly items: readonly RegistryItem[];
71
70
  }
72
- //#endregion
73
71
  //# sourceMappingURL=schema.d.ts.map
@@ -1,4 +1,3 @@
1
- //#region src/locales/ar.d.ts
2
1
  /**
3
2
  * The Arabic messages of the UI kit, as a catalog of messages: every label, description and live-region text the components show, keyed
4
3
  * by component. Pass it (or a copy with your own words) to the strings provider of your project, or into a `createI18n` catalog.
@@ -136,6 +135,5 @@ declare const ar: {
136
135
  readonly notifications: "الإشعارات";
137
136
  };
138
137
  };
139
- //#endregion
140
138
  export { ar as default };
141
139
  //# sourceMappingURL=ar.d.ts.map
@@ -1,4 +1,4 @@
1
- //#region src/locales/ar.ts
1
+ // src/locales/ar.ts
2
2
  /**
3
3
  * The Arabic messages of the UI kit, as a catalog of messages: every label, description and live-region text the components show, keyed
4
4
  * by component. Pass it (or a copy with your own words) to the strings provider of your project, or into a `createI18n` catalog.
@@ -122,7 +122,6 @@ const ar = {
122
122
  notifications: "الإشعارات"
123
123
  }
124
124
  };
125
- //#endregion
126
125
  export { ar as default };
127
126
 
128
127
  //# sourceMappingURL=ar.js.map
@@ -1,4 +1,3 @@
1
- //#region src/locales/en.d.ts
2
1
  /**
3
2
  * The English messages of the UI kit, as a catalog of messages: every label, description and live-region text the components show, keyed
4
3
  * by component. Pass it (or a copy with your own words) to the strings provider of your project, or into a `createI18n` catalog.
@@ -136,6 +135,5 @@ declare const en: {
136
135
  readonly notifications: "Notifications";
137
136
  };
138
137
  };
139
- //#endregion
140
138
  export { en as default };
141
139
  //# sourceMappingURL=en.d.ts.map
@@ -1,4 +1,4 @@
1
- //#region src/locales/en.ts
1
+ // src/locales/en.ts
2
2
  /**
3
3
  * The English messages of the UI kit, as a catalog of messages: every label, description and live-region text the components show, keyed
4
4
  * by component. Pass it (or a copy with your own words) to the strings provider of your project, or into a `createI18n` catalog.
@@ -122,7 +122,6 @@ const en = {
122
122
  notifications: "Notifications"
123
123
  }
124
124
  };
125
- //#endregion
126
125
  export { en as default };
127
126
 
128
127
  //# sourceMappingURL=en.js.map
@@ -1,4 +1,3 @@
1
- //#region registry/utils/utils.d.ts
2
1
  /**
3
2
  * Anything a ShadowJS `class` attribute accepts: strings, numbers, nested (readonly) arrays and
4
3
  * `{ name: condition }` objects. Falsy values are skipped.
@@ -25,5 +24,4 @@ export type ClassValue = string | number | bigint | boolean | null | undefined |
25
24
  * @see {@link ClassValue}
26
25
  */
27
26
  export declare function cn(...inputs: ClassValue[]): string;
28
- //#endregion
29
27
  //# sourceMappingURL=utils.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { clsx } from "clsx";
2
2
  import { twMerge } from "tailwind-merge";
3
- //#region registry/utils/utils.ts
3
+ // registry/utils/utils.ts
4
4
  /**
5
5
  * Joins class names (strings, arrays, `{ name: condition }` objects) and resolves Tailwind
6
6
  * conflicts, so the last utility wins: `cn("px-2", props.class)` lets the caller override `px-2`.
@@ -21,7 +21,6 @@ import { twMerge } from "tailwind-merge";
21
21
  function cn(...inputs) {
22
22
  return twMerge(clsx(...inputs));
23
23
  }
24
- //#endregion
25
24
  export { cn };
26
25
 
27
26
  //# sourceMappingURL=utils.js.map
@@ -1,4 +1,3 @@
1
- //#region src/theme/theme-logic.d.ts
2
1
  /**
3
2
  * Pure theme logic, shared by the provider and the no-flash script so both agree on what
4
3
  * "system", "light" and "dark" mean. Nothing here touches `window` unless it is handed an object.
@@ -183,5 +182,4 @@ export declare function themeScript(options?: ThemeOptions): string;
183
182
  export declare function themeScriptTag(options?: ThemeOptions & {
184
183
  readonly nonce?: string | undefined;
185
184
  }): string;
186
- //#endregion
187
185
  //# sourceMappingURL=theme-logic.d.ts.map
@@ -1,4 +1,4 @@
1
- //#region src/theme/theme-logic.ts
1
+ // src/theme/theme-logic.ts
2
2
  /**
3
3
  * The `localStorage` key that holds the theme choice when `storageKey` is not given.
4
4
  * @example
@@ -178,7 +178,6 @@ function themeScript(options = {}) {
178
178
  function themeScriptTag(options = {}) {
179
179
  return `<script${options.nonce === void 0 ? "" : ` nonce="${options.nonce.replace(/[^A-Za-z0-9+/=_-]/g, "")}"`}>${themeScript(options)}<\/script>`;
180
180
  }
181
- //#endregion
182
181
  export { DEFAULT_STORAGE_KEY, applyTheme, isTheme, readStoredTheme, resolveTheme, themeScript, themeScriptTag, writeStoredTheme };
183
182
 
184
183
  //# sourceMappingURL=theme-logic.js.map
@@ -1,7 +1,6 @@
1
1
  import { ResolvedTheme, Theme, ThemeOptions } from "./theme-logic.js";
2
2
  import { Accessor } from "@shadow-js/core";
3
3
  import { JsxChild } from "@shadow-js/web/jsx-runtime";
4
- //#region src/theme/theme-provider.d.ts
5
4
  /**
6
5
  * What {@link useTheme} returns. Every value is an accessor: call it to read.
7
6
  * @since 1.0.0
@@ -86,5 +85,4 @@ export declare function ThemeProvider(props: ThemeProviderProps): JsxChild;
86
85
  * @since 1.0.0
87
86
  */
88
87
  export declare function useTheme(): ThemeContextValue;
89
- //#endregion
90
88
  //# sourceMappingURL=theme-provider.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { applyTheme, isTheme, readStoredTheme, resolveTheme, writeStoredTheme } from "./theme-logic.js";
2
2
  import { batch, createContext, onCleanup, onMount, untrack, useContext, useEffect, useMemo, useSignal } from "@shadow-js/core";
3
- //#region src/theme/theme-provider.ts
3
+ // src/theme/theme-provider.ts
4
4
  const MISSING = {
5
5
  theme: () => "system",
6
6
  resolvedTheme: () => void 0,
@@ -144,7 +144,6 @@ function useTheme() {
144
144
  if (value === MISSING) throw new Error("useTheme() must be used within <ThemeProvider>");
145
145
  return value;
146
146
  }
147
- //#endregion
148
147
  export { ThemeProvider, useTheme };
149
148
 
150
149
  //# sourceMappingURL=theme-provider.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shadow-js/ui",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "shadcn-style components you add to your project with a CLI, styled with Tailwind v4",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -60,18 +60,18 @@
60
60
  "check": "npm run lint && npm run typecheck && npm run build && npm test && npm run test:perf && npm run test:gc"
61
61
  },
62
62
  "peerDependencies": {
63
- "@shadow-js/core": "^1.0.0",
64
- "@shadow-js/primitives": "^1.0.0",
65
- "@shadow-js/web": "^1.0.0"
63
+ "@shadow-js/core": "^1.0.2",
64
+ "@shadow-js/primitives": "^1.0.2",
65
+ "@shadow-js/web": "^1.0.2"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@biomejs/biome": "2.5.15",
69
- "@shadow-js/compiler": "^1.0.0",
70
- "@shadow-js/core": "^1.0.0",
71
- "@shadow-js/forms": "^1.0.0",
72
- "@shadow-js/primitives": "^1.0.0",
73
- "@shadow-js/testing": "^1.0.0",
74
- "@shadow-js/web": "^1.0.0",
69
+ "@shadow-js/compiler": "^1.0.2",
70
+ "@shadow-js/core": "^1.0.2",
71
+ "@shadow-js/forms": "^1.0.2",
72
+ "@shadow-js/primitives": "^1.0.2",
73
+ "@shadow-js/testing": "^1.0.2",
74
+ "@shadow-js/web": "^1.0.2",
75
75
  "@tailwindcss/node": "4.3.3",
76
76
  "@types/node": "^24.0.0",
77
77
  "fast-check": "^4.10.2",
@@ -84,7 +84,7 @@
84
84
  "vitest": "5.0.3"
85
85
  },
86
86
  "dependencies": {
87
- "@shadow-js/table": "1.0.0",
87
+ "@shadow-js/table": "1.0.2",
88
88
  "class-variance-authority": "0.7.1",
89
89
  "clsx": "2.1.1",
90
90
  "tailwind-merge": "3.7.0"
@@ -1,7 +1,9 @@
1
1
  import { onCleanup, onMount, splitProps, useEffect, useSignal } from "@shadow-js/core";
2
2
  import {
3
+ callHandler,
3
4
  createPartContext,
4
5
  type Direction,
6
+ mergeElementProps,
5
7
  useDirectionAround,
6
8
  useRefSignal,
7
9
  } from "@shadow-js/primitives";
@@ -99,6 +101,7 @@ interface CarouselContextValue {
99
101
  readonly setViewport: (element: HTMLElement | undefined) => void;
100
102
  readonly claimSlide: () => number;
101
103
  readonly registerCount: () => void;
104
+ readonly releaseSlide: () => void;
102
105
  readonly pause: (paused: boolean) => void;
103
106
  readonly labels: () => ResolvedLabels;
104
107
  /** True once the carousel is mounted in the browser; the slide names wait for it (see CarouselItem). */
@@ -306,6 +309,12 @@ function Carousel(props: CarouselProps): JSX.Element {
306
309
  const [ready, setReady] = useSignal(false);
307
310
  let viewport: HTMLElement | undefined;
308
311
  let claimed = 0;
312
+ // The slide count the "reInit" listeners were last told, and whether the carousel is gone.
313
+ let reported = 0;
314
+ let disposed = false;
315
+ onCleanup(() => {
316
+ disposed = true;
317
+ });
309
318
  // The slide a programmatic move is heading for, until the scroll arrives there. While it is set,
310
319
  // the intermediate positions of a smooth scroll do not move the index back.
311
320
  let pending: number | undefined;
@@ -376,6 +385,27 @@ function Carousel(props: CarouselProps): JSX.Element {
376
385
  if (typeof viewport.scrollTo !== "function") viewport.scrollLeft = position * sign;
377
386
  }
378
387
  };
388
+ /** Tells the "reInit" listeners when the slide count differs from what they last heard. */
389
+ const announceCount = (): void => {
390
+ if (!ready()) return;
391
+ const total = count();
392
+ if (total === reported) return;
393
+ reported = total;
394
+ // Slides were removed: the current one may be gone.
395
+ if (total > 0 && index() > total - 1) setIndex(total - 1);
396
+ emit("reInit");
397
+ };
398
+ // A list that is replaced removes every slide and adds new ones in the same turn, and a carousel
399
+ // that unmounts removes them all: wait for the final count before telling anyone, once.
400
+ let announcing = false;
401
+ const scheduleAnnounce = (): void => {
402
+ if (!ready() || announcing) return;
403
+ announcing = true;
404
+ queueMicrotask(() => {
405
+ announcing = false;
406
+ if (!disposed) announceCount();
407
+ });
408
+ };
379
409
  const canPrevious = (): boolean => loops() || index() > 0;
380
410
  const canNext = (): boolean => loops() || index() < count() - 1;
381
411
 
@@ -402,9 +432,13 @@ function Carousel(props: CarouselProps): JSX.Element {
402
432
  return slot;
403
433
  },
404
434
  registerCount() {
405
- const changed = claimed !== count();
406
435
  setCount(claimed);
407
- if (changed && ready()) emit("reInit");
436
+ scheduleAnnounce();
437
+ },
438
+ releaseSlide() {
439
+ claimed = Math.max(0, claimed - 1);
440
+ setCount(claimed);
441
+ scheduleAnnounce();
408
442
  },
409
443
  pause: (value) => setPaused(value),
410
444
  labels,
@@ -436,6 +470,7 @@ function Carousel(props: CarouselProps): JSX.Element {
436
470
  onMount(() => {
437
471
  setRegionElement(regionNode);
438
472
  setReady(true);
473
+ reported = count();
439
474
  const start = opts().startIndex;
440
475
  if (start !== undefined && start > 0) scrollToIndex(start, true);
441
476
  (local.setApi as ((api: CarouselApi) => void) | undefined)?.(api);
@@ -445,7 +480,8 @@ function Carousel(props: CarouselProps): JSX.Element {
445
480
  // `autoplay` prop: changing it retimes the interval, and removing it stops the interval.
446
481
  useEffect(() => {
447
482
  const interval = local.autoplay as number | undefined;
448
- if (interval === undefined || interval <= 0) return undefined;
483
+ // NaN and Infinity must not reach setInterval: a NaN delay fires every tick and an Infinity one wraps to 0.
484
+ if (interval === undefined || !Number.isFinite(interval) || interval <= 0) return undefined;
449
485
  if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches)
450
486
  return undefined;
451
487
  const timer = setInterval(() => {
@@ -489,12 +525,24 @@ function Carousel(props: CarouselProps): JSX.Element {
489
525
  class={cn("relative", local.class)}
490
526
  onKeyDown={(event: KeyboardEvent) => {
491
527
  onKeyDown(event);
492
- (local.onKeyDown as ((event: KeyboardEvent) => void) | undefined)?.(event);
528
+ callHandler(local.onKeyDown, event);
529
+ }}
530
+ onPointerEnter={(event: PointerEvent) => {
531
+ callHandler(local.onPointerEnter, event);
532
+ setPaused(true);
533
+ }}
534
+ onPointerLeave={(event: PointerEvent) => {
535
+ callHandler(local.onPointerLeave, event);
536
+ setPaused(false);
537
+ }}
538
+ onFocusIn={(event: FocusEvent) => {
539
+ callHandler(local.onFocusIn, event);
540
+ setPaused(true);
541
+ }}
542
+ onFocusOut={(event: FocusEvent) => {
543
+ callHandler(local.onFocusOut, event);
544
+ setPaused(false);
493
545
  }}
494
- onPointerEnter={() => setPaused(true)}
495
- onPointerLeave={() => setPaused(false)}
496
- onFocusIn={() => setPaused(true)}
497
- onFocusOut={() => setPaused(false)}
498
546
  {...rest}
499
547
  >
500
548
  {local.children as JSX.Element}
@@ -551,15 +599,15 @@ function CarouselContent(props: ComponentProps<"div">): JSX.Element {
551
599
  local.class,
552
600
  )}
553
601
  onScroll={(event: Event) => {
554
- (local.onScroll as ((event: Event) => void) | undefined)?.(event);
602
+ callHandler(local.onScroll, event);
555
603
  carousel.scrolled();
556
604
  }}
557
605
  onWheel={(event: WheelEvent) => {
558
- (local.onWheel as ((event: WheelEvent) => void) | undefined)?.(event);
606
+ callHandler(local.onWheel, event);
559
607
  carousel.userMoved();
560
608
  }}
561
609
  onPointerDown={(event: PointerEvent) => {
562
- (local.onPointerDown as ((event: PointerEvent) => void) | undefined)?.(event);
610
+ callHandler(local.onPointerDown, event);
563
611
  carousel.userMoved();
564
612
  if (event.button !== 0 || event.pointerType === "touch") return;
565
613
  const element = event.currentTarget as HTMLElement;
@@ -630,12 +678,28 @@ function CarouselItem(props: ComponentProps<"div">): JSX.Element {
630
678
  // before the next one is counted; so a slide is named only once the carousel is mounted, and
631
679
  // the name then uses the final count (UI-CRS-18).
632
680
  carousel.registerCount();
681
+ // A slide that goes away (a list that shrinks or is replaced) no longer counts.
682
+ onCleanup(() => carousel.releaseSlide());
683
+ let node: HTMLElement | undefined;
684
+ // The position among the slides that exist now, from the DOM order: the slot a slide claimed when
685
+ // it rendered is stale once another slide has been removed or inserted before it.
686
+ const position = (): number => {
687
+ carousel.count();
688
+ const parent = node?.parentElement;
689
+ if (node === undefined || parent === null || parent === undefined) return slot + 1;
690
+ const slides = Array.from(parent.children).filter(
691
+ (child) => child.getAttribute("data-slot") === "carousel-item",
692
+ );
693
+ const at = slides.indexOf(node);
694
+ return at === -1 ? slot + 1 : at + 1;
695
+ };
696
+ const merged = mergeElementProps({ ref: (element: HTMLElement) => (node = element) }, rest);
633
697
  return (
634
698
  <div
635
699
  role="group"
636
700
  aria-roledescription="slide"
637
701
  aria-label={
638
- carousel.ready() ? carousel.labels().slide(slot + 1, carousel.count()) : undefined
702
+ carousel.ready() ? carousel.labels().slide(position(), carousel.count()) : undefined
639
703
  }
640
704
  data-slot="carousel-item"
641
705
  class={cn(
@@ -643,7 +707,7 @@ function CarouselItem(props: ComponentProps<"div">): JSX.Element {
643
707
  carousel.orientation() === "vertical" ? "pt-4" : "ps-4",
644
708
  local.class,
645
709
  )}
646
- {...rest}
710
+ {...merged}
647
711
  />
648
712
  );
649
713
  }
@@ -699,7 +763,7 @@ function CarouselPrevious(props: ComponentProps<"button">): JSX.Element {
699
763
  )}
700
764
  disabled={!carousel.canPrevious()}
701
765
  onClick={(event: MouseEvent) => {
702
- (local.onClick as ((event: MouseEvent) => void) | undefined)?.(event);
766
+ callHandler(local.onClick, event);
703
767
  carousel.previous();
704
768
  }}
705
769
  {...rest}
@@ -764,7 +828,7 @@ function CarouselNext(props: ComponentProps<"button">): JSX.Element {
764
828
  )}
765
829
  disabled={!carousel.canNext()}
766
830
  onClick={(event: MouseEvent) => {
767
- (local.onClick as ((event: MouseEvent) => void) | undefined)?.(event);
831
+ callHandler(local.onClick, event);
768
832
  carousel.next();
769
833
  }}
770
834
  {...rest}
@@ -220,7 +220,11 @@ function ChartContainer(props: ChartContainerProps): JSX.Element {
220
220
  "children",
221
221
  ]);
222
222
  const uiLocale = useUiLocale();
223
- const [width, setWidth] = useSignal<number>((local.initialWidth as number | undefined) ?? 600);
223
+ // A NaN or Infinity width would put "NaN" into every path; fall back to the default instead.
224
+ const initial = local.initialWidth as number | undefined;
225
+ const [width, setWidth] = useSignal<number>(
226
+ initial !== undefined && Number.isFinite(initial) ? initial : 600,
227
+ );
224
228
  const [element, setElement] = useRefSignal<HTMLElement>();
225
229
  let node: HTMLElement | undefined;
226
230
  const dir = useDirectionAround(() => local.dir as Direction | undefined, element);
@@ -1,4 +1,5 @@
1
1
  import { splitProps } from "@shadow-js/core";
2
+ import { callHandler } from "@shadow-js/primitives";
2
3
  import type { ComponentProps, JSX } from "@shadow-js/web";
3
4
  import { cva, type VariantProps } from "class-variance-authority";
4
5
  import { cn } from "@/lib/utils";
@@ -117,7 +118,7 @@ function InputGroupAddon(props: InputGroupAddonProps): JSX.Element {
117
118
  const [local, rest] = splitProps(props, ["class", "align", "onClick"]);
118
119
  // The application's own click handler runs first; the focus move is the default.
119
120
  const onClick = (event: MouseEvent): void => {
120
- (local.onClick as ((event: MouseEvent) => void) | undefined)?.(event);
121
+ callHandler(local.onClick, event);
121
122
  const target = event.target;
122
123
  if (target instanceof Element && target.closest("button") !== null) return;
123
124
  const group = (event.currentTarget as HTMLElement).closest('[data-slot="input-group"]');