@umami/shiso 0.55.0 → 1.0.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.
- package/CHANGELOG.md +39 -0
- package/README.md +9 -49
- package/bin/shiso.mjs +132 -0
- package/docs.schema.json +896 -0
- package/mdx.config.ts +143 -0
- package/package.json +74 -83
- package/scripts/check-package.mjs +54 -0
- package/scripts/generate-icon-registry.mjs +196 -0
- package/scripts/generate-last-modified.mjs +128 -0
- package/scripts/generate-search-index.mjs +298 -0
- package/scripts/lib/mdast.mjs +23 -0
- package/scripts/lib/slug.mjs +21 -0
- package/scripts/load-docs-config.mjs +244 -0
- package/scripts/prerender.mjs +194 -0
- package/scripts/validate-config.mjs +104 -0
- package/scripts/vite-docs-config.mjs +60 -0
- package/src/App.tsx +38 -0
- package/src/components/Banner.tsx +69 -0
- package/src/components/CodeBlock.tsx +46 -0
- package/src/components/ConfiguredIcon.tsx +15 -0
- package/src/components/ContextualMenu.tsx +93 -0
- package/src/components/DocContent.tsx +105 -0
- package/src/components/Docs.tsx +134 -0
- package/src/components/Footer.tsx +81 -0
- package/src/components/Header.tsx +82 -0
- package/src/components/LanguageSwitcher.tsx +59 -0
- package/src/components/Layout.tsx +24 -0
- package/src/components/PageLinks.tsx +71 -0
- package/src/components/Search.tsx +217 -0
- package/src/components/SideNav.tsx +347 -0
- package/src/components/SocialIcon.tsx +88 -0
- package/src/components/ThemeToggle.tsx +34 -0
- package/src/components/TopNav.tsx +127 -0
- package/src/components/VersionSwitcher.tsx +60 -0
- package/src/components/docs/Accordion.tsx +68 -0
- package/src/components/docs/Badge.tsx +171 -0
- package/src/components/docs/Callout.tsx +73 -0
- package/src/components/docs/Card.tsx +158 -0
- package/src/components/docs/CodeGroup.tsx +73 -0
- package/src/components/docs/Columns.tsx +20 -0
- package/src/components/docs/Expandable.tsx +28 -0
- package/src/components/docs/Frame.tsx +56 -0
- package/src/components/docs/Icon.tsx +30 -0
- package/src/components/docs/ParamField.tsx +45 -0
- package/src/components/docs/PropertiesTable.tsx +84 -0
- package/src/components/docs/ResponseField.tsx +36 -0
- package/src/components/docs/Steps.tsx +47 -0
- package/src/components/docs/Tabs.tsx +116 -0
- package/src/components/docs/Tooltip.tsx +21 -0
- package/src/components/docs/index.ts +15 -0
- package/src/components/docs/styles.ts +82 -0
- package/src/components/docs/utils.ts +118 -0
- package/src/components/icons/index.ts +17 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert.tsx +69 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/card.tsx +88 -0
- package/src/components/ui/collapsible.tsx +15 -0
- package/src/components/ui/command.tsx +173 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/scroll-area.tsx +71 -0
- package/src/components/ui/sheet.tsx +124 -0
- package/src/components/ui/tabs.tsx +73 -0
- package/src/components/ui/tooltip.tsx +52 -0
- package/src/declarations.d.ts +9 -0
- package/src/entry-client.tsx +17 -0
- package/src/entry-server.tsx +89 -0
- package/src/generated/last-modified.ts +2 -0
- package/src/lib/content.ts +44 -0
- package/src/lib/docs-config.ts +986 -0
- package/src/lib/head.ts +231 -0
- package/src/lib/icon-registry.generated.ts +4 -0
- package/src/lib/icons.ts +29 -0
- package/src/lib/inline-markdown.tsx +86 -0
- package/src/lib/locale.ts +39 -0
- package/src/lib/mdast.ts +56 -0
- package/src/lib/paths.ts +86 -0
- package/src/lib/remark-toc.ts +71 -0
- package/src/lib/search/config.ts +43 -0
- package/src/lib/search/provider.ts +85 -0
- package/src/lib/search/providers/local.ts +15 -0
- package/src/lib/search-index.generated.ts +4 -0
- package/src/lib/search.ts +128 -0
- package/src/lib/site-config.ts +117 -0
- package/src/lib/site-model.ts +221 -0
- package/src/lib/slug.ts +38 -0
- package/src/lib/types.ts +515 -0
- package/src/lib/utils.ts +6 -0
- package/src/pages/DocPage.tsx +33 -0
- package/src/styles/global.css +268 -0
- package/src/styles/tokens.css +114 -0
- package/types/client.d.ts +3 -0
- package/types/search.d.ts +27 -0
- package/vite.config.ts +342 -0
- package/LICENSE +0 -21
- package/dist/index.css +0 -189
- package/dist/index.d.ts +0 -57
- package/dist/index.js +0 -464
- package/dist/index.mjs +0 -437
- package/server/index.d.ts +0 -30
- package/server/index.js +0 -189
- package/styles.css +0 -4766
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { Link, useLocation, useNavigate } from 'react-router';
|
|
3
|
+
import { ConfiguredIcon } from '@/components/ConfiguredIcon';
|
|
4
|
+
import { ChevronRight } from '@/components/icons';
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from '@/components/ui/dropdown-menu';
|
|
11
|
+
import type { DocsTab, LinkTarget, NavNode, NormalizedDocsConfig } from '@/lib/types';
|
|
12
|
+
|
|
13
|
+
interface MenuLink {
|
|
14
|
+
label: string;
|
|
15
|
+
href: string;
|
|
16
|
+
target: LinkTarget;
|
|
17
|
+
routed: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function collectMenuLinks(nodes: NavNode[]): MenuLink[] {
|
|
21
|
+
const links: MenuLink[] = [];
|
|
22
|
+
|
|
23
|
+
for (const node of nodes) {
|
|
24
|
+
if (node.kind === 'page') {
|
|
25
|
+
if (!node.page.hidden) {
|
|
26
|
+
links.push({ label: node.page.label, href: node.page.url, target: '_self', routed: true });
|
|
27
|
+
}
|
|
28
|
+
} else if (node.kind === 'link') {
|
|
29
|
+
if (!node.hidden) {
|
|
30
|
+
links.push({ label: node.label, href: node.href, target: node.target, routed: false });
|
|
31
|
+
}
|
|
32
|
+
} else if (!node.hidden) {
|
|
33
|
+
if (node.root && !node.root.page.hidden) {
|
|
34
|
+
links.push({
|
|
35
|
+
label: node.root.page.label,
|
|
36
|
+
href: node.root.page.url,
|
|
37
|
+
target: '_self',
|
|
38
|
+
routed: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
links.push(...collectMenuLinks(node.children));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return links;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function TopNav({ docs, label }: { docs: NormalizedDocsConfig; label: string }) {
|
|
49
|
+
const { pathname } = useLocation();
|
|
50
|
+
const navigate = useNavigate();
|
|
51
|
+
const tabs = docs.tabs.filter(tab => !tab.hidden);
|
|
52
|
+
|
|
53
|
+
if (!tabs.length) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const page = docs.pages.find(item => item.url === pathname);
|
|
58
|
+
const selected =
|
|
59
|
+
page?.tabId ||
|
|
60
|
+
[...tabs]
|
|
61
|
+
.sort((a, b) => b.url.length - a.url.length)
|
|
62
|
+
.find(tab => pathname === tab.url || pathname.startsWith(`${tab.url}/`))?.id ||
|
|
63
|
+
tabs[0]?.id;
|
|
64
|
+
const tabClass = (tab: DocsTab) =>
|
|
65
|
+
classNames(
|
|
66
|
+
'flex h-full items-center gap-1 whitespace-nowrap border-transparent border-b-2 font-medium',
|
|
67
|
+
{
|
|
68
|
+
'border-b-primary text-foreground': tab.id === selected,
|
|
69
|
+
'text-muted-foreground hover:text-foreground': tab.id !== selected,
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<nav
|
|
75
|
+
className="hidden h-[calc(100%+1px)] max-w-screen self-start items-center gap-7 overflow-x-auto text-sm [scrollbar-width:none] lg:flex [&::-webkit-scrollbar]:hidden"
|
|
76
|
+
aria-label={label}
|
|
77
|
+
>
|
|
78
|
+
{tabs.map(tab => {
|
|
79
|
+
if (tab.presentation === 'dropdown') {
|
|
80
|
+
const menuLinks = collectMenuLinks(docs.navigation[tab.id] || []);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<DropdownMenu key={tab.id}>
|
|
84
|
+
<DropdownMenuTrigger render={<button type="button" className={tabClass(tab)} />}>
|
|
85
|
+
<ConfiguredIcon icon={tab.icon} />
|
|
86
|
+
{tab.label}
|
|
87
|
+
<ChevronRight className="size-3.5 rotate-90" />
|
|
88
|
+
</DropdownMenuTrigger>
|
|
89
|
+
<DropdownMenuContent align="start" className="min-w-48">
|
|
90
|
+
{menuLinks.map(item => (
|
|
91
|
+
<DropdownMenuItem
|
|
92
|
+
key={item.href}
|
|
93
|
+
onClick={() => {
|
|
94
|
+
if (item.routed) {
|
|
95
|
+
navigate(item.href);
|
|
96
|
+
} else {
|
|
97
|
+
window.open(
|
|
98
|
+
item.href,
|
|
99
|
+
item.target,
|
|
100
|
+
item.target === '_blank' ? 'noreferrer' : undefined,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{item.label}
|
|
106
|
+
</DropdownMenuItem>
|
|
107
|
+
))}
|
|
108
|
+
</DropdownMenuContent>
|
|
109
|
+
</DropdownMenu>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Link
|
|
115
|
+
key={tab.id}
|
|
116
|
+
to={tab.url}
|
|
117
|
+
className={tabClass(tab)}
|
|
118
|
+
aria-current={tab.id === selected ? 'page' : undefined}
|
|
119
|
+
>
|
|
120
|
+
<ConfiguredIcon icon={tab.icon} />
|
|
121
|
+
{tab.label}
|
|
122
|
+
</Link>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</nav>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useLocation, useNavigate } from 'react-router';
|
|
2
|
+
import { Check, ChevronRight } from '@/components/icons';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuTrigger,
|
|
9
|
+
} from '@/components/ui/dropdown-menu';
|
|
10
|
+
import { docsSite, getScopeByPathname } from '@/lib/site-config';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Version selector for multi-version sites. Options come from the visible
|
|
14
|
+
* version scopes of the active language; selecting one navigates to that
|
|
15
|
+
* scope's first visible page. Hidden versions never appear as options, but a
|
|
16
|
+
* hidden version still shows as the current value while it is being viewed.
|
|
17
|
+
*/
|
|
18
|
+
export function VersionSwitcher() {
|
|
19
|
+
const { pathname } = useLocation();
|
|
20
|
+
const navigate = useNavigate();
|
|
21
|
+
const current = getScopeByPathname(pathname);
|
|
22
|
+
const options = docsSite.scopes.filter(
|
|
23
|
+
scope => scope.version && !scope.hidden && scope.language === current.language,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (!current.version || (options.length < 2 && !current.hidden)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<DropdownMenu>
|
|
32
|
+
<DropdownMenuTrigger
|
|
33
|
+
render={
|
|
34
|
+
<Button
|
|
35
|
+
variant="outline"
|
|
36
|
+
className="h-auto gap-1.5 rounded-md bg-card px-2.5 py-1.5 text-sm font-medium text-foreground"
|
|
37
|
+
/>
|
|
38
|
+
}
|
|
39
|
+
>
|
|
40
|
+
{current.version}
|
|
41
|
+
<ChevronRight className="size-3.5 rotate-90 text-muted-foreground" />
|
|
42
|
+
</DropdownMenuTrigger>
|
|
43
|
+
<DropdownMenuContent align="start" className="min-w-32">
|
|
44
|
+
{options.map(scope => (
|
|
45
|
+
<DropdownMenuItem
|
|
46
|
+
key={scope.id}
|
|
47
|
+
onClick={() => {
|
|
48
|
+
if (scope.id !== current.id) {
|
|
49
|
+
navigate(scope.firstPageUrl);
|
|
50
|
+
}
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<span className="grow">{scope.version}</span>
|
|
54
|
+
{scope.id === current.id ? <Check className="size-3.5" /> : null}
|
|
55
|
+
</DropdownMenuItem>
|
|
56
|
+
))}
|
|
57
|
+
</DropdownMenuContent>
|
|
58
|
+
</DropdownMenu>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
AccordionContent,
|
|
4
|
+
AccordionItem,
|
|
5
|
+
Accordion as AccordionPrimitive,
|
|
6
|
+
AccordionTrigger,
|
|
7
|
+
} from '@/components/ui/accordion';
|
|
8
|
+
import { styles } from './styles';
|
|
9
|
+
import { resolveIcon, toElementArray } from './utils';
|
|
10
|
+
|
|
11
|
+
export interface AccordionProps {
|
|
12
|
+
title?: ReactNode;
|
|
13
|
+
icon?: ReactNode | string;
|
|
14
|
+
defaultOpen?: boolean;
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function AccordionSection({ value, title, icon, children }: AccordionProps & { value: string }) {
|
|
19
|
+
return (
|
|
20
|
+
<AccordionItem value={value} className={styles.accordionItem}>
|
|
21
|
+
<AccordionTrigger className={styles.accordionTrigger}>
|
|
22
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
23
|
+
{resolveIcon(icon)}
|
|
24
|
+
{title}
|
|
25
|
+
</span>
|
|
26
|
+
</AccordionTrigger>
|
|
27
|
+
<AccordionContent className={styles.accordionContent}>{children}</AccordionContent>
|
|
28
|
+
</AccordionItem>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function Accordion({ title, icon, defaultOpen, children }: AccordionProps) {
|
|
33
|
+
const value = 'accordion-item';
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<AccordionPrimitive defaultValue={defaultOpen ? [value] : []} className={styles.accordion}>
|
|
37
|
+
<AccordionSection value={value} title={title} icon={icon}>
|
|
38
|
+
{children}
|
|
39
|
+
</AccordionSection>
|
|
40
|
+
</AccordionPrimitive>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AccordionGroupProps {
|
|
45
|
+
children?: ReactNode;
|
|
46
|
+
defaultOpen?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function AccordionGroup({ children, defaultOpen = false }: AccordionGroupProps) {
|
|
50
|
+
const items = toElementArray<AccordionProps>(children);
|
|
51
|
+
|
|
52
|
+
if (!items.length) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const values = items.map((_, index) => `accordion-${index + 1}`);
|
|
57
|
+
const defaultValue = values.filter((_, index) => {
|
|
58
|
+
return items[index].props.defaultOpen ?? (defaultOpen && index === 0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<AccordionPrimitive multiple defaultValue={defaultValue} className={styles.accordionGroup}>
|
|
63
|
+
{items.map((child, index) => (
|
|
64
|
+
<AccordionSection key={child.key ?? values[index]} value={values[index]} {...child.props} />
|
|
65
|
+
))}
|
|
66
|
+
</AccordionPrimitive>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Badge as BadgePrimitive } from '@/components/ui/badge';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
import { resolveIcon } from './utils';
|
|
5
|
+
|
|
6
|
+
export type BadgeColor =
|
|
7
|
+
| 'gray'
|
|
8
|
+
| 'blue'
|
|
9
|
+
| 'green'
|
|
10
|
+
| 'yellow'
|
|
11
|
+
| 'orange'
|
|
12
|
+
| 'red'
|
|
13
|
+
| 'purple'
|
|
14
|
+
| 'white'
|
|
15
|
+
| 'surface'
|
|
16
|
+
| 'white-destructive'
|
|
17
|
+
| 'surface-destructive';
|
|
18
|
+
|
|
19
|
+
export type BadgeSize = 'xs' | 'sm' | 'md' | 'lg';
|
|
20
|
+
export type BadgeShape = 'rounded' | 'pill';
|
|
21
|
+
|
|
22
|
+
export interface BadgeProps {
|
|
23
|
+
color?: BadgeColor;
|
|
24
|
+
size?: BadgeSize;
|
|
25
|
+
shape?: BadgeShape;
|
|
26
|
+
icon?: ReactNode;
|
|
27
|
+
stroke?: boolean;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
className?: string;
|
|
30
|
+
/** @deprecated Use `color` instead. */
|
|
31
|
+
tone?: 'muted' | 'primary';
|
|
32
|
+
children?: ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const colorClasses: Record<BadgeColor | 'primary', { filled: string; stroke: string }> = {
|
|
36
|
+
gray: {
|
|
37
|
+
filled: 'border-transparent bg-muted text-muted-foreground',
|
|
38
|
+
stroke: 'border-border bg-transparent text-muted-foreground',
|
|
39
|
+
},
|
|
40
|
+
blue: {
|
|
41
|
+
filled: 'border-transparent bg-blue-100 text-blue-800 dark:bg-blue-400/15 dark:text-blue-300',
|
|
42
|
+
stroke:
|
|
43
|
+
'border-blue-300 bg-transparent text-blue-700 dark:border-blue-400/50 dark:text-blue-300',
|
|
44
|
+
},
|
|
45
|
+
green: {
|
|
46
|
+
filled:
|
|
47
|
+
'border-transparent bg-green-100 text-green-800 dark:bg-green-400/15 dark:text-green-300',
|
|
48
|
+
stroke:
|
|
49
|
+
'border-green-300 bg-transparent text-green-700 dark:border-green-400/50 dark:text-green-300',
|
|
50
|
+
},
|
|
51
|
+
yellow: {
|
|
52
|
+
filled:
|
|
53
|
+
'border-transparent bg-yellow-100 text-yellow-800 dark:bg-yellow-400/15 dark:text-yellow-300',
|
|
54
|
+
stroke:
|
|
55
|
+
'border-yellow-300 bg-transparent text-yellow-700 dark:border-yellow-400/50 dark:text-yellow-300',
|
|
56
|
+
},
|
|
57
|
+
orange: {
|
|
58
|
+
filled:
|
|
59
|
+
'border-transparent bg-orange-100 text-orange-800 dark:bg-orange-400/15 dark:text-orange-300',
|
|
60
|
+
stroke:
|
|
61
|
+
'border-orange-300 bg-transparent text-orange-700 dark:border-orange-400/50 dark:text-orange-300',
|
|
62
|
+
},
|
|
63
|
+
red: {
|
|
64
|
+
filled: 'border-transparent bg-red-100 text-red-800 dark:bg-red-400/15 dark:text-red-300',
|
|
65
|
+
stroke: 'border-red-300 bg-transparent text-red-700 dark:border-red-400/50 dark:text-red-300',
|
|
66
|
+
},
|
|
67
|
+
purple: {
|
|
68
|
+
filled:
|
|
69
|
+
'border-transparent bg-purple-100 text-purple-800 dark:bg-purple-400/15 dark:text-purple-300',
|
|
70
|
+
stroke:
|
|
71
|
+
'border-purple-300 bg-transparent text-purple-700 dark:border-purple-400/50 dark:text-purple-300',
|
|
72
|
+
},
|
|
73
|
+
white: {
|
|
74
|
+
filled: 'border-border bg-white text-zinc-900',
|
|
75
|
+
stroke: 'border-white bg-transparent text-foreground dark:text-white',
|
|
76
|
+
},
|
|
77
|
+
surface: {
|
|
78
|
+
filled: 'border-border bg-background text-foreground',
|
|
79
|
+
stroke: 'border-border bg-transparent text-foreground',
|
|
80
|
+
},
|
|
81
|
+
'white-destructive': {
|
|
82
|
+
filled: 'border-border bg-white text-destructive',
|
|
83
|
+
stroke: 'border-white bg-transparent text-destructive',
|
|
84
|
+
},
|
|
85
|
+
'surface-destructive': {
|
|
86
|
+
filled: 'border-destructive/30 bg-background text-destructive',
|
|
87
|
+
stroke: 'border-destructive/50 bg-transparent text-destructive',
|
|
88
|
+
},
|
|
89
|
+
primary: {
|
|
90
|
+
filled: 'border-transparent bg-primary/10 text-primary',
|
|
91
|
+
stroke: 'border-primary/50 bg-transparent text-primary',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const sizeClasses: Record<BadgeSize, string> = {
|
|
96
|
+
xs: 'gap-1 px-1.5 py-0 text-[0.625rem] leading-4',
|
|
97
|
+
sm: 'gap-1 px-2 py-0.5 text-xs leading-4',
|
|
98
|
+
md: 'gap-1.5 px-2.5 py-0.5 text-sm leading-5',
|
|
99
|
+
lg: 'gap-1.5 px-3 py-1 text-base leading-5',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const iconSizes: Record<BadgeSize, number> = {
|
|
103
|
+
xs: 10,
|
|
104
|
+
sm: 12,
|
|
105
|
+
md: 14,
|
|
106
|
+
lg: 16,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function isImageSource(value: string): boolean {
|
|
110
|
+
return (
|
|
111
|
+
/^(?:https?:|data:image\/|\/|\.\.?\/)/i.test(value) ||
|
|
112
|
+
/\.(?:avif|gif|jpe?g|png|svg|webp)(?:[?#].*)?$/i.test(value)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function BadgeIcon({ icon, size }: { icon: ReactNode; size: number }) {
|
|
117
|
+
const content =
|
|
118
|
+
typeof icon === 'string' && isImageSource(icon) ? (
|
|
119
|
+
<img src={icon} alt="" width={size} height={size} />
|
|
120
|
+
) : (
|
|
121
|
+
resolveIcon(icon, size)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (!content) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<span
|
|
130
|
+
className="inline-flex shrink-0 items-center justify-center [&>img]:size-full [&>svg]:size-full"
|
|
131
|
+
style={{ width: size, height: size }}
|
|
132
|
+
aria-hidden="true"
|
|
133
|
+
>
|
|
134
|
+
{content}
|
|
135
|
+
</span>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function Badge({
|
|
140
|
+
color,
|
|
141
|
+
size = 'md',
|
|
142
|
+
shape = 'rounded',
|
|
143
|
+
icon,
|
|
144
|
+
stroke = false,
|
|
145
|
+
disabled = false,
|
|
146
|
+
className,
|
|
147
|
+
tone,
|
|
148
|
+
children,
|
|
149
|
+
}: BadgeProps) {
|
|
150
|
+
const resolvedColor = color || (tone === 'primary' ? 'primary' : 'gray');
|
|
151
|
+
const palette = colorClasses[resolvedColor];
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<BadgePrimitive
|
|
155
|
+
variant="secondary"
|
|
156
|
+
className={cn(
|
|
157
|
+
'h-auto justify-start overflow-visible whitespace-normal border align-middle transition-none',
|
|
158
|
+
sizeClasses[size],
|
|
159
|
+
stroke ? palette.stroke : palette.filled,
|
|
160
|
+
shape === 'pill' ? 'rounded-full' : 'rounded-md',
|
|
161
|
+
disabled && 'cursor-not-allowed opacity-50',
|
|
162
|
+
className,
|
|
163
|
+
)}
|
|
164
|
+
aria-disabled={disabled || undefined}
|
|
165
|
+
data-disabled={disabled ? '' : undefined}
|
|
166
|
+
>
|
|
167
|
+
{icon ? <BadgeIcon icon={icon} size={iconSizes[size]} /> : null}
|
|
168
|
+
{children}
|
|
169
|
+
</BadgePrimitive>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Check as CheckIcon,
|
|
4
|
+
CircleAlert,
|
|
5
|
+
InfoIcon,
|
|
6
|
+
Lightbulb,
|
|
7
|
+
TriangleAlert,
|
|
8
|
+
} from '@/components/icons';
|
|
9
|
+
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
|
10
|
+
import { styles } from './styles';
|
|
11
|
+
import { resolveIcon } from './utils';
|
|
12
|
+
|
|
13
|
+
export interface CalloutProps {
|
|
14
|
+
title?: ReactNode;
|
|
15
|
+
icon?: ReactNode | string;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
variant?: 'note' | 'tip' | 'warning' | 'info' | 'check' | 'danger';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CALLOUT_ICONS = {
|
|
21
|
+
note: <InfoIcon size={14} />,
|
|
22
|
+
info: <InfoIcon size={14} />,
|
|
23
|
+
warning: <TriangleAlert size={14} />,
|
|
24
|
+
tip: <Lightbulb size={14} />,
|
|
25
|
+
check: <CheckIcon size={14} />,
|
|
26
|
+
danger: <CircleAlert size={14} />,
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export function Callout({ title, icon, children, variant = 'note' }: CalloutProps) {
|
|
30
|
+
return (
|
|
31
|
+
<Alert role="note" className={`${styles.callout} ${styles[variant]}`}>
|
|
32
|
+
<span className={styles.calloutIcon} aria-hidden={true}>
|
|
33
|
+
{resolveIcon(icon) || CALLOUT_ICONS[variant]}
|
|
34
|
+
</span>
|
|
35
|
+
<div className={styles.calloutBody}>
|
|
36
|
+
{title ? <AlertTitle className={styles.calloutTitle}>{title}</AlertTitle> : null}
|
|
37
|
+
<AlertDescription className="text-sm text-inherit leading-6">{children}</AlertDescription>
|
|
38
|
+
</div>
|
|
39
|
+
</Alert>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function Note({ children }: { children?: ReactNode }) {
|
|
44
|
+
return <Callout variant="note">{children}</Callout>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function Tip({ children }: { children?: ReactNode }) {
|
|
48
|
+
return <Callout variant="tip">{children}</Callout>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function Warning({ children }: { children?: ReactNode }) {
|
|
52
|
+
return <Callout variant="warning">{children}</Callout>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function Info({ children }: { children?: ReactNode }) {
|
|
56
|
+
return <Callout variant="info">{children}</Callout>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function Check({ children }: { children?: ReactNode }) {
|
|
60
|
+
return <Callout variant="check">{children}</Callout>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function Danger({ children }: { children?: ReactNode }) {
|
|
64
|
+
return <Callout variant="danger">{children}</Callout>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function WarningBanner({ children }: { children?: ReactNode }) {
|
|
68
|
+
return (
|
|
69
|
+
<Callout variant="warning" icon={<CircleAlert size={14} />}>
|
|
70
|
+
{children}
|
|
71
|
+
</Callout>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Link } from 'react-router';
|
|
3
|
+
import {
|
|
4
|
+
Check as CheckIcon,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
CircleAlert,
|
|
7
|
+
InfoIcon,
|
|
8
|
+
Lightbulb,
|
|
9
|
+
TriangleAlert,
|
|
10
|
+
} from '@/components/icons';
|
|
11
|
+
import { CardContent, Card as CardPrimitive } from '@/components/ui/card';
|
|
12
|
+
import { styles } from './styles';
|
|
13
|
+
import { gridColsClass, resolveIcon } from './utils';
|
|
14
|
+
|
|
15
|
+
export interface CardProps {
|
|
16
|
+
title?: ReactNode;
|
|
17
|
+
href?: string;
|
|
18
|
+
icon?: ReactNode | string;
|
|
19
|
+
color?: string;
|
|
20
|
+
img?: string;
|
|
21
|
+
cta?: string;
|
|
22
|
+
horizontal?: boolean;
|
|
23
|
+
arrow?: boolean;
|
|
24
|
+
type?: CardType;
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type CardType = 'note' | 'info' | 'warning' | 'tip' | 'check' | 'danger';
|
|
29
|
+
|
|
30
|
+
const CARD_TYPE_ICONS: Record<CardType, ReactNode> = {
|
|
31
|
+
note: <InfoIcon size={16} />,
|
|
32
|
+
info: <InfoIcon size={16} />,
|
|
33
|
+
warning: <TriangleAlert size={16} />,
|
|
34
|
+
tip: <Lightbulb size={16} />,
|
|
35
|
+
check: <CheckIcon size={16} />,
|
|
36
|
+
danger: <CircleAlert size={16} />,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const CARD_TYPE_HOVER: Record<CardType, string> = {
|
|
40
|
+
note: 'hover:border-border',
|
|
41
|
+
info: 'hover:border-blue-300 dark:hover:border-blue-400/40',
|
|
42
|
+
warning: 'hover:border-amber-300 dark:hover:border-amber-400/40',
|
|
43
|
+
tip: 'hover:border-green-300 dark:hover:border-green-400/40',
|
|
44
|
+
check: 'hover:border-emerald-300 dark:hover:border-emerald-400/40',
|
|
45
|
+
danger: 'hover:border-destructive/40',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function DocsCardContent({
|
|
49
|
+
title,
|
|
50
|
+
icon,
|
|
51
|
+
color,
|
|
52
|
+
img,
|
|
53
|
+
cta,
|
|
54
|
+
arrow,
|
|
55
|
+
type,
|
|
56
|
+
children,
|
|
57
|
+
}: Omit<CardProps, 'href'>) {
|
|
58
|
+
const resolvedIcon = resolveIcon(icon, 16) || (type ? CARD_TYPE_ICONS[type] : null);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<CardContent className="p-0">
|
|
62
|
+
{img ? <img src={img} alt="" className={styles.cardImage} /> : null}
|
|
63
|
+
<div className={styles.cardInner} data-slot="card-inner">
|
|
64
|
+
<div className={styles.cardMain} data-slot="card-main">
|
|
65
|
+
<div className={styles.cardHeader}>
|
|
66
|
+
{resolvedIcon ? (
|
|
67
|
+
<span
|
|
68
|
+
className={`${styles.cardIcon} ${type ? 'text-inherit' : 'text-primary'}`}
|
|
69
|
+
style={color ? { color } : undefined}
|
|
70
|
+
>
|
|
71
|
+
{resolvedIcon}
|
|
72
|
+
</span>
|
|
73
|
+
) : null}
|
|
74
|
+
{title ? (
|
|
75
|
+
<div className={styles.cardTitle} data-slot="card-title">
|
|
76
|
+
{title}
|
|
77
|
+
</div>
|
|
78
|
+
) : null}
|
|
79
|
+
</div>
|
|
80
|
+
{children ? (
|
|
81
|
+
<div className={styles.cardBody} data-slot="card-body">
|
|
82
|
+
{children}
|
|
83
|
+
</div>
|
|
84
|
+
) : null}
|
|
85
|
+
</div>
|
|
86
|
+
{cta || arrow ? (
|
|
87
|
+
<div className={styles.cardCta} data-slot="card-cta">
|
|
88
|
+
{cta ? <span>{cta}</span> : null}
|
|
89
|
+
{arrow ? (
|
|
90
|
+
<ChevronRight size={14} className={styles.cardArrow} data-slot="card-arrow" />
|
|
91
|
+
) : null}
|
|
92
|
+
</div>
|
|
93
|
+
) : null}
|
|
94
|
+
</div>
|
|
95
|
+
</CardContent>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function Card({
|
|
100
|
+
title,
|
|
101
|
+
href,
|
|
102
|
+
icon,
|
|
103
|
+
color,
|
|
104
|
+
img,
|
|
105
|
+
cta,
|
|
106
|
+
horizontal,
|
|
107
|
+
arrow,
|
|
108
|
+
type,
|
|
109
|
+
children,
|
|
110
|
+
}: CardProps) {
|
|
111
|
+
const external = typeof href === 'string' && /^https?:\/\//i.test(href);
|
|
112
|
+
const showArrow = arrow ?? external;
|
|
113
|
+
const className = `${styles.card} ${type ? `${styles.cardTyped} ${styles[type]} ${CARD_TYPE_HOVER[type]}` : ''} ${horizontal ? styles.cardHorizontal : ''} ${img ? styles.cardImageLayout : ''}`;
|
|
114
|
+
const content = (
|
|
115
|
+
<CardPrimitive className={className}>
|
|
116
|
+
<DocsCardContent
|
|
117
|
+
title={title}
|
|
118
|
+
icon={icon}
|
|
119
|
+
color={color}
|
|
120
|
+
img={img}
|
|
121
|
+
cta={cta}
|
|
122
|
+
arrow={showArrow}
|
|
123
|
+
type={type}
|
|
124
|
+
>
|
|
125
|
+
{children}
|
|
126
|
+
</DocsCardContent>
|
|
127
|
+
</CardPrimitive>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!href) {
|
|
131
|
+
return content;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const linkClassName = 'block h-full no-underline hover:no-underline active:no-underline';
|
|
135
|
+
|
|
136
|
+
if (external) {
|
|
137
|
+
return (
|
|
138
|
+
<a href={href} className={linkClassName} target="_blank" rel="noreferrer">
|
|
139
|
+
{content}
|
|
140
|
+
</a>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Link to={href} className={linkClassName}>
|
|
146
|
+
{content}
|
|
147
|
+
</Link>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface CardGroupProps {
|
|
152
|
+
children?: ReactNode;
|
|
153
|
+
cols?: 1 | 2 | 3 | 4;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function CardGroup({ children, cols = 2 }: CardGroupProps) {
|
|
157
|
+
return <div className={`${styles.grid} ${styles[gridColsClass(cols)]}`}>{children}</div>;
|
|
158
|
+
}
|