@samva/markup 0.8.0 → 0.10.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 (172) hide show
  1. package/AGENTS.md +107 -46
  2. package/CHANGELOG.md +345 -0
  3. package/README.md +188 -52
  4. package/dist/{brand-DcoVkh_S.d.ts → assets-BIsyMQ92.d.ts} +41 -11
  5. package/dist/{email/brand.js → brand-C0MKTwmX.js} +64 -4
  6. package/dist/{theme-css.d.ts → brand.d.ts} +12 -3
  7. package/dist/brand.js +4 -0
  8. package/dist/channel-specs-C2feJwuP.d.ts +13 -0
  9. package/dist/compiler.d.ts +88 -115
  10. package/dist/compiler.js +750 -171
  11. package/dist/components-BZCqegS8.js +1864 -0
  12. package/dist/diagnostic-model-DJtqbETd.js +38 -0
  13. package/dist/{diagnostics-OcQKQYSY.d.ts → diagnostic-model-DxthUZBT.d.ts} +3 -21
  14. package/dist/{diagnostics-BMYtBCQk.d.ts → diagnostics-B-Gym34R.d.ts} +8 -4
  15. package/dist/{email/diagnostics.js → diagnostics-CnKFefKz.js} +4 -3
  16. package/dist/diagnostics.d.ts +410 -2
  17. package/dist/diagnostics.js +402 -52
  18. package/dist/edit.d.ts +234 -0
  19. package/dist/edit.js +947 -0
  20. package/dist/email/jsx-dev-runtime.d.ts +2 -2
  21. package/dist/email/jsx-dev-runtime.js +1 -2
  22. package/dist/email/jsx-runtime.d.ts +1 -1
  23. package/dist/email/jsx-runtime.js +1 -85
  24. package/dist/{email/components.d.ts → email.d.ts} +22 -11
  25. package/dist/email.js +2 -0
  26. package/dist/fmt.d.ts +32 -0
  27. package/dist/fmt.js +174 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +2 -0
  30. package/dist/{render-vAv_4r9v.js → ir-CLhlklKV.js} +96 -279
  31. package/dist/jsx-runtime-CBlexoeR.js +98 -0
  32. package/dist/{elements-BzjjnoCS.d.ts → jsx-runtime-ylIOnaSf.d.ts} +76 -76
  33. package/dist/profile-pWGtWZXF.js +3803 -0
  34. package/dist/render-BPonDpCl.d.ts +539 -0
  35. package/dist/render-BQmfh94k.js +257 -0
  36. package/dist/render.d.ts +35 -0
  37. package/dist/render.js +49 -0
  38. package/dist/sms.d.ts +16 -0
  39. package/dist/sms.js +12 -0
  40. package/dist/source-locations-C8wCsiAy.d.ts +23 -0
  41. package/dist/template-DMPwxMSq.d.ts +65 -0
  42. package/dist/template-zLUjcwfF.js +42 -0
  43. package/dist/{theme-css-hFqcPSar.js → theme-css-JtRCm1fb.js} +2 -2
  44. package/dist/whatsapp.d.ts +57 -0
  45. package/dist/whatsapp.js +22 -0
  46. package/generated/contract.json +1788 -0
  47. package/generated/reference.md +360 -0
  48. package/package.json +46 -239
  49. package/samva-brand.d.ts +4 -4
  50. package/skills/sml/SKILL.md +123 -0
  51. package/dist/ast-BrDtApwS.d.ts +0 -107
  52. package/dist/ast.d.ts +0 -2
  53. package/dist/ast.js +0 -124
  54. package/dist/candidate.d.ts +0 -29
  55. package/dist/candidate.js +0 -88
  56. package/dist/catalog-CKuOooB8.d.ts +0 -354
  57. package/dist/checks-D73Xizbw.js +0 -59366
  58. package/dist/checks.d.ts +0 -33
  59. package/dist/checks.js +0 -2
  60. package/dist/classes.d.ts +0 -26
  61. package/dist/classes.js +0 -184
  62. package/dist/compile-sms.d.ts +0 -28
  63. package/dist/compile-sms.js +0 -44
  64. package/dist/compile-whatsapp.d.ts +0 -56
  65. package/dist/compile-whatsapp.js +0 -186
  66. package/dist/compile.d.ts +0 -18
  67. package/dist/compile.js +0 -29
  68. package/dist/components.d.ts +0 -289
  69. package/dist/components.js +0 -106
  70. package/dist/conditionals.d.ts +0 -10
  71. package/dist/conditionals.js +0 -74
  72. package/dist/corpus-CPPfgAHN.d.ts +0 -106
  73. package/dist/css-CS2jLID6.d.ts +0 -109
  74. package/dist/email/assets.d.ts +0 -40
  75. package/dist/email/assets.js +0 -63
  76. package/dist/email/brand-footer.d.ts +0 -34
  77. package/dist/email/brand-footer.js +0 -62
  78. package/dist/email/brand.d.ts +0 -2
  79. package/dist/email/components.js +0 -240
  80. package/dist/email/diagnostics.d.ts +0 -2
  81. package/dist/email/elements.d.ts +0 -2
  82. package/dist/email/elements.js +0 -299
  83. package/dist/email/html.d.ts +0 -2
  84. package/dist/email/html.js +0 -278
  85. package/dist/email/render.d.ts +0 -50
  86. package/dist/email/render.js +0 -3
  87. package/dist/email/selection.d.ts +0 -2
  88. package/dist/email/selection.js +0 -91
  89. package/dist/email/source-edit.d.ts +0 -67
  90. package/dist/email/source-edit.js +0 -185
  91. package/dist/email/stylesheets.d.ts +0 -15
  92. package/dist/email/stylesheets.js +0 -48
  93. package/dist/email/text.d.ts +0 -7
  94. package/dist/email/text.js +0 -100
  95. package/dist/emit.d.ts +0 -51
  96. package/dist/emit.js +0 -151
  97. package/dist/html-Bl9BAdqc.d.ts +0 -42
  98. package/dist/interpolate.d.ts +0 -24
  99. package/dist/interpolate.js +0 -43
  100. package/dist/jsx-dev-runtime.d.ts +0 -11
  101. package/dist/jsx-dev-runtime.js +0 -15
  102. package/dist/jsx-runtime-I-1nCtl2.d.ts +0 -57
  103. package/dist/jsx-runtime.d.ts +0 -2
  104. package/dist/jsx-runtime.js +0 -121
  105. package/dist/lexer.d.ts +0 -60
  106. package/dist/lexer.js +0 -281
  107. package/dist/model.d.ts +0 -24
  108. package/dist/model.js +0 -77
  109. package/dist/ops-BvwqHl7V.js +0 -630
  110. package/dist/ops.d.ts +0 -114
  111. package/dist/ops.js +0 -2
  112. package/dist/parser-t29cHLVL.d.ts +0 -396
  113. package/dist/parser.d.ts +0 -2
  114. package/dist/parser.js +0 -859
  115. package/dist/predicate-BI6uRRM-.js +0 -549
  116. package/dist/predicate-M-05Nd7R.d.ts +0 -56
  117. package/dist/predicate.d.ts +0 -2
  118. package/dist/predicate.js +0 -2
  119. package/dist/printer.d.ts +0 -6
  120. package/dist/printer.js +0 -122
  121. package/dist/registry-Dcp6QEK_.d.ts +0 -73
  122. package/dist/registry.d.ts +0 -2
  123. package/dist/registry.js +0 -646
  124. package/dist/render-template.d.ts +0 -45
  125. package/dist/render-template.js +0 -76
  126. package/dist/selection-CCArx9lU.d.ts +0 -91
  127. package/dist/showcase-egkusQBf.d.ts +0 -415
  128. package/dist/sms-segments-y4FpPilJ.d.ts +0 -13
  129. package/dist/sms-segments.d.ts +0 -2
  130. package/dist/sms-segments.js +0 -46
  131. package/dist/source-locations-DrCM82Ym.d.ts +0 -67
  132. package/dist/source-locations.d.ts +0 -2
  133. package/dist/source-locations.js +0 -102
  134. package/dist/starters/category-mapping.d.ts +0 -13
  135. package/dist/starters/category-mapping.js +0 -49
  136. package/dist/starters/corpus.d.ts +0 -2
  137. package/dist/starters/corpus.js +0 -194
  138. package/dist/starters/library.d.ts +0 -18
  139. package/dist/starters/library.js +0 -692
  140. package/dist/starters/public-gallery.d.ts +0 -266
  141. package/dist/starters/public-gallery.js +0 -916
  142. package/dist/starters/public-handoff.d.ts +0 -164
  143. package/dist/starters/public-handoff.js +0 -361
  144. package/dist/starters/public-preview-artifacts.d.ts +0 -43
  145. package/dist/starters/public-preview-artifacts.js +0 -104
  146. package/dist/starters/public-preview.d.ts +0 -61
  147. package/dist/starters/public-preview.js +0 -83
  148. package/dist/starters/showcase.d.ts +0 -2
  149. package/dist/starters/showcase.js +0 -335
  150. package/dist/starters/sources.d.ts +0 -45
  151. package/dist/starters/sources.js +0 -7762
  152. package/dist/starters/theme.d.ts +0 -45
  153. package/dist/starters/theme.js +0 -77
  154. package/dist/starters.d.ts +0 -2
  155. package/dist/starters.js +0 -874
  156. package/dist/tailwind-BYq7Lo6u.js +0 -727
  157. package/dist/template-DWftqh4x.d.ts +0 -60
  158. package/dist/template.d.ts +0 -2
  159. package/dist/template.js +0 -37
  160. package/dist/theme-css.js +0 -2
  161. package/dist/tsx-source-instrumentation.d.ts +0 -30
  162. package/dist/tsx-source-instrumentation.js +0 -115
  163. package/dist/tsx-tracer.d.ts +0 -112
  164. package/dist/tsx-tracer.js +0 -985
  165. package/dist/variables-J_L3qxRa.d.ts +0 -77
  166. package/dist/variables.d.ts +0 -2
  167. package/dist/variables.js +0 -141
  168. package/dist/version-BITr58kk.js +0 -96
  169. package/generated/sml-agent-contract.json +0 -709
  170. package/generated/sml-email-agent-contract.json +0 -615
  171. package/generated/sml-email-language-reference.md +0 -94
  172. package/generated/sml-language-reference.md +0 -195
package/README.md CHANGED
@@ -6,59 +6,195 @@ directly for TSX templates or together with `@samva/vite` for the visual editor
6
6
  ## Install
7
7
 
8
8
  ```sh
9
- npm install --save-dev @samva/markup @samva/vite vite
9
+ npm install --save-dev @samva/markup
10
10
  ```
11
11
 
12
- Configure TypeScript with `jsx: "react-jsx"` and
13
- `jsxImportSource: "@samva/markup/email"`. Install `effect` for this schema example
14
- (or use a Zod 4 schema). Email entries use `.tsx` and default-export a definition:
12
+ Configure TypeScript with `jsx: "react-jsx"` and `jsxImportSource: "@samva/markup/email"`. A
13
+ template is one `.tsx` file that default-exports `defineTemplate`:
15
14
 
16
15
  ```tsx
17
- /** @jsxImportSource @samva/markup/email */
18
- import { Schema } from "effect";
19
- import { Email, Section } from "@samva/markup/email/components";
20
- import { defineEmail } from "@samva/markup/template";
21
-
22
- export default defineEmail({
23
- id: "welcome",
24
- schema: Schema.toStandardJSONSchemaV1(
25
- Schema.Struct({ firstName: Schema.String, items: Schema.Array(Schema.String) }),
26
- ),
27
- fixtures: { basic: { firstName: "Ada", items: ["Your first project"] } },
28
- render: (input) => ({
29
- subject: `Welcome, ${input.firstName}`,
30
- preheader: "Your account is ready",
31
- body: (
16
+ import { defineTemplate } from "@samva/markup";
17
+ import { Button, Email, Section } from "@samva/markup/email";
18
+ import { fmt } from "@samva/markup/fmt";
19
+ import { jsonSchema } from "@samva/markup/input-schema";
20
+
21
+ import { OrderRow } from "../partials/order-row";
22
+
23
+ export default defineTemplate({
24
+ id: "order-shipped",
25
+ schema: jsonSchema<{
26
+ name: string;
27
+ orderId: string;
28
+ currency: string;
29
+ total: number;
30
+ items: { title: string; qty: number; price: number }[];
31
+ trackingUrl?: string;
32
+ }>({
33
+ type: "object",
34
+ properties: {
35
+ name: { type: "string" },
36
+ orderId: { type: "string" },
37
+ currency: { type: "string" },
38
+ total: { type: "number" },
39
+ items: {
40
+ type: "array",
41
+ items: {
42
+ type: "object",
43
+ properties: {
44
+ title: { type: "string" },
45
+ qty: { type: "number" },
46
+ price: { type: "number" },
47
+ },
48
+ required: ["title", "qty", "price"],
49
+ additionalProperties: false,
50
+ },
51
+ },
52
+ trackingUrl: { type: "string" },
53
+ },
54
+ required: ["name", "orderId", "currency", "total", "items"],
55
+ additionalProperties: false,
56
+ }),
57
+ fixtures: {
58
+ default: {
59
+ name: "Ada",
60
+ orderId: "A-1042",
61
+ currency: "USD",
62
+ total: 84,
63
+ items: [{ title: "Linen shirt", qty: 2, price: 42 }],
64
+ trackingUrl: "https://track.example.com/A-1042",
65
+ },
66
+ "no-tracking": {
67
+ name: "Ada",
68
+ orderId: "A-1042",
69
+ currency: "USD",
70
+ total: 84,
71
+ items: [{ title: "Linen shirt", qty: 2, price: 42 }],
72
+ },
73
+ },
74
+ email: {
75
+ subject: (input) => `Order ${input.orderId} is on its way`,
76
+ preheader: (input) => `${input.items.length} items, ${fmt.money(input.total, input.currency)}`,
77
+ body: (input) => (
32
78
  <Email>
33
- <Section>
34
- <h1>Welcome, {input.firstName}</h1>
35
- {input.items.length > 0 && (
36
- <ul>
37
- {input.items.map((item) => (
38
- <li key={item}>{item}</li>
39
- ))}
40
- </ul>
79
+ <Section className="px-8 py-6">
80
+ <h1 className="text-2xl font-semibold">Hi {input.name}, it shipped.</h1>
81
+ {input.items.map((item) => (
82
+ <OrderRow item={item} currency={input.currency} />
83
+ ))}
84
+ <p className="mt-4 font-medium">Total {fmt.money(input.total, input.currency)}</p>
85
+ {input.trackingUrl && (
86
+ <Button href={input.trackingUrl} width={200} height={44}>
87
+ Track package
88
+ </Button>
41
89
  )}
42
90
  </Section>
43
91
  </Email>
44
92
  ),
45
- }),
93
+ },
94
+ });
95
+ ```
96
+
97
+ A template is read, not run. `@samva/markup/compiler` parses the file and the project files it
98
+ imports without evaluating them, checks every binding against the schema, resolves Tailwind and CSS
99
+ against the markup, and produces the template's IR: JSON, versioned (`"sml": 1`), carrying the
100
+ markup, the bindings, the conditions, the loops and the formatters. `@samva/markup/render`
101
+ renders that IR against an input. It is a pure function with no dependencies, so nothing executes
102
+ at send time.
103
+
104
+ ```ts
105
+ import { renderIr } from "@samva/markup/render";
106
+ import { compileTemplate } from "@samva/markup/compiler";
107
+
108
+ const compiled = await compileTemplate({
109
+ files, // the project's files by path; binary files become content-addressed assets
110
+ entry: "templates/order-shipped.tsx",
111
+ assetBase: "https://assets.example.com",
112
+ tailwind: { css: files["theme.css"] },
113
+ });
114
+ if (compiled.ir === undefined) throw new Error(compiled.diagnostics.map(String).join("\n"));
115
+
116
+ const { subject, preheader, html, text } = renderIr(compiled.ir, compiled.fixtures.default, {
117
+ locale: "en-US",
118
+ timeZone: "UTC",
46
119
  });
47
120
  ```
48
121
 
49
- The definition owns a stable, project-unique `id`, a portable JSON input schema, named fixtures,
50
- and a synchronous render function. Imported helpers are ordinary modules, not catalog entries.
51
- Email executes real TypeScript against validated input and lowers a semantic tree to HTML and text.
52
- SMS and WhatsApp keep their structured channel compiler and `@samva/markup` JSX runtime.
122
+ ### The static profile
123
+
124
+ The profile is the subset of TSX the compiler reads without running it: the forms an agent already
125
+ writes for React. Anything else is a compile error that names the fix.
126
+
127
+ | Form | Example |
128
+ | ------------------- | ------------------------------------------------------------------------ |
129
+ | Text binding | `{input.name}` |
130
+ | Attribute binding | `href={input.trackingUrl}` |
131
+ | Template string | `` `Order ${input.orderId}` `` |
132
+ | Conditional | `{input.trackingUrl && <Button … />}` |
133
+ | Either/or | `{input.vip ? <Gold /> : <Standard />}` |
134
+ | Conditions | `===`, `!==`, `<`, `>`, `<=`, `>=`, `!`, `&&`, `\|\|`, `.length` |
135
+ | Loop | `{input.items.map((item, i) => <Row … />)}` |
136
+ | Filter | `input.steps.filter((step) => step.done).map(…)`, `.filter(…).length` |
137
+ | Grid | `<Columns each={input.deals} per={2}>{(deal) => <Column … />}</Columns>` |
138
+ | Formatter | `fmt.money(input.total, input.currency)` |
139
+ | Partial | `<OrderRow item={item} />`, with props and `children` |
140
+ | Conditional class | `className={input.vip ? "bg-amber-100" : "bg-white"}` |
141
+ | Destructured params | `body: ({ name, items }) => …` |
142
+ | Arithmetic | `item.price * item.qty`, inside a formatter argument or a condition |
143
+
144
+ A `.filter` callback takes the item and one condition from the same grammar as `&&` and `?:`; it
145
+ chains before `.map(...)` or `.length`. `<Columns each per>` lays a list out in rows of `per` cells
146
+ (one row without `per`): the child is a function from an item, and its index within the row, to a
147
+ `Column`. A `Column` that states no `width` takes an equal share of the row, whole percent rounded
148
+ down, and a last row with fewer items keeps that share, so it leaves empty space instead of
149
+ stretching. Both lower to a loop in the IR, so nothing is computed at send time.
150
+
151
+ Rejected, each with its own diagnostic: calls other than `fmt.*` and `.map` (`dynamic-expression`),
152
+ statements in a body (`statement-in-body`), imports other than `@samva/markup` and project files
153
+ (`non-project-import`), event handlers, hooks and `dangerouslySetInnerHTML`, class names built at
154
+ run time (`dynamic-class`), and recursion (`recursive-partial`). There is no escape hatch: logic the
155
+ profile cannot express is computed by the caller and sent in the payload.
156
+
157
+ Each diagnostic has a stable `code`, the exact `file:line:column`, a one-sentence `message` and a
158
+ `fix`. `unknown-field` names the field and suggests the nearest one, `unguarded-optional` marks an
159
+ optional field read outside a guard, and `fixture-invalid` names the fixture that no longer matches
160
+ the schema.
161
+
162
+ Only `@samva/markup` and files inside the project can be imported, so a project has no other
163
+ dependency to install. A fixture is data: a literal, or constants and imported assets assembled from
164
+ literals.
165
+
166
+ ### Formatters
167
+
168
+ `@samva/markup/fmt` is the only set of functions a body may call. Each one is deterministic `Intl`
169
+ with the locale and time zone the render supplies; the template's `locale` is the default and an
170
+ explicit argument overrides it.
171
+
172
+ | Formatter | Example | Output |
173
+ | -------------------------------------- | -------------------------------------------------- | ------------- |
174
+ | `fmt.money(amount, currency, locale?)` | `fmt.money(84, "EUR", "de")` | 84,00 € |
175
+ | `fmt.number(n, options?, locale?)` | `fmt.number(1234.5, { maximumFractionDigits: 0 })` | 1,235 |
176
+ | `fmt.date(iso, style?, locale?)` | `fmt.date(input.shippedAt, "medium")` | Sep 29, 2026 |
177
+ | `fmt.time(iso, style?, locale?)` | `fmt.time(input.slot, "short")` | 3:30 PM |
178
+ | `fmt.plural(n, forms, locale?)` | `fmt.plural(n, { one: "item", other: "items" })` | items |
179
+ | `fmt.list(values, locale?)` | `fmt.list(["Ada", "Grace"])` | Ada and Grace |
180
+
181
+ ### Channels
182
+
183
+ One template carries one schema and a body for each channel it supports. `@samva/markup/sms` and
184
+ `@samva/markup/whatsapp` export the channel components (`Sms`; `WhatsApp` with `WhatsApp.Header`,
185
+ `.Body`, `.Footer`, `.Buttons`, `.UrlButton`, `.QuickReplyButton`, `.PhoneButton` and
186
+ `.CopyCodeButton`). Their names are distinct per channel, so a wrong prop fails in the type checker.
187
+ WhatsApp bindings compile to Meta's positional `{{1}}` placeholders (`whatsappTemplate`), and a
188
+ conditional in a WhatsApp text part is refused because Meta templates cannot branch. The platform
189
+ gates these channels.
53
190
 
54
191
  ### Explicit template inputs
55
192
 
56
- `@samva/markup/template` exports `defineEmail({ id, schema, fixtures, render })`, the email
57
- channel's authoring helper; `TemplateDefinition` and `TemplateInput` are channel-neutral.
193
+ `@samva/markup` exports `defineTemplate` and `TemplateInput`.
58
194
  The schema must implement Standard **JSON Schema** v1; a validator-only Standard Schema is
59
195
  insufficient. `Schema.toStandardJSONSchemaV1` (Effect) and Zod 4 schemas support caller-input
60
- inference, including imported render functions and named fixtures. `TemplateInput<typeof template>`
61
- extracts the caller type. Rendering receives input unchanged.
196
+ inference, including named fixtures. `TemplateInput<typeof template>` extracts the caller type.
197
+ Rendering receives input unchanged.
62
198
 
63
199
  The portable profile uses JSON Schema 2020-12. Supported validation keywords are `type`, `enum`,
64
200
  `const`, `required`, numeric bounds and `multipleOf`, string lengths and `pattern`, object/array
@@ -84,22 +220,22 @@ The pinned Zod/Effect adapter rejects lazy/suspended schema nodes before executi
84
220
  author recursive contracts directly using local JSON Schema references. Custom predicate metadata
85
221
  is checked for these vendors, but a converter-supplied JSON Schema constraint remains its contract.
86
222
 
87
- ### Actual-input email JSX
88
-
89
- Use `jsxImportSource: "@samva/markup/email"` for object template bodies. Ordinary HTML JSX,
90
- imported synchronous components, nested JSON, conditionals and mapped arrays lower directly to a
91
- semantic tree. `renderEmail(template, data)` validates unchanged input and returns subject,
92
- preheader, escaped HTML, explicit or derived text, and the derived tree. Development JSX preserves
93
- source origins and component call sites. This synchronous library call requires an isolated host
94
- before executing untrusted source; it is not itself a sandbox.
95
-
96
- The rendering host has no ambient clock and no entropy source. Reading the current instant or a
97
- random value throws unless the template declares the matching fixed value in `runtime`, which takes
98
- an optional `now` (epoch milliseconds) and `randomSeed` (32-bit unsigned integer). A declaration is
99
- recorded on the publication and applies to every render, including module initializers that capture
100
- a value at import. `new Date(value)` with an explicit argument always works, so a message either
101
- takes its time from validated input or states it once as configuration. Locale (`en-US`) and time
102
- zone (UTC) are publication-wide and back every `Intl` constructor and `toLocale*` call.
223
+ ### Authoring with an agent
224
+
225
+ The package ships what an agent needs without any other source. `AGENTS.md` opens with the model:
226
+ a template is TSX in the static profile that compiles to IR, and nothing runs at send time.
227
+ `@samva/markup/contract.json` lists the components with their props, the profile's forms, the
228
+ formatters, the HTML elements and every diagnostic code with its fix; it is generated from the
229
+ compiler, so it matches the version you installed. `skills/sml/SKILL.md` is the `sml` skill: it
230
+ routes an agent through write, check, render, look and repair. Run `samva templates check --json`
231
+ for diagnostics an agent can apply.
232
+
233
+ ### Previewing a render
234
+
235
+ `renderIrPreview(ir, input)` from `@samva/markup/render` renders a fixture the way `renderIr` does
236
+ and stamps `data-samva-instance` on every element. Its `selections` map each stamped element to the
237
+ line and column of the JSX that produced it, so an editor can select an element on the canvas and
238
+ edit the source at that span. A delivered message never carries the attribute.
103
239
 
104
240
  Portable validation uses `@cfworker/json-schema` without runtime code generation. The
105
241
  2020-12 meta-schema is checked at every supported schema position, and the explicit profile rejects
@@ -1,12 +1,5 @@
1
- import { m as EmailNode } from "./elements-BzjjnoCS.js";
2
- //#region src/internal/unsubscribe-link.d.ts
3
- /**
4
- * The per-recipient unsubscribe URL is not known at render: the send path
5
- * substitutes each delivery's link for this placeholder, or removes the
6
- * unsubscribe row when the send resolved no unsubscribe group.
7
- */
8
- declare const UNSUBSCRIBE_URL_PLACEHOLDER = "%samva:unsubscribe-url%";
9
- //#endregion
1
+ import { r as EmailDiagnostic } from "./diagnostics-B-Gym34R.js";
2
+ import { r as EmailNode } from "./jsx-runtime-ylIOnaSf.js";
10
3
  //#region src/email/brand.d.ts
11
4
  interface EmailBrandAddress {
12
5
  readonly street1: string;
@@ -84,5 +77,42 @@ declare const withoutUnsubscribeRow: (rendered: {
84
77
  readonly text: string | undefined;
85
78
  };
86
79
  //#endregion
87
- export { EmailBrand as a, EmailBrandFooter as c, RenderedFooterMarkers as d, UNSUBSCRIBE_ROW_ATTRIBUTE as f, UNSUBSCRIBE_URL_PLACEHOLDER as g, withoutUnsubscribeRow as h, BrandLogoProps as i, EmailBrandSocialLink as l, renderedFooterMarkers as m, BrandComponents as n, EmailBrandAddress as o, brandComponents as p, BrandFooterProps as r, EmailBrandAssets as s, BRAND_COMPONENT_CLASSES as t, FOOTER_ATTRIBUTE as u };
88
- //# sourceMappingURL=brand-DcoVkh_S.d.ts.map
80
+ //#region src/email/assets.d.ts
81
+ /** Files a template may import, and what they are served as. */
82
+ declare const ASSET_CONTENT_TYPES: Readonly<Record<string, string>>;
83
+ interface AssetEntry {
84
+ /** Project-relative path the template imported. */
85
+ readonly path: string;
86
+ /** SHA-256 of the exact bytes, lowercase hex. */
87
+ readonly digest: string;
88
+ readonly bytes: number;
89
+ readonly contentType: string;
90
+ /** `<digest><ext>` — the name the asset has under any base. */
91
+ readonly fileName: string;
92
+ }
93
+ interface AssetManifest {
94
+ /** Where these assets are served from in this build. */
95
+ readonly base: string;
96
+ readonly assets: readonly AssetEntry[];
97
+ }
98
+ /** True when the compiler turns this import into a content-addressed asset. */
99
+ declare const isAssetPath: (path: string) => boolean;
100
+ /** Address one file by its content. */
101
+ declare const assetEntry: (path: string, bytes: Uint8Array) => Promise<AssetEntry>;
102
+ /**
103
+ * The URL an asset has under a base. The base is a build input: publication
104
+ * passes its hosted origin, and a local export passes whatever it will serve
105
+ * the exported files from.
106
+ */
107
+ declare const assetUrl: (entry: AssetEntry, base: string) => string;
108
+ /** Record, but never fetch, a URL the template points at. */
109
+ declare const isExternalAsset: (reference: string) => boolean;
110
+ /**
111
+ * Reject an asset base that would produce URLs a client will not load. The
112
+ * relative forms are the ones a page on disk resolves against its own
113
+ * directory, which is what a local export writes.
114
+ */
115
+ declare const checkAssetBase: (base: string) => readonly EmailDiagnostic[];
116
+ //#endregion
117
+ export { withoutUnsubscribeRow as C, renderedFooterMarkers as S, EmailBrandSocialLink as _, assetUrl as a, UNSUBSCRIBE_ROW_ATTRIBUTE as b, isExternalAsset as c, BrandFooterProps as d, BrandLogoProps as f, EmailBrandFooter as g, EmailBrandAssets as h, assetEntry as i, BRAND_COMPONENT_CLASSES as l, EmailBrandAddress as m, AssetEntry as n, checkAssetBase as o, EmailBrand as p, AssetManifest as r, isAssetPath as s, ASSET_CONTENT_TYPES as t, BrandComponents as u, FOOTER_ATTRIBUTE as v, brandComponents as x, RenderedFooterMarkers as y };
118
+ //# sourceMappingURL=assets-BIsyMQ92.d.ts.map
@@ -1,5 +1,65 @@
1
- import { Fragment, jsx } from "./jsx-runtime.js";
2
- import { i as UNSUBSCRIBE_URL_PLACEHOLDER, r as UNSUBSCRIBE_LINK, t as BRAND_FOOTER_MARKER } from "../unsubscribe-link-CoesnQDU.js";
1
+ import { i as UNSUBSCRIBE_URL_PLACEHOLDER, r as UNSUBSCRIBE_LINK, t as BRAND_FOOTER_MARKER } from "./unsubscribe-link-CoesnQDU.js";
2
+ import { a as jsx, n as Fragment } from "./jsx-runtime-CBlexoeR.js";
3
+ //#region src/email/assets.ts
4
+ /** Files a template may import, and what they are served as. */
5
+ const ASSET_CONTENT_TYPES = {
6
+ ".png": "image/png",
7
+ ".jpg": "image/jpeg",
8
+ ".jpeg": "image/jpeg",
9
+ ".gif": "image/gif",
10
+ ".webp": "image/webp",
11
+ ".avif": "image/avif",
12
+ ".svg": "image/svg+xml",
13
+ ".ico": "image/x-icon",
14
+ ".woff": "font/woff",
15
+ ".woff2": "font/woff2",
16
+ ".ttf": "font/ttf",
17
+ ".otf": "font/otf",
18
+ ".pdf": "application/pdf",
19
+ ".txt": "text/plain"
20
+ };
21
+ const extensionOf = (path) => {
22
+ const dot = path.lastIndexOf(".");
23
+ return dot < 0 ? "" : path.slice(dot).toLowerCase();
24
+ };
25
+ /** True when the compiler turns this import into a content-addressed asset. */
26
+ const isAssetPath = (path) => Object.hasOwn(ASSET_CONTENT_TYPES, extensionOf(path));
27
+ const hex = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
28
+ /** Address one file by its content. */
29
+ const assetEntry = async (path, bytes) => {
30
+ const extension = extensionOf(path);
31
+ const view = new Uint8Array(bytes.byteLength);
32
+ view.set(bytes);
33
+ const digest = hex(await crypto.subtle.digest("SHA-256", view));
34
+ return {
35
+ path,
36
+ digest,
37
+ bytes: bytes.byteLength,
38
+ contentType: ASSET_CONTENT_TYPES[extension] ?? "application/octet-stream",
39
+ fileName: `${digest}${extension}`
40
+ };
41
+ };
42
+ /**
43
+ * The URL an asset has under a base. The base is a build input: publication
44
+ * passes its hosted origin, and a local export passes whatever it will serve
45
+ * the exported files from.
46
+ */
47
+ const assetUrl = (entry, base) => `${base.replace(/\/+$/, "")}/${entry.fileName}`;
48
+ const EXTERNAL_IMAGE = /^https:\/\//i;
49
+ /** Record, but never fetch, a URL the template points at. */
50
+ const isExternalAsset = (reference) => EXTERNAL_IMAGE.test(reference);
51
+ /**
52
+ * Reject an asset base that would produce URLs a client will not load. The
53
+ * relative forms are the ones a page on disk resolves against its own
54
+ * directory, which is what a local export writes.
55
+ */
56
+ const checkAssetBase = (base) => /^(https:\/\/|\.{1,2}\/|\/)/i.test(base) ? [] : [{
57
+ code: "invalid-asset-base",
58
+ severity: "error",
59
+ message: `Asset base ${JSON.stringify(base)} must be an https URL, an absolute path, or a relative path for a local export.`,
60
+ origins: []
61
+ }];
62
+ //#endregion
3
63
  //#region src/email/brand.ts
4
64
  /** Marks the block the send path recognizes as the template's own footer. */
5
65
  const FOOTER_ATTRIBUTE = "data-samva-footer";
@@ -143,6 +203,6 @@ const withoutUnsubscribeRow = (rendered) => ({
143
203
  text: rendered.text?.split("\n").filter((line) => !line.includes(UNSUBSCRIBE_URL_PLACEHOLDER)).join("\n")
144
204
  });
145
205
  //#endregion
146
- export { BRAND_COMPONENT_CLASSES, FOOTER_ATTRIBUTE, UNSUBSCRIBE_ROW_ATTRIBUTE, UNSUBSCRIBE_URL_PLACEHOLDER, brandComponents, renderedFooterMarkers, withoutUnsubscribeRow };
206
+ export { renderedFooterMarkers as a, assetEntry as c, isAssetPath as d, isExternalAsset as f, brandComponents as i, assetUrl as l, FOOTER_ATTRIBUTE as n, withoutUnsubscribeRow as o, UNSUBSCRIBE_ROW_ATTRIBUTE as r, ASSET_CONTENT_TYPES as s, BRAND_COMPONENT_CLASSES as t, checkAssetBase as u };
147
207
 
148
- //# sourceMappingURL=brand.js.map
208
+ //# sourceMappingURL=brand-C0MKTwmX.js.map
@@ -1,5 +1,14 @@
1
- import { t as Diagnostic } from "./diagnostics-OcQKQYSY.js";
1
+ import { C as withoutUnsubscribeRow, S as renderedFooterMarkers, _ as EmailBrandSocialLink, a as assetUrl, b as UNSUBSCRIBE_ROW_ATTRIBUTE, 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 FOOTER_ATTRIBUTE, x as brandComponents, y as RenderedFooterMarkers } from "./assets-BIsyMQ92.js";
2
+ import { t as Diagnostic } from "./diagnostic-model-DxthUZBT.js";
2
3
  import { Result } from "better-result";
4
+ //#region src/internal/unsubscribe-link.d.ts
5
+ /**
6
+ * The per-recipient unsubscribe URL is not known at render: the send path
7
+ * substitutes each delivery's link for this placeholder, or removes the
8
+ * unsubscribe row when the send resolved no unsubscribe group.
9
+ */
10
+ declare const UNSUBSCRIBE_URL_PLACEHOLDER = "%samva:unsubscribe-url%";
11
+ //#endregion
3
12
  //#region src/theme-css.d.ts
4
13
  /** One `@font-face` a brand declares. `src` is the declaration value as authored. */
5
14
  interface BrandFontFace {
@@ -61,5 +70,5 @@ declare const brandThemeVariables: (brand: BrandCss) => Readonly<Record<string,
61
70
  /** The stylesheet Tailwind compiles for `@import "samva:brand"`: `brandThemeVariables` and the font faces. */
62
71
  declare const brandTailwindCss: (brand: BrandCss) => string;
63
72
  //#endregion
64
- export { BRAND_COLOR_VARIABLE, BRAND_FOREGROUND_VARIABLE, BrandCss, BrandFontFace, brandForegroundDefaults, brandTailwindCss, brandThemeVariables, contrastForeground, darkVariableName, parseBrandCss, printBrandCss };
65
- //# sourceMappingURL=theme-css.d.ts.map
73
+ export { ASSET_CONTENT_TYPES, AssetEntry, AssetManifest, BRAND_COLOR_VARIABLE, BRAND_COMPONENT_CLASSES, BRAND_FOREGROUND_VARIABLE, BrandComponents, BrandCss, BrandFontFace, BrandFooterProps, BrandLogoProps, EmailBrand, EmailBrandAddress, EmailBrandAssets, EmailBrandFooter, EmailBrandSocialLink, FOOTER_ATTRIBUTE, RenderedFooterMarkers, UNSUBSCRIBE_ROW_ATTRIBUTE, UNSUBSCRIBE_URL_PLACEHOLDER, assetEntry, assetUrl, brandComponents, brandForegroundDefaults, brandTailwindCss, brandThemeVariables, checkAssetBase, contrastForeground, darkVariableName, isAssetPath, isExternalAsset, parseBrandCss, printBrandCss, renderedFooterMarkers, withoutUnsubscribeRow };
74
+ //# sourceMappingURL=brand.d.ts.map
package/dist/brand.js ADDED
@@ -0,0 +1,4 @@
1
+ import { i as UNSUBSCRIBE_URL_PLACEHOLDER } from "./unsubscribe-link-CoesnQDU.js";
2
+ import { a as brandThemeVariables, c as parseBrandCss, i as brandTailwindCss, l as printBrandCss, n as BRAND_FOREGROUND_VARIABLE, o as contrastForeground, r as brandForegroundDefaults, s as darkVariableName, t as BRAND_COLOR_VARIABLE } from "./theme-css-JtRCm1fb.js";
3
+ import { a as renderedFooterMarkers, c as assetEntry, d as isAssetPath, f as isExternalAsset, i as brandComponents, l as assetUrl, n as FOOTER_ATTRIBUTE, o as withoutUnsubscribeRow, r as UNSUBSCRIBE_ROW_ATTRIBUTE, s as ASSET_CONTENT_TYPES, t as BRAND_COMPONENT_CLASSES, u as checkAssetBase } from "./brand-C0MKTwmX.js";
4
+ export { ASSET_CONTENT_TYPES, BRAND_COLOR_VARIABLE, BRAND_COMPONENT_CLASSES, BRAND_FOREGROUND_VARIABLE, FOOTER_ATTRIBUTE, UNSUBSCRIBE_ROW_ATTRIBUTE, UNSUBSCRIBE_URL_PLACEHOLDER, assetEntry, assetUrl, brandComponents, brandForegroundDefaults, brandTailwindCss, brandThemeVariables, checkAssetBase, contrastForeground, darkVariableName, isAssetPath, isExternalAsset, parseBrandCss, printBrandCss, renderedFooterMarkers, withoutUnsubscribeRow };
@@ -0,0 +1,13 @@
1
+ //#region src/channel-specs.d.ts
2
+ declare const SMS_CATEGORIES: readonly ["transactional", "promotional", "service_implicit"];
3
+ declare const WHATSAPP_CATEGORIES: readonly ["marketing", "utility", "authentication"];
4
+ declare const WHATSAPP_HEADER_TYPES: readonly ["text", "image", "video", "document"];
5
+ /** The SMS categories `<Sms category>` accepts. */
6
+ type SmsCategory = (typeof SMS_CATEGORIES)[number];
7
+ /** The Meta template categories `<WhatsApp category>` accepts. */
8
+ type WhatsAppCategory = (typeof WHATSAPP_CATEGORIES)[number];
9
+ /** The header kinds `<WhatsApp.Header type>` accepts. */
10
+ type WhatsAppHeaderType = (typeof WHATSAPP_HEADER_TYPES)[number];
11
+ //#endregion
12
+ export { WhatsAppCategory as n, WhatsAppHeaderType as r, SmsCategory as t };
13
+ //# sourceMappingURL=channel-specs-C2feJwuP.d.ts.map