@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 +79 -0
- package/dist/index.d.mts +38 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +245 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +30 -0
- package/src/astro/blocks/Banner.astro +27 -0
- package/src/astro/blocks/Carousel.astro +30 -0
- package/src/astro/blocks/CopyButton.astro +22 -0
- package/src/astro/blocks/Details.astro +19 -0
- package/src/astro/blocks/ImageCompare.astro +22 -0
- package/src/astro/blocks/value.ts +24 -0
- package/src/astro/index.ts +18 -0
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`.
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
};
|