@getvitops/emdash 0.1.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/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @getvitops/emdash
2
+
3
+ EmDash CMS **native plugin** for the Vitops design system. It gives content
4
+ editors Vitops patterns as Portable Text blocks in the EmDash admin
5
+ (`/_emdash/admin`) — inserted from the slash menu, edited with simple forms,
6
+ and rendered on the public site with the design-system web components and CSS
7
+ framework (accessible no-JS fallbacks included).
8
+
9
+ ## Install
10
+
11
+ The plugin **composes with** the `getvitops()` Astro integration from
12
+ `@getvitops/astro` — the integration generates the design-system CSS and copies
13
+ the web-component bundles into `public/vitops/`; this plugin adds the
14
+ editor-facing layer. You need both:
15
+
16
+ ```js
17
+ // astro.config.mjs
18
+ import react from '@astrojs/react';
19
+ import getvitops from '@getvitops/astro';
20
+ import { vitopsEmdash } from '@getvitops/emdash';
21
+ import emdash, { local } from 'emdash/astro';
22
+ import { sqlite } from 'emdash/db';
23
+
24
+ export default defineConfig({
25
+ output: 'server',
26
+ adapter: /* node/cloudflare */,
27
+ integrations: [
28
+ react(),
29
+ getvitops({ css: { input: 'design-system.json', format: 'tailwind', out: 'src/styles' } }),
30
+ emdash({
31
+ database: sqlite({ url: 'file:./data.db' }),
32
+ storage: local({ directory: './uploads', baseUrl: '/_emdash/api/media/file' }),
33
+ plugins: [vitopsEmdash()],
34
+ }),
35
+ ],
36
+ });
37
+ ```
38
+
39
+ Tested against `emdash@0.31.x`.
40
+
41
+ ## Editor blocks (v1)
42
+
43
+ | Slash-menu entry | `_type` | Renders |
44
+ | ---------------- | --------------------- | ------------------------------------------- |
45
+ | Image compare | `vitops.imageCompare` | `<wc-image-compare>` before/after slider |
46
+ | Copy snippet | `vitops.copyButton` | `<copy-button>` with code/inline fallback |
47
+ | Banner | `vitops.banner` | `<wc-dismissable>` banner with tone colours |
48
+ | Disclosure | `vitops.details` | native `<details>`/`<summary>` |
49
+ | Carousel | `vitops.carousel` | `<wc-carousel>` scroll-snap carousel |
50
+
51
+ All blocks render accessible markup that works without JavaScript; the
52
+ web components progressively enhance it.
53
+
54
+ ## Script delivery
55
+
56
+ The web-component runtime (`/vitops/{polyfills,elements,deferred}.js`) must be
57
+ loaded on pages that render these blocks. Pick **one**:
58
+
59
+ - **`scripts: 'integration'` (default)** — your layout renders `<Head />` from
60
+ `@getvitops/astro`, which emits the script tags.
61
+ - **`vitopsEmdash({ scripts: 'fragments' })`** — for layouts built on EmDash's
62
+ `<EmDashHead/>`/`<EmDashBodyEnd/>` components: the plugin injects the tags
63
+ via the `page:fragments` hook (requires the `hooks.page-fragments:register`
64
+ capability, which the descriptor declares automatically).
65
+
66
+ Enabling both would load the scripts twice (harmless — element registration is
67
+ guarded — but wasteful).
68
+
69
+ ## Repeating structured patterns
70
+
71
+ EmDash's Portable Text block fields are flat, so patterns with repeating data
72
+ (card grids, FAQ lists, spec tables via `wc-entries`, forms) are deliberately
73
+ **not** v1 blocks. Use instead:
74
+
75
+ - **Sections** (`/section`): compose the pattern once as reusable content and
76
+ let editors insert copies.
77
+ - **Field Kit `list` widgets** on `json` collection fields: model the repeating
78
+ rows in the collection schema and render them at the template level with
79
+ `Cards.astro` / `NodeRenderer.astro` from `@getvitops/astro`.
@@ -0,0 +1,38 @@
1
+ import { PluginDescriptor, PortableTextBlockConfig } from "emdash";
2
+
3
+ //#region src/blocks.d.ts
4
+ /** The v1 subset of EmDash Block Kit field elements used by these blocks. */
5
+ type BlockKitFieldDef = Extract<NonNullable<PortableTextBlockConfig['fields']>[number], {
6
+ type: 'text_input' | 'number_input' | 'select' | 'toggle';
7
+ }>;
8
+ type PortableTextBlockDef = Omit<PortableTextBlockConfig, 'fields'> & {
9
+ fields?: BlockKitFieldDef[];
10
+ };
11
+ declare const BLOCKS: PortableTextBlockDef[];
12
+ //#endregion
13
+ //#region src/index.d.ts
14
+ interface VitopsEmdashOptions {
15
+ /**
16
+ * How the web-component runtime scripts reach the page:
17
+ * - `'integration'` (default) — the site's layout renders `<Head />` from
18
+ * `@getvitops/astro`, which emits the script tags; this plugin adds none.
19
+ * - `'fragments'` — for layouts using EmDash's `<EmDashHead/>`/`<EmDashBodyEnd/>`
20
+ * instead: the plugin injects the script tags via the `page:fragments` hook.
21
+ * Either way the bundles themselves must exist under `public/<wcBase>/`,
22
+ * which the `getvitops()` integration provides (webComponents: true).
23
+ * Do not enable both delivery paths — the scripts would load twice.
24
+ */
25
+ scripts?: 'integration' | 'fragments';
26
+ /** Public base path of the web-component bundles (default '/vitops'). */
27
+ wcBase?: string;
28
+ }
29
+ /**
30
+ * Descriptor factory — import in `astro.config.mjs` and pass to the emdash()
31
+ * integration's `plugins` array.
32
+ */
33
+ declare function vitopsEmdash(options?: VitopsEmdashOptions): PluginDescriptor;
34
+ /** Runtime side — EmDash imports and calls this from the descriptor's entrypoint. */
35
+ declare function createPlugin(options?: VitopsEmdashOptions): import("emdash").ResolvedPlugin<import("emdash").PluginStorageConfig>;
36
+ //#endregion
37
+ export { BLOCKS, type BlockKitFieldDef, type PortableTextBlockDef, VitopsEmdashOptions, createPlugin, createPlugin as default, vitopsEmdash };
38
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/blocks.ts","../src/index.ts"],"mappings":";;;;KAeY,gBAAA,GAAmB,OAAA,CAC7B,WAAA,CAAY,uBAAA;EACV,IAAA;AAAA;AAAA,KAGQ,oBAAA,GAAuB,IAAA,CAAK,uBAAA;EACtC,MAAA,GAAS,gBAAA;AAAA;AAAA,cAGE,MAAA,EAAQ,oBAAoB;;;UCSxB,mBAAA;EDlBW;;;;;;;;;;EC6B1B,OAAA;ED3BM;EC6BN,MAAM;AAAA;;;;;iBAOQ,YAAA,CAAa,OAAA,GAAS,mBAAA,GAA2B,gBAAgB;;iBAajE,YAAA,CAAa,OAAA,GAAS,mBAAwB,oBAAA,cAAA,kBAAA,mBAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,245 @@
1
+ import { definePlugin } from "emdash";
2
+
3
+ //#region src/blocks.ts
4
+ const BLOCKS = [
5
+ {
6
+ type: "vitops.imageCompare",
7
+ label: "Image compare",
8
+ placeholder: "Before/after image slider",
9
+ fields: [
10
+ {
11
+ type: "text_input",
12
+ action_id: "before",
13
+ label: "Before image URL"
14
+ },
15
+ {
16
+ type: "text_input",
17
+ action_id: "beforeAlt",
18
+ label: "Before alt text"
19
+ },
20
+ {
21
+ type: "text_input",
22
+ action_id: "beforeLabel",
23
+ label: "Before label",
24
+ initial_value: "Before"
25
+ },
26
+ {
27
+ type: "text_input",
28
+ action_id: "after",
29
+ label: "After image URL"
30
+ },
31
+ {
32
+ type: "text_input",
33
+ action_id: "afterAlt",
34
+ label: "After alt text"
35
+ },
36
+ {
37
+ type: "text_input",
38
+ action_id: "afterLabel",
39
+ label: "After label",
40
+ initial_value: "After"
41
+ }
42
+ ]
43
+ },
44
+ {
45
+ type: "vitops.copyButton",
46
+ label: "Copy snippet",
47
+ icon: "code",
48
+ placeholder: "Text with a copy-to-clipboard button",
49
+ fields: [
50
+ {
51
+ type: "text_input",
52
+ action_id: "text",
53
+ label: "Text to copy",
54
+ multiline: true
55
+ },
56
+ {
57
+ type: "text_input",
58
+ action_id: "label",
59
+ label: "Button label",
60
+ initial_value: "Copy"
61
+ },
62
+ {
63
+ type: "select",
64
+ action_id: "display",
65
+ label: "Display",
66
+ initial_value: "code-block",
67
+ options: [{
68
+ label: "Code block",
69
+ value: "code-block"
70
+ }, {
71
+ label: "Inline",
72
+ value: "inline"
73
+ }]
74
+ }
75
+ ]
76
+ },
77
+ {
78
+ type: "vitops.banner",
79
+ label: "Banner",
80
+ placeholder: "Announcement or notice",
81
+ fields: [
82
+ {
83
+ type: "text_input",
84
+ action_id: "message",
85
+ label: "Message",
86
+ multiline: true
87
+ },
88
+ {
89
+ type: "select",
90
+ action_id: "tone",
91
+ label: "Tone",
92
+ initial_value: "info",
93
+ options: [
94
+ {
95
+ label: "Info",
96
+ value: "info"
97
+ },
98
+ {
99
+ label: "Success",
100
+ value: "success"
101
+ },
102
+ {
103
+ label: "Warning",
104
+ value: "warning"
105
+ },
106
+ {
107
+ label: "Danger",
108
+ value: "danger"
109
+ }
110
+ ]
111
+ },
112
+ {
113
+ type: "toggle",
114
+ action_id: "dismissible",
115
+ label: "Dismissible",
116
+ initial_value: true
117
+ }
118
+ ]
119
+ },
120
+ {
121
+ type: "vitops.details",
122
+ label: "Disclosure",
123
+ placeholder: "Collapsible details/summary",
124
+ fields: [
125
+ {
126
+ type: "text_input",
127
+ action_id: "summary",
128
+ label: "Summary (always visible)"
129
+ },
130
+ {
131
+ type: "text_input",
132
+ action_id: "body",
133
+ label: "Body (revealed)",
134
+ multiline: true
135
+ },
136
+ {
137
+ type: "toggle",
138
+ action_id: "open",
139
+ label: "Open by default",
140
+ initial_value: false
141
+ }
142
+ ]
143
+ },
144
+ {
145
+ type: "vitops.carousel",
146
+ label: "Carousel",
147
+ placeholder: "Image carousel",
148
+ fields: [
149
+ {
150
+ type: "text_input",
151
+ action_id: "images",
152
+ label: "Image URLs (one per line)",
153
+ multiline: true
154
+ },
155
+ {
156
+ type: "text_input",
157
+ action_id: "alts",
158
+ label: "Alt texts (one per line, same order)",
159
+ multiline: true
160
+ },
161
+ {
162
+ type: "text_input",
163
+ action_id: "label",
164
+ label: "Accessible name",
165
+ initial_value: "Carousel"
166
+ },
167
+ {
168
+ type: "number_input",
169
+ action_id: "autoplay",
170
+ label: "Autoplay interval (ms, 0 = off)",
171
+ initial_value: 0
172
+ }
173
+ ]
174
+ }
175
+ ];
176
+
177
+ //#endregion
178
+ //#region src/index.ts
179
+ /**
180
+ * @getvitops/emdash — EmDash CMS native plugin for the Vitops design system.
181
+ *
182
+ * Declares Vitops Portable Text block types (editable from the EmDash admin's
183
+ * slash menu) and ships the Astro components that render them via the
184
+ * `componentsEntry` (`@getvitops/emdash/astro`).
185
+ *
186
+ * Composes with — does not replace — the `getvitops()` Astro integration from
187
+ * `@getvitops/astro`: the integration owns design-system CSS generation and
188
+ * copies the web-component bundles into `public/<wcBase>/`; this plugin only
189
+ * adds the editor-facing layer. Register both in the consumer site:
190
+ *
191
+ * ```js
192
+ * // astro.config.mjs
193
+ * integrations: [
194
+ * react(),
195
+ * getvitops({ css: {...} }),
196
+ * emdash({ database, storage, plugins: [vitopsEmdash()] }),
197
+ * ]
198
+ * ```
199
+ */
200
+ /** Keep in sync with package.json (asserted by index.test.ts). */
201
+ const VERSION = "0.1.0";
202
+ const PLUGIN_ID = "vitops";
203
+ const FRAGMENTS_CAPABILITY = "hooks.page-fragments:register";
204
+ /**
205
+ * Descriptor factory — import in `astro.config.mjs` and pass to the emdash()
206
+ * integration's `plugins` array.
207
+ */
208
+ function vitopsEmdash(options = {}) {
209
+ return {
210
+ id: PLUGIN_ID,
211
+ version: VERSION,
212
+ format: "native",
213
+ entrypoint: "@getvitops/emdash",
214
+ componentsEntry: "@getvitops/emdash/astro",
215
+ options,
216
+ capabilities: options.scripts === "fragments" ? [FRAGMENTS_CAPABILITY] : []
217
+ };
218
+ }
219
+ /** Runtime side — EmDash imports and calls this from the descriptor's entrypoint. */
220
+ function createPlugin(options = {}) {
221
+ const scripts = options.scripts ?? "integration";
222
+ const wcBase = options.wcBase ?? "/vitops";
223
+ return definePlugin({
224
+ id: PLUGIN_ID,
225
+ version: VERSION,
226
+ capabilities: scripts === "fragments" ? [FRAGMENTS_CAPABILITY] : [],
227
+ hooks: scripts === "fragments" ? { "page:fragments": async () => [{
228
+ kind: "html",
229
+ placement: "head",
230
+ html: `<link rel="modulepreload" href="${wcBase}/polyfills.js"><script type="module" src="${wcBase}/polyfills.js"><\/script><script type="module" src="${wcBase}/elements.js"><\/script>`,
231
+ key: "vitops-wc"
232
+ }, {
233
+ kind: "external-script",
234
+ placement: "body:end",
235
+ src: `${wcBase}/deferred.js`,
236
+ defer: true,
237
+ key: "vitops-deferred"
238
+ }] } : {},
239
+ admin: { portableTextBlocks: BLOCKS }
240
+ });
241
+ }
242
+
243
+ //#endregion
244
+ export { BLOCKS, createPlugin, createPlugin as default, vitopsEmdash };
245
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/blocks.ts","../src/index.ts"],"sourcesContent":["/**\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';\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' }\n>;\n\nexport type PortableTextBlockDef = Omit<PortableTextBlockConfig, 'fields'> & {\n fields?: BlockKitFieldDef[];\n};\n\nexport const BLOCKS: PortableTextBlockDef[] = [\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 },\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 `getvitops()` 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 * integrations: [\n * react(),\n * getvitops({ css: {...} }),\n * emdash({ database, storage, plugins: [vitopsEmdash()] }),\n * ]\n * ```\n */\nimport { definePlugin } from 'emdash';\nimport type { PluginDescriptor } from 'emdash';\nimport { BLOCKS } from './blocks.ts';\n\nexport { BLOCKS } from './blocks.ts';\nexport type { BlockKitFieldDef, PortableTextBlockDef } from './blocks.ts';\n\n/** Keep in sync with package.json (asserted by index.test.ts). */\nconst VERSION = '0.1.0';\nconst PLUGIN_ID = 'vitops';\nconst FRAGMENTS_CAPABILITY = 'hooks.page-fragments:register';\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 `getvitops()` 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\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,\n capabilities: options.scripts === 'fragments' ? [FRAGMENTS_CAPABILITY] : [],\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\n return definePlugin({\n id: PLUGIN_ID,\n version: VERSION,\n capabilities: scripts === 'fragments' ? [FRAGMENTS_CAPABILITY] : [],\n\n hooks:\n scripts === 'fragments'\n ? {\n // Mirrors @getvitops/astro Head.astro: module polyfills + elements\n // in head, deferred enhancements at end of body.\n 'page:fragments': async () => [\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 : {},\n\n admin: {\n portableTextBlocks: BLOCKS,\n },\n });\n}\n\nexport default createPlugin;\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@getvitops/emdash",
3
+ "version": "0.1.0",
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
+ "license": "MIT",
6
+ "files": [
7
+ "dist",
8
+ "src/astro"
9
+ ],
10
+ "type": "module",
11
+ "types": "./dist/index.d.mts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.mts",
15
+ "import": "./dist/index.mjs"
16
+ },
17
+ "./astro": "./src/astro/index.ts",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "devDependencies": {
24
+ "emdash": "^0.31.0",
25
+ "vitest": "^4.1.9"
26
+ },
27
+ "peerDependencies": {
28
+ "emdash": ">=0.31.0"
29
+ }
30
+ }
@@ -0,0 +1,27 @@
1
+ ---
2
+ import { resolveBlockValue } from './value.ts';
3
+
4
+ interface Value {
5
+ message?: string;
6
+ tone?: 'info' | 'success' | 'warning' | 'danger';
7
+ dismissible?: boolean;
8
+ }
9
+
10
+ const value = resolveBlockValue<Value>(Astro.props);
11
+ const { message, tone = 'info', dismissible = true } = value;
12
+ const bannerClass = `banner bg-${tone}${dismissible ? ' banner--dismissible' : ''}`;
13
+ ---
14
+
15
+ {message && dismissible && (
16
+ <wc-dismissable>
17
+ <div class={bannerClass}>
18
+ <div class="banner__content">{message}</div>
19
+ <button class="banner__close" data-dismiss aria-label="Dismiss">✕</button>
20
+ </div>
21
+ </wc-dismissable>
22
+ )}
23
+ {message && !dismissible && (
24
+ <div class={bannerClass}>
25
+ <div class="banner__content">{message}</div>
26
+ </div>
27
+ )}
@@ -0,0 +1,30 @@
1
+ ---
2
+ import { resolveBlockValue, splitLines } from './value.ts';
3
+
4
+ interface Value {
5
+ images?: string;
6
+ alts?: string;
7
+ label?: string;
8
+ autoplay?: number;
9
+ }
10
+
11
+ const value = resolveBlockValue<Value>(Astro.props);
12
+ const images = splitLines(value.images);
13
+ const alts = splitLines(value.alts);
14
+ const label = value.label || 'Carousel';
15
+ const autoplay = Number(value.autoplay) || 0;
16
+ ---
17
+
18
+ {images.length > 0 && (
19
+ <wc-carousel
20
+ class="carousel carousel--scroll-buttons carousel--scroll-markers"
21
+ role="region"
22
+ aria-roledescription="carousel"
23
+ aria-label={label}
24
+ autoplay={autoplay > 0 ? autoplay : undefined}
25
+ >
26
+ {images.map((src, i) => (
27
+ <div><img src={src} alt={alts[i] ?? ''} loading="lazy" /></div>
28
+ ))}
29
+ </wc-carousel>
30
+ )}
@@ -0,0 +1,22 @@
1
+ ---
2
+ import { resolveBlockValue } from './value.ts';
3
+
4
+ interface Value {
5
+ text?: string;
6
+ label?: string;
7
+ display?: 'inline' | 'code-block';
8
+ }
9
+
10
+ const value = resolveBlockValue<Value>(Astro.props);
11
+ const { text, label = 'Copy', display = 'code-block' } = value;
12
+ ---
13
+
14
+ {text && display === 'code-block' && (
15
+ <div class="copy">
16
+ <pre><code>{text}</code></pre>
17
+ <copy-button value={text} label={label}>{label}</copy-button>
18
+ </div>
19
+ )}
20
+ {text && display === 'inline' && (
21
+ <copy-button value={text} label={label}><code>{text}</code></copy-button>
22
+ )}
@@ -0,0 +1,19 @@
1
+ ---
2
+ import { resolveBlockValue } from './value.ts';
3
+
4
+ interface Value {
5
+ summary?: string;
6
+ body?: string;
7
+ open?: boolean;
8
+ }
9
+
10
+ const value = resolveBlockValue<Value>(Astro.props);
11
+ const { summary, body, open = false } = value;
12
+ ---
13
+
14
+ {summary && (
15
+ <details open={open}>
16
+ <summary>{summary}</summary>
17
+ <p>{body}</p>
18
+ </details>
19
+ )}
@@ -0,0 +1,22 @@
1
+ ---
2
+ import { resolveBlockValue } from './value.ts';
3
+
4
+ interface Value {
5
+ before?: string;
6
+ beforeAlt?: string;
7
+ beforeLabel?: string;
8
+ after?: string;
9
+ afterAlt?: string;
10
+ afterLabel?: string;
11
+ }
12
+
13
+ const value = resolveBlockValue<Value>(Astro.props);
14
+ const { before, beforeAlt = '', beforeLabel = 'Before', after, afterAlt = '', afterLabel = 'After' } = value;
15
+ ---
16
+
17
+ {before && after && (
18
+ <wc-image-compare before-label={beforeLabel} after-label={afterLabel}>
19
+ <img slot="before" src={before} alt={beforeAlt} loading="lazy" />
20
+ <img slot="after" src={after} alt={afterAlt} loading="lazy" />
21
+ </wc-image-compare>
22
+ )}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Normalize the props EmDash passes to a Portable Text block component.
3
+ *
4
+ * Verified against emdash 0.31: <PortableText> delegates to astro-portabletext,
5
+ * which passes the block object as `props.node` (fields flat on the block next
6
+ * to _type/_key). The `value` and spread branches are kept as tolerance for the
7
+ * shapes shown elsewhere in the EmDash docs, so a host-side change is a
8
+ * one-line fix here rather than a change in every block component.
9
+ */
10
+ export function resolveBlockValue<T extends Record<string, unknown>>(
11
+ props: Record<string, unknown>,
12
+ ): T {
13
+ const candidate = props.value ?? props.node ?? props;
14
+ return candidate as T;
15
+ }
16
+
17
+ /** Split a newline/comma-separated editor field into trimmed non-empty items. */
18
+ export function splitLines(input: unknown): string[] {
19
+ if (typeof input !== 'string') return [];
20
+ return input
21
+ .split(/[\n,]/)
22
+ .map((s) => s.trim())
23
+ .filter(Boolean);
24
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * componentsEntry for the vitops EmDash plugin. EmDash merges this map into
3
+ * <PortableText> automatically — the export name MUST be `blockComponents`
4
+ * and keys MUST match the block `type`s declared in ../blocks.ts.
5
+ */
6
+ import Banner from './blocks/Banner.astro';
7
+ import Carousel from './blocks/Carousel.astro';
8
+ import CopyButton from './blocks/CopyButton.astro';
9
+ import Details from './blocks/Details.astro';
10
+ import ImageCompare from './blocks/ImageCompare.astro';
11
+
12
+ export const blockComponents = {
13
+ 'vitops.imageCompare': ImageCompare,
14
+ 'vitops.copyButton': CopyButton,
15
+ 'vitops.banner': Banner,
16
+ 'vitops.details': Details,
17
+ 'vitops.carousel': Carousel,
18
+ };