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.
Files changed (81) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +27 -8
  3. package/dist/cli.js +1611 -121
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config.schema.json +20 -3
  6. package/dist/templates/pages/.env.example +5 -0
  7. package/dist/templates/pages/AGENTS.md +79 -0
  8. package/dist/templates/pages/LICENSE +21 -0
  9. package/dist/templates/pages/README.md +72 -0
  10. package/dist/templates/pages/andesine.config.ts +39 -0
  11. package/dist/templates/pages/astro.config.ts +23 -0
  12. package/dist/templates/pages/gitignore +13 -0
  13. package/dist/templates/pages/package.template.json +45 -0
  14. package/dist/templates/pages/src/api/openapi.yaml +246 -0
  15. package/dist/templates/pages/src/assets/andesine.svg +1 -0
  16. package/dist/templates/pages/src/assets/andesine.ts +6 -0
  17. package/dist/templates/pages/src/assets/favicon.svg +13 -0
  18. package/dist/templates/pages/src/assets/social-dots.svg +14 -0
  19. package/dist/templates/pages/src/components/content/callout.astro +26 -0
  20. package/dist/templates/pages/src/components/content/card-grid.astro +1 -0
  21. package/dist/templates/pages/src/components/content/card.astro +31 -0
  22. package/dist/templates/pages/src/components/content/code-block.astro +34 -0
  23. package/dist/templates/pages/src/components/content/code-group.astro +13 -0
  24. package/dist/templates/pages/src/components/content/copy-button.tsx +53 -0
  25. package/dist/templates/pages/src/components/content/disclosure.astro +26 -0
  26. package/dist/templates/pages/src/components/content/figure.astro +10 -0
  27. package/dist/templates/pages/src/components/content/file-tree.astro +47 -0
  28. package/dist/templates/pages/src/components/content/index.ts +30 -0
  29. package/dist/templates/pages/src/components/content/steps.astro +16 -0
  30. package/dist/templates/pages/src/components/content/tab.astro +7 -0
  31. package/dist/templates/pages/src/components/content/tabs.astro +15 -0
  32. package/dist/templates/pages/src/components/content/tabs.tsx +99 -0
  33. package/dist/templates/pages/src/components/effects/page-scroll-shadow.tsx +27 -0
  34. package/dist/templates/pages/src/components/effects/page-scrollbar.tsx +35 -0
  35. package/dist/templates/pages/src/components/layout/call-to-action.tsx +28 -0
  36. package/dist/templates/pages/src/components/layout/footer.astro +44 -0
  37. package/dist/templates/pages/src/components/layout/group-label.tsx +34 -0
  38. package/dist/templates/pages/src/components/layout/header.astro +53 -0
  39. package/dist/templates/pages/src/components/layout/mobile-navigation.tsx +51 -0
  40. package/dist/templates/pages/src/components/layout/navigation-links.tsx +57 -0
  41. package/dist/templates/pages/src/components/layout/navigation-tree.tsx +138 -0
  42. package/dist/templates/pages/src/components/layout/outline.tsx +162 -0
  43. package/dist/templates/pages/src/components/layout/page-actions.tsx +87 -0
  44. package/dist/templates/pages/src/components/layout/powered-by.tsx +29 -0
  45. package/dist/templates/pages/src/components/layout/section-tabs.tsx +83 -0
  46. package/dist/templates/pages/src/components/layout/sidebar.tsx +65 -0
  47. package/dist/templates/pages/src/components/primitives/button.tsx +121 -0
  48. package/dist/templates/pages/src/components/primitives/card.tsx +25 -0
  49. package/dist/templates/pages/src/components/primitives/dialog.tsx +47 -0
  50. package/dist/templates/pages/src/components/primitives/input.tsx +22 -0
  51. package/dist/templates/pages/src/components/primitives/logo.tsx +41 -0
  52. package/dist/templates/pages/src/components/primitives/menu.tsx +121 -0
  53. package/dist/templates/pages/src/components/primitives/scroll-area.tsx +103 -0
  54. package/dist/templates/pages/src/components/primitives/shortcut.tsx +60 -0
  55. package/dist/templates/pages/src/components/primitives/tooltip.tsx +39 -0
  56. package/dist/templates/pages/src/components/reference/facts.astro +27 -0
  57. package/dist/templates/pages/src/components/reference/fields.astro +102 -0
  58. package/dist/templates/pages/src/components/reference/method-badge.astro +18 -0
  59. package/dist/templates/pages/src/components/reference/methods.ts +17 -0
  60. package/dist/templates/pages/src/components/reference/operation.astro +252 -0
  61. package/dist/templates/pages/src/components/reference/styles.ts +14 -0
  62. package/dist/templates/pages/src/components/search/highlighted-text.tsx +22 -0
  63. package/dist/templates/pages/src/components/search/search-answer.tsx +123 -0
  64. package/dist/templates/pages/src/components/search/search-palette.tsx +174 -0
  65. package/dist/templates/pages/src/components/search/search-results.tsx +229 -0
  66. package/dist/templates/pages/src/components/social/card.ts +77 -0
  67. package/dist/templates/pages/src/content/docs/customization/index.mdx +11 -0
  68. package/dist/templates/pages/src/content/docs/customization/styles.mdx +18 -0
  69. package/dist/templates/pages/src/content/docs/getting-started/api-reference.mdx +100 -0
  70. package/dist/templates/pages/src/content/docs/getting-started/components.mdx +156 -0
  71. package/dist/templates/pages/src/content/docs/getting-started/configuration.mdx +194 -0
  72. package/dist/templates/pages/src/content/docs/getting-started/deployment.mdx +142 -0
  73. package/dist/templates/pages/src/content/docs/getting-started/index.mdx +22 -0
  74. package/dist/templates/pages/src/content/docs/getting-started/writing-pages.mdx +35 -0
  75. package/dist/templates/pages/src/content/docs/index.mdx +21 -0
  76. package/dist/templates/pages/src/content.config.ts +1 -0
  77. package/dist/templates/pages/src/layouts/page.astro +195 -0
  78. package/dist/templates/pages/src/styles/global.css +95 -0
  79. package/dist/templates/pages/tsconfig.json +10 -0
  80. package/dist/templates/pages/uno.config.ts +168 -0
  81. 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 };