diff --git a/.changeset/shadow-dom-default.md b/.changeset/shadow-dom-default.md new file mode 100644 index 0000000..47657fa --- /dev/null +++ b/.changeset/shadow-dom-default.md @@ -0,0 +1,13 @@ +--- +"@templatical/editor": minor +--- + +Mount the editor inside a Shadow DOM by default. `init({ container })` now resolve `shadowDom: true` when the option is omitted — host page stylesheets no longer cascade into editor elements (`p`, `h1`, `a`, `input`, etc.) via tag selectors, closing [issue #70](https://github.com/templatical/sdk/issues/70). + +**Behavior changes consumers may notice:** + +- External `document.querySelector("#editor .tpl-…")` queries no longer reach editor internals because the editor's DOM lives inside `container.shadowRoot`. Walk the shadow root explicitly (`container.shadowRoot.querySelector(...)`) or opt out with `shadowDom: false`. +- Host stylesheets that intentionally styled editor elements via element selectors stop applying. The supported theming protocol is now the `--tpl-user-*` CSS custom property namespace — set `--tpl-user-primary`, `--tpl-user-radius-md`, etc. on the editor container (or any ancestor) and the override inherits across the shadow boundary. The existing `theme` config option still takes precedence and works unchanged. +- Browser minimums in default mode bump to Firefox 101+ and Safari 16.4+ (required by the `adoptedStyleSheets` API). Chrome / Edge 80+ is unchanged. Pass `shadowDom: false` to keep the previous light-DOM mount with broader browser support. + +The `shadowDom: false` escape hatch remains supported. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 450f65b..4218269 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,6 +75,13 @@ jobs: e2e: runs-on: ubuntu-latest + # Playwright's official image ships Chromium + every system dep + # preinstalled, skipping the ~5 min `playwright install-deps` step that + # fresh ubuntu-latest runners would otherwise pay every run (apt state + # isn't cacheable across runs). Image tag must match the pinned + # `@playwright/test` version in pnpm-lock.yaml. + container: + image: mcr.microsoft.com/playwright:v1.59.1-jammy needs: [build] strategy: fail-fast: false @@ -87,15 +94,6 @@ jobs: with: name: dist path: packages/ - - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 - id: playwright-cache - with: - path: ~/.cache/ms-playwright - key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} - - run: pnpm exec playwright install --with-deps chromium - if: steps.playwright-cache.outputs.cache-hit != 'true' - - run: pnpm exec playwright install-deps chromium - if: steps.playwright-cache.outputs.cache-hit == 'true' - run: pnpm exec playwright test --shard=${{ matrix.shard }}/3 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: ${{ !cancelled() }} @@ -115,19 +113,14 @@ jobs: # (after publish) and every feature PR's run still cover this surface. if: ${{ !startsWith(github.head_ref, 'changeset-release/') && !startsWith(github.ref_name, 'changeset-release/') }} runs-on: ubuntu-latest + # Same rationale as the `e2e` job — use the Playwright image to skip + # the system-dep install step. + container: + image: mcr.microsoft.com/playwright:v1.59.1-jammy needs: [build] steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: ./.github/actions/setup - - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 - id: playwright-cache - with: - path: ~/.cache/ms-playwright - key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} - - run: pnpm exec playwright install --with-deps chromium - if: steps.playwright-cache.outputs.cache-hit != 'true' - - run: pnpm exec playwright install-deps chromium - if: steps.playwright-cache.outputs.cache-hit == 'true' - run: pnpm run test:e2e:consumer - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: failure() diff --git a/README.md b/README.md index c2128aa..f9ac622 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,11 @@ Things that are usually paid features in commercial editors — open-source in T ### And more - **Drop-in mount** — one `init()` call, one `unmount()`. No framework lock-in. +- **Style-isolated, both directions** — Shadow DOM by default keeps host CSS out of the editor; `tpl:` Tailwind prefix and no preflight reset keep editor styles out of your app. Drops into any page, any framework, any CMS — no resets, no conflicts. [Learn more →](https://docs.templatical.com/guide/shadow-dom) - **14 block types** — Title, Paragraph, Image, Button, Section, Divider, Spacer, Social Icons, Menu, Table, HTML, Video, Countdown, Custom. - **JSON templates** — portable, versionable, store anywhere, render anywhere. - **MJML output** — works with any email provider (Postmark, Resend, SES, Mailgun, anything). - **Framework-agnostic** — first-class examples for React, Vue, Svelte, Angular, vanilla. -- **Tailwind 4 with `tpl:` prefix** — no preflight reset, no style leaks into your app. - **Bilingual** — en/de built in, easy to add more locales. - **TypeScript strict** — full types for blocks, config, and callbacks. - **Battle-tested** — ~1,400 unit tests + Playwright E2E coverage. diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.ts index 5b4db52..eb88e3a 100644 --- a/apps/docs/.vitepress/config.ts +++ b/apps/docs/.vitepress/config.ts @@ -101,6 +101,7 @@ const enSidebar: DefaultTheme.SidebarMulti = { { text: "Block & Template Defaults", link: "/guide/defaults" }, { text: "Custom Fonts", link: "/guide/fonts" }, { text: "Internationalization", link: "/guide/i18n" }, + { text: "Shadow DOM", link: "/guide/shadow-dom" }, ], }, { @@ -244,6 +245,7 @@ const deSidebar: DefaultTheme.SidebarMulti = { { text: "Block- & Template-Standards", link: "/de/guide/defaults" }, { text: "Benutzerdefinierte Schriften", link: "/de/guide/fonts" }, { text: "Internationalisierung", link: "/de/guide/i18n" }, + { text: "Shadow DOM", link: "/de/guide/shadow-dom" }, ], }, { diff --git a/apps/docs/api/editor.md b/apps/docs/api/editor.md index e27de4e..58b58ff 100644 --- a/apps/docs/api/editor.md +++ b/apps/docs/api/editor.md @@ -12,11 +12,11 @@ The main entry point is the `init()` function from `@templatical/editor`. Creates and mounts the editor into a container element. Returns a promise that resolves when the editor is ready. ```ts -import { init } from '@templatical/editor'; -import '@templatical/editor/style.css'; +import { init } from "@templatical/editor"; +import "@templatical/editor/style.css"; const editor = await init({ - container: '#editor', + container: "#editor", content: savedTemplate, onChange(content) { // Auto-save or update state @@ -31,32 +31,42 @@ const editor = await init({ Destroys the editor instance and cleans up event listeners. ```ts -import { unmount } from '@templatical/editor'; +import { unmount } from "@templatical/editor"; unmount(); ``` ## TemplaticalEditorConfig -| Property | Type | Required | Description | -|----------|------|----------|-------------| -| `container` | `string \| HTMLElement` | Yes | CSS selector or DOM element to mount the editor into | -| `content` | `TemplateContent` | No | Initial template content. Defaults to empty template | -| `onChange` | `(content: TemplateContent) => void` | No | Called when template content changes (debounced) | -| `onSave` | `(content: TemplateContent) => void` | No | Called when the user triggers a save action | -| `onError` | `(error: Error) => void` | No | Called when an error occurs | -| `onRequestMedia` | `(context?: MediaRequestContext) => Promise` | No | Called when user wants to pick an image. Return `{ url, alt? }` or `null` | -| `mergeTags` | `MergeTagsConfig` | No | Merge tag configuration. See [Merge Tags](/guide/merge-tags) | -| `displayConditions` | `DisplayConditionsConfig` | No | Display condition configuration. See [Display Conditions](/guide/display-conditions) | -| `customBlocks` | `CustomBlockDefinition[]` | No | Custom block type definitions. See [Custom Blocks](/guide/custom-blocks) | -| `blockDefaults` | `BlockDefaults` | No | Default property overrides for new blocks. See [Defaults](/guide/defaults) | -| `templateDefaults` | `TemplateDefaults` | No | Default template settings for empty templates. See [Defaults](/guide/defaults) | -| `fonts` | `FontsConfig` | No | Font configuration. See [Custom Fonts](/guide/fonts) | -| `theme` | `ThemeOverrides` | No | Color token overrides. Supports a `dark` key for dark mode overrides. See [Theming](/guide/theming) | -| `uiTheme` | `'light' \| 'dark' \| 'auto'` | No | UI color scheme. `'auto'` follows system preference. Defaults to `'auto'` | -| `locale` | `string` | No | Locale code (e.g. `'en'`, `'de'`). Defaults to `'en'` | -| `branding` | `boolean` | No | Show the "Powered by Templatical" footer. Defaults to `true`. Set to `false` to hide it | +| Property | Type | Required | Description | +| ------------------- | ----------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `container` | `string \| HTMLElement` | Yes | CSS selector or DOM element to mount the editor into. In default (shadow DOM) mode, must be an element that can host a Shadow DOM — `
` is recommended. See [Container element requirements](#container-element-requirements) below | +| `shadowDom` | `boolean` | No | Mount inside a Shadow DOM for CSS isolation from the host page. Defaults to `true`. Set to `false` to mount in light DOM instead (e.g. for `document.querySelector` access to editor internals or Firefox <101 / Safari <16.4 support). See [Shadow DOM](/guide/shadow-dom) for trade-offs | +| `content` | `TemplateContent` | No | Initial template content. Defaults to empty template | +| `onChange` | `(content: TemplateContent) => void` | No | Called when template content changes (debounced) | +| `onSave` | `(content: TemplateContent) => void` | No | Called when the user triggers a save action | +| `onError` | `(error: Error) => void` | No | Called when an error occurs | +| `onRequestMedia` | `(context?: MediaRequestContext) => Promise` | No | Called when user wants to pick an image. Return `{ url, alt? }` or `null` | +| `mergeTags` | `MergeTagsConfig` | No | Merge tag configuration. See [Merge Tags](/guide/merge-tags) | +| `displayConditions` | `DisplayConditionsConfig` | No | Display condition configuration. See [Display Conditions](/guide/display-conditions) | +| `customBlocks` | `CustomBlockDefinition[]` | No | Custom block type definitions. See [Custom Blocks](/guide/custom-blocks) | +| `blockDefaults` | `BlockDefaults` | No | Default property overrides for new blocks. See [Defaults](/guide/defaults) | +| `templateDefaults` | `TemplateDefaults` | No | Default template settings for empty templates. See [Defaults](/guide/defaults) | +| `fonts` | `FontsConfig` | No | Font configuration. See [Custom Fonts](/guide/fonts) | +| `theme` | `ThemeOverrides` | No | Color token overrides. Supports a `dark` key for dark mode overrides. See [Theming](/guide/theming) | +| `uiTheme` | `'light' \| 'dark' \| 'auto'` | No | UI color scheme. `'auto'` follows system preference. Defaults to `'auto'` | +| `locale` | `string` | No | Locale code (e.g. `'en'`, `'de'`). Defaults to `'en'` | +| `branding` | `boolean` | No | Show the "Powered by Templatical" footer. Defaults to `true`. Set to `false` to hide it | +### Container element requirements + +The default (shadow DOM) mount calls `attachShadow()` on your container, and the HTML spec only allows shadow roots on a fixed set of elements. Use one of: + +`
`, `