@repraze/kernelkit 0.1.0-alpha.3 → 0.1.0-alpha.5

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.
Files changed (203) hide show
  1. package/README.md +17 -0
  2. package/dist/components/alert-dialog.d.ts.map +1 -1
  3. package/dist/components/alert-dialog.js +4 -1
  4. package/dist/components/alert-dialog.js.map +1 -1
  5. package/dist/components/alert.js +3 -3
  6. package/dist/components/alert.js.map +1 -1
  7. package/dist/components/badge.js +1 -1
  8. package/dist/components/badge.js.map +1 -1
  9. package/dist/components/button.js +1 -1
  10. package/dist/components/button.js.map +1 -1
  11. package/dist/components/calendar.d.ts +8 -1
  12. package/dist/components/calendar.d.ts.map +1 -1
  13. package/dist/components/calendar.js +33 -11
  14. package/dist/components/calendar.js.map +1 -1
  15. package/dist/components/callout.js +2 -2
  16. package/dist/components/callout.js.map +1 -1
  17. package/dist/components/combobox.js +1 -1
  18. package/dist/components/combobox.js.map +1 -1
  19. package/dist/components/date-picker.d.ts +11 -5
  20. package/dist/components/date-picker.d.ts.map +1 -1
  21. package/dist/components/date-picker.js +29 -7
  22. package/dist/components/date-picker.js.map +1 -1
  23. package/dist/components/description-list.d.ts +26 -0
  24. package/dist/components/description-list.d.ts.map +1 -0
  25. package/dist/components/description-list.js +59 -0
  26. package/dist/components/description-list.js.map +1 -0
  27. package/dist/components/locale.d.ts +19 -0
  28. package/dist/components/locale.d.ts.map +1 -0
  29. package/dist/components/locale.js +54 -0
  30. package/dist/components/locale.js.map +1 -0
  31. package/dist/components/popover.d.ts +1 -1
  32. package/dist/components/popover.d.ts.map +1 -1
  33. package/dist/components/popover.js +2 -2
  34. package/dist/components/popover.js.map +1 -1
  35. package/dist/components/progress.d.ts +19 -6
  36. package/dist/components/progress.d.ts.map +1 -1
  37. package/dist/components/progress.js +18 -3
  38. package/dist/components/progress.js.map +1 -1
  39. package/dist/components/search-dialog.d.ts +6 -1
  40. package/dist/components/search-dialog.d.ts.map +1 -1
  41. package/dist/components/search-dialog.js +13 -1
  42. package/dist/components/search-dialog.js.map +1 -1
  43. package/dist/components/search-trigger.d.ts +4 -1
  44. package/dist/components/search-trigger.d.ts.map +1 -1
  45. package/dist/components/search-trigger.js +5 -2
  46. package/dist/components/search-trigger.js.map +1 -1
  47. package/dist/components/shimmer.d.ts +13 -0
  48. package/dist/components/shimmer.d.ts.map +1 -0
  49. package/dist/components/shimmer.js +21 -0
  50. package/dist/components/shimmer.js.map +1 -0
  51. package/dist/components/spinner.d.ts +28 -6
  52. package/dist/components/spinner.d.ts.map +1 -1
  53. package/dist/components/spinner.js +66 -14
  54. package/dist/components/spinner.js.map +1 -1
  55. package/dist/components/stepper.d.ts +64 -0
  56. package/dist/components/stepper.d.ts.map +1 -0
  57. package/dist/components/stepper.js +167 -0
  58. package/dist/components/stepper.js.map +1 -0
  59. package/dist/components/tag-input.d.ts +47 -0
  60. package/dist/components/tag-input.d.ts.map +1 -0
  61. package/dist/components/tag-input.js +211 -0
  62. package/dist/components/tag-input.js.map +1 -0
  63. package/dist/components/time-input.d.ts +75 -0
  64. package/dist/components/time-input.d.ts.map +1 -0
  65. package/dist/components/time-input.js +341 -0
  66. package/dist/components/time-input.js.map +1 -0
  67. package/dist/components/time-select.d.ts +49 -0
  68. package/dist/components/time-select.d.ts.map +1 -0
  69. package/dist/components/time-select.js +178 -0
  70. package/dist/components/time-select.js.map +1 -0
  71. package/dist/components/timeline.d.ts +41 -0
  72. package/dist/components/timeline.d.ts.map +1 -0
  73. package/dist/components/timeline.js +83 -0
  74. package/dist/components/timeline.js.map +1 -0
  75. package/dist/components/toast.d.ts.map +1 -1
  76. package/dist/components/toast.js +4 -2
  77. package/dist/components/toast.js.map +1 -1
  78. package/dist/components/tree.d.ts +87 -0
  79. package/dist/components/tree.d.ts.map +1 -0
  80. package/dist/components/tree.js +516 -0
  81. package/dist/components/tree.js.map +1 -0
  82. package/dist/docs/doc-kit.d.ts +72 -0
  83. package/dist/docs/doc-kit.d.ts.map +1 -0
  84. package/dist/docs/doc-kit.js +45 -0
  85. package/dist/docs/doc-kit.js.map +1 -0
  86. package/dist/docs/doc-page.d.ts +27 -0
  87. package/dist/docs/doc-page.d.ts.map +1 -0
  88. package/dist/docs/doc-page.js +33 -0
  89. package/dist/docs/doc-page.js.map +1 -0
  90. package/dist/docs/docs-index.d.ts +60 -0
  91. package/dist/docs/docs-index.d.ts.map +1 -0
  92. package/dist/docs/docs-index.js +45 -0
  93. package/dist/docs/docs-index.js.map +1 -0
  94. package/dist/docs/docs-nav.d.ts +13 -0
  95. package/dist/docs/docs-nav.d.ts.map +1 -0
  96. package/dist/docs/docs-nav.js +8 -0
  97. package/dist/docs/docs-nav.js.map +1 -0
  98. package/dist/docs/docs-search.d.ts +19 -0
  99. package/dist/docs/docs-search.d.ts.map +1 -0
  100. package/dist/docs/docs-search.js +33 -0
  101. package/dist/docs/docs-search.js.map +1 -0
  102. package/dist/docs/index.d.ts +7 -0
  103. package/dist/docs/index.d.ts.map +1 -0
  104. package/dist/docs/index.js +7 -0
  105. package/dist/docs/index.js.map +1 -0
  106. package/dist/docs/review-matrix.d.ts +25 -0
  107. package/dist/docs/review-matrix.d.ts.map +1 -0
  108. package/dist/docs/review-matrix.js +31 -0
  109. package/dist/docs/review-matrix.js.map +1 -0
  110. package/dist/hooks/use-theme-palette.d.ts +8 -0
  111. package/dist/hooks/use-theme-palette.d.ts.map +1 -0
  112. package/dist/hooks/use-theme-palette.js +24 -0
  113. package/dist/hooks/use-theme-palette.js.map +1 -0
  114. package/dist/hooks/use-time-format.d.ts +23 -0
  115. package/dist/hooks/use-time-format.d.ts.map +1 -0
  116. package/dist/hooks/use-time-format.js +52 -0
  117. package/dist/hooks/use-time-format.js.map +1 -0
  118. package/dist/lib/color.d.ts +5 -1
  119. package/dist/lib/color.d.ts.map +1 -1
  120. package/dist/lib/color.js +14 -2
  121. package/dist/lib/color.js.map +1 -1
  122. package/dist/lib/recipes.d.ts +5 -4
  123. package/dist/lib/recipes.d.ts.map +1 -1
  124. package/dist/lib/recipes.js +7 -5
  125. package/dist/lib/recipes.js.map +1 -1
  126. package/dist/provider/theme-provider.d.ts +10 -1
  127. package/dist/provider/theme-provider.d.ts.map +1 -1
  128. package/dist/provider/theme-provider.js +23 -8
  129. package/dist/provider/theme-provider.js.map +1 -1
  130. package/dist/site/brand.d.ts +20 -0
  131. package/dist/site/brand.d.ts.map +1 -0
  132. package/dist/site/brand.js +17 -0
  133. package/dist/site/brand.js.map +1 -0
  134. package/dist/site/github-icon.d.ts +4 -0
  135. package/dist/site/github-icon.d.ts.map +1 -0
  136. package/dist/site/github-icon.js +6 -0
  137. package/dist/site/github-icon.js.map +1 -0
  138. package/dist/site/index.d.ts +6 -0
  139. package/dist/site/index.d.ts.map +1 -0
  140. package/dist/site/index.js +6 -0
  141. package/dist/site/index.js.map +1 -0
  142. package/dist/site/suite-footer.d.ts +24 -0
  143. package/dist/site/suite-footer.d.ts.map +1 -0
  144. package/dist/site/suite-footer.js +19 -0
  145. package/dist/site/suite-footer.js.map +1 -0
  146. package/dist/site/suite-header.d.ts +40 -0
  147. package/dist/site/suite-header.d.ts.map +1 -0
  148. package/dist/site/suite-header.js +19 -0
  149. package/dist/site/suite-header.js.map +1 -0
  150. package/dist/site/use-page-title.d.ts +3 -0
  151. package/dist/site/use-page-title.d.ts.map +1 -0
  152. package/dist/site/use-page-title.js +8 -0
  153. package/dist/site/use-page-title.js.map +1 -0
  154. package/dist/vite/index.d.ts +65 -0
  155. package/dist/vite/index.d.ts.map +1 -0
  156. package/dist/vite/index.js +242 -0
  157. package/dist/vite/index.js.map +1 -0
  158. package/package.json +39 -3
  159. package/src/components/alert-dialog.tsx +4 -1
  160. package/src/components/alert.tsx +3 -3
  161. package/src/components/badge.tsx +1 -1
  162. package/src/components/button.tsx +1 -1
  163. package/src/components/calendar.tsx +48 -4
  164. package/src/components/callout.tsx +2 -2
  165. package/src/components/combobox.tsx +1 -1
  166. package/src/components/date-picker.tsx +43 -9
  167. package/src/components/description-list.tsx +107 -0
  168. package/src/components/locale.tsx +67 -0
  169. package/src/components/popover.tsx +3 -1
  170. package/src/components/progress.tsx +70 -4
  171. package/src/components/search-dialog.tsx +14 -1
  172. package/src/components/search-trigger.tsx +8 -2
  173. package/src/components/shimmer.tsx +36 -0
  174. package/src/components/spinner.tsx +126 -23
  175. package/src/components/stepper.tsx +346 -0
  176. package/src/components/tag-input.tsx +345 -0
  177. package/src/components/time-input.tsx +578 -0
  178. package/src/components/time-select.tsx +291 -0
  179. package/src/components/timeline.tsx +190 -0
  180. package/src/components/toast.tsx +4 -4
  181. package/src/components/tree.tsx +832 -0
  182. package/src/docs/doc-kit.tsx +176 -0
  183. package/src/docs/doc-page.tsx +126 -0
  184. package/src/docs/docs-index.ts +108 -0
  185. package/src/docs/docs-nav.tsx +47 -0
  186. package/src/docs/docs-search.tsx +88 -0
  187. package/src/docs/index.ts +6 -0
  188. package/src/docs/review-matrix.tsx +67 -0
  189. package/src/hooks/use-theme-palette.ts +25 -0
  190. package/src/hooks/use-time-format.ts +72 -0
  191. package/src/lib/color.ts +19 -2
  192. package/src/lib/recipes.ts +7 -5
  193. package/src/provider/theme-provider.tsx +34 -9
  194. package/src/site/brand.tsx +41 -0
  195. package/src/site/github-icon.tsx +10 -0
  196. package/src/site/index.ts +5 -0
  197. package/src/site/suite-footer.tsx +68 -0
  198. package/src/site/suite-header.tsx +114 -0
  199. package/src/site/use-page-title.ts +8 -0
  200. package/src/styles/kernelkit.css +58 -0
  201. package/src/styles/site.css +15 -0
  202. package/src/vite/client.d.ts +11 -0
  203. package/src/vite/index.ts +298 -0
@@ -0,0 +1,107 @@
1
+ "use client";
2
+
3
+ import {cva} from "class-variance-authority";
4
+ import {cn} from "cn";
5
+ import * as React from "react";
6
+
7
+ type DescriptionListOrientation = "horizontal" | "vertical";
8
+
9
+ const DescriptionListContext = React.createContext<{orientation: DescriptionListOrientation; divided: boolean}>({
10
+ orientation: "horizontal",
11
+ divided: false,
12
+ });
13
+
14
+ const descriptionListVariants = cva("text-sm", {
15
+ variants: {
16
+ orientation: {
17
+ // one shared term column: every item is a subgrid row, so terms line up across rows
18
+ horizontal: "grid grid-cols-[minmax(0,max-content)_minmax(0,1fr)] gap-x-6",
19
+ vertical: "flex flex-col",
20
+ },
21
+ divided: {
22
+ true: "",
23
+ false: "",
24
+ },
25
+ },
26
+ compoundVariants: [
27
+ {orientation: "horizontal", divided: false, class: "gap-y-2.5"},
28
+ {orientation: "vertical", divided: false, class: "gap-4"},
29
+ ],
30
+ defaultVariants: {orientation: "horizontal", divided: false},
31
+ });
32
+
33
+ interface DescriptionListProps extends React.ComponentProps<"dl"> {
34
+ /** `horizontal`: terms in a column beside their details. `vertical`: each term above its details. */
35
+ orientation?: DescriptionListOrientation;
36
+ /** Rule between items. */
37
+ divided?: boolean;
38
+ }
39
+
40
+ /** Key / value pairs: properties of a server, an order, a file. Terms in the label face, details in the body face. */
41
+ function DescriptionList({orientation = "horizontal", divided = false, className, ...props}: DescriptionListProps) {
42
+ const ctx = React.useMemo(() => ({orientation, divided}), [orientation, divided]);
43
+ return (
44
+ <DescriptionListContext.Provider value={ctx}>
45
+ <dl
46
+ data-slot="description-list"
47
+ data-orientation={orientation}
48
+ data-divided={divided ? "" : undefined}
49
+ className={cn(descriptionListVariants({orientation, divided}), className)}
50
+ {...props}
51
+ />
52
+ </DescriptionListContext.Provider>
53
+ );
54
+ }
55
+
56
+ const descriptionItemVariants = cva("min-w-0", {
57
+ variants: {
58
+ orientation: {
59
+ horizontal: "col-span-2 grid grid-cols-subgrid items-baseline",
60
+ vertical: "flex flex-col gap-1",
61
+ },
62
+ divided: {
63
+ true: "border-b border-border py-2.5 first:pt-0 last:border-b-0 last:pb-0",
64
+ false: "",
65
+ },
66
+ },
67
+ defaultVariants: {orientation: "horizontal", divided: false},
68
+ });
69
+
70
+ /** One term with its details (a `<div>`, which `<dl>` allows around each group). */
71
+ function DescriptionItem({className, ...props}: React.ComponentProps<"div">) {
72
+ const {orientation, divided} = React.useContext(DescriptionListContext);
73
+ return (
74
+ <div
75
+ data-slot="description-item"
76
+ data-orientation={orientation}
77
+ data-divided={divided ? "" : undefined}
78
+ className={cn(descriptionItemVariants({orientation, divided}), className)}
79
+ {...props}
80
+ />
81
+ );
82
+ }
83
+
84
+ function DescriptionTerm({className, ...props}: React.ComponentProps<"dt">) {
85
+ return (
86
+ <dt
87
+ data-slot="description-term"
88
+ className={cn("min-w-0 font-alt text-xs text-muted-foreground", className)}
89
+ {...props}
90
+ />
91
+ );
92
+ }
93
+
94
+ /** The value. Inline content (a status dot, a copy button) sits on one row with `flex items-center gap-2`. */
95
+ function DescriptionDetails({className, ...props}: React.ComponentProps<"dd">) {
96
+ return <dd data-slot="description-details" className={cn("min-w-0 text-foreground", className)} {...props} />;
97
+ }
98
+
99
+ export {
100
+ DescriptionDetails,
101
+ DescriptionItem,
102
+ descriptionItemVariants,
103
+ DescriptionList,
104
+ descriptionListVariants,
105
+ DescriptionTerm,
106
+ };
107
+ export type {DescriptionListOrientation, DescriptionListProps};
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /**
6
+ * Locale for the kit's dates and times (Calendar, DatePicker, TimeInput, TimeSelect), all formatted with
7
+ * `Intl`. Resolution: a component's own `locale` prop › the nearest `LocaleProvider` › the browser's
8
+ * language. Hydration-safe: the server render and the hydration pass use `FALLBACK_LOCALE` (the browser's
9
+ * language is unknown on the server), then the client re-renders with the browser's. To get it right from
10
+ * the first paint in SSR, pass the request's locale (Accept-Language, user setting) to `LocaleProvider`.
11
+ */
12
+ const FALLBACK_LOCALE = "en-US";
13
+
14
+ const LocaleContext = React.createContext<string | undefined>(undefined);
15
+
16
+ function LocaleProvider({locale, children}: {locale?: string; children: React.ReactNode}) {
17
+ return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>;
18
+ }
19
+
20
+ function browserLocale(): string | null {
21
+ try {
22
+ const tag = navigator.languages?.[0] ?? navigator.language;
23
+ return tag && Intl.DateTimeFormat.supportedLocalesOf(tag).length ? tag : null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function subscribe(onChange: () => void) {
30
+ window.addEventListener("languagechange", onChange);
31
+ return () => window.removeEventListener("languagechange", onChange);
32
+ }
33
+
34
+ /** The locale to format with: `explicit` › LocaleProvider › browser (after hydration) › `FALLBACK_LOCALE`. */
35
+ function useLocale(explicit?: string): string {
36
+ const provided = React.useContext(LocaleContext);
37
+ const browser = React.useSyncExternalStore(subscribe, browserLocale, () => null);
38
+ return explicit ?? provided ?? browser ?? FALLBACK_LOCALE;
39
+ }
40
+
41
+ // Regions whose week starts on Sunday or Saturday (CLDR), for engines without Intl.Locale#getWeekInfo.
42
+ const SUNDAY = new Set(
43
+ "AG AS AU BD BR BS BT BW BZ CA CN CO DM DO ET GT GU HK HN ID IL IN JM JP KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT PY SA SG SV TH TT TW UM US VE VI WS YE ZA ZW".split(
44
+ " ",
45
+ ),
46
+ );
47
+ const SATURDAY = new Set("AE AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY".split(" "));
48
+
49
+ /** First day of the week for a locale: 0 Sunday … 6 Saturday. */
50
+ function weekStart(locale: string): 0 | 1 | 6 {
51
+ try {
52
+ const loc = new Intl.Locale(locale) as Intl.Locale & {
53
+ getWeekInfo?: () => {firstDay: number};
54
+ weekInfo?: {firstDay: number};
55
+ };
56
+ const info = loc.getWeekInfo?.() ?? loc.weekInfo;
57
+ if (info) {
58
+ return (info.firstDay % 7) as 0 | 1 | 6;
59
+ }
60
+ const region = loc.maximize().region ?? "";
61
+ return SUNDAY.has(region) ? 0 : SATURDAY.has(region) ? 6 : 1;
62
+ } catch {
63
+ return 0;
64
+ }
65
+ }
66
+
67
+ export {FALLBACK_LOCALE, LocaleProvider, useLocale, weekStart};
@@ -20,9 +20,10 @@ function PopoverContent({
20
20
  alignOffset = 0,
21
21
  side = "bottom",
22
22
  sideOffset = 4,
23
+ anchor,
23
24
  ...props
24
25
  }: PopoverPrimitive.Popup.Props &
25
- Pick<PopoverPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
26
+ Pick<PopoverPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset" | "anchor">) {
26
27
  return (
27
28
  <PopoverPrimitive.Portal container={usePortalContainer()}>
28
29
  <PopoverPrimitive.Positioner
@@ -30,6 +31,7 @@ function PopoverContent({
30
31
  alignOffset={alignOffset}
31
32
  side={side}
32
33
  sideOffset={sideOffset}
34
+ anchor={anchor}
33
35
  className="isolate z-50"
34
36
  >
35
37
  <PopoverPrimitive.Popup
@@ -2,8 +2,10 @@
2
2
 
3
3
  import {Progress as ProgressPrimitive} from "@base-ui/react/progress";
4
4
  import {type Color, colorVariants} from "@repraze/kernelkit/lib/color";
5
+ import {placeholderFill} from "@repraze/kernelkit/lib/recipes";
5
6
  import {cva} from "class-variance-authority";
6
7
  import {cn} from "cn";
8
+ import type * as React from "react";
7
9
 
8
10
  /** Root of a progress or meter: label + value on one row, track below. Sets the color vars. */
9
11
  const progressVariants = cva("group/progress flex w-full flex-wrap items-center gap-x-3 gap-y-1.5", {
@@ -29,24 +31,44 @@ interface ProgressProps extends Omit<ProgressPrimitive.Root.Props, "color"> {
29
31
  color?: Color | null;
30
32
  /** Track height: sm `h-1`, default `h-1.5`, lg `h-2`. */
31
33
  size?: ProgressSize | null;
34
+ /** `bar`: one pill track. `segmented`: a row of cells, lit in order; a wave runs through them while indeterminate. */
35
+ variant?: "bar" | "segmented";
36
+ /** `segmented` only: number of cells. */
37
+ segments?: number;
32
38
  }
33
39
 
34
- function Progress({className, children, value, color, size, ...props}: ProgressProps) {
40
+ function Progress({
41
+ className,
42
+ children,
43
+ value,
44
+ color,
45
+ size,
46
+ variant = "bar",
47
+ segments = 12,
48
+ max = 100,
49
+ ...props
50
+ }: ProgressProps) {
35
51
  const resolvedColor = color ?? "primary";
36
52
  const resolvedSize = size ?? "default";
37
53
  return (
38
54
  <ProgressPrimitive.Root
39
55
  value={value}
56
+ max={max}
40
57
  data-slot="progress"
58
+ data-variant={variant}
41
59
  data-color={resolvedColor}
42
60
  data-size={resolvedSize}
43
61
  className={cn(progressVariants({color: resolvedColor, size: resolvedSize}), className)}
44
62
  {...props}
45
63
  >
46
64
  {children}
47
- <ProgressTrack>
48
- <ProgressIndicator />
49
- </ProgressTrack>
65
+ {variant === "segmented" ? (
66
+ <ProgressSegments segments={segments} lit={value == null ? null : (value / max) * segments} />
67
+ ) : (
68
+ <ProgressTrack>
69
+ <ProgressIndicator />
70
+ </ProgressTrack>
71
+ )}
50
72
  </ProgressPrimitive.Root>
51
73
  );
52
74
  }
@@ -72,6 +94,49 @@ function ProgressIndicator({className, ...props}: ProgressPrimitive.Indicator.Pr
72
94
  );
73
95
  }
74
96
 
97
+ /** Negative delays so every cell is mid-cycle on the first frame (no dead start). */
98
+ const cellDelay = (i: number, n: number) => ({animationDelay: `${-((n - i) * 1.2) / n}s`});
99
+
100
+ /**
101
+ * The `segmented` track: cells in the fill tier, unlit ones on the placeholder fill (§4). `lit` is how many
102
+ * cells are on (fractional rounds), `null` runs the indeterminate wave. Reduced motion slows the wave down.
103
+ */
104
+ function ProgressSegments({
105
+ segments,
106
+ lit,
107
+ className,
108
+ ...props
109
+ }: React.ComponentProps<"div"> & {segments: number; lit: number | null}) {
110
+ const on = lit === null ? -1 : Math.round(Math.min(segments, Math.max(0, lit)));
111
+ return (
112
+ <div
113
+ data-slot="progress-segments"
114
+ aria-hidden
115
+ className={cn(
116
+ "flex h-1.5 w-full gap-0.5 group-data-[size=lg]/progress:h-2 group-data-[size=sm]/progress:h-1",
117
+ className,
118
+ )}
119
+ {...props}
120
+ >
121
+ {Array.from({length: segments}, (_, i) => (
122
+ <span
123
+ key={i}
124
+ data-lit={on < 0 || i < on ? "" : undefined}
125
+ className={cn(
126
+ "h-full flex-1 rounded-xs transition-colors duration-300 motion-reduce:transition-none",
127
+ on < 0
128
+ ? "animate-loader-cell bg-(--c-fill) motion-reduce:[animation-duration:3s]"
129
+ : i < on
130
+ ? "bg-(--c-fill)"
131
+ : placeholderFill,
132
+ )}
133
+ style={on < 0 ? cellDelay(i, segments) : undefined}
134
+ />
135
+ ))}
136
+ </div>
137
+ );
138
+ }
139
+
75
140
  function ProgressLabel({className, ...props}: ProgressPrimitive.Label.Props) {
76
141
  return (
77
142
  <ProgressPrimitive.Label
@@ -97,6 +162,7 @@ export {
97
162
  ProgressIndicator,
98
163
  progressIndicatorClass,
99
164
  ProgressLabel,
165
+ ProgressSegments,
100
166
  ProgressTrack,
101
167
  progressTrackClass,
102
168
  ProgressValue,
@@ -84,4 +84,17 @@ function SearchFooter({className, ...props}: React.ComponentProps<"div">) {
84
84
  );
85
85
  }
86
86
 
87
- export {SearchDialog, SearchFooter, SearchItem};
87
+ /**
88
+ * The suite's search filter for `<Command filter={substringFilter}>`: a plain substring match (prefix
89
+ * matches rank first). cmdk's default fuzzy match finds "t…a…b" in nearly every title.
90
+ */
91
+ function substringFilter(value: string, search: string, keywords?: string[]) {
92
+ const haystack = [value, ...(keywords ?? [])].join(" ").toLowerCase();
93
+ const needle = search.trim().toLowerCase();
94
+ if (haystack.startsWith(needle)) {
95
+ return 1;
96
+ }
97
+ return haystack.includes(needle) ? 0.5 : 0;
98
+ }
99
+
100
+ export {SearchDialog, SearchFooter, SearchItem, substringFilter};
@@ -51,7 +51,10 @@ interface SearchTriggerProps extends React.ComponentProps<typeof Button> {
51
51
  children?: React.ReactNode;
52
52
  }
53
53
 
54
- /** Header search button: icon, placeholder, OS-aware shortcut. Same 32px height as the header's icon buttons. Open your dialog in `onClick`. */
54
+ /**
55
+ * Header search button: icon, placeholder, OS-aware shortcut. Same 32px height as the header's icon buttons. Open your dialog in `onClick`.
56
+ * The keycap gets the same inset on the right as top/bottom (32 - 20 = 6px each side, border included).
57
+ */
55
58
  function SearchTrigger({children = "Search…", className, ...props}: SearchTriggerProps) {
56
59
  const mod = useModKey();
57
60
  return (
@@ -59,7 +62,10 @@ function SearchTrigger({children = "Search…", className, ...props}: SearchTrig
59
62
  variant="outline"
60
63
  aria-label="Search"
61
64
  data-slot="search-trigger"
62
- className={cn("w-8 justify-start px-0 text-muted-foreground sm:w-48 sm:px-2.5 md:w-56", className)}
65
+ className={cn(
66
+ "w-8 justify-start px-0 text-muted-foreground sm:w-48 sm:pr-1.25 sm:pl-2.5 md:w-56",
67
+ className,
68
+ )}
63
69
  {...props}
64
70
  >
65
71
  <SearchIcon data-icon="inline-start" className="mx-auto sm:mx-0" />
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import {mergeProps} from "@base-ui/react/merge-props";
4
+ import {useRender} from "@base-ui/react/use-render";
5
+ import {cn} from "cn";
6
+
7
+ interface ShimmerProps extends useRender.ComponentProps<"span"> {
8
+ /** Seconds per sweep. */
9
+ duration?: number;
10
+ }
11
+
12
+ /**
13
+ * Text with a highlight band sweeping across it ("Thinking…", "Indexing files…"). Muted text, foreground
14
+ * band. Reduced motion: plain muted text.
15
+ */
16
+ function Shimmer({render, duration, className, style, ...props}: ShimmerProps) {
17
+ return useRender({
18
+ defaultTagName: "span",
19
+ props: mergeProps<"span">(
20
+ {
21
+ className: cn(
22
+ "inline-block animate-shimmer bg-linear-to-r from-muted-foreground from-40% via-foreground via-50% to-muted-foreground to-60% bg-size-[250%_100%] bg-clip-text text-transparent",
23
+ "motion-reduce:animate-none motion-reduce:bg-none motion-reduce:text-muted-foreground",
24
+ className,
25
+ ),
26
+ style: duration ? {animationDuration: `${duration}s`, ...style} : style,
27
+ },
28
+ props,
29
+ ),
30
+ render,
31
+ state: {slot: "shimmer"},
32
+ });
33
+ }
34
+
35
+ export {Shimmer};
36
+ export type {ShimmerProps};
@@ -1,42 +1,145 @@
1
1
  "use client";
2
2
 
3
+ import {type Color, colorVariants} from "@repraze/kernelkit/lib/color";
4
+ import {placeholderFill} from "@repraze/kernelkit/lib/recipes";
3
5
  import {cva, type VariantProps} from "class-variance-authority";
4
6
  import {cn} from "cn";
5
7
  import {Loader2Icon} from "lucide-react";
8
+ import * as React from "react";
6
9
 
7
- /** Sizes follow the icon column of §4 so a spinner can replace an icon in place. */
8
- const spinnerVariants = cva(
9
- // reduced motion: slow the spin down rather than freeze it, a frozen spinner reads as a glyph
10
- "shrink-0 animate-spin text-current motion-reduce:[animation-duration:2.5s]",
11
- {
12
- variants: {
13
- size: {
14
- xs: "size-3",
15
- sm: "size-3.5",
16
- default: "size-4",
17
- lg: "size-5",
18
- xl: "size-6",
19
- },
10
+ /**
11
+ * Sizes follow the icon column of §4 so a spinner can replace an icon in place. Each size also sets the
12
+ * vars the techy variants draw with: `--ls` (box), `--lc`/`--lg` (dot cell and gap, whole pixels so cells
13
+ * stay crisp) and `--lb` (bar width).
14
+ */
15
+ const spinnerVariants = cva("relative inline-flex shrink-0 items-center justify-center align-middle text-current", {
16
+ variants: {
17
+ variant: {
18
+ // reduced motion: slow the spin down rather than freeze it, a frozen spinner reads as a glyph
19
+ ring: "size-(--ls) animate-spin motion-reduce:[animation-duration:2.5s]",
20
+ dots: "size-(--ls)",
21
+ braille: "h-(--ls) w-(--ls) items-start overflow-hidden font-mono text-(length:--ls) leading-none",
22
+ // hc: the placeholder fill is the dark border colour there, so the box becomes an outline instead
23
+ scan: `size-(--ls) overflow-hidden rounded-box ${placeholderFill} hc:bg-transparent hc:shadow-[inset_0_0_0_1px_var(--border)]`,
24
+ blink: "h-(--ls) w-[calc(var(--ls)/2)]",
25
+ bars: "size-(--ls) justify-between",
26
+ },
27
+ size: {
28
+ xs: "[--lb:2px] [--lc:3px] [--lg:1px] [--ls:0.75rem]",
29
+ sm: "[--lb:2px] [--lc:4px] [--lg:1px] [--ls:0.875rem]",
30
+ default: "[--lb:3px] [--lc:4px] [--lg:2px] [--ls:1rem]",
31
+ lg: "[--lb:3px] [--lc:6px] [--lg:1px] [--ls:1.25rem]",
32
+ xl: "[--lb:4px] [--lc:6px] [--lg:3px] [--ls:1.5rem]",
20
33
  },
21
- defaultVariants: {size: "default"},
22
34
  },
23
- );
35
+ defaultVariants: {variant: "ring", size: "default"},
36
+ });
37
+
38
+ type SpinnerVariant = NonNullable<VariantProps<typeof spinnerVariants>["variant"]>;
39
+
40
+ /** Every animated part: slower, not frozen, under reduced motion. */
41
+ const slow = "motion-reduce:[animation-duration:3s]";
24
42
 
25
- interface SpinnerProps extends React.ComponentProps<"svg">, VariantProps<typeof spinnerVariants> {}
43
+ const BRAILLE = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
44
+
45
+ /** Negative delays so every cell is mid-cycle on the first frame (no dead start). */
46
+ const delay = (i: number, n: number, duration: number) => ({animationDelay: `${-((n - i) * duration) / n}s`});
47
+
48
+ function SpinnerParts({variant}: {variant: SpinnerVariant}) {
49
+ switch (variant) {
50
+ case "ring":
51
+ return <Loader2Icon aria-hidden className="size-full" />;
52
+ case "dots":
53
+ return (
54
+ <span
55
+ data-slot="spinner-grid"
56
+ className="grid grid-cols-[repeat(3,var(--lc))] grid-rows-[repeat(3,var(--lc))] gap-(--lg)"
57
+ >
58
+ {Array.from({length: 9}, (_, i) => (
59
+ <span
60
+ key={i}
61
+ className={cn("animate-loader-cell rounded-pill bg-current", slow)}
62
+ // diagonal wave from the top-left corner
63
+ style={delay(4 - (Math.floor(i / 3) + (i % 3)), 5, 1.2)}
64
+ />
65
+ ))}
66
+ </span>
67
+ );
68
+ case "braille":
69
+ return (
70
+ <span aria-hidden className={cn("flex animate-loader-braille flex-col", slow)}>
71
+ {[...BRAILLE].map((g) => (
72
+ <span key={g} className="h-(--ls) leading-(--ls)">
73
+ {g}
74
+ </span>
75
+ ))}
76
+ </span>
77
+ );
78
+ case "scan":
79
+ return (
80
+ <span
81
+ className={cn(
82
+ "absolute inset-0 animate-loader-scan border-e-2 border-current bg-linear-to-r from-transparent to-current/40",
83
+ slow,
84
+ )}
85
+ />
86
+ );
87
+ case "blink":
88
+ return <span className={cn("size-full animate-loader-blink bg-current", slow)} />;
89
+ case "bars":
90
+ return Array.from({length: 4}, (_, i) => (
91
+ <span
92
+ key={i}
93
+ className={cn("h-full w-(--lb) origin-bottom animate-loader-bars rounded-box bg-current", slow)}
94
+ style={delay((i * 2 + Math.floor(i / 2)) % 4, 4, 0.9)}
95
+ />
96
+ ));
97
+ }
98
+ }
99
+
100
+ const SpinnerContext = React.createContext<SpinnerVariant>("ring");
101
+
102
+ /**
103
+ * Sets the default `variant` of every Spinner below it, including the ones the kit renders itself
104
+ * (tree lazy rows, stepper, file upload, toasts): one switch for the whole app's loading look.
105
+ */
106
+ function SpinnerProvider({variant, children}: {variant: SpinnerVariant; children: React.ReactNode}) {
107
+ return <SpinnerContext.Provider value={variant}>{children}</SpinnerContext.Provider>;
108
+ }
109
+
110
+ interface SpinnerProps
111
+ extends Omit<React.ComponentProps<"span">, "color" | "children">, VariantProps<typeof spinnerVariants> {
112
+ /** Color from the §2 axis; defaults to `currentColor`. */
113
+ color?: Color;
114
+ }
26
115
 
27
- /** Indeterminate loading indicator. Inherits `currentColor`; announces itself as a status. */
28
- function Spinner({className, size, ...props}: SpinnerProps) {
116
+ /**
117
+ * Indeterminate loading indicator at icon size: a spinning ring, or a techy variant (dot matrix, braille,
118
+ * scan line, block cursor, equalizer bars). Inherits `currentColor`; announces itself as a status. The
119
+ * default variant comes from the nearest `SpinnerProvider`.
120
+ */
121
+ function Spinner({variant, size, color, className, ...props}: SpinnerProps) {
122
+ const fallback = React.useContext(SpinnerContext);
123
+ const resolved = variant ?? fallback;
29
124
  return (
30
- <Loader2Icon
125
+ <span
31
126
  data-slot="spinner"
127
+ data-variant={resolved}
32
128
  data-size={size ?? "default"}
129
+ data-color={color}
33
130
  role="status"
34
131
  aria-label="Loading"
35
- className={cn(spinnerVariants({size}), className)}
132
+ className={cn(
133
+ spinnerVariants({variant: resolved, size}),
134
+ color && cn(colorVariants[color], "text-(--c)"),
135
+ className,
136
+ )}
36
137
  {...props}
37
- />
138
+ >
139
+ <SpinnerParts variant={resolved} />
140
+ </span>
38
141
  );
39
142
  }
40
143
 
41
- export {Spinner, spinnerVariants};
42
- export type {SpinnerProps};
144
+ export {Spinner, SpinnerProvider, spinnerVariants};
145
+ export type {SpinnerProps, SpinnerVariant};