domma-cms 1.6.60 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4349 -0
- package/README.md +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +2 -2
- package/admin/js/lib/mail-compose.js +1 -0
- package/admin/js/lib/page-editor-reference.js +5 -5
- package/admin/js/lib/shortcode-tags.js +1 -1
- package/admin/js/templates/call-new.html +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +39 -0
- package/admin/js/views/call-new.js +1 -1
- package/admin/js/views/contacts.js +15 -15
- package/admin/js/views/form-submissions.js +2 -2
- package/docs/DommaFeatures.md +3005 -0
- package/docs/MultiTenant.md +176 -0
- package/docs/api-reference.md +1667 -0
- package/docs/cache.md +96 -0
- package/docs/configuration.md +1811 -0
- package/docs/editions-and-licences.md +190 -0
- package/docs/getting-started.md +201 -0
- package/docs/guides/form-triggers.md +260 -0
- package/docs/guides/mongodb-storage-adapter.md +282 -0
- package/docs/guides/multi-page-forms.md +121 -0
- package/docs/images/scribe-composer.png +0 -0
- package/docs/markdown-shortcodes.md +1738 -0
- package/docs/pentest-report-2026-04-03.md +181 -0
- package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
- package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
- package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
- package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
- package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
- package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
- package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
- package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
- package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
- package/docs/plans/2026-03-05-user-types-design.md +139 -0
- package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
- package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
- package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
- package/docs/plans/2026-06-10-core-project-design.md +240 -0
- package/docs/plugin-development.md +827 -0
- package/docs/recruitment-recipe.md +501 -0
- package/docs/scaffolding.md +312 -0
- package/docs/screenshots/calculator-floating.png +0 -0
- package/docs/screenshots/calculator-standard.png +0 -0
- package/docs/screenshots/email-settings.png +0 -0
- package/docs/screenshots/messages-count.png +0 -0
- package/docs/structure/user-types.md +107 -0
- package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
- package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
- package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
- package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
- package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
- package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
- package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
- package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
- package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
- package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
- package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
- package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
- package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
- package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
- package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
- package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
- package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
- package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
- package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
- package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
- package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
- package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
- package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
- package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
- package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
- package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
- package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
- package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
- package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
- package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
- package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
- package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
- package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
- package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
- package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
- package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
- package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
- package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
- package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
- package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
- package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
- package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
- package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
- package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
- package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
- package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
- package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
- package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
- package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
- package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
- package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
- package/docs/theming.md +320 -0
- package/package.json +2 -2
- package/plugins/_lib/admin/mail/compose-window.js +3 -1
- package/plugins/_lib/admin/mail/identity.js +18 -3
- package/plugins/_lib/admin/mail/reader-view.js +32 -3
- package/plugins/blog/CLAUDE.md +4 -0
- package/plugins/blog/admin/lib/email.js +191 -0
- package/plugins/blog/admin/views/comments.js +3 -1
- package/plugins/blog/plugin.json +2 -2
- package/plugins/blog/plugin.public.js +1 -1
- package/plugins/blog/tests/email.test.js +69 -0
- package/plugins/booking/CLAUDE.md +4 -0
- package/plugins/booking/admin/lib/email.js +191 -0
- package/plugins/booking/admin/views/panels.js +7 -3
- package/plugins/booking/plugin.json +1 -1
- package/plugins/booking/tests/email.test.js +69 -0
- package/plugins/free-tier.lock.json +27 -19
- package/plugins/shopping-cart/CLAUDE.md +4 -0
- package/plugins/shopping-cart/admin/lib/email.js +191 -0
- package/plugins/shopping-cart/admin/views/orders.js +11 -3
- package/plugins/shopping-cart/plugin.json +2 -2
- package/plugins/shopping-cart/tests/email.test.js +69 -0
- package/plugins/support/CLAUDE.md +4 -0
- package/plugins/support/admin/lib/email.js +191 -0
- package/plugins/support/admin/views/desk.js +4 -1
- package/plugins/support/admin/views/inbox.js +6 -2
- package/plugins/support/plugin.json +2 -2
- package/plugins/support/tests/email.test.js +69 -0
- package/public/js/celebrations/core/canvas.js +1 -1
- package/public/js/celebrations/index.js +1 -1
- package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
- package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
- package/public/js/celebrations/message-attrs.mjs +1 -0
- package/public/js/celebrations/themes/anniversary.js +1 -0
- package/public/js/celebrations/themes/autumn.js +1 -0
- package/public/js/celebrations/themes/birthday.js +1 -0
- package/public/js/celebrations/themes/butterfly-flight.js +1 -0
- package/public/js/celebrations/themes/calendar.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year.js +1 -0
- package/public/js/celebrations/themes/christmas.js +1 -1
- package/public/js/celebrations/themes/easter.js +1 -0
- package/public/js/celebrations/themes/garden-drawing.js +1 -0
- package/public/js/celebrations/themes/garden.js +1 -0
- package/public/js/celebrations/themes/graduation.js +1 -0
- package/public/js/celebrations/themes/greeting.js +1 -0
- package/public/js/celebrations/themes/make-it-rain.js +1 -0
- package/public/js/celebrations/themes/money-rain.js +1 -0
- package/public/js/celebrations/themes/new-year.js +1 -0
- package/public/js/celebrations/themes/occasion-drawing.js +1 -0
- package/public/js/celebrations/themes/occasions.js +1 -0
- package/public/js/celebrations/themes/rain-control.js +1 -0
- package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
- package/public/js/celebrations/themes/sparkler-font.js +1 -0
- package/public/js/celebrations/themes/sparkler-writing.js +1 -0
- package/public/js/celebrations/themes/summer-garden.js +1 -0
- package/public/js/celebrations/themes/winter.js +1 -0
- package/public/js/celebrations/trait-data.mjs +1 -1
- package/public/js/effects.js +1 -1
- package/scripts/build.js +4 -0
- package/server/services/markdown.js +5 -2
- package/server/templates/page.html +1 -1
|
@@ -0,0 +1,1970 @@
|
|
|
1
|
+
# Card Builder Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:
|
|
4
|
+
> executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
5
|
+
|
|
6
|
+
**Goal:** Add an interactive card builder dialog to the page editor and expand the `[card]` shortcode to support 33
|
|
7
|
+
layouts, 6 style variants, and 14 gradient presets.
|
|
8
|
+
|
|
9
|
+
**Architecture:** A self-contained `card-builder.js` admin lib builds a custom DOM overlay (appended to `document.body`,
|
|
10
|
+
no E.modal), wired into the existing `toolbar.onCard()` hook in `page-editor.js`. Server-side, `processCardBlocks` in
|
|
11
|
+
`markdown.js` is refactored to route via a `LAYOUT_RENDERERS` map - existing shortcodes fall through to the unchanged
|
|
12
|
+
legacy renderer. CSS variant and layout classes are added to `site.css`; collapsible click wiring is added to `site.js`.
|
|
13
|
+
|
|
14
|
+
**Tech Stack:** Domma SPA (ESM, no bundler), Fastify 5 SSR, vanilla DOM APIs, CSS custom properties. No test framework -
|
|
15
|
+
curl integration tests against the running server on port 4096.
|
|
16
|
+
|
|
17
|
+
**Spec:** `docs/superpowers/specs/2026-04-05-card-builder-design.md`
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## File Map
|
|
22
|
+
|
|
23
|
+
| File | Action | Responsibility |
|
|
24
|
+
|---------------------------------|------------|---------------------------------------------------------------------------|
|
|
25
|
+
| `public/css/site.css` | Modify | 6 variant classes, 14 gradient classes, layout structural CSS |
|
|
26
|
+
| `public/js/site.js` | Modify | Collapsible card click delegation |
|
|
27
|
+
| `server/services/markdown.js` | Modify | `processCardBlocks` routing + 33 layout renderers |
|
|
28
|
+
| `admin/js/lib/card-builder.js` | **Create** | Builder dialog - state, fields, preview, icon picker, shortcode generator |
|
|
29
|
+
| `admin/js/views/page-editor.js` | Modify | Wire `toolbar.onCard()` + `onMediaBrowse` callback |
|
|
30
|
+
|
|
31
|
+
No new routes. No schema changes. No new npm packages.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Task 1: CSS - 6 variant classes + utilities
|
|
36
|
+
|
|
37
|
+
**Files:**
|
|
38
|
+
|
|
39
|
+
- Modify: `public/css/site.css`
|
|
40
|
+
|
|
41
|
+
- [ ] **Step 1: Add the 6 variant base + hover rules after the existing `.page-body .card` block**
|
|
42
|
+
|
|
43
|
+
```css
|
|
44
|
+
/* ── Card style variants ── */
|
|
45
|
+
.dm-card-clean {
|
|
46
|
+
background: #fff;
|
|
47
|
+
border: 1px solid #e2e8f0;
|
|
48
|
+
box-shadow: 0 1px 4px rgba(0,0,0,.06), 0 2px 8px rgba(0,0,0,.04);
|
|
49
|
+
transition: transform .2s ease, box-shadow .2s ease;
|
|
50
|
+
}
|
|
51
|
+
.dm-card-clean:hover {
|
|
52
|
+
transform: translateY(-4px);
|
|
53
|
+
box-shadow: 0 8px 24px rgba(0,0,0,.10), 0 2px 8px rgba(0,0,0,.06);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.dm-card-gradient {
|
|
57
|
+
background: #fff;
|
|
58
|
+
border: none;
|
|
59
|
+
box-shadow: 0 4px 20px rgba(99,102,241,.14);
|
|
60
|
+
transition: transform .2s ease, box-shadow .2s ease;
|
|
61
|
+
}
|
|
62
|
+
.dm-card-gradient:hover {
|
|
63
|
+
transform: translateY(-5px);
|
|
64
|
+
box-shadow: 0 16px 40px rgba(99,102,241,.25);
|
|
65
|
+
}
|
|
66
|
+
.dm-card-gradient .card-header {
|
|
67
|
+
background: linear-gradient(135deg, var(--dm-card-g-start, #6366f1), var(--dm-card-g-end, #8b5cf6));
|
|
68
|
+
color: #fff;
|
|
69
|
+
border-bottom: none;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.dm-card-glass {
|
|
73
|
+
background: rgba(255,255,255,.18);
|
|
74
|
+
backdrop-filter: blur(14px);
|
|
75
|
+
-webkit-backdrop-filter: blur(14px);
|
|
76
|
+
border: 1px solid rgba(255,255,255,.38);
|
|
77
|
+
box-shadow: 0 4px 24px rgba(0,0,0,.08);
|
|
78
|
+
transition: transform .2s ease, box-shadow .2s ease, background .2s ease;
|
|
79
|
+
}
|
|
80
|
+
.dm-card-glass:hover {
|
|
81
|
+
transform: translateY(-4px);
|
|
82
|
+
box-shadow: 0 12px 36px rgba(0,0,0,.14);
|
|
83
|
+
background: rgba(255,255,255,.26);
|
|
84
|
+
}
|
|
85
|
+
.dm-card-glass .card-header { border-bottom: 1px solid rgba(255,255,255,.25); }
|
|
86
|
+
|
|
87
|
+
.dm-card-accent {
|
|
88
|
+
background: #fff;
|
|
89
|
+
border: 1px solid #e2e8f0;
|
|
90
|
+
border-left: 4px solid #6366f1;
|
|
91
|
+
box-shadow: 0 1px 4px rgba(0,0,0,.05);
|
|
92
|
+
transition: transform .2s ease, box-shadow .2s ease, border-left-color .2s ease;
|
|
93
|
+
}
|
|
94
|
+
.dm-card-accent:hover {
|
|
95
|
+
transform: translateY(-3px);
|
|
96
|
+
border-left-color: #4f46e5;
|
|
97
|
+
box-shadow: 0 8px 24px rgba(99,102,241,.12);
|
|
98
|
+
}
|
|
99
|
+
.dm-card-accent .card-header { color: #4338ca; border-bottom: none; }
|
|
100
|
+
|
|
101
|
+
.dm-card-dark {
|
|
102
|
+
background: linear-gradient(160deg, #1e293b 0%, #0f172a 100%);
|
|
103
|
+
border: 1px solid rgba(255,255,255,.06);
|
|
104
|
+
box-shadow: 0 4px 20px rgba(0,0,0,.30);
|
|
105
|
+
transition: transform .2s ease, box-shadow .2s ease;
|
|
106
|
+
}
|
|
107
|
+
.dm-card-dark:hover {
|
|
108
|
+
transform: translateY(-4px) scale(1.012);
|
|
109
|
+
box-shadow: 0 16px 40px rgba(0,0,0,.45);
|
|
110
|
+
}
|
|
111
|
+
.dm-card-dark .card-header { color: #f1f5f9; border-bottom: 1px solid rgba(255,255,255,.08); }
|
|
112
|
+
.dm-card-dark .card-body { color: #94a3b8; }
|
|
113
|
+
.dm-card-dark .card-footer { color: #475569; border-top: 1px solid rgba(255,255,255,.06); }
|
|
114
|
+
|
|
115
|
+
.dm-card-glow {
|
|
116
|
+
background: #fff;
|
|
117
|
+
border: 1px solid #a5b4fc;
|
|
118
|
+
box-shadow: 0 0 0 0 rgba(99,102,241,0);
|
|
119
|
+
transition: transform .2s ease, box-shadow .3s ease, border-color .2s ease;
|
|
120
|
+
}
|
|
121
|
+
.dm-card-glow:hover {
|
|
122
|
+
transform: translateY(-3px);
|
|
123
|
+
border-color: #818cf8;
|
|
124
|
+
box-shadow: 0 0 0 4px rgba(99,102,241,.14), 0 0 28px rgba(99,102,241,.22);
|
|
125
|
+
}
|
|
126
|
+
.dm-card-glow .card-header { color: #4338ca; border-bottom: 1px solid #e0e7ff; }
|
|
127
|
+
.dm-card-glow .card-footer { border-top: 1px solid #e0e7ff; color: #818cf8; }
|
|
128
|
+
|
|
129
|
+
/* opt-in lift for layouts without a variant */
|
|
130
|
+
.card-hover { transition: transform .2s ease, box-shadow .2s ease; }
|
|
131
|
+
.card-hover:hover { transform: translateY(-3px); box-shadow: 0 8px 20px rgba(0,0,0,.10); }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- [ ] **Step 2: Create test page `content/pages/card-variants-test.md`**
|
|
135
|
+
|
|
136
|
+
```markdown
|
|
137
|
+
---
|
|
138
|
+
title: Card Variant Test
|
|
139
|
+
layout: default
|
|
140
|
+
---
|
|
141
|
+
[card variant="clean" title="Clean" hover]Body.[/card]
|
|
142
|
+
[card variant="gradient" title="Gradient"]Body.[/card]
|
|
143
|
+
[card variant="accent" title="Accent"]Body.[/card]
|
|
144
|
+
[card variant="dark" title="Dark"]Body.[/card]
|
|
145
|
+
[card variant="glow" title="Glow"]Body.[/card]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- [ ] **Step 3: Verify**
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
curl -s http://localhost:4096/card-variants-test | grep -c 'dm-card-'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Expected: `5`
|
|
155
|
+
|
|
156
|
+
- [ ] **Step 4: Delete test page, commit**
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
rm content/pages/card-variants-test.md
|
|
160
|
+
git add public/css/site.css
|
|
161
|
+
git commit -m "feat(css): add 6 card variant classes with hover effects"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Task 2: CSS - 14 gradient preset classes
|
|
167
|
+
|
|
168
|
+
**Files:**
|
|
169
|
+
|
|
170
|
+
- Modify: `public/css/site.css`
|
|
171
|
+
|
|
172
|
+
- [ ] **Step 1: Add gradient classes after the variant block**
|
|
173
|
+
|
|
174
|
+
```css
|
|
175
|
+
/* ── Gradient presets ── */
|
|
176
|
+
/* Apply as class on .card-header, .card-badge-band, .card-step-bg, .card-split-left */
|
|
177
|
+
.card-gradient-indigo { background: linear-gradient(135deg,#6366f1,#8b5cf6); }
|
|
178
|
+
.card-gradient-ocean { background: linear-gradient(135deg,#0891b2,#2563eb); }
|
|
179
|
+
.card-gradient-sunset { background: linear-gradient(135deg,#f59e0b,#ef4444,#ec4899); }
|
|
180
|
+
.card-gradient-forest { background: linear-gradient(135deg,#10b981,#0d9488); }
|
|
181
|
+
.card-gradient-rose { background: linear-gradient(135deg,#fb7185,#e11d48); }
|
|
182
|
+
.card-gradient-midnight { background: linear-gradient(135deg,#1e1b4b,#4338ca); }
|
|
183
|
+
.card-gradient-aurora { background: linear-gradient(135deg,#06b6d4,#6366f1,#a855f7); }
|
|
184
|
+
.card-gradient-fire { background: linear-gradient(135deg,#ef4444,#f97316,#fbbf24); }
|
|
185
|
+
.card-gradient-lagoon { background: linear-gradient(135deg,#06b6d4,#0e7490); }
|
|
186
|
+
.card-gradient-dusk { background: linear-gradient(135deg,#7c3aed,#db2777); }
|
|
187
|
+
.card-gradient-lime { background: linear-gradient(135deg,#84cc16,#10b981); }
|
|
188
|
+
.card-gradient-gold { background: linear-gradient(135deg,#f59e0b,#b45309); }
|
|
189
|
+
.card-gradient-arctic { background: linear-gradient(135deg,#bae6fd,#e0f2fe); color: #1e293b; }
|
|
190
|
+
.card-gradient-slate { background: linear-gradient(135deg,#475569,#1e293b); }
|
|
191
|
+
/* All gradient elements get white text except arctic */
|
|
192
|
+
[class*="card-gradient-"]:not(.card-gradient-arctic) { color: #fff; }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- [ ] **Step 2: Commit**
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
git add public/css/site.css
|
|
199
|
+
git commit -m "feat(css): add 14 gradient preset classes"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Task 3: CSS - Layout structural CSS
|
|
205
|
+
|
|
206
|
+
**Files:**
|
|
207
|
+
|
|
208
|
+
- Modify: `public/css/site.css`
|
|
209
|
+
|
|
210
|
+
- [ ] **Step 1: Add all layout structural rules**
|
|
211
|
+
|
|
212
|
+
```css
|
|
213
|
+
/* ── Card layout structural CSS ── */
|
|
214
|
+
|
|
215
|
+
/* Image containers */
|
|
216
|
+
.card-img-top {
|
|
217
|
+
width:100%; height:180px; background-size:cover; background-position:center;
|
|
218
|
+
background-color:#e2e8f0; display:flex; align-items:center;
|
|
219
|
+
justify-content:center; font-size:2.5rem; color:rgba(0,0,0,.15);
|
|
220
|
+
}
|
|
221
|
+
.card-img-left, .card-img-right {
|
|
222
|
+
width:90px; flex-shrink:0; background-size:cover; background-position:center;
|
|
223
|
+
background-color:#e2e8f0; display:flex; align-items:center;
|
|
224
|
+
justify-content:center; font-size:1.75rem; color:rgba(0,0,0,.2);
|
|
225
|
+
}
|
|
226
|
+
.card-img-wide {
|
|
227
|
+
width:130px; flex-shrink:0; background-size:cover; background-position:center;
|
|
228
|
+
background-color:#e2e8f0; display:flex; align-items:center;
|
|
229
|
+
justify-content:center; font-size:2rem; color:rgba(0,0,0,.2);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* Horizontal shells */
|
|
233
|
+
.card-layout-horizontal,
|
|
234
|
+
.card-layout-thumb-left,
|
|
235
|
+
.card-layout-thumb-right { display:flex; }
|
|
236
|
+
.card-layout-thumb-left .card-body,
|
|
237
|
+
.card-layout-thumb-right .card-body,
|
|
238
|
+
.card-layout-horizontal .card-body { flex:1; min-width:0; }
|
|
239
|
+
|
|
240
|
+
/* Split */
|
|
241
|
+
.card-layout-split { display:flex; min-height:140px; }
|
|
242
|
+
.card-split-left { width:50%; display:flex; align-items:center; justify-content:center; font-size:2.25rem; flex-shrink:0; background:linear-gradient(160deg,#6366f1,#8b5cf6); }
|
|
243
|
+
.card-split-right { flex:1; background:#1e293b; display:flex; flex-direction:column; justify-content:center; }
|
|
244
|
+
.card-split-right .card-body { color:#94a3b8; }
|
|
245
|
+
.card-split-right .card-title { color:#f1f5f9; }
|
|
246
|
+
|
|
247
|
+
/* Image overlay */
|
|
248
|
+
.card-img-overlay { position:relative; height:180px; overflow:hidden; background-size:cover; background-position:center; background-color:#1e293b; display:flex; align-items:flex-end; }
|
|
249
|
+
.card-overlay-text { width:100%; padding:40px 16px 14px; background:linear-gradient(to top,rgba(0,0,0,.75),transparent); }
|
|
250
|
+
.card-overlay-text .card-title { color:#fff; }
|
|
251
|
+
|
|
252
|
+
/* Dark variant on image containers */
|
|
253
|
+
.dm-card-dark .card-img-top,
|
|
254
|
+
.dm-card-dark .card-img-left,
|
|
255
|
+
.dm-card-dark .card-img-right,
|
|
256
|
+
.dm-card-dark .card-img-wide { filter:brightness(.7); }
|
|
257
|
+
|
|
258
|
+
/* Stat */
|
|
259
|
+
.card-stat-value { font-size:2.4rem; font-weight:800; color:#111827; line-height:1.1; }
|
|
260
|
+
.card-stat-delta { font-size:.8rem; font-weight:600; margin-top:2px; }
|
|
261
|
+
.card-stat-delta.positive { color:#10b981; }
|
|
262
|
+
.card-stat-delta.negative { color:#ef4444; }
|
|
263
|
+
.card-stat-bar { height:5px; background:#f1f5f9; border-radius:3px; margin-top:14px; overflow:hidden; }
|
|
264
|
+
.card-stat-fill { height:100%; border-radius:3px; background:linear-gradient(90deg,#6366f1,#8b5cf6); }
|
|
265
|
+
|
|
266
|
+
/* Progress */
|
|
267
|
+
.card-progress-bar { height:8px; background:#f1f5f9; border-radius:4px; overflow:hidden; margin:8px 0; }
|
|
268
|
+
.card-progress-fill { height:100%; border-radius:4px; background:linear-gradient(90deg,#6366f1,#8b5cf6); }
|
|
269
|
+
.card-milestone { display:flex; align-items:center; gap:8px; font-size:.8rem; color:#4b5563; padding:3px 0; }
|
|
270
|
+
.card-milestone-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
|
|
271
|
+
.card-milestone-dot.done { background:#10b981; }
|
|
272
|
+
.card-milestone-dot.pending { background:#e5e7eb; }
|
|
273
|
+
|
|
274
|
+
/* Icon stacked header */
|
|
275
|
+
.card-header-icon-stacked { text-align:center; padding:20px 16px 12px; }
|
|
276
|
+
.card-header-icon-stacked [data-icon],
|
|
277
|
+
.card-header-icon-stacked .card-icon { font-size:2rem; display:block; margin-bottom:8px; }
|
|
278
|
+
|
|
279
|
+
/* Step numbered */
|
|
280
|
+
.card-step-bg { position:relative; overflow:hidden; min-height:60px; display:flex; align-items:center; padding:14px 16px; }
|
|
281
|
+
.card-step-ghost { position:absolute; right:8px; top:-10px; font-size:5rem; font-weight:900; color:rgba(99,102,241,.12); line-height:1; pointer-events:none; }
|
|
282
|
+
.card-step-badge { width:32px; height:32px; border-radius:50%; background:#6366f1; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:.85rem; z-index:1; flex-shrink:0; }
|
|
283
|
+
|
|
284
|
+
/* Corner badge */
|
|
285
|
+
.card-corner-badge-wrap { position:relative; }
|
|
286
|
+
.card-corner-badge { position:absolute; top:10px; right:10px; background:#ef4444; color:#fff; font-size:.65rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; padding:2px 8px; border-radius:20px; }
|
|
287
|
+
|
|
288
|
+
/* Callout */
|
|
289
|
+
.card-callout { border-left:4px solid #6366f1; }
|
|
290
|
+
.card-callout.warn { border-left-color:#f59e0b; }
|
|
291
|
+
.card-callout.success { border-left-color:#10b981; }
|
|
292
|
+
.card-callout.error { border-left-color:#ef4444; }
|
|
293
|
+
.card-callout-inner { display:flex; gap:12px; align-items:flex-start; }
|
|
294
|
+
.card-callout-icon { font-size:1.2rem; flex-shrink:0; margin-top:1px; }
|
|
295
|
+
|
|
296
|
+
/* Quote / testimonial */
|
|
297
|
+
.card-quote-mark { font-size:3.5rem; color:#ede9fe; line-height:.8; padding:14px 16px 0; font-family:Georgia,serif; display:block; }
|
|
298
|
+
.card-quote-text { padding:4px 16px 14px; font-style:italic; color:#374151; line-height:1.7; }
|
|
299
|
+
.card-quote-attr { display:flex; align-items:center; gap:10px; }
|
|
300
|
+
.card-quote-avatar { width:32px; height:32px; border-radius:50%; background:linear-gradient(135deg,#6366f1,#8b5cf6); display:flex; align-items:center; justify-content:center; font-size:.85rem; color:#fff; flex-shrink:0; }
|
|
301
|
+
|
|
302
|
+
/* Video */
|
|
303
|
+
.card-video-thumb { position:relative; height:160px; background:#1e293b; display:flex; align-items:center; justify-content:center; background-size:cover; background-position:center; }
|
|
304
|
+
.card-play-btn { width:52px; height:52px; border-radius:50%; background:rgba(255,255,255,.9); display:flex; align-items:center; justify-content:center; font-size:1.2rem; box-shadow:0 4px 16px rgba(0,0,0,.3); }
|
|
305
|
+
.card-video-duration { position:absolute; bottom:8px; right:10px; background:rgba(0,0,0,.7); color:#fff; font-size:.65rem; font-weight:600; padding:2px 7px; border-radius:4px; }
|
|
306
|
+
|
|
307
|
+
/* Map */
|
|
308
|
+
.card-map-placeholder { height:130px; background:linear-gradient(160deg,#bfdbfe,#dbeafe); position:relative; display:flex; align-items:center; justify-content:center; overflow:hidden; }
|
|
309
|
+
.card-map-grid { position:absolute; inset:0; background-image:repeating-linear-gradient(0deg,rgba(59,130,246,.08) 0,rgba(59,130,246,.08) 1px,transparent 1px,transparent 32px),repeating-linear-gradient(90deg,rgba(59,130,246,.08) 0,rgba(59,130,246,.08) 1px,transparent 1px,transparent 32px); }
|
|
310
|
+
.card-map-pin { font-size:2rem; z-index:1; }
|
|
311
|
+
|
|
312
|
+
/* Avatar */
|
|
313
|
+
.card-avatar { width:64px; height:64px; border-radius:50%; background:linear-gradient(135deg,#6366f1,#8b5cf6); display:flex; align-items:center; justify-content:center; font-size:1.8rem; color:#fff; margin:0 auto 12px; }
|
|
314
|
+
.card-avatar-wrap { text-align:center; padding:24px 16px 16px; }
|
|
315
|
+
.card-tag-pills { display:flex; flex-wrap:wrap; gap:6px; justify-content:center; margin-top:10px; }
|
|
316
|
+
.card-pill { background:#ede9fe; color:#7c3aed; font-size:.7rem; font-weight:700; padding:3px 9px; border-radius:20px; }
|
|
317
|
+
|
|
318
|
+
/* Tag cloud */
|
|
319
|
+
.card-tag-cloud { padding:14px 16px; display:flex; flex-wrap:wrap; gap:7px; }
|
|
320
|
+
.card-tag { padding:4px 11px; border-radius:20px; font-size:.78rem; font-weight:600; }
|
|
321
|
+
|
|
322
|
+
/* File document */
|
|
323
|
+
.card-file-row { display:flex; gap:14px; align-items:center; padding:16px; }
|
|
324
|
+
.card-file-icon { width:48px; height:58px; border-radius:7px; background:linear-gradient(160deg,#6366f1,#8b5cf6); display:flex; align-items:center; justify-content:center; font-size:1.4rem; color:#fff; flex-shrink:0; position:relative; }
|
|
325
|
+
.card-file-ext { position:absolute; bottom:4px; left:0; right:0; text-align:center; font-size:.55rem; font-weight:800; color:rgba(255,255,255,.9); letter-spacing:.05em; }
|
|
326
|
+
.card-file-dl { font-size:.78rem; font-weight:700; color:#6366f1; display:inline-block; margin-top:4px; }
|
|
327
|
+
|
|
328
|
+
/* Rating */
|
|
329
|
+
.card-stars { color:#f59e0b; letter-spacing:2px; padding:14px 16px 0; display:block; }
|
|
330
|
+
.card-verified { display:inline-block; background:#d1fae5; color:#065f46; font-size:.6rem; font-weight:700; padding:1px 5px; border-radius:3px; margin-left:6px; }
|
|
331
|
+
|
|
332
|
+
/* Activity feed */
|
|
333
|
+
.card-activity-item { display:flex; gap:10px; align-items:flex-start; padding:8px 14px; border-bottom:1px solid #f8fafc; }
|
|
334
|
+
.card-activity-item:last-child { border-bottom:none; }
|
|
335
|
+
.card-activity-avatar { width:30px; height:30px; border-radius:50%; flex-shrink:0; background:linear-gradient(135deg,#6366f1,#8b5cf6); display:flex; align-items:center; justify-content:center; font-size:.8rem; color:#fff; }
|
|
336
|
+
|
|
337
|
+
/* Before/After */
|
|
338
|
+
.card-compare-grid { display:grid; grid-template-columns:1fr 1fr; }
|
|
339
|
+
.card-compare-col { padding:12px 14px; }
|
|
340
|
+
.card-compare-col + .card-compare-col { border-left:1px solid #f1f5f9; }
|
|
341
|
+
.card-compare-label { font-size:.68rem; font-weight:700; text-transform:uppercase; letter-spacing:.07em; margin-bottom:8px; display:block; }
|
|
342
|
+
.card-compare-label.before { color:#ef4444; }
|
|
343
|
+
.card-compare-label.after { color:#10b981; }
|
|
344
|
+
.card-compare-item { display:flex; gap:7px; align-items:flex-start; font-size:.78rem; color:#4b5563; padding:3px 0; line-height:1.4; }
|
|
345
|
+
.card-compare-x { color:#ef4444; flex-shrink:0; }
|
|
346
|
+
.card-compare-check { color:#10b981; flex-shrink:0; }
|
|
347
|
+
|
|
348
|
+
/* Pricing */
|
|
349
|
+
.card-pricing-features { padding:14px 16px; }
|
|
350
|
+
.card-pricing-feature { display:flex; align-items:center; gap:8px; font-size:.82rem; color:#374151; padding:4px 0; }
|
|
351
|
+
.card-pricing-check { color:#10b981; }
|
|
352
|
+
.card-pricing-cta { display:block; text-align:center; background:#6366f1; color:#fff; font-size:.82rem; font-weight:700; padding:9px; border-radius:8px; text-decoration:none; margin:0 16px 14px; }
|
|
353
|
+
|
|
354
|
+
/* Timeline */
|
|
355
|
+
.card-timeline-row { display:flex; }
|
|
356
|
+
.card-timeline-side { width:44px; flex-shrink:0; display:flex; flex-direction:column; align-items:center; padding-top:14px; }
|
|
357
|
+
.card-timeline-dot { width:12px; height:12px; border-radius:50%; background:#6366f1; flex-shrink:0; }
|
|
358
|
+
.card-timeline-line { width:2px; background:#e5e7eb; flex:1; margin-top:4px; }
|
|
359
|
+
.card-timeline-body { flex:1; padding:12px 14px 12px 0; }
|
|
360
|
+
.card-timeline-date { font-size:.7rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:#9ca3af; }
|
|
361
|
+
.card-timeline-tag { display:inline-block; background:#d1fae5; color:#065f46; font-size:.65rem; font-weight:700; padding:2px 6px; border-radius:4px; margin-top:6px; }
|
|
362
|
+
|
|
363
|
+
/* Code snippet */
|
|
364
|
+
.card-code-header { background:#1e293b; display:flex; justify-content:space-between; align-items:center; padding:8px 14px; }
|
|
365
|
+
.card-code-lang { font-size:.68rem; font-weight:700; text-transform:uppercase; letter-spacing:.08em; color:#6366f1; }
|
|
366
|
+
.card-code-body { background:#0f172a; padding:14px; font-family:'Courier New',monospace; font-size:.78rem; line-height:1.8; color:#94a3b8; overflow-x:auto; }
|
|
367
|
+
|
|
368
|
+
/* Glass gradient border */
|
|
369
|
+
.card-glass-outer { background:linear-gradient(135deg,#6366f1,#8b5cf6,#ec4899); border-radius:12px; padding:2px; }
|
|
370
|
+
.card-glass-inner { background:rgba(255,255,255,.92); backdrop-filter:blur(10px); border-radius:10px; padding:16px; }
|
|
371
|
+
|
|
372
|
+
/* Badge band */
|
|
373
|
+
.card-badge-band { display:flex; align-items:center; justify-content:space-between; padding:12px 16px; }
|
|
374
|
+
.card-badge-band-label { background:rgba(255,255,255,.2); color:#fff; font-size:.65rem; font-weight:700; text-transform:uppercase; letter-spacing:.08em; padding:3px 8px; border-radius:4px; }
|
|
375
|
+
.card-badge-band-icon { font-size:1.4rem; }
|
|
376
|
+
|
|
377
|
+
/* Full bg */
|
|
378
|
+
.dm-card-full-bg { background:linear-gradient(160deg,#1e293b,#0f172a); }
|
|
379
|
+
.dm-card-full-bg .card-body { color:#94a3b8; }
|
|
380
|
+
.dm-card-full-bg .card-title { color:#f1f5f9; font-size:1rem; }
|
|
381
|
+
.dm-card-full-bg .card-footer { border-top-color:rgba(255,255,255,.08); color:#475569; }
|
|
382
|
+
|
|
383
|
+
/* Feature comparison */
|
|
384
|
+
.card-fc-row { display:flex; justify-content:space-between; align-items:center; padding:8px 16px; border-bottom:1px solid #f8fafc; font-size:.82rem; color:#374151; }
|
|
385
|
+
.card-fc-row:last-child { border-bottom:none; }
|
|
386
|
+
.card-fc-yes { color:#10b981; }
|
|
387
|
+
.card-fc-no { color:#d1d5db; }
|
|
388
|
+
|
|
389
|
+
/* Responsive: side-image layouts stack at 480px */
|
|
390
|
+
@media (max-width: 480px) {
|
|
391
|
+
.card-layout-thumb-left,
|
|
392
|
+
.card-layout-thumb-right,
|
|
393
|
+
.card-layout-horizontal { flex-direction:column; }
|
|
394
|
+
.card-img-left, .card-img-right, .card-img-wide { width:100%; height:120px; }
|
|
395
|
+
.card-layout-split { flex-direction:column; }
|
|
396
|
+
.card-split-left { width:100%; height:100px; }
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
- [ ] **Step 2: Commit**
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
git add public/css/site.css
|
|
404
|
+
git commit -m "feat(css): add card layout structural CSS for all 33 layouts"
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Task 4: site.js - Collapsible card click handler
|
|
410
|
+
|
|
411
|
+
**Files:**
|
|
412
|
+
|
|
413
|
+
- Modify: `public/js/site.js`
|
|
414
|
+
|
|
415
|
+
- [ ] **Step 1: Check if handler already exists**
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
grep -n 'collapsible\|is-collapsed' public/js/site.js
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Skip this task if a working handler is already present.
|
|
422
|
+
|
|
423
|
+
- [ ] **Step 2: Add click delegation near end of site init**
|
|
424
|
+
|
|
425
|
+
```js
|
|
426
|
+
// Collapsible card toggle
|
|
427
|
+
document.addEventListener('click', function (e) {
|
|
428
|
+
const header = e.target.closest('.card-collapsible .card-header');
|
|
429
|
+
if (!header) return;
|
|
430
|
+
header.closest('.card-collapsible').classList.toggle('is-collapsed');
|
|
431
|
+
});
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
- [ ] **Step 3: Verify** - Create a test page with `[card title="Click me" collapsible="true"]Body.[/card]`, open in
|
|
435
|
+
browser, click header, body toggles. Delete test page.
|
|
436
|
+
|
|
437
|
+
- [ ] **Step 4: Commit**
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
git add public/js/site.js
|
|
441
|
+
git commit -m "feat(site): add collapsible card click delegation"
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Task 5: markdown.js - Refactor processCardBlocks (routing infrastructure)
|
|
447
|
+
|
|
448
|
+
**Files:**
|
|
449
|
+
|
|
450
|
+
- Modify: `server/services/markdown.js`
|
|
451
|
+
|
|
452
|
+
- [ ] **Step 1: Locate `processCardBlocks` (around line 837)**
|
|
453
|
+
|
|
454
|
+
- [ ] **Step 2: Rename the existing callback body to `renderLegacyCard`**
|
|
455
|
+
|
|
456
|
+
Extract the existing `(_, attrStr, body) => { ... }` callback body into:
|
|
457
|
+
|
|
458
|
+
```js
|
|
459
|
+
function renderLegacyCard(attrs, body, markedInstance, escAttr) {
|
|
460
|
+
// paste existing match callback body here verbatim
|
|
461
|
+
// replace `marked` with `markedInstance`, `escapeAttr` with `escAttr`
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
- [ ] **Step 3: Add routing infrastructure**
|
|
466
|
+
|
|
467
|
+
```js
|
|
468
|
+
const LAYOUT_RENDERERS = {}; // populated by Tasks 6-9
|
|
469
|
+
|
|
470
|
+
function processCardBlocks(markdown) {
|
|
471
|
+
const {scrubbed, restore} = scrubCodeRegions(markdown);
|
|
472
|
+
return restore(scrubbed.replace(
|
|
473
|
+
/\[card([^\]]*)\]([\s\S]*?)\[\/card\]/gi,
|
|
474
|
+
(_, attrStr, body) => {
|
|
475
|
+
const attrs = parseShortcodeAttrs(attrStr);
|
|
476
|
+
const layout = typeof attrs.layout === 'string' ? attrs.layout.trim() : '';
|
|
477
|
+
const renderer = LAYOUT_RENDERERS[layout];
|
|
478
|
+
return renderer
|
|
479
|
+
? renderer(attrs, body, marked, escapeAttr)
|
|
480
|
+
: renderLegacyCard(attrs, body, marked, escapeAttr);
|
|
481
|
+
}
|
|
482
|
+
));
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
- [ ] **Step 4: Add shared helpers**
|
|
487
|
+
|
|
488
|
+
```js
|
|
489
|
+
function cardVariantClasses(attrs) {
|
|
490
|
+
const variant = typeof attrs.variant === 'string' ? attrs.variant.trim() : '';
|
|
491
|
+
const gradient = typeof attrs.gradient === 'string' ? attrs.gradient.trim() : 'indigo';
|
|
492
|
+
const classes = ['card', 'mb-4'];
|
|
493
|
+
const valid = ['clean','gradient','glass','accent','dark','glow'];
|
|
494
|
+
if (valid.includes(variant)) {
|
|
495
|
+
classes.push(`dm-card-${variant}`);
|
|
496
|
+
if (variant === 'gradient') classes.push(`card-gradient-${gradient}`);
|
|
497
|
+
}
|
|
498
|
+
if ('hover' in attrs) classes.push('card-hover');
|
|
499
|
+
if (attrs.collapsible === 'true') classes.push('card-collapsible');
|
|
500
|
+
if (typeof attrs.class === 'string') classes.push(attrs.class.trim());
|
|
501
|
+
return classes;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function cardRoot(attrs, extraClasses = []) {
|
|
505
|
+
const classes = [...cardVariantClasses(attrs), ...extraClasses];
|
|
506
|
+
const id = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
507
|
+
const coll = attrs.collapsible === 'true' ? ' data-collapsible="true"' : '';
|
|
508
|
+
return `<div class="${classes.join(' ')}"${id}${coll}>`;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function cardFooterAttr(attrs) {
|
|
512
|
+
return attrs.footer ? `<div class="card-footer">${escapeAttr(attrs.footer)}</div>` : '';
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function imgStyle(attrs) {
|
|
516
|
+
const img = typeof attrs.image === 'string' ? attrs.image.trim() : '';
|
|
517
|
+
return img ? ` style="background-image:url('${escapeAttr(img)}')"` : '';
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function parseSubTagList(body, tagName) {
|
|
521
|
+
const re = new RegExp(`\\[${tagName}([^\\]]*)\\]([\\s\\S]*?)\\[\\/${tagName}\\]`, 'gi');
|
|
522
|
+
const items = [];
|
|
523
|
+
body.replace(re, (_, attrStr, inner) => {
|
|
524
|
+
items.push({ attrs: parseShortcodeAttrs(attrStr), content: inner.trim() });
|
|
525
|
+
});
|
|
526
|
+
return items;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function parseSubTag(body, tagName) {
|
|
530
|
+
const re = new RegExp(`\\[${tagName}([^\\]]*)\\]([\\s\\S]*?)\\[\\/${tagName}\\]`, 'i');
|
|
531
|
+
const m = body.match(re);
|
|
532
|
+
return m ? m[2].trim() : null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function tagColorStyle(i) {
|
|
536
|
+
const p = [
|
|
537
|
+
'background:#ede9fe;color:#7c3aed',
|
|
538
|
+
'background:#dbeafe;color:#1d4ed8',
|
|
539
|
+
'background:#d1fae5;color:#065f46',
|
|
540
|
+
'background:#fef3c7;color:#92400e',
|
|
541
|
+
'background:#fce7f3;color:#9d174d',
|
|
542
|
+
];
|
|
543
|
+
return p[i % p.length];
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
- [ ] **Step 5: Verify server starts and existing cards still render**
|
|
548
|
+
|
|
549
|
+
```bash
|
|
550
|
+
curl -s http://localhost:4096/ | grep -c 'card' || echo "0 - no cards on homepage, that is fine"
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
- [ ] **Step 6: Commit**
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
git add server/services/markdown.js
|
|
557
|
+
git commit -m "refactor(markdown): extract card renderer routing infrastructure"
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## Task 6: markdown.js - Tier 1 basic + icon layouts
|
|
563
|
+
|
|
564
|
+
**Files:**
|
|
565
|
+
|
|
566
|
+
- Modify: `server/services/markdown.js`
|
|
567
|
+
|
|
568
|
+
- [ ] **Step 1: Add basic, header-body, header-body-footer, no-header-footer**
|
|
569
|
+
|
|
570
|
+
```js
|
|
571
|
+
LAYOUT_RENDERERS['basic'] = (attrs, body) => {
|
|
572
|
+
const bodyHtml = marked.parse(body.trim());
|
|
573
|
+
return `${cardRoot(attrs)}<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
LAYOUT_RENDERERS['header-body'] = (attrs, body) => {
|
|
577
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
578
|
+
const hdr = title ? `<div class="card-header"><div class="card-title">${escapeAttr(title)}</div></div>` : '';
|
|
579
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
580
|
+
return `${cardRoot(attrs)}${hdr}<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
LAYOUT_RENDERERS['header-body-footer'] = (attrs, body) => {
|
|
584
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
585
|
+
const footer = typeof attrs.footer === 'string' ? attrs.footer.trim() : '';
|
|
586
|
+
const hdr = title ? `<div class="card-header"><div class="card-title">${escapeAttr(title)}</div></div>` : '';
|
|
587
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
588
|
+
const ftr = footer ? `<div class="card-footer">${escapeAttr(footer)}</div>` : '';
|
|
589
|
+
return `${cardRoot(attrs)}${hdr}<div class="card-body">${bodyHtml}</div>${ftr}</div>\n`;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
LAYOUT_RENDERERS['no-header-footer'] = (attrs, body) => {
|
|
593
|
+
const footer = typeof attrs.footer === 'string' ? attrs.footer.trim() : '';
|
|
594
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
595
|
+
const ftr = footer ? `<div class="card-footer">${escapeAttr(footer)}</div>` : '';
|
|
596
|
+
return `${cardRoot(attrs)}<div class="card-body">${bodyHtml}</div>${ftr}</div>\n`;
|
|
597
|
+
};
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
- [ ] **Step 2: Add icon-top and icon-inline**
|
|
601
|
+
|
|
602
|
+
```js
|
|
603
|
+
LAYOUT_RENDERERS['icon-top'] = (attrs, body) => {
|
|
604
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
605
|
+
const subtitle = typeof attrs.subtitle === 'string' ? attrs.subtitle.trim() : '';
|
|
606
|
+
const icon = typeof attrs.icon === 'string' ? attrs.icon.trim() : '';
|
|
607
|
+
const iconHtml = icon ? `<span data-icon="${escapeAttr(icon)}" class="card-icon"></span>` : '';
|
|
608
|
+
const subHtml = subtitle ? `<div class="card-subtitle">${escapeAttr(subtitle)}</div>` : '';
|
|
609
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
610
|
+
return `${cardRoot(attrs)}
|
|
611
|
+
<div class="card-header card-header-icon-stacked">
|
|
612
|
+
<div class="card-avatar-wrap">${iconHtml}<div class="card-title">${escapeAttr(title)}</div>${subHtml}</div>
|
|
613
|
+
</div>
|
|
614
|
+
<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
LAYOUT_RENDERERS['icon-inline'] = (attrs, body) => {
|
|
618
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
619
|
+
const subtitle = typeof attrs.subtitle === 'string' ? attrs.subtitle.trim() : '';
|
|
620
|
+
const icon = typeof attrs.icon === 'string' ? attrs.icon.trim() : '';
|
|
621
|
+
const iconHtml = icon ? `<span data-icon="${escapeAttr(icon)}"></span>` : '';
|
|
622
|
+
const subHtml = subtitle ? `<div class="card-subtitle">${escapeAttr(subtitle)}</div>` : '';
|
|
623
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
624
|
+
return `${cardRoot(attrs)}
|
|
625
|
+
<div class="card-header card-header-icon-inline">
|
|
626
|
+
${iconHtml}<div class="card-header-content"><div class="card-title">${escapeAttr(title)}</div>${subHtml}</div>
|
|
627
|
+
</div>
|
|
628
|
+
<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
629
|
+
};
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
- [ ] **Step 3: Test**
|
|
633
|
+
|
|
634
|
+
```bash
|
|
635
|
+
# Create content/pages/card-icon-test.md:
|
|
636
|
+
# [card layout="icon-top" title="Feature" icon="star" subtitle="Sub"]Body.[/card]
|
|
637
|
+
# [card layout="icon-inline" title="Feature" icon="check"]Body.[/card]
|
|
638
|
+
curl -s http://localhost:4096/card-icon-test | grep -c 'card-header-icon'
|
|
639
|
+
# Expected: 2
|
|
640
|
+
# Delete test page
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
- [ ] **Step 4: Commit**
|
|
644
|
+
|
|
645
|
+
```bash
|
|
646
|
+
git add server/services/markdown.js
|
|
647
|
+
git commit -m "feat(markdown): add Tier 1 basic + icon card layout renderers"
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
## Task 7: markdown.js - Tier 1 image layouts
|
|
653
|
+
|
|
654
|
+
**Files:**
|
|
655
|
+
|
|
656
|
+
- Modify: `server/services/markdown.js`
|
|
657
|
+
|
|
658
|
+
- [ ] **Step 1: Add all 7 image layouts**
|
|
659
|
+
|
|
660
|
+
```js
|
|
661
|
+
LAYOUT_RENDERERS['image-top'] = (attrs, body) => {
|
|
662
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
663
|
+
const hdr = title ? `<div class="card-header"><div class="card-title">${escapeAttr(title)}</div></div>` : '';
|
|
664
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
665
|
+
return `${cardRoot(attrs)}<div class="card-img-top"${imgStyle(attrs)}></div>${hdr}<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
LAYOUT_RENDERERS['image-overlay'] = (attrs, body) => {
|
|
669
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
670
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
671
|
+
return `${cardRoot(attrs)}
|
|
672
|
+
<div class="card-img-overlay"${imgStyle(attrs)}>
|
|
673
|
+
<div class="card-overlay-text"><div class="card-title">${escapeAttr(title)}</div></div>
|
|
674
|
+
</div>
|
|
675
|
+
<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
LAYOUT_RENDERERS['thumb-left'] = (attrs, body) => {
|
|
679
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
680
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
681
|
+
return `${cardRoot(attrs, ['card-layout-thumb-left'])}<div class="card-img-left"${imgStyle(attrs)}></div>
|
|
682
|
+
<div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
LAYOUT_RENDERERS['thumb-right'] = (attrs, body) => {
|
|
686
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
687
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
688
|
+
return `${cardRoot(attrs, ['card-layout-thumb-right'])}
|
|
689
|
+
<div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div>
|
|
690
|
+
<div class="card-img-right"${imgStyle(attrs)}></div></div>\n`;
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
LAYOUT_RENDERERS['wide-left-image'] = (attrs, body) => {
|
|
694
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
695
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
696
|
+
return `${cardRoot(attrs, ['card-layout-horizontal'])}<div class="card-img-wide"${imgStyle(attrs)}></div>
|
|
697
|
+
<div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}${cardFooterAttr(attrs)}</div></div>\n`;
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
LAYOUT_RENDERERS['full-bg'] = (attrs, body) => {
|
|
701
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
702
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
703
|
+
return `${cardRoot(attrs, ['dm-card-full-bg'])}<div class="card-img-top"${imgStyle(attrs)}></div>
|
|
704
|
+
<div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
LAYOUT_RENDERERS['split-half'] = (attrs, body) => {
|
|
708
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
709
|
+
const gradient = typeof attrs.gradient === 'string' ? attrs.gradient.trim() : 'indigo';
|
|
710
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
711
|
+
return `${cardRoot(attrs, ['card-layout-split'])}<div class="card-split-left card-gradient-${escapeAttr(gradient)}"${imgStyle(attrs)}></div>
|
|
712
|
+
<div class="card-split-right"><div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div></div></div>\n`;
|
|
713
|
+
};
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
- [ ] **Step 2: Test**
|
|
717
|
+
|
|
718
|
+
```bash
|
|
719
|
+
# Create content/pages/card-img-test.md with image-top, thumb-left, split-half shortcodes
|
|
720
|
+
curl -s http://localhost:4096/card-img-test | grep -c 'card-img'
|
|
721
|
+
# Expected: 3+
|
|
722
|
+
# Delete test page
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
- [ ] **Step 3: Commit**
|
|
726
|
+
|
|
727
|
+
```bash
|
|
728
|
+
git add server/services/markdown.js
|
|
729
|
+
git commit -m "feat(markdown): add Tier 1 image card layout renderers"
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## Task 8: markdown.js - Tier 1 people, data, and special layouts
|
|
735
|
+
|
|
736
|
+
**Files:**
|
|
737
|
+
|
|
738
|
+
- Modify: `server/services/markdown.js`
|
|
739
|
+
|
|
740
|
+
- [ ] **Step 1: Add avatar-profile, stat-metric, quote-testimonial, callout**
|
|
741
|
+
|
|
742
|
+
```js
|
|
743
|
+
LAYOUT_RENDERERS['avatar-profile'] = (attrs, body) => {
|
|
744
|
+
const title = escapeAttr(attrs.title || '');
|
|
745
|
+
const subtitle = escapeAttr(attrs.subtitle || '');
|
|
746
|
+
const icon = escapeAttr(attrs.icon || '');
|
|
747
|
+
const tags = typeof attrs.tags === 'string' ? attrs.tags.split(',').map(t => t.trim()).filter(Boolean) : [];
|
|
748
|
+
const tagHtml = tags.length ? `<div class="card-tag-pills">${tags.map(t => `<span class="card-pill">${escapeAttr(t)}</span>`).join('')}</div>` : '';
|
|
749
|
+
const iconHtml = icon ? `<span data-icon="${icon}"></span>` : '';
|
|
750
|
+
const bodyHtml = body.trim() ? `<div class="card-body">${marked.parse(body.trim())}</div>` : '';
|
|
751
|
+
return `${cardRoot(attrs)}<div class="card-avatar-wrap">
|
|
752
|
+
<div class="card-avatar">${iconHtml}</div>
|
|
753
|
+
${title ? `<div class="card-title">${title}</div>` : ''}
|
|
754
|
+
${subtitle ? `<div class="card-subtitle">${subtitle}</div>` : ''}${tagHtml}
|
|
755
|
+
</div>${bodyHtml}${cardFooterAttr(attrs)}</div>\n`;
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
LAYOUT_RENDERERS['stat-metric'] = (attrs, _body) => {
|
|
759
|
+
const title = escapeAttr(attrs.title || '');
|
|
760
|
+
const value = escapeAttr(attrs.value || '-');
|
|
761
|
+
const delta = escapeAttr(attrs.delta || '');
|
|
762
|
+
const progress = Math.min(100, Math.max(0, parseInt(attrs.progress || '0', 10)));
|
|
763
|
+
const isPos = delta && !delta.startsWith('-') && !delta.startsWith('↓');
|
|
764
|
+
const deltaHtml = delta ? `<div class="card-stat-delta ${isPos ? 'positive' : 'negative'}">${delta}</div>` : '';
|
|
765
|
+
const barHtml = progress > 0 ? `<div class="card-stat-bar"><div class="card-stat-fill" style="width:${progress}%"></div></div>` : '';
|
|
766
|
+
return `${cardRoot(attrs)}<div class="card-body">
|
|
767
|
+
${title ? `<div class="card-subtitle">${title}</div>` : ''}
|
|
768
|
+
<div class="card-stat-value">${value}</div>${deltaHtml}${barHtml}
|
|
769
|
+
</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
LAYOUT_RENDERERS['quote-testimonial'] = (attrs, body) => {
|
|
773
|
+
const author = escapeAttr(attrs.title || '');
|
|
774
|
+
const role = escapeAttr(attrs.subtitle || '');
|
|
775
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
776
|
+
return `${cardRoot(attrs)}<span class="card-quote-mark">\u201c</span>
|
|
777
|
+
<div class="card-quote-text">${bodyHtml}</div>
|
|
778
|
+
<div class="card-footer card-quote-attr">
|
|
779
|
+
<div class="card-quote-avatar"></div>
|
|
780
|
+
<div>${author ? `<div style="font-weight:700;font-size:.85rem">${author}</div>` : ''}${role ? `<div class="card-subtitle">${role}</div>` : ''}</div>
|
|
781
|
+
</div></div>\n`;
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
LAYOUT_RENDERERS['callout'] = (attrs, body) => {
|
|
785
|
+
const type = typeof attrs['callout-type'] === 'string' ? attrs['callout-type'].trim() : 'info';
|
|
786
|
+
const defaults = { info:'💡', warn:'⚠️', success:'✅', error:'❌' };
|
|
787
|
+
const icon = escapeAttr(attrs.icon || defaults[type] || '💡');
|
|
788
|
+
const title = escapeAttr(attrs.title || '');
|
|
789
|
+
const bodyHtml = marked.parse(body.trim());
|
|
790
|
+
return `${cardRoot(attrs, ['card-callout', type])}<div class="card-callout-inner card-body">
|
|
791
|
+
<div class="card-callout-icon"><span data-icon="${icon}"></span></div>
|
|
792
|
+
<div>${title ? `<div class="card-title">${title}</div>` : ''}${bodyHtml}</div>
|
|
793
|
+
</div></div>\n`;
|
|
794
|
+
};
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
- [ ] **Step 2: Add video-media, location-map, step-numbered, corner-badge, badge-band**
|
|
798
|
+
|
|
799
|
+
```js
|
|
800
|
+
LAYOUT_RENDERERS['video-media'] = (attrs, body) => {
|
|
801
|
+
const title = escapeAttr(attrs.title || '');
|
|
802
|
+
const duration = escapeAttr(attrs.duration || '');
|
|
803
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
804
|
+
const dur = duration ? `<div class="card-video-duration">${duration}</div>` : '';
|
|
805
|
+
return `${cardRoot(attrs)}<div class="card-video-thumb"${imgStyle(attrs)}>
|
|
806
|
+
<div class="card-play-btn">\u25B6</div>${dur}
|
|
807
|
+
</div>${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
|
|
808
|
+
<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
LAYOUT_RENDERERS['location-map'] = (attrs, body) => {
|
|
812
|
+
const address = escapeAttr(attrs.address || '');
|
|
813
|
+
const title = escapeAttr(attrs.title || '');
|
|
814
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
815
|
+
return `${cardRoot(attrs)}<div class="card-map-placeholder">
|
|
816
|
+
<div class="card-map-grid"></div><div class="card-map-pin">📍</div>
|
|
817
|
+
</div>${title || address ? `<div class="card-body">${title ? `<div class="card-title">${title}</div>` : ''}${address ? `<div class="card-subtitle">${address}</div>` : ''}${bodyHtml}</div>` : ''}</div>\n`;
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
LAYOUT_RENDERERS['step-numbered'] = (attrs, body) => {
|
|
821
|
+
const step = escapeAttr(String(attrs.step || '1'));
|
|
822
|
+
const title = escapeAttr(attrs.title || '');
|
|
823
|
+
const gradient = escapeAttr(attrs.gradient || 'indigo');
|
|
824
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
825
|
+
return `${cardRoot(attrs)}<div class="card-step-bg card-gradient-${gradient}">
|
|
826
|
+
<div class="card-step-badge">${step}</div>
|
|
827
|
+
<div class="card-step-ghost">${step}</div>
|
|
828
|
+
</div>${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
|
|
829
|
+
<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
LAYOUT_RENDERERS['corner-badge'] = (attrs, body) => {
|
|
833
|
+
const badge = escapeAttr(attrs.badge || 'New');
|
|
834
|
+
const title = escapeAttr(attrs.title || '');
|
|
835
|
+
const icon = escapeAttr(attrs.icon || '');
|
|
836
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
837
|
+
return `${cardRoot(attrs, ['card-corner-badge-wrap'])}<span class="card-corner-badge">${badge}</span>
|
|
838
|
+
${icon ? `<div style="display:flex;justify-content:center;padding:18px 16px 4px"><span data-icon="${icon}" style="font-size:2rem"></span></div>` : ''}
|
|
839
|
+
<div class="card-body" style="text-align:center">${title ? `<div class="card-title">${title}</div>` : ''}${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
LAYOUT_RENDERERS['badge-band'] = (attrs, body) => {
|
|
843
|
+
const badge = escapeAttr(attrs.badge || 'New');
|
|
844
|
+
const icon = escapeAttr(attrs.icon || '');
|
|
845
|
+
const gradient = escapeAttr(attrs.gradient || 'indigo');
|
|
846
|
+
const title = escapeAttr(attrs.title || '');
|
|
847
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
848
|
+
return `${cardRoot(attrs)}<div class="card-badge-band card-gradient-${gradient}">
|
|
849
|
+
<span class="card-badge-band-label">${badge}</span>
|
|
850
|
+
${icon ? `<span class="card-badge-band-icon" data-icon="${icon}"></span>` : ''}
|
|
851
|
+
</div>${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
|
|
852
|
+
<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
853
|
+
};
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
- [ ] **Step 3: Add glass-gradient-border, rating-review, tag-cloud, timeline-entry, code-snippet**
|
|
857
|
+
|
|
858
|
+
```js
|
|
859
|
+
LAYOUT_RENDERERS['glass-gradient-border'] = (attrs, body) => {
|
|
860
|
+
const title = escapeAttr(attrs.title || '');
|
|
861
|
+
const bodyHtml = marked.parse(body.trim());
|
|
862
|
+
return `<div class="card-glass-outer mb-4">
|
|
863
|
+
<div class="card-glass-inner">
|
|
864
|
+
${title ? `<div class="card-title">${title}</div>` : ''}<div>${bodyHtml}</div>
|
|
865
|
+
</div></div>\n`;
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
LAYOUT_RENDERERS['rating-review'] = (attrs, body) => {
|
|
869
|
+
const rating = Math.min(5, Math.max(0, parseInt(attrs.rating || '5', 10)));
|
|
870
|
+
const stars = '\u2605'.repeat(rating) + '\u2606'.repeat(5 - rating);
|
|
871
|
+
const author = escapeAttr(attrs.title || '');
|
|
872
|
+
const role = escapeAttr(attrs.subtitle || '');
|
|
873
|
+
const verified = 'verified' in attrs;
|
|
874
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
875
|
+
return `${cardRoot(attrs)}<span class="card-stars">${stars}</span>
|
|
876
|
+
<div class="card-quote-text">${bodyHtml}</div>
|
|
877
|
+
<div class="card-footer card-quote-attr">
|
|
878
|
+
<div class="card-quote-avatar"></div>
|
|
879
|
+
<div>${author ? `<div style="font-weight:700;font-size:.82rem">${author}${verified ? '<span class="card-verified">Verified</span>' : ''}</div>` : ''}
|
|
880
|
+
${role ? `<div class="card-subtitle">${role}</div>` : ''}</div>
|
|
881
|
+
</div></div>\n`;
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
LAYOUT_RENDERERS['tag-cloud'] = (attrs, body) => {
|
|
885
|
+
const title = escapeAttr(attrs.title || '');
|
|
886
|
+
const tagAttr = typeof attrs.tags === 'string' ? attrs.tags.split(',').map(t => t.trim()).filter(Boolean) : [];
|
|
887
|
+
const tagHtml = tagAttr.map((t, i) => `<span class="card-tag" style="${tagColorStyle(i)}">${escapeAttr(t)}</span>`).join('');
|
|
888
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
889
|
+
return `${cardRoot(attrs)}${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
|
|
890
|
+
<div class="card-tag-cloud">${tagHtml}${bodyHtml}</div></div>\n`;
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
LAYOUT_RENDERERS['timeline-entry'] = (attrs, body) => {
|
|
894
|
+
const date = escapeAttr(attrs.subtitle || '');
|
|
895
|
+
const title = escapeAttr(attrs.title || '');
|
|
896
|
+
const tag = escapeAttr(attrs.badge || '');
|
|
897
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
898
|
+
return `${cardRoot(attrs)}<div class="card-timeline-row">
|
|
899
|
+
<div class="card-timeline-side"><div class="card-timeline-dot"></div><div class="card-timeline-line"></div></div>
|
|
900
|
+
<div class="card-timeline-body">
|
|
901
|
+
${date ? `<div class="card-timeline-date">${date}</div>` : ''}
|
|
902
|
+
${title ? `<div class="card-title">${title}</div>` : ''}
|
|
903
|
+
${bodyHtml}
|
|
904
|
+
${tag ? `<span class="card-timeline-tag">${tag}</span>` : ''}
|
|
905
|
+
</div>
|
|
906
|
+
</div></div>\n`;
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
LAYOUT_RENDERERS['code-snippet'] = (attrs, body) => {
|
|
910
|
+
const lang = escapeAttr(attrs.lang || 'Code');
|
|
911
|
+
const code = body.trim()
|
|
912
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
913
|
+
return `${cardRoot(attrs)}<div class="card-code-header">
|
|
914
|
+
<span class="card-code-lang">${lang}</span>
|
|
915
|
+
</div><div class="card-code-body"><pre><code>${code}</code></pre></div></div>\n`;
|
|
916
|
+
};
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
- [ ] **Step 4: Test**
|
|
920
|
+
|
|
921
|
+
```bash
|
|
922
|
+
# Create content/pages/card-special-test.md with callout, stat-metric, timeline-entry, code-snippet
|
|
923
|
+
curl -s http://localhost:4096/card-special-test | grep -c 'card-callout\|card-stat\|card-timeline\|card-code'
|
|
924
|
+
# Expected: 4
|
|
925
|
+
# Delete test page
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
- [ ] **Step 5: Commit**
|
|
929
|
+
|
|
930
|
+
```bash
|
|
931
|
+
git add server/services/markdown.js
|
|
932
|
+
git commit -m "feat(markdown): add Tier 1 people/data/special card layout renderers"
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
---
|
|
936
|
+
|
|
937
|
+
## Task 9: markdown.js - Tier 2 sub-tag layouts
|
|
938
|
+
|
|
939
|
+
**Files:**
|
|
940
|
+
|
|
941
|
+
- Modify: `server/services/markdown.js`
|
|
942
|
+
|
|
943
|
+
- [ ] **Step 1: Add before-after, pricing, feature-comparison**
|
|
944
|
+
|
|
945
|
+
```js
|
|
946
|
+
LAYOUT_RENDERERS['before-after'] = (attrs, body) => {
|
|
947
|
+
const title = escapeAttr(attrs.title || '');
|
|
948
|
+
const beforeRaw = parseSubTag(body, 'before') || '';
|
|
949
|
+
const afterRaw = parseSubTag(body, 'after') || '';
|
|
950
|
+
const beforeItems = beforeRaw.split(/[·\n]/).map(s => s.trim()).filter(Boolean);
|
|
951
|
+
const afterItems = afterRaw.split(/[·\n]/).map(s => s.trim()).filter(Boolean);
|
|
952
|
+
const colBefore = beforeItems.map(i => `<div class="card-compare-item"><span class="card-compare-x">\u2717</span>${escapeAttr(i)}</div>`).join('');
|
|
953
|
+
const colAfter = afterItems.map(i => `<div class="card-compare-item"><span class="card-compare-check">\u2713</span>${escapeAttr(i)}</div>`).join('');
|
|
954
|
+
return `${cardRoot(attrs)}${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
|
|
955
|
+
<div class="card-compare-grid">
|
|
956
|
+
<div class="card-compare-col"><span class="card-compare-label before">Before</span>${colBefore}</div>
|
|
957
|
+
<div class="card-compare-col"><span class="card-compare-label after">After</span>${colAfter}</div>
|
|
958
|
+
</div></div>\n`;
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
LAYOUT_RENDERERS['pricing'] = (attrs, body) => {
|
|
962
|
+
const title = escapeAttr(attrs.title || '');
|
|
963
|
+
const price = escapeAttr(attrs.price || '');
|
|
964
|
+
const period = escapeAttr(attrs.period || '');
|
|
965
|
+
const gradient = escapeAttr(attrs.gradient || 'indigo');
|
|
966
|
+
const cta = escapeAttr(attrs.footer || 'Get started');
|
|
967
|
+
const features = parseSubTagList(body, 'feature');
|
|
968
|
+
const featHtml = features.map(f => `<div class="card-pricing-feature"><span class="card-pricing-check">\u2713</span>${escapeAttr(f.content)}</div>`).join('');
|
|
969
|
+
return `${cardRoot(attrs)}<div class="card-header card-gradient-${gradient}">
|
|
970
|
+
<div class="card-subtitle">${title}</div>
|
|
971
|
+
<div class="card-stat-value" style="color:#fff">${price}</div>
|
|
972
|
+
${period ? `<div style="opacity:.75;font-size:.8rem">${period}</div>` : ''}
|
|
973
|
+
</div><div class="card-pricing-features">${featHtml}</div>
|
|
974
|
+
<a class="card-pricing-cta" href="#">${cta}</a></div>\n`;
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
LAYOUT_RENDERERS['feature-comparison'] = (attrs, body) => {
|
|
978
|
+
const title = escapeAttr(attrs.title || '');
|
|
979
|
+
const plan = escapeAttr(attrs.subtitle || '');
|
|
980
|
+
const gradient = escapeAttr(attrs.gradient || 'indigo');
|
|
981
|
+
const features = parseSubTagList(body, 'feature');
|
|
982
|
+
const rows = features.map(f => {
|
|
983
|
+
const included = !('excluded' in f.attrs);
|
|
984
|
+
return `<div class="card-fc-row"><span>${escapeAttr(f.content)}</span><span class="${included ? 'card-fc-yes' : 'card-fc-no'}">${included ? '\u2713' : '\u2717'}</span></div>`;
|
|
985
|
+
}).join('');
|
|
986
|
+
return `${cardRoot(attrs)}<div class="card-header card-gradient-${gradient}">
|
|
987
|
+
${plan ? `<div class="card-subtitle" style="opacity:.8;font-size:.75rem">${plan}</div>` : ''}
|
|
988
|
+
<div class="card-title">${title}</div>
|
|
989
|
+
</div>${rows}</div>\n`;
|
|
990
|
+
};
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
- [ ] **Step 2: Add activity-feed, progress-goal, file-document**
|
|
994
|
+
|
|
995
|
+
```js
|
|
996
|
+
LAYOUT_RENDERERS['activity-feed'] = (attrs, body) => {
|
|
997
|
+
const title = escapeAttr(attrs.title || 'Activity');
|
|
998
|
+
const activities = parseSubTagList(body, 'activity');
|
|
999
|
+
const items = activities.map(a => {
|
|
1000
|
+
const user = escapeAttr(a.attrs.user || '');
|
|
1001
|
+
const action = escapeAttr(a.attrs.action || '');
|
|
1002
|
+
const time = escapeAttr(a.attrs.time || '');
|
|
1003
|
+
return `<div class="card-activity-item">
|
|
1004
|
+
<div class="card-activity-avatar">${user.charAt(0).toUpperCase()}</div>
|
|
1005
|
+
<div><div style="font-size:.82rem"><strong>${user}</strong> ${action}</div>${time ? `<div style="font-size:.7rem;color:#9ca3af;margin-top:2px">${time}</div>` : ''}</div>
|
|
1006
|
+
</div>`;
|
|
1007
|
+
}).join('');
|
|
1008
|
+
return `${cardRoot(attrs)}<div class="card-header"><div class="card-title">${title}</div></div>
|
|
1009
|
+
<div>${items}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
LAYOUT_RENDERERS['progress-goal'] = (attrs, body) => {
|
|
1013
|
+
const title = escapeAttr(attrs.title || '');
|
|
1014
|
+
const subtitle = escapeAttr(attrs.subtitle || '');
|
|
1015
|
+
const progress = Math.min(100, Math.max(0, parseInt(attrs.progress || '0', 10)));
|
|
1016
|
+
const milestones = parseSubTagList(body, 'milestone');
|
|
1017
|
+
const msHtml = milestones.map(m => {
|
|
1018
|
+
const done = 'done' in m.attrs;
|
|
1019
|
+
return `<div class="card-milestone"><div class="card-milestone-dot ${done ? 'done' : 'pending'}"></div>${escapeAttr(m.content)}</div>`;
|
|
1020
|
+
}).join('');
|
|
1021
|
+
return `${cardRoot(attrs)}<div class="card-body">
|
|
1022
|
+
${title ? `<div class="card-title">${title}</div>` : ''}
|
|
1023
|
+
<div style="display:flex;justify-content:space-between;font-size:.82rem;margin-top:8px">
|
|
1024
|
+
<span>${subtitle}</span><strong style="color:#6366f1">${progress}%</strong>
|
|
1025
|
+
</div>
|
|
1026
|
+
<div class="card-progress-bar"><div class="card-progress-fill" style="width:${progress}%"></div></div>
|
|
1027
|
+
${msHtml}
|
|
1028
|
+
</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
LAYOUT_RENDERERS['file-document'] = (attrs, _body) => {
|
|
1032
|
+
const filename = escapeAttr(attrs.filename || 'document');
|
|
1033
|
+
const filesize = escapeAttr(attrs.filesize || '');
|
|
1034
|
+
const filetype = escapeAttr((attrs.filetype || '').toUpperCase());
|
|
1035
|
+
const title = escapeAttr(attrs.title || filename);
|
|
1036
|
+
return `${cardRoot(attrs)}<div class="card-file-row">
|
|
1037
|
+
<div class="card-file-icon">\uD83D\uDCC4<div class="card-file-ext">${filetype}</div></div>
|
|
1038
|
+
<div><div class="card-title" style="font-size:.9rem">${title}</div>
|
|
1039
|
+
${filesize ? `<div class="card-subtitle">${filesize}</div>` : ''}
|
|
1040
|
+
<a class="card-file-dl" href="#">\u2193 Download</a></div>
|
|
1041
|
+
</div></div>\n`;
|
|
1042
|
+
};
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
- [ ] **Step 3: Test**
|
|
1046
|
+
|
|
1047
|
+
```bash
|
|
1048
|
+
# Create content/pages/card-tier2-test.md with pricing, before-after, activity-feed shortcodes
|
|
1049
|
+
curl -s http://localhost:4096/card-tier2-test | grep -c 'card-pricing\|card-compare\|card-activity'
|
|
1050
|
+
# Expected: 3+
|
|
1051
|
+
# Delete test page
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
- [ ] **Step 4: Commit**
|
|
1055
|
+
|
|
1056
|
+
```bash
|
|
1057
|
+
git add server/services/markdown.js
|
|
1058
|
+
git commit -m "feat(markdown): add Tier 2 sub-tag card layout renderers"
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
---
|
|
1062
|
+
|
|
1063
|
+
## Task 10: card-builder.js - Module scaffold, state, shortcode generator
|
|
1064
|
+
|
|
1065
|
+
**Files:**
|
|
1066
|
+
|
|
1067
|
+
- Create: `admin/js/lib/card-builder.js`
|
|
1068
|
+
|
|
1069
|
+
- [ ] **Step 1: Create the file with layout registry, state object, and `CardBuilder.open()`**
|
|
1070
|
+
|
|
1071
|
+
```js
|
|
1072
|
+
/**
|
|
1073
|
+
* card-builder.js
|
|
1074
|
+
* Self-contained card builder dialog.
|
|
1075
|
+
* API: CardBuilder.open(textarea, { onMediaBrowse })
|
|
1076
|
+
*/
|
|
1077
|
+
|
|
1078
|
+
const LAYOUTS = [
|
|
1079
|
+
{ key:'header-body', group:'Basic', label:'Header + Body', thumb:'▤' },
|
|
1080
|
+
{ key:'basic', group:'Basic', label:'Body only', thumb:'▭' },
|
|
1081
|
+
{ key:'header-body-footer', group:'Basic', label:'Header + Body + Footer', thumb:'☰' },
|
|
1082
|
+
{ key:'no-header-footer', group:'Basic', label:'Body + Footer (no header)', thumb:'▥' },
|
|
1083
|
+
{ key:'icon-top', group:'Icon', label:'Icon top (stacked)', thumb:'⬆★' },
|
|
1084
|
+
{ key:'icon-inline', group:'Icon', label:'Icon inline', thumb:'★▶' },
|
|
1085
|
+
{ key:'image-top', group:'Image', label:'Image top', thumb:'🖼' },
|
|
1086
|
+
{ key:'image-overlay', group:'Image', label:'Image + overlay title', thumb:'🖼✦' },
|
|
1087
|
+
{ key:'thumb-left', group:'Image', label:'Thumbnail left', thumb:'🖼▐' },
|
|
1088
|
+
{ key:'thumb-right', group:'Image', label:'Thumbnail right', thumb:'▌🖼' },
|
|
1089
|
+
{ key:'wide-left-image', group:'Image', label:'Wide image left', thumb:'🖼▍' },
|
|
1090
|
+
{ key:'full-bg', group:'Image', label:'Full background', thumb:'🖼' },
|
|
1091
|
+
{ key:'split-half', group:'Image', label:'Split half', thumb:'▒▐' },
|
|
1092
|
+
{ key:'avatar-profile', group:'People', label:'Avatar / Profile', thumb:'👤' },
|
|
1093
|
+
{ key:'stat-metric', group:'People', label:'Stat / Metric', thumb:'📊' },
|
|
1094
|
+
{ key:'quote-testimonial', group:'People', label:'Quote / Testimonial', thumb:'\u201c' },
|
|
1095
|
+
{ key:'rating-review', group:'People', label:'Rating / Review', thumb:'★' },
|
|
1096
|
+
{ key:'activity-feed', group:'People', label:'Activity feed', thumb:'🔔' },
|
|
1097
|
+
{ key:'callout', group:'Content', label:'Callout', thumb:'💡' },
|
|
1098
|
+
{ key:'code-snippet', group:'Content', label:'Code snippet', thumb:'</>' },
|
|
1099
|
+
{ key:'file-document', group:'Content', label:'File / Document', thumb:'📄' },
|
|
1100
|
+
{ key:'tag-cloud', group:'Content', label:'Tag cloud', thumb:'🏷' },
|
|
1101
|
+
{ key:'video-media', group:'Media', label:'Video / Media', thumb:'▶' },
|
|
1102
|
+
{ key:'location-map', group:'Special', label:'Location / Map', thumb:'📍' },
|
|
1103
|
+
{ key:'step-numbered', group:'Special', label:'Step / Numbered', thumb:'①' },
|
|
1104
|
+
{ key:'corner-badge', group:'Special', label:'Corner badge', thumb:'🏷' },
|
|
1105
|
+
{ key:'badge-band', group:'Special', label:'Badge band', thumb:'⬛' },
|
|
1106
|
+
{ key:'glass-gradient-border', group:'Special', label:'Glass gradient border', thumb:'◈' },
|
|
1107
|
+
{ key:'timeline-entry', group:'Special', label:'Timeline entry', thumb:'•-' },
|
|
1108
|
+
{ key:'before-after', group:'Compare', label:'Before / After', thumb:'↔' },
|
|
1109
|
+
{ key:'pricing', group:'Compare', label:'Pricing card', thumb:'£' },
|
|
1110
|
+
{ key:'feature-comparison', group:'Compare', label:'Feature comparison', thumb:'☑' },
|
|
1111
|
+
{ key:'progress-goal', group:'Compare', label:'Progress / Goal', thumb:'▰' },
|
|
1112
|
+
];
|
|
1113
|
+
|
|
1114
|
+
const VARIANTS = ['clean','gradient','glass','accent','dark','glow'];
|
|
1115
|
+
const GRADIENTS = ['indigo','ocean','sunset','forest','rose','midnight','aurora','fire','lagoon','dusk','lime','gold','arctic','slate'];
|
|
1116
|
+
|
|
1117
|
+
const GRAD_COLORS = {
|
|
1118
|
+
indigo:'#6366f1,#8b5cf6', ocean:'#0891b2,#2563eb', sunset:'#f59e0b,#ef4444',
|
|
1119
|
+
forest:'#10b981,#0d9488', rose:'#fb7185,#e11d48', midnight:'#1e1b4b,#4338ca',
|
|
1120
|
+
aurora:'#06b6d4,#6366f1', fire:'#ef4444,#f97316', lagoon:'#06b6d4,#0e7490',
|
|
1121
|
+
dusk:'#7c3aed,#db2777', lime:'#84cc16,#10b981', gold:'#f59e0b,#b45309',
|
|
1122
|
+
arctic:'#bae6fd,#e0f2fe', slate:'#475569,#1e293b',
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
// Singleton state
|
|
1126
|
+
let _overlay = null;
|
|
1127
|
+
let _state = null;
|
|
1128
|
+
let _ta = null;
|
|
1129
|
+
let _opts = null;
|
|
1130
|
+
|
|
1131
|
+
export const CardBuilder = {
|
|
1132
|
+
open(textarea, options = {}) {
|
|
1133
|
+
if (_overlay) return;
|
|
1134
|
+
_ta = textarea;
|
|
1135
|
+
_opts = options;
|
|
1136
|
+
_state = {
|
|
1137
|
+
layout:'header-body', variant:'clean', gradient:'indigo',
|
|
1138
|
+
title:'', subtitle:'', icon:'', iconLayout:'inline', image:'',
|
|
1139
|
+
body:'', footer:'', hover:false, collapsible:false,
|
|
1140
|
+
value:'', delta:'', progress:'50', price:'', period:'',
|
|
1141
|
+
lang:'JavaScript', filename:'', filesize:'', filetype:'PDF',
|
|
1142
|
+
rating:'5', duration:'', address:'', step:'1', badge:'New',
|
|
1143
|
+
tags:'', calloutType:'info',
|
|
1144
|
+
};
|
|
1145
|
+
_buildDialog();
|
|
1146
|
+
},
|
|
1147
|
+
close() { _closeDialog(); },
|
|
1148
|
+
};
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
- [ ] **Step 2: Add `_buildShortcode()`**
|
|
1152
|
+
|
|
1153
|
+
```js
|
|
1154
|
+
function _buildShortcode() {
|
|
1155
|
+
const s = _state;
|
|
1156
|
+
const a = [];
|
|
1157
|
+
|
|
1158
|
+
if (s.layout !== 'header-body') a.push(`layout="${s.layout}"`);
|
|
1159
|
+
if (s.title) a.push(`title="${s.title}"`);
|
|
1160
|
+
if (s.subtitle) a.push(`subtitle="${s.subtitle}"`);
|
|
1161
|
+
if (s.icon) a.push(`icon="${s.icon}"`);
|
|
1162
|
+
if (s.icon && s.iconLayout === 'stacked') a.push('icon-layout="stacked"');
|
|
1163
|
+
if (s.image) a.push(`image="${s.image}"`);
|
|
1164
|
+
if (s.variant !== 'clean') a.push(`variant="${s.variant}"`);
|
|
1165
|
+
if (s.variant === 'gradient' && s.gradient !== 'indigo') a.push(`gradient="${s.gradient}"`);
|
|
1166
|
+
if (s.hover) a.push('hover');
|
|
1167
|
+
if (s.collapsible) a.push('collapsible="true"');
|
|
1168
|
+
if (s.footer && !['pricing','feature-comparison','activity-feed','before-after','progress-goal'].includes(s.layout)) {
|
|
1169
|
+
a.push(`footer="${s.footer}"`);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const lsa = {
|
|
1173
|
+
'stat-metric': () => { if (s.value) a.push(`value="${s.value}"`); if (s.delta) a.push(`delta="${s.delta}"`); if (s.progress) a.push(`progress="${s.progress}"`); },
|
|
1174
|
+
'pricing': () => { if (s.price) a.push(`price="${s.price}"`); if (s.period) a.push(`period="${s.period}"`); },
|
|
1175
|
+
'code-snippet': () => a.push(`lang="${s.lang}"`),
|
|
1176
|
+
'file-document': () => { a.push(`filename="${s.filename}"`); if (s.filesize) a.push(`filesize="${s.filesize}"`); if (s.filetype) a.push(`filetype="${s.filetype}"`); },
|
|
1177
|
+
'rating-review': () => a.push(`rating="${s.rating}"`),
|
|
1178
|
+
'video-media': () => { if (s.duration) a.push(`duration="${s.duration}"`); },
|
|
1179
|
+
'location-map': () => { if (s.address) a.push(`address="${s.address}"`); },
|
|
1180
|
+
'step-numbered': () => a.push(`step="${s.step}"`),
|
|
1181
|
+
'corner-badge': () => { if (s.badge) a.push(`badge="${s.badge}"`); },
|
|
1182
|
+
'badge-band': () => { if (s.badge) a.push(`badge="${s.badge}"`); },
|
|
1183
|
+
'tag-cloud': () => { if (s.tags) a.push(`tags="${s.tags}"`); },
|
|
1184
|
+
'avatar-profile': () => { if (s.tags) a.push(`tags="${s.tags}"`); },
|
|
1185
|
+
'callout': () => { if (s.calloutType) a.push(`callout-type="${s.calloutType}"`); },
|
|
1186
|
+
'split-half': () => {},
|
|
1187
|
+
};
|
|
1188
|
+
if (lsa[s.layout]) lsa[s.layout]();
|
|
1189
|
+
|
|
1190
|
+
const attrStr = a.length ? ' ' + a.join(' ') : '';
|
|
1191
|
+
const body = s.body.trim() || 'Content here.';
|
|
1192
|
+
|
|
1193
|
+
const tier2defaults = {
|
|
1194
|
+
'before-after': `[before]Before item 1 · Before item 2[/before]\n[after]After item 1 · After item 2[/after]`,
|
|
1195
|
+
'pricing': `[feature]Feature one[/feature]\n[feature]Feature two[/feature]`,
|
|
1196
|
+
'feature-comparison':`[feature]Included feature[/feature]\n[feature excluded]Excluded feature[/feature]`,
|
|
1197
|
+
'activity-feed': `[activity user="Alice" action="published a page" time="2 min ago"]`,
|
|
1198
|
+
'progress-goal': `[milestone done]Phase 1[/milestone]\n[milestone]Phase 2[/milestone]`,
|
|
1199
|
+
};
|
|
1200
|
+
const resolvedBody = tier2defaults[s.layout] || body;
|
|
1201
|
+
|
|
1202
|
+
return `[card${attrStr}]\n${resolvedBody}\n[/card]`;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function _closeDialog() {
|
|
1206
|
+
if (_overlay) { _overlay.remove(); _overlay = null; _state = null; }
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function _insert() {
|
|
1210
|
+
const snippet = _buildShortcode();
|
|
1211
|
+
const start = _ta.selectionStart;
|
|
1212
|
+
const end = _ta.selectionEnd;
|
|
1213
|
+
_ta.value = _ta.value.slice(0, start) + snippet + _ta.value.slice(end);
|
|
1214
|
+
_ta.selectionStart = _ta.selectionEnd = start + snippet.length;
|
|
1215
|
+
_ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1216
|
+
_ta.focus();
|
|
1217
|
+
_closeDialog();
|
|
1218
|
+
}
|
|
1219
|
+
```
|
|
1220
|
+
|
|
1221
|
+
- [ ] **Step 3: Verify module syntax**
|
|
1222
|
+
|
|
1223
|
+
```bash
|
|
1224
|
+
node --input-type=module << 'EOF'
|
|
1225
|
+
import { CardBuilder } from '/home/darryl/src/js/domma-cms/admin/js/lib/card-builder.js';
|
|
1226
|
+
console.log('OK:', Object.keys(CardBuilder).join(', '));
|
|
1227
|
+
EOF
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
Expected: `OK: open, close`
|
|
1231
|
+
|
|
1232
|
+
- [ ] **Step 4: Commit**
|
|
1233
|
+
|
|
1234
|
+
```bash
|
|
1235
|
+
git add admin/js/lib/card-builder.js
|
|
1236
|
+
git commit -m "feat(admin): add card-builder module scaffold + shortcode generator"
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
---
|
|
1240
|
+
|
|
1241
|
+
## Task 11: card-builder.js - Dialog DOM + control panel
|
|
1242
|
+
|
|
1243
|
+
**Files:**
|
|
1244
|
+
|
|
1245
|
+
- Modify: `admin/js/lib/card-builder.js`
|
|
1246
|
+
|
|
1247
|
+
- [ ] **Step 1: Add `_buildDialog()` - overlay, two-panel layout, header, footer**
|
|
1248
|
+
|
|
1249
|
+
```js
|
|
1250
|
+
function _buildDialog() {
|
|
1251
|
+
_overlay = document.createElement('div');
|
|
1252
|
+
Object.assign(_overlay.style, {
|
|
1253
|
+
position:'fixed', inset:'0', background:'rgba(0,0,0,.6)',
|
|
1254
|
+
display:'flex', alignItems:'center', justifyContent:'center',
|
|
1255
|
+
zIndex:'9999', padding:'24px',
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
const dialog = document.createElement('div');
|
|
1259
|
+
Object.assign(dialog.style, {
|
|
1260
|
+
background:'#1e293b', border:'1px solid #334155', borderRadius:'14px',
|
|
1261
|
+
width:'100%', maxWidth:'860px', maxHeight:'90vh',
|
|
1262
|
+
display:'flex', flexDirection:'column',
|
|
1263
|
+
boxShadow:'0 24px 64px rgba(0,0,0,.6)', overflow:'hidden',
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
const hdr = _el('div', 'display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid #334155;flex-shrink:0');
|
|
1267
|
+
const htitle = _el('span', 'font-weight:700;color:#f1f5f9;font-size:14px');
|
|
1268
|
+
htitle.textContent = 'Insert Card';
|
|
1269
|
+
const closeBtn = _el('button', 'background:#334155;border:none;color:#94a3b8;border-radius:6px;width:26px;height:26px;cursor:pointer;font-size:14px');
|
|
1270
|
+
closeBtn.textContent = '\u2715';
|
|
1271
|
+
closeBtn.addEventListener('click', _closeDialog);
|
|
1272
|
+
hdr.appendChild(htitle);
|
|
1273
|
+
hdr.appendChild(closeBtn);
|
|
1274
|
+
|
|
1275
|
+
const body = _el('div', 'display:grid;grid-template-columns:280px 1fr;flex:1;overflow:hidden');
|
|
1276
|
+
body.appendChild(_buildControlPanel());
|
|
1277
|
+
body.appendChild(_buildPreviewPanel());
|
|
1278
|
+
|
|
1279
|
+
const ftr = _el('div', 'display:flex;justify-content:flex-end;gap:10px;padding:12px 20px;border-top:1px solid #334155;flex-shrink:0');
|
|
1280
|
+
const cancelBtn = _el('button', 'padding:8px 18px;border-radius:8px;border:none;background:#334155;color:#94a3b8;font-size:12px;font-weight:700;cursor:pointer');
|
|
1281
|
+
cancelBtn.textContent = 'Cancel';
|
|
1282
|
+
cancelBtn.addEventListener('click', _closeDialog);
|
|
1283
|
+
const insertBtn = _el('button', 'padding:8px 18px;border-radius:8px;border:none;background:#6366f1;color:#fff;font-size:12px;font-weight:700;cursor:pointer');
|
|
1284
|
+
insertBtn.textContent = 'Insert Card';
|
|
1285
|
+
insertBtn.addEventListener('click', _insert);
|
|
1286
|
+
ftr.appendChild(cancelBtn);
|
|
1287
|
+
ftr.appendChild(insertBtn);
|
|
1288
|
+
|
|
1289
|
+
dialog.appendChild(hdr);
|
|
1290
|
+
dialog.appendChild(body);
|
|
1291
|
+
dialog.appendChild(ftr);
|
|
1292
|
+
_overlay.appendChild(dialog);
|
|
1293
|
+
_overlay.addEventListener('click', e => { if (e.target === _overlay) _closeDialog(); });
|
|
1294
|
+
document.body.appendChild(_overlay);
|
|
1295
|
+
_refreshDynFields();
|
|
1296
|
+
_updatePreview();
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// DOM helper
|
|
1300
|
+
function _el(tag, css = '') {
|
|
1301
|
+
const el = document.createElement(tag);
|
|
1302
|
+
if (css) el.style.cssText = css;
|
|
1303
|
+
return el;
|
|
1304
|
+
}
|
|
1305
|
+
```
|
|
1306
|
+
|
|
1307
|
+
- [ ] **Step 2: Add `_buildControlPanel()` - layout dropdown, style swatches, gradient picker, options**
|
|
1308
|
+
|
|
1309
|
+
```js
|
|
1310
|
+
const IS = 'padding:7px 10px;border-radius:7px;border:1px solid #334155;background:#0f172a;color:#e2e8f0;font-size:12px;font-family:inherit;width:100%';
|
|
1311
|
+
|
|
1312
|
+
function _buildControlPanel() {
|
|
1313
|
+
const panel = _el('div', 'padding:18px;border-right:1px solid #334155;display:flex;flex-direction:column;gap:14px;overflow-y:auto');
|
|
1314
|
+
panel.appendChild(_field('Layout', _layoutSelect()));
|
|
1315
|
+
|
|
1316
|
+
const dynFields = _el('div', 'display:contents');
|
|
1317
|
+
dynFields.id = 'cb-dyn-fields';
|
|
1318
|
+
panel.appendChild(dynFields);
|
|
1319
|
+
|
|
1320
|
+
panel.appendChild(_divider());
|
|
1321
|
+
panel.appendChild(_field('Style', _styleSwatches()));
|
|
1322
|
+
|
|
1323
|
+
const gradWrap = _el('div');
|
|
1324
|
+
gradWrap.id = 'cb-grad-wrap';
|
|
1325
|
+
gradWrap.style.display = _state.variant === 'gradient' ? '' : 'none';
|
|
1326
|
+
gradWrap.appendChild(_field('Gradient', _gradientPicker()));
|
|
1327
|
+
panel.appendChild(gradWrap);
|
|
1328
|
+
|
|
1329
|
+
panel.appendChild(_field('Options', _optionToggles()));
|
|
1330
|
+
return panel;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function _layoutSelect() {
|
|
1334
|
+
const sel = _el('select', IS + ';cursor:pointer');
|
|
1335
|
+
const groups = {};
|
|
1336
|
+
LAYOUTS.forEach(l => { (groups[l.group] = groups[l.group] || []).push(l); });
|
|
1337
|
+
Object.entries(groups).forEach(([grp, items]) => {
|
|
1338
|
+
const og = document.createElement('optgroup');
|
|
1339
|
+
og.label = grp;
|
|
1340
|
+
items.forEach(l => {
|
|
1341
|
+
const opt = document.createElement('option');
|
|
1342
|
+
opt.value = l.key;
|
|
1343
|
+
opt.textContent = `${l.thumb} ${l.label}`;
|
|
1344
|
+
if (l.key === _state.layout) opt.selected = true;
|
|
1345
|
+
og.appendChild(opt);
|
|
1346
|
+
});
|
|
1347
|
+
sel.appendChild(og);
|
|
1348
|
+
});
|
|
1349
|
+
sel.addEventListener('change', () => { _state.layout = sel.value; _refreshDynFields(); _updatePreview(); });
|
|
1350
|
+
return sel;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function _styleSwatches() {
|
|
1354
|
+
const wrap = _el('div', 'display:grid;grid-template-columns:repeat(3,1fr);gap:5px');
|
|
1355
|
+
const swStyle = {
|
|
1356
|
+
clean: 'background:#f8fafc;color:#374151;border:2px solid #e2e8f0',
|
|
1357
|
+
gradient:'background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;border:2px solid transparent',
|
|
1358
|
+
glass: 'background:rgba(255,255,255,.06);color:#94a3b8;border:2px dashed #334155',
|
|
1359
|
+
accent: 'background:#fff;color:#4338ca;border-left:4px solid #6366f1;border-top:2px solid #e2e8f0;border-right:2px solid #e2e8f0;border-bottom:2px solid #e2e8f0',
|
|
1360
|
+
dark: 'background:#0f172a;color:#818cf8;border:2px solid #334155',
|
|
1361
|
+
glow: 'color:#818cf8;border:2px solid #6366f1;box-shadow:0 0 6px rgba(99,102,241,.4)',
|
|
1362
|
+
};
|
|
1363
|
+
VARIANTS.forEach(v => {
|
|
1364
|
+
const sw = _el('div', `padding:6px 4px;border-radius:7px;font-size:10px;font-weight:700;text-align:center;cursor:pointer;${swStyle[v]}`);
|
|
1365
|
+
sw.textContent = v.charAt(0).toUpperCase() + v.slice(1);
|
|
1366
|
+
sw.dataset.variant = v;
|
|
1367
|
+
if (v === _state.variant) sw.style.outline = '2px solid #6366f1';
|
|
1368
|
+
sw.addEventListener('click', () => {
|
|
1369
|
+
_state.variant = v;
|
|
1370
|
+
wrap.querySelectorAll('[data-variant]').forEach(s => { s.style.outline = ''; });
|
|
1371
|
+
sw.style.outline = '2px solid #6366f1';
|
|
1372
|
+
const gw = document.getElementById('cb-grad-wrap');
|
|
1373
|
+
if (gw) gw.style.display = v === 'gradient' ? '' : 'none';
|
|
1374
|
+
_updatePreview();
|
|
1375
|
+
});
|
|
1376
|
+
wrap.appendChild(sw);
|
|
1377
|
+
});
|
|
1378
|
+
return wrap;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function _gradientPicker() {
|
|
1382
|
+
const wrap = _el('div', 'display:flex;flex-wrap:wrap;gap:7px');
|
|
1383
|
+
GRADIENTS.forEach(g => {
|
|
1384
|
+
const [c1, c2] = (GRAD_COLORS[g] || '#6366f1,#8b5cf6').split(',');
|
|
1385
|
+
const c = _el('div', `width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,${c1},${c2});cursor:pointer;border:2px solid transparent;transition:border-color .15s`);
|
|
1386
|
+
c.title = g;
|
|
1387
|
+
if (g === _state.gradient) c.style.borderColor = '#fff';
|
|
1388
|
+
c.addEventListener('click', () => {
|
|
1389
|
+
_state.gradient = g;
|
|
1390
|
+
wrap.querySelectorAll('div').forEach(d => { d.style.borderColor = 'transparent'; });
|
|
1391
|
+
c.style.borderColor = '#fff';
|
|
1392
|
+
_updatePreview();
|
|
1393
|
+
});
|
|
1394
|
+
wrap.appendChild(c);
|
|
1395
|
+
});
|
|
1396
|
+
return wrap;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function _optionToggles() {
|
|
1400
|
+
const row = _el('div', 'display:flex;gap:6px');
|
|
1401
|
+
['hover','collapsible'].forEach(key => {
|
|
1402
|
+
const btn = _el('div');
|
|
1403
|
+
const setStyle = (on) => {
|
|
1404
|
+
btn.style.cssText = `flex:1;padding:6px 8px;border-radius:7px;border:1px solid;font-size:11px;font-weight:700;text-align:center;cursor:pointer;${on ? 'border-color:#6366f1;background:#1e1b4b;color:#a5b4fc' : 'border-color:#334155;background:#0f172a;color:#64748b'}`;
|
|
1405
|
+
};
|
|
1406
|
+
setStyle(_state[key]);
|
|
1407
|
+
btn.textContent = key.charAt(0).toUpperCase() + key.slice(1);
|
|
1408
|
+
btn.addEventListener('click', () => { _state[key] = !_state[key]; setStyle(_state[key]); _updatePreview(); });
|
|
1409
|
+
row.appendChild(btn);
|
|
1410
|
+
});
|
|
1411
|
+
return row;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// Field builder helpers
|
|
1415
|
+
function _field(label, el) {
|
|
1416
|
+
const wrap = _el('div', 'display:flex;flex-direction:column;gap:5px');
|
|
1417
|
+
const lbl = _el('div', 'font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:#475569');
|
|
1418
|
+
lbl.textContent = label;
|
|
1419
|
+
wrap.appendChild(lbl);
|
|
1420
|
+
wrap.appendChild(el);
|
|
1421
|
+
return wrap;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function _divider() {
|
|
1425
|
+
const d = _el('div', 'height:1px;background:#1e293b;margin:2px 0');
|
|
1426
|
+
return d;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function _textInput(stateKey, placeholder) {
|
|
1430
|
+
const input = _el('input', IS);
|
|
1431
|
+
input.type = 'text';
|
|
1432
|
+
if (placeholder) input.placeholder = placeholder;
|
|
1433
|
+
input.value = _state[stateKey] || '';
|
|
1434
|
+
input.addEventListener('input', () => { _state[stateKey] = input.value; _updatePreview(); });
|
|
1435
|
+
return input;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function _selectInput(stateKey, options) {
|
|
1439
|
+
const sel = _el('select', IS + ';cursor:pointer');
|
|
1440
|
+
options.forEach(o => {
|
|
1441
|
+
const opt = document.createElement('option');
|
|
1442
|
+
opt.value = o; opt.textContent = o;
|
|
1443
|
+
if (o === _state[stateKey]) opt.selected = true;
|
|
1444
|
+
sel.appendChild(opt);
|
|
1445
|
+
});
|
|
1446
|
+
sel.addEventListener('change', () => { _state[stateKey] = sel.value; _updatePreview(); });
|
|
1447
|
+
return sel;
|
|
1448
|
+
}
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
- [ ] **Step 3: Add `_refreshDynFields()` - replaces the dynamic fields on layout change**
|
|
1452
|
+
|
|
1453
|
+
```js
|
|
1454
|
+
const LAYOUT_FIELDS = {
|
|
1455
|
+
'icon-top': ['subtitle','icon','iconLayout'],
|
|
1456
|
+
'icon-inline': ['subtitle','icon','iconLayout'],
|
|
1457
|
+
'image-top': ['image'],
|
|
1458
|
+
'image-overlay': ['image'],
|
|
1459
|
+
'thumb-left': ['image'],
|
|
1460
|
+
'thumb-right': ['image'],
|
|
1461
|
+
'wide-left-image': ['image'],
|
|
1462
|
+
'full-bg': ['image'],
|
|
1463
|
+
'split-half': ['image'],
|
|
1464
|
+
'avatar-profile': ['subtitle','icon','tags'],
|
|
1465
|
+
'stat-metric': ['value','delta','progress'],
|
|
1466
|
+
'quote-testimonial': ['subtitle','icon'],
|
|
1467
|
+
'callout': ['calloutType','icon'],
|
|
1468
|
+
'video-media': ['image','duration'],
|
|
1469
|
+
'location-map': ['address'],
|
|
1470
|
+
'step-numbered': ['step'],
|
|
1471
|
+
'corner-badge': ['icon','badge'],
|
|
1472
|
+
'badge-band': ['icon','badge'],
|
|
1473
|
+
'rating-review': ['rating','subtitle','icon'],
|
|
1474
|
+
'tag-cloud': ['tags'],
|
|
1475
|
+
'activity-feed': [],
|
|
1476
|
+
'code-snippet': ['lang'],
|
|
1477
|
+
'file-document': ['filename','filesize','filetype'],
|
|
1478
|
+
'before-after': [],
|
|
1479
|
+
'pricing': ['price','period'],
|
|
1480
|
+
'feature-comparison':['subtitle'],
|
|
1481
|
+
'progress-goal': ['progress'],
|
|
1482
|
+
'timeline-entry': ['subtitle','badge'],
|
|
1483
|
+
'glass-gradient-border':[],
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
const NO_TITLE = new Set(['basic','no-header-footer','code-snippet','file-document','stat-metric']);
|
|
1487
|
+
const NO_BODY = new Set(['stat-metric','file-document']);
|
|
1488
|
+
const NO_FOOTER = new Set(['pricing','feature-comparison','activity-feed','before-after','progress-goal']);
|
|
1489
|
+
|
|
1490
|
+
function _refreshDynFields() {
|
|
1491
|
+
const container = document.getElementById('cb-dyn-fields');
|
|
1492
|
+
if (!container) return;
|
|
1493
|
+
while (container.firstChild) container.removeChild(container.firstChild);
|
|
1494
|
+
|
|
1495
|
+
const layout = _state.layout;
|
|
1496
|
+
const fields = LAYOUT_FIELDS[layout] || [];
|
|
1497
|
+
|
|
1498
|
+
if (!NO_TITLE.has(layout)) container.appendChild(_field('Title', _textInput('title', 'Card heading')));
|
|
1499
|
+
|
|
1500
|
+
fields.forEach(f => {
|
|
1501
|
+
const fieldMap = {
|
|
1502
|
+
subtitle: () => _field('Subtitle', _textInput('subtitle', 'Secondary line')),
|
|
1503
|
+
icon: () => _field('Icon', _iconRow()),
|
|
1504
|
+
iconLayout: () => _field('Icon layout', _iconLayoutToggle()),
|
|
1505
|
+
image: () => _field('Image', _imageRow()),
|
|
1506
|
+
value: () => _field('Value', _textInput('value', '\u00a324,980')),
|
|
1507
|
+
delta: () => _field('Delta', _textInput('delta', '\u2191 12.4%')),
|
|
1508
|
+
progress: () => _field('Progress %', _textInput('progress', '50')),
|
|
1509
|
+
price: () => _field('Price', _textInput('price', '\u00a329')),
|
|
1510
|
+
period: () => _field('Period', _textInput('period', 'per month')),
|
|
1511
|
+
lang: () => _field('Language', _textInput('lang', 'JavaScript')),
|
|
1512
|
+
filename: () => _field('Filename', _textInput('filename', 'document.pdf')),
|
|
1513
|
+
filesize: () => _field('File size', _textInput('filesize', '2.4 MB')),
|
|
1514
|
+
filetype: () => _field('File type', _textInput('filetype', 'PDF')),
|
|
1515
|
+
rating: () => _field('Rating (1-5)', _textInput('rating', '5')),
|
|
1516
|
+
duration: () => _field('Duration', _textInput('duration', '4:32')),
|
|
1517
|
+
address: () => _field('Address', _textInput('address', '12 Main St')),
|
|
1518
|
+
step: () => _field('Step number', _textInput('step', '1')),
|
|
1519
|
+
badge: () => _field('Badge label', _textInput('badge', 'New')),
|
|
1520
|
+
tags: () => _field('Tags (comma-separated)', _textInput('tags', 'Design, CSS')),
|
|
1521
|
+
calloutType: () => _field('Type', _selectInput('calloutType', ['info','warn','success','error'])),
|
|
1522
|
+
};
|
|
1523
|
+
if (fieldMap[f]) container.appendChild(fieldMap[f]());
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
if (!NO_BODY.has(layout)) container.appendChild(_field('Body content', _bodyTextarea()));
|
|
1527
|
+
if (!NO_FOOTER.has(layout)) container.appendChild(_field('Footer', _textInput('footer', '')));
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function _bodyTextarea() {
|
|
1531
|
+
const ta = _el('textarea', IS + ';resize:vertical;min-height:60px;line-height:1.5');
|
|
1532
|
+
ta.value = _state.body || '';
|
|
1533
|
+
ta.addEventListener('input', () => { _state.body = ta.value; _updatePreview(); });
|
|
1534
|
+
return ta;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function _iconRow() {
|
|
1538
|
+
const row = _el('div', 'display:flex;gap:6px');
|
|
1539
|
+
const input = _el('input', IS + ';flex:1');
|
|
1540
|
+
input.type = 'text'; input.placeholder = 'icon name';
|
|
1541
|
+
input.value = _state.icon || '';
|
|
1542
|
+
input.addEventListener('input', () => { _state.icon = input.value; _updatePreview(); });
|
|
1543
|
+
const btn = _el('button', 'padding:0 10px;height:34px;background:#334155;border:none;color:#94a3b8;border-radius:7px;font-size:13px;cursor:pointer;flex-shrink:0');
|
|
1544
|
+
btn.title = 'Browse icons';
|
|
1545
|
+
btn.textContent = '\u2605';
|
|
1546
|
+
btn.addEventListener('click', () => _openIconPicker(name => { _state.icon = name; input.value = name; _updatePreview(); }));
|
|
1547
|
+
row.appendChild(input);
|
|
1548
|
+
row.appendChild(btn);
|
|
1549
|
+
return row;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function _iconLayoutToggle() {
|
|
1553
|
+
const row = _el('div', 'display:flex;gap:6px');
|
|
1554
|
+
['inline','stacked'].forEach(v => {
|
|
1555
|
+
const btn = _el('div');
|
|
1556
|
+
const setS = (on) => {
|
|
1557
|
+
btn.style.cssText = `flex:1;padding:6px;border-radius:7px;border:1px solid;font-size:11px;font-weight:700;text-align:center;cursor:pointer;${on ? 'border-color:#6366f1;background:#1e1b4b;color:#a5b4fc' : 'border-color:#334155;background:#0f172a;color:#64748b'}`;
|
|
1558
|
+
};
|
|
1559
|
+
setS(_state.iconLayout === v);
|
|
1560
|
+
btn.textContent = v.charAt(0).toUpperCase() + v.slice(1);
|
|
1561
|
+
btn.addEventListener('click', () => {
|
|
1562
|
+
_state.iconLayout = v;
|
|
1563
|
+
row.querySelectorAll('div').forEach((b, i) => setS(['inline','stacked'][i] === v));
|
|
1564
|
+
_updatePreview();
|
|
1565
|
+
});
|
|
1566
|
+
row.appendChild(btn);
|
|
1567
|
+
});
|
|
1568
|
+
return row;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function _imageRow() {
|
|
1572
|
+
const row = _el('div', 'display:flex;gap:6px');
|
|
1573
|
+
const input = _el('input', IS + ';flex:1');
|
|
1574
|
+
input.type = 'text'; input.placeholder = '/media/image.jpg';
|
|
1575
|
+
input.value = _state.image || '';
|
|
1576
|
+
input.addEventListener('input', () => { _state.image = input.value; _updatePreview(); });
|
|
1577
|
+
const btn = _el('button', 'padding:0 10px;height:34px;background:#334155;border:none;color:#94a3b8;border-radius:7px;font-size:13px;cursor:pointer;flex-shrink:0');
|
|
1578
|
+
btn.title = 'Browse media';
|
|
1579
|
+
btn.textContent = '\uD83D\uDCC1';
|
|
1580
|
+
btn.addEventListener('click', async () => {
|
|
1581
|
+
if (_opts && typeof _opts.onMediaBrowse === 'function') {
|
|
1582
|
+
const url = await _opts.onMediaBrowse();
|
|
1583
|
+
if (url) { _state.image = url; input.value = url; _updatePreview(); }
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
row.appendChild(input);
|
|
1587
|
+
row.appendChild(btn);
|
|
1588
|
+
return row;
|
|
1589
|
+
}
|
|
1590
|
+
```
|
|
1591
|
+
|
|
1592
|
+
- [ ] **Step 4: Commit**
|
|
1593
|
+
|
|
1594
|
+
```bash
|
|
1595
|
+
git add admin/js/lib/card-builder.js
|
|
1596
|
+
git commit -m "feat(admin): add card builder dialog DOM + dynamic control panel"
|
|
1597
|
+
```
|
|
1598
|
+
|
|
1599
|
+
---
|
|
1600
|
+
|
|
1601
|
+
## Task 12: card-builder.js - Preview panel + live preview renderer
|
|
1602
|
+
|
|
1603
|
+
**Files:**
|
|
1604
|
+
|
|
1605
|
+
- Modify: `admin/js/lib/card-builder.js`
|
|
1606
|
+
|
|
1607
|
+
- [ ] **Step 1: Add `_buildPreviewPanel()` - right panel structure**
|
|
1608
|
+
|
|
1609
|
+
```js
|
|
1610
|
+
function _buildPreviewPanel() {
|
|
1611
|
+
const panel = _el('div', 'padding:20px;background:#f8fafc;display:flex;flex-direction:column;gap:14px;overflow-y:auto');
|
|
1612
|
+
|
|
1613
|
+
const lbl = _el('div', 'font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#94a3b8');
|
|
1614
|
+
lbl.textContent = 'Live Preview';
|
|
1615
|
+
|
|
1616
|
+
const area = _el('div', 'flex:1;display:flex;align-items:flex-start;justify-content:center');
|
|
1617
|
+
area.id = 'cb-preview-area';
|
|
1618
|
+
|
|
1619
|
+
const sLbl = _el('div', 'font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:#94a3b8;margin-bottom:6px');
|
|
1620
|
+
sLbl.textContent = 'Shortcode';
|
|
1621
|
+
|
|
1622
|
+
const sBox = _el('div', 'background:#1e293b;border:1px solid #334155;border-radius:8px;padding:10px 14px;font-family:monospace;font-size:11px;color:#a5b4fc;line-height:1.8;white-space:pre-wrap;word-break:break-all');
|
|
1623
|
+
sBox.id = 'cb-snippet';
|
|
1624
|
+
|
|
1625
|
+
panel.appendChild(lbl);
|
|
1626
|
+
panel.appendChild(area);
|
|
1627
|
+
panel.appendChild(sLbl);
|
|
1628
|
+
panel.appendChild(sBox);
|
|
1629
|
+
return panel;
|
|
1630
|
+
}
|
|
1631
|
+
```
|
|
1632
|
+
|
|
1633
|
+
- [ ] **Step 2: Add `_updatePreview()` and `_buildPreviewCard()`**
|
|
1634
|
+
|
|
1635
|
+
```js
|
|
1636
|
+
function _updatePreview() {
|
|
1637
|
+
const area = document.getElementById('cb-preview-area');
|
|
1638
|
+
if (!area) return;
|
|
1639
|
+
while (area.firstChild) area.removeChild(area.firstChild);
|
|
1640
|
+
area.appendChild(_buildPreviewCard());
|
|
1641
|
+
const box = document.getElementById('cb-snippet');
|
|
1642
|
+
if (box) box.textContent = _buildShortcode();
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function _buildPreviewCard() {
|
|
1646
|
+
const s = _state;
|
|
1647
|
+
const card = _el('div');
|
|
1648
|
+
card.style.cssText = 'width:100%;max-width:300px;border-radius:12px;overflow:hidden;font-size:13px;font-family:system-ui,sans-serif';
|
|
1649
|
+
card.className = ['card','mb-4', s.variant !== 'clean' ? `dm-card-${s.variant}` : '', s.variant === 'gradient' ? `card-gradient-${s.gradient}` : ''].filter(Boolean).join(' ');
|
|
1650
|
+
|
|
1651
|
+
// Image area (for image-based layouts)
|
|
1652
|
+
const imgLayouts = new Set(['image-top','image-overlay','thumb-left','thumb-right','wide-left-image','full-bg','split-half','video-media']);
|
|
1653
|
+
if (imgLayouts.has(s.layout)) {
|
|
1654
|
+
const img = _el('div');
|
|
1655
|
+
img.className = 'card-img-top';
|
|
1656
|
+
if (s.image) img.style.backgroundImage = `url('${s.image}')`;
|
|
1657
|
+
img.style.backgroundSize = 'cover';
|
|
1658
|
+
img.style.backgroundPosition = 'center';
|
|
1659
|
+
if (!s.image) img.textContent = '\uD83D\uDDBC';
|
|
1660
|
+
card.appendChild(img);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// Header
|
|
1664
|
+
const noHdr = new Set(['basic','no-header-footer','image-overlay','stat-metric','quote-testimonial','activity-feed','before-after','code-snippet','file-document','glass-gradient-border']);
|
|
1665
|
+
if (!noHdr.has(s.layout) && s.title) {
|
|
1666
|
+
const hdr = _el('div', '');
|
|
1667
|
+
hdr.className = s.variant === 'gradient' ? `card-header card-gradient-${s.gradient}` : 'card-header';
|
|
1668
|
+
const t = _el('div', 'font-weight:700');
|
|
1669
|
+
t.textContent = s.title;
|
|
1670
|
+
hdr.appendChild(t);
|
|
1671
|
+
card.appendChild(hdr);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// Body
|
|
1675
|
+
const body = _el('div', '');
|
|
1676
|
+
body.className = 'card-body';
|
|
1677
|
+
const tier2 = new Set(['before-after','pricing','feature-comparison','activity-feed','progress-goal']);
|
|
1678
|
+
if (tier2.has(s.layout)) {
|
|
1679
|
+
const note = _el('em', 'font-size:11px;color:#9ca3af');
|
|
1680
|
+
note.textContent = 'Simplified preview - full render on save.';
|
|
1681
|
+
body.appendChild(note);
|
|
1682
|
+
} else if (s.layout === 'stat-metric') {
|
|
1683
|
+
const val = _el('div', 'font-size:2rem;font-weight:800;color:#111827');
|
|
1684
|
+
val.textContent = s.value || '-';
|
|
1685
|
+
const lbl2 = _el('div', 'font-size:.8rem;color:#6b7280');
|
|
1686
|
+
lbl2.textContent = s.title || '';
|
|
1687
|
+
body.appendChild(lbl2);
|
|
1688
|
+
body.appendChild(val);
|
|
1689
|
+
if (s.delta) { const d = _el('div', 'font-size:.78rem;font-weight:600;color:#10b981'); d.textContent = s.delta; body.appendChild(d); }
|
|
1690
|
+
} else {
|
|
1691
|
+
body.textContent = s.body || 'Card body content.';
|
|
1692
|
+
}
|
|
1693
|
+
card.appendChild(body);
|
|
1694
|
+
|
|
1695
|
+
// Footer
|
|
1696
|
+
if (s.footer) {
|
|
1697
|
+
const ftr = _el('div', '');
|
|
1698
|
+
ftr.className = 'card-footer';
|
|
1699
|
+
ftr.textContent = s.footer;
|
|
1700
|
+
card.appendChild(ftr);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
return card;
|
|
1704
|
+
}
|
|
1705
|
+
```
|
|
1706
|
+
|
|
1707
|
+
- [ ] **Step 3: Commit**
|
|
1708
|
+
|
|
1709
|
+
```bash
|
|
1710
|
+
git add admin/js/lib/card-builder.js
|
|
1711
|
+
git commit -m "feat(admin): add card builder live preview renderer"
|
|
1712
|
+
```
|
|
1713
|
+
|
|
1714
|
+
---
|
|
1715
|
+
|
|
1716
|
+
## Task 13: card-builder.js - Icon picker overlay
|
|
1717
|
+
|
|
1718
|
+
**Files:**
|
|
1719
|
+
|
|
1720
|
+
- Modify: `admin/js/lib/card-builder.js`
|
|
1721
|
+
|
|
1722
|
+
- [ ] **Step 1: Get the icon list from markdown-toolbar.js**
|
|
1723
|
+
|
|
1724
|
+
```bash
|
|
1725
|
+
grep -A 5 "const allIcons\|ALL_ICONS\|iconNames" /home/darryl/src/js/domma-cms/admin/js/lib/markdown-toolbar.js | head -20
|
|
1726
|
+
```
|
|
1727
|
+
|
|
1728
|
+
Find the array name and copy its contents. Store as `const ALL_ICONS = [...]` at the top of card-builder.js.
|
|
1729
|
+
|
|
1730
|
+
- [ ] **Step 2: Add `_openIconPicker(onSelect)` - overlay with search + grid**
|
|
1731
|
+
|
|
1732
|
+
```js
|
|
1733
|
+
function _openIconPicker(onSelect) {
|
|
1734
|
+
const overlay = _el('div', 'position:fixed;inset:0;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;z-index:10000;padding:24px');
|
|
1735
|
+
|
|
1736
|
+
const panel = _el('div', 'background:#1e293b;border:1px solid #475569;border-radius:12px;padding:16px;width:100%;max-width:520px;max-height:70vh;display:flex;flex-direction:column;gap:12px');
|
|
1737
|
+
|
|
1738
|
+
const searchRow = _el('div', 'display:flex;gap:8px');
|
|
1739
|
+
const searchInput = _el('input', 'flex:1;padding:8px 12px;border-radius:8px;border:1px solid #334155;background:#0f172a;color:#e2e8f0;font-size:13px;font-family:inherit');
|
|
1740
|
+
searchInput.type = 'text'; searchInput.placeholder = 'Search icons\u2026';
|
|
1741
|
+
const closeBtn2 = _el('button', 'padding:8px 14px;background:#334155;border:none;border-radius:8px;color:#94a3b8;font-size:12px;cursor:pointer');
|
|
1742
|
+
closeBtn2.textContent = '\u2715 Close';
|
|
1743
|
+
closeBtn2.addEventListener('click', () => overlay.remove());
|
|
1744
|
+
searchRow.appendChild(searchInput);
|
|
1745
|
+
searchRow.appendChild(closeBtn2);
|
|
1746
|
+
|
|
1747
|
+
const grid = _el('div', 'display:grid;grid-template-columns:repeat(auto-fill,minmax(52px,1fr));gap:6px;overflow-y:auto;max-height:400px');
|
|
1748
|
+
let selectedItem = null;
|
|
1749
|
+
|
|
1750
|
+
const confirmBtn = _el('button', 'padding:8px 18px;background:#6366f1;border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:700;cursor:pointer;align-self:flex-end');
|
|
1751
|
+
confirmBtn.textContent = 'Select an icon';
|
|
1752
|
+
|
|
1753
|
+
function renderGrid(filter) {
|
|
1754
|
+
while (grid.firstChild) grid.removeChild(grid.firstChild);
|
|
1755
|
+
const icons = filter
|
|
1756
|
+
? ALL_ICONS.filter(n => n.includes(filter.toLowerCase()))
|
|
1757
|
+
: ALL_ICONS;
|
|
1758
|
+
icons.slice(0, 120).forEach(name => {
|
|
1759
|
+
const item = _el('div', 'display:flex;flex-direction:column;align-items:center;gap:4px;padding:8px 4px;border-radius:8px;cursor:pointer;border:2px solid transparent;background:#0f172a');
|
|
1760
|
+
const glyph = document.createElement('span');
|
|
1761
|
+
glyph.setAttribute('data-icon', name);
|
|
1762
|
+
glyph.style.fontSize = '20px';
|
|
1763
|
+
const label = _el('div', 'font-size:8px;color:#475569;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:100%');
|
|
1764
|
+
label.textContent = name;
|
|
1765
|
+
item.appendChild(glyph);
|
|
1766
|
+
item.appendChild(label);
|
|
1767
|
+
item.addEventListener('click', () => {
|
|
1768
|
+
if (selectedItem) selectedItem.style.borderColor = 'transparent';
|
|
1769
|
+
item.style.borderColor = '#6366f1';
|
|
1770
|
+
selectedItem = item;
|
|
1771
|
+
confirmBtn.textContent = `Use: ${name}`;
|
|
1772
|
+
});
|
|
1773
|
+
item.addEventListener('dblclick', () => { onSelect(name); overlay.remove(); });
|
|
1774
|
+
grid.appendChild(item);
|
|
1775
|
+
});
|
|
1776
|
+
if (typeof I !== 'undefined' && I.scan) I.scan(grid);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
searchInput.addEventListener('input', () => renderGrid(searchInput.value));
|
|
1780
|
+
renderGrid('');
|
|
1781
|
+
|
|
1782
|
+
confirmBtn.addEventListener('click', () => {
|
|
1783
|
+
if (selectedItem) {
|
|
1784
|
+
const name = selectedItem.querySelector('[data-icon]').getAttribute('data-icon');
|
|
1785
|
+
onSelect(name);
|
|
1786
|
+
overlay.remove();
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
panel.appendChild(searchRow);
|
|
1791
|
+
panel.appendChild(grid);
|
|
1792
|
+
panel.appendChild(confirmBtn);
|
|
1793
|
+
overlay.appendChild(panel);
|
|
1794
|
+
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
|
|
1795
|
+
document.body.appendChild(overlay);
|
|
1796
|
+
searchInput.focus();
|
|
1797
|
+
}
|
|
1798
|
+
```
|
|
1799
|
+
|
|
1800
|
+
- [ ] **Step 3: Commit**
|
|
1801
|
+
|
|
1802
|
+
```bash
|
|
1803
|
+
git add admin/js/lib/card-builder.js
|
|
1804
|
+
git commit -m "feat(admin): add icon picker overlay to card builder"
|
|
1805
|
+
```
|
|
1806
|
+
|
|
1807
|
+
---
|
|
1808
|
+
|
|
1809
|
+
## Task 14: page-editor.js - Wire toolbar.onCard()
|
|
1810
|
+
|
|
1811
|
+
**Files:**
|
|
1812
|
+
|
|
1813
|
+
- Modify: `admin/js/views/page-editor.js`
|
|
1814
|
+
|
|
1815
|
+
- [ ] **Step 1: Find toolbar initialisation and existing onXxx handlers**
|
|
1816
|
+
|
|
1817
|
+
```bash
|
|
1818
|
+
grep -n 'toolbar\.\|onCard\|onGrid\|onImage\|MarkdownToolbar\|markdown-toolbar' /home/darryl/src/js/domma-cms/admin/js/views/page-editor.js | head -30
|
|
1819
|
+
```
|
|
1820
|
+
|
|
1821
|
+
Identify:
|
|
1822
|
+
|
|
1823
|
+
1. Where the toolbar instance is created
|
|
1824
|
+
2. Where other `toolbar.onXxx()` handlers are registered (e.g. `toolbar.onImage`, `toolbar.onGrid`)
|
|
1825
|
+
3. How the existing media picker is called (pattern for `onMediaBrowse` callback)
|
|
1826
|
+
|
|
1827
|
+
- [ ] **Step 2: Add import at top of page-editor.js**
|
|
1828
|
+
|
|
1829
|
+
```js
|
|
1830
|
+
import { CardBuilder } from '../lib/card-builder.js';
|
|
1831
|
+
```
|
|
1832
|
+
|
|
1833
|
+
- [ ] **Step 3: Register onCard handler alongside other toolbar handlers**
|
|
1834
|
+
|
|
1835
|
+
```js
|
|
1836
|
+
toolbar.onCard((ta) => {
|
|
1837
|
+
CardBuilder.open(ta, {
|
|
1838
|
+
onMediaBrowse: () => new Promise((resolve) => {
|
|
1839
|
+
// Mirror the existing media picker invocation pattern.
|
|
1840
|
+
// Check how toolbar.onImage or toolbar.onLink calls the media modal
|
|
1841
|
+
// and adapt the same call here, resolving with the selected URL.
|
|
1842
|
+
openMediaPicker(resolve); // replace with the actual function name used in page-editor
|
|
1843
|
+
}),
|
|
1844
|
+
});
|
|
1845
|
+
});
|
|
1846
|
+
```
|
|
1847
|
+
|
|
1848
|
+
Look at the existing `toolbar.onImage` or `toolbar.onLink` handler - copy its media modal invocation pattern exactly.
|
|
1849
|
+
|
|
1850
|
+
- [ ] **Step 4: Verify in browser**
|
|
1851
|
+
|
|
1852
|
+
1. Open `http://localhost:4096/admin/#/pages`
|
|
1853
|
+
2. Open any page in the editor
|
|
1854
|
+
3. Click **Card** in the toolbar
|
|
1855
|
+
4. Confirm dialog opens with 33 layouts in the dropdown
|
|
1856
|
+
5. Change layout - fields update, preview updates
|
|
1857
|
+
6. Change variant - preview updates
|
|
1858
|
+
7. Click Insert - shortcode inserted at cursor
|
|
1859
|
+
8. Preview page - card renders correctly
|
|
1860
|
+
|
|
1861
|
+
- [ ] **Step 5: Commit**
|
|
1862
|
+
|
|
1863
|
+
```bash
|
|
1864
|
+
git add admin/js/views/page-editor.js
|
|
1865
|
+
git commit -m "feat(admin): wire card builder into page editor toolbar"
|
|
1866
|
+
```
|
|
1867
|
+
|
|
1868
|
+
---
|
|
1869
|
+
|
|
1870
|
+
## Task 15: End-to-end verification
|
|
1871
|
+
|
|
1872
|
+
**Files:**
|
|
1873
|
+
|
|
1874
|
+
- Temporary: `content/pages/card-e2e-test.md`
|
|
1875
|
+
|
|
1876
|
+
- [ ] **Step 1: Create comprehensive test page**
|
|
1877
|
+
|
|
1878
|
+
```markdown
|
|
1879
|
+
---
|
|
1880
|
+
title: Card E2E Test
|
|
1881
|
+
layout: default
|
|
1882
|
+
---
|
|
1883
|
+
|
|
1884
|
+
[card variant="clean" title="Clean" hover]Plain body text.[/card]
|
|
1885
|
+
|
|
1886
|
+
[card layout="image-top" title="Image Top" variant="gradient" gradient="aurora" footer="5 min read" hover]Short description.[/card]
|
|
1887
|
+
|
|
1888
|
+
[card layout="callout" callout-type="info" icon="bulb" title="Tip"]Body here.[/card]
|
|
1889
|
+
|
|
1890
|
+
[card layout="stat-metric" title="Revenue" value="£24,980" delta="↑ 12.4%" progress="68"][/card]
|
|
1891
|
+
|
|
1892
|
+
[card layout="pricing" title="Pro" price="£29" period="per month" variant="gradient" gradient="indigo"]
|
|
1893
|
+
[feature]Unlimited pages[/feature]
|
|
1894
|
+
[feature]All plugins[/feature]
|
|
1895
|
+
[/card]
|
|
1896
|
+
|
|
1897
|
+
[card layout="before-after" title="Why switch?"]
|
|
1898
|
+
[before]Manual updates · Dev dependency[/before]
|
|
1899
|
+
[after]Auto deploy · Editor-owned[/after]
|
|
1900
|
+
[/card]
|
|
1901
|
+
|
|
1902
|
+
[card layout="glass-gradient-border" title="Glass card"]Frosted glass border.[/card]
|
|
1903
|
+
|
|
1904
|
+
[card layout="timeline-entry" title="Plugin system" subtitle="Apr 2026" badge="Feature"]Plugins can now inject routes.[/card]
|
|
1905
|
+
|
|
1906
|
+
[card layout="code-snippet" lang="JavaScript"]const x = 1 + 2;[/card]
|
|
1907
|
+
```
|
|
1908
|
+
|
|
1909
|
+
- [ ] **Step 2: Verify all layouts render**
|
|
1910
|
+
|
|
1911
|
+
```bash
|
|
1912
|
+
curl -s http://localhost:4096/card-e2e-test > /tmp/card-test.html
|
|
1913
|
+
grep -c 'dm-card\|card-gradient\|card-callout\|card-stat\|card-pricing\|card-compare\|card-glass\|card-timeline\|card-code' /tmp/card-test.html
|
|
1914
|
+
```
|
|
1915
|
+
|
|
1916
|
+
Expected: `9` or more
|
|
1917
|
+
|
|
1918
|
+
- [ ] **Step 3: Visual verification in browser** - `http://localhost:4096/card-e2e-test`
|
|
1919
|
+
|
|
1920
|
+
Confirm:
|
|
1921
|
+
|
|
1922
|
+
- Clean card lifts on hover
|
|
1923
|
+
- Gradient card (aurora) shows coloured header
|
|
1924
|
+
- Callout has left accent stripe
|
|
1925
|
+
- Stat shows large value + progress bar
|
|
1926
|
+
- Pricing shows feature list
|
|
1927
|
+
- Before/After shows two columns
|
|
1928
|
+
- Glass has frosted appearance
|
|
1929
|
+
- Timeline shows dot + line + tag
|
|
1930
|
+
- Code shows dark block with language badge
|
|
1931
|
+
|
|
1932
|
+
- [ ] **Step 4: Card builder dialog verification** - `http://localhost:4096/admin/#/pages`
|
|
1933
|
+
|
|
1934
|
+
- Click Card in toolbar → dialog opens
|
|
1935
|
+
- Select each layout group → fields update correctly
|
|
1936
|
+
- Select Gradient variant → gradient circle picker appears
|
|
1937
|
+
- Type title → preview card updates
|
|
1938
|
+
- Click Browse icons → icon picker opens, search works, double-click selects
|
|
1939
|
+
- Click Insert → shortcode inserted, dialog closes
|
|
1940
|
+
- Preview the page - card renders correctly
|
|
1941
|
+
|
|
1942
|
+
- [ ] **Step 5: Clean up and final commit**
|
|
1943
|
+
|
|
1944
|
+
```bash
|
|
1945
|
+
rm content/pages/card-e2e-test.md
|
|
1946
|
+
git status
|
|
1947
|
+
# commit any remaining uncommitted changes
|
|
1948
|
+
```
|
|
1949
|
+
|
|
1950
|
+
---
|
|
1951
|
+
|
|
1952
|
+
## Summary
|
|
1953
|
+
|
|
1954
|
+
| Task | Produces | Verified by |
|
|
1955
|
+
|------|--------------------------------------|---------------------------------|
|
|
1956
|
+
| 1 | 6 variant CSS classes | `curl grep dm-card-` |
|
|
1957
|
+
| 2 | 14 gradient CSS classes | Visual |
|
|
1958
|
+
| 3 | All layout structural CSS | Visual with layouts |
|
|
1959
|
+
| 4 | Collapsible click handler | Browser click test |
|
|
1960
|
+
| 5 | `processCardBlocks` routing | Server restart + existing cards |
|
|
1961
|
+
| 6 | Basic + icon renderers | `curl grep card-header-icon` |
|
|
1962
|
+
| 7 | Image layout renderers | `curl grep card-img` |
|
|
1963
|
+
| 8 | People/data/special renderers | `curl grep card-callout` |
|
|
1964
|
+
| 9 | Tier 2 sub-tag renderers | `curl grep card-pricing` |
|
|
1965
|
+
| 10 | CardBuilder scaffold + shortcode gen | `node --input-type=module` |
|
|
1966
|
+
| 11 | Control panel + dynamic fields | Browser dialog |
|
|
1967
|
+
| 12 | Live preview renderer | Browser preview |
|
|
1968
|
+
| 13 | Icon picker | Browser icon browse |
|
|
1969
|
+
| 14 | page-editor wiring | Full browser flow |
|
|
1970
|
+
| 15 | End-to-end | All 33 layouts render + dialog |
|