@repraze/kernelkit 0.1.0-alpha.2 → 0.1.0-alpha.4
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.
- package/README.md +17 -0
- package/dist/components/alert.d.ts.map +1 -1
- package/dist/components/alert.js +4 -6
- package/dist/components/alert.js.map +1 -1
- package/dist/components/avatar.js +1 -1
- package/dist/components/avatar.js.map +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/badge.js.map +1 -1
- package/dist/components/breadcrumb.d.ts +1 -0
- package/dist/components/breadcrumb.d.ts.map +1 -1
- package/dist/components/breadcrumb.js +7 -6
- package/dist/components/breadcrumb.js.map +1 -1
- package/dist/components/callout.d.ts +8 -4
- package/dist/components/callout.d.ts.map +1 -1
- package/dist/components/callout.js +22 -12
- package/dist/components/callout.js.map +1 -1
- package/dist/components/chart.js +2 -2
- package/dist/components/chart.js.map +1 -1
- package/dist/components/code-block.js +1 -1
- package/dist/components/code-block.js.map +1 -1
- package/dist/components/command.js +1 -1
- package/dist/components/command.js.map +1 -1
- package/dist/components/data-table/elements/data-table-info-row.js +1 -1
- package/dist/components/data-table/elements/data-table-info-row.js.map +1 -1
- package/dist/components/dropdown-menu.d.ts +1 -1
- package/dist/components/dropdown-menu.d.ts.map +1 -1
- package/dist/components/dropdown-menu.js +1 -1
- package/dist/components/dropdown-menu.js.map +1 -1
- package/dist/components/kbd.d.ts.map +1 -1
- package/dist/components/kbd.js +2 -4
- package/dist/components/kbd.js.map +1 -1
- package/dist/components/message.js +2 -2
- package/dist/components/message.js.map +1 -1
- package/dist/components/meter.js +1 -1
- package/dist/components/meter.js.map +1 -1
- package/dist/components/progress.js +1 -1
- package/dist/components/progress.js.map +1 -1
- package/dist/components/questionnaire.js +1 -1
- package/dist/components/questionnaire.js.map +1 -1
- package/dist/components/search-dialog.d.ts +6 -1
- package/dist/components/search-dialog.d.ts.map +1 -1
- package/dist/components/search-dialog.js +13 -1
- package/dist/components/search-dialog.js.map +1 -1
- package/dist/components/search-trigger.d.ts +1 -1
- package/dist/components/search-trigger.d.ts.map +1 -1
- package/dist/components/search-trigger.js +2 -2
- package/dist/components/search-trigger.js.map +1 -1
- package/dist/components/section.js +1 -1
- package/dist/components/section.js.map +1 -1
- package/dist/components/sidebar.js +1 -1
- package/dist/components/sidebar.js.map +1 -1
- package/dist/components/site-footer.js +1 -1
- package/dist/components/site-footer.js.map +1 -1
- package/dist/components/site-header.js +2 -2
- package/dist/components/site-header.js.map +1 -1
- package/dist/components/stat.js +2 -2
- package/dist/components/stat.js.map +1 -1
- package/dist/components/tabs.d.ts +4 -2
- package/dist/components/tabs.d.ts.map +1 -1
- package/dist/components/tabs.js +13 -7
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/theme-menu.js +1 -1
- package/dist/components/theme-menu.js.map +1 -1
- package/dist/components/toc.js +1 -1
- package/dist/components/toc.js.map +1 -1
- package/dist/components/tooltip.d.ts.map +1 -1
- package/dist/components/tooltip.js +5 -2
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/trace.js +1 -1
- package/dist/components/trace.js.map +1 -1
- package/dist/docs/doc-kit.d.ts +72 -0
- package/dist/docs/doc-kit.d.ts.map +1 -0
- package/dist/docs/doc-kit.js +45 -0
- package/dist/docs/doc-kit.js.map +1 -0
- package/dist/docs/doc-page.d.ts +27 -0
- package/dist/docs/doc-page.d.ts.map +1 -0
- package/dist/docs/doc-page.js +33 -0
- package/dist/docs/doc-page.js.map +1 -0
- package/dist/docs/docs-index.d.ts +60 -0
- package/dist/docs/docs-index.d.ts.map +1 -0
- package/dist/docs/docs-index.js +45 -0
- package/dist/docs/docs-index.js.map +1 -0
- package/dist/docs/docs-nav.d.ts +13 -0
- package/dist/docs/docs-nav.d.ts.map +1 -0
- package/dist/docs/docs-nav.js +8 -0
- package/dist/docs/docs-nav.js.map +1 -0
- package/dist/docs/docs-search.d.ts +19 -0
- package/dist/docs/docs-search.d.ts.map +1 -0
- package/dist/docs/docs-search.js +33 -0
- package/dist/docs/docs-search.js.map +1 -0
- package/dist/docs/index.d.ts +7 -0
- package/dist/docs/index.d.ts.map +1 -0
- package/dist/docs/index.js +7 -0
- package/dist/docs/index.js.map +1 -0
- package/dist/docs/review-matrix.d.ts +25 -0
- package/dist/docs/review-matrix.d.ts.map +1 -0
- package/dist/docs/review-matrix.js +31 -0
- package/dist/docs/review-matrix.js.map +1 -0
- package/dist/hooks/use-theme-palette.d.ts +8 -0
- package/dist/hooks/use-theme-palette.d.ts.map +1 -0
- package/dist/hooks/use-theme-palette.js +24 -0
- package/dist/hooks/use-theme-palette.js.map +1 -0
- package/dist/lib/recipes.d.ts +10 -0
- package/dist/lib/recipes.d.ts.map +1 -1
- package/dist/lib/recipes.js +12 -0
- package/dist/lib/recipes.js.map +1 -1
- package/dist/provider/theme-provider.d.ts +10 -1
- package/dist/provider/theme-provider.d.ts.map +1 -1
- package/dist/provider/theme-provider.js +23 -8
- package/dist/provider/theme-provider.js.map +1 -1
- package/dist/site/brand.d.ts +20 -0
- package/dist/site/brand.d.ts.map +1 -0
- package/dist/site/brand.js +17 -0
- package/dist/site/brand.js.map +1 -0
- package/dist/site/github-icon.d.ts +4 -0
- package/dist/site/github-icon.d.ts.map +1 -0
- package/dist/site/github-icon.js +6 -0
- package/dist/site/github-icon.js.map +1 -0
- package/dist/site/index.d.ts +6 -0
- package/dist/site/index.d.ts.map +1 -0
- package/dist/site/index.js +6 -0
- package/dist/site/index.js.map +1 -0
- package/dist/site/suite-footer.d.ts +24 -0
- package/dist/site/suite-footer.d.ts.map +1 -0
- package/dist/site/suite-footer.js +19 -0
- package/dist/site/suite-footer.js.map +1 -0
- package/dist/site/suite-header.d.ts +40 -0
- package/dist/site/suite-header.d.ts.map +1 -0
- package/dist/site/suite-header.js +19 -0
- package/dist/site/suite-header.js.map +1 -0
- package/dist/site/use-page-title.d.ts +3 -0
- package/dist/site/use-page-title.d.ts.map +1 -0
- package/dist/site/use-page-title.js +8 -0
- package/dist/site/use-page-title.js.map +1 -0
- package/dist/vite/index.d.ts +65 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +242 -0
- package/dist/vite/index.js.map +1 -0
- package/package.json +39 -3
- package/src/components/alert.tsx +4 -6
- package/src/components/avatar.tsx +1 -1
- package/src/components/badge.tsx +1 -1
- package/src/components/breadcrumb.tsx +8 -7
- package/src/components/callout.tsx +27 -12
- package/src/components/chart.tsx +2 -2
- package/src/components/code-block.tsx +1 -1
- package/src/components/command.tsx +1 -1
- package/src/components/data-table/elements/data-table-info-row.tsx +1 -1
- package/src/components/dropdown-menu.tsx +1 -1
- package/src/components/kbd.tsx +2 -4
- package/src/components/message.tsx +2 -2
- package/src/components/meter.tsx +1 -1
- package/src/components/progress.tsx +1 -1
- package/src/components/questionnaire.tsx +1 -1
- package/src/components/search-dialog.tsx +14 -1
- package/src/components/search-trigger.tsx +2 -3
- package/src/components/section.tsx +1 -1
- package/src/components/sidebar.tsx +1 -1
- package/src/components/site-footer.tsx +1 -1
- package/src/components/site-header.tsx +2 -2
- package/src/components/stat.tsx +2 -2
- package/src/components/tabs.tsx +17 -9
- package/src/components/theme-menu.tsx +1 -1
- package/src/components/toc.tsx +1 -1
- package/src/components/tooltip.tsx +15 -3
- package/src/components/trace.tsx +1 -1
- package/src/docs/doc-kit.tsx +176 -0
- package/src/docs/doc-page.tsx +126 -0
- package/src/docs/docs-index.ts +108 -0
- package/src/docs/docs-nav.tsx +47 -0
- package/src/docs/docs-search.tsx +88 -0
- package/src/docs/index.ts +6 -0
- package/src/docs/review-matrix.tsx +67 -0
- package/src/hooks/use-theme-palette.ts +25 -0
- package/src/lib/recipes.ts +14 -0
- package/src/provider/theme-provider.tsx +34 -9
- package/src/site/brand.tsx +41 -0
- package/src/site/github-icon.tsx +10 -0
- package/src/site/index.ts +5 -0
- package/src/site/suite-footer.tsx +68 -0
- package/src/site/suite-header.tsx +114 -0
- package/src/site/use-page-title.ts +8 -0
- package/src/styles/kernelkit.css +6 -0
- package/src/styles/site.css +15 -0
- package/src/vite/client.d.ts +11 -0
- package/src/vite/index.ts +298 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type {DocDef, DocEntry} from "./doc-kit";
|
|
2
|
+
|
|
3
|
+
/** What the nav, search and header need about a docs page, without loading its demos (`virtual:docs-meta`). */
|
|
4
|
+
export interface DocMeta<Category extends string = string> {
|
|
5
|
+
slug: string;
|
|
6
|
+
title: string;
|
|
7
|
+
category: Category;
|
|
8
|
+
description: string;
|
|
9
|
+
order?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A docs file's module: its `doc` and the demo components it exports. */
|
|
13
|
+
export type DocModule<Category extends string = string> = {doc: DocDef<Category>} & Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
/** What docsMetaPlugin serves for `<file>.tsx?demo-sources`: each top-level declaration's own source. */
|
|
16
|
+
export interface DemoSources {
|
|
17
|
+
functions: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Glob<T> = Record<string, () => Promise<T>>;
|
|
21
|
+
|
|
22
|
+
export interface DocsIndexOptions<Category extends string> {
|
|
23
|
+
/** Category order: the sidebar, search groups and pager follow it. */
|
|
24
|
+
categories: readonly Category[];
|
|
25
|
+
/** `import META from "virtual:docs-meta"`. */
|
|
26
|
+
meta: readonly {slug: string; title: string; category: string; description: string; order?: number}[];
|
|
27
|
+
/** `import.meta.glob("../docs/*.tsx")`: each page is its own chunk, loaded when opened. */
|
|
28
|
+
modules: Glob<unknown>;
|
|
29
|
+
/** `import.meta.glob("../docs/*.tsx", {query: "?demo-sources", import: "default"})`. */
|
|
30
|
+
sources: Glob<unknown>;
|
|
31
|
+
/** Where docs pages live, for `slugOf` (default "/docs"). */
|
|
32
|
+
basePath?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DocsIndex<Category extends string = string> {
|
|
36
|
+
categories: readonly Category[];
|
|
37
|
+
/** Every page, in category order, then `order`, then title. */
|
|
38
|
+
docs: DocMeta<Category>[];
|
|
39
|
+
bySlug: Map<string, DocMeta<Category>>;
|
|
40
|
+
/** Non-empty categories with their pages, for the sidebar and search. */
|
|
41
|
+
byCategory: {category: Category; docs: DocMeta<Category>[]}[];
|
|
42
|
+
/** Load a page's module and its demos' sources; undefined for an unknown slug. */
|
|
43
|
+
loadDoc(slug: string): Promise<DocEntry<Category> | undefined>;
|
|
44
|
+
/** The pages before and after one, for the Pager. */
|
|
45
|
+
neighbours(slug: string): {prev?: DocMeta<Category>; next?: DocMeta<Category>};
|
|
46
|
+
/** The docs slug in a pathname ("/base/docs/button" → "button"), or undefined outside the docs. */
|
|
47
|
+
slugOf(pathname: string): string | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const slugOfFile = (path: string) => /([^/]+)\.tsx$/.exec(path)?.[1];
|
|
51
|
+
|
|
52
|
+
/** The site's docs index, from the build-time manifest and the per-page globs. */
|
|
53
|
+
export function createDocsIndex<Category extends string>({
|
|
54
|
+
categories,
|
|
55
|
+
meta,
|
|
56
|
+
modules,
|
|
57
|
+
sources,
|
|
58
|
+
basePath = "/docs",
|
|
59
|
+
}: DocsIndexOptions<Category>): DocsIndex<Category> {
|
|
60
|
+
const moduleBySlug = new Map(Object.entries(modules).map(([path, load]) => [slugOfFile(path), load]));
|
|
61
|
+
const sourcesBySlug = new Map(Object.entries(sources).map(([path, load]) => [slugOfFile(path), load]));
|
|
62
|
+
|
|
63
|
+
const docs = (meta as DocMeta<Category>[]).toSorted(
|
|
64
|
+
(a, b) =>
|
|
65
|
+
categories.indexOf(a.category) - categories.indexOf(b.category) ||
|
|
66
|
+
(a.order ?? 50) - (b.order ?? 50) ||
|
|
67
|
+
a.title.localeCompare(b.title),
|
|
68
|
+
);
|
|
69
|
+
const bySlug = new Map(docs.map((d) => [d.slug, d]));
|
|
70
|
+
const byCategory = categories
|
|
71
|
+
.map((category) => ({category, docs: docs.filter((d) => d.category === category)}))
|
|
72
|
+
.filter((group) => group.docs.length > 0);
|
|
73
|
+
const pathRe = new RegExp(`${basePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/([^/]+)`);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
categories,
|
|
77
|
+
docs,
|
|
78
|
+
bySlug,
|
|
79
|
+
byCategory,
|
|
80
|
+
async loadDoc(slug) {
|
|
81
|
+
const load = moduleBySlug.get(slug);
|
|
82
|
+
const loadSources = sourcesBySlug.get(slug);
|
|
83
|
+
if (!load || !loadSources) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
const [mod, demoSources] = (await Promise.all([load(), loadSources()])) as [
|
|
87
|
+
DocModule<Category>,
|
|
88
|
+
DemoSources,
|
|
89
|
+
];
|
|
90
|
+
// Match demos to their export by identity: function names don't survive minification.
|
|
91
|
+
const exportName = (component: unknown) => Object.keys(mod).find((key) => mod[key] === component) ?? "";
|
|
92
|
+
return {
|
|
93
|
+
...mod.doc,
|
|
94
|
+
slug,
|
|
95
|
+
demoSources: mod.doc.demos.map((demo) => demoSources.functions[exportName(demo.component)] ?? ""),
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
neighbours(slug) {
|
|
99
|
+
const doc = bySlug.get(slug);
|
|
100
|
+
const index = doc ? docs.indexOf(doc) : -1;
|
|
101
|
+
return index < 0 ? {} : {prev: docs[index - 1], next: docs[index + 1]};
|
|
102
|
+
},
|
|
103
|
+
slugOf(pathname) {
|
|
104
|
+
const slug = pathRe.exec(pathname)?.[1];
|
|
105
|
+
return slug && bySlug.has(slug) ? slug : undefined;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
SidebarGroup,
|
|
5
|
+
SidebarGroupLabel,
|
|
6
|
+
SidebarMenu,
|
|
7
|
+
SidebarMenuButton,
|
|
8
|
+
SidebarMenuItem,
|
|
9
|
+
} from "@repraze/kernelkit/components/sidebar";
|
|
10
|
+
|
|
11
|
+
import type {DocLinkFor} from "./doc-page";
|
|
12
|
+
import type {DocsIndex} from "./docs-index";
|
|
13
|
+
|
|
14
|
+
export interface DocsNavProps {
|
|
15
|
+
index: DocsIndex;
|
|
16
|
+
/** The open page's slug (`index.slugOf(pathname)`), marked current. */
|
|
17
|
+
current?: string;
|
|
18
|
+
linkFor: DocLinkFor;
|
|
19
|
+
/** Called when a link is clicked (the phone menu closes). */
|
|
20
|
+
onNavigate?: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Docs navigation, one Sidebar group per category: the desktop SiteSidebar and the phone menu share it. */
|
|
24
|
+
export function DocsNav({index, current, linkFor, onNavigate}: DocsNavProps) {
|
|
25
|
+
return (
|
|
26
|
+
<nav aria-label="Docs" className="flex flex-col">
|
|
27
|
+
{index.byCategory.map(({category, docs}) => (
|
|
28
|
+
<SidebarGroup key={category}>
|
|
29
|
+
<SidebarGroupLabel className="font-mono uppercase">{category}</SidebarGroupLabel>
|
|
30
|
+
<SidebarMenu>
|
|
31
|
+
{docs.map((doc) => (
|
|
32
|
+
<SidebarMenuItem key={doc.slug}>
|
|
33
|
+
<SidebarMenuButton
|
|
34
|
+
isActive={doc.slug === current}
|
|
35
|
+
render={linkFor(doc.slug)}
|
|
36
|
+
onClick={onNavigate}
|
|
37
|
+
>
|
|
38
|
+
<span>{doc.title}</span>
|
|
39
|
+
</SidebarMenuButton>
|
|
40
|
+
</SidebarMenuItem>
|
|
41
|
+
))}
|
|
42
|
+
</SidebarMenu>
|
|
43
|
+
</SidebarGroup>
|
|
44
|
+
))}
|
|
45
|
+
</nav>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {Command, CommandEmpty, CommandGroup, CommandInput, CommandList} from "@repraze/kernelkit/components/command";
|
|
4
|
+
import {SearchDialog, SearchFooter, SearchItem, substringFilter} from "@repraze/kernelkit/components/search-dialog";
|
|
5
|
+
import {SearchTrigger, useSearchHotkeys} from "@repraze/kernelkit/components/search-trigger";
|
|
6
|
+
import {FileTextIcon, type LucideIcon} from "lucide-react";
|
|
7
|
+
import {useCallback, useState} from "react";
|
|
8
|
+
|
|
9
|
+
import type {DocsIndex} from "./docs-index";
|
|
10
|
+
|
|
11
|
+
export interface DocsSearchProps {
|
|
12
|
+
index: DocsIndex;
|
|
13
|
+
/** One icon per category, so a result's kind reads at a glance; `fallbackIcon` for the others. */
|
|
14
|
+
icons?: Record<string, LucideIcon>;
|
|
15
|
+
fallbackIcon?: LucideIcon;
|
|
16
|
+
/** Slugs shown before the first keystroke, in a "Suggested" group. */
|
|
17
|
+
suggested?: string[];
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
/** Go to a page (the router's navigate). */
|
|
20
|
+
onSelect: (slug: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The suite's docs search: SearchTrigger + hotkeys (Ctrl/⌘ K, `/`) opening a SearchDialog over the docs
|
|
25
|
+
* index, grouped by category, with a plain substring filter.
|
|
26
|
+
*/
|
|
27
|
+
export function DocsSearch({
|
|
28
|
+
index,
|
|
29
|
+
icons = {},
|
|
30
|
+
fallbackIcon = FileTextIcon,
|
|
31
|
+
suggested = [],
|
|
32
|
+
placeholder = "Search the docs…",
|
|
33
|
+
onSelect,
|
|
34
|
+
}: DocsSearchProps) {
|
|
35
|
+
const [open, setOpen] = useState(false);
|
|
36
|
+
const [query, setQuery] = useState("");
|
|
37
|
+
useSearchHotkeys(useCallback((toggle: boolean) => setOpen((o) => (toggle ? !o : true)), []));
|
|
38
|
+
|
|
39
|
+
const groups = query.trim()
|
|
40
|
+
? index.byCategory
|
|
41
|
+
: [{category: "Suggested", docs: suggested.flatMap((slug) => index.docs.filter((d) => d.slug === slug))}];
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<SearchTrigger onClick={() => setOpen(true)} aria-label="Search docs" />
|
|
46
|
+
<SearchDialog
|
|
47
|
+
open={open}
|
|
48
|
+
onOpenChange={(next) => {
|
|
49
|
+
setOpen(next);
|
|
50
|
+
if (!next) {
|
|
51
|
+
setQuery("");
|
|
52
|
+
}
|
|
53
|
+
}}
|
|
54
|
+
description="Search the docs"
|
|
55
|
+
>
|
|
56
|
+
<Command filter={substringFilter} loop>
|
|
57
|
+
<CommandInput value={query} onValueChange={setQuery} placeholder={placeholder} />
|
|
58
|
+
<CommandList>
|
|
59
|
+
<CommandEmpty>No results for “{query}”.</CommandEmpty>
|
|
60
|
+
{groups.map(({category, docs}) => (
|
|
61
|
+
<CommandGroup key={category} heading={category}>
|
|
62
|
+
{docs.map((doc) => {
|
|
63
|
+
const Icon = icons[doc.category] ?? fallbackIcon;
|
|
64
|
+
return (
|
|
65
|
+
<SearchItem
|
|
66
|
+
key={doc.slug}
|
|
67
|
+
value={doc.title}
|
|
68
|
+
keywords={[doc.category, doc.slug]}
|
|
69
|
+
icon={<Icon />}
|
|
70
|
+
title={doc.title}
|
|
71
|
+
description={doc.description}
|
|
72
|
+
onSelect={() => {
|
|
73
|
+
setOpen(false);
|
|
74
|
+
setQuery("");
|
|
75
|
+
onSelect(doc.slug);
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</CommandGroup>
|
|
81
|
+
))}
|
|
82
|
+
</CommandList>
|
|
83
|
+
<SearchFooter />
|
|
84
|
+
</Command>
|
|
85
|
+
</SearchDialog>
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type {Accent, Radius, ResolvedMode} from "@repraze/kernelkit/provider";
|
|
4
|
+
import {ThemeScope, type ThemeScopeProps} from "@repraze/kernelkit/provider";
|
|
5
|
+
import {cn} from "cn";
|
|
6
|
+
|
|
7
|
+
import type {DocEntry} from "./doc-kit";
|
|
8
|
+
|
|
9
|
+
export interface ReviewScope {
|
|
10
|
+
label: string;
|
|
11
|
+
scope: Omit<ThemeScopeProps, "children">;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The GUIDELINES §8 scopes: dark, light and both high-contrast modes in the site's own accent and radius,
|
|
16
|
+
* then two variants (by default dark · radius-none · cyan and light · radius-xl · yellow).
|
|
17
|
+
*/
|
|
18
|
+
export function reviewScopes(
|
|
19
|
+
site: {accent: Accent; radius: Radius},
|
|
20
|
+
variants: {mode: ResolvedMode; accent: Accent; radius: Radius}[] = [
|
|
21
|
+
{mode: "dark", accent: "cyan", radius: "none"},
|
|
22
|
+
{mode: "light", accent: "yellow", radius: "xl"},
|
|
23
|
+
],
|
|
24
|
+
): ReviewScope[] {
|
|
25
|
+
return [
|
|
26
|
+
{label: "dark", scope: {mode: "dark", contrast: "normal", ...site}},
|
|
27
|
+
{label: "light", scope: {mode: "light", contrast: "normal", ...site}},
|
|
28
|
+
{label: "dark · hc", scope: {mode: "dark", contrast: "high", ...site}},
|
|
29
|
+
{label: "light · hc", scope: {mode: "light", contrast: "high", ...site}},
|
|
30
|
+
...variants.map(({mode, accent, radius}) => ({
|
|
31
|
+
label: `${mode} · radius-${radius} · ${accent}`,
|
|
32
|
+
scope: {mode, contrast: "normal" as const, accent, radius},
|
|
33
|
+
})),
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Every demo of a page in each scope, side by side, for visual review (GUIDELINES §8). */
|
|
38
|
+
export function ReviewMatrix({doc, scopes}: {doc: DocEntry; scopes: ReviewScope[]}) {
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex flex-col gap-6 p-6" data-slot="review">
|
|
41
|
+
<h1 className="font-mono text-lg font-semibold">{doc.title} · review</h1>
|
|
42
|
+
{doc.demos.map((demo) => {
|
|
43
|
+
const Component = demo.component;
|
|
44
|
+
return (
|
|
45
|
+
<section key={demo.title} className="flex flex-col gap-2">
|
|
46
|
+
<h2 className="font-mono text-sm text-muted-foreground">{demo.title}</h2>
|
|
47
|
+
<div className="grid grid-cols-2 gap-px overflow-hidden border bg-border">
|
|
48
|
+
{scopes.map(({label, scope}) => (
|
|
49
|
+
<ThemeScope key={label} {...scope} className="flex flex-col gap-3 p-4">
|
|
50
|
+
<span className="font-mono text-[10px] text-muted-foreground">{label}</span>
|
|
51
|
+
<div
|
|
52
|
+
className={cn(
|
|
53
|
+
"flex min-h-24 flex-1",
|
|
54
|
+
demo.layout === "full" ? "" : "items-center justify-center",
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
<Component />
|
|
58
|
+
</div>
|
|
59
|
+
</ThemeScope>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
</section>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {useResolvedTheme} from "@repraze/kernelkit/provider";
|
|
2
|
+
import {allModes, createPalette, type Mode} from "@repraze/termicolor";
|
|
3
|
+
import {useMemo} from "react";
|
|
4
|
+
|
|
5
|
+
const cache = new Map<string, Mode>();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The TermiColor mode that applies here (nearest ThemeScope, else the ThemeProvider): neutrals, accents
|
|
9
|
+
* and roles as hex strings, with `roles.accent` & co. in the current accent. For canvas, chart and WebGPU
|
|
10
|
+
* code that can't read CSS variables. Memoised per mode, contrast and accent.
|
|
11
|
+
*/
|
|
12
|
+
export function useThemePalette(): Mode {
|
|
13
|
+
const {mode, contrast, accent} = useResolvedTheme();
|
|
14
|
+
return useMemo(() => {
|
|
15
|
+
const id = `${mode}${contrast === "high" ? "-hc" : ""}`;
|
|
16
|
+
const key = `${id}:${accent}`;
|
|
17
|
+
let found = cache.get(key);
|
|
18
|
+
if (!found) {
|
|
19
|
+
const modes = allModes(createPalette({accent}));
|
|
20
|
+
found = modes.find((m) => m.id === id) ?? modes[0]!;
|
|
21
|
+
cache.set(key, found);
|
|
22
|
+
}
|
|
23
|
+
return found;
|
|
24
|
+
}, [mode, contrast, accent]);
|
|
25
|
+
}
|
package/src/lib/recipes.ts
CHANGED
|
@@ -52,3 +52,17 @@ export const placeholderFill = "bg-foreground/10 hc:bg-border";
|
|
|
52
52
|
|
|
53
53
|
/** Selected row (table, data table, code line, list): accent tint + 2px inset primary indicator (§6). */
|
|
54
54
|
export const selectedRow = "bg-accent text-accent-foreground shadow-[inset_2px_0_0_var(--primary-text)]";
|
|
55
|
+
|
|
56
|
+
/**
|
|
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
|
|
59
|
+
* plain border on a card surface (`statusSurfaceNeutral`).
|
|
60
|
+
*/
|
|
61
|
+
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)",
|
|
64
|
+
// no wash: card surface with a hue border, for dense layouts
|
|
65
|
+
outline: "border-(--c)/50 bg-card hc:border-(--c)",
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
export const statusSurfaceNeutral = "border-border bg-card hc:border-border";
|
|
@@ -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 =
|
|
223
|
+
const parent = useResolvedTheme();
|
|
204
224
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
|
205
|
-
const theme =
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
<
|
|
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,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
|
+
}
|