@agent-native/core 0.138.0 → 0.140.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 (100) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
  3. package/corpus/templates/analytics/server/lib/emails.ts +51 -0
  4. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
  5. package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
  6. package/corpus/templates/calendar/server/lib/emails.ts +167 -0
  7. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
  8. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
  9. package/corpus/templates/clips/actions/invite-member.ts +39 -15
  10. package/corpus/templates/clips/server/lib/emails.ts +256 -0
  11. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
  12. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
  13. package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
  14. package/corpus/templates/content/server/lib/emails.ts +70 -0
  15. package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
  16. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +44 -1
  17. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +265 -1
  18. package/corpus/templates/design/actions/create-design-system.ts +9 -10
  19. package/corpus/templates/design/actions/get-design-system.ts +53 -0
  20. package/corpus/templates/design/actions/import-design-tokens.ts +5 -71
  21. package/corpus/templates/design/actions/index-design-tokens.ts +61 -80
  22. package/corpus/templates/design/actions/update-design-system.ts +7 -10
  23. package/corpus/templates/design/app/components/design/EditPanel.tsx +45 -2
  24. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +12 -0
  25. package/corpus/templates/design/app/components/design/edit-panel/design-system-swatches.ts +215 -0
  26. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +10 -6
  27. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +19 -3
  28. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +36 -10
  29. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +15 -0
  30. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +19 -1
  31. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +6 -0
  32. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +53 -10
  33. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +142 -4
  34. package/corpus/templates/design/app/components/design/types.ts +6 -0
  35. package/corpus/templates/design/app/i18n-data.ts +20 -0
  36. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +1 -1
  37. package/corpus/templates/design/app/pages/DesignSystems.tsx +78 -1
  38. package/corpus/templates/design/app/pages/design-editor/html-layer-positioning.ts +6 -1
  39. package/corpus/templates/design/changelog/2026-08-04-design-system-tokens-show-by-name-in-the-editor.md +6 -0
  40. package/corpus/templates/design/e2e/helpers.ts +17 -0
  41. package/corpus/templates/design/server/lib/design-system-tokens.ts +35 -0
  42. package/corpus/templates/design/shared/authored-color-styles.ts +401 -0
  43. package/corpus/templates/design/shared/resolve-tweaks.ts +10 -18
  44. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
  45. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
  46. package/corpus/templates/forms/server/lib/emails.ts +52 -0
  47. package/corpus/templates/forms/server/lib/response-email.ts +2 -0
  48. package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
  49. package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
  50. package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
  51. package/corpus/templates/plan/server/lib/emails.ts +67 -0
  52. package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
  53. package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
  54. package/corpus/templates/slides/server/lib/emails.ts +40 -0
  55. package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
  56. package/dist/brand-kit/index.d.ts +1 -0
  57. package/dist/brand-kit/index.js +1 -0
  58. package/dist/brand-kit/tokens.d.ts +92 -0
  59. package/dist/brand-kit/tokens.js +280 -0
  60. package/dist/brand-kit/types.d.ts +25 -0
  61. package/dist/collab/awareness.d.ts +2 -2
  62. package/dist/collab/routes.d.ts +2 -2
  63. package/dist/email-catalog/actions/list-email-activity.d.ts +6 -0
  64. package/dist/email-catalog/actions/list-email-activity.js +15 -0
  65. package/dist/email-catalog/actions/list-email-engagement.d.ts +6 -0
  66. package/dist/email-catalog/actions/list-email-engagement.js +15 -0
  67. package/dist/email-catalog/actions/list-email-log.d.ts +8 -0
  68. package/dist/email-catalog/actions/list-email-log.js +22 -0
  69. package/dist/email-catalog/actions/list-transactional-emails.d.ts +23 -0
  70. package/dist/email-catalog/actions/list-transactional-emails.js +61 -0
  71. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +10 -0
  72. package/dist/email-catalog/actions/render-transactional-email-preview.js +24 -0
  73. package/dist/email-catalog/authorize.d.ts +2 -0
  74. package/dist/email-catalog/authorize.js +10 -0
  75. package/dist/email-catalog/log.d.ts +57 -0
  76. package/dist/email-catalog/log.js +117 -0
  77. package/dist/email-catalog/provider-metrics.d.ts +34 -0
  78. package/dist/email-catalog/provider-metrics.js +165 -0
  79. package/dist/email-catalog/registry.d.ts +79 -0
  80. package/dist/email-catalog/registry.js +71 -0
  81. package/dist/email-catalog/schema.d.ts +212 -0
  82. package/dist/email-catalog/schema.js +49 -0
  83. package/dist/email-catalog/system-emails.d.ts +14 -0
  84. package/dist/email-catalog/system-emails.js +68 -0
  85. package/dist/localization/default-messages.d.ts +45 -0
  86. package/dist/localization/default-messages.js +45 -0
  87. package/dist/notifications/routes.d.ts +6 -6
  88. package/dist/org/handlers.js +8 -1
  89. package/dist/progress/routes.d.ts +1 -1
  90. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  91. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  92. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  93. package/dist/resources/handlers.d.ts +1 -1
  94. package/dist/server/action-discovery.js +22 -0
  95. package/dist/server/better-auth-instance.js +17 -2
  96. package/dist/server/email.d.ts +9 -0
  97. package/dist/server/email.js +44 -3
  98. package/dist/server/index.d.ts +1 -0
  99. package/dist/server/index.js +1 -0
  100. package/package.json +3 -1
@@ -0,0 +1,401 @@
1
+ /**
2
+ * Read the colour each property was authored with, walking the cascade. A
3
+ * generated design styles through classes, so `el.style` is empty and computed
4
+ * has already flattened `var()`. Dependency-free: bundled into the bridge.
5
+ */
6
+
7
+ const AUTHORED_COLOR_PROPERTIES = [
8
+ "color",
9
+ "backgroundColor",
10
+ "borderColor",
11
+ "outlineColor",
12
+ // Not colours themselves, but the only place a shadow's colour keeps its name.
13
+ "boxShadow",
14
+ "textShadow",
15
+ ] as const;
16
+
17
+ /**
18
+ * A shorthand holding `var()` cannot be expanded, so CSSOM serialises its
19
+ * longhands as "" — indistinguishable from unset.
20
+ */
21
+ const SHORTHAND_FALLBACK: Record<string, string> = {
22
+ backgroundColor: "background",
23
+ borderColor: "border",
24
+ outlineColor: "outline",
25
+ };
26
+
27
+ /** Colours these inherit from an ancestor when no rule targets the element. */
28
+ const INHERITED_PROPERTIES = ["color"];
29
+
30
+ /**
31
+ * Expanding a shorthand emits these for the longhands it resets, so treating
32
+ * them as authored would print "initial" in the fill row.
33
+ */
34
+ const RESET_KEYWORDS = new Set([
35
+ "initial",
36
+ "inherit",
37
+ "unset",
38
+ "revert",
39
+ "revert-layer",
40
+ ]);
41
+
42
+ function isAuthoredColor(value: unknown): value is string {
43
+ return (
44
+ typeof value === "string" &&
45
+ value !== "" &&
46
+ !RESET_KEYWORDS.has(value.trim().toLowerCase())
47
+ );
48
+ }
49
+
50
+ const LINE_STYLE_KEYWORDS = new Set([
51
+ "none",
52
+ "hidden",
53
+ "solid",
54
+ "dashed",
55
+ "dotted",
56
+ "double",
57
+ "groove",
58
+ "ridge",
59
+ "inset",
60
+ "outset",
61
+ ]);
62
+
63
+ const LINE_WIDTH = /^(thin|medium|thick|-?\d*\.?\d+[a-z%]*)$/i;
64
+
65
+ /** Split on spaces outside parentheses, so a `var()` fallback stays intact. */
66
+ function splitTopLevel(value: string): string[] {
67
+ const parts: string[] = [];
68
+ let depth = 0;
69
+ let current = "";
70
+ for (const char of value) {
71
+ if (char === "(") depth += 1;
72
+ else if (char === ")") depth -= 1;
73
+ if (depth === 0 && /\s/.test(char)) {
74
+ if (current) parts.push(current);
75
+ current = "";
76
+ continue;
77
+ }
78
+ current += char;
79
+ }
80
+ if (current) parts.push(current);
81
+ return parts;
82
+ }
83
+
84
+ /**
85
+ * The colour component of a `border`/`outline` shorthand. Width and style are
86
+ * a closed keyword set, so whatever single value is left over is the colour —
87
+ * without this, `border: 1px solid var(--x)` loses its token name.
88
+ */
89
+ function lineShorthandColor(value: string): string | null {
90
+ const rest = splitTopLevel(value).filter((part) => {
91
+ const lower = part.toLowerCase();
92
+ return !LINE_STYLE_KEYWORDS.has(lower) && !LINE_WIDTH.test(part);
93
+ });
94
+ return rest.length === 1 ? rest[0] : null;
95
+ }
96
+
97
+ /**
98
+ * A shorthand only stands in for the colour when it isolates one. Exported for
99
+ * tests: a real engine leaves the longhand empty here, which happy-dom does not
100
+ * reproduce, so the wiring alone cannot exercise this.
101
+ */
102
+ export function shorthandColorValue(
103
+ shorthand: string,
104
+ value: string,
105
+ ): string | null {
106
+ const trimmed = value.trim();
107
+ if (!trimmed) return null;
108
+ if (/url\(|gradient\(/i.test(trimmed)) return null;
109
+ // A single `var()` may contain spaces (its fallback); anything else with a
110
+ // space is a layered shorthand, not a colour.
111
+ if (/^var\(.*\)$/.test(trimmed)) return trimmed;
112
+ if (shorthand === "border" || shorthand === "outline") {
113
+ return lineShorthandColor(trimmed);
114
+ }
115
+ return /\s/.test(trimmed) ? null : trimmed;
116
+ }
117
+
118
+ type StyleRuleLike = {
119
+ selectorText?: string;
120
+ style?: Record<string, string> & {
121
+ getPropertyPriority?: (property: string) => string;
122
+ };
123
+ conditionText?: string;
124
+ cssRules?: ArrayLike<unknown>;
125
+ };
126
+
127
+ /**
128
+ * Approximate CSS specificity as one comparable number. Approximate is safe:
129
+ * it only orders selectors that already match, and `authoredMatchesPainted`
130
+ * still discards a winner that is not what paints. `:is()`/`:not()` inner
131
+ * specificity is not counted.
132
+ */
133
+ function selectorSpecificity(selector: string): number {
134
+ const cleaned = selector
135
+ .replace(/\\./g, "")
136
+ .replace(/\[[^\]]*\]/g, " :attr ")
137
+ .replace(/\(([^()]*)\)/g, " ");
138
+ const count = (pattern: RegExp) => (cleaned.match(pattern) ?? []).length;
139
+ const ids = count(/#[\w-]+/g);
140
+ const classes = count(/\.[\w-]+/g) + count(/:(?!:)[\w-]+/g);
141
+ const types = count(/(?:^|[\s>+~])[a-zA-Z][\w-]*/g) + count(/::[\w-]+/g);
142
+ return ids * 10000 + classes * 100 + types;
143
+ }
144
+
145
+ /** Specificity of the selector in a comma list that actually matches. */
146
+ function matchedSpecificity(element: Element, selectorText: string): number {
147
+ let best = -1;
148
+ for (const selector of selectorText.split(",")) {
149
+ const trimmed = selector.trim();
150
+ if (!trimmed) continue;
151
+ try {
152
+ if (!element.matches(trimmed)) continue;
153
+ } catch {
154
+ continue;
155
+ }
156
+ best = Math.max(best, selectorSpecificity(trimmed));
157
+ }
158
+ return best;
159
+ }
160
+
161
+ /** Above any selector: only importance can outrank an inline declaration. */
162
+ const INLINE_SPECIFICITY = Number.MAX_SAFE_INTEGER;
163
+
164
+ interface Candidate {
165
+ value: string;
166
+ important: boolean;
167
+ specificity: number;
168
+ order: number;
169
+ }
170
+
171
+ /** Cascade order for declarations that all match: important, then specificity. */
172
+ function beats(next: Candidate, current: Candidate | undefined): boolean {
173
+ if (!current) return true;
174
+ if (next.important !== current.important) return next.important;
175
+ if (next.specificity !== current.specificity) {
176
+ return next.specificity > current.specificity;
177
+ }
178
+ return next.order >= current.order;
179
+ }
180
+
181
+ function readRules(
182
+ element: Element,
183
+ rules: ArrayLike<unknown> | undefined,
184
+ out: Record<string, Candidate>,
185
+ counter: { order: number },
186
+ ): void {
187
+ if (!rules) return;
188
+ for (let index = 0; index < rules.length; index += 1) {
189
+ const rule = rules[index] as StyleRuleLike | undefined;
190
+ if (!rule) continue;
191
+
192
+ // Tested before `cssRules`: a style rule carries one too (CSS nesting), so
193
+ // treating anything with `cssRules` as a group skips every declaration.
194
+ const declaresOwnStyles = Boolean(rule.selectorText && rule.style);
195
+
196
+ if (!declaresOwnStyles) {
197
+ if (!rule.cssRules) continue;
198
+ // Any grouping rule, not just conditional ones: an `@layer` block holds
199
+ // cssRules with no conditionText, and Tailwind puts its utilities there.
200
+ // Layer precedence is not modelled, only membership.
201
+ if (rule.conditionText === undefined) {
202
+ readRules(element, rule.cssRules, out, counter);
203
+ continue;
204
+ }
205
+ // A conditional group counts only while it applies, so the walk matches
206
+ // what the canvas paints.
207
+ let applies = false;
208
+ try {
209
+ applies =
210
+ typeof window !== "undefined" &&
211
+ typeof window.matchMedia === "function"
212
+ ? window.matchMedia(rule.conditionText).matches
213
+ : false;
214
+ } catch {
215
+ applies = false;
216
+ }
217
+ if (applies) readRules(element, rule.cssRules, out, counter);
218
+ continue;
219
+ }
220
+
221
+ if (rule.cssRules?.length) {
222
+ readRules(element, rule.cssRules, out, counter);
223
+ }
224
+
225
+ if (!rule.selectorText || !rule.style) continue;
226
+ const specificity = matchedSpecificity(element, rule.selectorText);
227
+ if (specificity < 0) continue;
228
+ const order = counter.order++;
229
+ const style = rule.style;
230
+ const priority = (name: string) =>
231
+ style.getPropertyPriority?.(name) === "important";
232
+
233
+ const offer = (property: string, value: string, cssName: string) => {
234
+ const candidate: Candidate = {
235
+ value,
236
+ important: priority(cssName),
237
+ specificity,
238
+ order,
239
+ };
240
+ if (beats(candidate, out[property])) out[property] = candidate;
241
+ };
242
+
243
+ for (const property of AUTHORED_COLOR_PROPERTIES) {
244
+ const declared = style[property];
245
+ if (isAuthoredColor(declared)) {
246
+ offer(property, declared, cssPropertyName(property));
247
+ continue;
248
+ }
249
+ const shorthand = SHORTHAND_FALLBACK[property];
250
+ if (!shorthand) continue;
251
+ const shorthandValue = style[shorthand];
252
+ if (typeof shorthandValue !== "string") continue;
253
+ const colorOnly = shorthandColorValue(shorthand, shorthandValue);
254
+ if (colorOnly) offer(property, colorOnly, shorthand);
255
+ }
256
+ }
257
+ }
258
+
259
+ function cssPropertyName(property: string): string {
260
+ return property.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
261
+ }
262
+
263
+ /**
264
+ * Whether `authored` actually resolves to the colour the element paints.
265
+ *
266
+ * Without this an ancestor's `color: var(--color-text)` gets reported for a
267
+ * child painted green by its own utility class — a confidently wrong token name,
268
+ * which is worse than showing the hex. Resolved via a probe child so var()
269
+ * chains and shorthands are evaluated in the element's own scope.
270
+ */
271
+ export function authoredMatchesPainted(
272
+ element: Element,
273
+ property: string,
274
+ authored: string,
275
+ ): boolean {
276
+ const view = element.ownerDocument?.defaultView;
277
+ if (!view || typeof view.getComputedStyle !== "function") return true;
278
+
279
+ let painted = "";
280
+ try {
281
+ painted =
282
+ (view.getComputedStyle(element) as unknown as Record<string, string>)[
283
+ property
284
+ ] ?? "";
285
+ } catch {
286
+ return true;
287
+ }
288
+ if (!painted) return true;
289
+
290
+ const probe = element.ownerDocument!.createElement("span");
291
+ const name = cssPropertyName(property);
292
+ probe.style.setProperty("display", "none");
293
+ probe.style.setProperty(name, authored);
294
+ // An unparseable value never lands, and the probe would then just inherit the
295
+ // element's own colour and compare equal.
296
+ if (!probe.style.getPropertyValue(name)) return false;
297
+
298
+ element.appendChild(probe);
299
+ let resolved = "";
300
+ try {
301
+ resolved =
302
+ (view.getComputedStyle(probe) as unknown as Record<string, string>)[
303
+ property
304
+ ] ?? "";
305
+ } catch {
306
+ resolved = "";
307
+ }
308
+ probe.remove();
309
+
310
+ return resolved === "" || resolved === painted;
311
+ }
312
+
313
+ function collectForElement(element: Element): Record<string, string> {
314
+ const ranked: Record<string, Candidate> = {};
315
+ const counter = { order: 0 };
316
+
317
+ try {
318
+ const doc = element.ownerDocument;
319
+ // Adopted sheets are not in `styleSheets`, and a runtime CSS injector (the
320
+ // Tailwind CDN) puts every utility rule there.
321
+ const sheets = [
322
+ ...Array.from(doc?.styleSheets ?? []),
323
+ ...Array.from(
324
+ (doc as unknown as { adoptedStyleSheets?: ArrayLike<CSSStyleSheet> })
325
+ ?.adoptedStyleSheets ?? [],
326
+ ),
327
+ ];
328
+ for (const sheet of sheets) {
329
+ let rules: ArrayLike<unknown> | undefined;
330
+ try {
331
+ rules = sheet.cssRules as unknown as ArrayLike<unknown>;
332
+ } catch {
333
+ // A cross-origin sheet cannot be read; skip it.
334
+ continue;
335
+ }
336
+ readRules(element, rules, ranked, counter);
337
+ }
338
+ } catch (error) {
339
+ // Silently losing the walk downgrades every token colour to a hex.
340
+ console.warn(
341
+ "[design] could not read stylesheets for authored colours; token names will fall back to resolved values",
342
+ error,
343
+ );
344
+ }
345
+
346
+ // Inline outranks any selector, but a normal inline declaration still loses
347
+ // to an `!important` stylesheet one — so it competes rather than overwrites.
348
+ const inline = (element as HTMLElement).style;
349
+ if (inline) {
350
+ const order = counter.order++;
351
+ for (const property of AUTHORED_COLOR_PROPERTIES) {
352
+ const value = inline[property as never] as unknown as string;
353
+ if (!isAuthoredColor(value)) continue;
354
+ const candidate: Candidate = {
355
+ value,
356
+ important:
357
+ inline.getPropertyPriority?.(cssPropertyName(property)) ===
358
+ "important",
359
+ specificity: INLINE_SPECIFICITY,
360
+ order,
361
+ };
362
+ if (beats(candidate, ranked[property])) ranked[property] = candidate;
363
+ }
364
+ }
365
+
366
+ const out: Record<string, string> = {};
367
+ for (const [property, candidate] of Object.entries(ranked)) {
368
+ out[property] = candidate.value;
369
+ }
370
+
371
+ return out;
372
+ }
373
+
374
+ export function collectAuthoredColorStyles(
375
+ element: Element,
376
+ ): Record<string, string> {
377
+ const out = collectForElement(element);
378
+ for (const property of Object.keys(out)) {
379
+ if (!authoredMatchesPainted(element, property, out[property])) {
380
+ delete out[property];
381
+ }
382
+ }
383
+
384
+ // `color` is inherited: a heading with no rule of its own still has a token.
385
+ for (const property of INHERITED_PROPERTIES) {
386
+ if (out[property]) continue;
387
+ let ancestor = element.parentElement;
388
+ while (ancestor) {
389
+ const inherited = collectForElement(ancestor)[property];
390
+ if (inherited) {
391
+ if (authoredMatchesPainted(element, property, inherited)) {
392
+ out[property] = inherited;
393
+ }
394
+ break;
395
+ }
396
+ ancestor = ancestor.parentElement;
397
+ }
398
+ }
399
+
400
+ return out;
401
+ }
@@ -11,13 +11,21 @@
11
11
  * - the snapshot / coding-handoff actions inject so an external agent
12
12
  * continues from the *tuned* design, not the original generated tokens.
13
13
  *
14
- * Keep it pure and dependency-free so the UI and the server actions produce
15
- * byte-identical output.
14
+ * Keep it pure so the UI and the server actions produce byte-identical
15
+ * output. The CSS-safety predicates are re-exported from core so a Brand Kit
16
+ * token and a tweak value can never be validated by two drifting copies.
16
17
  */
17
18
 
19
+ import {
20
+ isSafeCssTokenValue,
21
+ isSafeCssVarName,
22
+ } from "@agent-native/core/brand-kit/tokens";
23
+
18
24
  import type { TweakDefinition } from "./api.js";
19
25
  import { sourceContentHash } from "./source-workspace.js";
20
26
 
27
+ export { isSafeCssTokenValue, isSafeCssVarName };
28
+
21
29
  export type TweakSelections = Record<string, string | number | boolean>;
22
30
 
23
31
  /**
@@ -49,22 +57,6 @@ export function tweakSelectionsHash(
49
57
  * emitted directly, which lets token edits persist vars that were not part of
50
58
  * the generated tweak definition list.
51
59
  */
52
- const CSS_CUSTOM_PROPERTY_NAME = /^--[-_a-zA-Z0-9]+$/;
53
-
54
- export function isSafeCssVarName(value: string): boolean {
55
- return CSS_CUSTOM_PROPERTY_NAME.test(value);
56
- }
57
-
58
- export function isSafeCssTokenValue(value: string): boolean {
59
- return (
60
- value.length > 0 &&
61
- value.length <= 300 &&
62
- !/[;{}<>]/.test(value) &&
63
- !/\/\*/.test(value) &&
64
- !/[\u0000-\u001f\u007f]/.test(value)
65
- );
66
- }
67
-
68
60
  export function resolveTweaksToCssVars(
69
61
  tweaks: TweakDefinition[],
70
62
  selections: TweakSelections,
@@ -0,0 +1,4 @@
1
+ export {
2
+ default,
3
+ meta,
4
+ } from "@agent-native/dispatch/routes/pages/transactional-email.$appId.$id";
@@ -0,0 +1,4 @@
1
+ export {
2
+ default,
3
+ meta,
4
+ } from "@agent-native/dispatch/routes/pages/transactional-email";
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Catalog entries for the transactional emails Forms sends.
3
+ *
4
+ * Registered from `server/plugins/transactional-emails.ts` so Dispatch can list
5
+ * and preview them without the app having sent anything yet.
6
+ */
7
+
8
+ import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
9
+
10
+ import { renderNewResponseEmail } from "./response-email.js";
11
+
12
+ export const FORMS_NEW_RESPONSE_EMAIL_ID = "forms.new-response";
13
+
14
+ let registered = false;
15
+
16
+ export function registerFormsEmails(): void {
17
+ if (registered) return;
18
+ registered = true;
19
+
20
+ defineTransactionalEmail({
21
+ id: FORMS_NEW_RESPONSE_EMAIL_ID,
22
+ name: "New form response",
23
+ trigger:
24
+ "A response is submitted and persisted for a form whose settings have `emailOnNewResponses` enabled. Delivery is best-effort: a send failure never rejects the submission.",
25
+ recipientLabel: "Form owner",
26
+ recipient:
27
+ "The form's `ownerEmail`, and only that address — the respondent is never emailed. Skipped when the form has no owner address.",
28
+ senderLabel: "Default sender",
29
+ sender:
30
+ "The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`, so a reply goes to the sending mailbox rather than to the respondent.",
31
+ preview: () =>
32
+ renderNewResponseEmail({
33
+ formTitle: "Beta feedback",
34
+ fields: [
35
+ { id: "name", type: "text", label: "Name", required: true },
36
+ { id: "email", type: "email", label: "Email", required: true },
37
+ {
38
+ id: "notes",
39
+ type: "textarea",
40
+ label: "What should we fix first?",
41
+ required: false,
42
+ },
43
+ ],
44
+ data: {
45
+ name: "Sam Rivera",
46
+ email: "sam.rivera@example.com",
47
+ notes: "The export button is hard to find on mobile.",
48
+ },
49
+ submittedAt: "2025-03-04T17:20:00.000Z",
50
+ }),
51
+ });
52
+ }
@@ -1,6 +1,7 @@
1
1
  import { emailStrong, renderEmail, sendEmail } from "@agent-native/core/server";
2
2
 
3
3
  import type { FormField } from "../../shared/types.js";
4
+ import { FORMS_NEW_RESPONSE_EMAIL_ID } from "./emails.js";
4
5
 
5
6
  export interface NewResponseEmailArgs {
6
7
  to: string;
@@ -61,5 +62,6 @@ export async function sendNewResponseEmail(
61
62
  await sendEmail({
62
63
  to: args.to,
63
64
  ...renderNewResponseEmail(args),
65
+ templateId: FORMS_NEW_RESPONSE_EMAIL_ID,
64
66
  });
65
67
  }
@@ -0,0 +1,5 @@
1
+ import { registerFormsEmails } from "../lib/emails.js";
2
+
3
+ export default function registerTransactionalEmails(): void {
4
+ registerFormsEmails();
5
+ }
@@ -22,6 +22,8 @@ import {
22
22
  } from "../server/lib/local-identity.js";
23
23
  import { newId, nowIso, planPath, writeEvent } from "../server/plans.js";
24
24
 
25
+ export const PLAN_ACCESS_REQUEST_EMAIL_ID = "plan.access-request";
26
+
25
27
  function httpError(message: string, statusCode: number): Error {
26
28
  return Object.assign(new Error(message), { statusCode });
27
29
  }
@@ -49,6 +51,28 @@ function absolutePlanUrl(planId: string, kind: "plan" | "recap"): string {
49
51
  }
50
52
  }
51
53
 
54
+ export function renderPlanAccessRequestEmail(input: {
55
+ requesterName: string;
56
+ requesterEmail: string;
57
+ planTitle: string;
58
+ url: string;
59
+ }) {
60
+ const subject = `${input.requesterName} requested access to "${input.planTitle}"`;
61
+ return {
62
+ subject,
63
+ ...renderEmail({
64
+ preheader: subject,
65
+ heading: "Access request",
66
+ paragraphs: [
67
+ `${emailStrong(input.requesterName)} (${emailStrong(input.requesterEmail)}) requested access to ${emailStrong(input.planTitle)}.`,
68
+ "Open the plan and use Share to grant access if this request should be approved.",
69
+ ],
70
+ cta: { label: "Open plan", url: input.url },
71
+ footer: "You received this because you own this Agent-Native Plan.",
72
+ }),
73
+ };
74
+ }
75
+
52
76
  async function notifyOwner(input: {
53
77
  planId: string;
54
78
  planKind: "plan" | "recap";
@@ -62,19 +86,16 @@ async function notifyOwner(input: {
62
86
  return false;
63
87
  }
64
88
 
65
- const url = absolutePlanUrl(input.planId, input.planKind);
66
- const subject = `${input.requesterName} requested access to "${input.planTitle}"`;
67
- const { html, text } = renderEmail({
68
- preheader: subject,
69
- heading: "Access request",
70
- paragraphs: [
71
- `${emailStrong(input.requesterName)} (${emailStrong(input.requesterEmail)}) requested access to ${emailStrong(input.planTitle)}.`,
72
- "Open the plan and use Share to grant access if this request should be approved.",
73
- ],
74
- cta: { label: "Open plan", url },
75
- footer: "You received this because you own this Agent-Native Plan.",
89
+ await sendEmail({
90
+ ...renderPlanAccessRequestEmail({
91
+ requesterName: input.requesterName,
92
+ requesterEmail: input.requesterEmail,
93
+ planTitle: input.planTitle,
94
+ url: absolutePlanUrl(input.planId, input.planKind),
95
+ }),
96
+ to: input.ownerEmail,
97
+ templateId: PLAN_ACCESS_REQUEST_EMAIL_ID,
76
98
  });
77
- await sendEmail({ to: input.ownerEmail, subject, html, text });
78
99
  return true;
79
100
  }
80
101