@samva/markup 0.15.1 → 0.17.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.
Files changed (51) hide show
  1. package/AGENTS.md +42 -35
  2. package/CHANGELOG.md +92 -0
  3. package/README.md +10 -9
  4. package/dist/{assets-tISHaQNF.d.ts → assets-CS5Xa_mr.d.ts} +3 -3
  5. package/dist/{brand-nreeKWxF.js → brand-CIiEqkU7.js} +24 -31
  6. package/dist/{brand-plugin-CHhcei-j.d.ts → brand-plugin-Dj9SMpfS.d.ts} +3 -1
  7. package/dist/brand.d.ts +2 -2
  8. package/dist/brand.js +3 -3
  9. package/dist/compatibility.d.ts +76 -0
  10. package/dist/compatibility.js +807 -0
  11. package/dist/compiled-template-Bc4Dq9zy.d.ts +268 -0
  12. package/dist/compiler.d.ts +58 -225
  13. package/dist/compiler.js +261 -1297
  14. package/dist/{components-3XHjYD3i.js → components-C5850-Qg.js} +37 -29
  15. package/dist/css-B_aVKrDo.js +436 -0
  16. package/dist/css-color-DLM12Osu.js +315 -0
  17. package/dist/{diagnostic-codes-UXwOO3A-.js → diagnostic-codes-DZKb0Xcg.js} +4 -0
  18. package/dist/{diagnostics-CUYlQ_6x.d.ts → diagnostics-D4vSLQrO.d.ts} +1 -10
  19. package/dist/diagnostics.d.ts +6 -2
  20. package/dist/diagnostics.js +3 -3
  21. package/dist/edit.d.ts +10 -9
  22. package/dist/edit.js +12 -11
  23. package/dist/email/jsx-dev-runtime.d.ts +1 -1
  24. package/dist/email/jsx-dev-runtime.js +1 -1
  25. package/dist/email/jsx-runtime.d.ts +2 -2
  26. package/dist/email/jsx-runtime.js +2 -2
  27. package/dist/email.d.ts +1 -1
  28. package/dist/email.js +1 -1
  29. package/dist/html-DJUK2YJF.js +483 -0
  30. package/dist/index.d.ts +1 -1
  31. package/dist/{jsx-runtime-DC7xG0aA.d.ts → jsx-runtime-DL8NVF1f.d.ts} +12 -6
  32. package/dist/{jsx-runtime-CBlexoeR.js → jsx-runtime-qL19u3Fy.js} +17 -10
  33. package/dist/preview-rNoJBEIY.d.ts +104 -0
  34. package/dist/{profile-AatO_SvP.js → profile-JTvgCD6X.js} +614 -24
  35. package/dist/{html-BvRFepeO.js → render-template-DwPDm3Hq.js} +155 -572
  36. package/dist/render.d.ts +40 -28
  37. package/dist/render.js +3 -47
  38. package/dist/selectors-CYWCoxZr.js +157 -0
  39. package/dist/sms.d.ts +1 -1
  40. package/dist/sms.js +1 -1
  41. package/dist/{template-B-UDXK8a.d.ts → template-DmNcoH4Q.d.ts} +2 -9
  42. package/dist/{theme-css-s9QwD5Ko.js → theme-css-P9F5S7Ic.js} +3 -316
  43. package/dist/whatsapp.d.ts +1 -1
  44. package/dist/whatsapp.js +1 -1
  45. package/generated/contract.json +7 -2
  46. package/generated/reference.md +3 -2
  47. package/package.json +5 -1
  48. package/skills/sml/SKILL.md +2 -2
  49. package/dist/ir-DNL6H7jA.js +0 -705
  50. package/dist/render-BdvewoQt.d.ts +0 -564
  51. package/dist/render-DpYouYYG.js +0 -258
package/AGENTS.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  `@samva/markup` is the authoring language and compiler for Samva templates. A template is one TSX
4
4
  file in a static profile that default-exports `defineTemplate`: ordinary JSX, HTML and Tailwind that
5
- a compiler reads without running. The compiler produces a versioned JSON IR, and a pure renderer
6
- turns the IR and an input into subject, preheader, HTML and text, so nothing executes at send time.
5
+ a compiler reads without running. The compiler produces a versioned JSON compiled template, and a
6
+ pure renderer turns it and an input into a message, so nothing executes at send time.
7
7
  A body that would compute over its input is a diagnostic, not a second mode: compute the value in
8
8
  the caller and send it in the payload. The package is plain TypeScript with no application runtime;
9
9
  the local editor, project builds, and exports live in `@samva/vite`.
@@ -32,38 +32,45 @@ Authoring entries are what a template file imports:
32
32
  `"types": ["@samva/markup/samva-brand"]` to the project's tsconfig for `tsc`. In `theme.css`,
33
33
  `@import "samva:brand";` layers the brand theme over the default theme (`DEFAULT_THEME`); `samva:brand/<slug>` names one brand. These are
34
34
  Samva's brand plugin (`samvaBrandPlugin` from `@samva/markup/brand`), the default; a host that
35
- delivers mail itself passes its own `BrandPlugin` as `brandPlugin` to `compileTemplate`,
36
- `renderIr` and `renderBrandFooter` to rename the brand import, the footer markers, the
37
- unsubscribe placeholder and the unsubscribe link's no-track marker.
35
+ delivers mail itself passes its own `BrandPlugin` as the `brandPlugin` option of `compileTemplate`,
36
+ `render` and `compileFooter` to rename the brand import, the footer markers, the unsubscribe
37
+ placeholder and the unsubscribe link's no-track marker.
38
38
 
39
39
  Tooling entries are what a host (Vite, the CLI, the editor, the API, the template agent) imports:
40
40
 
41
- - `@samva/markup/compiler` — `compileTemplate`, which reads a project's files and returns the IR,
42
- the fixtures, the assets and the diagnostics; `checkTemplateCompatibility` checks each fixture
43
- against the client matrix and locates findings at source. `checkStaticProfile(source | files,
44
- entry?)` is the synchronous structural check: it runs the compiler's lowering without Tailwind, a
45
- brand or the files an import names, and returns only findings the source itself owns.
46
- `introducedProfileErrors(before, after)` is what a visual edit is refused on. It also holds the
47
- compiler's building blocks (`transformProject`, Tailwind, the cascade, CSS), the brand footer and
48
- sample renders (`renderBrandFooter`, `renderBrandSample`), and `TEMPLATE_NAME_PATTERN`. It imports no Vite, esbuild, or Node builtins, so it runs in any
49
- JavaScript host.
41
+ - `@samva/markup/compiler` — `compileTemplate`, which reads a project's files and returns a
42
+ `CompileResult`: the compiled `template`, the fixtures, the assets and the diagnostics.
43
+ `checkTemplate(source | files, { entry?, brandPlugin? })` is the synchronous structural check: it
44
+ runs the compiler's lowering without Tailwind, a brand or the files an import names, and returns
45
+ only findings the source itself owns. `introducedErrors(before, after)` is what a visual edit is
46
+ refused on. It also holds the brand compiles: `compileFooter`, the footer a send appends (with
47
+ its unsubscribe row, which the send drops with `withoutUnsubscribeRow`; a brand theme outside the
48
+ grammar comes back as diagnostics), and `previewBrand`, a sample email in an unsaved brand
49
+ compiled and rendered as a template that imports the brand. It imports no
50
+ Vite, esbuild, Node builtins or caniemail, so it runs in Node, Bun, Workers and the browser
51
+ (`tests/browser-entries.test.ts` loads it without `process`).
52
+ - `@samva/markup/compatibility` — `checkCompatibility`, which checks each fixture against
53
+ the client matrix and locates findings at source; `checkHtmlCompatibility` for HTML a host
54
+ already has; `hasBlockingCompatibility` and the matrix. It is the only entry that loads
55
+ caniemail, whose glob dependency reads `process` when it loads.
50
56
  - `@samva/markup/diagnostics` — source spans and line maps, the `DIAGNOSTIC_CODES` registry with
51
- each code's fix and upgrade recipe, and `EmailDiagnostic`/`EmailCompileError`. It does not load
52
- the compiler, so a host that only reports imports it.
53
- - `@samva/markup/render` — `renderIr(ir, input, { locale, timeZone })`, `renderIrSms` and
54
- `renderIrWhatsApp`; the IR types and `SML_IR_VERSION`; `renderIrPreview(ir, input)`, the render an
55
- editor shows, with an instance path (`INSTANCE_PATH_ATTRIBUTE`) on every element and the
56
- `selections` that map each one to its TSX span; `compileEmail`, the HTML serializer and text
57
- derivation; the email element table; SMS segment counts. Its import closure excludes the
58
- compiler's libraries (`tests/entry-closure.test.ts`): it is the send-time security boundary and
59
- what the API bundles.
57
+ each code's fix and upgrade recipe, and `EmailDiagnostic`. It does not load the compiler, so a
58
+ host that only reports imports it.
59
+ - `@samva/markup/render` — `render(template, input, { channel, locale?, timeZone?, brandPlugin? })`,
60
+ which returns the `channel`'s message (`RenderedEmail`, `RenderedSms` with its segment counts, or
61
+ `RenderedWhatsApp`) and throws only `RenderError`, including for a template compiled by another
62
+ `COMPILED_TEMPLATE_VERSION`; the `Compiled*` types; the email element table and `escapeText` /
63
+ `escapeAttribute`; `smsSegmentInfo`. Its import closure excludes the compiler's libraries
64
+ (`tests/entry-closure.test.ts`): it is the send-time security boundary and what the API bundles.
60
65
  - `@samva/markup/edit` — the code transforms behind visual edits (`inspectSourceElement`,
61
66
  `literalReplacement`, `expressionReplacement`, `classNameEdit`, `structuralReplacements`,
62
67
  `applySourceReplacements` with a `ProfileGuard`); form reads and exact edits for the `sms` and
63
68
  `whatsapp` channel bodies (`readSmsChannel`, `editSmsChannel`, `readWhatsAppChannel`,
64
69
  `editWhatsAppChannel`), where text between expressions is editable and every expression,
65
- conditional or element is a locked segment; and JSX source locations. Browser-safe: Babel parser
66
- only.
70
+ conditional or element is a locked segment; JSX source locations; and `previewEmail(template,
71
+ input)`, the render an editor shows, with an instance path (`INSTANCE_PATH_ATTRIBUTE`) on every
72
+ element and the `selections` that map each one to its TSX span. Browser-safe: its closure holds
73
+ Babel's parser and css-tree only (`tests/entry-closure.test.ts`).
67
74
  - `@samva/markup/brand` — brand records and their CSS model (`parseBrandCss`, `printBrandCss`), the
68
75
  theme vocabulary (`THEME_COLOR_ROLES`, `THEME_TOKENS`, `DEFAULT_THEME`, `foregroundDefaults`), the
69
76
  footer markers a send reads (`renderedFooterMarkers`, `withoutUnsubscribeRow`), and project
@@ -124,7 +131,7 @@ Read the contract before authoring. Import only subpaths declared in `package.js
124
131
  generated `sml-dark-*` class, so `bg-card` switches by itself. A `--color-card-dark` token is just
125
132
  another color, and `dark:` only picks a different token. A block styled with palette colors
126
133
  (`bg-indigo-600`) or raw hex ignores the theme; style with the vocabulary in `THEME_COLOR_ROLES`.
127
- - **Reading contrast off the light render.** `checkEmailCompatibility` includes `email-contrast`,
134
+ - **Reading contrast off the light render.** `checkHtmlCompatibility` includes `email-contrast`,
128
135
  which judges each fixture in light and dark mode (`CONTRAST_THRESHOLDS`); a color a dark block
129
136
  leaves unpaired is an error there, not a visual nit.
130
137
  - **Declaring a webfont as if every client loads it.** A project font is an ordinary `@font-face`
@@ -146,12 +153,12 @@ Read the contract before authoring. Import only subpaths declared in `package.js
146
153
  channel. Helper modules are ordinary imports, not catalog entries; a partial is a function from
147
154
  props (and `children`) to JSX that the compiler inlines. A fixture is data: literals, constants
148
155
  and imported assets.
149
- - **The IR is the artifact.** Rendering is a pure function of the IR and the input. The renderer
150
- escapes per context, applies the serializer's URL and style rules to every bound value, and
151
- rejects unsafe input rather than sanitizing it.
156
+ - **The compiled template is the artifact.** Rendering is a pure function of the template and the
157
+ input. The renderer escapes per context, applies the serializer's URL and style rules to every
158
+ bound value, and rejects unsafe input rather than sanitizing it.
152
159
  - **Failures are typed.** Compilation reports diagnostic values with a code, a location and a fix:
153
- `compileTemplate` returns them beside the IR, and an error finding leaves no IR. A declaration the compiler cannot
154
- accept at all, such as an invalid name or a nonportable input schema, throws a `TypeError`.
160
+ `compileTemplate` returns them beside the template, and an error finding leaves no template. A
161
+ declaration the compiler cannot accept at all, such as an invalid name or a nonportable input schema, throws a `TypeError`.
155
162
  - **Data models are plain types.** Markup's data models are TypeScript types, not schema
156
163
  declarations, so there is no schema library to adopt to consume them. Unions discriminate on
157
164
  `type`; only error classes carry `_tag`. The input schema is the one
@@ -161,9 +168,9 @@ Read the contract before authoring. Import only subpaths declared in `package.js
161
168
 
162
169
  - `tsc --noEmit` over the template project catches unsupported elements, attributes, and props.
163
170
  - `compileTemplate` reports every static-profile, binding and fixture finding with a source
164
- location, and `renderIr(compiled.ir, fixture)` proves one fixture's output without executing the
165
- template.
171
+ location, and `render(compiled.template, fixture, { channel: "email" })` proves one fixture's
172
+ output without executing the template.
166
173
  - `samva templates check` (from `@samva/cli`) compiles every entry and renders every fixture,
167
174
  reporting findings with source locations.
168
- - `renderIrPreview(compiled.ir, fixture)` returns the same document with the source spans an editor
169
- selects by.
175
+ - `previewEmail(compiled.template, fixture)` returns the same document with the source spans an
176
+ editor selects by.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,95 @@
1
+ ## @samva/markup@0.17.0
2
+
3
+ ### One compiler for every host, one render, one error model
4
+
5
+ Breaking. Update imports and calls as follows:
6
+
7
+ | Before | After |
8
+ | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
9
+ | `renderIr(ir, input, options)` | `render(template, input, { channel: "email", ...options })` |
10
+ | `renderIrSms(ir, input)` (a string) | `render(template, input, { channel: "sms" })` (`{ text, segments }`) |
11
+ | `renderIrWhatsApp(ir, input)` | `render(template, input, { channel: "whatsapp" })` |
12
+ | `renderIrPreview` from `./render` | `previewEmail` from `./edit` |
13
+ | `TemplateIr`, `SML_IR_VERSION` | `CompiledTemplate`, `COMPILED_TEMPLATE_VERSION` |
14
+ | `IrRenderError` | `RenderError`, a tagged error |
15
+ | `compileTemplate(...).ir` | `compileTemplate(...).template` |
16
+ | `checkStaticProfile(files, entry, plugin)` | `checkTemplate(files, { entry, brandPlugin })` |
17
+ | `introducedProfileErrors` | `introducedErrors` |
18
+ | `checkTemplateCompatibility`, `checkEmailCompatibility` from `./compiler` | `checkCompatibility`, `checkHtmlCompatibility` from `./compatibility` |
19
+ | `renderBrandFooter(brand, { unsubscribe })` | `compileFooter(brand)`, which returns `{ footer, diagnostics }`; strip the unsubscribe row with `withoutUnsubscribeRow` |
20
+ | `renderBrandSample` from `./brand-sample` | `previewBrand(brand)` from `./compiler` |
21
+ | `parseBrandSpecifier(s, plugin)`, `projectBrandSpecifier(files, path, plugin)` | `parseBrandSpecifier(s, { brandPlugin })`, `projectBrandSpecifier(files, { themePath, brandPlugin })` |
22
+ | `@samva/vite` `TemplateBuild.ir`, `CompiledEntry.ir` | `.compiled` |
23
+
24
+ - `./compiler` now loads in a browser. caniemail, whose glob dependency reads Node's `process`, is
25
+ only in `./compatibility`.
26
+ - `./compiler` exports only `compileTemplate`, `checkTemplate`, `introducedErrors`,
27
+ `compileFooter`, `previewBrand` and the brand specifiers. The cascade, CSS, Tailwind and
28
+ serializer helpers are no longer public, and `./brand-sample` is removed.
29
+ - Compile and check functions return diagnostics and do not throw for bad input; `render` throws
30
+ only `RenderError`, including for a compiled template version it does not know. Stored compiled
31
+ templates keep their `sml` key and stay readable.
32
+ - `readSmsChannel` and `readWhatsAppChannel` return `undefined` instead of `null`.
33
+
34
+ ### Design mode and pins
35
+
36
+ The preview bar has a **Design** switch. In Design, a click on the email selects it with a
37
+ toolbar that steps out to its container, moves, duplicates or deletes it, and beside a docked
38
+ assistant the element's properties sit next to the email. Edits from the toolbar, the properties
39
+ and the fixture data are staged instead of saved one field at a time: the preview shows them, a
40
+ dock counts them, and undo, redo, reset (after a confirm) and a locked before-and-after compare
41
+ work over them. **Apply** saves them as one version named "Design: N edits". Leaving Design waits
42
+ for the edits to be applied or reset, and Design waits while an assistant turn is running.
43
+
44
+ The Design tab speaks the author's language. Layers name the blocks and elements the template
45
+ wrote (`Section`, `Button`, `Heading`, `Text`, `Image`) and leave out the compiler's tables and
46
+ cells; a loop's copies are one layer. The inspector names the element and the blocks around it,
47
+ with no internal paths, and edits it with plain controls: text with data bindings shown as chips,
48
+ image description and size, component settings, theme colors as named swatches, type size and
49
+ weight, alignment, and spacing. Each change is staged like any Design edit. The raw props and
50
+ classes are under **Advanced**, and the whole template is in Source.
51
+
52
+ Beside a docked assistant, the **Pin** switch drops numbered pins on parts of the email. A tray
53
+ lists them with a note each, removes one at a time, and clears all after a confirm.
54
+
55
+ For hosts:
56
+
57
+ - `EditorProvider` takes `onPinsChange`, called with every `EditorPin` in tray order whenever the
58
+ pins change. The `rail.assistant` contribution reads `workspace.pins` and gets `removePin` and
59
+ `clearPins`, so a chat can attach pins to its next message and clear them once sent.
60
+ - `workspace.design` reports `{ on, pending, applying, waiting }`. Hold the chat while `waiting`
61
+ is true.
62
+ - `versions.save(revision, options)` receives `options.message`, the name the editor asks for.
63
+ The parameter is optional, so an existing host keeps working and names versions itself.
64
+
65
+ ### A warning for hotlinked images
66
+
67
+ `email-external-asset` warns when an email's image points at an absolute URL outside the project's
68
+ asset base. A hotlinked image can disappear, be blocked, track recipients or slow the message; the
69
+ fix is to add the image to the project and import it. It is a warning, so every template that
70
+ compiled before still compiles, and compiled output is unchanged: the email parity digests do not
71
+ move.
72
+
73
+ ## @samva/markup@0.16.0
74
+
75
+ ### The brand footer is one line
76
+
77
+ `BrandFooter` renders the company name, social links and the unsubscribe link on one line,
78
+ separated by `·`, with the postal address on the line below. It follows a `text-left` or
79
+ `text-right` `className` instead of always centering. Sends without an unsubscribe group drop only
80
+ the unsubscribe link and its separator, and still strip the row of footers compiled before this
81
+ release.
82
+
83
+ This is an intended change to published output: the email parity digests
84
+ (`packages/markup/tests/support/email-parity.ts`) move with the new footer markup, and Samva's hosted
85
+ runtime must take the same values when it repins to this release.
86
+
87
+ ### Render a brand sample in the browser
88
+
89
+ `@samva/markup/brand-sample` exports `renderBrandSample` without caniemail, whose glob dependency
90
+ reads Node's `process` when it loads. Import it to preview a brand in a browser; `./compiler`
91
+ still exports `renderBrandSample` for Node, Bun and Workers.
92
+
1
93
  ## @samva/markup@0.15.0
2
94
 
3
95
  ### WhatsApp template FLOW buttons
package/README.md CHANGED
@@ -7,7 +7,7 @@ directly for TSX templates or together with `@samva/vite` for the visual editor
7
7
 
8
8
  - **SML** is the name of this toolkit, not a second syntax. An email is a TSX file that
9
9
  default-exports `defineTemplate`, written in the static profile (ordinary JSX, HTML, and Tailwind
10
- classes read without running). Compiling it produces the **IR**, the JSON description that
10
+ classes read without running). Compiling it produces the **compiled template**, the JSON description that
11
11
  previews and sends render.
12
12
  - The package ships the `sml` skill at `skills/sml/SKILL.md`; it routes you through write, check,
13
13
  render, look, and repair. `AGENTS.md` and `contract.json` ship alongside it.
@@ -107,13 +107,13 @@ export default defineTemplate({
107
107
 
108
108
  A template is read, not run. `@samva/markup/compiler` parses the file and the project files it
109
109
  imports without evaluating them, checks every binding against the schema, resolves Tailwind and CSS
110
- against the markup, and produces the template's IR: JSON, versioned (`"sml": 1`), carrying the
110
+ against the markup, and produces the compiled template: JSON, versioned (`"sml": 1`), carrying the
111
111
  markup, the bindings, the conditions, the loops and the formatters. `@samva/markup/render`
112
- renders that IR against an input. It is a pure function with no dependencies, so nothing executes
112
+ renders that template against an input. It is a pure function with no dependencies, so nothing executes
113
113
  at send time.
114
114
 
115
115
  ```ts
116
- import { renderIr } from "@samva/markup/render";
116
+ import { render } from "@samva/markup/render";
117
117
  import { compileTemplate } from "@samva/markup/compiler";
118
118
 
119
119
  const compiled = await compileTemplate({
@@ -122,9 +122,10 @@ const compiled = await compileTemplate({
122
122
  assetBase: "https://assets.example.com",
123
123
  tailwind: { css: files["theme.css"] },
124
124
  });
125
- if (compiled.ir === undefined) throw new Error(compiled.diagnostics.map(String).join("\n"));
125
+ if (compiled.template === undefined) throw new Error(compiled.diagnostics.map(String).join("\n"));
126
126
 
127
- const { subject, preheader, html, text } = renderIr(compiled.ir, compiled.fixtures.default, {
127
+ const { subject, preheader, html, text } = render(compiled.template, compiled.fixtures.default, {
128
+ channel: "email",
128
129
  locale: "en-US",
129
130
  timeZone: "UTC",
130
131
  });
@@ -157,7 +158,7 @@ chains before `.map(...)` or `.length`. `<Columns each per>` lays a list out in
157
158
  (one row without `per`): the child is a function from an item, and its index within the row, to a
158
159
  `Column`. A `Column` that states no `width` takes an equal share of the row, whole percent rounded
159
160
  down, and a last row with fewer items keeps that share, so it leaves empty space instead of
160
- stretching. Both lower to a loop in the IR, so nothing is computed at send time.
161
+ stretching. Both lower to a loop in the compiled template, so nothing is computed at send time.
161
162
 
162
163
  Rejected, each with its own diagnostic: calls other than `fmt.*` and `.map` (`dynamic-expression`),
163
164
  statements in a body (`statement-in-body`), imports other than `@samva/markup` and project files
@@ -234,7 +235,7 @@ is checked for these vendors, but a converter-supplied JSON Schema constraint re
234
235
  ### Authoring with an agent
235
236
 
236
237
  The package ships what an agent needs without any other source. `AGENTS.md` opens with the model:
237
- a template is TSX in the static profile that compiles to IR, and nothing runs at send time.
238
+ a template is TSX in the static profile that compiles to a compiled template, and nothing runs at send time.
238
239
  `@samva/markup/contract.json` lists the components with their props, the profile's forms, the
239
240
  formatters, the HTML elements and every diagnostic code with its fix; it is generated from the
240
241
  compiler, so it matches the version you installed. `skills/sml/SKILL.md` is the `sml` skill: it
@@ -243,7 +244,7 @@ for diagnostics an agent can apply.
243
244
 
244
245
  ### Previewing a render
245
246
 
246
- `renderIrPreview(ir, input)` from `@samva/markup/render` renders a fixture the way `renderIr` does
247
+ `previewEmail(template, input)` from `@samva/markup/edit` renders a fixture the way `render` does
247
248
  and stamps `data-samva-instance` on every element. Its `selections` map each stamped element to the
248
249
  line and column of the JSX that produced it, so an editor can select an element on the canvas and
249
250
  edit the source at that span. A delivered message never carries the attribute.
@@ -1,6 +1,6 @@
1
- import { t as BrandPlugin } from "./brand-plugin-CHhcei-j.js";
2
- import { r as EmailDiagnostic } from "./diagnostics-CUYlQ_6x.js";
3
- import { r as EmailNode } from "./jsx-runtime-DC7xG0aA.js";
1
+ import { t as BrandPlugin } from "./brand-plugin-Dj9SMpfS.js";
2
+ import { n as EmailDiagnostic } from "./diagnostics-D4vSLQrO.js";
3
+ import { i as EmailNode } from "./jsx-runtime-DL8NVF1f.js";
4
4
  //#region src/email/brand.d.ts
5
5
  interface EmailBrandAddress {
6
6
  readonly street1: string;
@@ -1,5 +1,6 @@
1
- import { D as samvaBrandPlugin, O as BRAND_FOOTER_MARKER, k as UNSUBSCRIBE_LINK, r as escapeAttribute } from "./html-BvRFepeO.js";
2
- import { a as jsx, n as Fragment } from "./jsx-runtime-CBlexoeR.js";
1
+ import { _ as UNSUBSCRIBE_LINK, g as BRAND_FOOTER_MARKER, r as escapeAttribute } from "./html-DJUK2YJF.js";
2
+ import { f as samvaBrandPlugin } from "./render-template-DwPDm3Hq.js";
3
+ import { a as jsx, c as rawMarkup, n as Fragment } from "./jsx-runtime-qL19u3Fy.js";
3
4
  //#region src/email/assets.ts
4
5
  /** Files a template may import, and what they are served as. */
5
6
  const ASSET_CONTENT_TYPES = {
@@ -86,13 +87,12 @@ const classes = (...names) => {
86
87
  const joined = names.filter((name) => name !== void 0 && name !== "").join(" ");
87
88
  return joined === "" ? void 0 : joined;
88
89
  };
89
- const row = (children, props = {}) => jsx("tr", {
90
- ...props,
91
- children: jsx("td", {
92
- style: { padding: "0 0 8px 0" },
93
- children
94
- })
90
+ const line = (children, last) => jsx("p", {
91
+ style: { margin: last ? "0" : "0 0 8px 0" },
92
+ children
95
93
  });
94
+ /** The alignment a footer `className` asks for; the footer centers when it names none. */
95
+ const footerAlignment = (className) => /(?:^|\s)text-(left|center|right)(?:\s|$)/.exec(className ?? "")?.[1] ?? "center";
96
96
  /** Bind `BrandLogo` and `BrandFooter` to one brand, marked as the plugin names. */
97
97
  const brandComponents = (brand, plugin = samvaBrandPlugin) => {
98
98
  const assets = brand.assets ?? {};
@@ -126,25 +126,24 @@ const brandComponents = (brand, plugin = samvaBrandPlugin) => {
126
126
  })] });
127
127
  };
128
128
  const BrandFooter = ({ unsubscribeLabel = "Unsubscribe", className }) => {
129
- const rows = [];
130
- if (footer.companyName !== void 0 && footer.companyName.trim() !== "") rows.push(row(jsx("span", {
129
+ const items = [];
130
+ const separator = () => items.length === 0 ? [] : [" · "];
131
+ if (footer.companyName !== void 0 && footer.companyName.trim() !== "") items.push(jsx("span", {
131
132
  className: "font-semibold",
132
133
  children: footer.companyName
133
- })));
134
- if (footer.address !== void 0) {
135
- const lines = addressLines(footer.address);
136
- if (lines.length > 0) rows.push(row(lines.flatMap((line, index) => index === 0 ? [line] : [jsx("br", {}), line])));
137
- }
138
- const links = footer.socialLinks ?? [];
139
- if (links.length > 0) rows.push(row(links.flatMap((link, index) => [...index === 0 ? [] : [" · "], jsx("a", {
134
+ }));
135
+ for (const link of footer.socialLinks ?? []) items.push(...separator(), jsx("a", {
140
136
  href: link.url,
141
137
  className: "text-muted-foreground underline",
142
138
  children: link.label
143
- })])));
144
- rows.push(row(unsubscribeLink(unsubscribeLabel, plugin), {
139
+ }));
140
+ items.push(jsx("span", {
145
141
  [plugin.unsubscribeRowAttribute]: "",
146
- [BRAND_FOOTER_MARKER]: true
142
+ [BRAND_FOOTER_MARKER]: true,
143
+ children: [...items.length === 0 ? [] : [rawMarkup(" · ", "\n")], unsubscribeLink(unsubscribeLabel, plugin)]
147
144
  }));
145
+ const address = footer.address === void 0 ? [] : addressLines(footer.address);
146
+ const alignment = footerAlignment(className);
148
147
  return jsx("table", {
149
148
  role: "presentation",
150
149
  width: "100%",
@@ -154,16 +153,10 @@ const brandComponents = (brand, plugin = samvaBrandPlugin) => {
154
153
  [plugin.footerAttribute]: "",
155
154
  [BRAND_FOOTER_MARKER]: true,
156
155
  children: jsx("tbody", { children: jsx("tr", { children: jsx("td", {
157
- align: "center",
158
- className: classes("text-xs text-muted-foreground text-center", className),
156
+ align: alignment,
157
+ className: classes("text-xs text-muted-foreground", alignment === "center" ? "text-center" : void 0, className),
159
158
  style: { padding: "32px 16px" },
160
- children: jsx("table", {
161
- role: "presentation",
162
- border: 0,
163
- cellpadding: 0,
164
- cellspacing: 0,
165
- children: jsx("tbody", { children: rows })
166
- })
159
+ children: [line(items, address.length === 0), ...address.length === 0 ? [] : [line([address.join(", ")], true)]]
167
160
  }) }) })
168
161
  });
169
162
  };
@@ -193,8 +186,8 @@ const renderedFooterMarkers = (html, plugin = samvaBrandPlugin) => ({
193
186
  * no unsubscribe group: there is no link to put there, so the row goes.
194
187
  */
195
188
  const withoutUnsubscribeRow = (rendered, plugin = samvaBrandPlugin) => ({
196
- html: rendered.html?.replace(new RegExp(`<tr ${plugin.unsubscribeRowAttribute}=""[^>]*>[\\s\\S]*?</tr>`, "g"), ""),
197
- text: rendered.text?.split("\n").filter((line) => !line.includes(plugin.unsubscribeUrlPlaceholder)).join("\n")
189
+ html: rendered.html?.replace(new RegExp(`<(span|tr) ${plugin.unsubscribeRowAttribute}=""[^>]*>[\\s\\S]*?</\\1>`, "g"), ""),
190
+ text: rendered.text?.split("\n").filter((textLine) => !textLine.includes(plugin.unsubscribeUrlPlaceholder)).join("\n")
198
191
  });
199
192
  //#endregion
200
193
  export { ASSET_CONTENT_TYPES as a, checkAssetBase as c, withoutUnsubscribeRow as i, isAssetPath as l, brandComponents as n, assetEntry as o, renderedFooterMarkers as r, assetUrl as s, BRAND_COMPONENT_CLASSES as t, isExternalAsset as u };
@@ -24,7 +24,9 @@ interface BrandPlugin {
24
24
  readonly noTrackAttribute: `data-${string}`;
25
25
  }
26
26
  /** The brand a specifier names: `undefined` slug means the default brand; `undefined` when it names none. */
27
- declare const parseBrandSpecifier: (specifier: string, plugin?: BrandPlugin) => {
27
+ declare const parseBrandSpecifier: (specifier: string, options?: {
28
+ readonly brandPlugin?: BrandPlugin | undefined;
29
+ }) => {
28
30
  readonly slug: string | undefined;
29
31
  } | undefined;
30
32
  //#endregion
package/dist/brand.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { t as BrandPlugin } from "./brand-plugin-CHhcei-j.js";
2
- import { _ as EmailBrandSocialLink, a as assetUrl, b as renderedFooterMarkers, c as isExternalAsset, d as BrandFooterProps, f as BrandLogoProps, g as EmailBrandFooter, h as EmailBrandAssets, i as assetEntry, l as BRAND_COMPONENT_CLASSES, m as EmailBrandAddress, n as AssetEntry, o as checkAssetBase, p as EmailBrand, r as AssetManifest, s as isAssetPath, t as ASSET_CONTENT_TYPES, u as BrandComponents, v as RenderedFooterMarkers, x as withoutUnsubscribeRow, y as brandComponents } from "./assets-tISHaQNF.js";
1
+ import { t as BrandPlugin } from "./brand-plugin-Dj9SMpfS.js";
2
+ import { _ as EmailBrandSocialLink, a as assetUrl, b as renderedFooterMarkers, c as isExternalAsset, d as BrandFooterProps, f as BrandLogoProps, g as EmailBrandFooter, h as EmailBrandAssets, i as assetEntry, l as BRAND_COMPONENT_CLASSES, m as EmailBrandAddress, n as AssetEntry, o as checkAssetBase, p as EmailBrand, r as AssetManifest, s as isAssetPath, t as ASSET_CONTENT_TYPES, u as BrandComponents, v as RenderedFooterMarkers, x as withoutUnsubscribeRow, y as brandComponents } from "./assets-CS5Xa_mr.js";
3
3
  import { t as Diagnostic } from "./diagnostic-model-DxthUZBT.js";
4
4
  import { Result } from "better-result";
5
5
  //#region src/email/samva-brand-plugin.d.ts
package/dist/brand.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D as samvaBrandPlugin } from "./html-BvRFepeO.js";
2
- import { a as THEME_COLOR_ROLES, c as foregroundDefaults, i as THEME_COLORS, l as parseBrandCss, n as DEFAULT_THEME_CSS, o as THEME_TOKENS, r as FILL_COLORS, s as contrastForeground, t as DEFAULT_THEME, u as printBrandCss } from "./theme-css-s9QwD5Ko.js";
3
- import { a as ASSET_CONTENT_TYPES, c as checkAssetBase, i as withoutUnsubscribeRow, l as isAssetPath, n as brandComponents, o as assetEntry, r as renderedFooterMarkers, s as assetUrl, t as BRAND_COMPONENT_CLASSES, u as isExternalAsset } from "./brand-nreeKWxF.js";
1
+ import { f as samvaBrandPlugin } from "./render-template-DwPDm3Hq.js";
2
+ import { a as ASSET_CONTENT_TYPES, c as checkAssetBase, i as withoutUnsubscribeRow, l as isAssetPath, n as brandComponents, o as assetEntry, r as renderedFooterMarkers, s as assetUrl, t as BRAND_COMPONENT_CLASSES, u as isExternalAsset } from "./brand-CIiEqkU7.js";
3
+ import { a as THEME_COLOR_ROLES, c as foregroundDefaults, i as THEME_COLORS, l as parseBrandCss, n as DEFAULT_THEME_CSS, o as THEME_TOKENS, r as FILL_COLORS, s as contrastForeground, t as DEFAULT_THEME, u as printBrandCss } from "./theme-css-P9F5S7Ic.js";
4
4
  export { ASSET_CONTENT_TYPES, BRAND_COMPONENT_CLASSES, DEFAULT_THEME, DEFAULT_THEME_CSS, FILL_COLORS, THEME_COLORS, THEME_COLOR_ROLES, THEME_TOKENS, assetEntry, assetUrl, brandComponents, checkAssetBase, contrastForeground, foregroundDefaults, isAssetPath, isExternalAsset, parseBrandCss, printBrandCss, renderedFooterMarkers, samvaBrandPlugin, withoutUnsubscribeRow };
@@ -0,0 +1,76 @@
1
+ import { t as BrandPlugin } from "./brand-plugin-Dj9SMpfS.js";
2
+ import { n as EmailDiagnostic } from "./diagnostics-D4vSLQrO.js";
3
+ import { v as CompiledTemplate } from "./compiled-template-Bc4Dq9zy.js";
4
+ import { r as EmittedPosition } from "./preview-rNoJBEIY.js";
5
+ import { caniemail } from "caniemail";
6
+ //#region src/email/compatibility.d.ts
7
+ type CaniemailClient = Parameters<typeof caniemail>[0]["clients"][number];
8
+ /** One client this compiler makes claims about. */
9
+ interface EmailClientTarget {
10
+ readonly id: CaniemailClient;
11
+ readonly label: string;
12
+ }
13
+ /**
14
+ * The pinned matrix. Every finding is reported per client, and a client absent
15
+ * from this list is a client Samva makes no claim about.
16
+ */
17
+ declare const EMAIL_CLIENT_MATRIX: readonly EmailClientTarget[];
18
+ interface CompatibilityProvenance {
19
+ /** The exact rules package a finding came from. */
20
+ readonly rules: string;
21
+ readonly apiVersion: string;
22
+ /** The day caniemail last changed the data this build was checked against. */
23
+ readonly lastUpdate: string;
24
+ }
25
+ declare const COMPATIBILITY_PROVENANCE: CompatibilityProvenance;
26
+ /**
27
+ * The constructs whose loss actually breaks a message: the recipient reads
28
+ * something different, in a different order, follows a link that does not work,
29
+ * or sees a layout that has collapsed. A target client dropping one of these
30
+ * blocks publication.
31
+ *
32
+ * The list is deliberately short and enumerated, because the contract is to
33
+ * block *known* failures. Nonessential differences require an assessment of
34
+ * emitted values: a concrete risk warns, expected differences and unverified
35
+ * behavior remain compatibility observations.
36
+ */
37
+ declare const ESSENTIAL_CONSTRUCTS: ReadonlySet<string>;
38
+ interface CompatibilityInput {
39
+ /** The document the recipient receives. */
40
+ readonly html: string;
41
+ /** The declared fixture checked by this invocation. */
42
+ readonly fixture?: string | undefined;
43
+ /** Retained head rules, when they are not already inside the HTML. */
44
+ readonly css?: string | undefined;
45
+ /** Emitted-offset ranges with the origins of the node that produced them. */
46
+ readonly positions?: readonly EmittedPosition[] | undefined;
47
+ readonly clients?: readonly EmailClientTarget[] | undefined;
48
+ }
49
+ /**
50
+ * Check the emitted document against the pinned matrix.
51
+ *
52
+ * Findings are grouped by construct rather than repeated per client, so one
53
+ * unsupported property is one diagnostic naming every client that drops it.
54
+ */
55
+ declare const checkHtmlCompatibility: (input: CompatibilityInput) => readonly EmailDiagnostic[];
56
+ /**
57
+ * Constructs the pinned data says nothing about for a target client. These are
58
+ * reported so "we did not check that" is never mistaken for "that works".
59
+ */
60
+ declare const unknownCoverage: (titles: Iterable<string>) => readonly EmailDiagnostic[];
61
+ /** A publication carrying one of these is refused; the message would arrive broken. */
62
+ declare const hasBlockingCompatibility: (findings: readonly EmailDiagnostic[]) => boolean;
63
+ //#endregion
64
+ //#region src/sml/compatibility.d.ts
65
+ /**
66
+ * Check what each declared fixture delivers against the pinned client matrix. The rules run on the
67
+ * rendered document, because what a client drops is what the compiler produced, and every finding
68
+ * is located at the source of the element that produced it. A fixture the renderer refuses throws
69
+ * its `RenderError`: validate fixtures before asking what they deliver.
70
+ */
71
+ declare const checkCompatibility: (template: CompiledTemplate, fixtures: Readonly<Record<string, unknown>>, options?: {
72
+ readonly clients?: readonly EmailClientTarget[] | undefined;
73
+ readonly brandPlugin?: BrandPlugin | undefined;
74
+ }) => readonly EmailDiagnostic[];
75
+ //#endregion
76
+ export { COMPATIBILITY_PROVENANCE, type CompatibilityInput, type CompatibilityProvenance, EMAIL_CLIENT_MATRIX, ESSENTIAL_CONSTRUCTS, type EmailClientTarget, checkCompatibility, checkHtmlCompatibility, hasBlockingCompatibility, unknownCoverage };