andesine 0.1.0 → 0.4.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 +16 -0
- package/README.md +27 -8
- package/dist/cli.js +1611 -121
- package/dist/cli.js.map +1 -1
- package/dist/config.schema.json +20 -3
- package/dist/templates/pages/.env.example +5 -0
- package/dist/templates/pages/AGENTS.md +79 -0
- package/dist/templates/pages/LICENSE +21 -0
- package/dist/templates/pages/README.md +72 -0
- package/dist/templates/pages/andesine.config.ts +39 -0
- package/dist/templates/pages/astro.config.ts +23 -0
- package/dist/templates/pages/gitignore +13 -0
- package/dist/templates/pages/package.template.json +45 -0
- package/dist/templates/pages/src/api/openapi.yaml +246 -0
- package/dist/templates/pages/src/assets/andesine.svg +1 -0
- package/dist/templates/pages/src/assets/andesine.ts +6 -0
- package/dist/templates/pages/src/assets/favicon.svg +13 -0
- package/dist/templates/pages/src/assets/social-dots.svg +14 -0
- package/dist/templates/pages/src/components/content/callout.astro +26 -0
- package/dist/templates/pages/src/components/content/card-grid.astro +1 -0
- package/dist/templates/pages/src/components/content/card.astro +31 -0
- package/dist/templates/pages/src/components/content/code-block.astro +34 -0
- package/dist/templates/pages/src/components/content/code-group.astro +13 -0
- package/dist/templates/pages/src/components/content/copy-button.tsx +53 -0
- package/dist/templates/pages/src/components/content/disclosure.astro +26 -0
- package/dist/templates/pages/src/components/content/figure.astro +10 -0
- package/dist/templates/pages/src/components/content/file-tree.astro +47 -0
- package/dist/templates/pages/src/components/content/index.ts +30 -0
- package/dist/templates/pages/src/components/content/steps.astro +16 -0
- package/dist/templates/pages/src/components/content/tab.astro +7 -0
- package/dist/templates/pages/src/components/content/tabs.astro +15 -0
- package/dist/templates/pages/src/components/content/tabs.tsx +99 -0
- package/dist/templates/pages/src/components/effects/page-scroll-shadow.tsx +27 -0
- package/dist/templates/pages/src/components/effects/page-scrollbar.tsx +35 -0
- package/dist/templates/pages/src/components/layout/call-to-action.tsx +28 -0
- package/dist/templates/pages/src/components/layout/footer.astro +44 -0
- package/dist/templates/pages/src/components/layout/group-label.tsx +34 -0
- package/dist/templates/pages/src/components/layout/header.astro +53 -0
- package/dist/templates/pages/src/components/layout/mobile-navigation.tsx +51 -0
- package/dist/templates/pages/src/components/layout/navigation-links.tsx +57 -0
- package/dist/templates/pages/src/components/layout/navigation-tree.tsx +138 -0
- package/dist/templates/pages/src/components/layout/outline.tsx +162 -0
- package/dist/templates/pages/src/components/layout/page-actions.tsx +87 -0
- package/dist/templates/pages/src/components/layout/powered-by.tsx +29 -0
- package/dist/templates/pages/src/components/layout/section-tabs.tsx +83 -0
- package/dist/templates/pages/src/components/layout/sidebar.tsx +65 -0
- package/dist/templates/pages/src/components/primitives/button.tsx +121 -0
- package/dist/templates/pages/src/components/primitives/card.tsx +25 -0
- package/dist/templates/pages/src/components/primitives/dialog.tsx +47 -0
- package/dist/templates/pages/src/components/primitives/input.tsx +22 -0
- package/dist/templates/pages/src/components/primitives/logo.tsx +41 -0
- package/dist/templates/pages/src/components/primitives/menu.tsx +121 -0
- package/dist/templates/pages/src/components/primitives/scroll-area.tsx +103 -0
- package/dist/templates/pages/src/components/primitives/shortcut.tsx +60 -0
- package/dist/templates/pages/src/components/primitives/tooltip.tsx +39 -0
- package/dist/templates/pages/src/components/reference/facts.astro +27 -0
- package/dist/templates/pages/src/components/reference/fields.astro +102 -0
- package/dist/templates/pages/src/components/reference/method-badge.astro +18 -0
- package/dist/templates/pages/src/components/reference/methods.ts +17 -0
- package/dist/templates/pages/src/components/reference/operation.astro +252 -0
- package/dist/templates/pages/src/components/reference/styles.ts +14 -0
- package/dist/templates/pages/src/components/search/highlighted-text.tsx +22 -0
- package/dist/templates/pages/src/components/search/search-answer.tsx +123 -0
- package/dist/templates/pages/src/components/search/search-palette.tsx +174 -0
- package/dist/templates/pages/src/components/search/search-results.tsx +229 -0
- package/dist/templates/pages/src/components/social/card.ts +77 -0
- package/dist/templates/pages/src/content/docs/customization/index.mdx +11 -0
- package/dist/templates/pages/src/content/docs/customization/styles.mdx +18 -0
- package/dist/templates/pages/src/content/docs/getting-started/api-reference.mdx +100 -0
- package/dist/templates/pages/src/content/docs/getting-started/components.mdx +156 -0
- package/dist/templates/pages/src/content/docs/getting-started/configuration.mdx +194 -0
- package/dist/templates/pages/src/content/docs/getting-started/deployment.mdx +142 -0
- package/dist/templates/pages/src/content/docs/getting-started/index.mdx +22 -0
- package/dist/templates/pages/src/content/docs/getting-started/writing-pages.mdx +35 -0
- package/dist/templates/pages/src/content/docs/index.mdx +21 -0
- package/dist/templates/pages/src/content.config.ts +1 -0
- package/dist/templates/pages/src/layouts/page.astro +195 -0
- package/dist/templates/pages/src/styles/global.css +95 -0
- package/dist/templates/pages/tsconfig.json +10 -0
- package/dist/templates/pages/uno.config.ts +168 -0
- package/package.json +2 -2
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PageContext, SectionContext } from "@andesine/pages";
|
|
2
|
+
import { MobileNavigation as Menu } from "@andesine/ui/solid";
|
|
3
|
+
import { type Component, Show } from "solid-js";
|
|
4
|
+
import { Button } from "../primitives/button";
|
|
5
|
+
import { Logo } from "../primitives/logo";
|
|
6
|
+
import { NavigationLinks } from "./navigation-links";
|
|
7
|
+
import { NavigationTree } from "./navigation-tree";
|
|
8
|
+
import { PoweredBy } from "./powered-by";
|
|
9
|
+
|
|
10
|
+
interface MobileNavigationProps {
|
|
11
|
+
page: Pick<PageContext, "site" | "links" | "navigation">;
|
|
12
|
+
/** Sections to show above the navigation, when the header has no tabs. */
|
|
13
|
+
sections: SectionContext[];
|
|
14
|
+
storageKey: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const MobileNavigation: Component<MobileNavigationProps> = (props) => (
|
|
18
|
+
<Menu.Root
|
|
19
|
+
mediaQuery="(max-width: 767.9px)"
|
|
20
|
+
navigationTarget={() => document.getElementById("main-content")}
|
|
21
|
+
class="group/menu md:hidden"
|
|
22
|
+
>
|
|
23
|
+
{/* A summary cannot hold a button, so it shows the button's look as a badge. */}
|
|
24
|
+
<Menu.Trigger
|
|
25
|
+
aria-label="Navigation"
|
|
26
|
+
class="fixed bottom-[max(1rem,env(safe-area-inset-bottom))] right-4 z-40 cursor-pointer list-none rounded-lg focus-visible:(outline outline-2 outline-tertiary) [&::-webkit-details-marker]:hidden"
|
|
27
|
+
>
|
|
28
|
+
<Button badge color="contrast" variant="outlined" text="softer" class="p-1.5">
|
|
29
|
+
<span aria-hidden="true" class="i-lucide:menu h-5 w-5 group-open/menu:hidden" />
|
|
30
|
+
<span aria-hidden="true" class="i-lucide:x hidden h-5 w-5 group-open/menu:block" />
|
|
31
|
+
</Button>
|
|
32
|
+
</Menu.Trigger>
|
|
33
|
+
{/* The last row lines up with the menu button, so the button never covers menu items. */}
|
|
34
|
+
<Menu.Content class="fixed inset-0 z-30 flex flex-col gap-6 overflow-y-auto bg-gray-50 px-4 pb-[max(1rem,env(safe-area-inset-bottom))] pt-4">
|
|
35
|
+
<a href={props.page.site.href} class="text-gray-900">
|
|
36
|
+
<Logo site={props.page.site} />
|
|
37
|
+
</a>
|
|
38
|
+
<Show when={props.sections.length || props.page.links.length}>
|
|
39
|
+
<NavigationLinks sections={props.sections} links={props.page.links} />
|
|
40
|
+
</Show>
|
|
41
|
+
<NavigationTree
|
|
42
|
+
items={props.page.navigation}
|
|
43
|
+
storageKey={props.storageKey}
|
|
44
|
+
defaultDepth={2}
|
|
45
|
+
/>
|
|
46
|
+
<PoweredBy class="mt-auto h-8 shrink-0" />
|
|
47
|
+
</Menu.Content>
|
|
48
|
+
</Menu.Root>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
export { MobileNavigation };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { IconLinkConfig, SectionContext } from "@andesine/pages";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { type Component, For } from "solid-js";
|
|
4
|
+
|
|
5
|
+
interface NavigationLinksProps {
|
|
6
|
+
/** Sections, when they show in the navigation in place of header tabs. */
|
|
7
|
+
sections?: SectionContext[];
|
|
8
|
+
links: IconLinkConfig[];
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface NavigationLinkProps {
|
|
13
|
+
href: string;
|
|
14
|
+
label: string;
|
|
15
|
+
icon: string;
|
|
16
|
+
current?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const NavigationLink: Component<NavigationLinkProps> = (props) => (
|
|
20
|
+
<a
|
|
21
|
+
href={props.href}
|
|
22
|
+
aria-current={props.current ? "location" : undefined}
|
|
23
|
+
class="group flex items-center gap-2 font-medium text-gray-700"
|
|
24
|
+
>
|
|
25
|
+
<span
|
|
26
|
+
class={clsx(
|
|
27
|
+
"flex shrink-0 items-center justify-center rounded-lg p-1 shadow-md outline outline-1 -outline-offset-1 transition-[background-position,background-color] duration-200 ease-out text-gray-500",
|
|
28
|
+
props.current
|
|
29
|
+
? "bg-gradient-to-tr bg-[length:125%_auto] text-white outline-tertiary group-hover:bg-right group-focus-visible:bg-right"
|
|
30
|
+
: "bg-white shadow-gray-200 outline-gray-200 group-hover:bg-gray-100 group-focus-visible:bg-gray-100"
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
<span aria-hidden="true" class={clsx("h-5 w-5", props.icon)} />
|
|
34
|
+
</span>
|
|
35
|
+
{props.label}
|
|
36
|
+
</a>
|
|
37
|
+
);
|
|
38
|
+
/** Links at the top of the navigation: the sections, then the configured links. */
|
|
39
|
+
const NavigationLinks: Component<NavigationLinksProps> = (props) => (
|
|
40
|
+
<nav aria-label="Links" class={clsx(":base: flex flex-col gap-2", props.class)}>
|
|
41
|
+
<For each={props.sections}>
|
|
42
|
+
{(section) => (
|
|
43
|
+
<NavigationLink
|
|
44
|
+
href={section.href}
|
|
45
|
+
label={section.label}
|
|
46
|
+
icon={section.icon ?? "i-lucide:book-open"}
|
|
47
|
+
current={section.current}
|
|
48
|
+
/>
|
|
49
|
+
)}
|
|
50
|
+
</For>
|
|
51
|
+
<For each={props.links}>
|
|
52
|
+
{(link) => <NavigationLink href={link.href} label={link.label} icon={link.icon} />}
|
|
53
|
+
</For>
|
|
54
|
+
</nav>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export { NavigationLinks };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { NavigationItem } from "@andesine/pages";
|
|
2
|
+
import { NavigationTree as Tree } from "@andesine/ui/solid";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { type Component, For, Show } from "solid-js";
|
|
5
|
+
import { getMethodColor, getMethodLabel } from "../reference/methods";
|
|
6
|
+
|
|
7
|
+
interface NavigationTreeProps {
|
|
8
|
+
items: NavigationItem[];
|
|
9
|
+
storageKey: string;
|
|
10
|
+
/** Groups up to this depth start expanded. */
|
|
11
|
+
defaultDepth?: number;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface BranchProps {
|
|
16
|
+
items: NavigationItem[];
|
|
17
|
+
depth: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ItemLinkProps {
|
|
21
|
+
item: NavigationItem;
|
|
22
|
+
depth: number;
|
|
23
|
+
group?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Each nested list moves right by `ml-3.5` and `pl-px`.
|
|
27
|
+
const indent = (depth: number): string => `calc(${depth} * (0.875rem + 1px))`;
|
|
28
|
+
const rowClass =
|
|
29
|
+
"relative isolate flex h-7.5 min-w-0 items-center rounded-lg px-2 transition duration-200 ease-out";
|
|
30
|
+
const currentTextClass = "bg-gradient-to-tr bg-[length:125%_auto] bg-clip-text text-transparent";
|
|
31
|
+
const hoverClass =
|
|
32
|
+
"@hover:(bg-gradient-to-r from-gray-500/10 to-transparent) focus-visible:(bg-gradient-to-r from-gray-500/10 to-transparent)";
|
|
33
|
+
|
|
34
|
+
const ItemLink: Component<ItemLinkProps> = (props) => (
|
|
35
|
+
<Tree.Link
|
|
36
|
+
class={clsx(
|
|
37
|
+
rowClass,
|
|
38
|
+
"flex-1",
|
|
39
|
+
props.depth > 0 && "rounded-l-none",
|
|
40
|
+
props.group ? "text-sm font-semibold text-gray-700" : "text-base text-gray-500",
|
|
41
|
+
!props.item.current && clsx(hoverClass, "@hover:text-gray-700 focus-visible:text-gray-700")
|
|
42
|
+
)}
|
|
43
|
+
style={{ "--indent": indent(props.depth) }}
|
|
44
|
+
title={props.item.label}
|
|
45
|
+
>
|
|
46
|
+
<Show when={props.item.current}>
|
|
47
|
+
<span
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
class="pointer-events-none absolute inset-y-0 right-0 -z-1 rounded-lg bg-gradient-to-r from-secondary/10 via-primary/10 to-transparent"
|
|
50
|
+
style={{ left: `calc(-1 * ${indent(props.depth)})` }}
|
|
51
|
+
/>
|
|
52
|
+
<For each={Array.from({ length: props.depth }, (_, index) => index)}>
|
|
53
|
+
{(index) => (
|
|
54
|
+
<span
|
|
55
|
+
aria-hidden="true"
|
|
56
|
+
class="pointer-events-none absolute top-1/2 z-1 h-4 w-0.5 -translate-y-1/2 rounded-full bg-gradient-to-b"
|
|
57
|
+
style={{ left: `calc(-1.5px - ${indent(index)})` }}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
</For>
|
|
61
|
+
</Show>
|
|
62
|
+
<Show when={props.item.method}>
|
|
63
|
+
{(method) => (
|
|
64
|
+
<span
|
|
65
|
+
class={clsx(
|
|
66
|
+
"mr-2 w-9 shrink-0 font-mono text-[0.625rem] font-bold",
|
|
67
|
+
props.item.current ? currentTextClass : getMethodColor(method())
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{getMethodLabel(method())}
|
|
71
|
+
</span>
|
|
72
|
+
)}
|
|
73
|
+
</Show>
|
|
74
|
+
<span class={clsx("truncate", props.item.current && currentTextClass)}>{props.item.label}</span>
|
|
75
|
+
</Tree.Link>
|
|
76
|
+
);
|
|
77
|
+
const Chevron: Component = () => (
|
|
78
|
+
<span
|
|
79
|
+
aria-hidden="true"
|
|
80
|
+
class="i-lucide:chevron-right h-3.5 w-3.5 shrink-0 text-gray-400 transition-transform duration-200 [[data-state=open]>&]:rotate-90"
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
const Branch: Component<BranchProps> = (props) => (
|
|
84
|
+
<Tree.List class="flex flex-col gap-0.5">
|
|
85
|
+
<For each={props.items}>
|
|
86
|
+
{(item) => (
|
|
87
|
+
<Tree.Item item={item}>
|
|
88
|
+
<Show when={item.children.length} fallback={<ItemLink item={item} depth={props.depth} />}>
|
|
89
|
+
<div class="flex items-center">
|
|
90
|
+
<Show
|
|
91
|
+
when={item.href}
|
|
92
|
+
fallback={
|
|
93
|
+
<Tree.GroupTrigger
|
|
94
|
+
class={clsx(
|
|
95
|
+
rowClass,
|
|
96
|
+
hoverClass,
|
|
97
|
+
"w-full cursor-pointer gap-2 text-sm font-semibold text-gray-700",
|
|
98
|
+
props.depth > 0 && "rounded-l-none"
|
|
99
|
+
)}
|
|
100
|
+
>
|
|
101
|
+
<span class="flex-1 truncate text-start">{item.label}</span>
|
|
102
|
+
<Chevron />
|
|
103
|
+
</Tree.GroupTrigger>
|
|
104
|
+
}
|
|
105
|
+
>
|
|
106
|
+
<ItemLink item={item} depth={props.depth} group />
|
|
107
|
+
<Tree.GroupTrigger
|
|
108
|
+
aria-label={`${item.label} pages`}
|
|
109
|
+
class={clsx(
|
|
110
|
+
"flex h-7.5 w-7.5 shrink-0 cursor-pointer items-center justify-center rounded-lg transition duration-200 ease-out",
|
|
111
|
+
"@hover:bg-gray-500/10 focus-visible:bg-gray-500/10"
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
<Chevron />
|
|
115
|
+
</Tree.GroupTrigger>
|
|
116
|
+
</Show>
|
|
117
|
+
</div>
|
|
118
|
+
<Tree.GroupContent class="relative ml-3.5 mt-0.5 pl-px before:(absolute inset-y-0 left-0 w-px rounded-full bg-gray-200 content-[''])">
|
|
119
|
+
<Branch items={item.children} depth={props.depth + 1} />
|
|
120
|
+
</Tree.GroupContent>
|
|
121
|
+
</Show>
|
|
122
|
+
</Tree.Item>
|
|
123
|
+
)}
|
|
124
|
+
</For>
|
|
125
|
+
</Tree.List>
|
|
126
|
+
);
|
|
127
|
+
const NavigationTree: Component<NavigationTreeProps> = (props) => (
|
|
128
|
+
<Tree.Root
|
|
129
|
+
items={props.items}
|
|
130
|
+
storageKey={props.storageKey}
|
|
131
|
+
defaultDepth={props.defaultDepth}
|
|
132
|
+
class={props.class}
|
|
133
|
+
>
|
|
134
|
+
<Branch items={props.items} depth={0} />
|
|
135
|
+
</Tree.Root>
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
export { NavigationTree };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { HeadingContext } from "@andesine/pages";
|
|
2
|
+
import {
|
|
3
|
+
TableOfContents,
|
|
4
|
+
type TableOfContentsGuideRange,
|
|
5
|
+
createTableOfContentsGuide,
|
|
6
|
+
useTableOfContents
|
|
7
|
+
} from "@andesine/ui/solid";
|
|
8
|
+
import clsx from "clsx";
|
|
9
|
+
import { type Component, For, Show, createUniqueId } from "solid-js";
|
|
10
|
+
import { ScrollArea } from "../primitives/scroll-area";
|
|
11
|
+
|
|
12
|
+
interface OutlineProps {
|
|
13
|
+
headings: HeadingContext[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface OutlineListProps extends OutlineProps {
|
|
17
|
+
viewport(): HTMLElement | undefined;
|
|
18
|
+
level(heading: HeadingContext): number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const OutlineList: Component<OutlineListProps> = (props) => {
|
|
22
|
+
const toc = useTableOfContents();
|
|
23
|
+
const gradientID = createUniqueId();
|
|
24
|
+
const highlighted = (): string[] => {
|
|
25
|
+
const visible = toc.visibleIDs();
|
|
26
|
+
|
|
27
|
+
return visible.length ? visible : [toc.activeID() ?? ""];
|
|
28
|
+
};
|
|
29
|
+
const levels = new Map(props.headings.map((heading) => [heading.id, props.level(heading)]));
|
|
30
|
+
const guide = createTableOfContentsGuide({
|
|
31
|
+
ids: () => props.headings.map((heading) => heading.id),
|
|
32
|
+
highlightedIDs: highlighted,
|
|
33
|
+
position: (id) => 8.5 + (levels.get(id) ?? 0) * 8,
|
|
34
|
+
viewport: props.viewport
|
|
35
|
+
});
|
|
36
|
+
const range = (): TableOfContentsGuideRange | undefined => guide.stroke()?.range;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div ref={guide.trackRef} class="relative">
|
|
40
|
+
<Show when={guide.stroke()?.backdrop}>
|
|
41
|
+
{(backdrop) => (
|
|
42
|
+
<svg aria-hidden="true" class="pointer-events-none absolute inset-0 h-full w-full">
|
|
43
|
+
<defs>
|
|
44
|
+
<linearGradient id={`${gradientID}-backdrop`}>
|
|
45
|
+
<stop
|
|
46
|
+
offset="0%"
|
|
47
|
+
style={{ "stop-color": "var(--color-secondary)", "stop-opacity": 0.1 }}
|
|
48
|
+
/>
|
|
49
|
+
<stop
|
|
50
|
+
offset="50%"
|
|
51
|
+
style={{ "stop-color": "var(--color-primary)", "stop-opacity": 0.05 }}
|
|
52
|
+
/>
|
|
53
|
+
<stop
|
|
54
|
+
offset="100%"
|
|
55
|
+
style={{ "stop-color": "var(--color-secondary)", "stop-opacity": 0 }}
|
|
56
|
+
/>
|
|
57
|
+
</linearGradient>
|
|
58
|
+
</defs>
|
|
59
|
+
{/* The attribute is for Safari; browsers with the CSS `d` property animate it. */}
|
|
60
|
+
<path
|
|
61
|
+
d={backdrop()}
|
|
62
|
+
fill={`url(#${gradientID}-backdrop)`}
|
|
63
|
+
class="[transition:d_200ms_ease-out]"
|
|
64
|
+
style={{ d: `path("${backdrop()}")` }}
|
|
65
|
+
/>
|
|
66
|
+
</svg>
|
|
67
|
+
)}
|
|
68
|
+
</Show>
|
|
69
|
+
<TableOfContents.List
|
|
70
|
+
class={clsx(
|
|
71
|
+
"relative flex flex-col text-sm leading-5",
|
|
72
|
+
// Without scripts, a plain line stands in for the guide.
|
|
73
|
+
!guide.stroke() &&
|
|
74
|
+
"[@media(scripting:none)]:border-l [@media(scripting:none)]:border-gray-200"
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
<For each={props.headings}>
|
|
78
|
+
{(heading) => (
|
|
79
|
+
<TableOfContents.Item>
|
|
80
|
+
<TableOfContents.Link
|
|
81
|
+
ref={(element: HTMLAnchorElement) => guide.linkRef(heading.id, element)}
|
|
82
|
+
headingID={heading.id}
|
|
83
|
+
class={clsx(
|
|
84
|
+
"block py-1 pr-2 transition duration-200 ease-out [overflow-wrap:anywhere]",
|
|
85
|
+
highlighted().includes(heading.id)
|
|
86
|
+
? "text-gray-700"
|
|
87
|
+
: "text-gray-500 @hover:text-gray-800 focus-visible:text-gray-800"
|
|
88
|
+
)}
|
|
89
|
+
style={{ "padding-inline-start": `${20 + (levels.get(heading.id) ?? 0) * 12}px` }}
|
|
90
|
+
>
|
|
91
|
+
{/* The guide needs the rendered layout, so the text shows as skeleton lines until then. */}
|
|
92
|
+
<span class={clsx(!guide.stroke() && "outline-skeleton")}>{heading.text}</span>
|
|
93
|
+
</TableOfContents.Link>
|
|
94
|
+
</TableOfContents.Item>
|
|
95
|
+
)}
|
|
96
|
+
</For>
|
|
97
|
+
</TableOfContents.List>
|
|
98
|
+
<svg
|
|
99
|
+
aria-hidden="true"
|
|
100
|
+
class={clsx("pointer-events-none absolute left-0 top-0 h-full w-8 overflow-visible")}
|
|
101
|
+
fill="none"
|
|
102
|
+
>
|
|
103
|
+
<path ref={guide.pathRef} class="stroke-gray-200" stroke-width="1" />
|
|
104
|
+
<Show when={guide.stroke()}>
|
|
105
|
+
{(stroke) => (
|
|
106
|
+
<>
|
|
107
|
+
<defs>
|
|
108
|
+
<linearGradient
|
|
109
|
+
id={gradientID}
|
|
110
|
+
gradientUnits="userSpaceOnUse"
|
|
111
|
+
x1="0"
|
|
112
|
+
y1={(range()?.top ?? 0) + (range()?.height ?? 0)}
|
|
113
|
+
x2="16"
|
|
114
|
+
y2={range()?.top ?? 0}
|
|
115
|
+
>
|
|
116
|
+
<stop offset="0%" style={{ "stop-color": "var(--color-secondary)" }} />
|
|
117
|
+
<stop offset="50%" style={{ "stop-color": "var(--color-primary)" }} />
|
|
118
|
+
<stop offset="100%" style={{ "stop-color": "var(--color-secondary)" }} />
|
|
119
|
+
</linearGradient>
|
|
120
|
+
</defs>
|
|
121
|
+
<path
|
|
122
|
+
d={stroke().path}
|
|
123
|
+
stroke={`url(#${gradientID})`}
|
|
124
|
+
stroke-width="2"
|
|
125
|
+
stroke-linecap="round"
|
|
126
|
+
stroke-dasharray={`${stroke().length} ${stroke().total}`}
|
|
127
|
+
stroke-dashoffset={stroke().offset}
|
|
128
|
+
class="[transition:stroke-dashoffset_200ms_ease-out,stroke-dasharray_200ms_ease-out]"
|
|
129
|
+
/>
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
</Show>
|
|
133
|
+
</svg>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
const Outline: Component<OutlineProps> = (props) => {
|
|
138
|
+
const minDepth = Math.min(...props.headings.map((heading) => heading.depth));
|
|
139
|
+
|
|
140
|
+
let viewport: HTMLDivElement | undefined;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<TableOfContents.Root
|
|
144
|
+
headingIDs={props.headings.map((heading) => heading.id)}
|
|
145
|
+
class="flex min-h-0 flex-col gap-2"
|
|
146
|
+
>
|
|
147
|
+
<p class="flex shrink-0 items-center gap-1.5 text-sm font-medium text-gray-500">
|
|
148
|
+
<span aria-hidden="true" class="i-lucide:list h-4 w-4" />
|
|
149
|
+
On this page
|
|
150
|
+
</p>
|
|
151
|
+
<ScrollArea shadow viewportRef={(element) => (viewport = element)} contentClass="py-2">
|
|
152
|
+
<OutlineList
|
|
153
|
+
headings={props.headings}
|
|
154
|
+
viewport={() => viewport}
|
|
155
|
+
level={(heading) => Math.min(2, heading.depth - minDepth)}
|
|
156
|
+
/>
|
|
157
|
+
</ScrollArea>
|
|
158
|
+
</TableOfContents.Root>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export { Outline };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { OpenInLink, PageActionsContext } from "@andesine/pages";
|
|
2
|
+
import { type CopyResult, createPageActions } from "@andesine/ui/solid";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { type Component } from "solid-js";
|
|
5
|
+
import { Button, IconButton } from "../primitives/button";
|
|
6
|
+
import { Menu, type MenuEntry } from "../primitives/menu";
|
|
7
|
+
|
|
8
|
+
interface PageActionsProps {
|
|
9
|
+
actions: PageActionsContext;
|
|
10
|
+
/** URL of the page's Markdown alternative. */
|
|
11
|
+
markdown: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const serviceIcons: Record<OpenInLink["id"], string> = {
|
|
15
|
+
chatgpt: "i-simple-icons:openai",
|
|
16
|
+
claude: "i-simple-icons:claude",
|
|
17
|
+
perplexity: "i-simple-icons:perplexity"
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const getStatus = (result?: CopyResult): { icon: string; label: string } => {
|
|
21
|
+
if (!result) return { icon: "i-lucide:copy", label: "Copy page" };
|
|
22
|
+
if (!result.success) return { icon: "i-lucide:triangle-alert", label: "Copy failed" };
|
|
23
|
+
|
|
24
|
+
return { icon: "i-lucide:check", label: result.item === "link" ? "Link copied" : "Copied" };
|
|
25
|
+
};
|
|
26
|
+
/** Copies the page as Markdown, with a menu of other actions for the page. */
|
|
27
|
+
const PageActions: Component<PageActionsProps> = (props) => {
|
|
28
|
+
const actions = createPageActions({ markdown: props.markdown });
|
|
29
|
+
const status = (): ReturnType<typeof getStatus> => getStatus(actions.result());
|
|
30
|
+
const items = (): MenuEntry[] => [
|
|
31
|
+
{ value: "copy-link", label: "Copy link", icon: "i-lucide:link", onSelect: actions.copyLink },
|
|
32
|
+
{
|
|
33
|
+
value: "view-markdown",
|
|
34
|
+
label: "View as Markdown",
|
|
35
|
+
icon: "i-simple-icons:markdown",
|
|
36
|
+
href: props.markdown
|
|
37
|
+
},
|
|
38
|
+
...(props.actions.openIn.length
|
|
39
|
+
? ([
|
|
40
|
+
{ type: "separator" },
|
|
41
|
+
{ type: "header", label: "Open in" },
|
|
42
|
+
...props.actions.openIn.map((link) => ({
|
|
43
|
+
value: link.id,
|
|
44
|
+
label: link.label,
|
|
45
|
+
icon: serviceIcons[link.id],
|
|
46
|
+
href: link.href,
|
|
47
|
+
target: "_blank"
|
|
48
|
+
}))
|
|
49
|
+
] satisfies MenuEntry[])
|
|
50
|
+
: [])
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div class="flex shrink-0 gap-1">
|
|
55
|
+
<Button
|
|
56
|
+
color="contrast"
|
|
57
|
+
variant="outlined"
|
|
58
|
+
size="small"
|
|
59
|
+
text="base"
|
|
60
|
+
class="gap-1.5"
|
|
61
|
+
aria-label="Copy page as Markdown"
|
|
62
|
+
onClick={() => actions.copyMarkdown()}
|
|
63
|
+
>
|
|
64
|
+
<span aria-hidden="true" class={clsx("h-4 w-4 shrink-0 text-gray-400", status().icon)} />
|
|
65
|
+
<span aria-live="polite" class="max-sm:sr-only">
|
|
66
|
+
{status().label}
|
|
67
|
+
</span>
|
|
68
|
+
</Button>
|
|
69
|
+
<Menu
|
|
70
|
+
items={items()}
|
|
71
|
+
trigger={(triggerProps) => (
|
|
72
|
+
<IconButton
|
|
73
|
+
{...triggerProps()}
|
|
74
|
+
icon="i-lucide:chevron-down"
|
|
75
|
+
color="contrast"
|
|
76
|
+
variant="outlined"
|
|
77
|
+
size="small"
|
|
78
|
+
text="soft"
|
|
79
|
+
aria-label="More page actions"
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export { PageActions };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import type { Component } from "solid-js";
|
|
3
|
+
|
|
4
|
+
interface PoweredByProps {
|
|
5
|
+
class?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Credits Andesine Pages, with the Andesine wordmark */
|
|
9
|
+
const PoweredBy: Component<PoweredByProps> = (props) => (
|
|
10
|
+
<a
|
|
11
|
+
href="https://andesine.app"
|
|
12
|
+
class={clsx(
|
|
13
|
+
":base: flex items-center gap-1 rounded-md text-sm text-gray-500 transition duration-200 ease-out @hover:text-gray-700 focus-visible:text-gray-700",
|
|
14
|
+
props.class
|
|
15
|
+
)}
|
|
16
|
+
>
|
|
17
|
+
Powered by
|
|
18
|
+
<span role="img" aria-label="Andesine" class="flex items-center font-bold text-gray-700">
|
|
19
|
+
<span
|
|
20
|
+
aria-hidden="true"
|
|
21
|
+
class="i-andesine:logo h-[1.07em] w-[1.07em] shrink-0 bg-gradient-to-tr"
|
|
22
|
+
/>
|
|
23
|
+
<span aria-hidden="true">ndesine</span>
|
|
24
|
+
</span>
|
|
25
|
+
Pages
|
|
26
|
+
</a>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export { PoweredBy };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { SectionContext } from "@andesine/pages";
|
|
2
|
+
import { createTabIndicator } from "@andesine/ui/solid";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { type Component, For, Show } from "solid-js";
|
|
5
|
+
|
|
6
|
+
interface SectionTabsProps {
|
|
7
|
+
sections: SectionContext[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Lines sit on the last pixel row of the tabs, so the scroll container does not clip them.
|
|
11
|
+
const lineClass = "absolute bottom-0 h-px";
|
|
12
|
+
|
|
13
|
+
const SectionTabs: Component<SectionTabsProps> = (props) => {
|
|
14
|
+
const indicator = createTabIndicator({
|
|
15
|
+
current: () => props.sections.findIndex((section) => section.current)
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<nav ref={indicator.rootRef} aria-label="Sections" class="relative">
|
|
20
|
+
<span aria-hidden="true" class={clsx(lineClass, "inset-x-0 bg-gray-200")} />
|
|
21
|
+
<div
|
|
22
|
+
ref={indicator.scrollRef}
|
|
23
|
+
class={clsx(
|
|
24
|
+
"flex h-10 items-stretch gap-1 overflow-x-auto px-2",
|
|
25
|
+
"[scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
<For each={props.sections}>
|
|
29
|
+
{(section, index) => (
|
|
30
|
+
<a
|
|
31
|
+
ref={(element) => indicator.tabRef(index(), element)}
|
|
32
|
+
href={section.href}
|
|
33
|
+
aria-current={section.current ? "location" : undefined}
|
|
34
|
+
class={clsx(
|
|
35
|
+
"relative flex shrink-0 items-center gap-2 px-2 text-sm font-medium transition duration-200 ease-out",
|
|
36
|
+
section.current
|
|
37
|
+
? "bg-gradient-to-tr bg-[length:125%_auto] bg-clip-text text-transparent @hover:bg-right focus-visible:bg-right"
|
|
38
|
+
: clsx(
|
|
39
|
+
"text-gray-500 @hover:text-gray-700 focus-visible:text-gray-700",
|
|
40
|
+
"after:(absolute inset-x-2 bottom-0 h-px bg-gray-400 opacity-0 transition-opacity duration-200 content-[''])",
|
|
41
|
+
"@hover:after:opacity-100 focus-visible:after:opacity-100"
|
|
42
|
+
)
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
{/* Shown until the indicator is measured. */}
|
|
46
|
+
<Show when={section.current && !indicator.position()}>
|
|
47
|
+
<span
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
class="absolute inset-x-2 bottom-0 h-0.5 rounded-full bg-gradient-to-r"
|
|
50
|
+
/>
|
|
51
|
+
</Show>
|
|
52
|
+
<Show when={section.icon}>
|
|
53
|
+
<span
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
class={clsx(
|
|
56
|
+
"h-4 w-4 shrink-0",
|
|
57
|
+
section.current && "bg-gradient-to-tr",
|
|
58
|
+
section.icon
|
|
59
|
+
)}
|
|
60
|
+
/>
|
|
61
|
+
</Show>
|
|
62
|
+
{section.label}
|
|
63
|
+
</a>
|
|
64
|
+
)}
|
|
65
|
+
</For>
|
|
66
|
+
</div>
|
|
67
|
+
<Show when={indicator.position()}>
|
|
68
|
+
{(position) => (
|
|
69
|
+
<span
|
|
70
|
+
aria-hidden="true"
|
|
71
|
+
class="pointer-events-none absolute bottom-0 h-0.5 translate-y-[0.5px] rounded-full bg-gradient-to-r transition-[left,width] duration-200 ease-out"
|
|
72
|
+
style={{
|
|
73
|
+
left: `${position().left + 8}px`,
|
|
74
|
+
width: `${Math.max(0, position().width - 16)}px`
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
</Show>
|
|
79
|
+
</nav>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export { SectionTabs };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { IconLinkConfig, NavigationItem, SectionContext } from "@andesine/pages";
|
|
2
|
+
import { createScrollMemory } from "@andesine/ui/solid";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { type Component, Show } from "solid-js";
|
|
5
|
+
|
|
6
|
+
import { ScrollArea } from "../primitives/scroll-area";
|
|
7
|
+
import { NavigationLinks } from "./navigation-links";
|
|
8
|
+
import { NavigationTree } from "./navigation-tree";
|
|
9
|
+
import { PoweredBy } from "./powered-by";
|
|
10
|
+
|
|
11
|
+
interface SidebarProps {
|
|
12
|
+
navigation: NavigationItem[];
|
|
13
|
+
/** Sections to show above the navigation, in place of header tabs. */
|
|
14
|
+
sections: SectionContext[];
|
|
15
|
+
links: IconLinkConfig[];
|
|
16
|
+
storageKey: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const Sidebar: Component<SidebarProps> = (props) => {
|
|
21
|
+
const scrollKey = (): string => `${props.storageKey}:scroll`;
|
|
22
|
+
|
|
23
|
+
let viewport: HTMLDivElement | undefined;
|
|
24
|
+
|
|
25
|
+
createScrollMemory({
|
|
26
|
+
container: () => viewport,
|
|
27
|
+
key: scrollKey(),
|
|
28
|
+
current: () => viewport?.querySelector<HTMLElement>("[aria-current='page']")
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div class={props.class}>
|
|
33
|
+
<ScrollArea
|
|
34
|
+
shadow
|
|
35
|
+
class="h-full"
|
|
36
|
+
scrollKey={scrollKey()}
|
|
37
|
+
viewportRef={(element) => (viewport = element)}
|
|
38
|
+
contentClass="flex min-h-full flex-col px-3 pt-4"
|
|
39
|
+
>
|
|
40
|
+
<Show when={props.sections.length || props.links.length}>
|
|
41
|
+
<NavigationLinks sections={props.sections} links={props.links} class="px-2 pb-4" />
|
|
42
|
+
</Show>
|
|
43
|
+
<NavigationTree
|
|
44
|
+
items={props.navigation}
|
|
45
|
+
storageKey={props.storageKey}
|
|
46
|
+
defaultDepth={2}
|
|
47
|
+
class="flex-1 pb-4"
|
|
48
|
+
/>
|
|
49
|
+
{/* After the navigation, and at the bottom while the navigation scrolls. It covers the
|
|
50
|
+
bottom scroll shadow, so its fade stands in for it. */}
|
|
51
|
+
<div
|
|
52
|
+
class={clsx(
|
|
53
|
+
"sticky bottom-0 z-2 bg-gray-50 px-2 py-4",
|
|
54
|
+
"before:(pointer-events-none absolute inset-x-0 -top-12 h-12 bg-gradient-to-t from-gray-50 to-gray-50/0 opacity-0 transition-opacity duration-150 content-[''])",
|
|
55
|
+
"[[data-part=viewport][data-overflow-y]:not([data-at-bottom])_&]:before:opacity-100"
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
<PoweredBy />
|
|
59
|
+
</div>
|
|
60
|
+
</ScrollArea>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export { Sidebar };
|