@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,72 @@
1
+ "use client";
2
+
3
+ import {useLocale} from "@repraze/kernelkit/components/locale";
4
+ import * as React from "react";
5
+
6
+ /** `"auto"` follows the locale: 12 in en-US, 24 in fr-FR, en-GB or ja-JP. */
7
+ type HourCyclePreference = 12 | 24 | "auto";
8
+
9
+ interface TimeFormat {
10
+ locale: string;
11
+ hourCycle: 12 | 24;
12
+ /** Localized day periods: "AM" / "PM" in en-US, "a.m." / "p.m." in en-CA, "오전" / "오후" in ko. */
13
+ am: string;
14
+ pm: string;
15
+ /** The day period comes before the hour (ko, zh). */
16
+ periodFirst: boolean;
17
+ /** "09:30" (24h) or "9:30 AM" / "오전 9:30" (12h), in the locale's own pattern. */
18
+ format: (minutes: number) => string;
19
+ /** A two-digit segment in the locale's digits: "05", "٠٥" in ar-EG. */
20
+ digits: (n: number) => string;
21
+ }
22
+
23
+ const pad = (n: number) => String(n).padStart(2, "0");
24
+
25
+ /**
26
+ * Hour cycle, day-period labels and a formatter, from `Intl` for the resolved locale (see `useLocale`:
27
+ * prop › LocaleProvider › browser, hydration-safe). An explicit 12 / 24 overrides the locale's cycle.
28
+ */
29
+ function useTimeFormat(preference: HourCyclePreference = "auto", explicitLocale?: string): TimeFormat {
30
+ const locale = useLocale(explicitLocale);
31
+ return React.useMemo(() => {
32
+ let hourCycle: 12 | 24 = preference === "auto" ? 24 : preference;
33
+ let am = "AM";
34
+ let pm = "PM";
35
+ let periodFirst = false;
36
+ let formatter: Intl.DateTimeFormat | null = null;
37
+ let numbers: Intl.NumberFormat | null = null;
38
+ try {
39
+ numbers = new Intl.NumberFormat(locale, {minimumIntegerDigits: 2, useGrouping: false});
40
+ if (preference === "auto") {
41
+ const hc = new Intl.DateTimeFormat(locale, {hour: "numeric"}).resolvedOptions().hourCycle;
42
+ hourCycle = hc === "h11" || hc === "h12" ? 12 : 24;
43
+ }
44
+ const f = new Intl.DateTimeFormat(locale, {hour: "numeric", hourCycle: "h12"});
45
+ const parts = (h: number) => f.formatToParts(new Date(2000, 0, 1, h));
46
+ am = parts(9).find((p) => p.type === "dayPeriod")?.value ?? am;
47
+ pm = parts(21).find((p) => p.type === "dayPeriod")?.value ?? pm;
48
+ const types = parts(9).map((p) => p.type);
49
+ periodFirst = types.includes("dayPeriod") && types.indexOf("dayPeriod") < types.indexOf("hour");
50
+ formatter = new Intl.DateTimeFormat(locale, {
51
+ hour: hourCycle === 24 ? "2-digit" : "numeric",
52
+ minute: "2-digit",
53
+ hourCycle: hourCycle === 24 ? "h23" : "h12",
54
+ });
55
+ } catch {
56
+ formatter = null;
57
+ }
58
+ const format = (minutes: number) => {
59
+ const h = Math.floor(minutes / 60);
60
+ const m = minutes % 60;
61
+ if (formatter) {
62
+ return formatter.format(new Date(2000, 0, 1, h, m));
63
+ }
64
+ return hourCycle === 24 ? `${pad(h)}:${pad(m)}` : `${h % 12 || 12}:${pad(m)} ${h < 12 ? am : pm}`;
65
+ };
66
+ const digits = (n: number) => (numbers ? numbers.format(n) : pad(n));
67
+ return {locale, hourCycle, am, pm, periodFirst, format, digits};
68
+ }, [preference, locale]);
69
+ }
70
+
71
+ export {useTimeFormat};
72
+ export type {HourCyclePreference, TimeFormat};
package/src/lib/color.ts CHANGED
@@ -6,9 +6,13 @@
6
6
  * - `--c-tint` opaque wash behind foreground text
7
7
  * - `--c-fill` / `--c-fill-fg` vivid solid surface and its text (TermiColor "fills are the brand":
8
8
  * in light mode the text tier is darkened for contrast, the fill tier stays vivid)
9
+ * - `--c-soft-text` text on a wash of its own color (soft variants, soft status surfaces): `--c` mixed 25%
10
+ * toward the foreground with its chroma restored, which keeps hue and saturation and passes WCAG AA
11
+ * 4.5:1 for every color, in light, dark and both hc modes, on background, card and popover (plain `--c`
12
+ * on its own wash fell to ~3.6)
9
13
  *
10
14
  * Spread `colorVariants` into a cva `variants.color` map, then write variants against the vars
11
- * (`bg-(--c) text-(--c-fg)`, `bg-(--c)/12 text-(--c)`, …).
15
+ * (`bg-(--c) text-(--c-fg)`, `bg-(--c)/12 text-(--c-soft-text)`, …).
12
16
  */
13
17
 
14
18
  export const STATUS_COLORS = ["danger", "warning", "success", "info"] as const;
@@ -17,8 +21,17 @@ export const COLORS = ["neutral", "primary", ...STATUS_COLORS, ...HUES] as const
17
21
 
18
22
  export type Color = (typeof COLORS)[number];
19
23
 
24
+ /**
25
+ * Derived from `--c` on the same element, so it is set alongside every color (a :root rule would see no `--c`).
26
+ * The mix toward the (neutral) foreground sets the lightness for contrast but also scales chroma by 0.75, so
27
+ * where relative color syntax is supported the chroma is multiplied back by 1/0.75: same lightness and hue,
28
+ * the original saturation (≥ 5.08:1 everywhere, vs ≥ 5.68 for the plain mix kept as the fallback).
29
+ */
30
+ const SOFT_TEXT =
31
+ "[--c-soft-text:color-mix(in_oklab,var(--c)_75%,var(--foreground))] supports-[color:oklch(from_red_l_c_h)]:[--c-soft-text:oklch(from_color-mix(in_oklab,var(--c)_75%,var(--foreground))_l_calc(c*1.333)_h)]";
32
+
20
33
  // Literal strings on purpose: Tailwind only generates classes it can read in the source.
21
- export const colorVariants: Record<Color, string> = {
34
+ const baseColorVariants: Record<Color, string> = {
22
35
  neutral:
23
36
  "[--c:var(--foreground)] [--c-fg:var(--background)] [--c-tint:var(--secondary)] [--c-fill:var(--foreground)] [--c-fill-fg:var(--background)]",
24
37
  primary:
@@ -39,6 +52,10 @@ export const colorVariants: Record<Color, string> = {
39
52
  pink: "[--c:var(--tc-pink)] [--c-fg:var(--tc-pink-on)] [--c-tint:var(--tc-pink-tint)] [--c-fill:var(--tc-pink-fill)] [--c-fill-fg:var(--tc-pink-on-fill)]",
40
53
  };
41
54
 
55
+ export const colorVariants = Object.fromEntries(
56
+ Object.entries(baseColorVariants).map(([color, vars]) => [color, `${vars} ${SOFT_TEXT}`]),
57
+ ) as Record<Color, string>;
58
+
42
59
  /** Every color except neutral: the ones whose outline/ghost variants are tinted. */
43
60
  export const TINTED_COLORS = COLORS.filter((c) => c !== "neutral");
44
61
 
@@ -55,14 +55,16 @@ export const selectedRow = "bg-accent text-accent-foreground shadow-[inset_2px_0
55
55
 
56
56
  /**
57
57
  * Status surface shared by Alert and Callout (§2): one `soft` / `outline` axis, same values in both.
58
- * Expects a color variant (`--c`, `--c-tint`) on the same element. Neutral swaps the hue border for the
58
+ * Expects a color variant (`--c`, `--c-tint`) on the same element. Text, icons and links on it use
59
+ * `--c-text`, which each variant sets to the color that passes 4.5:1 on that surface. Neutral swaps the hue border for the
59
60
  * plain border on a card surface (`statusSurfaceNeutral`).
60
61
  */
61
62
  export const statusSurface = {
62
- // status tint behind foreground text; the hue lives in the border, icon and accents (TermiColor rule)
63
- soft: "border-(--c)/30 bg-(--c-tint) hc:border-(--c)",
63
+ // status tint behind foreground text; the hue lives in the border, icon and accents (TermiColor rule).
64
+ // `--c-text` is the hue for text, icons and links on it: on the tint it is the soft text (4.5:1)
65
+ soft: "border-(--c)/30 bg-(--c-tint) hc:border-(--c) [--c-text:var(--c-soft-text)]",
64
66
  // no wash: card surface with a hue border, for dense layouts
65
- outline: "border-(--c)/50 bg-card hc:border-(--c)",
67
+ outline: "border-(--c)/50 bg-card hc:border-(--c) [--c-text:var(--c)]",
66
68
  } as const;
67
69
 
68
- export const statusSurfaceNeutral = "border-border bg-card hc:border-border";
70
+ export const statusSurfaceNeutral = "border-border bg-card hc:border-border [--c-text:var(--c)]";
@@ -40,6 +40,9 @@ export interface ThemeContextValue extends ThemeState {
40
40
 
41
41
  const ThemeContext = createContext<ThemeContextValue | null>(null);
42
42
 
43
+ /** The nearest ThemeScope's resolved theme, so code that paints (canvas, charts) can follow a scope. */
44
+ const ScopeContext = createContext<ResolvedTheme | null>(null);
45
+
43
46
  /** Element overlays portal into, so popups inside a ThemeScope keep its theme. */
44
47
  const PortalContainerContext = createContext<HTMLElement | null>(null);
45
48
 
@@ -181,6 +184,23 @@ export function useTheme(): ThemeContextValue {
181
184
  return ctx;
182
185
  }
183
186
 
187
+ /** A theme with its mode resolved (never "system"). */
188
+ export type ResolvedTheme = Omit<ThemeState, "mode"> & {mode: ResolvedMode};
189
+
190
+ /**
191
+ * The theme that applies here: the nearest ThemeScope's, else the provider's with "system" resolved.
192
+ * Use it for things CSS can't reach (canvas, WebGL/WebGPU, charts) so they follow review scopes too.
193
+ */
194
+ export function useResolvedTheme(): ResolvedTheme {
195
+ const scope = useContext(ScopeContext);
196
+ const ctx = useContext(ThemeContext);
197
+ const mode = scope?.mode ?? ctx?.resolvedMode ?? "light";
198
+ const contrast = scope?.contrast ?? ctx?.contrast ?? DEFAULT_THEME.contrast;
199
+ const accent = scope?.accent ?? ctx?.accent ?? DEFAULT_THEME.accent;
200
+ const radius = scope?.radius ?? ctx?.radius ?? DEFAULT_THEME.radius;
201
+ return useMemo(() => ({mode, contrast, accent, radius}), [mode, contrast, accent, radius]);
202
+ }
203
+
184
204
  /** Renders the pre-paint script. Place in <head> (Next.js: in the root layout). */
185
205
  export function ThemeScript({nonce, ...options}: ThemeScriptOptions & {nonce?: string}) {
186
206
  return <script nonce={nonce} suppressHydrationWarning dangerouslySetInnerHTML={{__html: themeScript(options)}} />;
@@ -188,7 +208,7 @@ export function ThemeScript({nonce, ...options}: ThemeScriptOptions & {nonce?: s
188
208
 
189
209
  export interface ThemeScopeProps extends Omit<React.ComponentProps<"div">, "children"> {
190
210
  children?: ReactNode;
191
- /** Values not given inherit from the surrounding <ThemeProvider> (or the defaults). */
211
+ /** Values not given inherit from the surrounding ThemeScope or <ThemeProvider> (or the defaults). */
192
212
  mode?: ResolvedMode;
193
213
  contrast?: ThemeState["contrast"];
194
214
  accent?: ThemeState["accent"];
@@ -200,14 +220,17 @@ export interface ThemeScopeProps extends Omit<React.ComponentProps<"div">, "chil
200
220
  * the scope, so they keep its colors and radius.
201
221
  */
202
222
  export function ThemeScope({mode, contrast, accent, radius, className, children, ...props}: ThemeScopeProps) {
203
- const parent = useContext(ThemeContext);
223
+ const parent = useResolvedTheme();
204
224
  const [container, setContainer] = useState<HTMLDivElement | null>(null);
205
- const theme = {
206
- mode: mode ?? parent?.resolvedMode ?? "light",
207
- contrast: contrast ?? parent?.contrast ?? DEFAULT_THEME.contrast,
208
- accent: accent ?? parent?.accent ?? DEFAULT_THEME.accent,
209
- radius: radius ?? parent?.radius ?? DEFAULT_THEME.radius,
210
- };
225
+ const theme = useMemo(
226
+ () => ({
227
+ mode: mode ?? parent.mode,
228
+ contrast: contrast ?? parent.contrast,
229
+ accent: accent ?? parent.accent,
230
+ radius: radius ?? parent.radius,
231
+ }),
232
+ [mode, contrast, accent, radius, parent],
233
+ );
211
234
  const classes = themeClassName(theme);
212
235
  return (
213
236
  <div
@@ -215,7 +238,9 @@ export function ThemeScope({mode, contrast, accent, radius, className, children,
215
238
  className={[classes, "bg-background text-foreground", className].filter(Boolean).join(" ")}
216
239
  {...props}
217
240
  >
218
- <PortalContainerContext.Provider value={container}>{children}</PortalContainerContext.Provider>
241
+ <ScopeContext.Provider value={theme}>
242
+ <PortalContainerContext.Provider value={container}>{children}</PortalContainerContext.Provider>
243
+ </ScopeContext.Provider>
219
244
  <div ref={setContainer} data-slot="theme-scope-portal" className={classes} />
220
245
  </div>
221
246
  );
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import {cn} from "cn";
4
+
5
+ /**
6
+ * A site's brand mark (an SVG string from its `brand/mark.ts`), inlined. Size it in multiples of 32px
7
+ * (default `size-8`) so it stays pixel-sharp (GUIDELINES §7b).
8
+ */
9
+ export function BrandMark({svg, className}: {svg: string; className?: string}) {
10
+ return (
11
+ <span
12
+ aria-hidden
13
+ className={cn("inline-block size-8 shrink-0 leading-0", className)}
14
+ // Static markup generated by the site from its own mark shapes, never from user input.
15
+ dangerouslySetInnerHTML={{__html: svg}}
16
+ />
17
+ );
18
+ }
19
+
20
+ export interface BrandLogoProps {
21
+ /** The mark's SVG markup. */
22
+ svg: string;
23
+ /** The wordmark's first word, in the foreground ("Kernel"). */
24
+ first: string;
25
+ /** Its second word, in the accent text tier ("Kit"). */
26
+ second: string;
27
+ className?: string;
28
+ }
29
+
30
+ /** Mark + two-tone wordmark in mono semibold: Kernel**Kit**, Termi**Color** (the suite's brand pattern). */
31
+ export function BrandLogo({svg, first, second, className}: BrandLogoProps) {
32
+ return (
33
+ <span className={cn("inline-flex items-center gap-2", className)}>
34
+ <BrandMark svg={svg} />
35
+ <span className="font-mono text-sm font-semibold tracking-tight">
36
+ {first}
37
+ <span className="text-primary-text">{second}</span>
38
+ </span>
39
+ </span>
40
+ );
41
+ }
@@ -0,0 +1,10 @@
1
+ import type * as React from "react";
2
+
3
+ /** GitHub mark (lucide no longer ships brand icons). */
4
+ export function GithubIcon(props: React.SVGProps<SVGSVGElement>) {
5
+ return (
6
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden {...props}>
7
+ <path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 0-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.1.7.8.6A12 12 0 0 0 12 .3" />
8
+ </svg>
9
+ );
10
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./brand";
2
+ export * from "./github-icon";
3
+ export * from "./suite-footer";
4
+ export * from "./suite-header";
5
+ export * from "./use-page-title";
@@ -0,0 +1,68 @@
1
+ "use client";
2
+
3
+ import {SiteFooter, SiteFooterLink, SiteFooterLinks} from "@repraze/kernelkit/components/site-footer";
4
+ import type * as React from "react";
5
+ import {Fragment} from "react";
6
+
7
+ export interface SuiteFooterLinkItem {
8
+ label: string;
9
+ /** A router link as a `render` element; or `href` for a plain anchor. */
10
+ render?: React.ReactElement;
11
+ href?: string;
12
+ }
13
+
14
+ /** Suite projects a site can credit, in this order: "built with KernelKit", "themed by TermiColor". */
15
+ export type SuiteCredit = "kernelkit" | "termicolor";
16
+
17
+ const CREDITS: Record<SuiteCredit, {text: string; name: string; href: string}> = {
18
+ kernelkit: {text: "built with", name: "KernelKit", href: "https://repraze.github.io/kernelkit/"},
19
+ termicolor: {text: "themed by", name: "TermiColor", href: "https://repraze.github.io/termicolor/"},
20
+ };
21
+
22
+ export interface SuiteFooterProps {
23
+ links: SuiteFooterLinkItem[];
24
+ /** Which suite projects to credit (default `["termicolor"]`). */
25
+ credits?: SuiteCredit[];
26
+ /** A phrase of the site's own after the credits ("built from one palette"). */
27
+ tagline?: React.ReactNode;
28
+ /** Default "MIT". */
29
+ license?: string;
30
+ }
31
+
32
+ /**
33
+ * The suite's footer (GUIDELINES §7b): "MIT · themed by TermiColor · a repraze.com project" on the left,
34
+ * the site's links on the right.
35
+ */
36
+ export function SuiteFooter({links, credits = ["termicolor"], tagline, license = "MIT"}: SuiteFooterProps) {
37
+ return (
38
+ <SiteFooter>
39
+ <span>
40
+ {license}
41
+ {credits.map((key) => {
42
+ const credit = CREDITS[key];
43
+ return (
44
+ <Fragment key={key}>
45
+ {" "}
46
+ · {credit.text}{" "}
47
+ <SiteFooterLink href={credit.href} className="underline underline-offset-4">
48
+ {credit.name}
49
+ </SiteFooterLink>
50
+ </Fragment>
51
+ );
52
+ })}
53
+ {tagline ? <> · {tagline}</> : null} · a{" "}
54
+ <SiteFooterLink href="https://repraze.com" className="underline underline-offset-4">
55
+ repraze.com
56
+ </SiteFooterLink>{" "}
57
+ project
58
+ </span>
59
+ <SiteFooterLinks aria-label="Footer">
60
+ {links.map((link) => (
61
+ <SiteFooterLink key={link.label} href={link.href} render={link.render}>
62
+ {link.label}
63
+ </SiteFooterLink>
64
+ ))}
65
+ </SiteFooterLinks>
66
+ </SiteFooter>
67
+ );
68
+ }
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ import {Button} from "@repraze/kernelkit/components/button";
4
+ import {Separator} from "@repraze/kernelkit/components/separator";
5
+ import {
6
+ SiteHeader,
7
+ SiteHeaderActions,
8
+ SiteHeaderBrand,
9
+ SiteHeaderContent,
10
+ SiteHeaderMenu,
11
+ SiteNav,
12
+ SiteNavLink,
13
+ } from "@repraze/kernelkit/components/site-header";
14
+ import {ThemeMenu} from "@repraze/kernelkit/components/theme-menu";
15
+ import type * as React from "react";
16
+ import {useEffect, useState} from "react";
17
+
18
+ import {GithubIcon} from "./github-icon";
19
+
20
+ export interface SuiteNavItem {
21
+ label: string;
22
+ /** A router link as a `render` element (`<Link to="/ports" />`); or `href` for a plain anchor. */
23
+ render?: React.ReactElement;
24
+ href?: string;
25
+ /** Marks the entry current (`aria-current="page"`): the section the open page belongs to. */
26
+ current?: boolean;
27
+ }
28
+
29
+ export interface SuiteHeaderProps {
30
+ /** The brand (a BrandLogo), in the header and as the phone menu's title. */
31
+ brand: React.ReactNode;
32
+ /** Accessible name of the home link, e.g. "KernelKit home". */
33
+ brandLabel: string;
34
+ /** The home link as a `render` element (`<Link to="/" />`); a plain "/" anchor otherwise. */
35
+ home?: React.ReactElement;
36
+ nav: SuiteNavItem[];
37
+ /** Accessible name of the nav (default "Main"). */
38
+ navLabel?: string;
39
+ /** The search (DocsSearch, or the site's own), before the separator. */
40
+ search?: React.ReactNode;
41
+ /** Replaces the default `<ThemeMenu />`. */
42
+ themeMenu?: React.ReactNode;
43
+ /** The repository button (a GitHub icon). */
44
+ repository?: {href: string; label?: string; icon?: React.ReactNode};
45
+ /** The phone menu's content after the nav links (the docs nav, a tree). */
46
+ menu?: React.ReactNode;
47
+ /** Closes the phone menu when it changes: pass the pathname. */
48
+ locationKey?: string;
49
+ }
50
+
51
+ /**
52
+ * The suite's site header (GUIDELINES §7b): phone menu, brand, mono nav, then search · theme · repository.
53
+ * Every site renders this; only the brand, nav and menu content differ.
54
+ */
55
+ export function SuiteHeader({
56
+ brand,
57
+ brandLabel,
58
+ home,
59
+ nav,
60
+ navLabel = "Main",
61
+ search,
62
+ themeMenu,
63
+ repository,
64
+ menu,
65
+ locationKey,
66
+ }: SuiteHeaderProps) {
67
+ const [menuOpen, setMenuOpen] = useState(false);
68
+ useEffect(() => setMenuOpen(false), [locationKey]);
69
+
70
+ const navLinks = nav.map((item) => (
71
+ <SiteNavLink
72
+ key={item.label}
73
+ href={item.href}
74
+ aria-current={item.current ? "page" : undefined}
75
+ render={item.render}
76
+ >
77
+ {item.label}
78
+ </SiteNavLink>
79
+ ));
80
+
81
+ return (
82
+ <SiteHeader>
83
+ <SiteHeaderContent>
84
+ <SiteHeaderMenu title={brand} open={menuOpen} onOpenChange={setMenuOpen}>
85
+ <nav aria-label={navLabel} className="flex flex-col gap-1 px-2 pt-2">
86
+ {navLinks}
87
+ </nav>
88
+ {menu}
89
+ </SiteHeaderMenu>
90
+ <SiteHeaderBrand aria-label={brandLabel} href={home ? undefined : "/"} render={home}>
91
+ {brand}
92
+ </SiteHeaderBrand>
93
+ <SiteNav aria-label={navLabel}>{navLinks}</SiteNav>
94
+ <SiteHeaderActions>
95
+ {search}
96
+ <Separator
97
+ orientation="vertical"
98
+ className="mx-1 hidden data-vertical:h-5 data-vertical:self-center sm:block"
99
+ />
100
+ {themeMenu ?? <ThemeMenu />}
101
+ {repository ? (
102
+ <Button
103
+ variant="ghost"
104
+ size="icon"
105
+ render={<a href={repository.href} aria-label={repository.label ?? "GitHub"} />}
106
+ >
107
+ {repository.icon ?? <GithubIcon className="size-4" />}
108
+ </Button>
109
+ ) : null}
110
+ </SiteHeaderActions>
111
+ </SiteHeaderContent>
112
+ </SiteHeader>
113
+ );
114
+ }
@@ -0,0 +1,8 @@
1
+ import {useEffect} from "react";
2
+
3
+ /** Sets the tab title: "Page · Brand", or the bare brand on the home page (GUIDELINES §7b). */
4
+ export function usePageTitle(title: string | undefined, brand: string) {
5
+ useEffect(() => {
6
+ document.title = title ? `${title} · ${brand}` : brand;
7
+ }, [title, brand]);
8
+ }
@@ -17,6 +17,8 @@
17
17
  @source "../provider";
18
18
  @source "../lib";
19
19
  @source "../hooks";
20
+ @source "../docs";
21
+ @source "../site";
20
22
 
21
23
  /* Nearest scope wins, so a .light preview nested in a .dark page isn't styled as dark. */
22
24
  @custom-variant dark (&:is(.dark, .dark *):not(.dark .light, .dark .light *));
@@ -185,6 +187,62 @@
185
187
  translate: 250% 0;
186
188
  }
187
189
  }
190
+ /* Spinner variants (spinner.tsx) and segmented Progress: cells light up and fade, a glyph strip steps (braille), a cursor
191
+ blinks, equalizer bars bounce, a scan line sweeps. Delays are set per cell. */
192
+ --animate-loader-cell: kk-loader-cell 1.2s ease-in-out infinite;
193
+ @keyframes kk-loader-cell {
194
+ 0%,
195
+ 100% {
196
+ opacity: 0.2;
197
+ }
198
+ 30% {
199
+ opacity: 1;
200
+ }
201
+ 60% {
202
+ opacity: 0.2;
203
+ }
204
+ }
205
+ --animate-loader-braille: kk-loader-braille 0.8s steps(10) infinite;
206
+ @keyframes kk-loader-braille {
207
+ to {
208
+ translate: 0 -100%;
209
+ }
210
+ }
211
+ --animate-loader-blink: kk-loader-blink 1s step-end infinite;
212
+ @keyframes kk-loader-blink {
213
+ 50% {
214
+ opacity: 0;
215
+ }
216
+ }
217
+ --animate-loader-bars: kk-loader-bars 0.9s ease-in-out infinite;
218
+ @keyframes kk-loader-bars {
219
+ 0%,
220
+ 100% {
221
+ scale: 1 0.3;
222
+ }
223
+ 50% {
224
+ scale: 1 1;
225
+ }
226
+ }
227
+ --animate-loader-scan: kk-loader-scan 1.4s linear infinite;
228
+ @keyframes kk-loader-scan {
229
+ from {
230
+ translate: -100% 0;
231
+ }
232
+ to {
233
+ translate: 100% 0;
234
+ }
235
+ }
236
+ /* Shimmer: a highlight band sweeping across clipped text. */
237
+ --animate-shimmer: kk-shimmer 2s linear infinite;
238
+ @keyframes kk-shimmer {
239
+ from {
240
+ background-position: 100% 0;
241
+ }
242
+ to {
243
+ background-position: 0% 0;
244
+ }
245
+ }
188
246
  /* Boxes under 20px (checkbox, kbd keys, tiny chips): capped so they never turn into circles at xl. */
189
247
  --radius-box: min(calc(var(--kk-radius) * 0.5), 0.25rem);
190
248
 
@@ -0,0 +1,15 @@
1
+ /*
2
+ * The suite's site stylesheet: the kit's styles and the suite fonts (Geist to read, JetBrains Mono for
3
+ * code, labels and the wordmark). A site's index.css:
4
+ *
5
+ * @import "tailwindcss";
6
+ * @import "@repraze/kernelkit/site.css";
7
+ */
8
+ @import "@fontsource-variable/geist";
9
+ @import "@fontsource-variable/jetbrains-mono";
10
+ @import "./kernelkit.css";
11
+
12
+ :root {
13
+ --kk-font-sans: "Geist Variable", ui-sans-serif, system-ui, sans-serif;
14
+ --kk-font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
15
+ }
@@ -0,0 +1,11 @@
1
+ // Ambient types for docsMetaPlugin's modules: add "@repraze/kernelkit/vite/client" to tsconfig `types`.
2
+
3
+ declare module "virtual:docs-meta" {
4
+ const meta: {slug: string; title: string; category: string; description: string; order?: number}[];
5
+ export default meta;
6
+ }
7
+
8
+ declare module "*?demo-sources" {
9
+ const sources: {functions: Record<string, string>};
10
+ export default sources;
11
+ }