domma-cms 0.39.0 → 0.40.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.
- package/admin/css/admin.css +1 -1
- package/admin/index.html +1 -1
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/admin/js/templates/menu-editor.html +23 -0
- package/admin/js/views/form-editor.js +6 -6
- package/admin/js/views/menu-editor.js +7 -7
- package/package.json +1 -1
- package/public/css/site.css +1 -1
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/menu-decor.mjs +1 -1
- package/public/js/site.js +1 -1
- package/server/services/markdown.js +10 -2
- package/server/services/menus.js +76 -0
- package/server/templates/page.html +2 -2
package/server/services/menus.js
CHANGED
|
@@ -81,6 +81,65 @@ export function colourToCss(value) {
|
|
|
81
81
|
return '';
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Floating menus — a menu with position "floating" detaches from the document
|
|
86
|
+
// flow and pins itself to a viewport corner or edge, the same way the
|
|
87
|
+
// breadcrumb trail does. The anchor/offset shape deliberately mirrors
|
|
88
|
+
// site.json's `breadcrumbs` config so the two read alike in the admin.
|
|
89
|
+
//
|
|
90
|
+
// Keep FLOAT_ANCHORS and floatToCss in sync with the copies in
|
|
91
|
+
// public/js/menu-decor.mjs (tests/services/menu-decor-client.test.js asserts it).
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
export const FLOAT_ANCHORS = new Set(['TL', 'TR', 'BL', 'BR', 'L', 'R']);
|
|
94
|
+
|
|
95
|
+
export const FLOAT_DEFAULTS = {anchor: 'TL', offsetX: 16, offsetY: 16, zIndex: 150};
|
|
96
|
+
|
|
97
|
+
function floatNumber(value, fallback, min, max) {
|
|
98
|
+
const n = Number(value);
|
|
99
|
+
if (!Number.isFinite(n)) return fallback;
|
|
100
|
+
return Math.min(max, Math.max(min, Math.round(n)));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build the inline CSS for a floating menu. Returns '' for any menu that is
|
|
105
|
+
* not floating, so callers can apply it unconditionally.
|
|
106
|
+
*
|
|
107
|
+
* `L` and `R` anchor to the middle of that edge; the four corner anchors use
|
|
108
|
+
* both offsets. Width is optional and only meaningful for panel-shaped menus
|
|
109
|
+
* (the admin sidebar); it is ignored when absent.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} menu A menu record (uses .position and .float)
|
|
112
|
+
* @returns {string} CSS declarations, semicolon-separated, no trailing ';'
|
|
113
|
+
*/
|
|
114
|
+
export function floatToCss(menu) {
|
|
115
|
+
if (!menu || menu.position !== 'floating') return '';
|
|
116
|
+
|
|
117
|
+
const f = (menu.float && typeof menu.float === 'object' && !Array.isArray(menu.float)) ? menu.float : {};
|
|
118
|
+
const anchor = FLOAT_ANCHORS.has(f.anchor) ? f.anchor : FLOAT_DEFAULTS.anchor;
|
|
119
|
+
const x = floatNumber(f.offsetX, FLOAT_DEFAULTS.offsetX, 0, 4000);
|
|
120
|
+
const y = floatNumber(f.offsetY, FLOAT_DEFAULTS.offsetY, 0, 4000);
|
|
121
|
+
const z = floatNumber(f.zIndex, FLOAT_DEFAULTS.zIndex, 0, 2147483647);
|
|
122
|
+
|
|
123
|
+
const decls = ['position:fixed', `z-index:${z}`];
|
|
124
|
+
|
|
125
|
+
if (anchor === 'L' || anchor === 'R') {
|
|
126
|
+
decls.push('top:50%', 'transform:translateY(-50%)');
|
|
127
|
+
decls.push(anchor === 'L' ? `left:${x}px` : `right:${x}px`);
|
|
128
|
+
} else {
|
|
129
|
+
decls.push(anchor[0] === 'T' ? `top:${y}px` : `bottom:${y}px`);
|
|
130
|
+
decls.push(anchor[1] === 'L' ? `left:${x}px` : `right:${x}px`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (f.width != null && f.width !== '') {
|
|
134
|
+
decls.push(`width:${floatNumber(f.width, 260, 120, 600)}px`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// A pinned panel must never grow past the viewport — it scrolls instead.
|
|
138
|
+
decls.push(`max-height:calc(100vh - ${(anchor === 'L' || anchor === 'R') ? 32 : y * 2}px)`);
|
|
139
|
+
|
|
140
|
+
return decls.join(';');
|
|
141
|
+
}
|
|
142
|
+
|
|
84
143
|
/**
|
|
85
144
|
* Walk a menu tree and resolve live badge counts. For any item whose
|
|
86
145
|
* `badge.countFrom` names a collection, the entry count replaces `badge.text`.
|
|
@@ -215,6 +274,7 @@ export async function createMenu(input) {
|
|
|
215
274
|
...(input.variant != null && {variant: input.variant}),
|
|
216
275
|
...(input.position != null && {position: input.position}),
|
|
217
276
|
...(input.appearOnHover != null && {appearOnHover: !!input.appearOnHover}),
|
|
277
|
+
...(input.float != null && {float: input.float}),
|
|
218
278
|
...(input.style != null && {style: input.style}),
|
|
219
279
|
items: Array.isArray(input.items) ? input.items : [],
|
|
220
280
|
meta: {
|
|
@@ -253,6 +313,7 @@ export async function updateMenu(slug, input) {
|
|
|
253
313
|
...(input.variant != null && {variant: input.variant}),
|
|
254
314
|
...(input.position != null && {position: input.position}),
|
|
255
315
|
...(input.appearOnHover != null && {appearOnHover: !!input.appearOnHover}),
|
|
316
|
+
...(input.float != null && {float: input.float}),
|
|
256
317
|
...(input.style != null && {style: input.style}),
|
|
257
318
|
items: Array.isArray(input.items) ? input.items : [],
|
|
258
319
|
meta: {
|
|
@@ -333,6 +394,21 @@ export function validateMenu(menu) {
|
|
|
333
394
|
if (menu.position != null && !POSITIONS.has(menu.position)) {
|
|
334
395
|
errors.push(`Invalid position "${menu.position}" — expected one of: ${[...POSITIONS].join(', ')}`);
|
|
335
396
|
}
|
|
397
|
+
if (menu.float != null) {
|
|
398
|
+
if (typeof menu.float !== 'object' || Array.isArray(menu.float)) {
|
|
399
|
+
errors.push('float must be an object');
|
|
400
|
+
} else {
|
|
401
|
+
if (menu.float.anchor != null && !FLOAT_ANCHORS.has(menu.float.anchor)) {
|
|
402
|
+
errors.push(`float.anchor must be one of: ${[...FLOAT_ANCHORS].join(', ')}`);
|
|
403
|
+
}
|
|
404
|
+
for (const key of ['offsetX', 'offsetY', 'width', 'zIndex']) {
|
|
405
|
+
const v = menu.float[key];
|
|
406
|
+
if (v != null && v !== '' && !Number.isFinite(Number(v))) {
|
|
407
|
+
errors.push(`float.${key} must be a number`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
336
412
|
if (!Array.isArray(menu.items)) {
|
|
337
413
|
errors.push('items must be an array');
|
|
338
414
|
} else {
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
</script>
|
|
117
117
|
|
|
118
118
|
<!-- Site initialisation -->
|
|
119
|
-
<script src="/public/js/site.js?v=20260817-
|
|
119
|
+
<script src="/public/js/site.js?v=20260817-floating-menus" type="module"></script>
|
|
120
120
|
<script src="/public/js/collection-context.js?v=20260722-ctx-cursor" defer></script>
|
|
121
121
|
|
|
122
122
|
<!-- Interactive Collection Browser (used by [collection] shortcodes with
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
<script src="/public/js/collection-browser.js?v=20260523-browser"></script>
|
|
125
125
|
|
|
126
126
|
<!-- Core Forms (logic engine must load before renderer) -->
|
|
127
|
-
<script src="/public/js/form-logic-engine.js?v=20260817-
|
|
127
|
+
<script src="/public/js/form-logic-engine.js?v=20260817-transition-speed"></script>
|
|
128
128
|
<script src="/public/js/forms.js?v=20260817-multi-chooser" type="module"></script>
|
|
129
129
|
|
|
130
130
|
<!-- Core effects runtime -->
|