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 @@
1
+ <div class="my-2 grid gap-4 sm:grid-cols-2"><slot /></div>
@@ -0,0 +1,31 @@
1
+ ---
2
+ interface Props {
3
+ title: string;
4
+ /** Card text; the slot content can be used instead. */
5
+ description?: string;
6
+ icon?: string;
7
+ /** Makes the whole card a link. */
8
+ href?: string;
9
+ }
10
+
11
+ const { title, description, icon, href } = Astro.props;
12
+ const Element = href ? "a" : "div";
13
+ ---
14
+
15
+ <Element
16
+ href={href}
17
+ class:list={[
18
+ "not-prose flex flex-col gap-2 rounded-xl border border-gray-200 bg-white p-4 shadow-[0_0_12px_0px] shadow-gray-200",
19
+ href && "transition duration-200 ease-out @hover:bg-gray-100 focus-visible:bg-gray-100"
20
+ ]}
21
+ >
22
+ {icon && <span aria-hidden="true" class:list={["h-6 w-6 bg-gradient-to-tr", icon]} />}
23
+ <span class="flex items-center gap-1 font-semibold text-gray-900">
24
+ {title}
25
+ {href && <span aria-hidden="true" class="i-lucide:arrow-right h-4 w-4 text-gray-400" />}
26
+ </span>
27
+ <div class="text-sm text-gray-500 [&>p]:m-0">
28
+ {description && <p>{description}</p>}
29
+ <slot />
30
+ </div>
31
+ </Element>
@@ -0,0 +1,34 @@
1
+ ---
2
+ import { ScrollArea } from "../primitives/scroll-area";
3
+ import { CopyButton } from "./copy-button";
4
+
5
+ interface Props {
6
+ "data-language"?: string;
7
+ "data-title"?: string;
8
+ [attribute: string]: unknown;
9
+ }
10
+
11
+ // Shiki's inline background and class are replaced by the card styles.
12
+ const {
13
+ "data-language": language,
14
+ "data-title": title,
15
+ "class": _class,
16
+ "style": _style,
17
+ ...rest
18
+ } = Astro.props;
19
+ ---
20
+
21
+ <figure
22
+ data-code
23
+ class="not-prose my-2 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-[0_0_12px_0px] shadow-gray-200 [[data-code-group]_&]:(my-0 rounded-none border-none shadow-none)"
24
+ >
25
+ <figcaption
26
+ class="flex h-9 items-center justify-between gap-2 border-b border-gray-200 pl-3 pr-1 [[data-code-group]_&]:hidden"
27
+ >
28
+ <span class="truncate font-mono text-xs text-gray-500">{title ?? language}</span>
29
+ <CopyButton client:visible />
30
+ </figcaption>
31
+ <ScrollArea client:visible orientation="horizontal">
32
+ <pre {...rest} class="p-4 font-mono text-sm leading-6 text-gray-700"><slot /></pre>
33
+ </ScrollArea>
34
+ </figure>
@@ -0,0 +1,13 @@
1
+ ---
2
+ import { renderTabs } from "@andesine/pages/astro";
3
+ import { ContentTabs } from "./tabs";
4
+
5
+ interface Props {
6
+ /** Code labels, added by the Andesine MDX plugin or content renderer. */
7
+ values: string;
8
+ }
9
+
10
+ const { values, panels } = await renderTabs(Astro.props.values, Astro.slots);
11
+ ---
12
+
13
+ <ContentTabs client:visible values={values} panels={panels} syncKey="code" variant="code" />
@@ -0,0 +1,53 @@
1
+ import { createClipboard } from "@andesine/ui/solid";
2
+ import type { Component } from "solid-js";
3
+ import { IconButton } from "../primitives/button";
4
+
5
+ interface CopyButtonProps {
6
+ /** Copied in place of the closest code block. */
7
+ text?: string;
8
+ label?: string;
9
+ }
10
+
11
+ /** Copies `text`, or the visible code block inside the closest `[data-code]` element. */
12
+ const CopyButton: Component<CopyButtonProps> = (props) => {
13
+ const clipboard = createClipboard();
14
+ const icon = (): string => {
15
+ if (clipboard.status() === true) return "i-lucide:check";
16
+ if (clipboard.status() === false) return "i-lucide:x";
17
+
18
+ return "i-lucide:copy";
19
+ };
20
+ const copy = (event: MouseEvent): void => {
21
+ if (props.text !== undefined) {
22
+ void clipboard.copy(props.text);
23
+
24
+ return;
25
+ }
26
+
27
+ const root = (event.currentTarget as HTMLElement).closest("[data-code], [data-code-group]");
28
+ const pre = Array.from(root?.querySelectorAll("pre") ?? []).find((element) => {
29
+ return element.getClientRects().length > 0;
30
+ });
31
+
32
+ if (pre) void clipboard.copy(pre.textContent ?? "");
33
+ };
34
+
35
+ return (
36
+ <>
37
+ <IconButton
38
+ icon={icon()}
39
+ variant="text"
40
+ size="xs"
41
+ text="softer"
42
+ aria-label={props.label ?? "Copy code"}
43
+ onClick={copy}
44
+ />
45
+ <span role="status" class="sr-only">
46
+ {clipboard.status() === true ? "Copied" : ""}
47
+ {clipboard.status() === false ? "Copy failed" : ""}
48
+ </span>
49
+ </>
50
+ );
51
+ };
52
+
53
+ export { CopyButton };
@@ -0,0 +1,26 @@
1
+ ---
2
+ interface Props {
3
+ title: string;
4
+ open?: boolean;
5
+ }
6
+
7
+ const { title, open } = Astro.props;
8
+ ---
9
+
10
+ <details
11
+ open={open}
12
+ class="my-2 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-[0_0_12px_0px] shadow-gray-200"
13
+ >
14
+ <summary
15
+ class="not-prose flex cursor-pointer list-none items-center justify-between gap-2 min-h-9 px-4 py-1.5 font-semibold text-gray-900 transition duration-200 ease-out @hover:bg-gray-100 focus-visible:bg-gray-100 [[open]>&]:bg-gray-100 [&::-webkit-details-marker]:hidden"
16
+ >
17
+ {title}
18
+ <span
19
+ aria-hidden="true"
20
+ class="i-lucide:chevron-right h-4.5 w-4.5 shrink-0 text-gray-400 transition-transform duration-200 [[open]>summary>&]:rotate-90"
21
+ ></span>
22
+ </summary>
23
+ <div class="border-t border-gray-200 px-4 py-3 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
24
+ <slot />
25
+ </div>
26
+ </details>
@@ -0,0 +1,10 @@
1
+ ---
2
+ interface Props {
3
+ caption: string;
4
+ }
5
+ ---
6
+
7
+ <figure class="my-2">
8
+ <slot />
9
+ <figcaption class="mt-2 text-center text-sm text-gray-500">{Astro.props.caption}</figcaption>
10
+ </figure>
@@ -0,0 +1,47 @@
1
+ ---
2
+ import type { FileTreeItem } from "@andesine/pages";
3
+ import { readFileTreeItems } from "@andesine/pages/astro";
4
+
5
+ interface Props {
6
+ /** Items, added by the Andesine MDX plugin; nested levels get their items. */
7
+ items: string | FileTreeItem[];
8
+ nested?: boolean;
9
+ }
10
+
11
+ // Matches the app's explorer: 1.75rem rows, 24px gray icons, and a guide line per level.
12
+ const { nested } = Astro.props;
13
+ const items = readFileTreeItems(Astro.props.items);
14
+ ---
15
+
16
+ <ul class:list={["not-prose flex flex-col gap-0.5 font-medium text-gray-700", !nested && "my-2"]}>
17
+ {
18
+ items.map((item) => (
19
+ <li class="flex flex-col">
20
+ <div class="flex min-h-7 items-center gap-1 pl-0.5">
21
+ <span class="flex h-6 w-6 shrink-0 items-center justify-center">
22
+ <span
23
+ aria-hidden="true"
24
+ class:list={[
25
+ "h-6 w-6 text-gray-400",
26
+ item.type === "folder"
27
+ ? "i-material-symbols:folder-open-rounded"
28
+ : "i-lucide:file-text"
29
+ ]}
30
+ />
31
+ </span>
32
+ <span class="line-clamp-1 flex-1">{item.name}</span>
33
+ </div>
34
+ {item.children.length > 0 && (
35
+ <div class="mt-0.5 flex">
36
+ <div class="flex min-w-3.5 justify-end pl-0.5">
37
+ <div class="h-full w-px rounded-full bg-gray-300" />
38
+ </div>
39
+ <div class="min-w-0 flex-1">
40
+ <Astro.self items={item.children} nested />
41
+ </div>
42
+ </div>
43
+ )}
44
+ </li>
45
+ ))
46
+ }
47
+ </ul>
@@ -0,0 +1,30 @@
1
+ import Callout from "./callout.astro";
2
+ import Card from "./card.astro";
3
+ import CardGrid from "./card-grid.astro";
4
+ import CodeBlock from "./code-block.astro";
5
+ import CodeGroup from "./code-group.astro";
6
+ import Disclosure from "./disclosure.astro";
7
+ import FileTree from "./file-tree.astro";
8
+ import Figure from "./figure.astro";
9
+ import Steps from "./steps.astro";
10
+ import Tab from "./tab.astro";
11
+ import Tabs from "./tabs.astro";
12
+ import Operation from "../reference/operation.astro";
13
+
14
+ /** Components for MDX content: custom elements, `pre` for code blocks, and API operations. */
15
+ const components = {
16
+ Callout,
17
+ Card,
18
+ CardGrid,
19
+ CodeGroup,
20
+ Disclosure,
21
+ FileTree,
22
+ Figure,
23
+ Operation,
24
+ Steps,
25
+ Tab,
26
+ Tabs,
27
+ pre: CodeBlock
28
+ };
29
+
30
+ export { components };
@@ -0,0 +1,16 @@
1
+ ---
2
+ // Wraps an ordered list; each item is one step, and its first paragraph is the step title.
3
+ ---
4
+
5
+ <div
6
+ class:list={[
7
+ "my-2 [&>ol]:(list-none p-0 [counter-reset:step])",
8
+ "[&>ol>li]:(relative m-0 pl-11 [counter-increment:step]) [&>ol>li:not(:last-child)]:pb-4",
9
+ "[&>ol>li]:before:(absolute left-0 top-0 flex h-7 w-7 items-center justify-center rounded-lg bg-white text-sm font-medium text-gray-700 shadow-md shadow-gray-200 outline outline-1 -outline-offset-1 outline-gray-200 content-[counter(step)])",
10
+ "[&>ol>li]:after:(absolute bottom-0 left-3.5 top-9 w-px rounded-full bg-gray-200 content-[''])",
11
+ "[&>ol>li>p:first-child]:(mb-0 mt-0.5 font-semibold text-gray-900)",
12
+ "[&>ol>li>*+*]:mt-1 [&>ol>li>*:last-child]:mb-0"
13
+ ]}
14
+ >
15
+ <slot />
16
+ </div>
@@ -0,0 +1,7 @@
1
+ ---
2
+ interface Props {
3
+ label: string;
4
+ }
5
+ ---
6
+
7
+ <slot />
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { renderTabs } from "@andesine/pages/astro";
3
+ import { ContentTabs } from "./tabs";
4
+
5
+ interface Props {
6
+ /** Tab labels, added by the Andesine MDX plugin or content renderer. */
7
+ values: string;
8
+ /** Tabs with the same key share their selection. */
9
+ syncKey?: string;
10
+ }
11
+
12
+ const { values, panels } = await renderTabs(Astro.props.values, Astro.slots);
13
+ ---
14
+
15
+ <ContentTabs client:visible values={values} panels={panels} syncKey={Astro.props.syncKey} />
@@ -0,0 +1,99 @@
1
+ import { Tabs } from "@andesine/ui/solid";
2
+ import clsx from "clsx";
3
+ import { type Component, For, Show } from "solid-js";
4
+ import { ScrollArea } from "../primitives/scroll-area";
5
+ import { CopyButton } from "./copy-button";
6
+
7
+ interface TabsProps {
8
+ values: string[];
9
+ syncKey?: string;
10
+ variant?: "content" | "code";
11
+ /** Rendered HTML of each tab panel. */
12
+ panels: string[];
13
+ }
14
+
15
+ const triggerClasses = {
16
+ content:
17
+ "cursor-pointer rounded-md px-2 py-0.5 transition duration-200 ease-out @hover:text-gray-700 focus-visible:text-gray-700 data-[selected]:(bg-white text-gray-700 shadow-md outline outline-1 outline-gray-200)",
18
+ // Matches the section tabs: gradient text for the current tab, a gray line on hover.
19
+ code: clsx(
20
+ "relative flex h-full cursor-pointer items-center px-1.5 font-mono transition duration-200 ease-out",
21
+ "@hover:text-gray-700 focus-visible:text-gray-700",
22
+ "after:(absolute inset-x-1.5 bottom-0 h-px bg-gray-400 opacity-0 transition-opacity duration-200 content-[''])",
23
+ "@hover:after:opacity-100 focus-visible:after:opacity-100",
24
+ "data-[selected]:(bg-gradient-to-tr bg-[length:125%_auto] bg-clip-text text-transparent! after:hidden)"
25
+ )
26
+ };
27
+
28
+ /** Tabs in a card: a segmented control for content, or a code header with a copy button. */
29
+ const ContentTabs: Component<TabsProps> = (props) => {
30
+ const variant = (): "content" | "code" => props.variant ?? "content";
31
+
32
+ return (
33
+ <Tabs.Root
34
+ values={props.values}
35
+ syncKey={props.syncKey}
36
+ data-code-group={variant() === "code" || undefined}
37
+ class="not-prose my-2 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-[0_0_12px_0px] shadow-gray-200"
38
+ >
39
+ <div
40
+ class={clsx(
41
+ // The standard card header: 2.25rem high, with a bottom line. The line is drawn over the
42
+ // header, so the tabs can scroll across the full height and their indicator sits on it.
43
+ "relative flex h-9 items-center gap-2",
44
+ "after:(pointer-events-none absolute inset-x-0 bottom-0 h-px bg-gray-200 content-[''])",
45
+ variant() === "code" && "pl-1.5 pr-1"
46
+ )}
47
+ >
48
+ {/* Many tabs scroll sideways, with a hover-only scrollbar on the bottom line. */}
49
+ <ScrollArea
50
+ orientation="horizontal"
51
+ class="z-1 min-w-0 flex-1 self-stretch"
52
+ contentClass="h-full"
53
+ scrollbarClass="h-1.5 px-0"
54
+ thumbClass="border-0"
55
+ >
56
+ <Tabs.List
57
+ class={clsx(
58
+ "relative flex h-full text-gray-500",
59
+ variant() === "code"
60
+ ? "gap-1 text-xs"
61
+ : "min-w-full items-center gap-1 bg-gray-100 px-1 pb-px text-sm"
62
+ )}
63
+ >
64
+ <For each={props.values}>
65
+ {(value) => (
66
+ <Tabs.Trigger value={value} class={clsx("shrink-0", triggerClasses[variant()])}>
67
+ {value}
68
+ </Tabs.Trigger>
69
+ )}
70
+ </For>
71
+ <Show when={variant() === "code"}>
72
+ {/* On the header's bottom line, like the section tabs indicator. */}
73
+ <Tabs.Indicator class="pointer-events-none bottom-0 h-0.5 w-[var(--width)] rounded-full bg-gradient-to-r [--transition-duration:200ms] [--transition-timing-function:ease-out]" />
74
+ </Show>
75
+ </Tabs.List>
76
+ </ScrollArea>
77
+ <Show when={variant() === "code"}>
78
+ <div class="ml-auto flex shrink-0 items-center">
79
+ <CopyButton />
80
+ </div>
81
+ </Show>
82
+ </div>
83
+ <For each={props.values}>
84
+ {(value, index) => (
85
+ <Tabs.Content
86
+ value={value}
87
+ class={clsx(
88
+ variant() === "content" &&
89
+ "prose max-w-none px-4 py-3 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
90
+ )}
91
+ innerHTML={props.panels[index()]}
92
+ />
93
+ )}
94
+ </For>
95
+ </Tabs.Root>
96
+ );
97
+ };
98
+
99
+ export { ContentTabs };
@@ -0,0 +1,27 @@
1
+ import { createScrollEdges } from "@andesine/ui/solid";
2
+ import clsx from "clsx";
3
+ import type { Component } from "solid-js";
4
+
5
+ interface PageScrollShadowProps {
6
+ /** Positions the top fade below the sticky header. */
7
+ topClass: string;
8
+ }
9
+
10
+ const shadowClass =
11
+ "absolute inset-x-0 h-16 from-gray-50 to-gray-50/0 transition-opacity duration-150";
12
+
13
+ /** Fades the page content at the viewport edges while it scrolls past them. */
14
+ const PageScrollShadow: Component<PageScrollShadowProps> = (props) => {
15
+ const edges = createScrollEdges();
16
+
17
+ return (
18
+ <div aria-hidden="true" class="pointer-events-none sticky top-0 h-dvh">
19
+ <div
20
+ class={clsx(shadowClass, "bg-gradient-to-b", props.topClass, !edges.start() && "opacity-0")}
21
+ />
22
+ <div class={clsx(shadowClass, "bottom-0 bg-gradient-to-t", !edges.end() && "opacity-0")} />
23
+ </div>
24
+ );
25
+ };
26
+
27
+ export { PageScrollShadow };
@@ -0,0 +1,35 @@
1
+ import { createPageScrollbar } from "@andesine/ui/solid";
2
+ import clsx from "clsx";
3
+ import type { Component } from "solid-js";
4
+
5
+ /**
6
+ * The overlay page scrollbar. The native one is hidden, so pages with and without overflow have
7
+ * the same width. Touch screens keep their native scrollbar.
8
+ */
9
+ const PageScrollbar: Component = () => {
10
+ const scrollbar = createPageScrollbar();
11
+
12
+ return (
13
+ <div
14
+ aria-hidden="true"
15
+ class={clsx(
16
+ "fixed inset-y-0 right-0.5 z-40 w-3 transition-opacity duration-200 media-touch:hidden",
17
+ scrollbar.visible() ? "opacity-100" : "opacity-0",
18
+ !scrollbar.thumb() && "hidden",
19
+ !scrollbar.interactive() && "pointer-events-none"
20
+ )}
21
+ onPointerDown={scrollbar.onTrackPointerDown}
22
+ >
23
+ <div
24
+ {...scrollbar.thumbHandlers}
25
+ class="absolute inset-x-0 cursor-default rounded-lg border-2 border-solid border-transparent bg-gray-200 bg-clip-padding shadow-inner"
26
+ style={{
27
+ top: `${scrollbar.thumb()?.top ?? 0}px`,
28
+ height: `${scrollbar.thumb()?.height ?? 0}px`
29
+ }}
30
+ />
31
+ </div>
32
+ );
33
+ };
34
+
35
+ export { PageScrollbar };
@@ -0,0 +1,28 @@
1
+ import type { LinkConfig } from "@andesine/pages";
2
+ import type { Component } from "solid-js";
3
+ import { Button, IconButton } from "../primitives/button";
4
+
5
+ interface CallToActionProps {
6
+ link: LinkConfig;
7
+ }
8
+
9
+ /** The main action link, after the Andesine landing page: the label, then an optional icon. */
10
+ const CallToAction: Component<CallToActionProps> = (props) =>
11
+ props.link.icon ? (
12
+ <IconButton
13
+ link={props.link.href}
14
+ icon={props.link.icon}
15
+ label={props.link.label}
16
+ color="primary"
17
+ variant="outlined"
18
+ size="small"
19
+ class="flex-row-reverse gap-1 pr-1.5"
20
+ iconClass="opacity-50 h-4.5 w-4.5"
21
+ />
22
+ ) : (
23
+ <Button link={props.link.href} color="primary" variant="outlined" size="small">
24
+ {props.link.label}
25
+ </Button>
26
+ );
27
+
28
+ export { CallToAction };
@@ -0,0 +1,44 @@
1
+ ---
2
+ import type { PageContext } from "@andesine/pages";
3
+ import { IconButton } from "../primitives/button";
4
+
5
+ interface Props {
6
+ page: PageContext;
7
+ }
8
+
9
+ const { page } = Astro.props;
10
+ const { site } = page;
11
+ const updated =
12
+ page.updatedAt &&
13
+ new Intl.DateTimeFormat(site.language, { dateStyle: "long", timeZone: "UTC" }).format(
14
+ new Date(page.updatedAt)
15
+ );
16
+ ---
17
+
18
+ <!-- A line with one item keeps it at the start: icons go left when they wrap or have no date. -->
19
+ <footer class="flex min-h-8 flex-wrap items-center justify-between gap-4">
20
+ {
21
+ updated && (
22
+ <p class="text-sm text-gray-400">
23
+ Last updated <time datetime={page.updatedAt}>{updated}</time>
24
+ </p>
25
+ )
26
+ }
27
+ {
28
+ site.socialLinks.length > 0 && (
29
+ <div class="flex items-center gap-1">
30
+ {site.socialLinks.map((link) => (
31
+ <IconButton
32
+ link={link.href}
33
+ icon={link.icon}
34
+ variant="text"
35
+ text="softer"
36
+ size="small"
37
+ aria-label={link.label}
38
+ title={link.label}
39
+ />
40
+ ))}
41
+ </div>
42
+ )
43
+ }
44
+ </footer>
@@ -0,0 +1,34 @@
1
+ import type { BreadcrumbItem } from "@andesine/pages";
2
+ import { Breadcrumbs } from "@andesine/ui/solid";
3
+ import { type Component, Show } from "solid-js";
4
+
5
+ interface GroupLabelProps {
6
+ group: BreadcrumbItem;
7
+ }
8
+
9
+ const labelClass = "bg-gradient-to-tr bg-[length:125%_auto] bg-clip-text text-transparent";
10
+
11
+ /** The direct parent group of the page. */
12
+ const GroupLabel: Component<GroupLabelProps> = (props) => (
13
+ <Breadcrumbs.Root>
14
+ <Breadcrumbs.List class="flex text-sm font-semibold">
15
+ <Breadcrumbs.Item class="min-w-0 truncate">
16
+ <Show
17
+ when={props.group.href}
18
+ fallback={<Breadcrumbs.Label class={labelClass}>{props.group.label}</Breadcrumbs.Label>}
19
+ >
20
+ {(href) => (
21
+ <Breadcrumbs.Link
22
+ href={href()}
23
+ class={`${labelClass} decoration-tertiary underline-offset-4 @hover:underline focus-visible:underline`}
24
+ >
25
+ {props.group.label}
26
+ </Breadcrumbs.Link>
27
+ )}
28
+ </Show>
29
+ </Breadcrumbs.Item>
30
+ </Breadcrumbs.List>
31
+ </Breadcrumbs.Root>
32
+ );
33
+
34
+ export { GroupLabel };
@@ -0,0 +1,53 @@
1
+ ---
2
+ import type { PageContext } from "@andesine/pages";
3
+ import { IconButton } from "../primitives/button";
4
+ import { Logo } from "../primitives/logo";
5
+ import { SearchPalette } from "../search/search-palette";
6
+ import { CallToAction } from "./call-to-action";
7
+ import { SectionTabs } from "./section-tabs";
8
+
9
+ interface Props {
10
+ page: PageContext;
11
+ }
12
+
13
+ const { page } = Astro.props;
14
+ const { site } = page;
15
+ ---
16
+
17
+ <header class="sticky top-0 z-30 col-span-full bg-gray-50">
18
+ <div
19
+ class="grid h-16 grid-cols-[minmax(0,1fr)_auto] items-center gap-4 px-4 md:(grid-cols-[17rem_minmax(0,1fr)_auto] gap-0 px-0) xl:grid-cols-[17rem_minmax(0,1fr)_17rem]"
20
+ >
21
+ <a
22
+ href={site.href}
23
+ class="min-w-0 rounded-lg text-gray-900 focus-visible:(outline outline-2 outline-offset-2 outline-tertiary) md:px-4"
24
+ >
25
+ <Logo site={site} />
26
+ </a>
27
+ <div class="hidden md:(block w-full max-w-screen-md justify-self-center px-8)">
28
+ {site.search && <SearchPalette client:idle search={site.search} />}
29
+ </div>
30
+ <div class="flex min-w-0 items-center justify-end gap-2 md:px-4">
31
+ {
32
+ site.search && (
33
+ <IconButton
34
+ data-search-trigger
35
+ icon="i-tabler:search"
36
+ color="contrast"
37
+ variant="outlined"
38
+ size="small"
39
+ text="softer"
40
+ class="md:hidden"
41
+ aria-label="Search"
42
+ />
43
+ )
44
+ }
45
+ {site.cta && <CallToAction link={site.cta} />}
46
+ </div>
47
+ </div>
48
+ {
49
+ page.sections.length > 0 && site.sectionsDisplay === "tabs" && (
50
+ <SectionTabs client:load sections={page.sections} />
51
+ )
52
+ }
53
+ </header>