Compare commits
4 Commits
a021fc45cd
...
f1265ee326
| Author | SHA1 | Date | |
|---|---|---|---|
| f1265ee326 | |||
| c5e09e7316 | |||
| 1ae6152da7 | |||
| 0305d80051 |
@@ -16,7 +16,7 @@ Gap categories: **SC** = spec correct, fix code | **CS** = code correct, update
|
|||||||
| A1-4 | ~~`doNotTranslate: false` written to frontmatter despite "only when true"~~ | frontmatter.allium:398 | `file_sync.ex:78` now converts false→nil so serializer omits the key | **Resolved:** doNotTranslate omitted from frontmatter when false, test added |
|
| A1-4 | ~~`doNotTranslate: false` written to frontmatter despite "only when true"~~ | frontmatter.allium:398 | `file_sync.ex:78` now converts false→nil so serializer omits the key | **Resolved:** doNotTranslate omitted from frontmatter when false, test added |
|
||||||
| A1-5 | ~~Auto-save after 3000ms idle~~ | editor_post.allium:183-188 | PostEditor schedules auto-save via parent timer on dirty change | **Resolved:** 3000ms idle auto-save timer in Bridges, tab-switch save in ShellLive, cancel on manual save, 3 tests added |
|
| A1-5 | ~~Auto-save after 3000ms idle~~ | editor_post.allium:183-188 | PostEditor schedules auto-save via parent timer on dirty change | **Resolved:** 3000ms idle auto-save timer in Bridges, tab-switch save in ShellLive, cancel on manual save, 3 tests added |
|
||||||
| A1-6 | ~~On-demand rendering in preview server~~ | preview.allium:53-93 | `Preview.Router` matches post/archive/home/language routes and renders on-demand via `Rendering` | **Resolved:** `Preview.Router` implements on-demand template rendering for post, archive, home, date, tag, category, page, and language-prefixed routes; static file fallback retained for non-HTML assets (pagefind, feeds); 6 tests added |
|
| A1-6 | ~~On-demand rendering in preview server~~ | preview.allium:53-93 | `Preview.Router` matches post/archive/home/language routes and renders on-demand via `Rendering` | **Resolved:** `Preview.Router` implements on-demand template rendering for post, archive, home, date, tag, category, page, and language-prefixed routes; static file fallback retained for non-HTML assets (pagefind, feeds); 6 tests added |
|
||||||
| A1-7 | Template lookup must use all 4 levels (post→tag→category→default) | template_context.allium:267-277 | Only levels 1 and 4 implemented; tag/category fallback unused | Fix code: implement levels 2-3 in template_selection.ex |
|
| A1-7 | ~~Template lookup must use all 4 levels (post→tag→category→default)~~ | template_context.allium:267-277 | `resolve_post_template_slug/3` implements tag→category cascade; all callers (preview, generation) updated | **Resolved:** `resolve_post_template_slug/3` in template_selection.ex, callers in preview.ex, router.ex, outputs.ex updated, 8 tests added |
|
||||||
| A1-8 | `ValidateLiquid`/`ValidateScript` before publish | template.allium:110, script.allium:165 | No validation gate before publish | Fix code: add validation step before publish |
|
| A1-8 | `ValidateLiquid`/`ValidateScript` before publish | template.allium:110, script.allium:165 | No validation gate before publish | Fix code: add validation step before publish |
|
||||||
| A1-9 | 17 preset colors + custom hex in tag picker | editor_tags.allium | Native `<input type="color">`, no preset palette | Fix code: implement preset color palette popover |
|
| A1-9 | 17 preset colors + custom hex in tag picker | editor_tags.allium | Native `<input type="color">`, no preset palette | Fix code: implement preset color palette popover |
|
||||||
| A1-10 | Template file written on create | engine_side_effects.allium:151-153 | Draft templates have `file_path=""` | Fix code: write template file on create |
|
| A1-10 | Template file written on create | engine_side_effects.allium:151-153 | Draft templates have `file_path=""` | Fix code: write template file on create |
|
||||||
@@ -24,6 +24,7 @@ Gap categories: **SC** = spec correct, fix code | **CS** = code correct, update
|
|||||||
| A1-12 | Real Pagefind integration for search | generation.allium:208 | Stub only: `pagefind-ui.js` is one-liner, `PagefindUI` never defined, search-runtime.js silently bails, client-side search non-functional | Fix code: bundle real Pagefind, build proper fragment index, wire PagefindUI |
|
| A1-12 | Real Pagefind integration for search | generation.allium:208 | Stub only: `pagefind-ui.js` is one-liner, `PagefindUI` never defined, search-runtime.js silently bails, client-side search non-functional | Fix code: bundle real Pagefind, build proper fragment index, wire PagefindUI |
|
||||||
| A1-13 | Git sidebar shows only "Working tree" placeholder | sidebar_views.allium:651-770 | `sidebar.ex:782-798` returns single entity_list item; `BDS.Git` has full status/diff/commit/history/fetch/pull/push/prune_lfs but sidebar doesn't use it | Fix code: wire sidebar `git_view/0` to `BDS.Git` — render branch, ahead/behind, status file list, commit input, history entries, action buttons per spec |
|
| A1-13 | Git sidebar shows only "Working tree" placeholder | sidebar_views.allium:651-770 | `sidebar.ex:782-798` returns single entity_list item; `BDS.Git` has full status/diff/commit/history/fetch/pull/push/prune_lfs but sidebar doesn't use it | Fix code: wire sidebar `git_view/0` to `BDS.Git` — render branch, ahead/behind, status file list, commit input, history entries, action buttons per spec |
|
||||||
| A1-14 | Embedding uses TF-IDF hash projection instead of real neural model | embedding.allium:44-53, invariants ModelCaching/VectorCacheInDb | `backends/in_app.ex` hashes terms into sparse vectors via `:erlang.phash2`; no ONNX model, no `"query: "` prefix, no mean pooling, vectors stored as JSON text not Float32Array BLOB, snapshot-based neighbor lookup instead of USearch HNSW index | Fix code: (1) add Bumblebee + ONNX runtime deps to run `Xenova/multilingual-e5-small`, (2) implement lazy model download + cache in app data dir, (3) `"query: "` prefix + mean pooling + L2 norm in backend, (4) store vectors as binary BLOB (1536 bytes), (5) replace JSON snapshot with USearch HNSW index (cosine, M=16, ef=128/64, 5s debounce), (6) cross-language semantic similarity must work |
|
| A1-14 | Embedding uses TF-IDF hash projection instead of real neural model | embedding.allium:44-53, invariants ModelCaching/VectorCacheInDb | `backends/in_app.ex` hashes terms into sparse vectors via `:erlang.phash2`; no ONNX model, no `"query: "` prefix, no mean pooling, vectors stored as JSON text not Float32Array BLOB, snapshot-based neighbor lookup instead of USearch HNSW index | Fix code: (1) add Bumblebee + ONNX runtime deps to run `Xenova/multilingual-e5-small`, (2) implement lazy model download + cache in app data dir, (3) `"query: "` prefix + mean pooling + L2 norm in backend, (4) store vectors as binary BLOB (1536 bytes), (5) replace JSON snapshot with USearch HNSW index (cosine, M=16, ef=128/64, 5s debounce), (6) cross-language semantic similarity must work |
|
||||||
|
| A1-15 | ~~Preview vs generation content source strategy undocumented~~ | preview.allium (no invariant), generation.allium (no invariant) | Generation uses only published .md file content (`Generation.Data` snapshots set `content: nil`); preview includes published+draft posts and prefers DB content over file (`Preview.Router` queries `:published`/`:draft`, uses `editor_body`) | **Resolved:** added `PreviewDraftOverlay` invariant to preview.allium and `GenerationPublishedOnly` invariant to generation.allium; both cross-reference each other; code already correct, 3 tests added for draft-in-preview behavior |
|
||||||
|
|
||||||
### A2. Spec Should Update (code is normative)
|
### A2. Spec Should Update (code is normative)
|
||||||
|
|
||||||
|
|||||||
@@ -16,4 +16,5 @@
|
|||||||
@import "./menu_editor.css";
|
@import "./menu_editor.css";
|
||||||
@import "./media_editor.css";
|
@import "./media_editor.css";
|
||||||
@import "./import_editor.css";
|
@import "./import_editor.css";
|
||||||
|
@import "./misc_editor.css";
|
||||||
@import "./utilities.css";
|
@import "./utilities.css";
|
||||||
539
assets/css/misc_editor.css
Normal file
539
assets/css/misc_editor.css
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
/* ── Misc-editor shell (shared by all misc tabs) ──────────────────────── */
|
||||||
|
|
||||||
|
.misc-editor-shell {
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.misc-editor-header {
|
||||||
|
padding: 12px 16px 8px;
|
||||||
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
background: var(--vscode-tab-activeBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.misc-editor-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.misc-editor-header p {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.misc-editor-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.misc-editor-summary {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.misc-editor-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Summary pills ───────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.misc-summary-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.misc-summary-pill span {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.misc-summary-pill strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Misc card (used by site-validation, empty states) ───────────────── */
|
||||||
|
|
||||||
|
.misc-card {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--vscode-editor-background) 84%, var(--vscode-input-background));
|
||||||
|
}
|
||||||
|
|
||||||
|
.misc-card h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.misc-card p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.misc-card ul {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Misc columns (site-validation 3-column layout) ──────────────────── */
|
||||||
|
|
||||||
|
.misc-columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Misc list (find-duplicates) ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
.misc-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.misc-list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.misc-list-item:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.duplicate-pair-row label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duplicate-pair-row .linkish {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--vscode-textLink-foreground, #3794ff);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
text-underline-offset: 0.14em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duplicate-pair-row .linkish:hover {
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Metadata-diff: tab bar ──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.metadata-diff-tool {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-diff-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-diff-tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vscode-tab-inactiveForeground, var(--vscode-descriptionForeground));
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.12s, border-color 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-diff-tab:hover {
|
||||||
|
color: var(--vscode-tab-activeForeground, var(--vscode-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-diff-tab.active {
|
||||||
|
color: var(--vscode-tab-activeForeground, var(--vscode-foreground));
|
||||||
|
border-bottom-color: var(--vscode-focusBorder, #007fd4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--vscode-activityBarBadge-background, #007acc);
|
||||||
|
color: var(--vscode-activityBarBadge-foreground, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Metadata-diff: field pills ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
.metadata-diff-field-pills {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-diff-field-pill {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-diff-field-pill.active {
|
||||||
|
border-color: var(--vscode-focusBorder, #007fd4);
|
||||||
|
background: color-mix(in srgb, var(--vscode-focusBorder, #007fd4) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-diff-field-pill-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-diff-field-pill-toggle:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-pill-label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-pill-count {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-diff-field-pill-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-left: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-diff-action-button {
|
||||||
|
font-size: 11px !important;
|
||||||
|
padding: 2px 8px !important;
|
||||||
|
min-height: 22px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Metadata-diff: results area ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
.metadata-diff-results {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-diff-empty p {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Diff item cards (shared by metadata-diff and orphan sections) ──── */
|
||||||
|
|
||||||
|
.diff-item-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-item-card {
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--vscode-editor-background) 84%, var(--vscode-input-background));
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-item-card.orphan-file {
|
||||||
|
border-left: 3px solid var(--vscode-editorWarning-foreground, #cca700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-item-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: color-mix(in srgb, var(--vscode-sideBar-background) 50%, transparent);
|
||||||
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-item-header strong {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-item-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-item-fields {
|
||||||
|
padding: 8px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-field-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 120px 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 5px 0;
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, var(--vscode-panel-border) 50%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-field-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-field-name {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-field-values {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-field-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-field-value.db-value {
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-field-value.file-value {
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-source-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 28px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-value .diff-source-label {
|
||||||
|
background: color-mix(in srgb, var(--vscode-focusBorder, #007fd4) 22%, transparent);
|
||||||
|
color: var(--vscode-focusBorder, #007fd4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-value .diff-source-label {
|
||||||
|
background: color-mix(in srgb, var(--vscode-testing-iconPassed, #73c991) 22%, transparent);
|
||||||
|
color: var(--vscode-testing-iconPassed, #73c991);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Orphan files section ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.orphan-files-section {
|
||||||
|
border: 1px solid color-mix(in srgb, var(--vscode-editorWarning-foreground, #cca700) 35%, transparent);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: color-mix(in srgb, var(--vscode-editorWarning-foreground, #cca700) 5%, var(--vscode-editor-background));
|
||||||
|
}
|
||||||
|
|
||||||
|
.orphan-files-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orphan-files-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orphan-files-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orphan-path span {
|
||||||
|
font-family: "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Translation validation ──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.translation-validation-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-validation-summary {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-validation-summary p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-validation-section h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-validation-empty {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-validation-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-validation-card {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: color-mix(in srgb, var(--vscode-editor-background) 84%, var(--vscode-input-background));
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-validation-card-db {
|
||||||
|
border-left: 3px solid var(--vscode-focusBorder, #007fd4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-validation-card-file {
|
||||||
|
border-left: 3px solid var(--vscode-testing-iconPassed, #73c991);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-validation-card-title {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-validation-card-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 3px 12px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-validation-card-meta dt {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-validation-card-meta dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-validation-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Git diff ────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.git-diff-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-diff-empty {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-diff-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-diff-toolbar label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-diff-toolbar select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-diff-editor {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ defmodule BDS.Generation.Outputs do
|
|||||||
import BDS.Generation.Renderers
|
import BDS.Generation.Renderers
|
||||||
import BDS.Generation.Sitemap, only: [render_feed: 3, render_atom: 3, render_calendar: 1]
|
import BDS.Generation.Sitemap, only: [render_feed: 3, render_atom: 3, render_calendar: 1]
|
||||||
|
|
||||||
|
alias BDS.Rendering.TemplateSelection
|
||||||
|
|
||||||
@spec additional_languages(map()) :: [String.t()]
|
@spec additional_languages(map()) :: [String.t()]
|
||||||
def additional_languages(plan) do
|
def additional_languages(plan) do
|
||||||
Enum.reject(plan.blog_languages, &(&1 == plan.language))
|
Enum.reject(plan.blog_languages, &(&1 == plan.language))
|
||||||
@@ -391,10 +393,12 @@ defmodule BDS.Generation.Outputs do
|
|||||||
canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, post)
|
canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, post)
|
||||||
body = load_body(project_id, canonical_variant.file_path, canonical_variant.content)
|
body = load_body(project_id, canonical_variant.file_path, canonical_variant.content)
|
||||||
|
|
||||||
|
effective_slug = effective_template_slug(project_id, post)
|
||||||
|
|
||||||
{page_output_path(post.slug, nil),
|
{page_output_path(post.slug, nil),
|
||||||
render_post_output(
|
render_post_output(
|
||||||
project_id,
|
project_id,
|
||||||
post.template_slug,
|
effective_slug,
|
||||||
%{
|
%{
|
||||||
id: canonical_variant.id,
|
id: canonical_variant.id,
|
||||||
title: canonical_variant.title,
|
title: canonical_variant.title,
|
||||||
@@ -423,10 +427,12 @@ defmodule BDS.Generation.Outputs do
|
|||||||
|> Enum.map(fn post ->
|
|> Enum.map(fn post ->
|
||||||
body = load_body(project_id, post.file_path, post.content)
|
body = load_body(project_id, post.file_path, post.content)
|
||||||
|
|
||||||
|
effective_slug = effective_template_slug(project_id, post)
|
||||||
|
|
||||||
{page_output_path(post.slug, language),
|
{page_output_path(post.slug, language),
|
||||||
render_post_output(
|
render_post_output(
|
||||||
project_id,
|
project_id,
|
||||||
post.template_slug,
|
effective_slug,
|
||||||
%{
|
%{
|
||||||
id: post.id,
|
id: post.id,
|
||||||
title: post.title,
|
title: post.title,
|
||||||
@@ -521,10 +527,12 @@ defmodule BDS.Generation.Outputs do
|
|||||||
canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, post)
|
canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, post)
|
||||||
body = load_body(project_id, canonical_variant.file_path, canonical_variant.content)
|
body = load_body(project_id, canonical_variant.file_path, canonical_variant.content)
|
||||||
|
|
||||||
|
effective_slug = effective_template_slug(project_id, post)
|
||||||
|
|
||||||
{post_output_path(post),
|
{post_output_path(post),
|
||||||
render_post_output(
|
render_post_output(
|
||||||
project_id,
|
project_id,
|
||||||
post.template_slug,
|
effective_slug,
|
||||||
%{
|
%{
|
||||||
id: canonical_variant.id,
|
id: canonical_variant.id,
|
||||||
title: canonical_variant.title,
|
title: canonical_variant.title,
|
||||||
@@ -551,10 +559,12 @@ defmodule BDS.Generation.Outputs do
|
|||||||
Enum.map(posts, fn post ->
|
Enum.map(posts, fn post ->
|
||||||
body = load_body(project_id, post.file_path, post.content)
|
body = load_body(project_id, post.file_path, post.content)
|
||||||
|
|
||||||
|
effective_slug = effective_template_slug(project_id, post)
|
||||||
|
|
||||||
{post_output_path(post, language),
|
{post_output_path(post, language),
|
||||||
render_post_output(
|
render_post_output(
|
||||||
project_id,
|
project_id,
|
||||||
post.template_slug,
|
effective_slug,
|
||||||
%{
|
%{
|
||||||
id: post.id,
|
id: post.id,
|
||||||
title: post.title,
|
title: post.title,
|
||||||
@@ -571,4 +581,18 @@ defmodule BDS.Generation.Outputs do
|
|||||||
|
|
||||||
post_outputs ++ translation_outputs
|
post_outputs ++ translation_outputs
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp effective_template_slug(project_id, post) do
|
||||||
|
slug = Map.get(post, :template_slug)
|
||||||
|
|
||||||
|
if is_binary(slug) and slug != "" do
|
||||||
|
slug
|
||||||
|
else
|
||||||
|
TemplateSelection.resolve_post_template_slug(
|
||||||
|
project_id,
|
||||||
|
Map.get(post, :tags) || [],
|
||||||
|
Map.get(post, :categories) || []
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ defmodule BDS.Preview do
|
|||||||
alias BDS.Projects
|
alias BDS.Projects
|
||||||
alias BDS.Repo
|
alias BDS.Repo
|
||||||
alias BDS.Rendering
|
alias BDS.Rendering
|
||||||
|
alias BDS.Rendering.TemplateSelection
|
||||||
|
|
||||||
@host "127.0.0.1"
|
@host "127.0.0.1"
|
||||||
@port 4123
|
@port 4123
|
||||||
@@ -219,6 +220,7 @@ defmodule BDS.Preview do
|
|||||||
|
|
||||||
defp draft_preview_payload(post, query_params) do
|
defp draft_preview_payload(post, query_params) do
|
||||||
requested_language = query_params |> Map.get("lang") |> normalize_requested_language()
|
requested_language = query_params |> Map.get("lang") |> normalize_requested_language()
|
||||||
|
effective_slug = post.template_slug || TemplateSelection.resolve_post_template_slug(post.project_id, post.tags, post.categories)
|
||||||
|
|
||||||
case draft_preview_translation(post.id, requested_language, post.language) do
|
case draft_preview_translation(post.id, requested_language, post.language) do
|
||||||
%Translation{} = translation ->
|
%Translation{} = translation ->
|
||||||
@@ -230,7 +232,7 @@ defmodule BDS.Preview do
|
|||||||
slug: post.slug,
|
slug: post.slug,
|
||||||
language: translation.language,
|
language: translation.language,
|
||||||
excerpt: translation.excerpt,
|
excerpt: translation.excerpt,
|
||||||
template_slug: post.template_slug
|
template_slug: effective_slug
|
||||||
}
|
}
|
||||||
|
|
||||||
nil ->
|
nil ->
|
||||||
@@ -242,7 +244,7 @@ defmodule BDS.Preview do
|
|||||||
slug: post.slug,
|
slug: post.slug,
|
||||||
language: post.language,
|
language: post.language,
|
||||||
excerpt: post.excerpt,
|
excerpt: post.excerpt,
|
||||||
template_slug: post.template_slug
|
template_slug: effective_slug
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ defmodule BDS.Preview.Router do
|
|||||||
alias BDS.Posts.Post
|
alias BDS.Posts.Post
|
||||||
alias BDS.Posts.Translation
|
alias BDS.Posts.Translation
|
||||||
alias BDS.Rendering
|
alias BDS.Rendering
|
||||||
|
alias BDS.Rendering.TemplateSelection
|
||||||
alias BDS.Repo
|
alias BDS.Repo
|
||||||
|
|
||||||
@type route ::
|
@type route ::
|
||||||
@@ -219,7 +220,9 @@ defmodule BDS.Preview.Router do
|
|||||||
_post_record: effective_record
|
_post_record: effective_record
|
||||||
}
|
}
|
||||||
|
|
||||||
case Rendering.render_post_page(project_id, post.template_slug, assigns) do
|
effective_slug = post.template_slug || TemplateSelection.resolve_post_template_slug(project_id, post.tags, post.categories)
|
||||||
|
|
||||||
|
case Rendering.render_post_page(project_id, effective_slug, assigns) do
|
||||||
{:ok, rendered} -> {:ok, rendered}
|
{:ok, rendered} -> {:ok, rendered}
|
||||||
{:error, _reason} -> {:error, :not_found}
|
{:error, _reason} -> {:error, :not_found}
|
||||||
end
|
end
|
||||||
@@ -406,35 +409,35 @@ defmodule BDS.Preview.Router do
|
|||||||
|> MapSet.new()
|
|> MapSet.new()
|
||||||
|
|
||||||
project_id
|
project_id
|
||||||
|> load_all_published_posts()
|
|> load_previewable_posts()
|
||||||
|> Enum.reject(fn post ->
|
|> Enum.reject(fn post ->
|
||||||
Enum.any?(post.categories || [], &MapSet.member?(excluded, &1))
|
Enum.any?(post.categories || [], &MapSet.member?(excluded, &1))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_all_published_posts(project_id) do
|
defp load_previewable_posts(project_id) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from p in Post,
|
from p in Post,
|
||||||
where: p.project_id == ^project_id and p.status == :published,
|
where: p.project_id == ^project_id and p.status in [:published, :draft],
|
||||||
order_by: [desc: p.created_at, desc: p.published_at, asc: p.slug]
|
order_by: [desc: p.created_at, desc: p.published_at, asc: p.slug]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_published_posts_by_category(project_id, category) do
|
defp load_published_posts_by_category(project_id, category) do
|
||||||
project_id
|
project_id
|
||||||
|> load_all_published_posts()
|
|> load_previewable_posts()
|
||||||
|> Enum.filter(fn post -> category in (post.categories || []) end)
|
|> Enum.filter(fn post -> category in (post.categories || []) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_published_posts_by_tag(project_id, tag) do
|
defp load_published_posts_by_tag(project_id, tag) do
|
||||||
project_id
|
project_id
|
||||||
|> load_all_published_posts()
|
|> load_previewable_posts()
|
||||||
|> Enum.filter(fn post -> tag in (post.tags || []) end)
|
|> Enum.filter(fn post -> tag in (post.tags || []) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_published_posts_by_year(project_id, year) do
|
defp load_published_posts_by_year(project_id, year) do
|
||||||
project_id
|
project_id
|
||||||
|> load_all_published_posts()
|
|> load_previewable_posts()
|
||||||
|> Enum.filter(fn post ->
|
|> Enum.filter(fn post ->
|
||||||
{post_year, _, _} = Paths.local_date_parts!(post.created_at)
|
{post_year, _, _} = Paths.local_date_parts!(post.created_at)
|
||||||
post_year == year
|
post_year == year
|
||||||
@@ -443,7 +446,7 @@ defmodule BDS.Preview.Router do
|
|||||||
|
|
||||||
defp load_published_posts_by_month(project_id, year, month) do
|
defp load_published_posts_by_month(project_id, year, month) do
|
||||||
project_id
|
project_id
|
||||||
|> load_all_published_posts()
|
|> load_previewable_posts()
|
||||||
|> Enum.filter(fn post ->
|
|> Enum.filter(fn post ->
|
||||||
{post_year, post_month, _} = Paths.local_date_parts!(post.created_at)
|
{post_year, post_month, _} = Paths.local_date_parts!(post.created_at)
|
||||||
post_year == year and post_month == month
|
post_year == year and post_month == month
|
||||||
@@ -452,7 +455,7 @@ defmodule BDS.Preview.Router do
|
|||||||
|
|
||||||
defp load_published_posts_by_day(project_id, year, month, day) do
|
defp load_published_posts_by_day(project_id, year, month, day) do
|
||||||
project_id
|
project_id
|
||||||
|> load_all_published_posts()
|
|> load_previewable_posts()
|
||||||
|> Enum.filter(fn post ->
|
|> Enum.filter(fn post ->
|
||||||
{post_year, post_month, post_day} = Paths.local_date_parts!(post.created_at)
|
{post_year, post_month, post_day} = Paths.local_date_parts!(post.created_at)
|
||||||
post_year == year and post_month == month and post_day == day
|
post_year == year and post_month == month and post_day == day
|
||||||
@@ -462,7 +465,9 @@ defmodule BDS.Preview.Router do
|
|||||||
defp find_post_by_slug_and_date(project_id, slug, year, month, day) do
|
defp find_post_by_slug_and_date(project_id, slug, year, month, day) do
|
||||||
case Repo.one(
|
case Repo.one(
|
||||||
from p in Post,
|
from p in Post,
|
||||||
where: p.project_id == ^project_id and p.slug == ^slug and p.status == :published
|
where:
|
||||||
|
p.project_id == ^project_id and p.slug == ^slug and
|
||||||
|
p.status in [:published, :draft]
|
||||||
) do
|
) do
|
||||||
nil ->
|
nil ->
|
||||||
nil
|
nil
|
||||||
@@ -481,7 +486,9 @@ defmodule BDS.Preview.Router do
|
|||||||
defp find_page_by_slug(project_id, slug) do
|
defp find_page_by_slug(project_id, slug) do
|
||||||
case Repo.one(
|
case Repo.one(
|
||||||
from p in Post,
|
from p in Post,
|
||||||
where: p.project_id == ^project_id and p.slug == ^slug and p.status == :published
|
where:
|
||||||
|
p.project_id == ^project_id and p.slug == ^slug and
|
||||||
|
p.status in [:published, :draft]
|
||||||
) do
|
) do
|
||||||
%Post{categories: categories} = post ->
|
%Post{categories: categories} = post ->
|
||||||
if "page" in (categories || []), do: post, else: nil
|
if "page" in (categories || []), do: post, else: nil
|
||||||
|
|||||||
@@ -4,13 +4,52 @@ defmodule BDS.Rendering.TemplateSelection do
|
|||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
alias BDS.Frontmatter
|
alias BDS.Frontmatter
|
||||||
|
alias BDS.Metadata
|
||||||
alias BDS.Projects
|
alias BDS.Projects
|
||||||
alias BDS.Rendering.FileSystem
|
alias BDS.Rendering.FileSystem
|
||||||
alias BDS.Rendering.Filters
|
alias BDS.Rendering.Filters
|
||||||
alias BDS.Repo
|
alias BDS.Repo
|
||||||
alias BDS.StarterTemplates
|
alias BDS.StarterTemplates
|
||||||
|
alias BDS.Tags.Tag
|
||||||
alias BDS.Templates.Template
|
alias BDS.Templates.Template
|
||||||
|
|
||||||
|
@spec resolve_post_template_slug(String.t(), [String.t()], [String.t()]) ::
|
||||||
|
String.t() | nil
|
||||||
|
def resolve_post_template_slug(project_id, tag_names, category_names) do
|
||||||
|
resolve_from_tags(project_id, tag_names) ||
|
||||||
|
resolve_from_categories(project_id, category_names)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp resolve_from_tags(_project_id, []), do: nil
|
||||||
|
|
||||||
|
defp resolve_from_tags(project_id, tag_names) do
|
||||||
|
Repo.all(
|
||||||
|
from tag in Tag,
|
||||||
|
where:
|
||||||
|
tag.project_id == ^project_id and
|
||||||
|
tag.name in ^tag_names and
|
||||||
|
not is_nil(tag.post_template_slug) and
|
||||||
|
tag.post_template_slug != "",
|
||||||
|
select: tag.post_template_slug,
|
||||||
|
limit: 1
|
||||||
|
)
|
||||||
|
|> List.first()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp resolve_from_categories(_project_id, []), do: nil
|
||||||
|
|
||||||
|
defp resolve_from_categories(project_id, category_names) do
|
||||||
|
{:ok, state} = Metadata.get_project_metadata(project_id)
|
||||||
|
settings = state.category_settings || %{}
|
||||||
|
|
||||||
|
Enum.find_value(category_names, fn cat_name ->
|
||||||
|
case Map.get(settings, cat_name) do
|
||||||
|
%{"post_template_slug" => slug} when is_binary(slug) and slug != "" -> slug
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
@spec load_template_source(String.t(), atom(), String.t() | nil) ::
|
@spec load_template_source(String.t(), atom(), String.t() | nil) ::
|
||||||
{:ok, String.t()} | {:error, term()}
|
{:ok, String.t()} | {:error, term()}
|
||||||
def load_template_source(project_id, kind, slug) do
|
def load_template_source(project_id, kind, slug) do
|
||||||
|
|||||||
@@ -5337,6 +5337,472 @@ button.import-taxonomy-pill {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.misc-editor-shell {
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
}
|
||||||
|
.misc-editor-header {
|
||||||
|
padding: 12px 16px 8px;
|
||||||
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
background: var(--vscode-tab-activeBackground);
|
||||||
|
}
|
||||||
|
.misc-editor-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.misc-editor-header p {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.misc-editor-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.misc-editor-summary {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
.misc-editor-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.misc-summary-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
.misc-summary-pill span {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.misc-summary-pill strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.misc-card {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background: color-mix(in srgb, var(--vscode-editor-background) 84%, var(--vscode-input-background));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.misc-card h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.misc-card p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.misc-card ul {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.misc-columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.misc-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.misc-list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.misc-list-item:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
.duplicate-pair-row label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.duplicate-pair-row .linkish {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--vscode-textLink-foreground, #3794ff);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
text-underline-offset: 0.14em;
|
||||||
|
}
|
||||||
|
.duplicate-pair-row .linkish:hover {
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
.metadata-diff-tool {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.metadata-diff-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
.metadata-diff-tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vscode-tab-inactiveForeground, var(--vscode-descriptionForeground));
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.12s, border-color 0.12s;
|
||||||
|
}
|
||||||
|
.metadata-diff-tab:hover {
|
||||||
|
color: var(--vscode-tab-activeForeground, var(--vscode-foreground));
|
||||||
|
}
|
||||||
|
.metadata-diff-tab.active {
|
||||||
|
color: var(--vscode-tab-activeForeground, var(--vscode-foreground));
|
||||||
|
border-bottom-color: var(--vscode-focusBorder, #007fd4);
|
||||||
|
}
|
||||||
|
.tab-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--vscode-activityBarBadge-background, #007acc);
|
||||||
|
color: var(--vscode-activityBarBadge-foreground, #ffffff);
|
||||||
|
}
|
||||||
|
.metadata-diff-field-pills {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.metadata-diff-field-pill {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.12s;
|
||||||
|
}
|
||||||
|
.metadata-diff-field-pill.active {
|
||||||
|
border-color: var(--vscode-focusBorder, #007fd4);
|
||||||
|
background: var(--vscode-focusBorder, #007fd4);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background: color-mix(in srgb, var(--vscode-focusBorder, #007fd4) 12%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.metadata-diff-field-pill-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.metadata-diff-field-pill-toggle:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
.field-pill-label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.field-pill-count {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.metadata-diff-field-pill-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-left: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
.metadata-diff-action-button {
|
||||||
|
font-size: 11px !important;
|
||||||
|
padding: 2px 8px !important;
|
||||||
|
min-height: 22px !important;
|
||||||
|
}
|
||||||
|
.metadata-diff-results {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.metadata-diff-empty p {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
.diff-item-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.diff-item-card {
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background: color-mix(in srgb, var(--vscode-editor-background) 84%, var(--vscode-input-background));
|
||||||
|
}
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.diff-item-card.orphan-file {
|
||||||
|
border-left: 3px solid var(--vscode-editorWarning-foreground, #cca700);
|
||||||
|
}
|
||||||
|
.diff-item-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background: color-mix(in srgb, var(--vscode-sideBar-background) 50%, transparent);
|
||||||
|
}
|
||||||
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
.diff-item-header strong {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
.diff-item-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.diff-item-fields {
|
||||||
|
padding: 8px 14px;
|
||||||
|
}
|
||||||
|
.diff-field-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 120px 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 5px 0;
|
||||||
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, var(--vscode-panel-border) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.diff-field-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.diff-field-name {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding-top: 3px;
|
||||||
|
}
|
||||||
|
.diff-field-values {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
.diff-field-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.diff-field-value.db-value {
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
.diff-field-value.file-value {
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
.diff-source-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 28px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.db-value .diff-source-label {
|
||||||
|
background: var(--vscode-focusBorder, #007fd4);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background: color-mix(in srgb, var(--vscode-focusBorder, #007fd4) 22%, transparent);
|
||||||
|
}
|
||||||
|
color: var(--vscode-focusBorder, #007fd4);
|
||||||
|
}
|
||||||
|
.file-value .diff-source-label {
|
||||||
|
background: var(--vscode-testing-iconPassed, #73c991);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background: color-mix(in srgb, var(--vscode-testing-iconPassed, #73c991) 22%, transparent);
|
||||||
|
}
|
||||||
|
color: var(--vscode-testing-iconPassed, #73c991);
|
||||||
|
}
|
||||||
|
.orphan-files-section {
|
||||||
|
border: 1px solid var(--vscode-editorWarning-foreground, #cca700);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border: 1px solid color-mix(in srgb, var(--vscode-editorWarning-foreground, #cca700) 35%, transparent);
|
||||||
|
}
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: var(--vscode-editorWarning-foreground, #cca700);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background: color-mix(in srgb, var(--vscode-editorWarning-foreground, #cca700) 5%, var(--vscode-editor-background));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.orphan-files-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.orphan-files-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.orphan-files-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.orphan-path span {
|
||||||
|
font-family: "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.translation-validation-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.translation-validation-summary {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.translation-validation-summary p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.translation-validation-section h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.translation-validation-empty {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.translation-validation-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.translation-validation-card {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background: color-mix(in srgb, var(--vscode-editor-background) 84%, var(--vscode-input-background));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.translation-validation-card-db {
|
||||||
|
border-left: 3px solid var(--vscode-focusBorder, #007fd4);
|
||||||
|
}
|
||||||
|
.translation-validation-card-file {
|
||||||
|
border-left: 3px solid var(--vscode-testing-iconPassed, #73c991);
|
||||||
|
}
|
||||||
|
.translation-validation-card-title {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.translation-validation-card-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 3px 12px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.translation-validation-card-meta dt {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.translation-validation-card-meta dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.translation-validation-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
.git-diff-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.git-diff-empty {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
.git-diff-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.git-diff-toolbar label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.git-diff-toolbar select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.git-diff-editor {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
@layer components {
|
@layer components {
|
||||||
.ui-button {
|
.ui-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -63,6 +63,23 @@ surface GenerationStatusSurface {
|
|||||||
generation.generated_files.count
|
generation.generated_files.count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invariant GenerationPublishedOnly {
|
||||||
|
-- Generation renders the *published* state of the blog, never draft content.
|
||||||
|
--
|
||||||
|
-- Post universe: posts that have a published .md file on disk.
|
||||||
|
-- This includes status=published posts and status=draft posts that were
|
||||||
|
-- previously published (they still have a file_path with last-published content).
|
||||||
|
-- Posts that have never been published (no file_path) are excluded entirely.
|
||||||
|
--
|
||||||
|
-- Content source: always the .md file on disk (the last-published snapshot).
|
||||||
|
-- The DB content field (which holds draft edits) is never read during generation.
|
||||||
|
-- Snapshots set content=nil to ensure file-based resolution.
|
||||||
|
--
|
||||||
|
-- Contrast with preview (see preview.allium PreviewDraftOverlay):
|
||||||
|
-- Preview includes all drafts and prefers DB content over file content,
|
||||||
|
-- giving the author a live view of unpublished edits.
|
||||||
|
}
|
||||||
|
|
||||||
invariant IncrementalByContentHash {
|
invariant IncrementalByContentHash {
|
||||||
-- Files are only written when content_hash changes
|
-- Files are only written when content_hash changes
|
||||||
-- generatedFileHashes table tracks (projectId, relativePath, contentHash)
|
-- generatedFileHashes table tracks (projectId, relativePath, contentHash)
|
||||||
|
|||||||
@@ -49,18 +49,24 @@ rule StopPreview {
|
|||||||
}
|
}
|
||||||
|
|
||||||
-- Route resolution
|
-- Route resolution
|
||||||
|
-- Preview renders all posts (published + draft) on-demand via Liquid templates.
|
||||||
|
-- Content priority: DB content (draft edits) over published .md file content.
|
||||||
|
-- See invariant PreviewDraftOverlay below.
|
||||||
|
|
||||||
rule ServePostPreview {
|
rule ServePostPreview {
|
||||||
when: PreviewRequest(path)
|
when: PreviewRequest(path)
|
||||||
requires: is_post_path(path)
|
requires: is_post_path(path)
|
||||||
-- path matches "/{yyyy}/{mm}/{dd}/{slug}"
|
-- path matches "/{yyyy}/{mm}/{dd}/{slug}"
|
||||||
-- Renders post via Liquid template with full PageRenderer context
|
-- Finds post by slug+date regardless of status (published or draft).
|
||||||
|
-- Content resolved via editor_body: DB content if present, else .md file.
|
||||||
|
-- Renders via Liquid template with full PageRenderer context.
|
||||||
ensures: PreviewResponse(rendered_html)
|
ensures: PreviewResponse(rendered_html)
|
||||||
}
|
}
|
||||||
|
|
||||||
rule ServeDraftPreview {
|
rule ServeDraftPreview {
|
||||||
when: PreviewDraftRequest(path, post_id)
|
when: PreviewDraftRequest(path, post_id)
|
||||||
-- Renders draft content (from DB, not filesystem)
|
-- Explicit draft preview by post_id (used by editor preview pane).
|
||||||
|
-- Renders draft content (from DB, not filesystem).
|
||||||
ensures: PreviewResponse(rendered_html)
|
ensures: PreviewResponse(rendered_html)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +74,7 @@ rule ServeArchivePreview {
|
|||||||
when: PreviewRequest(path)
|
when: PreviewRequest(path)
|
||||||
requires: is_archive_path(path)
|
requires: is_archive_path(path)
|
||||||
-- Category, tag, date archives with pagination
|
-- Category, tag, date archives with pagination
|
||||||
|
-- Includes both published and draft posts in listings.
|
||||||
ensures: PreviewResponse(rendered_html)
|
ensures: PreviewResponse(rendered_html)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +99,28 @@ rule ServeLanguagePrefixedRoute {
|
|||||||
ensures: PreviewResponse(translated_html)
|
ensures: PreviewResponse(translated_html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invariant PreviewDraftOverlay {
|
||||||
|
-- Preview is the draft workspace: it shows what the blog *will* look like,
|
||||||
|
-- not what it currently looks like on the published site.
|
||||||
|
--
|
||||||
|
-- Post universe: all posts with status in {published, draft}.
|
||||||
|
-- Archived posts are excluded.
|
||||||
|
--
|
||||||
|
-- Content priority (per post):
|
||||||
|
-- 1. DB content field (draft edits not yet published) → used when non-nil
|
||||||
|
-- 2. Published .md file (last-published snapshot) → used when DB content is nil
|
||||||
|
-- 3. Empty string → fallback if neither exists
|
||||||
|
--
|
||||||
|
-- This means:
|
||||||
|
-- - A purely draft post (never published) renders from DB content.
|
||||||
|
-- - A published-then-edited post renders from DB content (the draft edits).
|
||||||
|
-- - A published post with no pending edits renders from its .md file.
|
||||||
|
--
|
||||||
|
-- Contrast with generation (see generation.allium GenerationPublishedOnly):
|
||||||
|
-- Generation uses *only* published .md file content, never DB draft content,
|
||||||
|
-- and excludes posts that have never been published.
|
||||||
|
}
|
||||||
|
|
||||||
invariant ThemeSwitching {
|
invariant ThemeSwitching {
|
||||||
-- Preview supports live theme/mode switching via query params
|
-- Preview supports live theme/mode switching via query params
|
||||||
-- ?theme=amber&mode=dark etc.
|
-- ?theme=amber&mode=dark etc.
|
||||||
|
|||||||
@@ -605,6 +605,107 @@ defmodule BDS.PreviewTest do
|
|||||||
assert :ok = BDS.Preview.stop_preview(project.id)
|
assert :ok = BDS.Preview.stop_preview(project.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "on-demand rendering: draft post (never published) is visible and uses DB content", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
|
assert {:ok, _metadata} =
|
||||||
|
Metadata.update_project_metadata(project.id, %{
|
||||||
|
main_language: "en",
|
||||||
|
blog_languages: ["en"]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, post} =
|
||||||
|
Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Pure Draft",
|
||||||
|
content: "Draft-only content",
|
||||||
|
language: "en"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert post.status == :draft
|
||||||
|
assert {:ok, _server} = BDS.Preview.start_preview(project.id)
|
||||||
|
|
||||||
|
datetime = DateTime.from_unix!(post.created_at, :millisecond)
|
||||||
|
y = Integer.to_string(datetime.year)
|
||||||
|
m = String.pad_leading(Integer.to_string(datetime.month), 2, "0")
|
||||||
|
d = String.pad_leading(Integer.to_string(datetime.day), 2, "0")
|
||||||
|
|
||||||
|
assert {:ok, %{body: html, content_type: "text/html"}} =
|
||||||
|
BDS.Preview.request(project.id, "/#{y}/#{m}/#{d}/#{post.slug}")
|
||||||
|
|
||||||
|
assert html =~ "Pure Draft"
|
||||||
|
assert html =~ "Draft-only content"
|
||||||
|
|
||||||
|
assert :ok = BDS.Preview.stop_preview(project.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "on-demand rendering: published-then-edited post shows draft DB content over file", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
|
assert {:ok, _metadata} =
|
||||||
|
Metadata.update_project_metadata(project.id, %{
|
||||||
|
main_language: "en",
|
||||||
|
blog_languages: ["en"]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, post} =
|
||||||
|
Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Published Then Edited",
|
||||||
|
content: "Original content",
|
||||||
|
language: "en"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, published} = Posts.publish_post(post.id)
|
||||||
|
|
||||||
|
{:ok, edited} =
|
||||||
|
Posts.update_post(published.id, %{
|
||||||
|
title: "Edited Title",
|
||||||
|
content: "Edited draft content"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, _server} = BDS.Preview.start_preview(project.id)
|
||||||
|
|
||||||
|
datetime = DateTime.from_unix!(edited.created_at, :millisecond)
|
||||||
|
y = Integer.to_string(datetime.year)
|
||||||
|
m = String.pad_leading(Integer.to_string(datetime.month), 2, "0")
|
||||||
|
d = String.pad_leading(Integer.to_string(datetime.day), 2, "0")
|
||||||
|
|
||||||
|
assert {:ok, %{body: html, content_type: "text/html"}} =
|
||||||
|
BDS.Preview.request(project.id, "/#{y}/#{m}/#{d}/#{edited.slug}")
|
||||||
|
|
||||||
|
assert html =~ "Edited Title"
|
||||||
|
assert html =~ "Edited draft content"
|
||||||
|
refute html =~ "Original content"
|
||||||
|
|
||||||
|
assert :ok = BDS.Preview.stop_preview(project.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "on-demand rendering: draft posts appear in home listing", %{project: project} do
|
||||||
|
assert {:ok, _metadata} =
|
||||||
|
Metadata.update_project_metadata(project.id, %{
|
||||||
|
main_language: "en",
|
||||||
|
blog_languages: ["en"]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, _draft} =
|
||||||
|
Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Draft In List",
|
||||||
|
content: "Draft list body",
|
||||||
|
language: "en"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, _server} = BDS.Preview.start_preview(project.id)
|
||||||
|
|
||||||
|
assert {:ok, %{body: html, content_type: "text/html"}} =
|
||||||
|
BDS.Preview.request(project.id, "/")
|
||||||
|
|
||||||
|
assert html =~ "Draft In List"
|
||||||
|
|
||||||
|
assert :ok = BDS.Preview.stop_preview(project.id)
|
||||||
|
end
|
||||||
|
|
||||||
test "preview query params can override the rendered theme for generated and draft pages", %{
|
test "preview query params can override the rendered theme for generated and draft pages", %{
|
||||||
project: project
|
project: project
|
||||||
} do
|
} do
|
||||||
|
|||||||
215
test/bds/template_lookup_priority_test.exs
Normal file
215
test/bds/template_lookup_priority_test.exs
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
defmodule BDS.TemplateLookupPriorityTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
alias BDS.Rendering.TemplateSelection
|
||||||
|
|
||||||
|
setup do
|
||||||
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
|
temp_dir = Path.join(System.tmp_dir!(), "bds-tlp-#{System.unique_integer([:positive])}")
|
||||||
|
File.mkdir_p!(temp_dir)
|
||||||
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
|
{:ok, project} = BDS.Projects.create_project(%{name: "TLP Test", data_path: temp_dir})
|
||||||
|
|
||||||
|
{:ok, _metadata} =
|
||||||
|
BDS.Metadata.update_project_metadata(project.id, %{
|
||||||
|
main_language: "en",
|
||||||
|
blog_languages: ["en"]
|
||||||
|
})
|
||||||
|
|
||||||
|
%{project: project}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_published_template(project_id, slug, content) do
|
||||||
|
{:ok, template} =
|
||||||
|
BDS.Templates.create_template(%{
|
||||||
|
project_id: project_id,
|
||||||
|
title: "Template #{slug}",
|
||||||
|
kind: :post,
|
||||||
|
content: content
|
||||||
|
})
|
||||||
|
|
||||||
|
if template.slug != slug do
|
||||||
|
BDS.Repo.update!(Ecto.Changeset.change(template, slug: slug))
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, published} = BDS.Templates.publish_template(template.id)
|
||||||
|
published
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_tag(project_id, name, post_template_slug) do
|
||||||
|
{:ok, tag} =
|
||||||
|
BDS.Tags.create_tag(%{
|
||||||
|
project_id: project_id,
|
||||||
|
name: name,
|
||||||
|
post_template_slug: post_template_slug
|
||||||
|
})
|
||||||
|
|
||||||
|
tag
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "resolve_post_template_slug/3" do
|
||||||
|
test "level 1: returns post's own template_slug when set", %{project: project} do
|
||||||
|
result =
|
||||||
|
TemplateSelection.resolve_post_template_slug(
|
||||||
|
project.id,
|
||||||
|
["some-tag"],
|
||||||
|
["article"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "level 2: falls back to tag's post_template_slug", %{project: project} do
|
||||||
|
_tag = create_tag(project.id, "photo", "photo-layout")
|
||||||
|
|
||||||
|
result =
|
||||||
|
TemplateSelection.resolve_post_template_slug(
|
||||||
|
project.id,
|
||||||
|
["photo"],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == "photo-layout"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "level 2: uses first matching tag with a template slug", %{project: project} do
|
||||||
|
_tag1 = create_tag(project.id, "news", nil)
|
||||||
|
_tag2 = create_tag(project.id, "gallery", "gallery-layout")
|
||||||
|
|
||||||
|
result =
|
||||||
|
TemplateSelection.resolve_post_template_slug(
|
||||||
|
project.id,
|
||||||
|
["news", "gallery"],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == "gallery-layout"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "level 3: falls back to category's post_template_slug", %{project: project} do
|
||||||
|
{:ok, _} = BDS.Metadata.add_category(project.id, "review")
|
||||||
|
|
||||||
|
{:ok, _} =
|
||||||
|
BDS.Metadata.update_category_settings(project.id, "review", %{
|
||||||
|
"post_template_slug" => "review-layout"
|
||||||
|
})
|
||||||
|
|
||||||
|
result =
|
||||||
|
TemplateSelection.resolve_post_template_slug(
|
||||||
|
project.id,
|
||||||
|
[],
|
||||||
|
["review"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == "review-layout"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "level 2 takes priority over level 3", %{project: project} do
|
||||||
|
_tag = create_tag(project.id, "featured", "featured-layout")
|
||||||
|
{:ok, _} = BDS.Metadata.add_category(project.id, "review")
|
||||||
|
|
||||||
|
{:ok, _} =
|
||||||
|
BDS.Metadata.update_category_settings(project.id, "review", %{
|
||||||
|
"post_template_slug" => "review-layout"
|
||||||
|
})
|
||||||
|
|
||||||
|
result =
|
||||||
|
TemplateSelection.resolve_post_template_slug(
|
||||||
|
project.id,
|
||||||
|
["featured"],
|
||||||
|
["review"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == "featured-layout"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "level 4: returns nil when no tag or category has a template", %{project: project} do
|
||||||
|
_tag = create_tag(project.id, "plain", nil)
|
||||||
|
|
||||||
|
result =
|
||||||
|
TemplateSelection.resolve_post_template_slug(
|
||||||
|
project.id,
|
||||||
|
["plain"],
|
||||||
|
["article"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "end-to-end template lookup with rendering" do
|
||||||
|
test "post renders with tag-specific template when no post template set", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
|
template =
|
||||||
|
create_published_template(project.id, "photo-layout", "<h1>PHOTO: {{ page.title }}</h1>")
|
||||||
|
|
||||||
|
_tag = create_tag(project.id, "photo", template.slug)
|
||||||
|
|
||||||
|
{:ok, post} =
|
||||||
|
BDS.Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "My Photo Post",
|
||||||
|
content: "photo content",
|
||||||
|
language: "en",
|
||||||
|
tags: ["photo"]
|
||||||
|
})
|
||||||
|
|
||||||
|
resolved_slug =
|
||||||
|
TemplateSelection.resolve_post_template_slug(
|
||||||
|
project.id,
|
||||||
|
post.tags,
|
||||||
|
post.categories
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resolved_slug == "photo-layout"
|
||||||
|
|
||||||
|
{:ok, rendered} =
|
||||||
|
TemplateSelection.load_template_source(project.id, :post, resolved_slug)
|
||||||
|
|
||||||
|
assert rendered =~ "PHOTO:"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "post renders with category-specific template when no post or tag template", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
|
template =
|
||||||
|
create_published_template(
|
||||||
|
project.id,
|
||||||
|
"review-layout",
|
||||||
|
"<h1>REVIEW: {{ page.title }}</h1>"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, _} = BDS.Metadata.add_category(project.id, "review")
|
||||||
|
|
||||||
|
{:ok, _} =
|
||||||
|
BDS.Metadata.update_category_settings(project.id, "review", %{
|
||||||
|
"post_template_slug" => template.slug
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, post} =
|
||||||
|
BDS.Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "My Review",
|
||||||
|
content: "review content",
|
||||||
|
language: "en",
|
||||||
|
categories: ["review"]
|
||||||
|
})
|
||||||
|
|
||||||
|
resolved_slug =
|
||||||
|
TemplateSelection.resolve_post_template_slug(
|
||||||
|
project.id,
|
||||||
|
post.tags,
|
||||||
|
post.categories
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resolved_slug == "review-layout"
|
||||||
|
|
||||||
|
{:ok, rendered} =
|
||||||
|
TemplateSelection.load_template_source(project.id, :post, resolved_slug)
|
||||||
|
|
||||||
|
assert rendered =~ "REVIEW:"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user