domma-cms 0.74.0 → 0.76.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 (116) hide show
  1. package/CLAUDE.md +3 -1
  2. package/admin/js/app.js +4 -4
  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/api-tokens-arrange.js +3 -0
  6. package/admin/js/lib/blocks-arrange.js +1 -0
  7. package/admin/js/lib/collections-arrange.js +1 -0
  8. package/admin/js/lib/components-arrange.js +1 -0
  9. package/admin/js/lib/context-menus-arrange.js +1 -0
  10. package/admin/js/lib/effects-arrange.js +3 -0
  11. package/admin/js/lib/forms-arrange.js +1 -0
  12. package/admin/js/lib/layouts-arrange.js +1 -0
  13. package/admin/js/lib/media-arrange.js +1 -0
  14. package/admin/js/lib/menus-arrange.js +1 -0
  15. package/admin/js/lib/my-profile-arrange.js +1 -0
  16. package/admin/js/lib/notifications-arrange.js +1 -0
  17. package/admin/js/lib/pages-arrange.js +1 -0
  18. package/admin/js/lib/plugins-arrange.js +2 -0
  19. package/admin/js/lib/roles-arrange.js +1 -0
  20. package/admin/js/lib/search-settings-arrange.js +1 -0
  21. package/admin/js/lib/sidebar-renderer.js +3 -3
  22. package/admin/js/lib/site-settings-arrange.js +1 -0
  23. package/admin/js/lib/theme-settings-arrange.js +1 -0
  24. package/admin/js/lib/users-arrange.js +1 -0
  25. package/admin/js/lib/views-arrange.js +1 -0
  26. package/admin/js/templates/actions-list.html +73 -14
  27. package/admin/js/templates/api-endpoints.html +63 -10
  28. package/admin/js/templates/api-tokens.html +83 -10
  29. package/admin/js/templates/blocks.html +76 -15
  30. package/admin/js/templates/collections.html +108 -52
  31. package/admin/js/templates/components.html +82 -15
  32. package/admin/js/templates/context-menus.html +55 -13
  33. package/admin/js/templates/effects.html +155 -72
  34. package/admin/js/templates/forms.html +67 -14
  35. package/admin/js/templates/layouts.html +126 -43
  36. package/admin/js/templates/media.html +99 -13
  37. package/admin/js/templates/menus.html +57 -11
  38. package/admin/js/templates/my-profile.html +100 -12
  39. package/admin/js/templates/notifications.html +70 -10
  40. package/admin/js/templates/pages.html +78 -23
  41. package/admin/js/templates/plugins.html +100 -28
  42. package/admin/js/templates/roles.html +78 -7
  43. package/admin/js/templates/search.html +193 -216
  44. package/admin/js/templates/settings.html +404 -472
  45. package/admin/js/templates/theme.html +164 -132
  46. package/admin/js/templates/users.html +69 -9
  47. package/admin/js/templates/views-list.html +63 -9
  48. package/admin/js/views/actions-list.js +37 -1
  49. package/admin/js/views/api-endpoints.js +11 -7
  50. package/admin/js/views/api-tokens.js +62 -8
  51. package/admin/js/views/blocks.js +34 -5
  52. package/admin/js/views/collections.js +36 -1
  53. package/admin/js/views/components.js +27 -5
  54. package/admin/js/views/context-menus.js +10 -5
  55. package/admin/js/views/effects.js +31 -1
  56. package/admin/js/views/forms.js +57 -1
  57. package/admin/js/views/layouts.js +81 -1
  58. package/admin/js/views/media.js +40 -1
  59. package/admin/js/views/menus.js +11 -5
  60. package/admin/js/views/my-profile.js +1 -1
  61. package/admin/js/views/notifications.js +18 -1
  62. package/admin/js/views/pages.js +28 -8
  63. package/admin/js/views/plugins.js +97 -39
  64. package/admin/js/views/roles.js +49 -4
  65. package/admin/js/views/search.js +7 -1
  66. package/admin/js/views/settings.js +13 -3
  67. package/admin/js/views/theme.js +55 -1
  68. package/admin/js/views/users.js +13 -4
  69. package/admin/js/views/views-list.js +11 -1
  70. package/package.json +1 -1
  71. package/plugins/_lib/admin/mail/compose-window.js +97 -40
  72. package/plugins/_lib/admin/mail/contacts.js +4 -1
  73. package/plugins/_lib/admin/mail/diagnostics-section.js +4 -1
  74. package/plugins/_lib/admin/mail/folder-tree.js +1 -1
  75. package/plugins/_lib/admin/mail/image-senders-section.js +69 -90
  76. package/plugins/_lib/admin/mail/mail.css +1 -1
  77. package/plugins/_lib/admin/mail/mail.html +1 -1
  78. package/plugins/_lib/admin/mail/reader-view.js +303 -44
  79. package/plugins/_lib/admin/mail/when-closed.js +26 -0
  80. package/plugins/_lib/mail/readRoutes.js +43 -1
  81. package/plugins/_lib/mail/unreadBadge.js +42 -0
  82. package/plugins/free-tier.lock.json +5 -5
  83. package/plugins/mail-reader/CLAUDE.md +1 -0
  84. package/plugins/mail-reader/plugin.js +3 -1
  85. package/plugins/mail-reader/plugin.json +5 -4
  86. package/server/routes/api/actions.js +64 -2
  87. package/server/routes/api/api-endpoints.js +41 -0
  88. package/server/routes/api/api-tokens.js +56 -0
  89. package/server/routes/api/auth.js +7 -1
  90. package/server/routes/api/blocks.js +170 -8
  91. package/server/routes/api/collections.js +66 -2
  92. package/server/routes/api/components.js +46 -0
  93. package/server/routes/api/context-menus.js +8 -0
  94. package/server/routes/api/effects.js +68 -13
  95. package/server/routes/api/forms.js +123 -10
  96. package/server/routes/api/layouts.js +93 -2
  97. package/server/routes/api/media.js +56 -1
  98. package/server/routes/api/menus.js +53 -6
  99. package/server/routes/api/notifications.js +140 -14
  100. package/server/routes/api/pages.js +46 -0
  101. package/server/routes/api/plugins.js +135 -4
  102. package/server/routes/api/roles.js +111 -0
  103. package/server/routes/api/search.js +38 -4
  104. package/server/routes/api/settings.js +52 -0
  105. package/server/routes/api/theme.js +55 -0
  106. package/server/routes/api/users.js +53 -1
  107. package/server/routes/api/views.js +27 -3
  108. package/server/server.js +28 -1
  109. package/server/services/contextMenus.js +41 -0
  110. package/server/services/plugins.js +7 -0
  111. package/server/services/roles.js +3 -0
  112. package/server/services/search.js +58 -0
  113. package/server/services/users.js +14 -0
  114. package/server/services/views.js +38 -0
  115. package/admin/js/templates/plugin-marketplace.html +0 -29
  116. package/admin/js/views/plugin-marketplace.js +0 -1
@@ -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,81 +1,104 @@
1
- <div class="view-header">
2
- <h1><span data-icon="sparkles"></span> Domma Effects</h1>
3
- <div>
4
- <button id="save-settings-btn" class="btn btn-primary">
5
- <span data-icon="save"></span> Save
6
- </button>
1
+ <!-- System › Effects. Bound with M.applyBindings - see admin/js/views/effects.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 Save is a banner action. -->
4
+ <div class="fxx">
5
+ <div class="fxx-card fxx-live">
6
+ <div class="fxx-card-title">Live now
7
+ <span data-help-title="Live now" data-help="What the published site uses at the moment - the saved settings, not what is typed below until you Save. Every [reveal] and row reveal without its own duration, animation or threshold uses these."></span></div>
8
+ <div class="fxx-chips">
9
+ <span class="fxx-chip" data-bind-class="saved.value.respectMotion ? 'is-ok' : 'is-warn'" data-bind-text="motionText.value"></span>
10
+ <span class="fxx-chip" data-bind-text="'Reveal: ' + saved.value.defaultAnimation"></span>
11
+ <span class="fxx-chip" data-bind-text="saved.value.defaultDuration + ' ms'"></span>
12
+ <span class="fxx-chip" data-bind-text="'Threshold ' + saved.value.defaultThreshold"></span>
13
+ <span class="fxx-chip is-muted" data-bind-hidden="!defaultsText.value" data-bind-text="defaultsText.value"></span>
14
+ </div>
7
15
  </div>
8
- </div>
9
16
 
10
- <!-- Top-level tabs -->
11
- <div id="effects-tabs" class="tabs">
12
- <div class="tab-list">
13
- <button class="tab-item active">Settings</button>
14
- <button class="tab-item">Shortcode Reference</button>
15
- </div>
16
- <div class="tab-content">
17
+ <p class="fxx-unsaved" data-bind-hidden="!dirty.value">
18
+ <span data-icon="alert-circle" data-icon-size="15"></span> Unsaved changes
19
+ <span class="fxx-spacer"></span>
20
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
21
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannotSave.value">Save</button>
22
+ </p>
23
+
24
+ <div class="tabs fxx-tabs">
25
+ <div class="tab-list">
26
+ <button type="button" class="tab-item active">Settings <span class="fxx-dot" data-bind-hidden="!dirty.value" title="Unsaved changes">•</span></button>
27
+ <button type="button" class="tab-item">Effects <span class="fxx-count" data-bind-text="effectCount"></span></button>
28
+ <button type="button" class="tab-item">Guide</button>
29
+ </div>
17
30
 
18
- <!-- Settings tab panel -->
31
+ <!-- Settings -->
19
32
  <div class="tab-panel active">
20
- <div class="card mb-4">
21
- <div class="card-body">
22
- <div class="row mb-3">
23
- <div class="col">
24
- <label class="form-check-label">
25
- <input id="field-respect-motion" type="checkbox">
26
- Respect <code>prefers-reduced-motion</code>
27
- </label>
28
- <span class="form-hint">When enabled, JS effects are skipped for users who prefer reduced motion. Content remains visible.</span>
29
- </div>
33
+ <div class="fxx-card">
34
+ <div class="fxx-card-title">Scroll reveal defaults
35
+ <span data-help-title="Defaults" data-help="Used by [reveal] and [row reveal] when the shortcode does not set its own. A shortcode's own attribute always wins. Ctrl+S saves."></span></div>
36
+ <div class="fxx-grid">
37
+ <div class="fxx-wide fxx-setting">
38
+ <label class="form-label" for="fx-motion">Respect reduced motion
39
+ <span data-help-title="prefers-reduced-motion" data-help="Visitors can ask their device for less motion. With this on, the JavaScript effects are skipped for them and the content simply shows - nothing is hidden. Turn it off only if an effect carries meaning that would otherwise be lost."></span></label>
40
+ <input id="fx-motion" type="checkbox" class="form-switch" data-model="respectMotion.value">
30
41
  </div>
31
- <div class="row mb-3">
32
- <div class="col-6">
33
- <label class="form-label">Default animation duration (ms)</label>
34
- <input id="field-default-duration" type="number" class="form-input" min="0" max="10000"
35
- placeholder="600">
36
- <span class="form-hint">Used by reveal when no <code>duration</code> attribute is specified.</span>
37
- </div>
38
- <div class="col-6">
39
- <label class="form-label">Default reveal animation</label>
40
- <select id="field-default-animation" class="form-select">
41
- <option value="fade">Fade</option>
42
- <option value="slide-up">Slide up</option>
43
- <option value="slide-down">Slide down</option>
44
- <option value="zoom">Zoom</option>
45
- <option value="flip">Flip</option>
46
- </select>
47
- </div>
42
+ <div>
43
+ <label class="form-label" for="fx-anim">Reveal animation</label>
44
+ <select id="fx-anim" class="form-input" data-model="defaultAnimation.value">
45
+ <option value="fade">Fade</option>
46
+ <option value="slide-up">Slide up</option>
47
+ <option value="slide-down">Slide down</option>
48
+ <option value="zoom">Zoom</option>
49
+ <option value="flip">Flip</option>
50
+ </select>
51
+ <small class="fxx-err" data-bind-hidden="!animErr.value" data-bind-text="animErr.value"></small>
48
52
  </div>
49
- <div class="row">
50
- <div class="col-6">
51
- <label class="form-label">Default scroll threshold</label>
52
- <input id="field-default-threshold" type="number" class="form-input" min="0" max="1"
53
- step="0.05"
54
- placeholder="0.1">
55
- <span class="form-hint">Fraction of element visible before reveal fires (0.0-1.0).</span>
56
- </div>
53
+ <div>
54
+ <label class="form-label" for="fx-duration">Duration (ms)
55
+ <span data-help="How long a reveal takes. 600 ms is a comfortable default; above about 1000 it starts to feel slow."></span></label>
56
+ <input id="fx-duration" type="number" class="form-input" min="1" max="10000" step="50" placeholder="600" data-model="defaultDuration.value">
57
+ <small class="fxx-err" data-bind-hidden="!durErr.value" data-bind-text="durErr.value"></small>
57
58
  </div>
59
+ <div>
60
+ <label class="form-label" for="fx-threshold">Threshold
61
+ <span data-help-title="Threshold" data-help="How much of an element must be on screen before it reveals: 0 = as soon as any of it shows, 1 = only once all of it shows. 0.1 suits most pages; tall blocks need a low value or they may never reveal on a small screen."></span></label>
62
+ <input id="fx-threshold" type="number" class="form-input" min="0" max="1" step="0.05" placeholder="0.1" data-model="defaultThreshold.value">
63
+ <small class="fxx-err" data-bind-hidden="!thrErr.value" data-bind-text="thrErr.value"></small>
64
+ </div>
65
+ </div>
66
+ <div class="fxx-actions">
67
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="resetDefaults" title="Put back Domma's defaults (then Save)">Domma's defaults</button>
68
+ <span class="fxx-spacer"></span>
69
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannotSave.value">
70
+ <span data-icon="save" data-icon-size="14"></span> Save</button>
58
71
  </div>
59
72
  </div>
60
73
  </div>
61
74
 
62
- <!-- Shortcode Reference tab panel -->
75
+ <!-- Effects: the catalogue the page editor inserts from -->
63
76
  <div class="tab-panel">
64
- <p class="text-muted mb-3">Use these shortcodes in any page's Markdown content. The <strong>Effects</strong>
65
- toolbar button in the editor inserts them automatically.</p>
66
-
67
- <!-- Sub-tabs -->
68
- <div class="mb-3" style="display:flex;gap:6px;flex-wrap:wrap;">
69
- <button class="btn btn-sm effects-tab-btn active" data-fx-tab="entrance">Entrance</button>
70
- <button class="btn btn-sm effects-tab-btn" data-fx-tab="animation">Animation</button>
71
- <button class="btn btn-sm effects-tab-btn" data-fx-tab="text">Text</button>
72
- <button class="btn btn-sm effects-tab-btn" data-fx-tab="visual">Visual</button>
73
- <button class="btn btn-sm effects-tab-btn" data-fx-tab="examples">Examples</button>
74
- <button class="btn btn-sm effects-tab-btn" data-fx-tab="celebrations">Celebrations</button>
77
+ <div class="fxx-card">
78
+ <div class="fxx-bar">
79
+ <div class="fxx-seg fxx-cats" role="group" aria-label="Category" data-each="cats.value key=key"></div>
80
+ <input type="search" class="form-input fxx-search" placeholder="Search effects or settings…" aria-label="Search effects" data-model="query.value">
81
+ <span data-help-title="Effects" data-help="Every effect the page editor's Effects button can insert. Click one for its settings and an example; right-click it (or ⋮, or Shift+F10) to copy its shortcode. An effect that works inside [card], [hero] and friends can also be switched on as an attribute of that shortcode."></span>
82
+ </div>
83
+ <p class="fxx-note" data-bind-hidden="rows.value.length">No effects match.</p>
84
+ <div class="fxx-list" data-each="rows.value key=name"></div>
85
+ </div>
75
86
  </div>
76
87
 
77
- <!-- Entrance tab -->
78
- <div id="tab-entrance" class="effects-tab-panel">
88
+ <!-- Guide: the long-form reference -->
89
+ <div class="tab-panel">
90
+ <div class="fxx-card">
91
+ <p class="fxx-note">Use these shortcodes in any page's Markdown. The <strong>Effects</strong> toolbar button in
92
+ the editor inserts them for you.</p>
93
+ <div class="fxx-seg fxx-guide-tabs" role="group" aria-label="Guide section">
94
+ <button type="button" data-on-click="showGuide('entrance')" data-bind-class="guide.value === 'entrance' && 'is-on'">Entrance</button>
95
+ <button type="button" data-on-click="showGuide('animation')" data-bind-class="guide.value === 'animation' && 'is-on'">Animation</button>
96
+ <button type="button" data-on-click="showGuide('text')" data-bind-class="guide.value === 'text' && 'is-on'">Text</button>
97
+ <button type="button" data-on-click="showGuide('visual')" data-bind-class="guide.value === 'visual' && 'is-on'">Visual</button>
98
+ <button type="button" data-on-click="showGuide('examples')" data-bind-class="guide.value === 'examples' && 'is-on'">Examples</button>
99
+ <button type="button" data-on-click="showGuide('celebrations')" data-bind-class="guide.value === 'celebrations' && 'is-on'">Celebrations</button>
100
+ </div>
101
+ <div id="tab-entrance" class="fxx-guide-panel" data-bind-hidden="guide.value !== 'entrance'">
79
102
  <h3 class="mb-2">Reveal</h3>
80
103
  <p class="text-muted mb-2">Triggers a scroll-activated entrance animation when the element enters the
81
104
  viewport.
@@ -198,7 +221,7 @@ Markdown **works** inside.
198
221
  </div>
199
222
 
200
223
  <!-- Animation tab -->
201
- <div id="tab-animation" class="effects-tab-panel">
224
+ <div id="tab-animation" class="fxx-guide-panel" data-bind-hidden="guide.value !== 'animation'">
202
225
  <h3 class="mb-2">Breathe</h3>
203
226
  <p class="text-muted mb-2">Continuous gentle scaling - makes elements feel alive. Ideal for hero images or
204
227
  call-to-action buttons.</p>
@@ -256,7 +279,7 @@ Content here.
256
279
  </div>
257
280
 
258
281
  <!-- Text tab -->
259
- <div id="tab-text" class="effects-tab-panel">
282
+ <div id="tab-text" class="fxx-guide-panel" data-bind-hidden="guide.value !== 'text'">
260
283
  <h3 class="mb-2">Typewriter (Scribe)</h3>
261
284
  <p class="text-muted mb-2">Types text character by character. Supports a simple one-shot mode or a full
262
285
  action script for sequenced multi-step animations.</p>
@@ -418,7 +441,7 @@ Scrambled reveal text.
418
441
  </div>
419
442
 
420
443
  <!-- Visual tab -->
421
- <div id="tab-visual" class="effects-tab-panel">
444
+ <div id="tab-visual" class="fxx-guide-panel" data-bind-hidden="guide.value !== 'visual'">
422
445
  <h3 class="mb-2">Ripple</h3>
423
446
  <p class="text-muted mb-2">Click-triggered ripple effect on the element. Good for interactive content
424
447
  blocks.</p>
@@ -466,7 +489,7 @@ Content on animated background.
466
489
  </div>
467
490
 
468
491
  <!-- Examples tab -->
469
- <div id="tab-examples" class="effects-tab-panel">
492
+ <div id="tab-examples" class="fxx-guide-panel" data-bind-hidden="guide.value !== 'examples'">
470
493
  <h3 class="mb-2">Hero section with typewriter</h3>
471
494
  <pre class="code-block mb-3">[reveal animation="fade"]
472
495
  # Welcome to our site
@@ -530,7 +553,7 @@ A compelling subheading that draws users in.
530
553
  </div>
531
554
 
532
555
  <!-- Celebrations tab -->
533
- <div id="tab-celebrations" class="effects-tab-panel">
556
+ <div id="tab-celebrations" class="fxx-guide-panel" data-bind-hidden="guide.value !== 'celebrations'">
534
557
  <h3 class="mb-2">Firework <span class="badge badge-info">CSS only</span></h3>
535
558
  <p class="text-muted mb-2">Pure CSS particle effect - no JavaScript required. Works even without the plugin
536
559
  enabled. Use self-closing for standalone particles, or wrapping to apply the firework classes to
@@ -747,6 +770,66 @@ The strips rain down inside this block only.
747
770
  full-page overlay (<code>position:fixed</code>); the wrapping form attaches the canvas to the
748
771
  wrapping div so the parade is constrained to that block.</p>
749
772
  </div>
750
- </div><!-- /tab-panel shortcodes -->
751
- </div><!-- /tab-content -->
752
- </div><!-- /effects-tabs -->
773
+ </div>
774
+ </div>
775
+ </div>
776
+ </div>
777
+
778
+ <style>
779
+ .fxx { display: flex; flex-direction: column; gap: .75rem; }
780
+ .fxx [hidden], .fxx-form [hidden] { display: none !important; }
781
+ .fxx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .7rem; margin-top: .75rem; }
782
+ .fxx-live { margin-top: 0; }
783
+ .fxx-card-title { display: flex; align-items: center; gap: .35rem; font-weight: 600; }
784
+ .fxx-chips { display: flex; flex-wrap: wrap; gap: .35rem; }
785
+ .fxx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .55rem; border-radius: 999px; white-space: nowrap;
786
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
787
+ .fxx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
788
+ .fxx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
789
+ .fxx-chip.is-info { background: color-mix(in srgb, var(--dm-primary) 14%, transparent); color: var(--dm-primary); }
790
+ .fxx-chip.is-muted { background: none; font-weight: 400; }
791
+ .fxx-unsaved { margin: 0; display: flex; align-items: center; gap: .5rem; padding: .5rem .8rem; border-radius: var(--dm-radius-md);
792
+ background: color-mix(in srgb, var(--dm-warning) 16%, transparent); color: var(--dm-text); }
793
+ .fxx-dot { color: var(--dm-warning); margin-left: .2rem; }
794
+ .fxx-count { opacity: .65; font-size: var(--dm-text-xs); margin-left: .2rem; }
795
+ .fxx-spacer { flex: 1; }
796
+ .fxx-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .85rem; }
797
+ .fxx-wide { grid-column: 1 / -1; }
798
+ .fxx-setting { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
799
+ .fxx-setting .form-label { margin: 0; }
800
+ .fxx .form-label { display: flex; align-items: center; gap: .3rem; }
801
+ .fxx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
802
+ .fxx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
803
+ .fxx-actions .btn, .fxx-unsaved .btn { display: inline-flex; align-items: center; gap: .3rem; }
804
+ .fxx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
805
+ .fxx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
806
+ .fxx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
807
+ .fxx-seg { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
808
+ .fxx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .65rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
809
+ .fxx-seg button + button { border-left: 1px solid var(--dm-border); }
810
+ .fxx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
811
+ .fxx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
812
+ .fxx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
813
+ .fxx-list { display: flex; flex-direction: column; }
814
+ .fxx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
815
+ .fxx-row:last-child { border-bottom: 0; }
816
+ .fxx-row:hover, .fxx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
817
+ .fxx-tag { flex-shrink: 0; min-width: 7.5rem; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm); color: var(--dm-primary); }
818
+ .fxx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
819
+ .fxx-main strong, .fxx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
820
+ .fxx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
821
+ .fxx-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; }
822
+ .fxx-kebab:hover { color: var(--dm-primary); }
823
+ .fxx-guide-tabs { align-self: flex-start; }
824
+ /* The details slideover */
825
+ .fxx-form { display: flex; flex-direction: column; gap: .85rem; }
826
+ .fxx-form pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm);
827
+ padding: .6rem .75rem; border-radius: var(--dm-radius-md); border: 1px solid var(--dm-border); background: var(--dm-surface-raised, color-mix(in srgb, var(--dm-text) 5%, transparent)); color: var(--dm-text); }
828
+ .fxx-sub { display: flex; align-items: center; gap: .4rem; font-weight: 600; }
829
+ .fxx-attrs { width: 100%; border-collapse: collapse; font-size: var(--dm-text-sm); }
830
+ .fxx-attrs th, .fxx-attrs td { text-align: left; padding: .35rem .4rem; border-bottom: 1px solid var(--dm-border); vertical-align: top; }
831
+ .fxx-attrs th { color: var(--dm-text-muted); font-weight: 500; font-size: var(--dm-text-xs); }
832
+ .fxx-attrs code { color: var(--dm-primary); }
833
+ .fxx-attrs small { display: block; color: var(--dm-text-muted); }
834
+ @media (max-width: 720px) { .fxx-grid { grid-template-columns: 1fr; } .fxx-tag { min-width: 0; } }
835
+ </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>