domma-cms 0.74.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CLAUDE.md +3 -1
  2. package/admin/js/app.js +2 -2
  3. package/admin/js/lib/actions-arrange.js +1 -0
  4. package/admin/js/lib/api-endpoints-arrange.js +1 -0
  5. package/admin/js/lib/blocks-arrange.js +1 -0
  6. package/admin/js/lib/collections-arrange.js +1 -0
  7. package/admin/js/lib/components-arrange.js +1 -0
  8. package/admin/js/lib/context-menus-arrange.js +1 -0
  9. package/admin/js/lib/forms-arrange.js +1 -0
  10. package/admin/js/lib/media-arrange.js +1 -0
  11. package/admin/js/lib/menus-arrange.js +1 -0
  12. package/admin/js/lib/pages-arrange.js +1 -0
  13. package/admin/js/lib/sidebar-renderer.js +3 -3
  14. package/admin/js/lib/theme-settings-arrange.js +1 -0
  15. package/admin/js/lib/views-arrange.js +1 -0
  16. package/admin/js/templates/actions-list.html +73 -14
  17. package/admin/js/templates/api-endpoints.html +63 -10
  18. package/admin/js/templates/blocks.html +76 -15
  19. package/admin/js/templates/collections.html +108 -52
  20. package/admin/js/templates/components.html +82 -15
  21. package/admin/js/templates/context-menus.html +55 -13
  22. package/admin/js/templates/forms.html +67 -14
  23. package/admin/js/templates/media.html +99 -13
  24. package/admin/js/templates/menus.html +57 -11
  25. package/admin/js/templates/pages.html +78 -23
  26. package/admin/js/templates/theme.html +164 -132
  27. package/admin/js/templates/views-list.html +63 -9
  28. package/admin/js/views/actions-list.js +37 -1
  29. package/admin/js/views/api-endpoints.js +11 -7
  30. package/admin/js/views/blocks.js +34 -5
  31. package/admin/js/views/collections.js +36 -1
  32. package/admin/js/views/components.js +27 -5
  33. package/admin/js/views/context-menus.js +10 -5
  34. package/admin/js/views/forms.js +57 -1
  35. package/admin/js/views/media.js +40 -1
  36. package/admin/js/views/menus.js +11 -5
  37. package/admin/js/views/pages.js +28 -8
  38. package/admin/js/views/theme.js +55 -1
  39. package/admin/js/views/views-list.js +11 -1
  40. package/package.json +1 -1
  41. package/server/routes/api/actions.js +64 -2
  42. package/server/routes/api/api-endpoints.js +41 -0
  43. package/server/routes/api/blocks.js +170 -8
  44. package/server/routes/api/collections.js +53 -1
  45. package/server/routes/api/components.js +46 -0
  46. package/server/routes/api/context-menus.js +8 -0
  47. package/server/routes/api/forms.js +123 -10
  48. package/server/routes/api/media.js +56 -1
  49. package/server/routes/api/menus.js +53 -6
  50. package/server/routes/api/pages.js +46 -0
  51. package/server/routes/api/theme.js +55 -0
  52. package/server/routes/api/views.js +27 -3
  53. package/server/server.js +16 -1
  54. package/server/services/contextMenus.js +41 -0
  55. package/server/services/views.js +38 -0
@@ -1,18 +1,79 @@
1
- <div class="view-header">
2
- <h1><span data-icon="box"></span> Blocks</h1>
3
- <div style="display:flex;gap:.5rem;align-items:center;">
4
- <button id="import-block-btn" class="btn btn-ghost"
5
- title="Import a .dmblock.json bundle">
6
- <span data-icon="upload"></span> Import
7
- </button>
8
- <a href="#/blocks/new" class="btn btn-primary">
9
- <span data-icon="plus"></span> New Block
10
- </a>
1
+ <!-- Blocks. Bound with M.applyBindings - see admin/views/blocks.js, which also inserts the row template
2
+ (the router renders a templateUrl through _.render, eating any {{…}}). No page heading: the Tool
3
+ banner above is the heading, and New block and Import are banner actions. -->
4
+ <div class="blx">
5
+ <p class="blx-note">Reusable HTML templates - one look for a collection's entries, a card, a call to action - placed on a
6
+ page with a shortcode.
7
+ <span data-help-title="Blocks" data-help="A block is an HTML template with a placeholder for each field (the field name in double curly braces), and optionally its own CSS. Use it to show collection entries ([collection … display=&quot;block&quot; block=&quot;name&quot;]), on its own ([block template=&quot;name&quot; …]), or by its name as a short tag ([name … /]). Each block's comment at the top says what it expects; right-click a block to copy its shortcode."></span></p>
8
+
9
+ <!-- Hidden file picker driven by the Import banner action -->
10
+ <input class="blx-import-file" type="file" accept=".dmblock.json,application/json" hidden>
11
+
12
+ <div class="blx-card">
13
+ <div class="blx-bar">
14
+ <div class="blx-seg" role="group" aria-label="Show">
15
+ <button type="button" data-on-click="showAll" data-bind-class="filter.value === 'all' && 'is-on'">All <b data-bind-text="counts.value.all"></b></button>
16
+ <button type="button" data-on-click="showUsed" data-bind-class="filter.value === 'used' && 'is-on'">In use <b data-bind-text="counts.value.used"></b></button>
17
+ <button type="button" data-on-click="showUnused" data-bind-class="filter.value === 'unused' && 'is-on'">Unused <b data-bind-text="counts.value.unused"></b></button>
18
+ </div>
19
+ <input type="search" class="form-input blx-search" placeholder="Search blocks…" aria-label="Search blocks" data-model="query.value">
20
+ <span data-help-title="The list" data-help="Click a block to edit it. Right-click it (or ⋮, or Shift+F10) to copy its shortcode, see where it is used, duplicate, export or delete it. In use counts pages and views that place it; a block a plugin uses from its own code does not count, so check before deleting one marked unused."></span>
21
+ </div>
22
+ <p class="blx-note" data-bind-hidden="!loading.value">Loading…</p>
23
+ <p class="blx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
24
+ <p class="blx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
25
+ <div class="blx-list" data-each="rows.value key=name"></div>
11
26
  </div>
12
27
  </div>
13
28
 
14
- <!-- Hidden file picker driven by the Import button -->
15
- <input id="import-block-file" type="file" accept=".dmblock.json,application/json"
16
- style="display:none;">
17
-
18
- <div id="blocks-table-container"></div>
29
+ <style>
30
+ .blx { display: flex; flex-direction: column; gap: .75rem; }
31
+ .blx [hidden], .blx-form [hidden] { display: none !important; }
32
+ .blx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
33
+ .blx-note.is-bad { color: var(--dm-danger); }
34
+ .blx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
35
+ .blx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
36
+ .blx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
37
+ .blx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
38
+ .blx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
39
+ .blx-seg button + button { border-left: 1px solid var(--dm-border); }
40
+ .blx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
41
+ .blx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
42
+ .blx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
43
+ .blx-list { display: flex; flex-direction: column; }
44
+ .blx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
45
+ .blx-row:last-child { border-bottom: 0; }
46
+ .blx-row:hover, .blx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
47
+ .blx-icon { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; flex-shrink: 0;
48
+ border-radius: var(--dm-radius-md); color: var(--dm-primary); background: color-mix(in srgb, var(--dm-primary) 12%, transparent); }
49
+ .blx-row.is-unused .blx-icon { color: var(--dm-text-muted); background: color-mix(in srgb, var(--dm-text-muted) 12%, transparent); }
50
+ .blx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
51
+ .blx-main strong, .blx-main small, .blx-main span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
52
+ .blx-main span { color: var(--dm-text); font-size: var(--dm-text-sm); }
53
+ .blx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
54
+ .blx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; }
55
+ .blx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
56
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
57
+ .blx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
58
+ .blx-chip.is-info { background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 14%, transparent); color: var(--dm-info, var(--dm-primary)); }
59
+ .blx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
60
+ .blx-uses { min-width: 2.5rem; text-align: right; font-variant-numeric: tabular-nums; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
61
+ .blx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
62
+ .blx-kebab:hover { color: var(--dm-primary); }
63
+ /* Slideovers */
64
+ .blx-form { display: flex; flex-direction: column; gap: .85rem; }
65
+ .blx-form .form-label { display: flex; align-items: center; gap: .3rem; }
66
+ .blx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
67
+ .blx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
68
+ .blx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
69
+ .blx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
70
+ .blx-spacer { flex: 1; }
71
+ .blx-where { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; }
72
+ .blx-where li { padding: .4rem .2rem; border-bottom: 1px solid var(--dm-border); }
73
+ .blx-where li:last-child { border-bottom: 0; }
74
+ .blx-where a { color: var(--dm-primary); text-decoration: none; }
75
+ .blx-where small { color: var(--dm-text-muted); margin-left: .4rem; }
76
+ .blx-code { font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-xs); background: var(--dm-surface-raised, color-mix(in srgb, var(--dm-text) 6%, transparent));
77
+ border: 1px solid var(--dm-border); border-radius: var(--dm-radius-sm); padding: .4rem .5rem; word-break: break-all; }
78
+ @media (max-width: 640px) { .blx-uses, .blx-chips { display: none; } }
79
+ </style>
@@ -1,59 +1,115 @@
1
- <div class="view-header">
2
- <h1><span data-icon="database"></span> Collections</h1>
3
- <div id="collections-header-actions">
4
- <button id="create-collection-btn" class="btn btn-primary">
5
- <span data-icon="plus"></span> New Collection
6
- </button>
7
- </div>
8
- </div>
9
-
10
- <div class="tabs" id="collections-tabs">
11
- <div class="tab-list">
12
- <button class="tab-item active">Collections</button>
13
- <button class="tab-item">Options</button>
14
- </div>
15
- <div class="tab-content">
1
+ <!-- Collections. Bound with M.applyBindings - see admin/js/views/collections.js, which also inserts the row
2
+ templates (the router renders a templateUrl through _.render, eating any {{…}}). No page heading: the
3
+ Tool banner above is the heading, and New collection is a banner action. -->
4
+ <div class="clx">
5
+ <p class="clx-note clx-scope" data-bind-hidden="!project.value">
6
+ <span data-bind-text="scopeText.value"></span>
7
+ <a href="#/collections">Show all collections</a>
8
+ </p>
16
9
 
17
- <!-- Collections tab -->
18
- <div class="tab-panel active">
19
- <div class="card">
20
- <div class="card-body">
21
- <div id="collections-table"></div>
10
+ <div class="tabs clx-tabs">
11
+ <div class="tab-list">
12
+ <button type="button" class="tab-item active">Collections <span class="clx-count" data-bind-text="kinds.value.all"></span></button>
13
+ <button type="button" class="tab-item" data-bind-hidden="!canConnect.value">Database connections <span class="clx-dirty" data-bind-hidden="!connDirty.value" title="Changes not saved yet">●</span></button>
22
14
  </div>
23
- </div>
24
- </div>
25
15
 
26
- <!-- Options tab -->
27
- <div class="tab-panel">
28
- <div class="card">
29
- <div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
30
- <h2>Database Connections</h2>
31
- <div style="display:flex;align-items:center;gap:.75rem;">
32
- <label style="display:flex;align-items:center;gap:.4rem;font-size:.875rem;cursor:pointer;">
33
- <input type="checkbox" id="connections-raw-toggle"> Raw JSON
34
- </label>
35
- <button id="add-connection-btn" class="btn btn-ghost btn-sm" type="button">
36
- <span data-icon="plus"></span> Add Connection
37
- </button>
38
- <button id="save-connections-btn" class="btn btn-primary btn-sm" type="button">
39
- <span data-icon="save"></span> Save
40
- </button>
41
- </div>
16
+ <!-- Collections -->
17
+ <div class="tab-panel active">
18
+ <div class="clx-card">
19
+ <div class="clx-bar">
20
+ <div class="clx-seg" role="group" aria-label="Show">
21
+ <button type="button" data-on-click="showAll" data-bind-class="filter.value === 'all' && 'is-on'">All <b data-bind-text="kinds.value.all"></b></button>
22
+ <button type="button" data-on-click="showOwn" data-bind-class="filter.value === 'own' && 'is-on'">Yours <b data-bind-text="kinds.value.own"></b></button>
23
+ <button type="button" data-on-click="showBuiltin" data-bind-class="filter.value === 'builtin' && 'is-on'">Built in <b data-bind-text="kinds.value.builtin"></b></button>
24
+ <button type="button" data-on-click="showPlugin" data-bind-class="filter.value === 'plugin' && 'is-on'">Plugins <b data-bind-text="kinds.value.plugin"></b></button>
25
+ </div>
26
+ <input type="search" class="form-input clx-search" placeholder="Search collections…" aria-label="Search collections" data-model="query.value">
27
+ <span data-help-title="Collections" data-help="A collection is a set of entries with the fields you define - products, events, testimonials. Click one to see its entries. Right-click it (or ⋮, or Shift+F10) to edit its fields and settings, open its form, copy its slug, or delete it. Built-in collections are the CMS's own and cannot be deleted; a plugin's can, but the plugin may stop working."></span>
28
+ <span class="clx-spacer"></span>
29
+ <span class="clx-note" data-bind-text="totalText.value"></span>
30
+ </div>
31
+ <p class="clx-note" data-bind-hidden="!loading.value">Loading…</p>
32
+ <p class="clx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
33
+ <p class="clx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
34
+ <div class="clx-list" data-each="rows.value key=slug"></div>
35
+ </div>
42
36
  </div>
43
- <div class="card-body">
44
- <div id="connections-form-view">
45
- <div id="connections-list"></div>
46
- <p id="connections-empty" class="text-muted" style="text-align:center;padding:2rem 0;">
47
- No connections configured. Add a connection to enable MongoDB storage.
48
- </p>
49
- </div>
50
- <div id="connections-raw-view" style="display:none;">
51
- <textarea id="connections-raw-json" class="form-input" rows="12"
52
- style="font-family:monospace;font-size:.8rem;resize:vertical;width:100%;"></textarea>
53
- </div>
37
+
38
+ <!-- Database connections (admins only) -->
39
+ <div class="tab-panel">
40
+ <div class="clx-card">
41
+ <div class="clx-bar">
42
+ <strong>MongoDB connections</strong>
43
+ <span data-help-title="Database connections" data-help="Collections are stored as files unless their storage is set to MongoDB, which needs a connection here - usually one called default. Each needs a URI (mongodb://…) and a database name. Kept in config/connections.json; with none, every collection stays on files. Ctrl+S saves."></span>
44
+ <span class="clx-spacer"></span>
45
+ <label class="clx-toggle"><input type="checkbox" data-model="raw.value"> Raw JSON</label>
46
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="addConnection" data-bind-hidden="raw.value">
47
+ <span data-icon="plus" data-icon-size="14"></span> Add connection</button>
48
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="saveConnections" data-bind-disabled="savingConn.value || !connDirty.value">
49
+ <span data-icon="save" data-icon-size="14"></span> Save</button>
50
+ </div>
51
+ <p class="clx-note" data-bind-hidden="!connLoading.value">Loading…</p>
52
+ <div data-bind-hidden="raw.value || connLoading.value">
53
+ <p class="clx-note" data-bind-hidden="connRows.value.length">No connections - every collection is stored as files. Add one to use MongoDB.</p>
54
+ <div class="clx-conns" data-each="connRows.value key=id"></div>
55
+ </div>
56
+ <div data-bind-hidden="!raw.value">
57
+ <textarea class="form-input clx-raw" rows="12" spellcheck="false" aria-label="Connections as JSON" data-model="rawText.value"></textarea>
58
+ </div>
59
+ <p class="clx-note is-bad" data-bind-hidden="!connError.value" data-bind-text="connError.value"></p>
60
+ </div>
54
61
  </div>
55
- </div>
56
62
  </div>
57
-
58
- </div>
59
63
  </div>
64
+
65
+ <style>
66
+ .clx { display: flex; flex-direction: column; gap: .75rem; }
67
+ .clx [hidden], .clx-form [hidden] { display: none !important; }
68
+ .clx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
69
+ .clx-note.is-bad { color: var(--dm-danger); }
70
+ .clx-scope { padding: .5rem .75rem; border-radius: var(--dm-radius-md); background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 10%, transparent); color: var(--dm-text); }
71
+ .clx-count { opacity: .65; font-size: var(--dm-text-xs); margin-left: .2rem; }
72
+ .clx-dirty { color: var(--dm-warning); margin-left: .2rem; }
73
+ .clx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; margin-top: .75rem; }
74
+ .clx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
75
+ .clx-bar .btn { display: inline-flex; align-items: center; gap: .3rem; }
76
+ .clx-spacer { flex: 1; }
77
+ .clx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
78
+ .clx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
79
+ .clx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
80
+ .clx-seg button + button { border-left: 1px solid var(--dm-border); }
81
+ .clx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
82
+ .clx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
83
+ .clx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
84
+ .clx-toggle { display: inline-flex; align-items: center; gap: .35rem; font-size: var(--dm-text-sm); cursor: pointer; }
85
+ .clx-list { display: flex; flex-direction: column; }
86
+ .clx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
87
+ .clx-row:last-child { border-bottom: 0; }
88
+ .clx-row:hover, .clx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
89
+ .clx-icon { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; flex-shrink: 0;
90
+ border-radius: var(--dm-radius-md); color: var(--dm-primary); background: color-mix(in srgb, var(--dm-primary) 12%, transparent); }
91
+ .clx-row.is-builtin .clx-icon { color: var(--dm-text-muted); background: color-mix(in srgb, var(--dm-text-muted) 12%, transparent); }
92
+ .clx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
93
+ .clx-main strong, .clx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
94
+ .clx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
95
+ .clx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
96
+ .clx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
97
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
98
+ .clx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
99
+ .clx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
100
+ .clx-chip.is-info { background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 14%, transparent); color: var(--dm-info, var(--dm-primary)); }
101
+ .clx-num { min-width: 4.5rem; text-align: right; font-variant-numeric: tabular-nums; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
102
+ .clx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
103
+ .clx-kebab:hover { color: var(--dm-primary); }
104
+ .clx-conns { display: flex; flex-direction: column; gap: .5rem; }
105
+ .clx-conn { display: grid; grid-template-columns: 1fr 2fr 1fr auto; gap: .5rem; align-items: end; padding: .6rem; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); }
106
+ .clx-conn label { display: flex; flex-direction: column; gap: .2rem; font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
107
+ .clx-raw { width: 100%; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm); resize: vertical; }
108
+ .clx-form { display: flex; flex-direction: column; gap: .85rem; }
109
+ .clx-form .form-label { display: flex; align-items: center; gap: .3rem; }
110
+ .clx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
111
+ .clx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
112
+ .clx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
113
+ .clx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
114
+ @media (max-width: 720px) { .clx-conn { grid-template-columns: 1fr; } .clx-num { display: none; } }
115
+ </style>
@@ -1,18 +1,85 @@
1
- <div class="view-header">
2
- <h1><span data-icon="component"></span> Components</h1>
3
- <div style="display:flex;gap:.5rem;align-items:center;">
4
- <button id="import-component-btn" class="btn btn-ghost"
5
- title="Import a .dmcomponent.json bundle">
6
- <span data-icon="upload"></span> Import
7
- </button>
8
- <a href="#/components/new" class="btn btn-primary">
9
- <span data-icon="plus"></span> New Component
10
- </a>
1
+ <!-- Components. Bound with M.applyBindings - see admin/views/components.js, which also inserts the row
2
+ template (the router renders a templateUrl through _.render, eating any {{…}}). No page heading:
3
+ the Tool banner above is the heading; New component and Import are banner actions. -->
4
+ <div class="cpx">
5
+ <p class="cpx-note">Your own custom elements - markup, style and behaviour in one <code>.dmc</code> file - placed on any
6
+ page as a tag.
7
+ <span data-help-title="Components" data-help="A component named counter is placed on a page as <dm-counter></dm-counter>, with its props as attributes. Yours can be edited, duplicated, exported and deleted. Built-in ones came with the CMS and are yours to change. Plugin ones are read-only - they belong to the plugin that provides them."></span></p>
8
+
9
+ <p class="cpx-filter-note" data-bind-hidden="!projectNote.value">
10
+ <span data-bind-text="projectNote.value"></span>
11
+ <a href="#/components">Show all components</a></p>
12
+
13
+ <div class="cpx-card">
14
+ <div class="cpx-bar">
15
+ <div class="cpx-seg" role="group" aria-label="Show">
16
+ <button type="button" data-on-click="showAll" data-bind-class="filter.value === 'all' && 'is-on'">All <b data-bind-text="counts.value.all"></b></button>
17
+ <button type="button" data-on-click="showMine" data-bind-class="filter.value === 'mine' && 'is-on'">Yours <b data-bind-text="counts.value.mine"></b></button>
18
+ <button type="button" data-on-click="showBuiltin" data-bind-class="filter.value === 'builtin' && 'is-on'">Built in <b data-bind-text="counts.value.builtin"></b></button>
19
+ <button type="button" data-on-click="showPlugin" data-bind-class="filter.value === 'plugin' && 'is-on'" data-bind-hidden="!counts.value.plugin">Plugins <b data-bind-text="counts.value.plugin"></b></button>
20
+ </div>
21
+ <input type="search" class="form-input cpx-search" placeholder="Search by name or prop…" aria-label="Search components" data-model="query.value">
22
+ <select class="form-input cpx-sort" aria-label="Sort" data-model="sort.value">
23
+ <option value="name">Name</option>
24
+ <option value="updated">Recently changed</option>
25
+ </select>
26
+ <span data-help-title="The list" data-help="Click a component to edit it. Right-click it (or ⋮, or Shift+F10) to copy its tag, duplicate it, export it as a .dmcomponent.json file, or delete it. Import (in the banner) brings a .dmcomponent.json back - from a backup or another site."></span>
27
+ </div>
28
+ <p class="cpx-note" data-bind-hidden="!loading.value">Loading…</p>
29
+ <p class="cpx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
30
+ <p class="cpx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
31
+ <div class="cpx-list" data-each="rows.value key=name"></div>
11
32
  </div>
12
- </div>
13
33
 
14
- <!-- Hidden file picker driven by the Import button -->
15
- <input id="import-component-file" type="file" accept=".dmcomponent.json,application/json"
16
- style="display:none;">
34
+ <!-- The file picker behind the banner's Import action -->
35
+ <input class="cpx-import-file" type="file" accept=".dmcomponent.json,application/json" hidden>
36
+ </div>
17
37
 
18
- <div id="components-table-container"></div>
38
+ <style>
39
+ .cpx { display: flex; flex-direction: column; gap: .75rem; }
40
+ .cpx [hidden], .cpx-form [hidden] { display: none !important; }
41
+ .cpx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
42
+ .cpx-note.is-bad { color: var(--dm-danger); }
43
+ .cpx-note code, .cpx-main code { font-size: .92em; }
44
+ .cpx-filter-note { margin: 0; padding: .5rem .75rem; border-radius: var(--dm-radius-md); font-size: var(--dm-text-sm);
45
+ background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 12%, transparent); color: var(--dm-text); }
46
+ .cpx-filter-note a { margin-left: .35rem; }
47
+ .cpx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
48
+ .cpx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
49
+ .cpx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
50
+ .cpx-sort { width: auto; flex: 0 0 auto; }
51
+ .cpx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
52
+ .cpx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
53
+ .cpx-seg button + button { border-left: 1px solid var(--dm-border); }
54
+ .cpx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
55
+ .cpx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
56
+ .cpx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
57
+ .cpx-list { display: flex; flex-direction: column; }
58
+ .cpx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
59
+ .cpx-row:last-child { border-bottom: 0; }
60
+ .cpx-row:hover, .cpx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
61
+ .cpx-icon { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; flex-shrink: 0;
62
+ border-radius: var(--dm-radius-md); color: var(--dm-primary); background: color-mix(in srgb, var(--dm-primary) 12%, transparent); }
63
+ .cpx-row.is-plugin .cpx-icon { color: var(--dm-text-muted); background: color-mix(in srgb, var(--dm-text-muted) 12%, transparent); }
64
+ .cpx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
65
+ .cpx-main strong, .cpx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
66
+ .cpx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
67
+ .cpx-tag { color: var(--dm-text-muted); font-weight: 400; margin-left: .4rem; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-xs); }
68
+ .cpx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; }
69
+ .cpx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
70
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
71
+ .cpx-chip.is-builtin { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
72
+ .cpx-chip.is-plugin { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
73
+ .cpx-chip.is-project { background: color-mix(in srgb, var(--dm-primary) 14%, transparent); color: var(--dm-primary); }
74
+ .cpx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
75
+ .cpx-kebab:hover { color: var(--dm-primary); }
76
+ /* The Duplicate slideover */
77
+ .cpx-form { display: flex; flex-direction: column; gap: .85rem; }
78
+ .cpx-form .form-label { display: flex; align-items: center; gap: .3rem; }
79
+ .cpx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
80
+ .cpx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
81
+ .cpx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
82
+ .cpx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
83
+ .cpx-spacer { flex: 1; }
84
+ @media (max-width: 640px) { .cpx-chips { display: none; } .cpx-tag { display: none; } }
85
+ </style>
@@ -1,16 +1,58 @@
1
- <div class="page-header">
2
- <h2><span data-icon="menu"></span> Context Menus</h2>
3
- <div class="header-actions">
4
- <a class="btn btn-primary" href="#/context-menus/new"><span data-icon="plus"></span> New context menu</a>
5
- </div>
6
- </div>
7
- <p class="text-muted mb-4">
8
- Right-click menus bound to a region of a page. Unlike navigation menus these have no slot -
9
- they bind to a CSS selector and are resolved in the browser by whichever menu is nearest the click.
10
- </p>
1
+ <!-- Context Menus. Bound with M.applyBindings - see admin/views/context-menus.js, which also inserts the
2
+ row template (the router renders a templateUrl through _.render, eating any {{…}}). No page heading:
3
+ the Tool banner above is the heading, and New context menu is a banner action. -->
4
+ <div class="cmx">
5
+ <p class="cmx-note">Right-click menus bound to a region of a page. Unlike navigation menus these have no slot -
6
+ they bind to a CSS selector, and the menu nearest the click answers.
7
+ <span data-help-title="Context menus" data-help="Each menu binds to a region (a CSS selector) and, optionally, to the rows inside it that answer. When someone right-clicks, the menu bound nearest the click wins - so a menu on a card beats one on the whole page. A menu with no binding applies on every page; a binding narrows it to some projects or pages. One that binds to nothing never appears, and is marked so."></span></p>
11
8
 
12
- <div class="card">
13
- <div class="card-body">
14
- <div id="context-menus-table"></div>
9
+ <div class="cmx-card">
10
+ <div class="cmx-bar">
11
+ <div class="cmx-seg" role="group" aria-label="Show">
12
+ <button type="button" data-on-click="showAll" data-bind-class="filter.value === 'all' && 'is-on'">All <b data-bind-text="counts.value.all"></b></button>
13
+ <button type="button" data-on-click="showOn" data-bind-class="filter.value === 'on' && 'is-on'">On <b data-bind-text="counts.value.on"></b></button>
14
+ <button type="button" data-on-click="showOff" data-bind-class="filter.value === 'off' && 'is-on'">Off <b data-bind-text="counts.value.off"></b></button>
15
+ </div>
16
+ <input type="search" class="form-input cmx-search" placeholder="Search menus…" aria-label="Search context menus" data-model="query.value">
17
+ <span data-help-title="The list" data-help="Click a menu to edit it. Right-click it (or ⋮, or Shift+F10) to switch it on or off, duplicate it, copy its slug or delete it. The chip says where it applies: Site-wide, a number of binding rules (hover for them), or Never matches."></span>
18
+ </div>
19
+ <p class="cmx-note" data-bind-hidden="!projectNote.value" data-bind-text="projectNote.value"></p>
20
+ <p class="cmx-note" data-bind-hidden="!loading.value">Loading…</p>
21
+ <p class="cmx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
22
+ <p class="cmx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
23
+ <div class="cmx-list" data-each="rows.value key=slug"></div>
15
24
  </div>
16
25
  </div>
26
+
27
+ <style>
28
+ .cmx { display: flex; flex-direction: column; gap: .75rem; }
29
+ .cmx [hidden] { display: none !important; }
30
+ .cmx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
31
+ .cmx-note.is-bad { color: var(--dm-danger); }
32
+ .cmx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
33
+ .cmx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
34
+ .cmx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
35
+ .cmx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
36
+ .cmx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
37
+ .cmx-seg button + button { border-left: 1px solid var(--dm-border); }
38
+ .cmx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
39
+ .cmx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
40
+ .cmx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
41
+ .cmx-list { display: flex; flex-direction: column; }
42
+ .cmx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
43
+ .cmx-row:last-child { border-bottom: 0; }
44
+ .cmx-row:hover, .cmx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
45
+ .cmx-row.is-off .cmx-main strong { color: var(--dm-text-muted); }
46
+ .cmx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
47
+ .cmx-main strong, .cmx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
48
+ .cmx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); font-family: var(--dm-font-mono, monospace); }
49
+ .cmx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; }
50
+ .cmx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
51
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
52
+ .cmx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
53
+ .cmx-chip.is-info { background: color-mix(in srgb, var(--dm-info) 16%, transparent); color: var(--dm-info); }
54
+ .cmx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
55
+ .cmx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
56
+ .cmx-kebab:hover { color: var(--dm-primary); }
57
+ @media (max-width: 640px) { .cmx-main small { white-space: normal; } }
58
+ </style>
@@ -1,17 +1,70 @@
1
- <div class="view-header">
2
- <h1><span data-icon="layout"></span> Forms</h1>
3
- <div style="display:flex;gap:.5rem;">
4
- <button id="test-email-btn" class="btn btn-ghost btn-sm">
5
- <span data-icon="mail"></span> Test Email
6
- </button>
7
- <button id="create-form-btn" class="btn btn-primary">
8
- <span data-icon="plus"></span> Create Form
9
- </button>
10
- </div>
11
- </div>
1
+ <!-- Forms. Bound with M.applyBindings - see admin/views/forms.js, which also inserts the row template
2
+ (the router renders a templateUrl through _.render, eating any {{…}}). No page heading: the Tool
3
+ banner above is the heading; New form and Test email are banner actions. -->
4
+ <div class="fmx">
5
+ <p class="fmx-note">Forms collect answers on the site - contact, sign-up, feedback - and store, email or pass on what
6
+ is sent.
7
+ <span data-help-title="Forms" data-help="Build a form, then put it on any page with its shortcode (⋮ → Copy shortcode). Each submission is saved to the form's collection, and can also be emailed, sent to a webhook or run an Action - set in the form's Actions. Click a form to edit it; its submissions are one right-click away."></span></p>
12
8
 
13
- <div class="card">
14
- <div class="card-body">
15
- <div id="forms-table"></div>
9
+ <div class="fmx-card">
10
+ <div class="fmx-bar">
11
+ <div class="fmx-seg" role="group" aria-label="Show">
12
+ <button type="button" data-on-click="showAll" data-bind-class="filter.value === 'all' && 'is-on'">All <b data-bind-text="counts.value.all"></b></button>
13
+ <button type="button" data-on-click="showRecent" data-bind-class="filter.value === 'recent' && 'is-on'">New this week <b data-bind-text="counts.value.recent"></b></button>
14
+ <button type="button" data-on-click="showEmpty" data-bind-class="filter.value === 'empty' && 'is-on'">No submissions <b data-bind-text="counts.value.empty"></b></button>
15
+ </div>
16
+ <input type="search" class="form-input fmx-search" placeholder="Search forms…" aria-label="Search forms" data-model="query.value">
17
+ <span data-help-title="The list" data-help="Forms with the newest submissions come first. New this week counts submissions in the last 7 days - the same number as the badge on Forms in the sidebar. Right-click a form (or ⋮, or Shift+F10) to see its submissions, copy its shortcode, duplicate it or delete it."></span>
18
+ <span class="fmx-spacer"></span>
19
+ <small class="fmx-muted" data-bind-text="summary.value"></small>
20
+ </div>
21
+ <p class="fmx-note" data-bind-hidden="!loading.value">Loading…</p>
22
+ <p class="fmx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
23
+ <p class="fmx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
24
+ <div class="fmx-list" data-each="rows.value key=slug"></div>
16
25
  </div>
17
26
  </div>
27
+
28
+ <style>
29
+ .fmx { display: flex; flex-direction: column; gap: .75rem; }
30
+ .fmx [hidden], .fmx-form [hidden] { display: none !important; }
31
+ .fmx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
32
+ .fmx-note.is-bad { color: var(--dm-danger); }
33
+ .fmx-muted { color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
34
+ .fmx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
35
+ .fmx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
36
+ .fmx-spacer { flex: 1; }
37
+ .fmx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
38
+ .fmx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
39
+ .fmx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
40
+ .fmx-seg button + button { border-left: 1px solid var(--dm-border); }
41
+ .fmx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
42
+ .fmx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
43
+ .fmx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
44
+ .fmx-list { display: flex; flex-direction: column; }
45
+ .fmx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
46
+ .fmx-row:last-child { border-bottom: 0; }
47
+ .fmx-row:hover, .fmx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
48
+ .fmx-icon { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; flex-shrink: 0;
49
+ border-radius: var(--dm-radius-md); color: var(--dm-primary); background: color-mix(in srgb, var(--dm-primary) 12%, transparent); }
50
+ .fmx-row.is-recent .fmx-icon { color: var(--dm-success); background: color-mix(in srgb, var(--dm-success) 14%, transparent); }
51
+ .fmx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
52
+ .fmx-main strong, .fmx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
53
+ .fmx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
54
+ .fmx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
55
+ .fmx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
56
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
57
+ .fmx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
58
+ .fmx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
59
+ .fmx-count { min-width: 3rem; text-align: right; font-variant-numeric: tabular-nums; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
60
+ .fmx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
61
+ .fmx-kebab:hover { color: var(--dm-primary); }
62
+ /* Slideover forms */
63
+ .fmx-form { display: flex; flex-direction: column; gap: .85rem; }
64
+ .fmx-form .form-label { display: flex; align-items: center; gap: .3rem; }
65
+ .fmx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
66
+ .fmx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
67
+ .fmx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
68
+ .fmx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
69
+ @media (max-width: 640px) { .fmx-count { display: none; } .fmx-chips { display: none; } }
70
+ </style>