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,121 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { type JSX, type ParentComponent, mergeProps, Show, splitProps } from "solid-js";
|
|
3
|
+
import { Dynamic } from "solid-js/web";
|
|
4
|
+
|
|
5
|
+
interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
6
|
+
color?: ButtonColor;
|
|
7
|
+
variant?: ButtonVariant;
|
|
8
|
+
size?: ButtonSize;
|
|
9
|
+
text?: ButtonText;
|
|
10
|
+
/** Renders an `<a>` element. */
|
|
11
|
+
link?: string;
|
|
12
|
+
target?: string;
|
|
13
|
+
/** Renders a non-interactive `<div>` element. */
|
|
14
|
+
badge?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface IconButtonProps extends ButtonProps {
|
|
18
|
+
icon: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
iconClass?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type ButtonColor = "base" | "contrast" | "primary";
|
|
24
|
+
type ButtonVariant = "solid" | "text" | "outlined";
|
|
25
|
+
type ButtonSize = "xs" | "small" | "medium";
|
|
26
|
+
type ButtonText = "base" | "soft" | "softer";
|
|
27
|
+
|
|
28
|
+
const sizeClasses: Record<ButtonSize, string> = {
|
|
29
|
+
xs: ":base: px-1 py-0.5 text-xs",
|
|
30
|
+
small: ":base: px-1.5 py-1 text-sm",
|
|
31
|
+
medium: ":base: px-2 py-1 text-base"
|
|
32
|
+
};
|
|
33
|
+
const colorClasses: Record<ButtonColor, string> = {
|
|
34
|
+
base: ":base: bg-gray-200 outline-gray-200",
|
|
35
|
+
contrast: ":base: bg-white outline-gray-200 shadow-gray-200",
|
|
36
|
+
primary: ":base: bg-gradient-to-tr bg-[length:125%_auto] text-white outline-tertiary"
|
|
37
|
+
};
|
|
38
|
+
const variantClasses: Record<ButtonVariant, string> = {
|
|
39
|
+
solid: ":base: focus:outline-none",
|
|
40
|
+
text: ":base: bg-transparent focus:outline-none",
|
|
41
|
+
outlined: ":base: outline outline-1 -outline-offset-1 shadow-md focus-visible:outline-1"
|
|
42
|
+
};
|
|
43
|
+
const textClasses: Record<ButtonText, string> = {
|
|
44
|
+
base: ":base: text-gray-700",
|
|
45
|
+
soft: ":base: text-gray-400",
|
|
46
|
+
softer: ":base: text-gray-500"
|
|
47
|
+
};
|
|
48
|
+
const iconButtonSizes: Record<ButtonSize, Record<"button" | "icon" | "label", string>> = {
|
|
49
|
+
xs: { button: ":base-2: p-1", icon: ":base: h-4.5 w-4.5", label: ":base: pl-1" },
|
|
50
|
+
small: { button: ":base-2: p-1", icon: ":base: h-5 w-5", label: ":base: pl-1" },
|
|
51
|
+
medium: { button: ":base-2: p-1", icon: ":base: h-6 w-6", label: ":base: pl-1" }
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Keyboard focus shows the hover style.
|
|
55
|
+
const getHoverClasses = (color: ButtonColor, variant: ButtonVariant): string => {
|
|
56
|
+
if (color === "primary") return ":base: @hover:bg-right focus-visible:bg-right";
|
|
57
|
+
if (color === "contrast") return ":base: @hover:bg-gray-100 focus-visible:bg-gray-100";
|
|
58
|
+
if (variant === "text") return ":base: @hover:bg-gray-200 focus-visible:bg-gray-200";
|
|
59
|
+
|
|
60
|
+
return ":base: @hover:(shadow-inner outline-gray-300) focus-visible:(shadow-inner outline-gray-300)";
|
|
61
|
+
};
|
|
62
|
+
const Button: ParentComponent<ButtonProps> = (providedProps) => {
|
|
63
|
+
const props = mergeProps(
|
|
64
|
+
{ color: "base", variant: "solid", size: "medium" } as const,
|
|
65
|
+
providedProps
|
|
66
|
+
);
|
|
67
|
+
const [local, rest] = splitProps(props, [
|
|
68
|
+
"class",
|
|
69
|
+
"color",
|
|
70
|
+
"variant",
|
|
71
|
+
"size",
|
|
72
|
+
"text",
|
|
73
|
+
"link",
|
|
74
|
+
"badge"
|
|
75
|
+
]);
|
|
76
|
+
const component = (): "a" | "div" | "button" => {
|
|
77
|
+
if (local.link) return "a";
|
|
78
|
+
if (local.badge) return "div";
|
|
79
|
+
|
|
80
|
+
return "button";
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Dynamic
|
|
85
|
+
component={component()}
|
|
86
|
+
{...rest}
|
|
87
|
+
href={local.link}
|
|
88
|
+
type={component() === "button" ? (rest.type ?? "button") : undefined}
|
|
89
|
+
class={clsx(
|
|
90
|
+
":base: relative inline-flex items-center justify-center rounded-lg font-medium transition-[background-position,background-color,box-shadow,opacity] duration-200 ease-out disabled:opacity-70",
|
|
91
|
+
!local.badge && ":base: cursor-pointer",
|
|
92
|
+
sizeClasses[local.size],
|
|
93
|
+
variantClasses[local.variant],
|
|
94
|
+
local.variant !== "text" && colorClasses[local.color],
|
|
95
|
+
local.text && textClasses[local.text],
|
|
96
|
+
getHoverClasses(local.color, local.variant),
|
|
97
|
+
local.class
|
|
98
|
+
)}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
const IconButton: ParentComponent<IconButtonProps> = (providedProps) => {
|
|
103
|
+
const props = mergeProps({ size: "medium" } as const, providedProps);
|
|
104
|
+
const [local, rest] = splitProps(props, ["class", "icon", "label", "iconClass", "children"]);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Button {...rest} class={clsx(iconButtonSizes[props.size].button, local.class)}>
|
|
108
|
+
<span
|
|
109
|
+
aria-hidden="true"
|
|
110
|
+
class={clsx("shrink-0", iconButtonSizes[props.size].icon, local.icon, local.iconClass)}
|
|
111
|
+
/>
|
|
112
|
+
<Show when={local.label}>
|
|
113
|
+
<span class={clsx("truncate", iconButtonSizes[props.size].label)}>{local.label}</span>
|
|
114
|
+
</Show>
|
|
115
|
+
{local.children}
|
|
116
|
+
</Button>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export { Button, IconButton };
|
|
121
|
+
export type { ButtonProps, IconButtonProps };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { type JSX, type ParentComponent, splitProps } from "solid-js";
|
|
3
|
+
|
|
4
|
+
interface CardProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** A soft shadow around the card. */
|
|
6
|
+
shade?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Card: ParentComponent<CardProps> = (props) => {
|
|
10
|
+
const [local, rest] = splitProps(props, ["class", "shade"]);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
{...rest}
|
|
15
|
+
class={clsx(
|
|
16
|
+
":base: rounded-2xl border border-gray-200 bg-gray-50 p-2 outline-none",
|
|
17
|
+
local.shade && ":base: shadow-[0_0_12px_0px] shadow-gray-200",
|
|
18
|
+
local.class
|
|
19
|
+
)}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export { Card };
|
|
25
|
+
export type { CardProps };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Dialog as ArkDialog } from "@ark-ui/solid/dialog";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import type { ParentComponent } from "solid-js";
|
|
4
|
+
import { Portal } from "solid-js/web";
|
|
5
|
+
import { Card } from "./card";
|
|
6
|
+
|
|
7
|
+
interface DialogProps {
|
|
8
|
+
open: boolean;
|
|
9
|
+
/** The dialog's accessible name. */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Element that gets focus on open; the default is the first focusable element. */
|
|
12
|
+
initialFocus?(): HTMLElement | null | undefined;
|
|
13
|
+
class?: string;
|
|
14
|
+
onOpenChange(open: boolean): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A dialog near the top of the screen, over the blurred page, e.g. for search. */
|
|
18
|
+
const Dialog: ParentComponent<DialogProps> = (props) => (
|
|
19
|
+
<ArkDialog.Root
|
|
20
|
+
open={props.open}
|
|
21
|
+
initialFocusEl={props.initialFocus && (() => props.initialFocus?.() ?? null)}
|
|
22
|
+
onOpenChange={(details) => props.onOpenChange(details.open)}
|
|
23
|
+
>
|
|
24
|
+
<Portal>
|
|
25
|
+
<ArkDialog.Backdrop class="fixed inset-0 z-70 data-[state=open]:animate-overlay-in data-[state=closed]:animate-overlay-out" />
|
|
26
|
+
<ArkDialog.Positioner class="fixed inset-0 z-70 flex items-start justify-center px-4 pb-4 pt-[10dvh]">
|
|
27
|
+
<ArkDialog.Content
|
|
28
|
+
aria-label={props.label}
|
|
29
|
+
class="outline-none data-[state=open]:animate-dialog-in data-[state=closed]:animate-dialog-out"
|
|
30
|
+
>
|
|
31
|
+
<Card
|
|
32
|
+
shade
|
|
33
|
+
class={clsx(
|
|
34
|
+
":base-2: flex w-xl max-w-[calc(100vw-2rem)] flex-col gap-3 rounded-xl p-3 md:p-4",
|
|
35
|
+
props.class
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
{props.children}
|
|
39
|
+
</Card>
|
|
40
|
+
</ArkDialog.Content>
|
|
41
|
+
</ArkDialog.Positioner>
|
|
42
|
+
</Portal>
|
|
43
|
+
</ArkDialog.Root>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export { Dialog };
|
|
47
|
+
export type { DialogProps };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { type Component, type JSX, splitProps } from "solid-js";
|
|
3
|
+
|
|
4
|
+
type InputProps = JSX.InputHTMLAttributes<HTMLInputElement>;
|
|
5
|
+
|
|
6
|
+
// Fields use at least 16px text on mobile to prevent zoom on iOS.
|
|
7
|
+
const Input: Component<InputProps> = (props) => {
|
|
8
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<input
|
|
12
|
+
{...rest}
|
|
13
|
+
class={clsx(
|
|
14
|
+
":base: h-7 w-full rounded-lg bg-gray-200 px-2 text-[16px] text-gray-700 outline-gray-200 placeholder:(text-gray-500 opacity-50) focus:(shadow-inner outline-none) md:text-sm",
|
|
15
|
+
local.class
|
|
16
|
+
)}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export { Input };
|
|
22
|
+
export type { InputProps };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SiteContext } from "@andesine/pages";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { type Component, Show } from "solid-js";
|
|
4
|
+
|
|
5
|
+
interface LogoProps {
|
|
6
|
+
site: SiteContext;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The configured logo and the site name: a square icon next to the name, or a full logo and a
|
|
12
|
+
* bar before the name. Without a logo, the name only.
|
|
13
|
+
*/
|
|
14
|
+
const Logo: Component<LogoProps> = (props) => {
|
|
15
|
+
const full = (): boolean => props.site.logo?.format === "full";
|
|
16
|
+
const showName = (): boolean => !props.site.logo || props.site.logo.title;
|
|
17
|
+
// The name follows as text, so a logo image repeats only the text in the image.
|
|
18
|
+
const alt = (): string => props.site.logo?.alt ?? (showName() ? "" : props.site.name);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<span class={clsx(":base: flex min-w-0 items-center gap-3 text-2xl font-bold", props.class)}>
|
|
22
|
+
<Show when={props.site.logo}>
|
|
23
|
+
{(logo) => (
|
|
24
|
+
<img
|
|
25
|
+
src={logo().src}
|
|
26
|
+
alt={alt()}
|
|
27
|
+
class={full() ? "h-6.5 w-auto shrink-0" : "h-8 w-8 shrink-0"}
|
|
28
|
+
/>
|
|
29
|
+
)}
|
|
30
|
+
</Show>
|
|
31
|
+
<Show when={showName()}>
|
|
32
|
+
<Show when={full()}>
|
|
33
|
+
<span aria-hidden="true" class="h-6 w-0.5 shrink-0 rounded-full bg-gray-200" />
|
|
34
|
+
</Show>
|
|
35
|
+
<span class={clsx("truncate", full() && "text-xl font-semibold")}>{props.site.name}</span>
|
|
36
|
+
</Show>
|
|
37
|
+
</span>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export { Logo };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Menu as ArkMenu } from "@ark-ui/solid/menu";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { type Component, type JSX, For, Match, Show, Switch } from "solid-js";
|
|
4
|
+
import { Portal } from "solid-js/web";
|
|
5
|
+
|
|
6
|
+
interface MenuItem {
|
|
7
|
+
type?: "item";
|
|
8
|
+
value: string;
|
|
9
|
+
label: string;
|
|
10
|
+
icon?: string;
|
|
11
|
+
href?: string;
|
|
12
|
+
target?: string;
|
|
13
|
+
onSelect?(): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface MenuHeader {
|
|
17
|
+
type: "header";
|
|
18
|
+
label: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface MenuSeparator {
|
|
22
|
+
type: "separator";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface MenuProps {
|
|
26
|
+
items: MenuEntry[];
|
|
27
|
+
/** Renders the trigger; spread the given props on the trigger element. */
|
|
28
|
+
trigger(props: () => JSX.ButtonHTMLAttributes<HTMLButtonElement>): JSX.Element;
|
|
29
|
+
class?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface MenuItemContentProps {
|
|
33
|
+
item: MenuItem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type MenuEntry = MenuItem | MenuHeader | MenuSeparator;
|
|
37
|
+
|
|
38
|
+
const itemClasses =
|
|
39
|
+
"group relative flex w-full cursor-pointer items-center gap-1 rounded-md px-1 py-0.5 text-sm text-gray-700 outline-none data-[highlighted]:bg-gray-100";
|
|
40
|
+
|
|
41
|
+
const MenuItemContent: Component<MenuItemContentProps> = (props) => (
|
|
42
|
+
<>
|
|
43
|
+
<Show when={props.item.icon}>
|
|
44
|
+
<span
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
class={clsx("h-4.5 w-4.5 shrink-0 text-gray-500", props.item.icon)}
|
|
47
|
+
/>
|
|
48
|
+
</Show>
|
|
49
|
+
<span class="flex-1 truncate px-1 text-start">{props.item.label}</span>
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
const Menu: Component<MenuProps> = (props) => (
|
|
53
|
+
<ArkMenu.Root
|
|
54
|
+
positioning={{ placement: "bottom-end", offset: { mainAxis: 6 } }}
|
|
55
|
+
onSelect={(details) => {
|
|
56
|
+
const item = props.items.find(
|
|
57
|
+
(entry): entry is MenuItem => "value" in entry && entry.value === details.value
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
item?.onSelect?.();
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<ArkMenu.Trigger asChild={props.trigger} />
|
|
64
|
+
<Portal>
|
|
65
|
+
<ArkMenu.Positioner>
|
|
66
|
+
<ArkMenu.Content
|
|
67
|
+
class={clsx(
|
|
68
|
+
// Like the app, menus show and hide without animation.
|
|
69
|
+
"z-80 flex min-w-48 flex-col gap-0.5 rounded-[0.625rem] border border-gray-200 bg-white p-1 shadow-[0_0_12px_0px] shadow-black/15 outline-none",
|
|
70
|
+
props.class
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<For each={props.items}>
|
|
74
|
+
{(entry) => (
|
|
75
|
+
<Switch>
|
|
76
|
+
<Match when={entry.type === "separator"}>
|
|
77
|
+
<ArkMenu.Separator class="my-0.5 h-px border-none bg-gray-200" />
|
|
78
|
+
</Match>
|
|
79
|
+
<Match when={entry.type === "header" && entry}>
|
|
80
|
+
{(header) => (
|
|
81
|
+
<span class="truncate px-1 py-0.5 text-xs text-gray-500">{header().label}</span>
|
|
82
|
+
)}
|
|
83
|
+
</Match>
|
|
84
|
+
<Match when={entry.type !== "header" && entry.type !== "separator" && entry}>
|
|
85
|
+
{(item) => (
|
|
86
|
+
<Show
|
|
87
|
+
when={item().href}
|
|
88
|
+
fallback={
|
|
89
|
+
<ArkMenu.Item value={item().value} class={itemClasses}>
|
|
90
|
+
<MenuItemContent item={item()} />
|
|
91
|
+
</ArkMenu.Item>
|
|
92
|
+
}
|
|
93
|
+
>
|
|
94
|
+
{(href) => (
|
|
95
|
+
<ArkMenu.Item
|
|
96
|
+
value={item().value}
|
|
97
|
+
asChild={(itemProps) => (
|
|
98
|
+
<a
|
|
99
|
+
{...itemProps({ class: itemClasses })}
|
|
100
|
+
href={href()}
|
|
101
|
+
target={item().target}
|
|
102
|
+
>
|
|
103
|
+
<MenuItemContent item={item()} />
|
|
104
|
+
</a>
|
|
105
|
+
)}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
</Show>
|
|
109
|
+
)}
|
|
110
|
+
</Match>
|
|
111
|
+
</Switch>
|
|
112
|
+
)}
|
|
113
|
+
</For>
|
|
114
|
+
</ArkMenu.Content>
|
|
115
|
+
</ArkMenu.Positioner>
|
|
116
|
+
</Portal>
|
|
117
|
+
</ArkMenu.Root>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
export { Menu };
|
|
121
|
+
export type { MenuItem, MenuHeader, MenuSeparator, MenuEntry, MenuProps };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ScrollArea as ArkScrollArea } from "@ark-ui/solid/scroll-area";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { type ParentComponent, Show } from "solid-js";
|
|
4
|
+
|
|
5
|
+
interface ScrollAreaProps {
|
|
6
|
+
orientation?: "vertical" | "horizontal";
|
|
7
|
+
class?: string;
|
|
8
|
+
viewportClass?: string;
|
|
9
|
+
contentClass?: string;
|
|
10
|
+
/** Classes of the scrollbar and its thumb, e.g. to change their size or insets. */
|
|
11
|
+
scrollbarClass?: string;
|
|
12
|
+
thumbClass?: string;
|
|
13
|
+
/** Fades the top and bottom edges while content scrolls past them. Vertical only. */
|
|
14
|
+
shadow?: boolean;
|
|
15
|
+
/** The session storage key of a saved scroll position, restored before the first paint. */
|
|
16
|
+
scrollKey?: string;
|
|
17
|
+
viewportRef?(element: HTMLDivElement): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const shadowClass =
|
|
21
|
+
"pointer-events-none absolute inset-x-0 z-1 h-12 from-gray-50 to-gray-50/0 opacity-0 transition-opacity duration-150";
|
|
22
|
+
// Shown while the pointer is over the area, or while it scrolls.
|
|
23
|
+
const scrollbarClass =
|
|
24
|
+
":base: z-10 flex media-touch:hidden opacity-0 transition-opacity duration-200 data-[hover]:opacity-100 data-[scrolling]:opacity-100";
|
|
25
|
+
// A transparent border insets the thumb on any background.
|
|
26
|
+
const thumbClass =
|
|
27
|
+
":base: rounded-lg border-2 border-solid border-transparent bg-gray-200 bg-clip-padding shadow-inner";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Scrolls its content in one direction, with an overlay scrollbar.
|
|
31
|
+
* Ark sets `position: relative` on the root, so position a wrapper instead, e.g. with `sticky`.
|
|
32
|
+
*/
|
|
33
|
+
const ScrollArea: ParentComponent<ScrollAreaProps> = (props) => {
|
|
34
|
+
const horizontal = (): boolean => props.orientation === "horizontal";
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<ArkScrollArea.Root
|
|
38
|
+
class={clsx(
|
|
39
|
+
":base: relative flex min-h-0 overflow-hidden",
|
|
40
|
+
!horizontal() && ":base: flex-col",
|
|
41
|
+
props.class
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
<ArkScrollArea.Viewport
|
|
45
|
+
ref={props.viewportRef}
|
|
46
|
+
data-scroll-key={props.scrollKey}
|
|
47
|
+
class={clsx(
|
|
48
|
+
"peer",
|
|
49
|
+
":base: w-full [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
|
50
|
+
// Touch screens keep their native overlay scrollbars.
|
|
51
|
+
"media-touch:[scrollbar-width:auto] media-touch:[&::-webkit-scrollbar]:block",
|
|
52
|
+
// A percent height does not resolve under a `max-height`, so a column flex item sizes it.
|
|
53
|
+
horizontal()
|
|
54
|
+
? ":base: h-full overflow-y-hidden!"
|
|
55
|
+
: ":base: min-h-0 flex-1 overflow-x-hidden!",
|
|
56
|
+
props.viewportClass
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{/* Ark sets `min-width: fit-content`, which only horizontal scrolling needs. */}
|
|
60
|
+
<ArkScrollArea.Content class={clsx(!horizontal() && ":base: min-w-0!", props.contentClass)}>
|
|
61
|
+
{props.children}
|
|
62
|
+
</ArkScrollArea.Content>
|
|
63
|
+
</ArkScrollArea.Viewport>
|
|
64
|
+
<Show when={props.shadow && !horizontal()}>
|
|
65
|
+
<div
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
class={clsx(
|
|
68
|
+
shadowClass,
|
|
69
|
+
"top-0 bg-gradient-to-b peer-[[data-overflow-y]:not([data-at-top])]:opacity-100"
|
|
70
|
+
)}
|
|
71
|
+
/>
|
|
72
|
+
<div
|
|
73
|
+
aria-hidden="true"
|
|
74
|
+
class={clsx(
|
|
75
|
+
shadowClass,
|
|
76
|
+
"bottom-0 bg-gradient-to-t peer-[[data-overflow-y]:not([data-at-bottom])]:opacity-100"
|
|
77
|
+
)}
|
|
78
|
+
/>
|
|
79
|
+
</Show>
|
|
80
|
+
<ArkScrollArea.Context>
|
|
81
|
+
{(scrollArea) => (
|
|
82
|
+
<Show when={horizontal() ? scrollArea().hasOverflowX : scrollArea().hasOverflowY}>
|
|
83
|
+
<ArkScrollArea.Scrollbar
|
|
84
|
+
orientation={props.orientation ?? "vertical"}
|
|
85
|
+
class={clsx(
|
|
86
|
+
scrollbarClass,
|
|
87
|
+
horizontal() ? ":base: h-3 flex-col px-2.5" : ":base: w-3 py-2.5",
|
|
88
|
+
props.scrollbarClass
|
|
89
|
+
)}
|
|
90
|
+
>
|
|
91
|
+
<ArkScrollArea.Thumb
|
|
92
|
+
class={clsx(thumbClass, horizontal() ? "h-full" : "w-full", props.thumbClass)}
|
|
93
|
+
/>
|
|
94
|
+
</ArkScrollArea.Scrollbar>
|
|
95
|
+
</Show>
|
|
96
|
+
)}
|
|
97
|
+
</ArkScrollArea.Context>
|
|
98
|
+
</ArkScrollArea.Root>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export { ScrollArea };
|
|
103
|
+
export type { ScrollAreaProps };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { type Component, For, Match, Switch } from "solid-js";
|
|
3
|
+
|
|
4
|
+
interface ShortcutProps {
|
|
5
|
+
/** Keys joined with `+`, e.g. `$mod+K`. `$mod` is Command on Apple devices and Ctrl elsewhere. */
|
|
6
|
+
shortcut: string;
|
|
7
|
+
/** Shows `$mod` as Ctrl. */
|
|
8
|
+
ctrl?: boolean;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A keyboard shortcut with key icons, after the Andesine app's shortcut. */
|
|
13
|
+
const Shortcut: Component<ShortcutProps> = (props) => {
|
|
14
|
+
const pieces = (): string[] => {
|
|
15
|
+
return props.shortcut.split("+").map((piece) => {
|
|
16
|
+
const key = piece.toLowerCase().trim();
|
|
17
|
+
|
|
18
|
+
return key === "$mod" && props.ctrl ? "ctrl" : key;
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<kbd class={clsx(":base: flex items-center justify-center font-mono", props.class)}>
|
|
24
|
+
<For each={pieces()}>
|
|
25
|
+
{(piece) => (
|
|
26
|
+
<Switch>
|
|
27
|
+
<Match when={piece === "$mod"}>
|
|
28
|
+
<span class="i-lucide:command inline-block" />
|
|
29
|
+
</Match>
|
|
30
|
+
<Match when={piece === "ctrl"}>
|
|
31
|
+
<span class="pr-0.5 font-light leading-[1]">Ctrl</span>
|
|
32
|
+
</Match>
|
|
33
|
+
<Match when={piece === "alt"}>
|
|
34
|
+
<span class="i-lucide:option inline-block" />
|
|
35
|
+
</Match>
|
|
36
|
+
<Match when={piece === "shift"}>
|
|
37
|
+
<span class="i-lucide:arrow-big-up inline-block" />
|
|
38
|
+
</Match>
|
|
39
|
+
<Match when={piece === "enter"}>
|
|
40
|
+
<span class="i-lucide:corner-down-left inline-block" />
|
|
41
|
+
</Match>
|
|
42
|
+
<Match when={piece === "backspace"}>
|
|
43
|
+
<span class="i-lucide:delete inline-block" />
|
|
44
|
+
</Match>
|
|
45
|
+
<Match when={/^f\d\d?$/.test(piece)}>
|
|
46
|
+
<span class="min-w-3 text-center text-[90%] font-light leading-[1]">
|
|
47
|
+
{piece.toUpperCase()}
|
|
48
|
+
</span>
|
|
49
|
+
</Match>
|
|
50
|
+
<Match when={true}>
|
|
51
|
+
<span class="w-3 text-center font-light leading-[1]">{piece.toUpperCase()}</span>
|
|
52
|
+
</Match>
|
|
53
|
+
</Switch>
|
|
54
|
+
)}
|
|
55
|
+
</For>
|
|
56
|
+
</kbd>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export { Shortcut };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Tooltip as ArkTooltip } from "@ark-ui/solid/tooltip";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import type { JSX, ParentComponent } from "solid-js";
|
|
4
|
+
import { Portal } from "solid-js/web";
|
|
5
|
+
|
|
6
|
+
interface TooltipProps {
|
|
7
|
+
content: JSX.Element;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Tooltip: ParentComponent<TooltipProps> = (props) => (
|
|
12
|
+
<ArkTooltip.Root
|
|
13
|
+
openDelay={500}
|
|
14
|
+
closeDelay={250}
|
|
15
|
+
positioning={{ placement: "bottom", offset: { mainAxis: 6 } }}
|
|
16
|
+
>
|
|
17
|
+
<ArkTooltip.Trigger
|
|
18
|
+
asChild={(triggerProps) => (
|
|
19
|
+
<span {...triggerProps({ class: "inline-flex" })}>{props.children}</span>
|
|
20
|
+
)}
|
|
21
|
+
/>
|
|
22
|
+
<Portal>
|
|
23
|
+
<ArkTooltip.Positioner>
|
|
24
|
+
<ArkTooltip.Content
|
|
25
|
+
class={clsx(
|
|
26
|
+
":base: relative z-80 flex whitespace-nowrap rounded-md bg-gray-800 px-1.5 py-1 text-xs leading-none text-gray-50 shadow-inner shadow-gray-900 ring-1 ring-gray-900 pointer-events-none",
|
|
27
|
+
":base: data-[state=open]:animate-popover-in data-[state=closed]:animate-popover-out",
|
|
28
|
+
props.class
|
|
29
|
+
)}
|
|
30
|
+
>
|
|
31
|
+
{props.content}
|
|
32
|
+
</ArkTooltip.Content>
|
|
33
|
+
</ArkTooltip.Positioner>
|
|
34
|
+
</Portal>
|
|
35
|
+
</ArkTooltip.Root>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export { Tooltip };
|
|
39
|
+
export type { TooltipProps };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Fact } from "@andesine/pages";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
facts: Fact[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { facts } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
facts.length > 0 && (
|
|
13
|
+
<div class="flex flex-wrap gap-x-3 text-base leading-7 text-gray-500">
|
|
14
|
+
{facts.map((fact) => (
|
|
15
|
+
<span>
|
|
16
|
+
<span class="text-sm">{fact.label}:</span>{" "}
|
|
17
|
+
{fact.values.map((value, index) => (
|
|
18
|
+
<>
|
|
19
|
+
{index > 0 && ", "}
|
|
20
|
+
<code>{value}</code>
|
|
21
|
+
</>
|
|
22
|
+
))}
|
|
23
|
+
</span>
|
|
24
|
+
))}
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|