diff --git a/.planning/phases/35-mail-templates-and-layouts-editor/35-UI-SPEC.md b/.planning/phases/35-mail-templates-and-layouts-editor/35-UI-SPEC.md new file mode 100644 index 00000000..80c821a7 --- /dev/null +++ b/.planning/phases/35-mail-templates-and-layouts-editor/35-UI-SPEC.md @@ -0,0 +1,964 @@ +--- +phase: "35" +slug: "mail-templates-and-layouts-editor" +status: approved +reviewed_at: "2026-11-06" +shadcn_initialized: false +preset: none +created: "2026-11-06" +--- + +# Phase 35 — UI Design Contract + +> Visual and interaction contract for frontend phases. Generated by msd-ui-researcher, verified by msd-ui-checker. + +Most of this phase is **YAML-declared cabana screens** rendered by the existing generic views in `admin/` (`ListView.vue`, `FormView.vue`, `SettingsFormView.vue`, `SettingsIndexView.vue`). This is the same approach as 19.6.1 (Administrators/Roles). The contract fixes six things: + +1. **What the YAML declares** for the five Mail screens: columns, filters, form tabs and field order, record actions, settings entries. +2. **Four new SPA field controls**: + - `codeeditor` (D-09, a published type); + - `colorpicker` (Branding colours, Winter parity); + - `mailimage` (Branding logo, picked from the library); + - `mailpreview` (D-16 live preview, D-17 test send). + + Plus one shared dialog, `MailImagePicker` (D-20), opened from the markdown body, the code editors and `mailimage`. +3. **Narrow additions to existing SPA code**: + - FormView renders record actions in its footer and hides Delete when the record says so; + - partial fields reload after a save or a record action; + - SettingsFormView provides the form values; + - RowStateBadges gets two more states; + - MarkdownField gets the image-insert button; + - main.css gets a `.summer-diff` kit. +4. **All copy** (en + pl) for settings entries, permissions, lists, forms, preview, test send, picker, Branding and every refusal (D-03, D-06, D-07, D-08, D-13, D-17). +5. **Where each refusal or failure shows up** in the existing UI (field line, banner, toast, inline status). +6. **The code-editor library**: CodeMirror 6. It needs a `.planning/research/STACK.md` row and a decision note (project dependency rule). + +The SPA already has a complete design system, Direction C v2 (`.planning/phases/10-admin-vue-spa/design/README.md`). It is implemented as Tailwind 4 `@theme` tokens in `admin/src/styles/main.css` on Reka UI primitives. **This contract adds no colour tokens, fonts, radii or shadows.** The new CSS is the `.summer-diff` component kit (existing tokens only) and the CodeMirror theme (existing CSS variables only). Class strings below are copied from existing components unless marked **new**. + +Sources: +- 35-CONTEXT.md D-01..D-21 (locked); +- the approved 19.6.1 UI-SPEC (house conventions: phrasebook keys, CLDR plurals, refusal shapes, accent reserve); +- WinterCMS `modules/system/models/{mailtemplate,maillayout,mailpartial,mailbrandsetting}/{fields,columns}.yaml`, `MailBrandSetting.php` defaults and `lang/{en,pl}/lang.php` (`mail_templates.*`, `mail_brand.*`), for field sets and wording; +- a codebase scan (listed per section). + +--- + +## Design System + +| Property | Value | +|----------|-------| +| Tool | none. There is no shadcn: this is a Vue 3 SPA with its own Direction C system, so the shadcn gate does not apply (the same ruling as 10.1, 12.1, 12.2, 16.3, 19.6.1) | +| Preset | not applicable | +| Component library | Reka UI 2.9.10 (headless primitives) plus the local components in `admin/src/components/` | +| Icon library | `@lucide/vue` 1.17.0, named imports only. Settings tiles use registry names already in `admin/src/app/icons.ts`: `mail`, `layout-grid`, `puzzle`, `palette`, `image`. New controls import `ImagePlus`, `Send`, `TriangleAlert`, `Info`, `Search`, `Upload`, `Check` and `X` directly, at 14px for inline notes and 16px for toolbar buttons | +| Font | DM Sans 400/600 (700 only in inherited headings), self-hosted `@fontsource/dm-sans`. **DM Mono 400** (`font-mono`, already self-hosted) for code: the code editor, plain-text preview, diff, hex values and code snippets in info partials | +| Code editor | **CodeMirror 6**, lazily loaded in its own chunk like the TipTap richeditor. Packages and pins are in Registry Safety. It needs a STACK.md "Admin SPA (npm) additions" row and a decision note `.planning/notes/2026-11-xx-admin-code-editor-codemirror.md`, written before the first plan that installs it | + +**Why CodeMirror 6.** +- It is MIT and modular: state, view, language and the html/css grammars are separate packages, so the bundle carries only what the field uses. +- It needs no web workers, so the admin CSP is unchanged. +- It has keyboard and screen-reader support (a contenteditable with an ARIA textbox), and it lazy-loads cleanly behind a skeleton. + +Rejected alternatives: +- **Monaco:** multi-MB, needs workers and `worker-src`, desktop-only feel. +- **Ace** (Winter's choice): older API, weaker accessibility. +- **A textarea with a highlight overlay:** fragile caret and scroll sync, no bracket matching. +- **TipTap source mode:** it is a rich-text tool, not a code editor. + +--- + +## Component Inventory + +Enumerated by `node --input-type=module -e "import * as r from 'reka-ui'; console.log(Object.keys(r).length)"` (run in `admin/`) — 478 exports — reka-ui@2.9.10 — 2026-11-06. + +Local components were enumerated by `find admin/src/components -name '*.vue' | wc -l`: 63 components in summercms-admin@unversioned (a private workspace package with no `version` field in `admin/package.json`, pinned by repo commit 928ae00), on 2026-11-06. + +Icons were verified on 2026-11-06 by running `node --input-type=module -e "import * as l from '@lucide/vue'; for (const n of ['Mail','Puzzle','Palette','Image','ImagePlus','Send','TriangleAlert','Info','Search','Upload','Check','X','Code']) console.log(n, n in l)"` in `admin/` against @lucide/vue@1.17.0. All are exported. The tile names `mail`, `layout-grid`, `puzzle`, `palette` and `image` are already mapped in `admin/src/app/icons.ts`, so no registry change is needed. + +This table is a **non-exhaustive** list of known-good components for this phase, not a closed allowlist. Checking for another Reka export or local component is the expected path. + +| Component | Import path | Notes | +|-----------|-------------|-------| +| ListView, DataTable, FilterBar | `admin/src/views/ListView.vue`, `admin/src/components/list/*` | The Templates, Layouts and Partials lists. No change except the two row-state labels (C9) | +| RowStateBadges | `admin/src/components/list/RowStateBadges.vue` | Gains the `customized` and `outdated` states (C9) | +| FormView | `admin/src/views/FormView.vue` | The Template, Layout, Partial and Mail images (singleton) forms. Gains footer record actions, `meta.deletable` and the partial reload (C8) | +| SettingsFormView | `admin/src/views/SettingsFormView.vue` | The Branding page. Gains `provide(FORM_VALUES)` and `provide(FORM_LOCALE)` so `mailpreview` reads the draft (C8) | +| SettingsIndexView | `admin/src/views/SettingsIndexView.vue` | Five tiles in the new "Mail" category. No change | +| FormTabs | `admin/src/components/form/FormTabs.vue` | Form tabs, and its class strings for the preview's HTML/Plain text switch | +| FormGrid, FormField, FieldRenderer, `registry.ts` | `admin/src/components/form/*` | Register `codeeditor`, `colorpicker`, `mailimage`, `mailpreview`. `mailpreview` is valueless and group-labelled; `mailimage` is group-labelled | +| MarkdownField | `admin/src/components/form/fields/MarkdownField.vue` | Template body. Gains the "Insert image" button. Its own Preview toggle is hidden when the image option is on (C3) | +| RichEditorField (patterns only) | `admin/src/components/form/fields/RichEditorField.vue` | Lazy-load skeleton/failed pattern, toolbar class strings and the `font-mono text-[13px]` source textarea, copied for `codeeditor` | +| FileuploadField + FileCaptionModal | `admin/src/components/form/fields/FileuploadField.vue` | Mail images library (`mode: image`, attachMany tile grid, caption = alt text). No change | +| PartialField + PartialHost + `.summer-callout` kit | `admin/src/components/form/fields/PartialField.vue`, `main.css` | Info/drift partial and File version partial (server-rendered html/template) | +| RecordActions | `admin/src/components/form/RecordActions.vue` | "Reset to file version", now also in the FormView footer. No change to the component | +| RelationPickerModal (patterns only) | `admin/src/components/relation/RelationPickerModal.vue` | Dialog shell, search input and chosen-state class strings, copied for `MailImagePicker` | +| ConfirmDialog + `useConfirm` | `admin/src/components/ui/ConfirmDialog.vue`, `confirm.ts` | Reset, delete-layout and leave-with-changes confirmations | +| Button | `admin/src/components/ui/Button.vue` | `outline` / `ghost` / `danger` / `primary`, `size: sm` for in-field buttons | +| `showToast` | `admin/src/state/useToasts.ts` | Save, reset, test-send results and delete refusals | +| FormErrorBanner | `admin/src/components/form/FormErrorBanner.vue` | 422 count banner and 403 banner. No change | +| `FORM_VALUES`, `FORM_PATCH`, `FORM_SESSION` | `admin/src/components/form/formContext.ts` | `mailpreview` reads the draft; `mailimage` writes its value through the normal `update:modelValue` | + +New `.vue` files: +- `admin/src/components/form/fields/CodeEditorField.vue`; +- `admin/src/components/form/fields/ColorPickerField.vue`; +- `admin/src/components/form/fields/MailImageField.vue`; +- `admin/src/components/form/fields/MailPreviewField.vue`; +- `admin/src/components/mail/MailImagePicker.vue`. + +New `.ts` files: +- `admin/src/components/form/codeeditor/editor.ts` (CodeMirror setup, the lazy import target); +- `admin/src/components/mail/insertImage.ts` (the pure snippet builder, C4). + +--- + +## Spacing Scale + +Declared values for new markup in this phase (all multiples of 4): + +| Token | Value | Tailwind | Usage | +|-------|-------|----------|-------| +| xs | 4px | `gap-1`, `p-1` | Editor and preview toolbar padding and button gap (copied from the richeditor toolbar) | +| sm | 8px | `gap-2` | Icon-to-text gaps, colour swatch to hex input, picker tile grid gap, test-send row gap | +| md | 16px | `px-4`, `gap-4` | Preview subject row horizontal padding, picker dialog stack (copied from RelationPickerModal `gap-4`), `.summer-diff` horizontal padding | +| lg | 24px | `gap-x-6`, `p-6` | Form grid column gap and dialog padding (existing, unchanged) | +| xl | 32px | `px-8` | Sticky footer padding (existing, unchanged) | +| 2xl | 48px | — | not used | +| 3xl | 64px | — | not used | + +Fixed component heights (multiples of 16): +- **Preview frame:** 640px (`h-[640px]`). +- **Code editor by `size`:** `tiny` 128px, `small` 192px, `large` 320px (also the default when no size is set), `huge` 480px, `giant` 640px. +- **Plain-text preview and File version blocks:** at most 480px (`max-h-[480px]`), scrolling inside. + +Exceptions, all inherited from Direction C. They were approved as locked in 16.3 and re-used in 19.6.1, and none is new: +- label-to-control gap 6px (`gap-1.5` in `FormField`); +- form row gap 22px (`gap-y-[22px]` in `FormGrid`); +- card padding 28px (`p-7`); +- page stack 20px (`gap-5`); +- footer button gap 10px (`gap-2.5`); +- callout padding 14×18 (`.summer-callout`); +- 12px vertical padding (`py-3`), used by the preview subject row and `.summer-diff`, as in the permission rows; +- control height 44px (`h-input`), used by the colour swatch and the picker search; +- toolbar button height 32px (`h-8`, richeditor toolbar); +- tab pill height 34px (`h-[34px]`, FormTabs); +- input content padding 14×10 (`px-3.5 py-2.5`, copied from the inputs), used by the C1 CodeMirror content and the read-only `pre`. + +--- + +## Typography + +New markup uses exactly these styles. Everything else is rendered by existing components unchanged. + +| Role | Size | Weight | Line Height | Used by | +|------|------|--------|-------------|---------| +| Body | 14px | 400 | 1.5 | Field values, preview subject value, picker alt input, callout text, info partial text, toasts | +| Label | 14px | 600 | 1.5 | Field labels, tab pills, button text, callout titles, info partial `strong` lines, preview "Subject" label | +| Meta | 13px | 400 | 1.5 | Comments, error lines, preview status/notes, test-send hint, picker tile filenames, toolbar text buttons ("Insert image") | +| Code (Meta size, mono) | 13px DM Mono | 400 | 1.5 | **New:** CodeMirror content and gutter, plain-text preview `pre`, `.summer-diff`, hex text in `colorpicker`, `code` in info partials | +| Dialog heading (inherited) | 20px | 700 | 1.2 (`tracking-[-0.01em]`) | `MailImagePicker` title, copied from RelationPickerModal | +| Page heading (inherited) | 24px | 700 | 1.2 (`tracking-[-0.02em]`) | Record title in `FormView`/`SettingsFormView` header, unchanged | + +Weights: new content uses 400 and 600 only. 700 appears only in the two inherited headings (the locked system-level exception from 16.3 and 19.6.1). Sizes in new markup are 13 and 14; 20 and 24 are inherited. No 12px text is added; the 12px row-state badge is reused unchanged. The 12px badge, the 20/24px headings and weight 700 are inherited exceptions from 16.3 and 19.6.1: the effective scale for new markup in this phase is 13/14px at 400/600, and the planner must not use 12, 20 or 24 in new markup. + +--- + +## Color + +All admin UI colours are existing CSS variables (light / dark from `main.css`). Hard-coded hex values are forbidden in new admin markup, with two exceptions: + +1. the chosen-tile border `border-[#e0b020]`, copied verbatim from `RelationPickerModal`; +2. the preview canvas, explained below. + +**Mail content colours are not UI tokens.** The Branding defaults below (Winter's `MailBrandSetting` values) are data that the server compiles into mail CSS; they never style the admin. + +| Role | Value (light / dark) | Usage | +|------|----------------------|-------| +| Dominant (60%) | `bg` #f4f6f9 / #111726, `surface` #ffffff / #182033 | Page, form card, list table, code editor content area, picker dialog | +| Secondary (30%) | `subtle` #f3f5f8 / #1f283d, `border` #e6e9ef / #29334b, `border-strong` #d2d8e2 / #3a4661, `muted` #566175 / #a9b3c6 | Editor and preview toolbars, the CodeMirror gutter, plain-text preview and `.summer-diff` background, template-action highlight background, the `customized` badge outline, comments, notes and `Info` icons | +| Accent (10%) | `primary` #22304d / #fcd34d; `sel` #fdf3cf / #3a3622; `ring` | See the reserved list | +| Destructive | `danger` #c62828 / #f58a8a, `danger-soft` #fdf0f0 / #3a1d24 | The "Reset to file version" confirm button, the custom-layout Delete (danger outline) and its confirm, field error lines, the preview/test-send error line, `.summer-diff__del` lines, danger toasts | +| Semantic OK | `ok-bg` / `ok-text` | `.summer-diff__add` lines only. Code-editor strings and attribute values use `ok-text` (syntax colour, see below) | +| Preview canvas | `#ffffff` in both themes | The `mailpreview` iframe element background (`bg-white`). Mail clients render on white; without this, dark mode shows mail with a transparent body on the dark surface. It is not an admin UI colour | + +**Code editor syntax colours** (CodeMirror `HighlightStyle` and `EditorView.theme`, all through `var(--c-*)`, so light and dark mode swap): + +| Token | Variable | +|-------|----------| +| tag names, CSS properties, keywords | `--c-primary` | +| attribute values, strings | `--c-ok-text` | +| attribute names, CSS selectors, everything else | `--c-text` | +| comments, punctuation, brackets | `--c-muted` | +| Go template actions `{{ … }}` (a `MatchDecorator`, class `cm-summer-action`) | `--c-primary` on `--c-subtle`, radius 4px | +| gutter | `--c-subtle` background, `--c-muted` numbers | +| active line | `--c-hover` | +| selection | `--c-sel` | +| cursor | `--c-text` | + +Weights stay 400; there is no italic. + +Accent reserved for (exhaustive for this phase): +1. `primary` fill on the single primary button per screen: + - footer **Save** on every form and the Branding page (existing); + - **New layout** in the Layouts list toolbar; + - **Insert image** / **Use this image** in the picker dialog. + + "Send test to me" and "Reset to file version" are not primary. +2. `primary` text: tag/property/keyword tokens and Go template actions in the code editor (syntax, above). +3. `sel` tint: + - the `outdated` row-state badge ("File changed"); + - the drift callout (`.summer-callout--warning`); + - the chosen picker tile (with the copied `#e0b020` border); + - code-editor selection. +4. `ring` (3px): focus-visible on every focusable element, including the editor wrapper (`:focus-within`), colour swatches and picker tiles. Never removed. + +Accent is never used for: record action buttons (outline), the test-send button (outline), the `customized` badge (neutral outline), comments, links, settings tiles, or the preview toolbar. + +--- + +## Copywriting Contract + +All copy lives in `modules/phrasebook/backend/lang/{en,pl}/lang.yaml` (`backend::lang.*`). + +New groups: +- `mail:`, with nested `template:`, `layout:`, `partial:`, `brand:`, `images:`, `preview:`, `picker:` (names follow Winter's `mail_templates.*` / `mail_brand.*` meaning); +- `codeeditor:`, `colorpicker:`, `mailimage:` for the generic field controls. + +Additions to existing groups: +- `settings.category_mail`; +- `permissions.manage_mail_templates[_comment]`, `permissions.manage_mail_layouts[_comment]`; +- `list.*` is not touched. + +Conventions: +- Placeholders use `:name` / `:count` / `:email` / `:seconds` / `:message` / `:query`. +- Plural texts are CLDR maps (`one`/`other` in en; `one`/`few`/`many`/`other` in pl), never pipe strings. +- Polish is the design language; English values carry the same meaning. +- English spells "colour". +- Existing keys are reused unchanged and are not restated: `form.save`, `form.saving`, `form.save_and_close`, `form.cancel`, `form.delete`, `messages.form.saved`, `messages.form.deleted`, `form.load_failed`, `list.load_failed`, `messages.list.empty_search`, `list.empty_value`, the validation messages (`required`, `max.string`), `fileupload.*` and `form.tab_errors`. + +Literal `{{ .Name }}` / `{{ .Content }}` / `{{ .logoUrl }}` in copy are shown as text (the SPA renders phrases as text). In YAML they must be quoted so the phrasebook does not treat them as placeholders. + +### Primary CTAs and key states + +| Element | Key | EN | PL | +|---------|-----|----|----| +| Templates list title | `mail.template.list_title` | Mail templates | Szablony wiadomości | +| Templates: record name (`config_form.yaml` `name`) / form subtitle | `mail.template.name` | Mail template | Szablon wiadomości | +| Templates: primary CTA | — | none in the list (templates come from plugin files; no create, no delete); the form's primary is the existing **Save** | — | +| Layouts list title | `mail.layout.list_title` | Mail layouts | Układy wiadomości | +| Layouts: record name / form subtitle | `mail.layout.name` | Mail layout | Układ wiadomości | +| Layouts: primary CTA (toolbar create; create-form title) | `mail.layout.new` | New layout | Nowy układ | +| Partials list title | `mail.partial.list_title` | Mail partials | Fragmenty wiadomości | +| Partials: record name / form subtitle | `mail.partial.name` | Mail partial | Fragment wiadomości | +| Partials: primary CTA | — | none in the list (partials come from files); the form's primary is **Save** | — | +| Branding page title (settings entry label) | `mail.brand.menu_label` | Mail branding | Wygląd wiadomości | +| Mail images page title (singleton `name`) | `mail.images.menu_label` | Mail images | Obrazy w wiadomościach | +| Empty state, Templates (`noRecordsMessage`) | `mail.template.list_empty` | No mail templates in this view. Clear the search and filters. | Brak szablonów wiadomości w tym widoku. Wyczyść wyszukiwanie i filtry. | +| Empty state, Layouts | `mail.layout.list_empty` | No layouts in this view. Clear the search, or create a layout. | Brak układów w tym widoku. Wyczyść wyszukiwanie lub utwórz układ. | +| Empty state, Partials | `mail.partial.list_empty` | No partials in this view. Clear the search. | Brak fragmentów w tym widoku. Wyczyść wyszukiwanie. | +| Error state, list or form load failed | existing `list.load_failed`, `form.load_failed` | unchanged | unchanged | +| Saved toast | existing `messages.form.saved` | unchanged | unchanged | + +### Settings entries and permission catalog (D-02, D-03) + +| Element | Key | EN | PL | +|---------|-----|----|----| +| Settings category | `settings.category_mail` | Mail | Poczta e-mail | +| Templates tile label / description | `mail.template.menu_label` / `mail.template.menu_description` | Mail templates / Edit the subject and content of the mail the application sends. | Szablony wiadomości / Edytuj temat i treść wiadomości wysyłanych przez aplikację. | +| Layouts tile label / description | `mail.layout.menu_label` / `mail.layout.menu_description` | Mail layouts / Edit the layouts that wrap every message, or create your own. | Układy wiadomości / Edytuj układy otaczające każdą wiadomość lub utwórz własne. | +| Partials tile label / description | `mail.partial.menu_label` / `mail.partial.menu_description` | Mail partials / Edit the reusable blocks used in mail, such as buttons and panels. | Fragmenty wiadomości / Edytuj bloki wielokrotnego użytku w wiadomościach, np. przyciski i panele. | +| Branding tile label / description | `mail.brand.menu_label` / `mail.brand.menu_description` | Mail branding / Set the colours and logo used by every message. | Wygląd wiadomości / Ustaw kolory i logo używane we wszystkich wiadomościach. | +| Images tile label / description | `mail.images.menu_label` / `mail.images.menu_description` | Mail images / Upload images to use in mail templates, layouts and partials. | Obrazy w wiadomościach / Prześlij obrazy do użycia w szablonach, układach i fragmentach wiadomości. | +| `system.manage_mail_templates` label | `permissions.manage_mail_templates` | Manage mail templates | Zarządzanie szablonami wiadomości | +| … comment | `permissions.manage_mail_templates_comment` | Edit mail templates, preview them and send test messages to yourself. | Edycja szablonów wiadomości, ich podgląd i wysyłanie wiadomości testowych do siebie. | +| `system.manage_mail_layouts` label | `permissions.manage_mail_layouts` | Manage mail layouts and branding | Zarządzanie układami i wyglądem wiadomości | +| … comment | `permissions.manage_mail_layouts_comment` | Edit mail layouts, partials, branding and the mail image library. These change every message the application sends. | Edycja układów, fragmentów i wyglądu wiadomości oraz biblioteki obrazów. Zmiany dotyczą każdej wiadomości wysyłanej przez aplikację. | + +Both codes go in the existing permission tab `permissions.tab_system` ("System"); there is no new tab. This follows Winter, which lists mail permissions under System. + +### Lists (C3b, C5, C6, C9) + +| Element | Key | EN | PL | +|---------|-----|----|----| +| Column / search: code | `mail.code` | Code | Kod | +| Column: subject | `mail.template.subject` | Subject | Temat | +| Column: description | `mail.template.description` | Description | Opis | +| Column / filter: locale | `mail.template.locale` | Locale | Język | +| Locale value for a file without a suffix | `mail.template.locale_default` | Default | Domyślny | +| Column: layout | `mail.template.layout` | Layout | Układ | +| Column (invisible): customized at | `mail.customized_at` | Customized | Dostosowano | +| Column: name (layouts, partials) | `mail.name_field` | Name | Nazwa | +| Column: source (layouts, partials) | `mail.source` | Source | Źródło | +| Source value of a database-only layout | `mail.source_custom` | Custom | Własny | +| Filter: status (scope) | `mail.filter_status` | Status | Status | +| Filter option | `mail.filter_customized` | Customized | Dostosowane | +| Filter option | `mail.filter_not_customized` | Not customized | Niedostosowane | +| Filter option | `mail.filter_outdated` | File changed | Plik zmieniony | +| Row state `customized` (`messages.rowStateCustomized`) | `mail.state_customized` | Customized | Dostosowany | +| Row state `outdated` (`messages.rowStateOutdated`) | `mail.state_outdated` | File changed | Plik zmieniony | + +The source value of a file-backed record is the owning plugin's ID as text (for example `golem15.user`). It is not translated. + +### Template form (C3b) + +| Element | Key | EN | PL | +|---------|-----|----|----| +| Tab 1 | `mail.template.tab_template` | Template | Szablon | +| Tab 2 (update only) | `mail.tab_file_version` | File version | Wersja z pliku | +| Subject label / comment | `mail.template.subject` / `mail.template.subject_comment` | Subject / One line. Variables such as {{ .Name }} work here too. | Temat / Jeden wiersz. Zmienne, np. {{ .Name }}, działają także tutaj. | +| Layout label / comment | `mail.template.layout` / `mail.template.layout_comment` | Layout / The layout wraps the message. Custom layouts are listed too. | Układ / Układ otacza wiadomość. Na liście są też układy własne. | +| Body label / comment | `mail.template.body` / `mail.template.body_comment` | Message / Markdown with variables such as {{ .Name }}. The plain-text version is made from it automatically. | Treść / Markdown ze zmiennymi, np. {{ .Name }}. Wersja tekstowa powstaje z niej automatycznie. | +| Preview label | `mail.preview.label` | Preview | Podgląd | + +### Layout form (C5) + +| Element | Key | EN | PL | +|---------|-----|----|----| +| Tabs | `mail.layout.tab_html` / `mail.layout.tab_text` / `mail.layout.tab_css` / `mail.tab_file_version` | HTML / Plain text / CSS / File version | HTML / Zwykły tekst / CSS / Wersja z pliku | +| Name label | `mail.name_field` | Name | Nazwa | +| Code label / comment (create only) | `mail.code` / `mail.layout.code_comment` | Code / Templates refer to the layout by this code. It cannot be changed later. | Kod / Szablony odwołują się do układu przez ten kod. Później nie można go zmienić. | +| HTML wrapper label / comment | `mail.layout.html` / `mail.layout.html_comment` | HTML layout / Wraps the HTML message. Put {{ .Content }} where the message goes. | Układ HTML / Otacza wiadomość HTML. Wstaw {{ .Content }} w miejscu treści. | +| Text wrapper label / comment | `mail.layout.text` / `mail.layout.text_comment` | Plain-text layout / Wraps the plain-text message. Put {{ .Content }} where the message goes. | Układ tekstowy / Otacza wiadomość tekstową. Wstaw {{ .Content }} w miejscu treści. | +| CSS label / comment | `mail.layout.css` / `mail.layout.css_comment` | CSS / Styles for this layout. They are inlined into the HTML message after the branding styles. | CSS / Style tego układu. Są wstawiane do wiadomości HTML po stylach wyglądu. | +| Delete confirm (custom layouts only, danger) | `mail.layout.delete_confirm` | Delete this layout? This cannot be undone. A layout that customized templates still use cannot be deleted. | Usunąć ten układ? Tej operacji nie można cofnąć. Nie można usunąć układu, którego używają dostosowane szablony. | + +### Partial form (C6) + +| Element | Key | EN | PL | +|---------|-----|----|----| +| Tabs | `mail.partial.tab_html` / `mail.partial.tab_text` / `mail.tab_file_version` | HTML / Plain text / File version | HTML / Zwykły tekst / Wersja z pliku | +| HTML label / comment | `mail.partial.html` / `mail.partial.html_comment` | HTML / Used in the HTML part of messages. | HTML / Używany w części HTML wiadomości. | +| Text label / comment | `mail.partial.text` / `mail.partial.text_comment` | Plain text / Used in the plain-text part of messages. | Zwykły tekst / Używany w części tekstowej wiadomości. | + +### Info partial and drift notice (C7, D-06, D-07) + +| Element | Key | EN | PL | +|---------|-----|----|----| +| Line: description (templates) | `mail.info_description` | Description: :text | Opis: :text | +| Line: file | `mail.info_file` | File: :path in :plugin | Plik: :path we wtyczce :plugin | +| Status: not customized | `mail.info_not_customized` | Not customized. The form shows the file version; saving creates your version. | Nie dostosowano. Formularz pokazuje wersję z pliku; zapisanie utworzy Twoją wersję. | +| Status: customized | `mail.info_customized` | Customized. Your version is sent instead of the file. | Dostosowano. Zamiast pliku wysyłana jest Twoja wersja. | +| Status: custom layout | `mail.info_custom_layout` | Custom layout. It exists only in the database and can be deleted. | Układ własny. Istnieje tylko w bazie danych i można go usunąć. | +| Variables line (samples declared) | `mail.info_variables` | Variables with sample values: | Zmienne z przykładowymi wartościami: | +| Variables line (no samples) | `mail.info_no_samples` | This file declares no sample values. Variables show as [name] in the preview. | Ten plik nie deklaruje przykładowych wartości. W podglądzie zmienne są pokazane jako [nazwa]. | +| Partial usage line | `mail.info_partial_usage` | Use it in a template or layout with: | Użyj go w szablonie lub układzie tak: | +| Drift callout title | `mail.drift_title` | The file changed after this was customized | Plik zmienił się po dostosowaniu | +| Drift callout text | `mail.drift_text` | A plugin update changed the file. Your version is still sent. Compare both on the File version tab, then save your version again to mark the change as reviewed, or reset to the file version. | Aktualizacja wtyczki zmieniła plik. Nadal wysyłana jest Twoja wersja. Porównaj obie w zakładce Wersja z pliku, a potem zapisz swoją wersję ponownie, aby oznaczyć zmianę jako sprawdzoną, albo przywróć wersję z pliku. | +| File version: changes heading | `mail.file_changes` | Changes from the file | Zmiany względem pliku | +| File version: legend | `mail.file_legend` | Lines starting with + are only in your version. Lines starting with − are only in the file. | Wiersze zaczynające się od + są tylko w Twojej wersji. Wiersze zaczynające się od − są tylko w pliku. | +| File version: content heading | `mail.file_content` | File version | Wersja z pliku | +| File version: identical | `mail.file_identical` | Your version matches the file. | Twoja wersja jest zgodna z plikiem. | +| File version: not customized | `mail.file_not_customized` | Not customized: the form already shows this version. | Nie dostosowano: formularz pokazuje już tę wersję. | + +### Record actions and destructive confirmations (D-06, D-08) + +| Element | Key | EN | PL | +|---------|-----|----|----| +| Record action: Reset | `mail.reset` | Reset to file version | Przywróć wersję z pliku | +| Reset confirm, template (danger) | `mail.template.reset_confirm` | Reset this template to the file version? Your subject, message and layout choice are deleted, and unsaved edits on this page are discarded. This cannot be undone. | Przywrócić wersję szablonu z pliku? Twój temat, treść i wybór układu zostaną usunięte, a niezapisane zmiany na tej stronie odrzucone. Tej operacji nie można cofnąć. | +| Reset confirm, layout (danger) | `mail.layout.reset_confirm` | Reset this layout to the file version? Your HTML, plain-text and CSS changes are deleted, and unsaved edits on this page are discarded. This cannot be undone. | Przywrócić wersję układu z pliku? Twoje zmiany HTML, tekstu i CSS zostaną usunięte, a niezapisane zmiany na tej stronie odrzucone. Tej operacji nie można cofnąć. | +| Reset confirm, partial (danger) | `mail.partial.reset_confirm` | Reset this partial to the file version? Your HTML and plain-text changes are deleted, and unsaved edits on this page are discarded. This cannot be undone. | Przywrócić wersję fragmentu z pliku? Twoje zmiany HTML i tekstu zostaną usunięte, a niezapisane zmiany na tej stronie odrzucone. Tej operacji nie można cofnąć. | +| Reset success toast | `mail.reset_done` | Reset to the file version. | Przywrócono wersję z pliku. | +| Delete refused: layout in use (403 `Message`, toast) | `mail.layout.delete_in_use` | one: "The layout “:name” is used by :count customized template. Choose another layout for it first." other: "The layout “:name” is used by :count customized templates. Choose another layout for them first." | one: "Układ „:name” jest używany przez :count dostosowany szablon. Najpierw wybierz dla niego inny układ." few: "Układ „:name” jest używany przez :count dostosowane szablony. Najpierw wybierz dla nich inny układ." many: "Układ „:name” jest używany przez :count dostosowanych szablonów. Najpierw wybierz dla nich inny układ." other: "Układ „:name” jest używany przez :count dostosowanego szablonu. Najpierw wybierz dla nich inny układ." | + +### Preview and test send (C10, D-15, D-16, D-17) + +| Element | Key | EN | PL | +|---------|-----|----|----| +| Format switch | `mail.preview.html` / `mail.preview.text` | HTML / Plain text | HTML / Zwykły tekst | +| Subject row label | `mail.preview.subject` | Subject | Temat | +| iframe title | `mail.preview.frame_title` | Preview of the HTML message | Podgląd wiadomości HTML | +| Rendering status | `mail.preview.rendering` | Rendering preview… | Renderowanie podglądu… | +| Empty draft | `mail.preview.empty` | Nothing to preview yet. Write the message first. | Nie ma jeszcze czego podglądać. Najpierw napisz treść wiadomości. | +| Render error (keeps the last good render) | `mail.preview.failed` | The draft cannot be rendered: :message The preview shows the last version that rendered. | Nie można wyrenderować wersji roboczej: :message Podgląd pokazuje ostatnią wersję, która się wyrenderowała. | +| Render error, nothing rendered yet | `mail.preview.failed_first` | The draft cannot be rendered: :message | Nie można wyrenderować wersji roboczej: :message | +| Preview unavailable (network/5xx) | `mail.preview.unavailable` | Preview unavailable. It updates again with your next edit. | Podgląd niedostępny. Odświeży się przy następnej zmianie. | +| Sample note (templates) | `mail.preview.sample_note` | Variables are filled with sample values. A variable without a sample shows as [name]. | Zmienne są wypełnione przykładowymi wartościami. Zmienna bez przykładu jest pokazana jako [nazwa]. | +| Sample note (layouts, partials, branding) | `mail.preview.sample_content_note` | Previewed with sample content. | Podgląd z przykładową treścią. | +| Test-send button | `mail.preview.test_send` | Send test to me | Wyślij test do mnie | +| Test-send busy | `mail.preview.test_sending` | Sending… | Wysyłanie… | +| Test-send hint | `mail.preview.test_hint` | Sends the current draft, unsaved changes included, to :email. | Wysyła bieżącą wersję roboczą, razem z niezapisanymi zmianami, na adres :email. | +| Test-send success toast | `mail.preview.test_sent` | Test message sent to :email. | Wysłano wiadomość testową na adres :email. | +| Test-send rate limited (429) | `mail.preview.test_limited` | You have sent too many test messages. Try again in :seconds s. | Wysłano zbyt wiele wiadomości testowych. Spróbuj ponownie za :seconds s. | +| Test-send refused (422, draft invalid) | `mail.preview.test_invalid` | Fix the draft before sending a test: :message | Popraw wersję roboczą przed wysłaniem testu: :message | +| Test-send driver failure (5xx) | `mail.preview.test_failed` | The test message could not be sent. Check the mail settings and the application log. | Nie udało się wysłać wiadomości testowej. Sprawdź ustawienia poczty i log aplikacji. | + +### Image picker and image fields (C4, C11, D-18, D-20) + +| Element | Key | EN | PL | +|---------|-----|----|----| +| Editor toolbar button | `mail.picker.insert_button` | Insert image | Wstaw obraz | +| Dialog title, insert mode | `mail.picker.insert_title` | Insert an image from the library | Wstaw obraz z biblioteki | +| Dialog title, choose mode (`mailimage`) | `mail.picker.choose_title` | Choose an image | Wybierz obraz | +| Search label / placeholder | `mail.picker.search` | Search images | Szukaj obrazów | +| Alt text label / comment | `mail.picker.alt` / `mail.picker.alt_comment` | Alternative text / Describes the image for people who cannot see it. Leave it empty for a decorative image. | Tekst alternatywny / Opisuje obraz osobom, które go nie widzą. Zostaw puste dla obrazu dekoracyjnego. | +| Primary, insert mode | `mail.picker.insert` | Insert image | Wstaw obraz | +| Primary, choose mode | `mail.picker.use` | Use this image | Użyj tego obrazu | +| Upload button | `mail.picker.upload` | Upload image | Prześlij obraz | +| Uploading | `mail.picker.uploading` | Uploading… | Przesyłanie… | +| Empty library heading | `mail.picker.empty_title` | No images yet | Brak obrazów | +| Empty library body (may upload) | `mail.picker.empty_upload` | Upload an image to use it in mail. | Prześlij obraz, aby użyć go w wiadomościach. | +| Empty library body (pick only) | `mail.picker.empty_pick_only` | Images are added in Settings, Mail images, by an administrator who manages mail layouts. | Obrazy dodaje w Ustawieniach, w sekcji Obrazy w wiadomościach, administrator zarządzający układami wiadomości. | +| No search results | `mail.picker.no_results` | No images match “:query”. | Brak obrazów pasujących do „:query”. | +| Load failed | `mail.picker.load_failed` | The image library could not be loaded. Close this window and try again. | Nie udało się wczytać biblioteki obrazów. Zamknij to okno i spróbuj ponownie. | +| Close (icon button aria-label) | existing `form.cancel` | unchanged | unchanged | +| `mailimage`: nothing chosen | `mailimage.empty` | No image chosen | Nie wybrano obrazu | +| `mailimage`: choose / change / remove | `mailimage.choose` / `mailimage.change` / `mailimage.remove` | Choose image / Change image / Remove image | Wybierz obraz / Zmień obraz / Usuń obraz | +| `mailimage`: image gone from library | `mailimage.missing` | This image is no longer in the library. Choose another one. | Tego obrazu nie ma już w bibliotece. Wybierz inny. | +| Mail images field label / comment | `mail.images.field` / `mail.images.field_comment` | Images / Stored on the public disk and linked from mail by their public address. The caption title is the image's alternative text. Removing an image breaks every template, layout or partial that still shows it. | Obrazy / Przechowywane na dysku publicznym i linkowane z wiadomości przez publiczny adres. Tytuł podpisu to tekst alternatywny obrazu. Usunięcie obrazu psuje każdy szablon, układ lub fragment, który nadal go pokazuje. | + +### Code editor and colour picker (C1, C2) + +| Element | Key | EN | PL | +|---------|-----|----|----| +| Editor failed to load (fallback textarea note) | `codeeditor.load_failed` | The code editor could not be loaded. You can still edit the text here. | Nie udało się wczytać edytora kodu. Nadal możesz edytować tekst tutaj. | +| Toolbar aria-label | `codeeditor.toolbar` | Code editor tools | Narzędzia edytora kodu | +| Colour swatch aria-label | `colorpicker.pick` | Pick a colour for “:field” | Wybierz kolor: „:field” | +| Empty hex placeholder | `colorpicker.not_set` | Not set | Nie ustawiono | +| Invalid colour (422 field line) | `colorpicker.invalid` | Enter a colour as #rrggbb, for example #d66829. | Podaj kolor w formacie #rrggbb, na przykład #d66829. | + +### Branding page (C12, D-10, D-11) + +| Field | Key | EN | PL | Default (Winter) | +|-------|-----|----|----|------------------| +| `logo` (mailimage) label / comment | `mail.brand.logo` / `mail.brand.logo_comment` | Logo / Layouts show the logo where they use {{ .logoUrl }}. | Logo / Układy pokazują logo tam, gdzie używają {{ .logoUrl }}. | none | +| `body_bg` | `mail.brand.body_bg` | Body background | Główne tło | #f5f8fa | +| `content_bg` | `mail.brand.content_bg` | Content background | Tło treści | #ffffff | +| `header_color` | `mail.brand.header_color` | Header colour | Kolor górnego nagłówka | #bbbfc3 | +| `footer_color` | `mail.brand.footer_color` | Footer colour | Kolor stopki | #aeaeae | +| `heading_color` | `mail.brand.heading_color` | Headings colour | Kolor nagłówków | #2f3133 | +| `text_color` | `mail.brand.text_color` | Text colour | Kolor tekstu | #74787e | +| `link_color` | `mail.brand.link_color` | Link colour | Kolor linków | #2da7c7 | +| `button_text_color` | `mail.brand.button_text_color` | Button text colour | Kolor tekstu przycisków | #ffffff | +| `button_primary_bg` | `mail.brand.button_primary_bg` | Primary button background | Tło głównego przycisku | #d66829 | +| `button_positive_bg` | `mail.brand.button_positive_bg` | Positive button background | Tło pozytywnego przycisku | #52a838 | +| `button_negative_bg` | `mail.brand.button_negative_bg` | Negative button background | Tło negatywnego przycisku | #e01346 | +| `panel_bg` | `mail.brand.panel_bg` | Panel background | Tło panelu | #edeff2 | +| `table_border_color` | `mail.brand.table_border_color` | Table border colour | Kolor ramki tabeli | #edeff2 | +| `_preview` (mailpreview) comment | `mail.brand.preview_comment` | Until this page is saved for the first time, mail uses the brand CSS from the configuration (mail.brandCss). After that, these colours replace it. | Dopóki ta strona nie zostanie zapisana po raz pierwszy, wiadomości używają stylów wyglądu z konfiguracji (mail.brandCss). Potem zastępują je te kolory. | — | + +The set is D-10's list plus `panel_bg` and `table_border_color`, the colours of the built-in `panel` and `table` partials (D-12). Winter's promotion and subcopy colours are left out, because no built-in partial uses them. The planner may extend the set; each added colour follows the same row shape. + +### Save validation (D-13): server copy for 422 field lines + +| Rule | Key | EN | PL | Field | +|------|-----|----|----|-------| +| Parse error | `mail.invalid_parse` | This cannot be read as a template: :message | Nie można odczytać tego jako szablonu: :message | the edited content field (`body`, `content_html`, `content_text`, `content_css`) | +| Test render failed | `mail.invalid_render` | This does not render with the sample values: :message | To się nie renderuje z przykładowymi wartościami: :message | the field whose content failed | +| Unsafe content | `mail.invalid_unsafe` | Mail cannot contain scripts, frames, embedded objects, inline event handlers or javascript:, vbscript: or data: addresses. Found: :message | Wiadomość nie może zawierać skryptów, ramek, osadzonych obiektów, atrybutów zdarzeń ani adresów javascript:, vbscript: lub data:. Znaleziono: :message | the field whose content produced it (`body` for templates) | +| Subject with a line break | `mail.invalid_subject_line` | The subject must be a single line. | Temat musi mieścić się w jednym wierszu. | `subject` | +| Unknown layout | `mail.invalid_layout` | Choose a layout from the list. | Wybierz układ z listy. | `layout` | +| Unknown partial referenced | `mail.invalid_partial` | The partial “:name” does not exist. | Fragment „:name” nie istnieje. | the field that references it | +| Layout without {{ .Content }} | `mail.invalid_no_content` | Put {{ .Content }} where the message goes. | Wstaw {{ .Content }} w miejscu treści. | `content_html` or `content_text` | +| Custom layout code taken | `mail.layout.code_taken` | This code is already used by another layout. | Ten kod jest już używany przez inny układ. | `code` | +| Custom layout code format | `mail.layout.code_format` | Use lowercase letters, digits and dashes only. | Używaj tylko małych liter, cyfr i myślników. | `code` | + +`:message` is the parser/renderer/safety-checker text from postcard, passed through as plain text. It names the line when the parser reports one. + +--- + +## Focal Points + +- **Lists** (templates, layouts, partials): the code/name column with its row-state badges. +- **Template, layout and partial forms:** the body editor, with the preview directly under it. +- **Branding page:** the live preview. +- **Mail images page:** the image tile grid. +- **Image picker:** the tile grid; the primary button stays disabled until a tile is chosen. + +--- + +## Component Contracts + +Field names in the YAML below are recommendations (`body`, `content_html`, …). The planner may rename them, but the rendering, ordering and copy rules are fixed. New YAML keys are recommendations too: `language`, `mailImages`, and `mode` on `mailpreview` (`mode` is an existing key). Each new key goes into `formFieldKeys` (or type-specific options), the swag annotations, `admin/openapi/admin.json`, `schema.d.ts`, the cabana README and the docs. + +### C1. `codeeditor` field (D-09) — `CodeEditorField.vue` + +**YAML.** +- `type: codeeditor`; +- `language: html | css | text` (required; anything else is a boot error naming controller, field and value); +- optional `size` (`tiny|small|large|huge|giant`, heights in Spacing); +- optional `mailImages: true` (C4). + +The value is a string. It is part of the save body and sent unchanged until edited, like `richeditor`. + +**Loading.** +- CodeMirror is imported lazily from `codeeditor/editor.ts`. +- While it loads, the control shows `