andesine 0.4.0 → 0.6.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 (29) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +66 -0
  3. package/dist/cli.js +2492 -49
  4. package/dist/cli.js.map +1 -1
  5. package/dist/templates/extension/.env.example +5 -0
  6. package/dist/templates/extension/LICENSE +21 -0
  7. package/dist/templates/extension/README.md +39 -0
  8. package/dist/templates/extension/andesine.config.ts +83 -0
  9. package/dist/templates/extension/andesine.keys.json +4 -0
  10. package/dist/templates/extension/gitignore +4 -0
  11. package/dist/templates/extension/package.template.json +27 -0
  12. package/dist/templates/extension/src/backend/server.ts +80 -0
  13. package/dist/templates/extension/src/frontend/count-words-action.tsx +46 -0
  14. package/dist/templates/extension/src/frontend/hello-panel.tsx +93 -0
  15. package/dist/templates/extension/src/frontend/index.tsx +6 -0
  16. package/dist/templates/extension/src/frontend/note-view.tsx +51 -0
  17. package/dist/templates/extension/src/frontend/shout-action.tsx +33 -0
  18. package/dist/templates/extension/src/frontend/wrap-in-note-action.tsx +52 -0
  19. package/dist/templates/extension/tsconfig.json +16 -0
  20. package/dist/templates/pages/package.template.json +3 -3
  21. package/dist/templates/pages/src/components/content/copy-button.tsx +1 -1
  22. package/dist/templates/pages/src/components/layout/call-to-action.tsx +2 -6
  23. package/dist/templates/pages/src/components/layout/footer.astro +1 -2
  24. package/dist/templates/pages/src/components/layout/header.astro +0 -3
  25. package/dist/templates/pages/src/components/layout/mobile-navigation.tsx +1 -1
  26. package/dist/templates/pages/src/components/layout/page-actions.tsx +1 -7
  27. package/dist/templates/pages/src/components/primitives/button.tsx +21 -37
  28. package/dist/templates/pages/src/components/search/search-palette.tsx +1 -3
  29. package/package.json +18 -7
@@ -0,0 +1,5 @@
1
+ # The private key from `andesine extensions keys generate` (a JSON Web Key on one line)
2
+ ANDESINE_EXTENSION_KEY=
3
+ # API URLs of the Andesine instances that the backend works with, separated by commas
4
+ ANDESINE_INSTANCES=https://api.andesine.app
5
+ PORT=3000
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vrite, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,39 @@
1
+ # Andesine extension
2
+
3
+ An Andesine extension with a Solid frontend and a Node backend.
4
+
5
+ - `andesine.config.ts` — the manifest: name, permissions, backend, webhooks, configuration, and views.
6
+ - `andesine.keys.json` — the backend's public keys; `andesine extensions keys` maintains it.
7
+ - `src/frontend` — the views, rendered by Andesine in a sandbox; `index.tsx` exports every view that the manifest names.
8
+ - `src/backend` — the backend; it receives webhooks and verifies frontend sessions.
9
+
10
+ ## Samples
11
+
12
+ Each frontend module shows one kind of contribution; remove the ones you do not need from the manifest and `index.tsx`.
13
+
14
+ - `note-view.tsx` — element views: `<Note>` with its editable content in `ContentSlot`, a tone switch that edits the element's props, and a `<NoteTitle>` descendant view.
15
+ - Block actions for paragraphs and headings; each view decides its UI: `shout-action.tsx` runs without UI (the block menu shows a spinner until it closes), `count-words-action.tsx` shows a `Menu` in the block menu's place with the count and “Insert below”, and `wrap-in-note-action.tsx` asks for the tone in a `Dialog`, then wraps the blocks in a `<Note>`.
16
+ - `hello-panel.tsx` — a right panel for the open entry: configuration, an API call with the member's granted permissions, a call to the backend, and a “More” `Menu` with a copy action and an “About” `Dialog`.
17
+ - `src/backend/server.ts` — lifecycle and `entry.created` webhooks, session verification, and the secret `apiToken` field, read with the extension's credentials.
18
+
19
+ ## Local setup
20
+
21
+ Extensions are developed against a fully local Andesine stack; the hosted app has no development mode.
22
+
23
+ 1. On the local stack, set `PUBLIC_EXTENSIONS_ENABLED=true` and `EXTENSIONS_DEVELOPMENT_ENABLED=true` for the backend and the web app. For webhooks to reach a backend on your machine, also allow local HTTP destinations: `WEBHOOK_ALLOW_HTTP=true` and a `WEBHOOK_DESTINATION_EXCEPTIONS` entry for the backend's origin.
24
+ 2. Sign in the CLI to the local instance (API keys cannot develop extensions): `npx andesine --base-url http://localhost:3333 auth login`.
25
+ 3. Start the backend with `npm run backend`, then `npm run dev`. Only you see and run the development extension; `npm run dev -- --remove` uninstalls it.
26
+
27
+ ## Commands
28
+
29
+ - `npm run build` — validate the manifest and build the frontend, its CSS, and the artifact metadata into `dist/`.
30
+ - `npm run backend` — start the backend with the variables from `.env` (see `.env.example`).
31
+ - `npm run dev` — run the extension on a local Andesine instance for you, reloading it on each change; Ctrl+C stops it and keeps its state. It needs `andesine auth login` against the local instance, points the extension at the local backend (`--backend`, port 3000 as in `.env.example`), and writes a development key to `.env`.
32
+ - `npx andesine extensions test-event <webhook> <type>` — send a sample event to a webhook of the running development extension.
33
+ - `npx andesine extensions keys generate` — create a key pair; the private key never leaves your machine.
34
+ - `npx andesine extensions keys check` — compare the backend's private key with the manifest keys.
35
+ - `npx andesine extensions keys revoke <kid>` — stop listing a key; publish a new version afterwards.
36
+
37
+ Private keys stay in `.andesine/keys` and `.env`, which Git ignores.
38
+
39
+ Publish a version through a pull request to the extension registry.
@@ -0,0 +1,83 @@
1
+ import { defineBuild, defineExtension } from "@andesine/extensions";
2
+ import * as z from "zod";
3
+ // `andesine extensions keys generate` and `keys revoke` maintain this file.
4
+ import backendKeys from "./andesine.keys.json" with { type: "json" };
5
+
6
+ export default defineExtension({
7
+ // Your registry name, e.g. `acme/publish`.
8
+ name: "example/hello",
9
+ version: "0.1.0",
10
+ displayName: "Hello",
11
+ description: "Samples of extension features: views, a panel, an action, settings, and a backend.",
12
+ icon: "i-lucide:hand",
13
+ apiVersion: 1,
14
+ // The API permissions that members approve; the frontend gets them only where members have them.
15
+ permissions: ["read:entries"],
16
+ backend: {
17
+ // The deployed src/backend (HTTPS only); `npm run dev` uses the local one (see package.json).
18
+ url: "https://hello.example.com",
19
+ keys: backendKeys.keys
20
+ },
21
+ webhooks: {
22
+ lifecycle: { events: ["extension.installed", "extension.uninstalled"], path: "/webhooks" },
23
+ entries: { events: ["entry.created"], path: "/webhooks" }
24
+ },
25
+ // Managers fill it in Settings → Extensions; secret fields reach only the backend.
26
+ configuration: z.object({
27
+ greeting: z.string().min(1).max(100).default("Hello").meta({ title: "Greeting" }),
28
+ apiToken: z
29
+ .string()
30
+ .max(200)
31
+ .optional()
32
+ .meta({ title: "API token", description: "Only the backend reads it", secret: true })
33
+ }),
34
+ // Renders `<Note>` elements; `<NoteTitle>` inside a note uses its own view.
35
+ elementViews: [
36
+ {
37
+ id: "note",
38
+ name: "Note",
39
+ // Shown in the editor's slash menu, under Elements.
40
+ description: "A highlighted note with a title and a tone",
41
+ icon: "i-lucide:sticky-note",
42
+ element: "Note",
43
+ entry: "NoteView",
44
+ descendants: [{ element: "NoteTitle", entry: "NoteTitleView" }]
45
+ }
46
+ ],
47
+ // Shown in the block menu of the listed block types; each view decides its own UI.
48
+ blockActions: [
49
+ {
50
+ id: "shout",
51
+ label: "Shout",
52
+ icon: "i-lucide:megaphone",
53
+ blocks: ["paragraph", "heading"],
54
+ entry: "ShoutAction"
55
+ },
56
+ {
57
+ id: "count-words",
58
+ label: "Count words",
59
+ icon: "i-lucide:hash",
60
+ blocks: ["paragraph", "heading"],
61
+ entry: "CountWordsAction"
62
+ },
63
+ {
64
+ id: "wrap-in-note",
65
+ label: "Wrap in Note",
66
+ icon: "i-lucide:sticky-note",
67
+ blocks: ["paragraph", "heading"],
68
+ entry: "WrapInNoteAction"
69
+ }
70
+ ],
71
+ panels: [
72
+ {
73
+ id: "hello",
74
+ side: "right",
75
+ name: "Hello",
76
+ icon: "i-lucide:hand",
77
+ context: "entry",
78
+ entry: "HelloPanel"
79
+ }
80
+ ]
81
+ });
82
+
83
+ export const build = defineBuild({ frontend: "src/frontend/index.tsx" });
@@ -0,0 +1,4 @@
1
+ {
2
+ "keys": [],
3
+ "revokedKeys": []
4
+ }
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ .andesine
4
+ .env
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "andesine-extension",
3
+ "version": "0.6.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "scripts": {
7
+ "build": "andesine extensions build",
8
+ "dev": "andesine extensions dev --backend http://localhost:3000",
9
+ "backend": "node --env-file-if-exists=.env --import tsx src/backend/server.ts",
10
+ "check": "tsc --noEmit"
11
+ },
12
+ "engines": {
13
+ "node": ">=22.12.0"
14
+ },
15
+ "dependencies": {
16
+ "@andesine/extensions": "0.6.0",
17
+ "@andesine/sdk": "0.6.0",
18
+ "solid-js": "^1.9.14",
19
+ "zod": "4.4.3"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "26.1.2",
23
+ "andesine": "0.6.0",
24
+ "tsx": "^4.23.5",
25
+ "typescript": "6.0.3"
26
+ }
27
+ }
@@ -0,0 +1,80 @@
1
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2
+ import {
3
+ createExtensionBackend,
4
+ ExtensionNotificationError,
5
+ ExtensionSessionError
6
+ } from "@andesine/sdk/extensions";
7
+
8
+ const extension = createExtensionBackend({
9
+ name: "example/hello",
10
+ // The private key from `andesine extensions keys generate`, as a JSON Web Key.
11
+ privateKey: process.env.ANDESINE_EXTENSION_KEY ?? "",
12
+ // API URLs of the Andesine instances that this backend works with.
13
+ trustedInstances: (process.env.ANDESINE_INSTANCES ?? "https://api.andesine.app").split(",")
14
+ });
15
+
16
+ const readBody = async (request: IncomingMessage): Promise<string> => {
17
+ const chunks: Buffer[] = [];
18
+
19
+ for await (const chunk of request) chunks.push(chunk as Buffer);
20
+
21
+ return Buffer.concat(chunks).toString("utf8");
22
+ };
23
+ const send = (response: ServerResponse, status: number, body?: unknown): void => {
24
+ response.writeHead(status, body === undefined ? {} : { "content-type": "application/json" });
25
+ response.end(body === undefined ? undefined : JSON.stringify(body));
26
+ };
27
+ // The extension frontend calls the backend from the browser, so it needs CORS.
28
+ const allowFrontend = (response: ServerResponse): void => {
29
+ response.setHeader("access-control-allow-origin", "*");
30
+ response.setHeader("access-control-allow-methods", "POST, OPTIONS");
31
+ response.setHeader(
32
+ "access-control-allow-headers",
33
+ "authorization, content-type, andesine-extension, andesine-instance"
34
+ );
35
+ };
36
+ const server = createServer(async (request, response) => {
37
+ allowFrontend(response);
38
+
39
+ try {
40
+ if (request.method === "OPTIONS") {
41
+ send(response, 204);
42
+ } else if (request.method === "POST" && request.url === "/webhooks") {
43
+ // Fetching the delivery with the extension's key confirms the notification.
44
+ const { event, client } = await extension.receive(await readBody(request));
45
+
46
+ if (event.type === "entry.created") {
47
+ // The client acts for the installation, with the extension's permissions.
48
+ const entry = await client.entries.get({ id: event.subject.id });
49
+
50
+ console.log(`Entry created: ${entry.name}`);
51
+ } else {
52
+ console.log(`Received ${event.type}`);
53
+ }
54
+
55
+ send(response, 204);
56
+ } else if (request.method === "POST" && request.url === "/hello") {
57
+ const session = await extension.verifySession(request.headers);
58
+ // Secret fields are readable only here, with the extension's own credentials.
59
+ const { values } = await session.client.extensions.getSelfConfiguration();
60
+
61
+ console.log(`API token ${values.apiToken ? "set" : "not set"}`);
62
+ send(response, 200, {
63
+ name: session.member.profile.name ?? session.member.profile.email ?? "member"
64
+ });
65
+ } else {
66
+ send(response, 404);
67
+ }
68
+ } catch (error) {
69
+ const isRejected =
70
+ error instanceof ExtensionSessionError || error instanceof ExtensionNotificationError;
71
+
72
+ if (!isRejected) console.error(error);
73
+
74
+ send(response, isRejected ? 401 : 500);
75
+ }
76
+ });
77
+
78
+ server.listen(Number(process.env.PORT ?? 3000), () => {
79
+ console.log(`Backend listening on port ${process.env.PORT ?? 3000}`);
80
+ });
@@ -0,0 +1,46 @@
1
+ import {
2
+ type ContentNode,
3
+ Menu,
4
+ MenuContent,
5
+ MenuItem,
6
+ MenuSeparator,
7
+ Text,
8
+ useEditor
9
+ } from "@andesine/extensions/solid";
10
+
11
+ interface BlockActionProps {
12
+ blocks: ContentNode[];
13
+ }
14
+
15
+ const countWords = (node: ContentNode): number => {
16
+ const own = node.text?.split(/\s+/).filter(Boolean).length ?? 0;
17
+
18
+ return own + (node.content ?? []).reduce((total, child) => total + countWords(child), 0);
19
+ };
20
+
21
+ /**
22
+ * A block action menu where the block menu was: a custom item with the count and a structured
23
+ * item. Escape or a click outside closes it; after a choice, the action runs until it closes.
24
+ */
25
+ export const CountWordsAction = (props: BlockActionProps) => {
26
+ const editor = useEditor();
27
+ const words = () => props.blocks.reduce((total, block) => total + countWords(block), 0);
28
+ const insert = async () => {
29
+ await editor.insertAfter([
30
+ { type: "paragraph", content: [{ type: "text", text: `${words()} words.` }] }
31
+ ]);
32
+ await editor.close();
33
+ };
34
+
35
+ return (
36
+ <Menu size="medium">
37
+ <MenuContent>
38
+ <Text size="sm" tone="muted" class="px-2 py-1">
39
+ {words()} words
40
+ </Text>
41
+ </MenuContent>
42
+ <MenuSeparator />
43
+ <MenuItem label="Insert below" icon="i-lucide:list-plus" onSelect={insert} />
44
+ </Menu>
45
+ );
46
+ };
@@ -0,0 +1,93 @@
1
+ import { createClient } from "@andesine/sdk";
2
+ import {
3
+ apiFetch,
4
+ backend,
5
+ Button,
6
+ copyText,
7
+ Dialog,
8
+ EXTENSION_API_URL,
9
+ hasPermission,
10
+ IconButton,
11
+ Menu,
12
+ MenuItem,
13
+ MenuTrigger,
14
+ notify,
15
+ Stack,
16
+ Text,
17
+ useConfiguration
18
+ } from "@andesine/extensions/solid";
19
+ import { createResource, createSignal, Show } from "solid-js";
20
+
21
+ interface HelloPanelProps {
22
+ /** Right panels with the `entry` context get the open entry's ID. */
23
+ entryID?: string;
24
+ }
25
+
26
+ // API calls go through Andesine with the member's session, limited to the extension's grant.
27
+ const client = createClient({ baseURL: EXTENSION_API_URL, fetch: apiFetch });
28
+
29
+ /** The right panel: reads the open entry through the API, then asks the backend who you are. */
30
+ export const HelloPanel = (props: HelloPanelProps) => {
31
+ const configuration = useConfiguration<{ greeting: string }>();
32
+ const [name, setName] = createSignal<string | null>(null);
33
+ const [failed, setFailed] = createSignal(false);
34
+ const [aboutOpened, setAboutOpened] = createSignal(false);
35
+ const [entry] = createResource(
36
+ () => (hasPermission("read:entries") ? props.entryID : undefined),
37
+ // A failed request would end the view, so the entry line stays hidden instead.
38
+ (id) => client.entries.get({ id }).catch(() => null)
39
+ );
40
+ const greeting = () => `${configuration().greeting ?? "Hello"}, ${name() ?? "member"}!`;
41
+ const copyGreeting = async () => {
42
+ await copyText(greeting());
43
+ await notify("Greeting copied");
44
+ };
45
+ const askBackend = async () => {
46
+ try {
47
+ const response = await backend.fetch("/hello", { method: "POST" });
48
+ const result = (await response.json()) as { name: string };
49
+
50
+ setName(result.name);
51
+ } catch {
52
+ setFailed(true);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <Stack gap="small">
58
+ <Stack direction="row" justify="between" align="center">
59
+ <Text>{greeting()}</Text>
60
+ {/* Menus and dialogs work in any view; a dialog opens only right after an interaction. */}
61
+ <Menu>
62
+ <MenuTrigger>
63
+ <IconButton icon="i-lucide:ellipsis" label="More" />
64
+ </MenuTrigger>
65
+ <MenuItem label="Copy greeting" icon="i-lucide:copy" onSelect={copyGreeting} />
66
+ <MenuItem
67
+ label="About this panel"
68
+ icon="i-lucide:info"
69
+ onSelect={() => setAboutOpened(true)}
70
+ />
71
+ </Menu>
72
+ </Stack>
73
+ <Show when={aboutOpened()}>
74
+ <Dialog title="Hello panel" size="small" onClose={() => setAboutOpened(false)}>
75
+ <Text size="sm">
76
+ A sample panel: it reads the open entry and asks the extension's backend who you are.
77
+ </Text>
78
+ </Dialog>
79
+ </Show>
80
+ <Show when={entry()}>
81
+ {(loaded) => (
82
+ <Text size="sm" tone="muted">
83
+ Entry: {loaded().name}
84
+ </Text>
85
+ )}
86
+ </Show>
87
+ <Button onClick={askBackend}>Ask the backend</Button>
88
+ <Show when={failed()}>
89
+ <Text tone="danger">The backend did not answer.</Text>
90
+ </Show>
91
+ </Stack>
92
+ );
93
+ };
@@ -0,0 +1,6 @@
1
+ // Every view entry of the manifest is a named export of this module.
2
+ export { HelloPanel } from "./hello-panel";
3
+ export { NoteTitleView, NoteView } from "./note-view";
4
+ export { CountWordsAction } from "./count-words-action";
5
+ export { ShoutAction } from "./shout-action";
6
+ export { WrapInNoteAction } from "./wrap-in-note-action";
@@ -0,0 +1,51 @@
1
+ import {
2
+ Box,
3
+ ContentSlot,
4
+ Icon,
5
+ Stack,
6
+ Text,
7
+ ToggleGroup,
8
+ useEditor
9
+ } from "@andesine/extensions/solid";
10
+
11
+ interface ElementViewProps {
12
+ /** The element name, e.g. `Note`. */
13
+ element: string;
14
+ /** The element's props from the document. */
15
+ props: { tone?: string };
16
+ }
17
+
18
+ const tones = [
19
+ { value: "info", label: "Info" },
20
+ { value: "warning", label: "Warning" }
21
+ ];
22
+
23
+ /** The `<Note>` view: `ContentSlot` shows the editable content; the toggle edits element props. */
24
+ export const NoteView = (props: ElementViewProps) => {
25
+ const editor = useEditor();
26
+ const tone = () => props.props.tone ?? "info";
27
+
28
+ return (
29
+ <Box padding="medium" background="soft" border class="rounded-lg">
30
+ <Stack gap="small">
31
+ <Stack direction="row" justify="between" align="center">
32
+ <Icon name={tone() === "warning" ? "i-lucide:triangle-alert" : "i-lucide:info"} />
33
+ <ToggleGroup
34
+ value={tone()}
35
+ options={tones}
36
+ onChange={(value) => editor.setElementProps({ tone: value })}
37
+ />
38
+ </Stack>
39
+ <ContentSlot />
40
+ </Stack>
41
+ </Box>
42
+ );
43
+ };
44
+ /** The view of `<NoteTitle>` elements inside a note, from the note view's descendants. */
45
+ export const NoteTitleView = () => {
46
+ return (
47
+ <Text weight="semibold" class="uppercase tracking-wide">
48
+ <ContentSlot />
49
+ </Text>
50
+ );
51
+ };
@@ -0,0 +1,33 @@
1
+ import { type ContentNode, useEditor } from "@andesine/extensions/solid";
2
+ import { onMount } from "solid-js";
3
+
4
+ interface BlockActionProps {
5
+ /** The selected blocks as editor JSON. */
6
+ blocks: ContentNode[];
7
+ }
8
+
9
+ const shout = (node: ContentNode): ContentNode => {
10
+ return {
11
+ ...node,
12
+ ...(node.text !== undefined && { text: node.text.toUpperCase() }),
13
+ ...(node.content && { content: node.content.map(shout) })
14
+ };
15
+ };
16
+
17
+ /**
18
+ * A block action without UI: it renders nothing, so the block menu shows a spinner until it
19
+ * closes. Andesine shows an error if the edit fails.
20
+ */
21
+ export const ShoutAction = (props: BlockActionProps) => {
22
+ const editor = useEditor();
23
+
24
+ onMount(async () => {
25
+ try {
26
+ await editor.replace(props.blocks.map(shout));
27
+ } finally {
28
+ await editor.close();
29
+ }
30
+ });
31
+
32
+ return null;
33
+ };
@@ -0,0 +1,52 @@
1
+ import {
2
+ Button,
3
+ type ContentNode,
4
+ Dialog,
5
+ Stack,
6
+ Text,
7
+ ToggleGroup,
8
+ useEditor
9
+ } from "@andesine/extensions/solid";
10
+ import { createSignal, Show } from "solid-js";
11
+
12
+ interface BlockActionProps {
13
+ blocks: ContentNode[];
14
+ }
15
+
16
+ const tones = [
17
+ { value: "info", label: "Info" },
18
+ { value: "warning", label: "Warning" }
19
+ ];
20
+
21
+ /** A block action in a dialog: it asks for the tone, then wraps the blocks in a `<Note>`. */
22
+ export const WrapInNoteAction = (props: BlockActionProps) => {
23
+ const editor = useEditor();
24
+ const [tone, setTone] = createSignal("info");
25
+ const [error, setError] = createSignal<string | null>(null);
26
+ const wrap = async () => {
27
+ try {
28
+ // Andesine derives the element's tag from its name and props.
29
+ await editor.replace([
30
+ { type: "element", attrs: { name: "Note", props: { tone: tone() } }, content: props.blocks }
31
+ ]);
32
+ await editor.close();
33
+ } catch (failure) {
34
+ setError(failure instanceof Error ? failure.message : "The edit failed");
35
+ }
36
+ };
37
+
38
+ return (
39
+ <Dialog title="Wrap in Note" description="Choose the note's tone." size="small">
40
+ <Stack gap="medium">
41
+ <ToggleGroup value={tone()} options={tones} onChange={setTone} />
42
+ <Show when={error()}>{(message) => <Text tone="danger">{message()}</Text>}</Show>
43
+ <Stack direction="row" gap="small" justify="end">
44
+ <Button onClick={() => editor.close()}>Cancel</Button>
45
+ <Button variant="primary" onClick={wrap}>
46
+ Wrap
47
+ </Button>
48
+ </Stack>
49
+ </Stack>
50
+ </Dialog>
51
+ );
52
+ };
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM"],
7
+ "jsx": "preserve",
8
+ "jsxImportSource": "solid-js",
9
+ "strict": true,
10
+ "noEmit": true,
11
+ "skipLibCheck": true,
12
+ "resolveJsonModule": true,
13
+ "types": ["node"]
14
+ },
15
+ "include": ["andesine.config.ts", "src"]
16
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "andesine-pages-site",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -13,8 +13,8 @@
13
13
  "node": ">=24"
14
14
  },
15
15
  "dependencies": {
16
- "@andesine/pages": "0.4.0",
17
- "@andesine/ui": "0.4.0",
16
+ "@andesine/pages": "0.6.0",
17
+ "@andesine/ui": "0.6.0",
18
18
  "@ark-ui/solid": "5.38.0",
19
19
  "@astrojs/mdx": "8.0.1",
20
20
  "@astrojs/node": "11.1.6",
@@ -36,7 +36,7 @@ const CopyButton: Component<CopyButtonProps> = (props) => {
36
36
  <>
37
37
  <IconButton
38
38
  icon={icon()}
39
- variant="text"
39
+ variant="ghost"
40
40
  size="xs"
41
41
  text="softer"
42
42
  aria-label={props.label ?? "Copy code"}
@@ -13,16 +13,12 @@ const CallToAction: Component<CallToActionProps> = (props) =>
13
13
  link={props.link.href}
14
14
  icon={props.link.icon}
15
15
  label={props.link.label}
16
- color="primary"
17
- variant="outlined"
18
- size="small"
16
+ variant="primary"
19
17
  class="flex-row-reverse gap-1 pr-1.5"
20
18
  iconClass="opacity-50 h-4.5 w-4.5"
21
19
  />
22
20
  ) : (
23
- <Button link={props.link.href} color="primary" variant="outlined" size="small">
24
- {props.link.label}
25
- </Button>
21
+ <Button link={props.link.href}>{props.link.label}</Button>
26
22
  );
27
23
 
28
24
  export { CallToAction };
@@ -31,9 +31,8 @@ const updated =
31
31
  <IconButton
32
32
  link={link.href}
33
33
  icon={link.icon}
34
- variant="text"
34
+ variant="ghost"
35
35
  text="softer"
36
- size="small"
37
36
  aria-label={link.label}
38
37
  title={link.label}
39
38
  />
@@ -33,9 +33,6 @@ const { site } = page;
33
33
  <IconButton
34
34
  data-search-trigger
35
35
  icon="i-tabler:search"
36
- color="contrast"
37
- variant="outlined"
38
- size="small"
39
36
  text="softer"
40
37
  class="md:hidden"
41
38
  aria-label="Search"
@@ -25,7 +25,7 @@ const MobileNavigation: Component<MobileNavigationProps> = (props) => (
25
25
  aria-label="Navigation"
26
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
27
  >
28
- <Button badge color="contrast" variant="outlined" text="softer" class="p-1.5">
28
+ <Button badge variant="secondary" text="softer" size="medium" class="p-1.5">
29
29
  <span aria-hidden="true" class="i-lucide:menu h-5 w-5 group-open/menu:hidden" />
30
30
  <span aria-hidden="true" class="i-lucide:x hidden h-5 w-5 group-open/menu:block" />
31
31
  </Button>