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.
@@ -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-multi-chooser" type="module"></script>
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-multi-chooser"></script>
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 -->