@getvitops/emdash 1.0.0-alpha.0 → 1.0.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 +210 -3
- package/README.md +66 -21
- package/dist/index.d.mts +49 -17
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +368 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -13
- package/src/astro/blocks/Accordion.astro +39 -0
- package/src/astro/blocks/ActionLink.astro +4 -2
- package/src/astro/blocks/Cards.astro +52 -0
- package/src/astro/blocks/Carousel.astro +25 -8
- package/src/astro/blocks/Gallery.astro +40 -0
- package/src/astro/blocks/Hero.astro +61 -0
- package/src/astro/blocks/ImageCompare.astro +5 -2
- package/src/astro/blocks/Marquee.astro +38 -0
- package/src/astro/blocks/value.test.ts +21 -0
- package/src/astro/blocks/value.ts +26 -1
- package/src/astro/index.ts +10 -0
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["pkg.version"],"sources":["../src/icon-options.ts","../src/blocks.ts","../src/design-system-route.ts","../src/hosting.ts","../package.json","../src/index.ts"],"sourcesContent":["/**\n * Semantic icon names, as Block Kit combobox options.\n *\n * Derived from `@getvitops/utils`' `iconMap` at import time. This used to be a\n * generated copy, so that the package carried no runtime `@getvitops/*`\n * dependency and could version independently of the fixed toolchain group —\n * but that independence was already fiction: the blocks render from the\n * toolchain's SVG sprite (`<use href=\"…/icons.svg#icon-menu\">`), so an emdash\n * built against one icon vocabulary and a sprite built against another produce\n * empty boxes, silently and with no install-time error. Depending on the real\n * map makes the vocabulary one thing rather than two that can drift.\n *\n * `fa7` is the reference collection: every semantic name is defined there, and\n * the other sets are alternate resolutions of the same names.\n */\nimport { iconMap } from '@getvitops/utils';\n\nexport const SEMANTIC_ICON_OPTIONS: { label: string; value: string }[] = Object.keys(iconMap.fa7)\n .sort()\n // Label and value are the same string on purpose: the editor picks a semantic\n // name, and showing the set-specific glyph id would leak the icon set into\n // authored content — the thing the semantic layer exists to prevent.\n .map((name) => ({ label: name, value: name }));\n","/**\n * Portable Text block-type declarations for the EmDash editor.\n *\n * v1 exposes only Vitops patterns whose data model fits EmDash's flat Block Kit\n * fields (text_input / number_input / select / toggle). Repeating structured\n * patterns (Cards, wc-entries, FAQ, forms) go through EmDash Sections or\n * Field Kit `list` widgets on json collection fields instead — see the README.\n *\n * Every `action_id` here must have a matching prop consumed by the block's\n * .astro component in `src/astro/blocks/` (enforced by blocks.test.ts).\n */\n\nimport type { PortableTextBlockConfig } from 'emdash';\nimport { SEMANTIC_ICON_OPTIONS } from './icon-options.ts';\n\n/** The v1 subset of EmDash Block Kit field elements used by these blocks. */\nexport type BlockKitFieldDef = Extract<\n NonNullable<PortableTextBlockConfig['fields']>[number],\n { type: 'text_input' | 'number_input' | 'select' | 'toggle' | 'combobox' }\n>;\n\nexport type PortableTextBlockDef = Omit<PortableTextBlockConfig, 'fields'> & {\n fields?: BlockKitFieldDef[];\n};\n\nexport const BLOCKS: PortableTextBlockDef[] = [\n {\n /**\n * A link or button, optionally with an icon at either end.\n *\n * Flattened on purpose: Block Kit has no object-group element, so a shape\n * like `{ label, url }` becomes sibling fields. Same constraint the\n * marketing-blocks plugin documents.\n *\n * The icons are `combobox` — a searchable typeahead over a STATIC option\n * list, which is exactly what the semantic name list is. It is the closest\n * fit Block Kit offers: its element union is closed (no custom element, no\n * raw HTML), so `<wc-icon-picker>` cannot be mounted inside a block modal\n * however much nicer it would be. Revisit if EmDash opens that union.\n */\n type: 'vitops.actionLink',\n label: 'Link or button',\n icon: 'link',\n placeholder: 'A link or button, with optional icons',\n fields: [\n { type: 'text_input', action_id: 'label', label: 'Label' },\n { type: 'text_input', action_id: 'href', label: 'URL' },\n {\n type: 'select',\n action_id: 'variant',\n label: 'Style',\n initial_value: 'link',\n options: [\n { label: 'Link', value: 'link' },\n { label: 'Button', value: 'btn' },\n { label: 'Call to action', value: 'cta' },\n ],\n },\n {\n type: 'combobox',\n action_id: 'startIcon',\n label: 'Icon before the label',\n placeholder: 'None',\n options: SEMANTIC_ICON_OPTIONS,\n },\n {\n type: 'combobox',\n action_id: 'endIcon',\n label: 'Icon after the label',\n placeholder: 'None',\n options: SEMANTIC_ICON_OPTIONS,\n },\n {\n type: 'select',\n action_id: 'role',\n label: 'Colour role',\n initial_value: '',\n options: [\n { label: 'Default', value: '' },\n { label: 'Success', value: 'success' },\n { label: 'Danger', value: 'danger' },\n { label: 'Warning', value: 'warning' },\n { label: 'Info', value: 'info' },\n ],\n },\n { type: 'toggle', action_id: 'newTab', label: 'Open in a new tab', initial_value: false },\n ],\n },\n {\n type: 'vitops.imageCompare',\n label: 'Image compare',\n placeholder: 'Before/after image slider',\n fields: [\n { type: 'text_input', action_id: 'before', label: 'Before image URL' },\n { type: 'text_input', action_id: 'beforeAlt', label: 'Before alt text' },\n {\n type: 'text_input',\n action_id: 'beforeLabel',\n label: 'Before label',\n initial_value: 'Before',\n },\n { type: 'text_input', action_id: 'after', label: 'After image URL' },\n { type: 'text_input', action_id: 'afterAlt', label: 'After alt text' },\n { type: 'text_input', action_id: 'afterLabel', label: 'After label', initial_value: 'After' },\n ],\n },\n {\n type: 'vitops.copyButton',\n label: 'Copy snippet',\n icon: 'code',\n placeholder: 'Text with a copy-to-clipboard button',\n fields: [\n { type: 'text_input', action_id: 'text', label: 'Text to copy', multiline: true },\n { type: 'text_input', action_id: 'label', label: 'Button label', initial_value: 'Copy' },\n {\n type: 'select',\n action_id: 'display',\n label: 'Display',\n initial_value: 'code-block',\n options: [\n { label: 'Code block', value: 'code-block' },\n { label: 'Inline', value: 'inline' },\n ],\n },\n ],\n },\n {\n type: 'vitops.banner',\n label: 'Banner',\n placeholder: 'Announcement or notice',\n fields: [\n { type: 'text_input', action_id: 'message', label: 'Message', multiline: true },\n {\n type: 'select',\n action_id: 'tone',\n label: 'Tone',\n initial_value: 'info',\n options: [\n { label: 'Info', value: 'info' },\n { label: 'Success', value: 'success' },\n { label: 'Warning', value: 'warning' },\n { label: 'Danger', value: 'danger' },\n ],\n },\n { type: 'toggle', action_id: 'dismissible', label: 'Dismissible', initial_value: true },\n ],\n },\n {\n type: 'vitops.details',\n label: 'Disclosure',\n placeholder: 'Collapsible details/summary',\n fields: [\n { type: 'text_input', action_id: 'summary', label: 'Summary (always visible)' },\n { type: 'text_input', action_id: 'body', label: 'Body (revealed)', multiline: true },\n { type: 'toggle', action_id: 'open', label: 'Open by default', initial_value: false },\n ],\n },\n {\n type: 'vitops.carousel',\n label: 'Carousel',\n placeholder: 'Image carousel',\n fields: [\n {\n type: 'text_input',\n action_id: 'images',\n label: 'Image URLs (one per line)',\n multiline: true,\n },\n {\n type: 'text_input',\n action_id: 'alts',\n label: 'Alt texts (one per line, same order)',\n multiline: true,\n },\n {\n type: 'text_input',\n action_id: 'label',\n label: 'Accessible name',\n initial_value: 'Carousel',\n },\n {\n type: 'number_input',\n action_id: 'autoplay',\n label: 'Autoplay interval (ms, 0 = off)',\n initial_value: 0,\n },\n {\n type: 'toggle',\n action_id: 'loop',\n label: 'Loop (clones the slides — autoplay implies it)',\n initial_value: false,\n },\n {\n type: 'text_input',\n action_id: 'aspect',\n label: 'Slide aspect ratio',\n initial_value: '16 / 9',\n },\n ],\n },\n];\n","/**\n * The `vitops.design-system` plugin route — what `<wc-theme-editor>` probes\n * and POSTs its patch to when mounted inside the EmDash admin (see this\n * package's README and AGENTS.md's \"EmDash plugin as the design-system\n * editing surface\" section). Mounted at\n * `/_emdash/api/plugins/vitops/design-system`.\n *\n * Two responsibilities, matching the dev-only `@getvitops/vite` endpoint's\n * contract byte-for-byte (`GET → { writable }`, `POST { patch } → { ok } |\n * { errors }`) so `<wc-theme-editor>` needs no branch for which one it's\n * talking to:\n *\n * 1. **Immediate**: persist the patch to `ctx.settings` — read back by the\n * `page:fragments` hook in `index.ts`, which turns it into a site-wide\n * `:root` override `<style>` on every public page. This is what makes an\n * edit visible without a rebuild.\n * 2. **Canonical**: auto-commit the merged file to `site.deployment.branch`\n * (default `dev`) via `@getvitops/utils/git`'s `commitFile()`, so the edit\n * reaches the git-tracked `site.json` through this repo's own normal\n * `dev → promote → main` review path — never a silent second source of\n * truth. A commit failure (missing token, misconfigured repo, a transient\n * GitHub error) is logged and surfaces as a warning, but never fails the\n * save: the immediate override already works regardless.\n *\n * **Assumes `config` is a full site `Config`** (has `organization`/`site`),\n * not a bare `design-system.json` — true for any real EmDash site (it always\n * has a `site.json`), and the only case this plugin already handles anywhere\n * else (`vitopsHosting()`, `BLOCKS`).\n *\n * The GitHub token is read as `process.env.VITOPS_GITHUB_TOKEN` — matching\n * `hosting.ts`'s own `process.env.HOSTING` read in this same package, rather\n * than a `cloudflare:workers`-specific import: a native EmDash plugin runs\n * in-process with no isolation boundary (EmDash's own docs say plugins \"can\n * access environment variables\" directly), and this package already targets\n * both the Cloudflare and Node hosting targets through one `process.env`\n * convention (`nodejs_compat` on Workers maps bindings into `process.env`).\n */\nimport { pluginResponse } from 'emdash';\nimport type { PageFragmentContribution, PluginContext, PluginRoute, RouteContext } from 'emdash';\nimport {\n designSystemPath,\n formatConfig,\n getAt,\n isConfig,\n mergePatch,\n resolveInput,\n setAt,\n} from '@getvitops/generator/config';\nimport {\n buildReverseIndex,\n patchToOverrideCss,\n validateColorContract,\n} from '@getvitops/generator/tokens';\nimport { commitFile } from '@getvitops/utils/git';\n\nexport interface DesignSystemEditorOptions {\n /**\n * The site's own config, typically `import config from '../../site.json'\n * with { type: 'json' }`. A static, build-time-bundled value — this plugin\n * never reads it from disk at runtime (a deployed Worker has no\n * filesystem), so an edit doesn't take effect here until the *next*\n * deploy; that's exactly the point of the auto-commit half.\n */\n config: unknown;\n /** Which `designSystem.themes` entry this edits. Default: `'default'`. */\n theme?: string;\n}\n\nconst SETTINGS_KEY = 'designSystemPatch';\n\ninterface StoredPatch {\n patch: Record<string, unknown>;\n at: number;\n}\n\nfunction json(status: number, body: unknown) {\n return pluginResponse({\n status,\n headers: { 'content-type': 'application/json' },\n body: { kind: 'text', value: JSON.stringify(body) },\n });\n}\n\n/** Fetches the currently-stored patch, clearing it if the bundled `config`\n * (the last real build) already reflects every value in it — see the\n * module docblock's \"self-clearing\" note in AGENTS.md. */\nasync function currentPatch(\n ctx: Pick<RouteContext, 'settings'>,\n ds: { colors: Record<string, unknown> },\n): Promise<StoredPatch | null> {\n const stored = await ctx.settings.get<StoredPatch>(SETTINGS_KEY);\n if (!stored) return null;\n const patchedColors = stored.patch['colors'];\n if (\n patchedColors != null &&\n JSON.stringify(mergePatch(ds.colors, patchedColors)) === JSON.stringify(ds.colors)\n ) {\n await ctx.settings.delete(SETTINGS_KEY);\n return null;\n }\n return stored;\n}\n\nexport function createDesignSystemRoute(opts: DesignSystemEditorOptions): PluginRoute {\n const theme = opts.theme ?? 'default';\n\n return {\n methods: ['GET', 'POST'],\n response: 'raw',\n permission: 'plugins:manage',\n handler: async (ctx: RouteContext) => {\n // `resolveInput` handles both a bare design-system.json and a full site\n // Config (following `extends`, resolving `themes[theme]`), the same\n // entry point every other consumer of a config goes through.\n const { designSystem: ds, config } = resolveInput(opts.config, {\n theme,\n siteEnv: 'production',\n });\n\n if (ctx.request.method === 'GET') {\n await currentPatch(ctx, ds);\n return json(200, { writable: true });\n }\n\n const body = ctx.input as { patch?: unknown } | undefined;\n const patch = body?.patch;\n if (patch == null || typeof patch !== 'object' || Array.isArray(patch))\n return json(400, { errors: ['body must be { patch: object }'] });\n\n const mergedColors = mergePatch(\n ds.colors,\n (patch as Record<string, unknown>)['colors'] ?? {},\n );\n const contrastErrors = validateColorContract(\n mergedColors as Parameters<typeof validateColorContract>[0],\n );\n if (contrastErrors.length) return json(400, { errors: contrastErrors });\n\n await ctx.settings.set(SETTINGS_KEY, {\n patch: patch as Record<string, unknown>,\n at: Date.now(),\n } satisfies StoredPatch);\n\n const warnings: string[] = [];\n const repoSpec = config?.site.deployment?.repo;\n const token = process.env['VITOPS_GITHUB_TOKEN'];\n if (!repoSpec || !token) {\n warnings.push(\n !repoSpec\n ? 'design-system sync skipped: site.deployment.repo is not set — the change is live but not yet synced to source.'\n : 'design-system sync skipped: no GitHub token configured — the change is live but not yet synced to source.',\n );\n } else {\n const [owner, repo] = repoSpec.split('/');\n if (!owner || !repo) {\n warnings.push(\n `design-system sync skipped: site.deployment.repo \"${repoSpec}\" isn't \"owner/name\".`,\n );\n } else if (!isConfig(opts.config)) {\n warnings.push('design-system sync skipped: config is not a recognised site Config.');\n } else {\n const at = designSystemPath(opts.config, theme);\n const mergedSubtree = mergePatch(getAt(opts.config, at), patch);\n const content = `${formatConfig(setAt(opts.config, at, mergedSubtree))}\\n`;\n const result = await commitFile({\n owner,\n repo,\n branch: config.site.deployment?.branch ?? 'dev',\n path: 'site.json',\n content,\n message: 'Sync design-system edit from EmDash admin',\n token,\n fetchImpl:\n ctx.http && ((url: string, init?: RequestInit) => ctx.http!.fetch(url, init)),\n });\n if (!result.ok) {\n ctx.log.warn('design-system sync commit failed', { message: result.message });\n warnings.push(`design-system sync failed (change is still live): ${result.message}`);\n }\n }\n }\n\n return json(200, { ok: true, warnings });\n },\n };\n}\n\n/**\n * The `page:fragments` contribution serving the stored patch's override\n * CSS site-wide. Returns `null` (no fragment) when there's nothing unsynced\n * — the same \"no gated tag, no cost\" discipline `@getvitops/astro`'s consent\n * gate already follows.\n */\nexport async function designSystemOverrideFragment(\n ctx: Pick<PluginContext, 'settings'>,\n opts: DesignSystemEditorOptions,\n): Promise<PageFragmentContribution | null> {\n const { designSystem: ds } = resolveInput(opts.config, { theme: opts.theme ?? 'default' });\n const stored = await currentPatch(ctx, ds);\n if (!stored) return null;\n\n const css = patchToOverrideCss(stored.patch, buildReverseIndex(ds));\n if (!css) return null;\n return {\n kind: 'html',\n placement: 'head',\n html: `<style id=\"ds-overrides\">${css}</style>`,\n key: 'vitops-ds-overrides',\n };\n}\n","/**\n * Hosting seam — one call in astro.config.mjs resolves the adapter + EmDash\n * database/storage for the chosen hosting target, so a site can start on\n * Cloudflare and move to a Node host (VPS / docker-compose / k8s) — or back —\n * by flipping a single value instead of rewiring its config:\n *\n * ```js\n * import { vitopsEmdash, vitopsHosting } from '@getvitops/emdash';\n * const { adapter, database, storage } = await vitopsHosting();\n * export default defineConfig({\n * output: 'server',\n * adapter,\n * integrations: [react(), emdash({ database, storage, plugins: [vitopsEmdash()] })],\n * });\n * ```\n *\n * Targets:\n * - `'cloudflare'` (default) — `@astrojs/cloudflare` + D1/R2 bindings.\n * Needs `@astrojs/cloudflare` and `@emdash-cms/cloudflare` installed.\n * - `'node'` — `@astrojs/node` (standalone) + SQLite file + local uploads.\n * Needs `@astrojs/node` and `better-sqlite3` installed. Scheduled publishing\n * runs in-process on Node (no worker/cron trigger needed). For production,\n * move storage to S3-compatible (`s3()` from `emdash/astro`) and/or the\n * database to Postgres (`postgres()` from `emdash/db`) via `options.node`.\n *\n * The adapter packages are optional peer dependencies, resolved lazily — a\n * site only installs the stack for the target it uses. Content lives in the\n * database and media in the storage backend, so switching hosts is a data\n * migration (D1 export / EmDash seed round-trip + media copy), not a rewrite.\n */\nimport type { AstroIntegration } from 'astro';\n\nexport type HostingTarget = 'cloudflare' | 'node';\n\nconst TARGETS: HostingTarget[] = ['cloudflare', 'node'];\n\nexport interface VitopsHostingOptions {\n /** Hosting target. Overridden by the `HOSTING` env var. Default 'cloudflare'. */\n target?: HostingTarget;\n cloudflare?: {\n /** D1 binding name in wrangler config (default 'DB'). */\n dbBinding?: string;\n /** R2 binding name in wrangler config (default 'MEDIA'). */\n mediaBinding?: string;\n /** D1 Sessions API read-replication mode (default 'auto'). */\n session?: string;\n };\n node?: {\n /** SQLite URL (default `DATABASE_PATH` env or 'file:./data/emdash.db'). */\n databaseUrl?: string;\n /** Full database descriptor override (e.g. postgres() from 'emdash/db'). */\n database?: unknown;\n /** Local uploads directory (default './data/uploads'). */\n uploadsDir?: string;\n /** Full storage descriptor override (e.g. s3() from 'emdash/astro'). */\n storage?: unknown;\n };\n}\n\nexport interface VitopsHosting {\n target: HostingTarget;\n adapter: AstroIntegration;\n /** EmDash database descriptor — pass to emdash({ database }). */\n database: unknown;\n /** EmDash storage descriptor — pass to emdash({ storage }). */\n storage: unknown;\n}\n\n/**\n * Import an optional hosting dependency. The specifier is passed as a\n * variable so bundlers/type-checkers leave it as a plain runtime import —\n * these packages are optional peers a consumer installs per target.\n */\nasync function importFor(target: HostingTarget, specifier: string, install: string) {\n try {\n return await import(/* @vite-ignore */ specifier);\n } catch (cause) {\n throw new Error(\n `[vitops] hosting target '${target}' needs ${specifier}, which isn't installed.\\n` +\n `Install the ${target} stack first: ${install}`,\n { cause },\n );\n }\n}\n\nexport async function vitopsHosting(options: VitopsHostingOptions = {}): Promise<VitopsHosting> {\n const target = (process.env.HOSTING as HostingTarget) || options.target || 'cloudflare';\n\n if (target === 'cloudflare') {\n const install = 'pnpm add @astrojs/cloudflare @emdash-cms/cloudflare';\n const cf = options.cloudflare ?? {};\n const { default: cloudflare } = await importFor(target, '@astrojs/cloudflare', install);\n const { d1, r2 } = await importFor(target, '@emdash-cms/cloudflare', install);\n return {\n target,\n adapter: cloudflare(),\n database: d1({ binding: cf.dbBinding ?? 'DB', session: cf.session ?? 'auto' }),\n storage: r2({ binding: cf.mediaBinding ?? 'MEDIA' }),\n };\n }\n\n if (target === 'node') {\n const install = 'pnpm add @astrojs/node better-sqlite3';\n const opts = options.node ?? {};\n const { default: node } = await importFor(target, '@astrojs/node', install);\n // Required peer of this package, so always present — but still routed\n // through importFor: a literal import('emdash/db') would be statically\n // analyzable, and bundling the plugin runtime would drag EmDash's whole\n // integration graph into the site's server/client bundles.\n const { sqlite } = await importFor(target, 'emdash/db', install);\n const { local } = await importFor(target, 'emdash/astro', install);\n const { mkdirSync } = await import('node:fs');\n const { dirname } = await import('node:path');\n\n const databaseUrl = opts.databaseUrl ?? process.env.DATABASE_PATH ?? 'file:./data/emdash.db';\n const uploadsDir = opts.uploadsDir ?? './data/uploads';\n // better-sqlite3 refuses to create parent directories, so make sure the\n // defaults exist (runs at astro config load — dev and build). A deployed\n // host still needs the data directory mounted/persisted as a volume.\n if (!opts.database && databaseUrl.startsWith('file:')) {\n mkdirSync(dirname(databaseUrl.slice('file:'.length)), { recursive: true });\n }\n if (!opts.storage) mkdirSync(uploadsDir, { recursive: true });\n\n return {\n target,\n adapter: node({ mode: 'standalone' }),\n database: opts.database ?? sqlite({ url: databaseUrl }),\n storage: opts.storage ?? local({ directory: uploadsDir, baseUrl: '/_emdash/api/media/file' }),\n };\n }\n\n throw new Error(\n `[vitops] unknown hosting target '${target}' (from ${process.env.HOSTING ? 'HOSTING env var' : 'options.target'}). ` +\n `Valid targets: ${TARGETS.join(', ')}.`,\n );\n}\n","","/**\n * @getvitops/emdash — EmDash CMS native plugin for the Vitops design system.\n *\n * Declares Vitops Portable Text block types (editable from the EmDash admin's\n * slash menu) and ships the Astro components that render them via the\n * `componentsEntry` (`@getvitops/emdash/astro`).\n *\n * Composes with — does not replace — the `vitops()` Astro integration from\n * `@getvitops/astro`: the integration owns design-system CSS generation and\n * copies the web-component bundles into `public/<wcBase>/`; this plugin only\n * adds the editor-facing layer. Register both in the consumer site:\n *\n * ```js\n * // astro.config.mjs\n * import config from './site.json' with { type: 'json' };\n *\n * integrations: [\n * react(),\n * vitops({ css: {...} }),\n * emdash({ database, storage, plugins: [vitopsEmdash({ designSystemEditor: { config } })] }),\n * ]\n * ```\n */\nimport { definePlugin } from 'emdash';\nimport type {\n PageFragmentContribution,\n PageFragmentEvent,\n PluginCapability,\n PluginContext,\n PluginDescriptor,\n} from 'emdash';\nimport { BLOCKS } from './blocks.ts';\nimport {\n createDesignSystemRoute,\n designSystemOverrideFragment,\n type DesignSystemEditorOptions,\n} from './design-system-route.ts';\n\nexport { BLOCKS } from './blocks.ts';\nexport type { BlockKitFieldDef, PortableTextBlockDef } from './blocks.ts';\nexport { vitopsHosting } from './hosting.ts';\nexport type { HostingTarget, VitopsHosting, VitopsHostingOptions } from './hosting.ts';\nexport type { DesignSystemEditorOptions } from './design-system-route.ts';\n\n/**\n * Read from package.json rather than hand-maintained.\n *\n * This was a literal with a \"keep in sync\" comment, which drifted on every\n * release: `changeset version` bumps package.json and leaves the literal alone.\n * `blocks.test.ts` asserts they match, but the release chain didn't run tests,\n * so `@getvitops/emdash@0.2.1` shipped a descriptor reporting `0.2.0`. Deriving\n * it removes the failure mode instead of relying on remembering.\n */\nimport pkg from '../package.json' with { type: 'json' };\n\nconst VERSION = pkg.version;\nconst PLUGIN_ID = 'vitops';\nconst FRAGMENTS_CAPABILITY: PluginCapability = 'hooks.page-fragments:register';\n/** For the design-system-editor's GitHub auto-commit — see design-system-route.ts. */\nconst NETWORK_CAPABILITY: PluginCapability = 'network:request';\n\nexport interface VitopsEmdashOptions {\n /**\n * How the web-component runtime scripts reach the page:\n * - `'integration'` (default) — the site's layout renders `<Head />` from\n * `@getvitops/astro`, which emits the script tags; this plugin adds none.\n * - `'fragments'` — for layouts using EmDash's `<EmDashHead/>`/`<EmDashBodyEnd/>`\n * instead: the plugin injects the script tags via the `page:fragments` hook.\n * Either way the bundles themselves must exist under `public/<wcBase>/`,\n * which the `vitops()` integration provides (webComponents: true).\n * Do not enable both delivery paths — the scripts would load twice.\n */\n scripts?: 'integration' | 'fragments';\n /** Public base path of the web-component bundles (default '/vitops'). */\n wcBase?: string;\n /**\n * Mounts a `<wc-theme-editor>` admin page at `/design-system`, backed by a\n * plugin API route — see AGENTS.md's \"EmDash plugin as the design-system\n * editing surface\" section and `design-system-route.ts`'s own docblock.\n * Off by default. Requires the `network:request` capability (declared\n * automatically when this is set) for the GitHub auto-commit half —\n * reviewed once at plugin-install time, same as any other capability.\n */\n designSystemEditor?: DesignSystemEditorOptions | false;\n}\n\n/**\n * Descriptor factory — import in `astro.config.mjs` and pass to the emdash()\n * integration's `plugins` array.\n */\nexport function vitopsEmdash(options: VitopsEmdashOptions = {}): PluginDescriptor {\n return {\n id: PLUGIN_ID,\n version: VERSION,\n format: 'native',\n entrypoint: '@getvitops/emdash',\n componentsEntry: '@getvitops/emdash/astro',\n ...(options.designSystemEditor ? { adminEntry: '@getvitops/emdash/admin' } : {}),\n options,\n capabilities: [\n ...(options.scripts === 'fragments' || options.designSystemEditor\n ? [FRAGMENTS_CAPABILITY]\n : []),\n ...(options.designSystemEditor ? [NETWORK_CAPABILITY] : []),\n ] as PluginCapability[],\n } as PluginDescriptor;\n}\n\n/** Runtime side — EmDash imports and calls this from the descriptor's entrypoint. */\nexport function createPlugin(options: VitopsEmdashOptions = {}) {\n const scripts = options.scripts ?? 'integration';\n const wcBase = options.wcBase ?? '/vitops';\n const editor = options.designSystemEditor || undefined;\n const fragmentsEnabled = scripts === 'fragments' || !!editor;\n\n // Mirrors @getvitops/astro Head.astro: module polyfills + elements in\n // head, deferred enhancements at end of body. Composed with the\n // design-system-editor's override fragment below — either half can be on\n // independently of the other.\n const pageFragments = async (\n _event: PageFragmentEvent,\n ctx: PluginContext,\n ): Promise<PageFragmentContribution[]> => {\n const contributions: PageFragmentContribution[] = [];\n if (scripts === 'fragments') {\n contributions.push(\n {\n kind: 'html',\n placement: 'head',\n html:\n `<link rel=\"modulepreload\" href=\"${wcBase}/polyfills.js\">` +\n `<script type=\"module\" src=\"${wcBase}/polyfills.js\"></script>` +\n `<script type=\"module\" src=\"${wcBase}/elements.js\"></script>`,\n key: 'vitops-wc',\n },\n {\n kind: 'external-script',\n placement: 'body:end',\n src: `${wcBase}/deferred.js`,\n defer: true,\n key: 'vitops-deferred',\n },\n );\n }\n if (editor) {\n const fragment = await designSystemOverrideFragment(ctx, editor);\n if (fragment) contributions.push(fragment);\n }\n return contributions;\n };\n\n return definePlugin({\n id: PLUGIN_ID,\n version: VERSION,\n capabilities: [\n ...(fragmentsEnabled ? [FRAGMENTS_CAPABILITY] : []),\n ...(editor ? [NETWORK_CAPABILITY] : []),\n ],\n ...(editor ? { allowedHosts: ['api.github.com'] } : {}),\n ...(fragmentsEnabled ? { hooks: { 'page:fragments': pageFragments } } : {}),\n\n admin: {\n portableTextBlocks: BLOCKS,\n ...(editor\n ? {\n entry: '@getvitops/emdash/admin',\n pages: [{ path: '/design-system', label: 'Design System', icon: 'palette' }],\n }\n : {}),\n },\n\n ...(editor\n ? {\n routes: {\n 'design-system': createDesignSystemRoute(editor),\n },\n }\n : {}),\n });\n}\n\nexport default createPlugin;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["pkg.version"],"sources":["../src/icon-options.ts","../src/blocks.ts","../src/design-system-route.ts","../src/blocks-doc.ts","../src/hosting.ts","../package.json","../src/index.ts"],"sourcesContent":["/**\n * Semantic icon names, as Block Kit combobox options.\n *\n * Derived from `@getvitops/utils`' `iconMap` at import time. This used to be a\n * generated copy, so that the package carried no runtime `@getvitops/*`\n * dependency and could version independently of the fixed toolchain group —\n * but that independence was already fiction: the blocks render from the\n * toolchain's SVG sprite (`<use href=\"…/icons.svg#icon-menu\">`), so an emdash\n * built against one icon vocabulary and a sprite built against another produce\n * empty boxes, silently and with no install-time error. Depending on the real\n * map makes the vocabulary one thing rather than two that can drift.\n *\n * `fa7` is the reference collection: every semantic name is defined there, and\n * the other sets are alternate resolutions of the same names.\n */\nimport { iconMap } from '@getvitops/utils';\n\nexport const SEMANTIC_ICON_OPTIONS: { label: string; value: string }[] = Object.keys(iconMap.fa7)\n .sort()\n // Label and value are the same string on purpose: the editor picks a semantic\n // name, and showing the set-specific glyph id would leak the icon set into\n // authored content — the thing the semantic layer exists to prevent.\n .map((name) => ({ label: name, value: name }));\n","/**\n * Portable Text block-type declarations for the EmDash editor.\n *\n * Fields are EmDash Block Kit elements. Scalars (text_input / number_input / select /\n * toggle / combobox) cover single-valued patterns; `repeater` (rows of scalar\n * sub-fields) covers the repeating ones — cards, gallery, accordion, marquee, carousel\n * slides — and `media_picker` picks one asset from the media library. Both are\n * supported by the Portable Text block editor but not by Block Kit admin pages, which\n * is why the type below names them explicitly rather than accepting any `Element`.\n *\n * Patterns that need other blocks INSIDE them (a section with columns, a card holding a\n * button) cannot be a Portable Text block — a repeater holds scalars only. Those belong in\n * an EmDash `blocks` field; see the README.\n *\n * Every `action_id` here must have a matching prop consumed by the block's\n * .astro component in `src/astro/blocks/` (enforced by blocks.test.ts).\n */\n\nimport type { PortableTextBlockConfig } from 'emdash';\nimport { SEMANTIC_ICON_OPTIONS } from './icon-options.ts';\n\n/** The subset of EmDash Block Kit field elements these blocks use. */\nexport type BlockKitFieldDef = Extract<\n NonNullable<PortableTextBlockConfig['fields']>[number],\n {\n type:\n | 'text_input'\n | 'number_input'\n | 'select'\n | 'toggle'\n | 'combobox'\n | 'repeater'\n | 'media_picker';\n }\n>;\n\nexport type PortableTextBlockDef = Omit<PortableTextBlockConfig, 'fields'> & {\n fields?: BlockKitFieldDef[];\n};\n\nexport const BLOCKS: PortableTextBlockDef[] = [\n {\n /**\n * A link or button, optionally with an icon at either end.\n *\n * Flattened on purpose: Block Kit has no object-group element, so a shape\n * like `{ label, url }` becomes sibling fields. Same constraint the\n * marketing-blocks plugin documents.\n *\n * The icons are `combobox` — a searchable typeahead over a STATIC option\n * list, which is exactly what the semantic name list is. It is the closest\n * fit Block Kit offers: its element union is closed (no custom element, no\n * raw HTML), so `<wc-icon-picker>` cannot be mounted inside a block modal\n * however much nicer it would be. Revisit if EmDash opens that union.\n */\n type: 'vitops.actionLink',\n label: 'Link or button',\n icon: 'link',\n placeholder: 'A link or button, with optional icons',\n fields: [\n { type: 'text_input', action_id: 'label', label: 'Label' },\n { type: 'text_input', action_id: 'href', label: 'URL' },\n {\n type: 'select',\n action_id: 'variant',\n label: 'Style',\n initial_value: 'link',\n options: [\n { label: 'Link', value: 'link' },\n { label: 'Button', value: 'btn' },\n { label: 'Call to action', value: 'cta' },\n ],\n },\n {\n type: 'combobox',\n action_id: 'startIcon',\n label: 'Icon before the label',\n placeholder: 'None',\n options: SEMANTIC_ICON_OPTIONS,\n },\n {\n type: 'combobox',\n action_id: 'endIcon',\n label: 'Icon after the label',\n placeholder: 'None',\n options: SEMANTIC_ICON_OPTIONS,\n },\n {\n type: 'select',\n action_id: 'role',\n label: 'Colour role',\n initial_value: '',\n options: [\n { label: 'Default', value: '' },\n { label: 'Success', value: 'success' },\n { label: 'Danger', value: 'danger' },\n { label: 'Warning', value: 'warning' },\n { label: 'Info', value: 'info' },\n ],\n },\n { type: 'toggle', action_id: 'newTab', label: 'Open in a new tab', initial_value: false },\n ],\n },\n {\n type: 'vitops.imageCompare',\n label: 'Image compare',\n placeholder: 'Before/after image slider',\n fields: [\n { type: 'text_input', action_id: 'before', label: 'Before image URL' },\n { type: 'text_input', action_id: 'beforeAlt', label: 'Before alt text' },\n {\n type: 'text_input',\n action_id: 'beforeLabel',\n label: 'Before label',\n initial_value: 'Before',\n },\n { type: 'text_input', action_id: 'after', label: 'After image URL' },\n { type: 'text_input', action_id: 'afterAlt', label: 'After alt text' },\n { type: 'text_input', action_id: 'afterLabel', label: 'After label', initial_value: 'After' },\n ],\n },\n {\n type: 'vitops.copyButton',\n label: 'Copy snippet',\n icon: 'code',\n placeholder: 'Text with a copy-to-clipboard button',\n fields: [\n { type: 'text_input', action_id: 'text', label: 'Text to copy', multiline: true },\n { type: 'text_input', action_id: 'label', label: 'Button label', initial_value: 'Copy' },\n {\n type: 'select',\n action_id: 'display',\n label: 'Display',\n initial_value: 'code-block',\n options: [\n { label: 'Code block', value: 'code-block' },\n { label: 'Inline', value: 'inline' },\n ],\n },\n ],\n },\n {\n type: 'vitops.banner',\n label: 'Banner',\n placeholder: 'Announcement or notice',\n fields: [\n { type: 'text_input', action_id: 'message', label: 'Message', multiline: true },\n {\n type: 'select',\n action_id: 'tone',\n label: 'Tone',\n initial_value: 'info',\n options: [\n { label: 'Info', value: 'info' },\n { label: 'Success', value: 'success' },\n { label: 'Warning', value: 'warning' },\n { label: 'Danger', value: 'danger' },\n ],\n },\n { type: 'toggle', action_id: 'dismissible', label: 'Dismissible', initial_value: true },\n ],\n },\n {\n type: 'vitops.details',\n label: 'Disclosure',\n placeholder: 'Collapsible details/summary',\n fields: [\n { type: 'text_input', action_id: 'summary', label: 'Summary (always visible)' },\n { type: 'text_input', action_id: 'body', label: 'Body (revealed)', multiline: true },\n { type: 'toggle', action_id: 'open', label: 'Open by default', initial_value: false },\n ],\n },\n {\n type: 'vitops.carousel',\n label: 'Carousel',\n placeholder: 'Image carousel',\n category: 'Media',\n fields: [\n {\n type: 'repeater',\n action_id: 'slides',\n label: 'Slides',\n item_label: 'slide',\n min_items: 1,\n fields: [\n { type: 'text_input', action_id: 'src', label: 'Image URL' },\n { type: 'text_input', action_id: 'alt', label: 'Alt text' },\n ],\n },\n {\n type: 'text_input',\n action_id: 'images',\n label: 'Image URLs, one per line (legacy — use Slides)',\n multiline: true,\n },\n {\n type: 'text_input',\n action_id: 'alts',\n label: 'Alt texts, one per line (legacy — use Slides)',\n multiline: true,\n },\n {\n type: 'text_input',\n action_id: 'label',\n label: 'Accessible name',\n initial_value: 'Carousel',\n },\n {\n type: 'number_input',\n action_id: 'autoplay',\n label: 'Autoplay interval (ms, 0 = off)',\n initial_value: 0,\n },\n {\n type: 'toggle',\n action_id: 'loop',\n label: 'Loop (clones the slides — autoplay implies it)',\n initial_value: false,\n },\n {\n type: 'text_input',\n action_id: 'aspect',\n label: 'Slide aspect ratio',\n initial_value: '16 / 9',\n },\n ],\n },\n {\n type: 'vitops.gallery',\n label: 'Gallery',\n placeholder: 'Thumbnail grid that opens a lightbox carousel',\n category: 'Media',\n fields: [\n {\n type: 'text_input',\n action_id: 'label',\n label: 'Accessible name',\n initial_value: 'Gallery',\n },\n {\n type: 'repeater',\n action_id: 'images',\n label: 'Images',\n item_label: 'image',\n min_items: 1,\n fields: [\n { type: 'text_input', action_id: 'src', label: 'Image URL' },\n { type: 'text_input', action_id: 'alt', label: 'Alt text' },\n { type: 'text_input', action_id: 'caption', label: 'Caption' },\n ],\n },\n {\n type: 'text_input',\n action_id: 'aspect',\n label: 'Thumbnail aspect ratio',\n initial_value: '1',\n },\n { type: 'toggle', action_id: 'loop', label: 'Wrap around at the ends', initial_value: false },\n ],\n },\n {\n type: 'vitops.cards',\n label: 'Cards',\n placeholder: 'A grid of cards',\n category: 'Sections',\n fields: [\n {\n type: 'repeater',\n action_id: 'cards',\n label: 'Cards',\n item_label: 'card',\n min_items: 1,\n fields: [\n { type: 'text_input', action_id: 'title', label: 'Title' },\n { type: 'text_input', action_id: 'body', label: 'Text', multiline: true },\n { type: 'text_input', action_id: 'image', label: 'Image URL' },\n { type: 'text_input', action_id: 'imageAlt', label: 'Image alt text' },\n {\n type: 'text_input',\n action_id: 'href',\n label: 'Link URL (makes the whole card clickable)',\n },\n {\n type: 'text_input',\n action_id: 'linkLabel',\n label: 'Link text',\n initial_value: 'Read more',\n },\n ],\n },\n {\n type: 'select',\n action_id: 'columns',\n label: 'Columns',\n initial_value: '3',\n options: [\n { label: '1', value: '1' },\n { label: '2', value: '2' },\n { label: '3', value: '3' },\n { label: '4', value: '4' },\n ],\n },\n ],\n },\n {\n type: 'vitops.accordion',\n label: 'Accordion',\n placeholder: 'A group of collapsible sections',\n category: 'Sections',\n fields: [\n {\n type: 'repeater',\n action_id: 'items',\n label: 'Items',\n item_label: 'item',\n min_items: 1,\n fields: [\n { type: 'text_input', action_id: 'summary', label: 'Summary (always visible)' },\n { type: 'text_input', action_id: 'body', label: 'Body (revealed)', multiline: true },\n { type: 'toggle', action_id: 'open', label: 'Open by default', initial_value: false },\n ],\n },\n {\n type: 'toggle',\n action_id: 'exclusive',\n label: 'Only one open at a time',\n initial_value: false,\n },\n ],\n },\n {\n type: 'vitops.hero',\n label: 'Hero',\n placeholder: 'Page opener: heading, text, buttons and an optional image',\n category: 'Sections',\n fields: [\n { type: 'text_input', action_id: 'eyebrow', label: 'Eyebrow (small line above)' },\n { type: 'text_input', action_id: 'heading', label: 'Heading' },\n { type: 'text_input', action_id: 'body', label: 'Text', multiline: true },\n { type: 'text_input', action_id: 'primaryLabel', label: 'Main button text' },\n { type: 'text_input', action_id: 'primaryHref', label: 'Main button URL' },\n { type: 'text_input', action_id: 'secondaryLabel', label: 'Second link text' },\n { type: 'text_input', action_id: 'secondaryHref', label: 'Second link URL' },\n { type: 'text_input', action_id: 'image', label: 'Image URL (split layout)' },\n { type: 'text_input', action_id: 'imageAlt', label: 'Image alt text' },\n {\n type: 'select',\n action_id: 'layout',\n label: 'Layout',\n initial_value: 'centered',\n options: [\n { label: 'Centered', value: 'centered' },\n { label: 'Text and image side by side', value: 'split' },\n ],\n },\n ],\n },\n {\n type: 'vitops.marquee',\n label: 'Marquee',\n placeholder: 'A continuously scrolling row (logos, quotes, badges)',\n category: 'Media',\n fields: [\n {\n type: 'repeater',\n action_id: 'items',\n label: 'Items',\n item_label: 'item',\n min_items: 1,\n fields: [\n { type: 'text_input', action_id: 'text', label: 'Text' },\n { type: 'text_input', action_id: 'image', label: 'Image URL (shown instead of text)' },\n { type: 'text_input', action_id: 'alt', label: 'Image alt text' },\n ],\n },\n { type: 'toggle', action_id: 'pauseOnHover', label: 'Pause on hover', initial_value: true },\n { type: 'toggle', action_id: 'reverse', label: 'Scroll the other way', initial_value: false },\n ],\n },\n];\n\n/** Which Vitops blocks a site's editor offers, and any it adds of its own. */\nexport interface BlockSelection {\n /** Offer only these `vitops.*` types. Unknown names throw — a typo must not silently offer nothing. */\n include?: string[];\n /** Drop these `vitops.*` types. Applied after `include`. */\n exclude?: string[];\n /**\n * The site's own blocks. These are EDITOR definitions only: the plugin cannot render a\n * block it has never heard of, so the site maps each `type` to a component with\n * `<PortableText components={{ type: { … } }} />`. A type that collides with an existing\n * one throws, since the later definition would silently replace the earlier one's fields.\n */\n extra?: PortableTextBlockDef[];\n}\n\n/**\n * Resolve {@link BlockSelection} against {@link BLOCKS}.\n *\n * Pure so a misconfiguration fails at plugin construction, with the offending name in the\n * message, rather than as an editor menu that is quietly missing something.\n */\nexport function selectBlocks(selection: BlockSelection = {}): PortableTextBlockDef[] {\n const known = new Set(BLOCKS.map((b) => b.type));\n const check = (names: string[] | undefined, option: string): void => {\n for (const name of names ?? [])\n if (!known.has(name))\n throw new Error(\n `[vitops] blocks.${option}: unknown block '${name}'. Known: ${[...known].join(', ')}.`,\n );\n };\n check(selection.include, 'include');\n check(selection.exclude, 'exclude');\n\n const include = selection.include ? new Set(selection.include) : null;\n const exclude = new Set(selection.exclude);\n const base = BLOCKS.filter((b) => (!include || include.has(b.type)) && !exclude.has(b.type));\n\n const extra = selection.extra ?? [];\n const taken = new Set(BLOCKS.map((b) => b.type));\n for (const block of extra) {\n if (taken.has(block.type))\n throw new Error(`[vitops] blocks.extra: type '${block.type}' is already defined.`);\n taken.add(block.type);\n }\n return [...base, ...extra];\n}\n","/**\n * The `vitops.design-system` plugin route — what `<wc-theme-editor>` probes\n * and POSTs its patch to when mounted inside the EmDash admin (see this\n * package's README and AGENTS.md's \"EmDash plugin as the design-system\n * editing surface\" section). Mounted at\n * `/_emdash/api/plugins/vitops/design-system`.\n *\n * Two responsibilities, matching the dev-only `@getvitops/vite` endpoint's\n * contract byte-for-byte (`GET → { writable }`, `POST { patch } → { ok } |\n * { errors }`) so `<wc-theme-editor>` needs no branch for which one it's\n * talking to:\n *\n * 1. **Immediate**: persist the patch to `ctx.settings` — read back by the\n * `page:fragments` hook in `index.ts`, which turns it into a site-wide\n * `:root` override `<style>` on every public page. This is what makes an\n * edit visible without a rebuild.\n * 2. **Canonical**: auto-commit the merged file to `site.deployment.branch`\n * (default `dev`) via `@getvitops/utils/git`'s `commitFile()`, so the edit\n * reaches the git-tracked `site.json` through this repo's own normal\n * `dev → promote → main` review path — never a silent second source of\n * truth. A commit failure (missing token, misconfigured repo, a transient\n * GitHub error) is logged and surfaces as a warning, but never fails the\n * save: the immediate override already works regardless.\n *\n * **Assumes `config` is a full site `Config`** (has `organization`/`site`),\n * not a bare `design-system.json` — true for any real EmDash site (it always\n * has a `site.json`), and the only case this plugin already handles anywhere\n * else (`vitopsHosting()`, `BLOCKS`).\n *\n * The GitHub token is read as `process.env.VITOPS_GITHUB_TOKEN` — matching\n * `hosting.ts`'s own `process.env.HOSTING` read in this same package, rather\n * than a `cloudflare:workers`-specific import: a native EmDash plugin runs\n * in-process with no isolation boundary (EmDash's own docs say plugins \"can\n * access environment variables\" directly), and this package already targets\n * both the Cloudflare and Node hosting targets through one `process.env`\n * convention (`nodejs_compat` on Workers maps bindings into `process.env`).\n */\nimport { pluginResponse } from 'emdash';\nimport type { PageFragmentContribution, PluginContext, PluginRoute, RouteContext } from 'emdash';\nimport {\n designSystemPath,\n formatConfig,\n getAt,\n isConfig,\n mergePatch,\n resolveInput,\n setAt,\n} from '@getvitops/generator/config';\nimport {\n buildReverseIndex,\n patchToOverrideCss,\n validateColorContract,\n} from '@getvitops/generator/tokens';\nimport { commitFile } from '@getvitops/utils/git';\n\nexport interface DesignSystemEditorOptions {\n /**\n * The site's own config, typically `import config from '../../site.json'\n * with { type: 'json' }`. A static, build-time-bundled value — this plugin\n * never reads it from disk at runtime (a deployed Worker has no\n * filesystem), so an edit doesn't take effect here until the *next*\n * deploy; that's exactly the point of the auto-commit half.\n */\n config: unknown;\n /** Which `designSystem.themes` entry this edits. Default: `'default'`. */\n theme?: string;\n}\n\nconst SETTINGS_KEY = 'designSystemPatch';\n\ninterface StoredPatch {\n patch: Record<string, unknown>;\n at: number;\n}\n\nfunction json(status: number, body: unknown) {\n return pluginResponse({\n status,\n headers: { 'content-type': 'application/json' },\n body: { kind: 'text', value: JSON.stringify(body) },\n });\n}\n\n/** Fetches the currently-stored patch, clearing it if the bundled `config`\n * (the last real build) already reflects every value in it — see the\n * module docblock's \"self-clearing\" note in AGENTS.md. */\nasync function currentPatch(\n ctx: Pick<RouteContext, 'settings'>,\n ds: { colors: Record<string, unknown> },\n): Promise<StoredPatch | null> {\n const stored = await ctx.settings.get<StoredPatch>(SETTINGS_KEY);\n if (!stored) return null;\n const patchedColors = stored.patch['colors'];\n if (\n patchedColors != null &&\n JSON.stringify(mergePatch(ds.colors, patchedColors)) === JSON.stringify(ds.colors)\n ) {\n await ctx.settings.delete(SETTINGS_KEY);\n return null;\n }\n return stored;\n}\n\nexport function createDesignSystemRoute(opts: DesignSystemEditorOptions): PluginRoute {\n const theme = opts.theme ?? 'default';\n\n return {\n methods: ['GET', 'POST'],\n response: 'raw',\n permission: 'plugins:manage',\n handler: async (ctx: RouteContext) => {\n // `resolveInput` handles both a bare design-system.json and a full site\n // Config (following `extends`, resolving `themes[theme]`), the same\n // entry point every other consumer of a config goes through.\n const { designSystem: ds, config } = resolveInput(opts.config, {\n theme,\n siteEnv: 'production',\n });\n\n if (ctx.request.method === 'GET') {\n await currentPatch(ctx, ds);\n return json(200, { writable: true });\n }\n\n const body = ctx.input as { patch?: unknown } | undefined;\n const patch = body?.patch;\n if (patch == null || typeof patch !== 'object' || Array.isArray(patch))\n return json(400, { errors: ['body must be { patch: object }'] });\n\n const mergedColors = mergePatch(\n ds.colors,\n (patch as Record<string, unknown>)['colors'] ?? {},\n );\n const contrastErrors = validateColorContract(\n mergedColors as Parameters<typeof validateColorContract>[0],\n );\n if (contrastErrors.length) return json(400, { errors: contrastErrors });\n\n await ctx.settings.set(SETTINGS_KEY, {\n patch: patch as Record<string, unknown>,\n at: Date.now(),\n } satisfies StoredPatch);\n\n const warnings: string[] = [];\n const repoSpec = config?.site.deployment?.repo;\n const token = process.env['VITOPS_GITHUB_TOKEN'];\n if (!repoSpec || !token) {\n warnings.push(\n !repoSpec\n ? 'design-system sync skipped: site.deployment.repo is not set — the change is live but not yet synced to source.'\n : 'design-system sync skipped: no GitHub token configured — the change is live but not yet synced to source.',\n );\n } else {\n const [owner, repo] = repoSpec.split('/');\n if (!owner || !repo) {\n warnings.push(\n `design-system sync skipped: site.deployment.repo \"${repoSpec}\" isn't \"owner/name\".`,\n );\n } else if (!isConfig(opts.config)) {\n warnings.push('design-system sync skipped: config is not a recognised site Config.');\n } else {\n const at = designSystemPath(opts.config, theme);\n const mergedSubtree = mergePatch(getAt(opts.config, at), patch);\n const content = `${formatConfig(setAt(opts.config, at, mergedSubtree))}\\n`;\n const result = await commitFile({\n owner,\n repo,\n branch: config.site.deployment?.branch ?? 'dev',\n path: 'site.json',\n content,\n message: 'Sync design-system edit from EmDash admin',\n token,\n fetchImpl:\n ctx.http && ((url: string, init?: RequestInit) => ctx.http!.fetch(url, init)),\n });\n if (!result.ok) {\n ctx.log.warn('design-system sync commit failed', { message: result.message });\n warnings.push(`design-system sync failed (change is still live): ${result.message}`);\n }\n }\n }\n\n return json(200, { ok: true, warnings });\n },\n };\n}\n\n/**\n * The `page:fragments` contribution serving the stored patch's override\n * CSS site-wide. Returns `null` (no fragment) when there's nothing unsynced\n * — the same \"no gated tag, no cost\" discipline `@getvitops/astro`'s consent\n * gate already follows.\n */\nexport async function designSystemOverrideFragment(\n ctx: Pick<PluginContext, 'settings'>,\n opts: DesignSystemEditorOptions,\n): Promise<PageFragmentContribution | null> {\n const { designSystem: ds } = resolveInput(opts.config, { theme: opts.theme ?? 'default' });\n const stored = await currentPatch(ctx, ds);\n if (!stored) return null;\n\n const css = patchToOverrideCss(stored.patch, buildReverseIndex(ds));\n if (!css) return null;\n return {\n kind: 'html',\n placement: 'head',\n html: `<style id=\"ds-overrides\">${css}</style>`,\n key: 'vitops-ds-overrides',\n };\n}\n","import { BLOCKS, type BlockKitFieldDef, type PortableTextBlockDef } from './blocks.ts';\n\nconst cell = (s: string | undefined) => (s ?? '').replace(/\\|/g, '\\\\|').replace(/\\n/g, ' ');\n\nfunction describe(f: BlockKitFieldDef): string {\n const bits: string[] = [];\n if ('options' in f && Array.isArray(f.options) && f.options.length) {\n const values = f.options.map((o) => String((o as { value: unknown }).value));\n bits.push(\n values.length > 8\n ? `one of ${values.length} values (e.g. ${values.slice(0, 4).join(', ')})`\n : `one of ${values.join(', ')}`,\n );\n }\n if ('initial_value' in f && f.initial_value !== undefined) {\n bits.push(`default \\`${String(f.initial_value)}\\``);\n }\n if (f.type === 'repeater' && 'min_items' in f && f.min_items)\n bits.push(`min ${f.min_items} rows`);\n return bits.join('; ');\n}\n\nfunction fieldRows(fields: BlockKitFieldDef[], prefix = ''): string[] {\n return fields.flatMap((f) => {\n const own = `| \\`${prefix}${f.action_id}\\` | ${f.type} | ${cell((f as { label?: string }).label)} | ${cell(describe(f))} |`;\n return f.type === 'repeater' ? [own, ...fieldRows(f.fields, `${f.action_id}[].`)] : [own];\n });\n}\n\n/**\n * The block reference an agent reads via `vitops docs blocks`: every Portable Text `_type`,\n * its fields and what each saves. Rendered from the same `BLOCKS` the editor menu uses, so\n * it cannot describe a field the editor doesn't offer.\n */\nexport function renderBlocksDoc(blocks: readonly PortableTextBlockDef[] = BLOCKS): string {\n const out = [\n '---',\n 'type: concept',\n 'title: EmDash blocks',\n 'description: Portable Text block types from @getvitops/emdash, with every field',\n '---',\n '',\n '# EmDash blocks',\n '',\n 'Each block is a Portable Text object: `{ \"_type\": \"vitops.cards\", \"_key\": \"<unique>\", ...fields }`.',\n 'Blocks are flat — a block cannot contain other blocks, and a repeater row holds scalar fields only.',\n 'Give every block a unique `_key`; gallery and carousel derive their element id from it.',\n '',\n ];\n for (const b of blocks) {\n out.push(`## \\`${b.type}\\` — ${b.label}`, '');\n if (b.placeholder) out.push(`${b.placeholder}.`, '');\n if (b.fields?.length) {\n out.push(\n '| Field | Type | Label | Notes |',\n '| --- | --- | --- | --- |',\n ...fieldRows(b.fields),\n '',\n );\n }\n }\n return out.join('\\n');\n}\n","/**\n * Hosting seam — one call in astro.config.mjs resolves the adapter + EmDash\n * database/storage for the chosen hosting target, so a site can start on\n * Cloudflare and move to a Node host (VPS / docker-compose / k8s) — or back —\n * by flipping a single value instead of rewiring its config:\n *\n * ```js\n * import { vitopsEmdash, vitopsHosting } from '@getvitops/emdash';\n * const { adapter, database, storage } = await vitopsHosting();\n * export default defineConfig({\n * output: 'server',\n * adapter,\n * integrations: [react(), emdash({ database, storage, plugins: [vitopsEmdash()] })],\n * });\n * ```\n *\n * Targets:\n * - `'cloudflare'` (default) — `@astrojs/cloudflare` + D1/R2 bindings.\n * Needs `@astrojs/cloudflare` and `@emdash-cms/cloudflare` installed.\n * - `'node'` — `@astrojs/node` (standalone) + SQLite file + local uploads.\n * Needs `@astrojs/node` and `better-sqlite3` installed. Scheduled publishing\n * runs in-process on Node (no worker/cron trigger needed). For production,\n * move storage to S3-compatible (`s3()` from `emdash/astro`) and/or the\n * database to Postgres (`postgres()` from `emdash/db`) via `options.node`.\n *\n * The adapter packages are optional peer dependencies, resolved lazily — a\n * site only installs the stack for the target it uses. Content lives in the\n * database and media in the storage backend, so switching hosts is a data\n * migration (D1 export / EmDash seed round-trip + media copy), not a rewrite.\n */\nimport type { AstroIntegration } from 'astro';\n\nexport type HostingTarget = 'cloudflare' | 'node';\n\nconst TARGETS: HostingTarget[] = ['cloudflare', 'node'];\n\nexport interface VitopsHostingOptions {\n /** Hosting target. Overridden by the `HOSTING` env var. Default 'cloudflare'. */\n target?: HostingTarget;\n cloudflare?: {\n /** D1 binding name in wrangler config (default 'DB'). */\n dbBinding?: string;\n /** R2 binding name in wrangler config (default 'MEDIA'). */\n mediaBinding?: string;\n /** D1 Sessions API read-replication mode (default 'auto'). */\n session?: string;\n /**\n * Batch reads issued together into one D1 round trip (default true).\n * Experimental in emdash: a read and a write started together may run\n * write-first, so await a read before a write that must not affect it.\n * No effect without a session.\n */\n coalesce?: boolean;\n };\n node?: {\n /** SQLite URL (default `DATABASE_PATH` env or 'file:./data/emdash.db'). */\n databaseUrl?: string;\n /** Full database descriptor override (e.g. postgres() from 'emdash/db'). */\n database?: unknown;\n /** Local uploads directory (default './data/uploads'). */\n uploadsDir?: string;\n /** Full storage descriptor override (e.g. s3() from 'emdash/astro'). */\n storage?: unknown;\n };\n}\n\nexport interface VitopsHosting {\n target: HostingTarget;\n adapter: AstroIntegration;\n /** EmDash database descriptor — pass to emdash({ database }). */\n database: unknown;\n /** EmDash storage descriptor — pass to emdash({ storage }). */\n storage: unknown;\n}\n\n/**\n * Import an optional hosting dependency. The specifier is passed as a\n * variable so bundlers/type-checkers leave it as a plain runtime import —\n * these packages are optional peers a consumer installs per target.\n */\nasync function importFor(target: HostingTarget, specifier: string, install: string) {\n try {\n return await import(/* @vite-ignore */ specifier);\n } catch (cause) {\n throw new Error(\n `[vitops] hosting target '${target}' needs ${specifier}, which isn't installed.\\n` +\n `Install the ${target} stack first: ${install}`,\n { cause },\n );\n }\n}\n\nexport async function vitopsHosting(options: VitopsHostingOptions = {}): Promise<VitopsHosting> {\n const target = (process.env.HOSTING as HostingTarget) || options.target || 'cloudflare';\n\n if (target === 'cloudflare') {\n const install = 'pnpm add @astrojs/cloudflare @emdash-cms/cloudflare';\n const cf = options.cloudflare ?? {};\n const { default: cloudflare } = await importFor(target, '@astrojs/cloudflare', install);\n const { d1, r2 } = await importFor(target, '@emdash-cms/cloudflare', install);\n return {\n target,\n adapter: cloudflare(),\n database: d1({\n binding: cf.dbBinding ?? 'DB',\n session: cf.session ?? 'auto',\n coalesce: cf.coalesce ?? true,\n }),\n storage: r2({ binding: cf.mediaBinding ?? 'MEDIA' }),\n };\n }\n\n if (target === 'node') {\n const install = 'pnpm add @astrojs/node better-sqlite3';\n const opts = options.node ?? {};\n const { default: node } = await importFor(target, '@astrojs/node', install);\n // Required peer of this package, so always present — but still routed\n // through importFor: a literal import('emdash/db') would be statically\n // analyzable, and bundling the plugin runtime would drag EmDash's whole\n // integration graph into the site's server/client bundles.\n const { sqlite } = await importFor(target, 'emdash/db', install);\n const { local } = await importFor(target, 'emdash/astro', install);\n const { mkdirSync } = await import('node:fs');\n const { dirname } = await import('node:path');\n\n const databaseUrl = opts.databaseUrl ?? process.env.DATABASE_PATH ?? 'file:./data/emdash.db';\n const uploadsDir = opts.uploadsDir ?? './data/uploads';\n // better-sqlite3 refuses to create parent directories, so make sure the\n // defaults exist (runs at astro config load — dev and build). A deployed\n // host still needs the data directory mounted/persisted as a volume.\n if (!opts.database && databaseUrl.startsWith('file:')) {\n mkdirSync(dirname(databaseUrl.slice('file:'.length)), { recursive: true });\n }\n if (!opts.storage) mkdirSync(uploadsDir, { recursive: true });\n\n return {\n target,\n adapter: node({ mode: 'standalone' }),\n database: opts.database ?? sqlite({ url: databaseUrl }),\n storage: opts.storage ?? local({ directory: uploadsDir, baseUrl: '/_emdash/api/media/file' }),\n };\n }\n\n throw new Error(\n `[vitops] unknown hosting target '${target}' (from ${process.env.HOSTING ? 'HOSTING env var' : 'options.target'}). ` +\n `Valid targets: ${TARGETS.join(', ')}.`,\n );\n}\n","","/**\n * @getvitops/emdash — EmDash CMS native plugin for the Vitops design system.\n *\n * Declares Vitops Portable Text block types (editable from the EmDash admin's\n * slash menu) and ships the Astro components that render them via the\n * `componentsEntry` (`@getvitops/emdash/astro`).\n *\n * Composes with — does not replace — the `vitops()` Astro integration from\n * `@getvitops/astro`: the integration owns design-system CSS generation and\n * copies the web-component bundles into `public/<wcBase>/`; this plugin only\n * adds the editor-facing layer. Register both in the consumer site:\n *\n * ```js\n * // astro.config.mjs\n * import config from './site.json' with { type: 'json' };\n *\n * integrations: [\n * react(),\n * vitops({ css: {...} }),\n * emdash({ database, storage, plugins: [vitopsEmdash({ designSystemEditor: { config } })] }),\n * ]\n * ```\n */\nimport { definePlugin } from 'emdash';\nimport type {\n PageFragmentContribution,\n PageFragmentEvent,\n PluginCapability,\n PluginContext,\n PluginDescriptor,\n} from 'emdash';\nimport { BLOCKS, selectBlocks, type BlockSelection } from './blocks.ts';\nimport {\n createDesignSystemRoute,\n designSystemOverrideFragment,\n type DesignSystemEditorOptions,\n} from './design-system-route.ts';\n\nexport { BLOCKS } from './blocks.ts';\nexport { selectBlocks } from './blocks.ts';\nexport { renderBlocksDoc } from './blocks-doc.ts';\nexport type { BlockKitFieldDef, BlockSelection, PortableTextBlockDef } from './blocks.ts';\nexport { vitopsHosting } from './hosting.ts';\nexport type { HostingTarget, VitopsHosting, VitopsHostingOptions } from './hosting.ts';\nexport type { DesignSystemEditorOptions } from './design-system-route.ts';\n\n/**\n * Read from package.json rather than hand-maintained.\n *\n * This was a literal with a \"keep in sync\" comment, which drifted on every\n * release: `changeset version` bumps package.json and leaves the literal alone.\n * `blocks.test.ts` asserts they match, but the release chain didn't run tests,\n * so `@getvitops/emdash@0.2.1` shipped a descriptor reporting `0.2.0`. Deriving\n * it removes the failure mode instead of relying on remembering.\n */\nimport pkg from '../package.json' with { type: 'json' };\n\nconst VERSION = pkg.version;\nconst PLUGIN_ID = 'vitops';\nconst FRAGMENTS_CAPABILITY: PluginCapability = 'hooks.page-fragments:register';\n/** For the design-system-editor's GitHub auto-commit — see design-system-route.ts. */\nconst NETWORK_CAPABILITY: PluginCapability = 'network:request';\n\nexport interface VitopsEmdashOptions {\n /**\n * How the web-component runtime scripts reach the page:\n * - `'integration'` (default) — the site's layout renders `<Head />` from\n * `@getvitops/astro`, which emits the script tags; this plugin adds none.\n * - `'fragments'` — for layouts using EmDash's `<EmDashHead/>`/`<EmDashBodyEnd/>`\n * instead: the plugin injects the script tags via the `page:fragments` hook.\n * Either way the bundles themselves must exist under `public/<wcBase>/`,\n * which the `vitops()` integration provides (webComponents: true).\n * Do not enable both delivery paths — the scripts would load twice.\n */\n scripts?: 'integration' | 'fragments';\n /** Public base path of the web-component bundles (default '/vitops'). */\n wcBase?: string;\n /**\n * Mounts a `<wc-theme-editor>` admin page at `/design-system`, backed by a\n * plugin API route — see AGENTS.md's \"EmDash plugin as the design-system\n * editing surface\" section and `design-system-route.ts`'s own docblock.\n * Off by default. Requires the `network:request` capability (declared\n * automatically when this is set) for the GitHub auto-commit half —\n * reviewed once at plugin-install time, same as any other capability.\n */\n designSystemEditor?: DesignSystemEditorOptions | false;\n /**\n * Which blocks the editor offers. All of the `vitops.*` blocks by default. Use `exclude` to\n * drop one a client should not see, `include` for an allowlist, and `extra` to add the\n * site's own — whose renderers the site supplies, see {@link BlockSelection}.\n */\n blocks?: BlockSelection;\n}\n\n/**\n * Descriptor factory — import in `astro.config.mjs` and pass to the emdash()\n * integration's `plugins` array.\n */\nexport function vitopsEmdash(options: VitopsEmdashOptions = {}): PluginDescriptor {\n return {\n id: PLUGIN_ID,\n version: VERSION,\n format: 'native',\n entrypoint: '@getvitops/emdash',\n componentsEntry: '@getvitops/emdash/astro',\n ...(options.designSystemEditor ? { adminEntry: '@getvitops/emdash/admin' } : {}),\n options,\n capabilities: [\n ...(options.scripts === 'fragments' || options.designSystemEditor\n ? [FRAGMENTS_CAPABILITY]\n : []),\n ...(options.designSystemEditor ? [NETWORK_CAPABILITY] : []),\n ] as PluginCapability[],\n } as PluginDescriptor;\n}\n\n/** Runtime side — EmDash imports and calls this from the descriptor's entrypoint. */\nexport function createPlugin(options: VitopsEmdashOptions = {}) {\n const scripts = options.scripts ?? 'integration';\n const wcBase = options.wcBase ?? '/vitops';\n const editor = options.designSystemEditor || undefined;\n const fragmentsEnabled = scripts === 'fragments' || !!editor;\n\n // Mirrors @getvitops/astro Head.astro: module polyfills + elements in\n // head, deferred enhancements at end of body. Composed with the\n // design-system-editor's override fragment below — either half can be on\n // independently of the other.\n const pageFragments = async (\n _event: PageFragmentEvent,\n ctx: PluginContext,\n ): Promise<PageFragmentContribution[]> => {\n const contributions: PageFragmentContribution[] = [];\n if (scripts === 'fragments') {\n contributions.push(\n {\n kind: 'html',\n placement: 'head',\n html:\n `<link rel=\"modulepreload\" href=\"${wcBase}/polyfills.js\">` +\n `<script type=\"module\" src=\"${wcBase}/polyfills.js\"></script>` +\n `<script type=\"module\" src=\"${wcBase}/elements.js\"></script>`,\n key: 'vitops-wc',\n },\n {\n kind: 'external-script',\n placement: 'body:end',\n src: `${wcBase}/deferred.js`,\n defer: true,\n key: 'vitops-deferred',\n },\n );\n }\n if (editor) {\n const fragment = await designSystemOverrideFragment(ctx, editor);\n if (fragment) contributions.push(fragment);\n }\n return contributions;\n };\n\n return definePlugin({\n id: PLUGIN_ID,\n version: VERSION,\n capabilities: [\n ...(fragmentsEnabled ? [FRAGMENTS_CAPABILITY] : []),\n ...(editor ? [NETWORK_CAPABILITY] : []),\n ],\n ...(editor ? { allowedHosts: ['api.github.com'] } : {}),\n ...(fragmentsEnabled ? { hooks: { 'page:fragments': pageFragments } } : {}),\n\n admin: {\n portableTextBlocks: selectBlocks(options.blocks),\n ...(editor\n ? {\n entry: '@getvitops/emdash/admin',\n pages: [{ path: '/design-system', label: 'Design System', icon: 'palette' }],\n }\n : {}),\n },\n\n ...(editor\n ? {\n routes: {\n 'design-system': createDesignSystemRoute(editor),\n },\n }\n : {}),\n });\n}\n\nexport default createPlugin;\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getvitops/emdash",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "EmDash CMS native plugin for the Vitops design system — Portable Text block types editors can insert from the admin, rendered with the design-system web components and CSS framework.",
|
|
5
5
|
"homepage": "https://docs.vitops.ca",
|
|
6
6
|
"bugs": "https://github.com/getvitops/design-system/issues",
|
|
@@ -31,30 +31,30 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@getvitops/
|
|
35
|
-
"@getvitops/
|
|
34
|
+
"@getvitops/generator": "8.2.0",
|
|
35
|
+
"@getvitops/utils": "8.2.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@astrojs/cloudflare": "^14.0.0",
|
|
39
39
|
"@astrojs/react": "^7.0.0",
|
|
40
|
-
"@emdash-cms/cloudflare": "^1.0
|
|
40
|
+
"@emdash-cms/cloudflare": "^1.4.0",
|
|
41
|
+
"@getvitops/astro": "8.2.0",
|
|
42
|
+
"@getvitops/core": "8.2.0",
|
|
41
43
|
"@types/react": "^19.3.0",
|
|
42
44
|
"@types/react-dom": "^19.3.0",
|
|
43
|
-
"astro": "^7.
|
|
44
|
-
"emdash": "^1.0
|
|
45
|
+
"astro": "^7.4.4",
|
|
46
|
+
"emdash": "^1.4.0",
|
|
45
47
|
"react": "^19.3.0",
|
|
46
|
-
"react-dom": "^19.3.0"
|
|
47
|
-
"@getvitops/astro": "8.2.0-alpha.0",
|
|
48
|
-
"@getvitops/core": "8.2.0-alpha.0"
|
|
48
|
+
"react-dom": "^19.3.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@astrojs/cloudflare": ">=14",
|
|
52
52
|
"@astrojs/react": ">=7",
|
|
53
|
-
"@emdash-cms/cloudflare": ">=1.
|
|
54
|
-
"@getvitops/astro": ">=8.2.0
|
|
55
|
-
"@getvitops/core": ">=8.2.0
|
|
53
|
+
"@emdash-cms/cloudflare": ">=1.3.0 <2",
|
|
54
|
+
"@getvitops/astro": ">=8.2.0",
|
|
55
|
+
"@getvitops/core": ">=8.2.0",
|
|
56
56
|
"astro": ">=7",
|
|
57
|
-
"emdash": ">=1.
|
|
57
|
+
"emdash": ">=1.3.0 <2",
|
|
58
58
|
"react": ">=19",
|
|
59
59
|
"react-dom": ">=19"
|
|
60
60
|
},
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A group of native `<details>` — no JS at any tier. `exclusive` gives every item the
|
|
4
|
+
* same `name`, which the platform turns into "opening one closes the others". The name
|
|
5
|
+
* comes from the block's `_key`, so two accordions on a page never close each other.
|
|
6
|
+
*/
|
|
7
|
+
import { paragraphs, resolveBlockValue, rows, text } from './value.ts';
|
|
8
|
+
|
|
9
|
+
interface Value {
|
|
10
|
+
_key?: string;
|
|
11
|
+
items?: unknown;
|
|
12
|
+
exclusive?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const value = resolveBlockValue<Value>(Astro.props);
|
|
16
|
+
const items = rows<Record<string, unknown>>(value.items)
|
|
17
|
+
.map((row) => ({
|
|
18
|
+
summary: text(row.summary),
|
|
19
|
+
body: paragraphs(row.body),
|
|
20
|
+
open: Boolean(row.open),
|
|
21
|
+
}))
|
|
22
|
+
.filter((item) => item.summary);
|
|
23
|
+
const group = value.exclusive ? `accordion-${value._key ?? 'group'}` : undefined;
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
items.length > 0 && (
|
|
28
|
+
<div class="accordion">
|
|
29
|
+
{items.map((item) => (
|
|
30
|
+
<details name={group} open={item.open}>
|
|
31
|
+
<summary>{item.summary}</summary>
|
|
32
|
+
{item.body.map((p) => (
|
|
33
|
+
<p>{p}</p>
|
|
34
|
+
))}
|
|
35
|
+
</details>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* blocks.test.ts checks by literal substring, so reaching them via a spread would
|
|
20
20
|
* pass type-checking and fail the contract test.
|
|
21
21
|
*/
|
|
22
|
-
import { resolveBlockValue } from './value.ts';
|
|
22
|
+
import { resolveBlockValue, safeHref } from './value.ts';
|
|
23
23
|
|
|
24
24
|
interface Value {
|
|
25
25
|
label?: string;
|
|
@@ -37,7 +37,9 @@ interface Props {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const value = resolveBlockValue<Value>(Astro.props);
|
|
40
|
-
const { label,
|
|
40
|
+
const { label, variant = 'link', startIcon, endIcon, role, newTab = false } = value;
|
|
41
|
+
// A `javascript:`/`data:` href is dropped, so the block renders nothing rather than a script link.
|
|
42
|
+
const href = safeHref(value.href);
|
|
41
43
|
const { spriteBase = '/vitops' } = Astro.props as Props;
|
|
42
44
|
|
|
43
45
|
const sprite = `${spriteBase}/icons.svg`;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Thin adapter over `@getvitops/astro`'s `<Cards />`, which emits `<wc-cards>` itself —
|
|
4
|
+
* so this must not add its own wrapper. With JS a card holding a link becomes clickable
|
|
5
|
+
* as a whole while its text stays selectable; without JS the stretched link does the
|
|
6
|
+
* same job. The card CSS is the framework's `.card` pattern.
|
|
7
|
+
*
|
|
8
|
+
* The title is a styled paragraph rather than a fixed heading element: the block can
|
|
9
|
+
* sit at any depth in a document, and a hard-coded `<h3>` would skip or repeat levels.
|
|
10
|
+
*/
|
|
11
|
+
import Cards from '@getvitops/astro/components/Cards.astro';
|
|
12
|
+
import { resolveBlockValue, rows, safeHref, text } from './value.ts';
|
|
13
|
+
|
|
14
|
+
interface Value {
|
|
15
|
+
cards?: unknown;
|
|
16
|
+
columns?: string | number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const value = resolveBlockValue<Value>(Astro.props);
|
|
20
|
+
const cards = rows<Record<string, unknown>>(value.cards)
|
|
21
|
+
.map((row) => ({
|
|
22
|
+
title: text(row.title),
|
|
23
|
+
body: text(row.body),
|
|
24
|
+
image: safeHref(row.image),
|
|
25
|
+
imageAlt: text(row.imageAlt),
|
|
26
|
+
href: safeHref(row.href),
|
|
27
|
+
linkLabel: text(row.linkLabel) || 'Read more',
|
|
28
|
+
}))
|
|
29
|
+
.filter((card) => card.title || card.body || card.image);
|
|
30
|
+
const columns = Math.min(4, Math.max(1, Math.round(Number(value.columns) || 3)));
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
cards.length > 0 && (
|
|
35
|
+
<Cards style={`--subgrid-cols: ${columns}`}>
|
|
36
|
+
{cards.map((card) => (
|
|
37
|
+
<li class="card subgrid-card">
|
|
38
|
+
{card.image && <img src={card.image} alt={card.imageAlt} loading="lazy" />}
|
|
39
|
+
{card.title && <p class="font-heading">{card.title}</p>}
|
|
40
|
+
{card.body && <p>{card.body}</p>}
|
|
41
|
+
{card.href && (
|
|
42
|
+
<a class="link stretched-link" href={card.href}>
|
|
43
|
+
{card.linkLabel}
|
|
44
|
+
{/* "Read more" alone is meaningless in a list of links; name the card for AT. */}
|
|
45
|
+
{card.title && <span class="sr-only">: {card.title}</span>}
|
|
46
|
+
</a>
|
|
47
|
+
)}
|
|
48
|
+
</li>
|
|
49
|
+
))}
|
|
50
|
+
</Cards>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -3,14 +3,20 @@
|
|
|
3
3
|
* Thin adapter over `@getvitops/astro`'s `<Carousel />`.
|
|
4
4
|
*
|
|
5
5
|
* The markup, the image priority matrix and the accessibility wiring all live
|
|
6
|
-
* there — this only maps the block's
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* there — this only maps the block's fields onto its `slides` prop. Emitting the
|
|
7
|
+
* markup here too would be a second copy to keep in step, and the one that drifts
|
|
8
|
+
* is always the one nobody is looking at.
|
|
9
|
+
*
|
|
10
|
+
* `slides` (a repeater) is the editing surface. `images`/`alts` are the newline-
|
|
11
|
+
* separated fields every block saved before the repeater existed carries; they are
|
|
12
|
+
* still read when there are no `slides`, so existing content keeps rendering.
|
|
9
13
|
*/
|
|
10
14
|
import Carousel from '@getvitops/astro/components/Carousel.astro';
|
|
11
|
-
import { resolveBlockValue, splitLines } from './value.ts';
|
|
15
|
+
import { resolveBlockValue, rows, safeHref, splitLines, text } from './value.ts';
|
|
12
16
|
|
|
13
17
|
interface Value {
|
|
18
|
+
_key?: string;
|
|
19
|
+
slides?: unknown;
|
|
14
20
|
images?: string;
|
|
15
21
|
alts?: string;
|
|
16
22
|
label?: string;
|
|
@@ -20,19 +26,30 @@ interface Value {
|
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
const value = resolveBlockValue<Value>(Astro.props);
|
|
23
|
-
const
|
|
24
|
-
const
|
|
29
|
+
const fromRepeater = rows<{ src?: unknown; alt?: unknown }>(value.slides).flatMap((row) => {
|
|
30
|
+
const src = safeHref(row.src);
|
|
31
|
+
return src ? [{ src, alt: text(row.alt) }] : [];
|
|
32
|
+
});
|
|
33
|
+
const legacyAlts = splitLines(value.alts);
|
|
34
|
+
const fromLegacy = splitLines(value.images).flatMap((raw, i) => {
|
|
35
|
+
const src = safeHref(raw);
|
|
36
|
+
return src ? [{ src, alt: legacyAlts[i] ?? '' }] : [];
|
|
37
|
+
});
|
|
38
|
+
const slides = fromRepeater.length > 0 ? fromRepeater : fromLegacy;
|
|
39
|
+
// Two blocks with the same label would derive the same id; `_key` is unique per block.
|
|
40
|
+
const id = value._key ? `carousel-${value._key.replace(/[^a-z0-9_-]/gi, '')}` : undefined;
|
|
25
41
|
const autoplay = Number(value.autoplay) || 0;
|
|
26
42
|
---
|
|
27
43
|
|
|
28
44
|
{
|
|
29
|
-
|
|
45
|
+
slides.length > 0 && (
|
|
30
46
|
<Carousel
|
|
31
47
|
label={value.label || 'Carousel'}
|
|
32
48
|
autoplay={autoplay > 0 ? autoplay : undefined}
|
|
33
49
|
loop={Boolean(value.loop)}
|
|
34
50
|
aspect={value.aspect || '16 / 9'}
|
|
35
|
-
slides={
|
|
51
|
+
slides={slides}
|
|
52
|
+
id={id}
|
|
36
53
|
/>
|
|
37
54
|
)
|
|
38
55
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Thin adapter over `@getvitops/astro`'s `<Gallery />` — a thumbnail grid whose dialog
|
|
4
|
+
* is a carousel. The `<wc-gallery>` tag, the no-JS `:target` dialog and the image
|
|
5
|
+
* pipeline all live there; this only maps the repeater rows onto `images`.
|
|
6
|
+
*/
|
|
7
|
+
import Gallery from '@getvitops/astro/components/Gallery.astro';
|
|
8
|
+
import { resolveBlockValue, rows, safeHref, text } from './value.ts';
|
|
9
|
+
|
|
10
|
+
interface Value {
|
|
11
|
+
_key?: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
images?: unknown;
|
|
14
|
+
aspect?: string;
|
|
15
|
+
loop?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const value = resolveBlockValue<Value>(Astro.props);
|
|
19
|
+
// Two blocks with the same label would derive the same id; `_key` is unique per block.
|
|
20
|
+
const id = value._key ? `gallery-${value._key.replace(/[^a-z0-9_-]/gi, '')}` : undefined;
|
|
21
|
+
const images = rows<{ src?: unknown; alt?: unknown; caption?: unknown }>(value.images).flatMap(
|
|
22
|
+
(row) => {
|
|
23
|
+
const src = safeHref(row.src);
|
|
24
|
+
const caption = text(row.caption);
|
|
25
|
+
return src ? [{ src, alt: text(row.alt), ...(caption ? { caption } : {}) }] : [];
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
images.length > 0 && (
|
|
32
|
+
<Gallery
|
|
33
|
+
label={text(value.label) || 'Gallery'}
|
|
34
|
+
images={images}
|
|
35
|
+
id={id}
|
|
36
|
+
aspect={text(value.aspect) || undefined}
|
|
37
|
+
loop={Boolean(value.loop)}
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A page opener built from the layout primitives: `.centered` for a single column, `.split`
|
|
4
|
+
* for text beside an image. The heading is a real `<h1>` only when the editor chooses nothing
|
|
5
|
+
* else — a page has one, and EmDash renders the page title elsewhere, so this uses `<h2>`.
|
|
6
|
+
* Flat fields, because a block cannot contain other blocks.
|
|
7
|
+
*/
|
|
8
|
+
import { resolveBlockValue, safeHref, text } from './value.ts';
|
|
9
|
+
|
|
10
|
+
interface Value {
|
|
11
|
+
eyebrow?: string;
|
|
12
|
+
heading?: string;
|
|
13
|
+
body?: string;
|
|
14
|
+
primaryLabel?: string;
|
|
15
|
+
primaryHref?: string;
|
|
16
|
+
secondaryLabel?: string;
|
|
17
|
+
secondaryHref?: string;
|
|
18
|
+
image?: string;
|
|
19
|
+
imageAlt?: string;
|
|
20
|
+
layout?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const value = resolveBlockValue<Value>(Astro.props);
|
|
24
|
+
const eyebrow = text(value.eyebrow);
|
|
25
|
+
const heading = text(value.heading);
|
|
26
|
+
const body = text(value.body);
|
|
27
|
+
const primaryLabel = text(value.primaryLabel);
|
|
28
|
+
const primaryHref = safeHref(value.primaryHref);
|
|
29
|
+
const secondaryLabel = text(value.secondaryLabel);
|
|
30
|
+
const secondaryHref = safeHref(value.secondaryHref);
|
|
31
|
+
const image = safeHref(value.image);
|
|
32
|
+
const imageAlt = text(value.imageAlt);
|
|
33
|
+
const split = value.layout === 'split' && Boolean(image);
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
(heading || body) && (
|
|
38
|
+
<section class={split ? 'split split-1-2' : 'centered'}>
|
|
39
|
+
<div class="rhythm">
|
|
40
|
+
{eyebrow && <p class="font-eyebrow">{eyebrow}</p>}
|
|
41
|
+
{heading && <h2 class="font-display">{heading}</h2>}
|
|
42
|
+
{body && <p class="font-lead">{body}</p>}
|
|
43
|
+
{(primaryLabel && primaryHref) || (secondaryLabel && secondaryHref) ? (
|
|
44
|
+
<p class="cluster">
|
|
45
|
+
{primaryLabel && primaryHref && (
|
|
46
|
+
<a class="cta" href={primaryHref}>
|
|
47
|
+
{primaryLabel}
|
|
48
|
+
</a>
|
|
49
|
+
)}
|
|
50
|
+
{secondaryLabel && secondaryHref && (
|
|
51
|
+
<a class="link" href={secondaryHref}>
|
|
52
|
+
{secondaryLabel}
|
|
53
|
+
</a>
|
|
54
|
+
)}
|
|
55
|
+
</p>
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
{split && <img src={image} alt={imageAlt} loading="eager" />}
|
|
59
|
+
</section>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { resolveBlockValue } from './value.ts';
|
|
2
|
+
import { resolveBlockValue, safeHref } from './value.ts';
|
|
3
3
|
|
|
4
4
|
interface Value {
|
|
5
5
|
before?: string;
|
|
@@ -11,7 +11,10 @@ interface Value {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const value = resolveBlockValue<Value>(Astro.props);
|
|
14
|
-
const {
|
|
14
|
+
const { beforeAlt = '', beforeLabel = 'Before', afterAlt = '', afterLabel = 'After' } = value;
|
|
15
|
+
// A `javascript:`/`data:` src is dropped, so the block renders nothing rather than that image.
|
|
16
|
+
const before = safeHref(value.before);
|
|
17
|
+
const after = safeHref(value.after);
|
|
15
18
|
---
|
|
16
19
|
|
|
17
20
|
{before && after && (
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Thin adapter over `@getvitops/astro`'s `<Marquee />`, which emits `<wc-marquee>` itself
|
|
4
|
+
* and duplicates the slot (the copies are `aria-hidden`) for the seamless loop. Without
|
|
5
|
+
* JS the row still scrolls from CSS alone; `prefers-reduced-motion` is handled there.
|
|
6
|
+
*/
|
|
7
|
+
import Marquee from '@getvitops/astro/components/Marquee.astro';
|
|
8
|
+
import { resolveBlockValue, rows, safeHref, text } from './value.ts';
|
|
9
|
+
|
|
10
|
+
interface Value {
|
|
11
|
+
items?: unknown;
|
|
12
|
+
pauseOnHover?: boolean;
|
|
13
|
+
reverse?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const value = resolveBlockValue<Value>(Astro.props);
|
|
17
|
+
const items = rows<Record<string, unknown>>(value.items)
|
|
18
|
+
.map((row) => ({
|
|
19
|
+
text: text(row.text),
|
|
20
|
+
image: safeHref(row.image),
|
|
21
|
+
alt: text(row.alt),
|
|
22
|
+
}))
|
|
23
|
+
.filter((item) => item.text || item.image);
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
items.length > 0 && (
|
|
28
|
+
<Marquee pauseOnHover={Boolean(value.pauseOnHover)} reverse={Boolean(value.reverse)}>
|
|
29
|
+
{items.map((item) =>
|
|
30
|
+
item.image ? (
|
|
31
|
+
<img src={item.image} alt={item.alt} loading="lazy" />
|
|
32
|
+
) : (
|
|
33
|
+
<span>{item.text}</span>
|
|
34
|
+
),
|
|
35
|
+
)}
|
|
36
|
+
</Marquee>
|
|
37
|
+
)
|
|
38
|
+
}
|