@umami/shiso 0.55.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +9 -49
  3. package/bin/shiso.mjs +132 -0
  4. package/docs.schema.json +835 -0
  5. package/mdx.config.ts +143 -0
  6. package/package.json +73 -83
  7. package/scripts/check-package.mjs +18 -0
  8. package/scripts/generate-icon-registry.mjs +196 -0
  9. package/scripts/generate-last-modified.mjs +128 -0
  10. package/scripts/generate-search-index.mjs +252 -0
  11. package/scripts/load-docs-config.mjs +244 -0
  12. package/scripts/prerender.mjs +187 -0
  13. package/scripts/validate-config.mjs +104 -0
  14. package/scripts/vite-docs-config.mjs +60 -0
  15. package/src/App.tsx +39 -0
  16. package/src/components/Banner.tsx +69 -0
  17. package/src/components/CodeBlock.tsx +46 -0
  18. package/src/components/ConfiguredIcon.tsx +15 -0
  19. package/src/components/ContextualMenu.tsx +93 -0
  20. package/src/components/DocContent.tsx +100 -0
  21. package/src/components/Docs.tsx +126 -0
  22. package/src/components/Footer.tsx +84 -0
  23. package/src/components/Header.tsx +72 -0
  24. package/src/components/Layout.tsx +16 -0
  25. package/src/components/PageLinks.tsx +71 -0
  26. package/src/components/Search.tsx +208 -0
  27. package/src/components/SideNav.tsx +347 -0
  28. package/src/components/SocialIcon.tsx +88 -0
  29. package/src/components/ThemeToggle.tsx +34 -0
  30. package/src/components/TopNav.tsx +127 -0
  31. package/src/components/docs/Accordion.tsx +68 -0
  32. package/src/components/docs/Badge.tsx +171 -0
  33. package/src/components/docs/Callout.tsx +73 -0
  34. package/src/components/docs/Card.tsx +158 -0
  35. package/src/components/docs/CodeGroup.tsx +73 -0
  36. package/src/components/docs/Columns.tsx +20 -0
  37. package/src/components/docs/Expandable.tsx +28 -0
  38. package/src/components/docs/Frame.tsx +56 -0
  39. package/src/components/docs/Icon.tsx +30 -0
  40. package/src/components/docs/ParamField.tsx +45 -0
  41. package/src/components/docs/PropertiesTable.tsx +84 -0
  42. package/src/components/docs/ResponseField.tsx +36 -0
  43. package/src/components/docs/Steps.tsx +47 -0
  44. package/src/components/docs/Tabs.tsx +116 -0
  45. package/src/components/docs/Tooltip.tsx +21 -0
  46. package/src/components/docs/index.ts +15 -0
  47. package/src/components/docs/styles.ts +82 -0
  48. package/src/components/docs/utils.ts +118 -0
  49. package/src/components/icons/index.ts +17 -0
  50. package/src/components/ui/accordion.tsx +69 -0
  51. package/src/components/ui/alert.tsx +69 -0
  52. package/src/components/ui/badge.tsx +49 -0
  53. package/src/components/ui/button.tsx +58 -0
  54. package/src/components/ui/card.tsx +88 -0
  55. package/src/components/ui/collapsible.tsx +15 -0
  56. package/src/components/ui/command.tsx +173 -0
  57. package/src/components/ui/dialog.tsx +137 -0
  58. package/src/components/ui/dropdown-menu.tsx +257 -0
  59. package/src/components/ui/scroll-area.tsx +71 -0
  60. package/src/components/ui/sheet.tsx +124 -0
  61. package/src/components/ui/tabs.tsx +73 -0
  62. package/src/components/ui/tooltip.tsx +52 -0
  63. package/src/declarations.d.ts +9 -0
  64. package/src/entry-client.tsx +17 -0
  65. package/src/entry-server.tsx +77 -0
  66. package/src/generated/last-modified.ts +2 -0
  67. package/src/lib/content.ts +44 -0
  68. package/src/lib/docs-config.ts +682 -0
  69. package/src/lib/head.ts +220 -0
  70. package/src/lib/icon-registry.generated.ts +4 -0
  71. package/src/lib/icons.ts +29 -0
  72. package/src/lib/inline-markdown.tsx +86 -0
  73. package/src/lib/mdast.ts +56 -0
  74. package/src/lib/paths.ts +86 -0
  75. package/src/lib/remark-toc.ts +71 -0
  76. package/src/lib/search/config.ts +43 -0
  77. package/src/lib/search/provider.ts +80 -0
  78. package/src/lib/search/providers/local.ts +15 -0
  79. package/src/lib/search-index.generated.ts +4 -0
  80. package/src/lib/search.ts +100 -0
  81. package/src/lib/site-config.ts +104 -0
  82. package/src/lib/site-model.ts +221 -0
  83. package/src/lib/slug.ts +38 -0
  84. package/src/lib/types.ts +478 -0
  85. package/src/lib/utils.ts +6 -0
  86. package/src/pages/DocPage.tsx +33 -0
  87. package/src/styles/global.css +268 -0
  88. package/src/styles/tokens.css +114 -0
  89. package/types/client.d.ts +3 -0
  90. package/types/search.d.ts +17 -0
  91. package/vite.config.ts +342 -0
  92. package/LICENSE +0 -21
  93. package/dist/index.css +0 -189
  94. package/dist/index.d.ts +0 -57
  95. package/dist/index.js +0 -464
  96. package/dist/index.mjs +0 -437
  97. package/server/index.d.ts +0 -30
  98. package/server/index.js +0 -189
  99. package/styles.css +0 -4766
@@ -0,0 +1,84 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Badge } from './Badge';
3
+ import type { ResponseFieldProps } from './ResponseField';
4
+ import { decodeHtmlEntities, toElementArray } from './utils';
5
+
6
+ export interface PropertiesTableProps {
7
+ children?: ReactNode;
8
+ }
9
+
10
+ function displayValue(value: ResponseFieldProps['type']): string | undefined {
11
+ if (typeof value === 'string') {
12
+ return decodeHtmlEntities(value);
13
+ }
14
+
15
+ return value === undefined || value === null ? undefined : String(value);
16
+ }
17
+
18
+ export function PropertiesTable({ children }: PropertiesTableProps) {
19
+ const rows = toElementArray<ResponseFieldProps>(children);
20
+
21
+ if (!rows.length) {
22
+ return null;
23
+ }
24
+
25
+ return (
26
+ <div className="my-4 overflow-x-auto rounded-lg border border-border p-6">
27
+ <table className="m-0 min-w-[40rem] table-fixed border-collapse">
28
+ <thead>
29
+ <tr>
30
+ <th className="w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-3 pr-6 text-left text-sm font-semibold text-foreground">
31
+ Name
32
+ </th>
33
+ <th className="w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-3 pr-6 text-left text-sm font-semibold text-foreground">
34
+ Type
35
+ </th>
36
+ <th className="w-1/3 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-3 text-left text-sm font-semibold text-foreground">
37
+ Description
38
+ </th>
39
+ </tr>
40
+ </thead>
41
+ <tbody>
42
+ {rows.map((row, index) => {
43
+ const { name, type, required, deprecated, children: description } = row.props;
44
+ const defaultValue = displayValue(row.props.default);
45
+ const borderClass = index === rows.length - 1 ? 'border-b-0' : 'border-b';
46
+
47
+ return (
48
+ <tr key={row.key ?? name}>
49
+ <td
50
+ className={`${borderClass} border-x-0 border-t-0 border-border px-0 py-3 pr-6 align-top text-sm text-foreground`}
51
+ >
52
+ <span className="inline-flex flex-wrap items-center gap-2">
53
+ {name}
54
+ {required ? (
55
+ <Badge size="xs" tone="primary">
56
+ required
57
+ </Badge>
58
+ ) : null}
59
+ {deprecated ? <Badge size="xs">deprecated</Badge> : null}
60
+ </span>
61
+ </td>
62
+ <td
63
+ className={`${borderClass} border-x-0 border-t-0 border-border px-0 py-3 pr-6 align-top text-sm text-foreground`}
64
+ >
65
+ {displayValue(type)}
66
+ </td>
67
+ <td
68
+ className={`${borderClass} border-x-0 border-t-0 border-border px-0 py-3 align-top text-sm leading-6 text-foreground [&_p]:m-0`}
69
+ >
70
+ {description}
71
+ {defaultValue ? (
72
+ <span className="mt-1 block text-sm text-muted-foreground">
73
+ Default: {defaultValue}
74
+ </span>
75
+ ) : null}
76
+ </td>
77
+ </tr>
78
+ );
79
+ })}
80
+ </tbody>
81
+ </table>
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,36 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Badge } from './Badge';
3
+ import { styles } from './styles';
4
+ import { decodeHtmlEntities } from './utils';
5
+
6
+ type FieldValue = string | number | boolean | null | undefined;
7
+
8
+ export interface ResponseFieldProps {
9
+ name: string;
10
+ type?: FieldValue;
11
+ default?: FieldValue;
12
+ required?: boolean;
13
+ deprecated?: boolean;
14
+ children?: ReactNode;
15
+ }
16
+
17
+ export function ResponseField({ name, type, required, deprecated, children }: ResponseFieldProps) {
18
+ const normalizedType =
19
+ typeof type === 'string'
20
+ ? decodeHtmlEntities(type)
21
+ : type === undefined || type === null
22
+ ? undefined
23
+ : String(type);
24
+
25
+ return (
26
+ <div className={styles.field}>
27
+ <div className={styles.fieldHeader}>
28
+ <span className={styles.fieldName}>{name}</span>
29
+ {normalizedType ? <span className={styles.fieldType}>{normalizedType}</span> : null}
30
+ {required ? <Badge tone="primary">required</Badge> : null}
31
+ {deprecated ? <Badge>deprecated</Badge> : null}
32
+ </div>
33
+ {children ? <div className={styles.fieldBody}>{children}</div> : null}
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,47 @@
1
+ import type { ReactNode } from 'react';
2
+ import { useMemo } from 'react';
3
+ import { styles } from './styles';
4
+ import { toElementArray } from './utils';
5
+
6
+ interface StepChildProps {
7
+ title?: ReactNode;
8
+ children?: ReactNode;
9
+ }
10
+
11
+ export function Step({ children }: StepChildProps) {
12
+ return <>{children}</>;
13
+ }
14
+
15
+ export interface StepsProps {
16
+ children?: ReactNode;
17
+ }
18
+
19
+ export function Steps({ children }: StepsProps) {
20
+ const steps = useMemo(() => {
21
+ return toElementArray<StepChildProps>(children).map((child, index) => {
22
+ return {
23
+ title: child.props.title || `Step ${index + 1}`,
24
+ content: child.props.children,
25
+ };
26
+ });
27
+ }, [children]);
28
+
29
+ if (!steps.length) {
30
+ return null;
31
+ }
32
+
33
+ return (
34
+ <div className={`${styles.steps} ${steps.length === 1 ? 'before:hidden' : ''}`}>
35
+ {steps.map((step, index) => (
36
+ // biome-ignore lint/suspicious/noArrayIndexKey: steps are static MDX content
37
+ <div key={`step-${index + 1}`} className={styles.step}>
38
+ <div className={styles.stepNumber}>{index + 1}</div>
39
+ <div className={styles.stepContent}>
40
+ <div className={styles.stepTitle}>{step.title}</div>
41
+ <div>{step.content}</div>
42
+ </div>
43
+ </div>
44
+ ))}
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,116 @@
1
+ import type { ReactNode } from 'react';
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { TabsContent, TabsList, Tabs as TabsPrimitive, TabsTrigger } from '@/components/ui/tabs';
4
+ import { slugify, toElementArray } from './utils';
5
+
6
+ interface TabChildProps {
7
+ title?: ReactNode;
8
+ children?: ReactNode;
9
+ }
10
+
11
+ export function Tab({ children }: TabChildProps) {
12
+ return <>{children}</>;
13
+ }
14
+
15
+ export interface TabsProps {
16
+ children?: ReactNode;
17
+ group?: string;
18
+ }
19
+
20
+ export function Tabs({ children, group }: TabsProps) {
21
+ const tabs = useMemo(() => {
22
+ const counts = new Map<string, number>();
23
+
24
+ return toElementArray<TabChildProps>(children).map((child, index) => {
25
+ const title = child.props.title || `Tab ${index + 1}`;
26
+ const keyBase = slugify(title, `tab-${index + 1}`);
27
+ const count = counts.get(keyBase) || 0;
28
+ counts.set(keyBase, count + 1);
29
+
30
+ return {
31
+ id: count ? `${keyBase}-${count + 1}` : keyBase,
32
+ title,
33
+ content: child.props.children,
34
+ };
35
+ });
36
+ }, [children]);
37
+
38
+ const defaultTab = tabs[0]?.id;
39
+ const [selectedKey, setSelectedKey] = useState<string | undefined>(defaultTab);
40
+
41
+ useEffect(() => {
42
+ if (!group || typeof window === 'undefined') {
43
+ return;
44
+ }
45
+
46
+ const storedKey = window.localStorage.getItem(`shiso-tabs:${group}`);
47
+ if (storedKey && tabs.some(tab => tab.id === storedKey)) {
48
+ setSelectedKey(storedKey);
49
+ return;
50
+ }
51
+
52
+ if (defaultTab) {
53
+ setSelectedKey(defaultTab);
54
+ }
55
+ }, [defaultTab, group, tabs]);
56
+
57
+ useEffect(() => {
58
+ if (!tabs.length) {
59
+ setSelectedKey(undefined);
60
+ return;
61
+ }
62
+
63
+ if (!selectedKey || !tabs.some(tab => tab.id === selectedKey)) {
64
+ setSelectedKey(tabs[0].id);
65
+ }
66
+ }, [selectedKey, tabs]);
67
+
68
+ const handleSelect = (id: string) => {
69
+ setSelectedKey(id);
70
+
71
+ if (group && typeof window !== 'undefined') {
72
+ window.localStorage.setItem(`shiso-tabs:${group}`, id);
73
+ }
74
+ };
75
+
76
+ if (!tabs.length) {
77
+ return null;
78
+ }
79
+
80
+ if (tabs.length === 1) {
81
+ return <div className="my-4">{tabs[0].content}</div>;
82
+ }
83
+
84
+ return (
85
+ <TabsPrimitive
86
+ className="my-4 gap-0"
87
+ value={selectedKey}
88
+ onValueChange={value => {
89
+ if (typeof value === 'string') {
90
+ handleSelect(value);
91
+ }
92
+ }}
93
+ >
94
+ <TabsList
95
+ variant="line"
96
+ className="w-full justify-start gap-5 overflow-x-auto overflow-y-hidden p-0 shadow-[inset_0_-1px_0_var(--border)] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
97
+ aria-label="Content tabs"
98
+ >
99
+ {tabs.map(tab => (
100
+ <TabsTrigger
101
+ key={tab.id}
102
+ value={tab.id}
103
+ className="flex-none px-0 text-muted-foreground after:bg-primary data-active:text-foreground"
104
+ >
105
+ {tab.title}
106
+ </TabsTrigger>
107
+ ))}
108
+ </TabsList>
109
+ {tabs.map(tab => (
110
+ <TabsContent key={tab.id} value={tab.id} className="pt-4 text-base">
111
+ {tab.content}
112
+ </TabsContent>
113
+ ))}
114
+ </TabsPrimitive>
115
+ );
116
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from 'react';
2
+ import {
3
+ TooltipContent,
4
+ Tooltip as TooltipPrimitive,
5
+ TooltipTrigger,
6
+ } from '@/components/ui/tooltip';
7
+ import { styles } from './styles';
8
+
9
+ export interface TooltipProps {
10
+ tip?: ReactNode;
11
+ children?: ReactNode;
12
+ }
13
+
14
+ export function Tooltip({ tip, children }: TooltipProps) {
15
+ return (
16
+ <TooltipPrimitive>
17
+ <TooltipTrigger className={styles.tooltipTrigger}>{children}</TooltipTrigger>
18
+ <TooltipContent>{tip}</TooltipContent>
19
+ </TooltipPrimitive>
20
+ );
21
+ }
@@ -0,0 +1,15 @@
1
+ export * from './Accordion';
2
+ export * from './Badge';
3
+ export * from './Callout';
4
+ export * from './Card';
5
+ export * from './CodeGroup';
6
+ export * from './Columns';
7
+ export * from './Expandable';
8
+ export * from './Frame';
9
+ export * from './Icon';
10
+ export * from './ParamField';
11
+ export * from './PropertiesTable';
12
+ export * from './ResponseField';
13
+ export * from './Steps';
14
+ export * from './Tabs';
15
+ export * from './Tooltip';
@@ -0,0 +1,82 @@
1
+ /** Shared Tailwind utility groups for the built-in MDX components. */
2
+ export const styles = {
3
+ accordion: 'my-3',
4
+ accordionGroup:
5
+ 'my-4 overflow-hidden rounded-lg border border-border [&_[data-slot=accordion-item]]:rounded-none [&_[data-slot=accordion-item]]:border-0 [&_[data-slot=accordion-item]+[data-slot=accordion-item]]:border-t [&_[data-slot=accordion-trigger]]:rounded-none',
6
+ accordionItem: 'rounded-lg border border-border bg-transparent',
7
+ accordionTrigger:
8
+ 'items-center px-4 py-3 text-base font-semibold text-foreground hover:no-underline',
9
+ accordionContent: 'px-4 pb-4 text-base leading-7',
10
+ expandableTrigger:
11
+ 'items-center px-4 py-3 text-sm font-normal text-foreground hover:no-underline',
12
+
13
+ callout: 'my-4 flex items-start gap-3 rounded-lg border px-3 py-2.5 text-sm leading-6',
14
+ calloutIcon:
15
+ 'mt-[calc((1.5em-1.25rem)/2)] inline-flex size-5 shrink-0 items-center justify-center [&_svg]:block [&_svg]:size-4',
16
+ calloutBody:
17
+ 'flex min-w-0 flex-1 flex-col gap-[0.35rem] [&>:first-child]:mt-0 [&>:last-child]:mb-0 [&_:where(p,ul,ol)]:my-[0.35rem] [&>:where(p,ul,ol):first-child]:mt-0 [&>:where(p,ul,ol):last-child]:mb-0',
18
+ calloutTitle: 'font-semibold leading-6',
19
+ note: 'border-border bg-muted/50 text-foreground',
20
+ info: 'border-blue-300 bg-blue-50 text-blue-900 dark:border-blue-400/40 dark:bg-blue-400/10 dark:text-blue-300',
21
+ warning:
22
+ 'border-amber-300 bg-amber-50 text-amber-900 dark:border-amber-400/40 dark:bg-amber-400/10 dark:text-amber-300',
23
+ tip: 'border-green-300 bg-green-50 text-green-900 dark:border-green-400/40 dark:bg-green-400/10 dark:text-green-300',
24
+ check:
25
+ 'border-emerald-300 bg-emerald-50 text-emerald-900 dark:border-emerald-400/40 dark:bg-emerald-400/10 dark:text-emerald-300',
26
+ danger: 'border-destructive/40 bg-destructive/10 text-destructive',
27
+
28
+ card: 'block h-full gap-0 rounded-lg border border-border bg-transparent p-4 text-base ring-0 hover:border-input',
29
+ cardHorizontal:
30
+ '[&_[data-slot=card-inner]]:items-center [&_[data-slot=card-main]]:flex-row [&_[data-slot=card-main]]:items-center [&_[data-slot=card-main]]:gap-3 [&_[data-slot=card-body]]:m-0',
31
+ cardTyped:
32
+ '[&_[data-slot=card-title]]:text-inherit [&_[data-slot=card-body]]:text-inherit [&_[data-slot=card-cta]]:text-inherit [&_[data-slot=card-arrow]]:text-inherit',
33
+ cardImageLayout: 'p-0 [&_[data-slot=card-inner]]:p-6',
34
+ cardImage: 'block aspect-video w-full border-border border-b object-cover',
35
+ cardInner: 'flex items-start justify-between gap-3',
36
+ cardMain: 'flex min-w-0 flex-col gap-2',
37
+ cardHeader: 'flex items-center gap-2',
38
+ cardIcon: 'inline-flex shrink-0 items-center justify-center [&_img]:size-4 [&_svg]:size-4',
39
+ cardTitle: 'text-base font-semibold text-foreground',
40
+ cardBody: 'text-muted-foreground [&>:first-child]:mt-0 [&>:last-child]:mb-0',
41
+ cardCta:
42
+ 'flex shrink-0 items-center gap-[0.35rem] text-[0.9rem] font-medium text-muted-foreground',
43
+ cardArrow: 'shrink-0 text-muted-foreground',
44
+
45
+ grid: 'my-4 grid grid-cols-1 gap-4',
46
+ gridCols1: 'md:grid-cols-1',
47
+ gridCols2: 'md:grid-cols-2',
48
+ gridCols3: 'md:grid-cols-3',
49
+ gridCols4: 'md:grid-cols-4',
50
+ column: 'min-w-0 [&>:first-child]:mt-0 [&>:last-child]:mb-0',
51
+ icon: 'inline-block align-[-0.145em]',
52
+
53
+ tabs: 'my-4 gap-0',
54
+
55
+ frameWrapper: 'my-4',
56
+ frameHint: 'mb-4 flex items-start gap-2 text-sm font-medium leading-5 text-foreground',
57
+ frameHintIcon:
58
+ 'flex h-5 shrink-0 items-center text-muted-foreground [&_svg]:size-4 [&_svg]:fill-current',
59
+ frame: 'relative overflow-hidden rounded-2xl border border-border bg-muted/25 p-2',
60
+ frameContent:
61
+ 'relative flex justify-center overflow-hidden rounded-xl [&_p]:m-0 [&_img]:w-full [&_video]:w-full',
62
+ frameCaption:
63
+ 'relative mt-3 px-4 pb-1 text-center text-sm leading-5 text-muted-foreground [&_p]:m-0 [&_a]:font-semibold',
64
+
65
+ field: 'my-3',
66
+ paramField: 'my-3',
67
+ fieldHeader: 'flex flex-wrap items-center gap-2',
68
+ fieldBody: 'mt-3',
69
+ fieldType: 'text-[0.9rem] text-muted-foreground',
70
+ fieldName: 'font-bold text-foreground',
71
+ code: 'rounded-sm bg-[color-mix(in_srgb,currentColor_4%,transparent)] px-[0.35rem] py-[0.1rem] text-sm text-foreground font-mono',
72
+ steps:
73
+ "relative my-4 flex flex-col gap-5 before:absolute before:top-4 before:bottom-4 before:left-4 before:w-px before:-translate-x-1/2 before:bg-border before:content-['']",
74
+ step: 'relative flex items-start gap-4',
75
+ stepNumber:
76
+ 'relative z-[1] flex size-8 shrink-0 items-center justify-center rounded-full border border-border bg-card text-[0.9rem] font-semibold text-foreground',
77
+ stepContent: 'flex min-w-0 grow flex-col gap-2',
78
+ stepTitle: 'text-[1.1rem] font-semibold text-foreground',
79
+
80
+ tooltipTrigger:
81
+ 'peer cursor-help border-0 border-muted-foreground border-b border-dotted bg-transparent p-0 text-inherit [font:inherit]',
82
+ } as const;
@@ -0,0 +1,118 @@
1
+ import { Children, createElement, isValidElement, type ReactElement, type ReactNode } from 'react';
2
+ import { getIcon } from '@/lib/icons';
3
+
4
+ interface NodeWithClassName {
5
+ className?: string;
6
+ 'data-title'?: string;
7
+ children?: ReactNode;
8
+ }
9
+
10
+ export function toElementArray<T>(children: ReactNode): ReactElement<T>[] {
11
+ return Children.toArray(children).filter(isValidElement) as ReactElement<T>[];
12
+ }
13
+
14
+ /**
15
+ * Icon props accept either a rendered node or a lucide icon name, since MDX authors
16
+ * write `icon="rocket"` while first-party components pass elements directly.
17
+ */
18
+ export function resolveIcon(icon: ReactNode | string, size = 14): ReactNode {
19
+ if (typeof icon !== 'string') {
20
+ return icon;
21
+ }
22
+
23
+ if (
24
+ /^(?:https?:|data:image\/|\/|\.\.?\/)/i.test(icon) ||
25
+ /\.(?:avif|gif|jpe?g|png|svg|webp)(?:[?#].*)?$/i.test(icon)
26
+ ) {
27
+ return createElement('img', { src: icon, alt: '', width: size, height: size });
28
+ }
29
+
30
+ const Component = getIcon(icon);
31
+
32
+ return Component ? createElement(Component, { size }) : null;
33
+ }
34
+
35
+ export function slugify(value: ReactNode, fallback: string): string {
36
+ const text = typeof value === 'string' ? value : '';
37
+ const normalized = text
38
+ .trim()
39
+ .toLowerCase()
40
+ .replace(/[^a-z0-9]+/g, '-')
41
+ .replace(/^-+|-+$/g, '');
42
+
43
+ return normalized || fallback;
44
+ }
45
+
46
+ const GRID_COLS_CLASSES = {
47
+ 1: 'gridCols1',
48
+ 2: 'gridCols2',
49
+ 3: 'gridCols3',
50
+ 4: 'gridCols4',
51
+ } as const;
52
+
53
+ /** Shared by CardGroup and Columns: clamps a column count to a CSS module class name. */
54
+ export function gridColsClass(cols: number): (typeof GRID_COLS_CLASSES)[1 | 2 | 3 | 4] {
55
+ const columns = Math.min(Math.max(Math.round(cols) || 1, 1), 4) as 1 | 2 | 3 | 4;
56
+
57
+ return GRID_COLS_CLASSES[columns];
58
+ }
59
+
60
+ export function decodeHtmlEntities(value: string): string {
61
+ return value
62
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => {
63
+ return String.fromCodePoint(Number.parseInt(hex, 16));
64
+ })
65
+ .replace(/&#([0-9]+);/g, (_, decimal) => {
66
+ return String.fromCodePoint(Number.parseInt(decimal, 10));
67
+ })
68
+ .replace(/&quot;/g, '"')
69
+ .replace(/&apos;/g, "'")
70
+ .replace(/&lt;/g, '<')
71
+ .replace(/&gt;/g, '>')
72
+ .replace(/&amp;/g, '&');
73
+ }
74
+
75
+ function normalizeLanguage(value: string): string {
76
+ const label = value.toLowerCase();
77
+ const aliases: Record<string, string> = {
78
+ js: 'javascript',
79
+ ts: 'typescript',
80
+ sh: 'shell',
81
+ zsh: 'shell',
82
+ shell: 'shell',
83
+ bash: 'bash',
84
+ yml: 'yaml',
85
+ md: 'markdown',
86
+ };
87
+
88
+ return aliases[label] || label;
89
+ }
90
+
91
+ export function findCodeLanguage(node: ReactNode): string | undefined {
92
+ if (!isValidElement<NodeWithClassName>(node)) {
93
+ return undefined;
94
+ }
95
+
96
+ const className = node.props.className;
97
+ if (typeof className === 'string') {
98
+ const match = className.match(/language-([a-z0-9-]+)/i);
99
+ if (match?.[1]) {
100
+ return normalizeLanguage(match[1]);
101
+ }
102
+ }
103
+
104
+ return findCodeLanguage(node.props.children);
105
+ }
106
+
107
+ export function findCodeTitle(node: ReactNode): string | undefined {
108
+ if (!isValidElement<NodeWithClassName>(node)) {
109
+ return undefined;
110
+ }
111
+
112
+ const title = node.props['data-title'];
113
+ if (typeof title === 'string' && title.trim()) {
114
+ return title.trim();
115
+ }
116
+
117
+ return findCodeTitle(node.props.children);
118
+ }
@@ -0,0 +1,17 @@
1
+ export {
2
+ Check,
3
+ Check as CheckIcon,
4
+ ChevronRight,
5
+ CircleAlert,
6
+ Copy,
7
+ ExternalLink,
8
+ Hand,
9
+ Info as InfoIcon,
10
+ Lightbulb,
11
+ Menu,
12
+ Moon,
13
+ Search,
14
+ Sun,
15
+ TriangleAlert,
16
+ X,
17
+ } from 'lucide-react';
@@ -0,0 +1,69 @@
1
+ import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion';
2
+ import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
3
+ import { cn } from '@/lib/utils';
4
+
5
+ function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
6
+ return (
7
+ <AccordionPrimitive.Root
8
+ data-slot="accordion"
9
+ className={cn('flex w-full flex-col', className)}
10
+ {...props}
11
+ />
12
+ );
13
+ }
14
+
15
+ function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
16
+ return (
17
+ <AccordionPrimitive.Item
18
+ data-slot="accordion-item"
19
+ className={cn('not-last:border-b', className)}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ function AccordionTrigger({ className, children, ...props }: AccordionPrimitive.Trigger.Props) {
26
+ return (
27
+ <AccordionPrimitive.Header className="m-0 flex">
28
+ <AccordionPrimitive.Trigger
29
+ data-slot="accordion-trigger"
30
+ className={cn(
31
+ 'group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground',
32
+ className,
33
+ )}
34
+ {...props}
35
+ >
36
+ {children}
37
+ <ChevronDownIcon
38
+ data-slot="accordion-trigger-icon"
39
+ className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
40
+ />
41
+ <ChevronUpIcon
42
+ data-slot="accordion-trigger-icon"
43
+ className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
44
+ />
45
+ </AccordionPrimitive.Trigger>
46
+ </AccordionPrimitive.Header>
47
+ );
48
+ }
49
+
50
+ function AccordionContent({ className, children, ...props }: AccordionPrimitive.Panel.Props) {
51
+ return (
52
+ <AccordionPrimitive.Panel
53
+ data-slot="accordion-content"
54
+ className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
55
+ {...props}
56
+ >
57
+ <div
58
+ className={cn(
59
+ 'h-(--accordion-panel-height) pt-0 pb-2.5 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
60
+ className,
61
+ )}
62
+ >
63
+ {children}
64
+ </div>
65
+ </AccordionPrimitive.Panel>
66
+ );
67
+ }
68
+
69
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
@@ -0,0 +1,69 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+ import type * as React from 'react';
3
+
4
+ import { cn } from '@/lib/utils';
5
+
6
+ const alertVariants = cva(
7
+ "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'bg-card text-card-foreground',
12
+ destructive:
13
+ 'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current',
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: 'default',
18
+ },
19
+ },
20
+ );
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ 'font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground',
43
+ className,
44
+ )}
45
+ {...props}
46
+ />
47
+ );
48
+ }
49
+
50
+ function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
51
+ return (
52
+ <div
53
+ data-slot="alert-description"
54
+ className={cn(
55
+ 'text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
56
+ className,
57
+ )}
58
+ {...props}
59
+ />
60
+ );
61
+ }
62
+
63
+ function AlertAction({ className, ...props }: React.ComponentProps<'div'>) {
64
+ return (
65
+ <div data-slot="alert-action" className={cn('absolute top-2 right-2', className)} {...props} />
66
+ );
67
+ }
68
+
69
+ export { Alert, AlertAction, AlertDescription, AlertTitle };