@duffcloudservices/kit 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 +48 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives/DcsImage.vue.d.ts +21 -0
- package/dist/primitives/DcsSection.vue.d.ts +39 -0
- package/dist/primitives/DcsText.vue.d.ts +41 -0
- package/dist/primitives/index.d.ts +11 -0
- package/dist/style.css +1 -0
- package/package.json +67 -0
- package/src/__tests__/DcsImage.test.ts +29 -0
- package/src/__tests__/DcsSection.test.ts +39 -0
- package/src/__tests__/DcsText.test.ts +42 -0
- package/src/index.ts +24 -0
- package/src/primitives/DcsImage.vue +25 -0
- package/src/primitives/DcsSection.vue +33 -0
- package/src/primitives/DcsText.vue +39 -0
- package/src/primitives/index.ts +11 -0
- package/src/shims.d.ts +10 -0
- package/src/styles/layers.css +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @duffcloudservices/kit
|
|
2
|
+
|
|
3
|
+
DcsKit Vue runtime — the editable primitives (`DcsText` / `DcsImage` / `DcsSection`,
|
|
4
|
+
authoring sugar over the `[data-dcs-*]` editor-bridge markers) plus the canonical
|
|
5
|
+
cascade-layer contract (`@layer theme, base, components, utilities`). The Vue-side
|
|
6
|
+
companion to `@duffcloudservices/kit-vite` (the Vite plugin bundle / `defineDcsSite()`).
|
|
7
|
+
|
|
8
|
+
Part of the first-party site-framework initiative — see
|
|
9
|
+
[`.docs/plans/first-party-site-framework/`](../../.docs/plans/first-party-site-framework/).
|
|
10
|
+
|
|
11
|
+
## Status (milestone M9)
|
|
12
|
+
|
|
13
|
+
**Publish-ready, first publish is maintainer-gated.** The package builds (`dist/index.js`
|
|
14
|
+
+ `dist/style.css` + d.ts), exports the three primitives, and passes 8 Vitest marker
|
|
15
|
+
assertions. `@duffcloudservices/kit-vite` is likewise scaffolded and publish-ready.
|
|
16
|
+
|
|
17
|
+
## Publishing
|
|
18
|
+
|
|
19
|
+
The package has **never been published**, so the first publish must be done manually by a
|
|
20
|
+
maintainer with npm credentials (trusted/CI publishing can't be configured on npmjs.com
|
|
21
|
+
until the package exists):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd packages/kit
|
|
25
|
+
npm login # maintainer npm account
|
|
26
|
+
npm publish --access public
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then configure trusted publishing on npmjs.com (Owner=NateDuff, Repository=dcs,
|
|
30
|
+
Workflow=`npm-publish.yml`) so future releases publish automatically by pushing an
|
|
31
|
+
`npm-v<version>` tag — see [`.github/workflows/npm-publish.yml`](../../.github/workflows/npm-publish.yml).
|
|
32
|
+
|
|
33
|
+
## Consuming it in a pilot site (after the first publish)
|
|
34
|
+
|
|
35
|
+
The pilot sites currently consume `@duffcloudservices/cms` from npm and prove the kit
|
|
36
|
+
patterns in-repo. Once `@duffcloudservices/kit` is on npm, swapping a site to consume it
|
|
37
|
+
is a normal semver bump — e.g. Iron Oak's `site/src/kit/index.ts` barrel re-exports from
|
|
38
|
+
`@duffcloudservices/kit` instead of `@duffcloudservices/cms`:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cd <site>/site # e.g. E:/source/repos/handyman-bryan/site
|
|
42
|
+
pnpm add @duffcloudservices/kit@^0.1.0
|
|
43
|
+
# then point site/src/kit/index.ts at @duffcloudservices/kit and `pnpm build` to verify
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
No `file:` / `pnpm link` gymnastics — the same registry path the sites already use for cms.
|
|
47
|
+
Ensure `kit` and `cms` declare overlapping `@duffcloudservices/cms-core` ranges so a site
|
|
48
|
+
gets a single deduped `cms-core` (`pnpm why @duffcloudservices/cms-core`).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './primitives';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineComponent as a, createBlock as n, openBlock as c, resolveDynamicComponent as l, withCtx as o, renderSlot as d, createElementBlock as i } from "vue";
|
|
2
|
+
const u = /* @__PURE__ */ a({
|
|
3
|
+
__name: "DcsText",
|
|
4
|
+
props: {
|
|
5
|
+
textKey: {},
|
|
6
|
+
as: { default: "span" },
|
|
7
|
+
legacy: { type: Boolean, default: !1 }
|
|
8
|
+
},
|
|
9
|
+
setup(e) {
|
|
10
|
+
return (t, s) => (c(), n(l(e.as), {
|
|
11
|
+
"data-dcs-text": e.legacy ? void 0 : e.textKey,
|
|
12
|
+
"data-text-key": e.legacy ? e.textKey : void 0
|
|
13
|
+
}, {
|
|
14
|
+
default: o(() => [
|
|
15
|
+
d(t.$slots, "default")
|
|
16
|
+
]),
|
|
17
|
+
_: 3
|
|
18
|
+
}, 8, ["data-dcs-text", "data-text-key"]));
|
|
19
|
+
}
|
|
20
|
+
}), m = ["data-dcs-image-key", "src", "alt"], y = /* @__PURE__ */ a({
|
|
21
|
+
__name: "DcsImage",
|
|
22
|
+
props: {
|
|
23
|
+
imageKey: {},
|
|
24
|
+
src: {},
|
|
25
|
+
alt: {}
|
|
26
|
+
},
|
|
27
|
+
setup(e) {
|
|
28
|
+
return (t, s) => (c(), i("img", {
|
|
29
|
+
"data-dcs-image-key": e.imageKey,
|
|
30
|
+
src: e.src,
|
|
31
|
+
alt: e.alt ?? ""
|
|
32
|
+
}, null, 8, m));
|
|
33
|
+
}
|
|
34
|
+
}), f = /* @__PURE__ */ a({
|
|
35
|
+
__name: "DcsSection",
|
|
36
|
+
props: {
|
|
37
|
+
section: {},
|
|
38
|
+
label: {},
|
|
39
|
+
as: { default: "section" }
|
|
40
|
+
},
|
|
41
|
+
setup(e) {
|
|
42
|
+
return (t, s) => (c(), n(l(e.as), {
|
|
43
|
+
"data-section": e.section,
|
|
44
|
+
"data-section-label": e.label
|
|
45
|
+
}, {
|
|
46
|
+
default: o(() => [
|
|
47
|
+
d(t.$slots, "default")
|
|
48
|
+
]),
|
|
49
|
+
_: 3
|
|
50
|
+
}, 8, ["data-section", "data-section-label"]));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
export {
|
|
54
|
+
y as DcsImage,
|
|
55
|
+
f as DcsSection,
|
|
56
|
+
u as DcsText
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/primitives/DcsText.vue","../src/primitives/DcsImage.vue","../src/primitives/DcsSection.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * DcsText — editable text primitive.\n *\n * Authoring sugar over the `[data-dcs-text]` marker that `editorBridge.ts`\n * discovers (see `TEXT_KEY_SELECTOR = '[data-text-key], [data-dcs-text]'` and\n * `getTextKey()` which reads `dataset.dcsText ?? dataset.textKey`). Rendering a\n * `<DcsText text-key=\"hero.title\">` instead of a hand-authored\n * `<span data-dcs-text=\"hero.title\">` makes the marker correct *by construction*\n * — the recurring \"forgot the marker → not editable\" drift the kit retires.\n *\n * Defaults to the modern `data-dcs-text` attribute. Pass `legacy` to emit the\n * pre-migration `data-text-key` attribute instead; the bridge reads either.\n */\nwithDefaults(\n defineProps<{\n /** The content key the visual editor binds this text to. */\n textKey: string\n /** Element tag to render. Defaults to an inline `<span>`. */\n as?: string\n /** Emit the legacy `data-text-key` attribute instead of `data-dcs-text`. */\n legacy?: boolean\n }>(),\n {\n as: 'span',\n legacy: false,\n },\n)\n</script>\n\n<template>\n <component\n :is=\"as\"\n :data-dcs-text=\"legacy ? undefined : textKey\"\n :data-text-key=\"legacy ? textKey : undefined\"\n >\n <slot />\n </component>\n</template>\n","<script setup lang=\"ts\">\n/**\n * DcsImage — editable image primitive.\n *\n * Authoring sugar over the `data-dcs-image-key` marker that `editorBridge.ts`\n * reads via `getManagedImageKey()`:\n * img.dataset.dcsImageKey || img.closest('[data-dcs-image-key]')?.dataset.dcsImageKey\n * Rendering `<DcsImage image-key=\"hero.photo\" :src=\"…\">` instead of a\n * hand-authored `<img data-dcs-image-key=\"…\">` makes the managed-image marker\n * correct *by construction*, so the editor's \"Replace image\" affordance is wired\n * without per-site hand-authoring.\n */\ndefineProps<{\n /** The managed-image key the visual editor binds this image to. */\n imageKey: string\n /** Image source URL. */\n src: string\n /** Alt text — also surfaced to the editor's image-replace payload. */\n alt?: string\n}>()\n</script>\n\n<template>\n <img :data-dcs-image-key=\"imageKey\" :src=\"src\" :alt=\"alt ?? ''\" />\n</template>\n","<script setup lang=\"ts\">\n/**\n * DcsSection — editable section wrapper primitive.\n *\n * Authoring sugar over the `[data-section]` marker that `editorBridge.ts`\n * discovers in `discoverSections()`:\n * id = el.dataset.section\n * label = el.dataset.sectionLabel ?? null\n * The bridge groups text/image markers by their nearest `[data-section]`\n * ancestor and reports section bounds to the portal overlay layer. Rendering\n * `<DcsSection section=\"hero\" label=\"Hero\">` makes that grouping marker correct\n * *by construction* instead of hand-authored per view.\n */\nwithDefaults(\n defineProps<{\n /** The section id reported to the editor (`data-section`). */\n section: string\n /** Human label shown in the editor's section list (`data-section-label`). */\n label?: string\n /** Element tag to render. Defaults to a `<section>`. */\n as?: string\n }>(),\n {\n as: 'section',\n },\n)\n</script>\n\n<template>\n <component :is=\"as\" :data-section=\"section\" :data-section-label=\"label\">\n <slot />\n </component>\n</template>\n"],"names":["_openBlock","_createBlock","_resolveDynamicComponent","__props","_renderSlot","_ctx","_createElementBlock"],"mappings":";;;;;;;;;sBA+BEA,KAAAC,EAMYC,EALLC,EAAA,EAAE,GAAA;AAAA,MACN,iBAAeA,EAAA,SAAS,SAAYA,EAAA;AAAA,MACpC,iBAAeA,EAAA,SAASA,EAAA,UAAU;AAAA,IAAA;iBAEnC,MAAQ;AAAA,QAARC,EAAQC,EAAA,QAAA,SAAA;AAAA,MAAA;;;;;;;;;;;;2BCbVC,EAAkE,OAAA;AAAA,MAA5D,sBAAoBH,EAAA;AAAA,MAAW,KAAKA,EAAA;AAAA,MAAM,KAAKA,EAAA,OAAG;AAAA,IAAA;;;;;;;;;;sBCMxDH,KAAAC,EAEYC,EAFIC,EAAA,EAAE,GAAA;AAAA,MAAG,gBAAcA,EAAA;AAAA,MAAU,sBAAoBA,EAAA;AAAA,IAAA;iBAC/D,MAAQ;AAAA,QAARC,EAAQC,EAAA,QAAA,SAAA;AAAA,MAAA;;;;;"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DcsImage — editable image primitive.
|
|
3
|
+
*
|
|
4
|
+
* Authoring sugar over the `data-dcs-image-key` marker that `editorBridge.ts`
|
|
5
|
+
* reads via `getManagedImageKey()`:
|
|
6
|
+
* img.dataset.dcsImageKey || img.closest('[data-dcs-image-key]')?.dataset.dcsImageKey
|
|
7
|
+
* Rendering `<DcsImage image-key="hero.photo" :src="…">` instead of a
|
|
8
|
+
* hand-authored `<img data-dcs-image-key="…">` makes the managed-image marker
|
|
9
|
+
* correct *by construction*, so the editor's "Replace image" affordance is wired
|
|
10
|
+
* without per-site hand-authoring.
|
|
11
|
+
*/
|
|
12
|
+
type __VLS_Props = {
|
|
13
|
+
/** The managed-image key the visual editor binds this image to. */
|
|
14
|
+
imageKey: string;
|
|
15
|
+
/** Image source URL. */
|
|
16
|
+
src: string;
|
|
17
|
+
/** Alt text — also surfaced to the editor's image-replace payload. */
|
|
18
|
+
alt?: string;
|
|
19
|
+
};
|
|
20
|
+
declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLImageElement>;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DcsSection — editable section wrapper primitive.
|
|
3
|
+
*
|
|
4
|
+
* Authoring sugar over the `[data-section]` marker that `editorBridge.ts`
|
|
5
|
+
* discovers in `discoverSections()`:
|
|
6
|
+
* id = el.dataset.section
|
|
7
|
+
* label = el.dataset.sectionLabel ?? null
|
|
8
|
+
* The bridge groups text/image markers by their nearest `[data-section]`
|
|
9
|
+
* ancestor and reports section bounds to the portal overlay layer. Rendering
|
|
10
|
+
* `<DcsSection section="hero" label="Hero">` makes that grouping marker correct
|
|
11
|
+
* *by construction* instead of hand-authored per view.
|
|
12
|
+
*/
|
|
13
|
+
type __VLS_Props = {
|
|
14
|
+
/** The section id reported to the editor (`data-section`). */
|
|
15
|
+
section: string;
|
|
16
|
+
/** Human label shown in the editor's section list (`data-section-label`). */
|
|
17
|
+
label?: string;
|
|
18
|
+
/** Element tag to render. Defaults to a `<section>`. */
|
|
19
|
+
as?: string;
|
|
20
|
+
};
|
|
21
|
+
declare function __VLS_template(): {
|
|
22
|
+
attrs: Partial<{}>;
|
|
23
|
+
slots: {
|
|
24
|
+
default?(_: {}): any;
|
|
25
|
+
};
|
|
26
|
+
refs: {};
|
|
27
|
+
rootEl: any;
|
|
28
|
+
};
|
|
29
|
+
type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
|
|
30
|
+
declare const __VLS_component: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
31
|
+
as: string;
|
|
32
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
33
|
+
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
|
|
34
|
+
export default _default;
|
|
35
|
+
type __VLS_WithTemplateSlots<T, S> = T & {
|
|
36
|
+
new (): {
|
|
37
|
+
$slots: S;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DcsText — editable text primitive.
|
|
3
|
+
*
|
|
4
|
+
* Authoring sugar over the `[data-dcs-text]` marker that `editorBridge.ts`
|
|
5
|
+
* discovers (see `TEXT_KEY_SELECTOR = '[data-text-key], [data-dcs-text]'` and
|
|
6
|
+
* `getTextKey()` which reads `dataset.dcsText ?? dataset.textKey`). Rendering a
|
|
7
|
+
* `<DcsText text-key="hero.title">` instead of a hand-authored
|
|
8
|
+
* `<span data-dcs-text="hero.title">` makes the marker correct *by construction*
|
|
9
|
+
* — the recurring "forgot the marker → not editable" drift the kit retires.
|
|
10
|
+
*
|
|
11
|
+
* Defaults to the modern `data-dcs-text` attribute. Pass `legacy` to emit the
|
|
12
|
+
* pre-migration `data-text-key` attribute instead; the bridge reads either.
|
|
13
|
+
*/
|
|
14
|
+
type __VLS_Props = {
|
|
15
|
+
/** The content key the visual editor binds this text to. */
|
|
16
|
+
textKey: string;
|
|
17
|
+
/** Element tag to render. Defaults to an inline `<span>`. */
|
|
18
|
+
as?: string;
|
|
19
|
+
/** Emit the legacy `data-text-key` attribute instead of `data-dcs-text`. */
|
|
20
|
+
legacy?: boolean;
|
|
21
|
+
};
|
|
22
|
+
declare function __VLS_template(): {
|
|
23
|
+
attrs: Partial<{}>;
|
|
24
|
+
slots: {
|
|
25
|
+
default?(_: {}): any;
|
|
26
|
+
};
|
|
27
|
+
refs: {};
|
|
28
|
+
rootEl: any;
|
|
29
|
+
};
|
|
30
|
+
type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
|
|
31
|
+
declare const __VLS_component: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
32
|
+
as: string;
|
|
33
|
+
legacy: boolean;
|
|
34
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
35
|
+
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
|
|
36
|
+
export default _default;
|
|
37
|
+
type __VLS_WithTemplateSlots<T, S> = T & {
|
|
38
|
+
new (): {
|
|
39
|
+
$slots: S;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DcsKit editable primitives barrel.
|
|
3
|
+
*
|
|
4
|
+
* Each primitive is authoring sugar over the `[data-dcs-*]` markers
|
|
5
|
+
* `editorBridge.ts` already reads, making the markers correct *by construction*
|
|
6
|
+
* and guarded by a Vitest mount test asserting the rendered DOM matches the
|
|
7
|
+
* bridge's selector constants (one SFC + its test per commit — milestone M4).
|
|
8
|
+
*/
|
|
9
|
+
export { default as DcsText } from './DcsText.vue';
|
|
10
|
+
export { default as DcsImage } from './DcsImage.vue';
|
|
11
|
+
export { default as DcsSection } from './DcsSection.vue';
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer theme,base,components,utilities;
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@duffcloudservices/kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DcsKit Vue runtime — editable primitives over editorBridge markers + the canonical @layer cascade contract for first-party DCS sites",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./style.css": "./dist/style.css"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "vite build",
|
|
22
|
+
"dev": "vite build --watch",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"type-check": "vue-tsc --noEmit -p tsconfig.json",
|
|
26
|
+
"prepublishOnly": "pnpm run build"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"vue": "^3.5.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"@vitejs/plugin-vue": "^6.0.0",
|
|
34
|
+
"@vue/test-utils": "^2.4.6",
|
|
35
|
+
"@vue/tsconfig": "^0.7.0",
|
|
36
|
+
"jsdom": "^26.0.0",
|
|
37
|
+
"typescript": "~5.8.0",
|
|
38
|
+
"vite": "^7.0.0",
|
|
39
|
+
"vite-plugin-dts": "^4.5.0",
|
|
40
|
+
"vitest": "^3.0.0",
|
|
41
|
+
"vue": "^3.5.18",
|
|
42
|
+
"vue-tsc": "^3.0.0"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"dcs",
|
|
46
|
+
"dcskit",
|
|
47
|
+
"vue",
|
|
48
|
+
"kit",
|
|
49
|
+
"duff-cloud-services"
|
|
50
|
+
],
|
|
51
|
+
"author": "Duff Cloud Services",
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/duffn/dcs"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://portal.duffcloudservices.com",
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/duffn/dcs/issues"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=18.0.0"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import DcsImage from '../primitives/DcsImage.vue'
|
|
4
|
+
|
|
5
|
+
// These assertions mirror editorBridge.ts getManagedImageKey():
|
|
6
|
+
// img.dataset.dcsImageKey || img.closest('[data-dcs-image-key]')?.dataset.dcsImageKey
|
|
7
|
+
// The bridge keys the "Replace image" payload off data-dcs-image-key, so the
|
|
8
|
+
// rendered <img> must carry that attribute for the editor to wire the affordance.
|
|
9
|
+
describe('DcsImage', () => {
|
|
10
|
+
it('renders an <img> carrying the data-dcs-image-key marker', () => {
|
|
11
|
+
const wrapper = mount(DcsImage, {
|
|
12
|
+
props: { imageKey: 'hero.photo', src: '/images/hero.webp', alt: 'Hero' },
|
|
13
|
+
})
|
|
14
|
+
const el = wrapper.element as HTMLImageElement
|
|
15
|
+
expect(el.tagName).toBe('IMG')
|
|
16
|
+
expect(el.dataset.dcsImageKey).toBe('hero.photo')
|
|
17
|
+
// The bridge's selector would match this element.
|
|
18
|
+
expect(el.matches('[data-dcs-image-key]')).toBe(true)
|
|
19
|
+
expect(el.getAttribute('src')).toBe('/images/hero.webp')
|
|
20
|
+
expect(el.getAttribute('alt')).toBe('Hero')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('defaults alt to an empty string when omitted', () => {
|
|
24
|
+
const wrapper = mount(DcsImage, {
|
|
25
|
+
props: { imageKey: 'gallery.1', src: '/images/g1.webp' },
|
|
26
|
+
})
|
|
27
|
+
expect(wrapper.element.getAttribute('alt')).toBe('')
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import DcsSection from '../primitives/DcsSection.vue'
|
|
4
|
+
|
|
5
|
+
// These assertions mirror editorBridge.ts discoverSections():
|
|
6
|
+
// id = el.dataset.section
|
|
7
|
+
// label = el.dataset.sectionLabel ?? null
|
|
8
|
+
// textKeyCount = el.querySelectorAll('[data-text-key], [data-dcs-text]').length
|
|
9
|
+
// The bridge groups markers by their nearest [data-section] ancestor, so the
|
|
10
|
+
// wrapper must carry data-section (and optionally data-section-label).
|
|
11
|
+
describe('DcsSection', () => {
|
|
12
|
+
it('renders a <section> carrying the data-section / data-section-label markers', () => {
|
|
13
|
+
const wrapper = mount(DcsSection, {
|
|
14
|
+
props: { section: 'hero', label: 'Hero' },
|
|
15
|
+
})
|
|
16
|
+
const el = wrapper.element as HTMLElement
|
|
17
|
+
expect(el.tagName).toBe('SECTION')
|
|
18
|
+
expect(el.dataset.section).toBe('hero')
|
|
19
|
+
expect(el.dataset.sectionLabel).toBe('Hero')
|
|
20
|
+
expect(el.matches('[data-section]')).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('omits data-section-label when no label is given', () => {
|
|
24
|
+
const el = mount(DcsSection, { props: { section: 'hero' } }).element as HTMLElement
|
|
25
|
+
expect(el.dataset.section).toBe('hero')
|
|
26
|
+
expect(el.hasAttribute('data-section-label')).toBe(false)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('honors the `as` prop and groups child text markers like the bridge does', () => {
|
|
30
|
+
const wrapper = mount(DcsSection, {
|
|
31
|
+
props: { section: 'about', as: 'div' },
|
|
32
|
+
slots: { default: '<span data-dcs-text="about.title">T</span>' },
|
|
33
|
+
})
|
|
34
|
+
const el = wrapper.element as HTMLElement
|
|
35
|
+
expect(el.tagName).toBe('DIV')
|
|
36
|
+
// discoverSections() counts descendant text markers via TEXT_KEY_SELECTOR.
|
|
37
|
+
expect(el.querySelectorAll('[data-text-key], [data-dcs-text]').length).toBe(1)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import DcsText from '../primitives/DcsText.vue'
|
|
4
|
+
|
|
5
|
+
// These assertions mirror editorBridge.ts:
|
|
6
|
+
// TEXT_KEY_SELECTOR = '[data-text-key], [data-dcs-text]'
|
|
7
|
+
// getTextKey(el) => el.dataset.dcsText ?? el.dataset.textKey
|
|
8
|
+
// A silent marker break (typo'd attribute) produces NO build error, so the
|
|
9
|
+
// only guard is asserting the rendered DOM matches the bridge's selectors.
|
|
10
|
+
const TEXT_KEY_SELECTOR = '[data-text-key], [data-dcs-text]'
|
|
11
|
+
|
|
12
|
+
describe('DcsText', () => {
|
|
13
|
+
it('emits the modern data-dcs-text marker by default', () => {
|
|
14
|
+
const wrapper = mount(DcsText, {
|
|
15
|
+
props: { textKey: 'hero.title' },
|
|
16
|
+
slots: { default: 'Welcome' },
|
|
17
|
+
})
|
|
18
|
+
const el = wrapper.element as HTMLElement
|
|
19
|
+
expect(el.dataset.dcsText).toBe('hero.title')
|
|
20
|
+
expect(el.hasAttribute('data-text-key')).toBe(false)
|
|
21
|
+
// The bridge would discover this element.
|
|
22
|
+
expect(wrapper.element.matches(TEXT_KEY_SELECTOR)).toBe(true)
|
|
23
|
+
expect(wrapper.text()).toBe('Welcome')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('emits the legacy data-text-key marker when legacy is set', () => {
|
|
27
|
+
const wrapper = mount(DcsText, {
|
|
28
|
+
props: { textKey: 'hero.title', legacy: true },
|
|
29
|
+
})
|
|
30
|
+
const el = wrapper.element as HTMLElement
|
|
31
|
+
expect(el.dataset.textKey).toBe('hero.title')
|
|
32
|
+
expect(el.hasAttribute('data-dcs-text')).toBe(false)
|
|
33
|
+
expect(wrapper.element.matches(TEXT_KEY_SELECTOR)).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('renders a <span> by default and honors the `as` prop', () => {
|
|
37
|
+
expect(mount(DcsText, { props: { textKey: 'k' } }).element.tagName).toBe('SPAN')
|
|
38
|
+
expect(
|
|
39
|
+
mount(DcsText, { props: { textKey: 'k', as: 'h1' } }).element.tagName,
|
|
40
|
+
).toBe('H1')
|
|
41
|
+
})
|
|
42
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @duffcloudservices/kit — DcsKit Vue runtime.
|
|
3
|
+
*
|
|
4
|
+
* The Vue-side companion to `@duffcloudservices/kit-vite` (the plain-TS Vite
|
|
5
|
+
* plugin bundle). This package owns two things:
|
|
6
|
+
*
|
|
7
|
+
* 1. The canonical cascade-layer contract (`src/styles/layers.css`), imported
|
|
8
|
+
* here so consumers get the owned `@layer theme, base, components, utilities`
|
|
9
|
+
* order by importing `@duffcloudservices/kit/style.css`.
|
|
10
|
+
* 2. The editable primitives (`DcsText` / `DcsImage` / `DcsSection`) — authoring
|
|
11
|
+
* sugar over the `[data-dcs-*]` editor-bridge markers, each guarded by a
|
|
12
|
+
* Vitest marker assertion (M4).
|
|
13
|
+
*
|
|
14
|
+
* STATUS: publish-ready (M9). Runtime builds + emits `dist/index.js` +
|
|
15
|
+
* `dist/style.css`; primitives exported below; marker tests pass. The FIRST npm
|
|
16
|
+
* publish is a maintainer step (`npm login && npm publish --access public`) since
|
|
17
|
+
* trusted publishing can't be configured until the package exists — see
|
|
18
|
+
* `.github/workflows/npm-publish.yml`. After that, push an `npm-v*` tag to publish.
|
|
19
|
+
*
|
|
20
|
+
* See, in this monorepo, `.docs/plans/first-party-site-framework/`.
|
|
21
|
+
*/
|
|
22
|
+
import './styles/layers.css'
|
|
23
|
+
|
|
24
|
+
export * from './primitives'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DcsImage — editable image primitive.
|
|
4
|
+
*
|
|
5
|
+
* Authoring sugar over the `data-dcs-image-key` marker that `editorBridge.ts`
|
|
6
|
+
* reads via `getManagedImageKey()`:
|
|
7
|
+
* img.dataset.dcsImageKey || img.closest('[data-dcs-image-key]')?.dataset.dcsImageKey
|
|
8
|
+
* Rendering `<DcsImage image-key="hero.photo" :src="…">` instead of a
|
|
9
|
+
* hand-authored `<img data-dcs-image-key="…">` makes the managed-image marker
|
|
10
|
+
* correct *by construction*, so the editor's "Replace image" affordance is wired
|
|
11
|
+
* without per-site hand-authoring.
|
|
12
|
+
*/
|
|
13
|
+
defineProps<{
|
|
14
|
+
/** The managed-image key the visual editor binds this image to. */
|
|
15
|
+
imageKey: string
|
|
16
|
+
/** Image source URL. */
|
|
17
|
+
src: string
|
|
18
|
+
/** Alt text — also surfaced to the editor's image-replace payload. */
|
|
19
|
+
alt?: string
|
|
20
|
+
}>()
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<img :data-dcs-image-key="imageKey" :src="src" :alt="alt ?? ''" />
|
|
25
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DcsSection — editable section wrapper primitive.
|
|
4
|
+
*
|
|
5
|
+
* Authoring sugar over the `[data-section]` marker that `editorBridge.ts`
|
|
6
|
+
* discovers in `discoverSections()`:
|
|
7
|
+
* id = el.dataset.section
|
|
8
|
+
* label = el.dataset.sectionLabel ?? null
|
|
9
|
+
* The bridge groups text/image markers by their nearest `[data-section]`
|
|
10
|
+
* ancestor and reports section bounds to the portal overlay layer. Rendering
|
|
11
|
+
* `<DcsSection section="hero" label="Hero">` makes that grouping marker correct
|
|
12
|
+
* *by construction* instead of hand-authored per view.
|
|
13
|
+
*/
|
|
14
|
+
withDefaults(
|
|
15
|
+
defineProps<{
|
|
16
|
+
/** The section id reported to the editor (`data-section`). */
|
|
17
|
+
section: string
|
|
18
|
+
/** Human label shown in the editor's section list (`data-section-label`). */
|
|
19
|
+
label?: string
|
|
20
|
+
/** Element tag to render. Defaults to a `<section>`. */
|
|
21
|
+
as?: string
|
|
22
|
+
}>(),
|
|
23
|
+
{
|
|
24
|
+
as: 'section',
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<component :is="as" :data-section="section" :data-section-label="label">
|
|
31
|
+
<slot />
|
|
32
|
+
</component>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DcsText — editable text primitive.
|
|
4
|
+
*
|
|
5
|
+
* Authoring sugar over the `[data-dcs-text]` marker that `editorBridge.ts`
|
|
6
|
+
* discovers (see `TEXT_KEY_SELECTOR = '[data-text-key], [data-dcs-text]'` and
|
|
7
|
+
* `getTextKey()` which reads `dataset.dcsText ?? dataset.textKey`). Rendering a
|
|
8
|
+
* `<DcsText text-key="hero.title">` instead of a hand-authored
|
|
9
|
+
* `<span data-dcs-text="hero.title">` makes the marker correct *by construction*
|
|
10
|
+
* — the recurring "forgot the marker → not editable" drift the kit retires.
|
|
11
|
+
*
|
|
12
|
+
* Defaults to the modern `data-dcs-text` attribute. Pass `legacy` to emit the
|
|
13
|
+
* pre-migration `data-text-key` attribute instead; the bridge reads either.
|
|
14
|
+
*/
|
|
15
|
+
withDefaults(
|
|
16
|
+
defineProps<{
|
|
17
|
+
/** The content key the visual editor binds this text to. */
|
|
18
|
+
textKey: string
|
|
19
|
+
/** Element tag to render. Defaults to an inline `<span>`. */
|
|
20
|
+
as?: string
|
|
21
|
+
/** Emit the legacy `data-text-key` attribute instead of `data-dcs-text`. */
|
|
22
|
+
legacy?: boolean
|
|
23
|
+
}>(),
|
|
24
|
+
{
|
|
25
|
+
as: 'span',
|
|
26
|
+
legacy: false,
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<component
|
|
33
|
+
:is="as"
|
|
34
|
+
:data-dcs-text="legacy ? undefined : textKey"
|
|
35
|
+
:data-text-key="legacy ? textKey : undefined"
|
|
36
|
+
>
|
|
37
|
+
<slot />
|
|
38
|
+
</component>
|
|
39
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DcsKit editable primitives barrel.
|
|
3
|
+
*
|
|
4
|
+
* Each primitive is authoring sugar over the `[data-dcs-*]` markers
|
|
5
|
+
* `editorBridge.ts` already reads, making the markers correct *by construction*
|
|
6
|
+
* and guarded by a Vitest mount test asserting the rendered DOM matches the
|
|
7
|
+
* bridge's selector constants (one SFC + its test per commit — milestone M4).
|
|
8
|
+
*/
|
|
9
|
+
export { default as DcsText } from './DcsText.vue'
|
|
10
|
+
export { default as DcsImage } from './DcsImage.vue'
|
|
11
|
+
export { default as DcsSection } from './DcsSection.vue'
|
package/src/shims.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DcsKit canonical cascade-layer contract.
|
|
3
|
+
*
|
|
4
|
+
* This single `@layer` declaration is THE one contract DcsKit owns. Declaring the
|
|
5
|
+
* layer order up front fixes precedence as `theme < base < components < utilities`
|
|
6
|
+
* regardless of source order, so the Tailwind v4 "layered-utility loses to an
|
|
7
|
+
* unlayered scoped <style>" bug is impossible by construction — the kit owns the
|
|
8
|
+
* order instead of every site re-fighting the VitePress styling-suppression war
|
|
9
|
+
* with !important / postcssIsolateStyles firewalls.
|
|
10
|
+
*
|
|
11
|
+
* Order rationale (lowest → highest precedence):
|
|
12
|
+
* theme — design tokens / CSS custom properties (--color-*, --font-*)
|
|
13
|
+
* base — element resets and global element defaults
|
|
14
|
+
* components — kit primitive + composed component styles
|
|
15
|
+
* utilities — single-purpose helpers (Tailwind utilities) win last
|
|
16
|
+
*
|
|
17
|
+
* Consumers add `@import 'tailwindcss';` and their own `@layer` blocks; this
|
|
18
|
+
* declaration only fixes the precedence between them. It is emitted into the
|
|
19
|
+
* package's single dist/style.css (cssCodeSplit:false) so importing
|
|
20
|
+
* `@duffcloudservices/kit/style.css` establishes the order once.
|
|
21
|
+
*/
|
|
22
|
+
@layer theme, base, components, utilities;
|