@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 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`).
@@ -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,10 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue'
3
+ const component: DefineComponent<{}, {}, unknown>
4
+ export default component
5
+ }
6
+
7
+ declare module '*.css' {
8
+ const css: string
9
+ export default css
10
+ }
@@ -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;