domma-cms 0.106.0 → 0.107.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/admin/dist/domma/domma-tools.css +3 -3
  2. package/admin/dist/domma/domma-tools.min.js +3 -3
  3. package/admin/js/lib/effect-defs.js +1 -1
  4. package/admin/js/lib/effects-builder.js +2 -2
  5. package/admin/js/lib/shortcode-context-menu.js +2 -2
  6. package/admin/js/templates/docs/usage-tools.html +7 -0
  7. package/package.json +2 -2
  8. package/plugins/_lib/call/invite.js +172 -0
  9. package/plugins/_lib/call/rooms.js +19 -5
  10. package/plugins/_lib/call/routes.js +78 -3
  11. package/plugins/_lib/mail/render.js +14 -1
  12. package/plugins/_lib/public/call/call.css +1 -1
  13. package/plugins/_lib/public/call/call.js +287 -23
  14. package/plugins/booking/CLAUDE.md +18 -1
  15. package/plugins/booking/docs/guide.md +1 -0
  16. package/plugins/booking/lib/call.js +18 -2
  17. package/plugins/booking/lib/store.js +2 -0
  18. package/plugins/booking/plugin.js +59 -6
  19. package/plugins/booking/plugin.json +1 -1
  20. package/plugins/booking/plugin.public.js +4 -4
  21. package/plugins/booking/tests/api.test.js +114 -0
  22. package/plugins/free-tier.lock.json +10 -10
  23. package/public/js/celebrations/index.js +1 -1
  24. package/public/js/celebrations/themes/butterfly-drawing.js +1 -0
  25. package/public/js/celebrations/themes/halloween-drawing.js +1 -0
  26. package/public/js/celebrations/themes/halloween.js +1 -1
  27. package/public/js/celebrations/themes/valentines.js +1 -1
  28. package/public/js/celebrations/trait-data.mjs +1 -0
  29. package/public/js/effects.js +1 -1
  30. package/server/routes/api/calls.js +25 -3
  31. package/server/routes/call-public.js +4 -3
  32. package/server/services/callInvites.js +238 -0
  33. package/server/services/hooks.js +55 -0
  34. package/server/services/instantCalls.js +30 -3
  35. package/server/services/plugins.js +6 -1
  36. package/server/templates/page.html +1 -1
@@ -1,9 +1,9 @@
1
1
  /*!
2
- * Domma Tools CSS v0.51.4
2
+ * Domma Tools CSS v0.51.5
3
3
  * Dynamic Object Manipulation & Modeling API
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-09-30T15:11:39.929Z
6
- * Commit: 86dbba4
5
+ * Built: 2026-10-01T08:33:07.263Z
6
+ * Commit: 094a9d3
7
7
  */
8
8
 
9
9
  /*!
@@ -1,9 +1,9 @@
1
1
  /*!
2
- * Domma Tools v0.51.4
2
+ * Domma Tools v0.51.5
3
3
  * Developer tools: Theme Roller & Page Roller
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-09-30T15:11:28.566Z
6
- * Commit: 86dbba4
5
+ * Built: 2026-10-01T08:32:52.270Z
6
+ * Commit: 094a9d3
7
7
  *
8
8
  * Requires: domma.min.js
9
9
  */
@@ -1 +1 @@
1
- export const EFFECTS=[{name:"reveal",label:"Reveal",category:"Entrance",description:"Scroll-triggered entrance animation when the element enters the viewport.",attrs:[{name:"animation",label:"Animation",type:"select",default:"fade",options:[["fade","Fade"],["slide-up","Slide up"],["slide-down","Slide down"],["zoom","Zoom"],["flip","Flip"]]},{name:"duration",label:"Duration (ms)",type:"number",default:600},{name:"delay",label:"Delay (ms)",type:"number",default:0},{name:"threshold",label:"Threshold",type:"number",default:.1,step:.05,min:0,max:1},{name:"once",label:"Animate once",type:"checkbox",default:!0}],hosts:["card","box","hero","banner","col"],previewable:!0},{name:"breathe",label:"Breathe",category:"Animation",description:"Continuous gentle scaling - makes elements feel alive.",attrs:[{name:"amplitude",label:"Amplitude (px)",type:"number",default:8},{name:"duration",label:"Duration (ms)",type:"number",default:3e3}],hosts:["card","box","hero","col","button"],previewable:!0},{name:"pulse",label:"Pulse",category:"Animation",description:"A repeating scale pulse - stronger than breathe.",attrs:[{name:"scale",label:"Scale",type:"number",default:1.05,step:.01},{name:"duration",label:"Duration (ms)",type:"number",default:1e3}],hosts:["card","box","hero","banner","button"],previewable:!0},{name:"shake",label:"Shake",category:"Animation",description:"One-shot horizontal or vertical shake.",attrs:[{name:"intensity",label:"Intensity (px)",type:"number",default:5},{name:"duration",label:"Duration (ms)",type:"number",default:500},{name:"direction",label:"Direction",type:"select",default:"horizontal",options:[["horizontal","Horizontal"],["vertical","Vertical"]]}],hosts:["card","box","banner","button"],previewable:!0},{name:"animate",label:"CSS Animate",category:"Animation",description:"Applies a Domma CSS animation utility class - no JavaScript.",attrs:[{name:"type",label:"Type",type:"select",default:"fade-in-up",options:[["fade-in","fade-in"],["fade-in-up","fade-in-up"],["fade-in-down","fade-in-down"],["fade-in-left","fade-in-left"],["fade-in-right","fade-in-right"],["slide-in-up","slide-in-up"],["slide-in-down","slide-in-down"],["zoom-in","zoom-in"],["zoom-out","zoom-out"],["bounce","bounce"],["pulse","pulse"],["shake","shake"],["spin","spin"],["flip","flip"]]},{name:"duration",label:"Duration",type:"select",default:"normal",options:[["fast","fast"],["normal","normal"],["slow","slow"],["slower","slower"]]},{name:"delay",label:"Delay (ms)",type:"number",default:0},{name:"repeat",label:"Repeat",type:"select",default:"once",options:[["once","Once"],["infinite","Infinite"]]}],hosts:["card","box","hero","banner","col","button"],previewable:!0},{name:"scribe",label:"Scribe (typewriter)",category:"Text",description:"Types text character by character. Simple mode only - for the action-script composer use Insert \u2192 Scribe.",attrs:[{name:"speed",label:"Speed (ms/char)",type:"number",default:50},{name:"cursor",label:"Show cursor",type:"checkbox",default:!0},{name:"loop",label:"Loop",type:"checkbox",default:!1}],hosts:[],previewable:!0},{name:"scramble",label:"Scramble",category:"Text",description:"Reveals text through a character-scramble animation.",attrs:[{name:"speed",label:"Speed (ms/char)",type:"number",default:50},{name:"loop",label:"Loop",type:"checkbox",default:!1}],hosts:[],previewable:!0},{name:"counter",label:"Counter",category:"Text",description:"Animates a number from a starting value to a target. Self-closing shortcode.",attrs:[{name:"to",label:"To",type:"number",default:100},{name:"from",label:"From",type:"number",default:0},{name:"duration",label:"Duration (ms)",type:"number",default:2e3},{name:"prefix",label:"Prefix",type:"text",default:""},{name:"suffix",label:"Suffix",type:"text",default:""},{name:"decimals",label:"Decimal places",type:"number",default:0},{name:"separator",label:"Thousand separator",type:"text",default:""}],hosts:[],previewable:!0,selfClosing:!0},{name:"ripple",label:"Ripple",category:"Visual",description:"Click-triggered ripple effect on the element.",attrs:[{name:"colour",label:"Colour",type:"colour",default:"#7c6af7"},{name:"duration",label:"Duration (ms)",type:"number",default:600},{name:"opacity",label:"Opacity",type:"number",default:.3,step:.05,min:0,max:1}],hosts:["card","box","col","button"],previewable:!0},{name:"twinkle",label:"Twinkle",category:"Visual",description:"Generates animated particle effects inside the element.",attrs:[{name:"count",label:"Particle count",type:"number",default:80},{name:"shape",label:"Shape",type:"select",default:"star",options:[["star","Star"],["circle","Circle"],["square","Square"]]},{name:"colour",label:"Colour",type:"colour",default:"#ffffff"},{name:"min-size",label:"Min size (px)",type:"number",default:2},{name:"max-size",label:"Max size (px)",type:"number",default:6}],hosts:["card","box","hero"],previewable:!0},{name:"ambient",label:"Ambient background",category:"Visual",description:"Applies an animated CSS background class to the element.",attrs:[{name:"type",label:"Type",type:"select",default:"float-blobs",options:[["float-blobs","Float blobs"],["float-orbs","Float orbs"],["float-particles","Float particles"],["rotate-glow","Rotate glow"],["rotate-dual","Rotate dual"],["rotate-spotlight","Rotate spotlight"],["wave","Wave"],["ripple","Ripple"],["aurora","Aurora"]]},{name:"speed",label:"Speed",type:"select",default:"normal",options:[["slow","Slow"],["normal","Normal"],["fast","Fast"]]},{name:"intensity",label:"Intensity",type:"select",default:"subtle",options:[["subtle","Subtle"],["intense","Intense"]]}],hosts:["card","box","hero","banner"],previewable:!0},{name:"firework",label:"Firework",category:"Celebrations",description:"CSS-only firework particle. Self-closing or wrapping form.",attrs:[{name:"type",label:"Type",type:"select",default:"burst",options:[["burst","Burst"],["sparkle","Sparkle"],["trail","Trail"]]},{name:"colour",label:"Colour theme",type:"select",default:"rainbow",options:[["rainbow","Rainbow"],["primary","Primary"],["info","Info"],["success","Success"],["warning","Warning"],["danger","Danger"]]},{name:"size",label:"Size",type:"select",default:"md",options:[["sm","Small"],["md","Medium"],["lg","Large"]]},{name:"continuous",label:"Continuous",type:"checkbox",default:!1},{name:"hover",label:"On hover only",type:"checkbox",default:!1}],hosts:[],previewable:!0},{name:"fireworks",label:"Fireworks show",category:"Celebrations",description:"A container with multiple firework particles inside.",attrs:[],hosts:[],previewable:!1},{name:"celebrate",label:"Celebrate",category:"Celebrations",description:"Canvas-based seasonal particle system. Runs full-page on the published site.",attrs:[{name:"theme",label:"Theme",type:"select",default:"auto",options:[["auto","Auto (date-detected)"],["christmas","Christmas"],["halloween","Halloween"],["valentines","Valentines"],["guy-fawkes","Guy Fawkes"],["st-patricks","St Patrick's"],["st-davids","St David's"],["st-georges","St George's"],["st-andrews","St Andrew's"]]},{name:"intensity",label:"Intensity",type:"select",default:"medium",options:[["light","Light"],["medium","Medium"],["heavy","Heavy"]]},{name:"snow-only",label:"Snow only (Christmas)",type:"checkbox",default:!1,hint:"Just the falling snow - every Christmas decoration off."},{name:"trees",label:"Christmas: trees",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"wreaths",label:"Christmas: wreaths",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"northstars",label:"Christmas: North Star",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"snowmen",label:"Christmas: snowmen",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"sleigh",label:"Christmas: Santa's sleigh",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"elf",label:"Christmas: elf",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"train",label:"Christmas: train",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"firework",label:"Christmas: fireworks",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"robin",label:"Christmas: robin",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"peeing-elf",label:"Peeing elf (opt-in)",type:"checkbox",default:!1,hint:"A clothed rear-view gag. Off by default; Snow only keeps it off."},{name:"thieving-elf",label:"Thieving elf (opt-in)",type:"checkbox",default:!1,hint:"Runs to a tree, takes a present and escapes. Needs a visible tree."}],hosts:[],previewable:!1,selfClosing:!0},{name:"christmas",label:"Christmas",category:"Celebrations",description:"Christmas on the published site: snow, trees, wreaths, snowmen, Santa's sleigh and more. Snow only for just the snow.",attrs:[{name:"intensity",label:"Intensity",type:"select",default:"medium",options:[["light","Light"],["medium","Medium"],["heavy","Heavy"]]},{name:"snow-only",label:"Snow only",type:"checkbox",default:!1,hint:"Just the falling snow - every decoration off."},{name:"trees",label:"trees",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"wreaths",label:"wreaths",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"northstars",label:"North Star",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"snowmen",label:"snowmen",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"sleigh",label:"Santa's sleigh",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"elf",label:"elf",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"train",label:"train",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"firework",label:"fireworks",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"robin",label:"robin",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"peeing-elf",label:"Peeing elf (opt-in)",type:"checkbox",default:!1,hint:"A clothed rear-view gag. Off by default; Snow only keeps it off."},{name:"thieving-elf",label:"Thieving elf (opt-in)",type:"checkbox",default:!1,hint:"Runs to a tree, takes a present and escapes. Needs a visible tree."}],hosts:[],previewable:!1,selfClosing:!0},{name:"ticker-tape",label:"Ticker tape",category:"Celebrations",description:"Coloured rectangular strips fall from the top, sway, rotate, and fade. Inserted self-closing (full-page); hand-write the wrapping form for container-scoped.",attrs:[{name:"palette",label:"Palette",type:"select",default:"theme",options:[["theme","Theme"],["rainbow","Rainbow"],["gold","Gold"],["silver","Silver"],["festive","Festive"],["pastel","Pastel"],["mono","Mono"]]},{name:"density",label:"Density",type:"number",default:50},{name:"speed",label:"Speed multiplier",type:"number",default:1,step:.1},{name:"sway",label:"Sway (px)",type:"number",default:60},{name:"burst",label:"Burst mode (one-shot)",type:"checkbox",default:!1},{name:"burst-count",label:"Burst count",type:"number",default:150}],hosts:["hero"],previewable:!0,selfClosing:!0},{name:"butterflies",label:"Butterflies",category:"Celebrations",description:"Procedurally drawn butterflies wander and rise with flapping wings. Self-closing for full-page; wrapping form for container-scoped.",attrs:[{name:"palette",label:"Palette",type:"select",default:"meadow",options:[["meadow","Meadow"],["theme","Theme"],["rainbow","Rainbow"],["pastel","Pastel"],["festive","Festive"],["gold","Gold"],["silver","Silver"],["mono","Mono"],["sunset","Sunset"],["ocean","Ocean"],["forest","Forest"],["bridal","Bridal"],["firefly","Firefly"]]},{name:"density",label:"Density",type:"number",default:18},{name:"speed",label:"Speed multiplier",type:"number",default:1,step:.1},{name:"wander",label:"Wander",type:"number",default:1,step:.1,hint:"Steering erraticness"},{name:"rise-speed",label:"Rise speed",type:"number",default:.4,step:.1,hint:"Upward drift bias"},{name:"flap-speed",label:"Flap speed",type:"number",default:1,step:.1},{name:"min-size",label:"Min wingspan (px)",type:"number",default:14},{name:"max-size",label:"Max wingspan (px)",type:"number",default:26},{name:"two-tone",label:"Two-tone wings",type:"checkbox",default:!0},{name:"burst",label:"Burst mode (one-shot)",type:"checkbox",default:!1},{name:"burst-count",label:"Burst count",type:"number",default:40}],hosts:["hero"],previewable:!0},{name:"strobe",label:"Strobe lighting",category:"Celebrations",description:"Light beams sweep from the corners and brighten where they cross - club, concert, or searchlight moods. Self-closing for full-page; wrapping form for container-scoped.",attrs:[{name:"preset",label:"Preset",type:"select",default:"club",options:[["club","Club"],["concert","Concert"],["police","Police"],["searchlight","Searchlight"],["scanner","Scanner"],["mood","Mood"]],hint:"Other fields override the preset when changed"},{name:"motion",label:"Motion",type:"select",default:"sweep",options:[["sweep","Sweep (fan back and forth)"],["rotate","Rotate (full circle)"]]},{name:"speed",label:"Speed multiplier",type:"number",default:1,step:.1},{name:"sweep-arc",label:"Sweep arc (deg)",type:"number",default:100},{name:"beam-width",label:"Beam width (deg)",type:"number",default:16},{name:"intensity",label:"Intensity",type:"number",default:.7,step:.05,min:0,max:1},{name:"flicker",label:"Flicker",type:"checkbox",default:!0},{name:"hz",label:"Flicker rate (Hz)",type:"number",default:10,hint:"Rates above 5 flashes/sec can trigger photosensitive seizures"},{name:"colours",label:"Colours",type:"text",default:"",placeholder:"#ffffff, #38bdf8",hint:"Comma-separated hex colours - overrides the preset palette"},{name:"duration",label:"Auto-stop (ms)",type:"number",default:"",hint:"Stop after this many ms - blank runs forever"}],hosts:["hero"],previewable:!0}];export function getEffect(t){return EFFECTS.find(e=>e.name===t)||null}export function effectsByCategory(){return["Entrance","Animation","Text","Visual","Celebrations"].map(e=>({category:e,effects:EFFECTS.filter(a=>a.category===e)}))}export function effectsInjectableInto(t){return EFFECTS.filter(e=>e.hosts.includes(t))}
1
+ import{CELEBRATION_THEMES as i,traitAttribute as f}from"../../../public/js/celebrations/trait-data.mjs";export{i as CELEBRATION_THEMES};export const EFFECTS=[{name:"reveal",label:"Reveal",category:"Entrance",description:"Scroll-triggered entrance animation when the element enters the viewport.",attrs:[{name:"animation",label:"Animation",type:"select",default:"fade",options:[["fade","Fade"],["slide-up","Slide up"],["slide-down","Slide down"],["zoom","Zoom"],["flip","Flip"]]},{name:"duration",label:"Duration (ms)",type:"number",default:600},{name:"delay",label:"Delay (ms)",type:"number",default:0},{name:"threshold",label:"Threshold",type:"number",default:.1,step:.05,min:0,max:1},{name:"once",label:"Animate once",type:"checkbox",default:!0}],hosts:["card","box","hero","banner","col"],previewable:!0},{name:"breathe",label:"Breathe",category:"Animation",description:"Continuous gentle scaling - makes elements feel alive.",attrs:[{name:"amplitude",label:"Amplitude (px)",type:"number",default:8},{name:"duration",label:"Duration (ms)",type:"number",default:3e3}],hosts:["card","box","hero","col","button"],previewable:!0},{name:"pulse",label:"Pulse",category:"Animation",description:"A repeating scale pulse - stronger than breathe.",attrs:[{name:"scale",label:"Scale",type:"number",default:1.05,step:.01},{name:"duration",label:"Duration (ms)",type:"number",default:1e3}],hosts:["card","box","hero","banner","button"],previewable:!0},{name:"shake",label:"Shake",category:"Animation",description:"One-shot horizontal or vertical shake.",attrs:[{name:"intensity",label:"Intensity (px)",type:"number",default:5},{name:"duration",label:"Duration (ms)",type:"number",default:500},{name:"direction",label:"Direction",type:"select",default:"horizontal",options:[["horizontal","Horizontal"],["vertical","Vertical"]]}],hosts:["card","box","banner","button"],previewable:!0},{name:"animate",label:"CSS Animate",category:"Animation",description:"Applies a Domma CSS animation utility class - no JavaScript.",attrs:[{name:"type",label:"Type",type:"select",default:"fade-in-up",options:[["fade-in","fade-in"],["fade-in-up","fade-in-up"],["fade-in-down","fade-in-down"],["fade-in-left","fade-in-left"],["fade-in-right","fade-in-right"],["slide-in-up","slide-in-up"],["slide-in-down","slide-in-down"],["zoom-in","zoom-in"],["zoom-out","zoom-out"],["bounce","bounce"],["pulse","pulse"],["shake","shake"],["spin","spin"],["flip","flip"]]},{name:"duration",label:"Duration",type:"select",default:"normal",options:[["fast","fast"],["normal","normal"],["slow","slow"],["slower","slower"]]},{name:"delay",label:"Delay (ms)",type:"number",default:0},{name:"repeat",label:"Repeat",type:"select",default:"once",options:[["once","Once"],["infinite","Infinite"]]}],hosts:["card","box","hero","banner","col","button"],previewable:!0},{name:"scribe",label:"Scribe (typewriter)",category:"Text",description:"Types text character by character. Simple mode only - for the action-script composer use Insert \u2192 Scribe.",attrs:[{name:"speed",label:"Speed (ms/char)",type:"number",default:50},{name:"cursor",label:"Show cursor",type:"checkbox",default:!0},{name:"loop",label:"Loop",type:"checkbox",default:!1}],hosts:[],previewable:!0},{name:"scramble",label:"Scramble",category:"Text",description:"Reveals text through a character-scramble animation.",attrs:[{name:"speed",label:"Speed (ms/char)",type:"number",default:50},{name:"loop",label:"Loop",type:"checkbox",default:!1}],hosts:[],previewable:!0},{name:"counter",label:"Counter",category:"Text",description:"Animates a number from a starting value to a target. Self-closing shortcode.",attrs:[{name:"to",label:"To",type:"number",default:100},{name:"from",label:"From",type:"number",default:0},{name:"duration",label:"Duration (ms)",type:"number",default:2e3},{name:"prefix",label:"Prefix",type:"text",default:""},{name:"suffix",label:"Suffix",type:"text",default:""},{name:"decimals",label:"Decimal places",type:"number",default:0},{name:"separator",label:"Thousand separator",type:"text",default:""}],hosts:[],previewable:!0,selfClosing:!0},{name:"ripple",label:"Ripple",category:"Visual",description:"Click-triggered ripple effect on the element.",attrs:[{name:"colour",label:"Colour",type:"colour",default:"#7c6af7"},{name:"duration",label:"Duration (ms)",type:"number",default:600},{name:"opacity",label:"Opacity",type:"number",default:.3,step:.05,min:0,max:1}],hosts:["card","box","col","button"],previewable:!0},{name:"twinkle",label:"Twinkle",category:"Visual",description:"Generates animated particle effects inside the element.",attrs:[{name:"count",label:"Particle count",type:"number",default:80},{name:"shape",label:"Shape",type:"select",default:"star",options:[["star","Star"],["circle","Circle"],["square","Square"]]},{name:"colour",label:"Colour",type:"colour",default:"#ffffff"},{name:"min-size",label:"Min size (px)",type:"number",default:2},{name:"max-size",label:"Max size (px)",type:"number",default:6}],hosts:["card","box","hero"],previewable:!0},{name:"ambient",label:"Ambient background",category:"Visual",description:"Applies an animated CSS background class to the element.",attrs:[{name:"type",label:"Type",type:"select",default:"float-blobs",options:[["float-blobs","Float blobs"],["float-orbs","Float orbs"],["float-particles","Float particles"],["rotate-glow","Rotate glow"],["rotate-dual","Rotate dual"],["rotate-spotlight","Rotate spotlight"],["wave","Wave"],["ripple","Ripple"],["aurora","Aurora"]]},{name:"speed",label:"Speed",type:"select",default:"normal",options:[["slow","Slow"],["normal","Normal"],["fast","Fast"]]},{name:"intensity",label:"Intensity",type:"select",default:"subtle",options:[["subtle","Subtle"],["intense","Intense"]]}],hosts:["card","box","hero","banner"],previewable:!0},{name:"firework",label:"Firework",category:"Celebrations",description:"CSS-only firework particle. Self-closing or wrapping form.",attrs:[{name:"type",label:"Type",type:"select",default:"burst",options:[["burst","Burst"],["sparkle","Sparkle"],["trail","Trail"]]},{name:"colour",label:"Colour theme",type:"select",default:"rainbow",options:[["rainbow","Rainbow"],["primary","Primary"],["info","Info"],["success","Success"],["warning","Warning"],["danger","Danger"]]},{name:"size",label:"Size",type:"select",default:"md",options:[["sm","Small"],["md","Medium"],["lg","Large"]]},{name:"continuous",label:"Continuous",type:"checkbox",default:!1},{name:"hover",label:"On hover only",type:"checkbox",default:!1}],hosts:[],previewable:!0},{name:"fireworks",label:"Fireworks show",category:"Celebrations",description:"A container with multiple firework particles inside.",attrs:[],hosts:[],previewable:!1},{name:"celebrate",label:"Celebrate",category:"Celebrations",description:"Canvas-based seasonal particle system. Runs full-page on the published site.",attrs:[{name:"theme",label:"Theme",type:"select",default:"auto",options:[["auto","Auto (date-detected)"],["christmas","Christmas"],["halloween","Halloween"],["valentines","Valentines"],["guy-fawkes","Guy Fawkes"],["st-patricks","St Patrick's"],["st-davids","St David's"],["st-georges","St George's"],["st-andrews","St Andrew's"]]},{name:"intensity",label:"Intensity",type:"select",default:"medium",options:[["light","Light"],["medium","Medium"],["heavy","Heavy"]]},{name:"snow-only",label:"Snow only (Christmas)",type:"checkbox",default:!1,hint:"Just the falling snow - every Christmas decoration off."},{name:"trees",label:"Christmas: trees",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"wreaths",label:"Christmas: wreaths",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"northstars",label:"Christmas: North Star",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"snowmen",label:"Christmas: snowmen",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"sleigh",label:"Christmas: Santa's sleigh",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"elf",label:"Christmas: elf",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"train",label:"Christmas: train",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"firework",label:"Christmas: fireworks",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"robin",label:"Christmas: robin",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"peeing-elf",label:"Peeing elf (opt-in)",type:"checkbox",default:!1,hint:"A clothed rear-view gag. Off by default; Snow only keeps it off."},{name:"thieving-elf",label:"Thieving elf (opt-in)",type:"checkbox",default:!1,hint:"Runs to a tree, takes a present and escapes. Needs a visible tree."}],hosts:[],previewable:!1,selfClosing:!0},{name:"christmas",label:"Christmas",category:"Celebrations",description:"Christmas on the published site: snow, trees, wreaths, snowmen, Santa's sleigh and more. Snow only for just the snow.",attrs:[{name:"intensity",label:"Intensity",type:"select",default:"medium",options:[["light","Light"],["medium","Medium"],["heavy","Heavy"]]},{name:"snow-only",label:"Snow only",type:"checkbox",default:!1,hint:"Just the falling snow - every decoration off."},{name:"trees",label:"trees",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"wreaths",label:"wreaths",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"northstars",label:"North Star",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"snowmen",label:"snowmen",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"sleigh",label:"Santa's sleigh",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"elf",label:"elf",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"train",label:"train",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"firework",label:"fireworks",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"robin",label:"robin",type:"select",default:"",options:[["","Shown"],["off","Off"]]},{name:"peeing-elf",label:"Peeing elf (opt-in)",type:"checkbox",default:!1,hint:"A clothed rear-view gag. Off by default; Snow only keeps it off."},{name:"thieving-elf",label:"Thieving elf (opt-in)",type:"checkbox",default:!1,hint:"Runs to a tree, takes a present and escapes. Needs a visible tree."}],hosts:[],previewable:!1,selfClosing:!0},{name:"ticker-tape",label:"Ticker tape",category:"Celebrations",description:"Coloured rectangular strips fall from the top, sway, rotate, and fade. Inserted self-closing (full-page); hand-write the wrapping form for container-scoped.",attrs:[{name:"palette",label:"Palette",type:"select",default:"theme",options:[["theme","Theme"],["rainbow","Rainbow"],["gold","Gold"],["silver","Silver"],["festive","Festive"],["pastel","Pastel"],["mono","Mono"]]},{name:"density",label:"Density",type:"number",default:50},{name:"speed",label:"Speed multiplier",type:"number",default:1,step:.1},{name:"sway",label:"Sway (px)",type:"number",default:60},{name:"burst",label:"Burst mode (one-shot)",type:"checkbox",default:!1},{name:"burst-count",label:"Burst count",type:"number",default:150}],hosts:["hero"],previewable:!0,selfClosing:!0},{name:"butterflies",label:"Butterflies",category:"Celebrations",description:"Procedurally drawn butterflies wander and rise with flapping wings. Self-closing for full-page; wrapping form for container-scoped.",attrs:[{name:"palette",label:"Palette",type:"select",default:"meadow",options:[["meadow","Meadow"],["theme","Theme"],["rainbow","Rainbow"],["pastel","Pastel"],["festive","Festive"],["gold","Gold"],["silver","Silver"],["mono","Mono"],["sunset","Sunset"],["ocean","Ocean"],["forest","Forest"],["bridal","Bridal"],["firefly","Firefly"]]},{name:"density",label:"Density",type:"number",default:18},{name:"speed",label:"Speed multiplier",type:"number",default:1,step:.1},{name:"wander",label:"Wander",type:"number",default:1,step:.1,hint:"Steering erraticness"},{name:"rise-speed",label:"Rise speed",type:"number",default:.4,step:.1,hint:"Upward drift bias"},{name:"flap-speed",label:"Flap speed",type:"number",default:1,step:.1},{name:"min-size",label:"Min wingspan (px)",type:"number",default:14},{name:"max-size",label:"Max wingspan (px)",type:"number",default:26},{name:"two-tone",label:"Two-tone wings",type:"checkbox",default:!0},{name:"burst",label:"Burst mode (one-shot)",type:"checkbox",default:!1},{name:"burst-count",label:"Burst count",type:"number",default:40}],hosts:["hero"],previewable:!0},{name:"strobe",label:"Strobe lighting",category:"Celebrations",description:"Light beams sweep from the corners and brighten where they cross - club, concert, or searchlight moods. Self-closing for full-page; wrapping form for container-scoped.",attrs:[{name:"preset",label:"Preset",type:"select",default:"club",options:[["club","Club"],["concert","Concert"],["police","Police"],["searchlight","Searchlight"],["scanner","Scanner"],["mood","Mood"]],hint:"Other fields override the preset when changed"},{name:"motion",label:"Motion",type:"select",default:"sweep",options:[["sweep","Sweep (fan back and forth)"],["rotate","Rotate (full circle)"]]},{name:"speed",label:"Speed multiplier",type:"number",default:1,step:.1},{name:"sweep-arc",label:"Sweep arc (deg)",type:"number",default:100},{name:"beam-width",label:"Beam width (deg)",type:"number",default:16},{name:"intensity",label:"Intensity",type:"number",default:.7,step:.05,min:0,max:1},{name:"flicker",label:"Flicker",type:"checkbox",default:!0},{name:"hz",label:"Flicker rate (Hz)",type:"number",default:10,hint:"Rates above 5 flashes/sec can trigger photosensitive seizures"},{name:"colours",label:"Colours",type:"text",default:"",placeholder:"#ffffff, #38bdf8",hint:"Comma-separated hex colours - overrides the preset palette"},{name:"duration",label:"Auto-stop (ms)",type:"number",default:"",hint:"Stop after this many ms - blank runs forever"}],hosts:["hero"],previewable:!0}];export function getEffect(e){return EFFECTS.find(t=>t.name===e)||null}export function effectsByCategory(){return["Entrance","Animation","Text","Visual","Celebrations"].map(t=>({category:t,effects:EFFECTS.filter(l=>l.category===t)}))}export function effectsInjectableInto(e){return EFFECTS.filter(t=>t.hosts.includes(e))}const n=EFFECTS.find(e=>e.name==="celebrate");for(const e of n.attrs.slice(2))e.themes=["christmas"];n.attrs.push({name:"falling-only",label:"Falling particles only",type:"checkbox",default:!1});const r=new Map(n.attrs.map(e=>[e.name,e]));for(const[e,t]of Object.entries(i))if(e!=="christmas")for(const[l,o]of Object.entries(t.traits)){const s=f(l);let a=r.get(s);a||(a={name:s,label:o.label,type:"select",default:"",options:[["","Shown"],["off","Off"]],themes:[]},r.set(s,a)),a.themes.includes(e)||a.themes.push(e),a.themes.length>1&&(a.label=a.label.replace(/^Christmas: /,""))}for(const e of r.values())n.attrs.includes(e)||n.attrs.push(e);export function getEffectAttrs(e,t={}){if(e.name!=="celebrate")return e.attrs||[];const l=t.theme||"auto";return e.attrs.filter(o=>!o.themes||l==="auto"||o.themes.includes(l))}
@@ -1,3 +1,3 @@
1
- import{EFFECTS as _,effectsByCategory as z,effectsInjectableInto as A,getEffect as u}from"./effect-defs.js";import{insertAtCursor as R,replaceRange as x}from"./markdown-toolbar.js";let g=null;function j(e,t,r=14){e.textContent="";const o=document.createElement("span");return o.setAttribute("data-icon",t),o.setAttribute("data-icon-size",String(r)),o.style.cssText="display:inline-flex;align-items:center;vertical-align:middle;",e.appendChild(o),I.scan(e),e}function L(e,t,r,o=14){return j(e,t,o),e.appendChild(document.createTextNode(" "+r)),e}export const EffectsBuilder={open(e){if(g)return;const t={textarea:e,activeEffect:S.get("effects-builder-last")||"reveal",scope:"standalone",host:null,editing:null,fieldValues:{}};if(t.host=F(e),t.host&&A(t.host.name).map(c=>c.name).includes(t.activeEffect)&&!(t.activeEffect in t.host.attrs)&&(t.scope="inject"),t.editing=q(e),t.editing){t.activeEffect=t.editing.name,t.scope="standalone";const a=u(t.editing.name);for(const c of a.attrs)if(c.name in t.editing.attrs){const s=t.editing.attrs[c.name];c.type==="number"?t.fieldValues[c.name]=Number(s):c.type==="checkbox"?t.fieldValues[c.name]=s==="true":t.fieldValues[c.name]=s}}const r=E.slideover({title:"Effects",size:"lg",position:"right",onClose:()=>{g=null}}),o=O(t,r);r.element.appendChild(o),r.open(),I.scan(o),g={state:t,slideover:r}},close(){g&&(g.slideover.close(),g=null)}};function O(e,t){const r=document.createElement("div");r.style.cssText="display:grid;grid-template-columns:170px 1fr;height:100%;min-height:520px;";const o=document.createElement("div");o.className="effects-rail",o.style.cssText="background:var(--dm-background-alt,#15152a);border-right:1px solid var(--dm-border,#2a2a40);padding:.5rem .35rem;font-size:.8rem;overflow-y:auto;";const a=document.createElement("div");a.className="effects-pane",a.style.cssText="padding:1rem;display:flex;flex-direction:column;gap:.75rem;overflow-y:auto;";function c(){H(a,e,t)}return b(o,e,c),c(),r.appendChild(o),r.appendChild(a),r}function b(e,t,r){e.replaceChildren();for(const o of z()){const a=document.createElement("div");a.style.cssText="color:var(--dm-text-muted,#888);text-transform:uppercase;font-size:.65rem;letter-spacing:.05em;margin:.4rem .35rem .25rem;",a.textContent=o.category,e.appendChild(a);for(const c of o.effects){const s=document.createElement("button");s.type="button",s.dataset.effect=c.name,s.style.cssText="display:block;width:100%;text-align:left;padding:.3rem .5rem;border:none;background:transparent;color:var(--dm-text,#ddd);border-radius:3px;font-size:.8rem;cursor:pointer;margin-bottom:.1rem;",s.textContent=c.label,c.name===t.activeEffect&&(s.style.background="var(--dm-primary,#4338ca)",s.style.color="var(--dm-primary-text,#fff)"),s.addEventListener("click",()=>{t.activeEffect=c.name,S.set("effects-builder-last",c.name),t.fieldValues={},b(e,t,r),r()}),e.appendChild(s)}}}function V(e,t){if(e.type==="select"){const o=document.createElement("select");o.style.cssText="width:100%;padding:.35rem .5rem;background:var(--dm-input-bg,#1a1a1a);border:1px solid var(--dm-border,#333);border-radius:3px;color:var(--dm-text,#eee);font-size:.85rem;";for(const[a,c]of e.options){const s=document.createElement("option");s.value=a,s.textContent=c,o.appendChild(s)}return o.value=t??e.default??"",o}if(e.type==="checkbox"){const o=document.createElement("input");return o.type="checkbox",o.checked=t??e.default??!1,o}const r=document.createElement("input");return r.type=e.type==="number"?"number":e.type==="colour"?"color":"text",r.value=t??e.default??"",e.placeholder&&(r.placeholder=e.placeholder),e.type==="number"&&(e.min!=null&&(r.min=e.min),e.max!=null&&(r.max=e.max),e.step!=null&&(r.step=e.step)),r.style.cssText="width:100%;padding:.35rem .5rem;background:var(--dm-input-bg,#1a1a1a);border:1px solid var(--dm-border,#333);border-radius:3px;color:var(--dm-text,#eee);font-size:.85rem;",r}function v(e,t){return t.type==="checkbox"?e.checked:t.type==="number"?e.value===""?"":Number(e.value):e.value}function N(e,t,r){const o=u(t.activeEffect);if(e.replaceChildren(),o.attrs.length===0){const c=document.createElement("div");c.style.cssText="color:var(--dm-text-muted,#888);font-size:.8rem;font-style:italic;",c.textContent="This effect takes no parameters.",e.appendChild(c);return}const a=document.createElement("div");a.style.cssText="display:grid;grid-template-columns:1fr 1fr;gap:.6rem .7rem;";for(const c of o.attrs){const s=document.createElement("div"),d=document.createElement("label");d.style.cssText="display:block;color:var(--dm-text-muted,#888);font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.2rem;",d.textContent=c.label,s.appendChild(d);const i=V(c,t.fieldValues[c.name]);if(i.addEventListener("input",()=>{t.fieldValues[c.name]=v(i,c),r()}),i.addEventListener("change",()=>{t.fieldValues[c.name]=v(i,c),r()}),s.appendChild(i),c.hint){const n=document.createElement("div");n.style.cssText="color:var(--dm-text-muted,#666);font-size:.7rem;margin-top:.15rem;",n.textContent=c.hint,s.appendChild(n)}a.appendChild(s)}e.appendChild(a)}function y(e){const t={},r=/(\w[\w-]*)(?:\s*=\s*"([^"]*)"|\s*=\s*'([^']*)'|\s*=\s*(\S+))?/g;for(const o of e.matchAll(r)){const a=o[1],c=o[2]??o[3]??o[4]??"";t[a]=c}return t}const B=_.filter(e=>!e.selfClosing).map(e=>e.name);function q(e){const t=e.value,r=e.selectionStart;for(const o of B){const a=new RegExp(`\\[${o}([^\\]]*)\\]`,"g"),c=new RegExp(`\\[/${o}\\]`,"g"),s=[];for(const n of t.matchAll(a)){if(n.index>=r)break;s.push({type:"open",start:n.index,end:n.index+n[0].length,attrStr:n[1]})}for(const n of t.matchAll(c)){if(n.index>=r)break;s.push({type:"close",start:n.index,end:n.index+n[0].length})}s.sort((n,l)=>n.start-l.start);let d=0,i=null;for(const n of s)n.type==="open"?(d++,i=n):(d--,d<0&&(d=0,i=null));if(d>0&&i){const n=new RegExp(`\\[/${o}\\]`,"g"),l=t.slice(r),m=l.match(n);if(m){const f=r+l.indexOf(m[0]);return{name:o,blockStart:i.start,blockEnd:f+m[0].length,openTagEnd:i.end,attrs:y(i.attrStr)}}}}return null}const P=["card","box","hero","banner","col","button"];function F(e){const t=e.value,r=e.selectionStart;for(const o of P){const a=new RegExp(`\\[${o}([^\\]]*)\\]`,"g"),c=new RegExp(`\\[/${o}\\]`,"g"),s=[];for(const n of t.matchAll(a)){if(n.index>=r)break;s.push({type:"open",start:n.index,end:n.index+n[0].length,attrStr:n[1]})}for(const n of t.matchAll(c)){if(n.index>=r)break;s.push({type:"close",start:n.index,end:n.index+n[0].length})}s.sort((n,l)=>n.start-l.start);let d=0,i=null;for(const n of s)n.type==="open"?(d++,i=n):(d--,d<0&&(d=0,i=null));if(d>0&&i){const n=new RegExp(`\\[/${o}\\]`,"g"),l=t.slice(r),m=l.match(n);if(m){const f=r+l.indexOf(m[0]);return{name:o,openTagStart:i.start,openTagEnd:i.end,closeTagStart:f,closeTagEnd:f+m[0].length,attrs:y(i.attrStr)}}}}return null}function C(e,t){const r=[];for(const o of t.attrs){const a=e.fieldValues[o.name];a===void 0||a===""||a===null||a!==o.default&&(o.type==="checkbox"?r.push(`${o.name}="${a?"true":"false"}"`):r.push(`${o.name}="${String(a).replace(/"/g,"&quot;")}"`))}return r.join(" ")}function k(e){const t=u(e.activeEffect),r=C(e,t),o=r?" ":"";if(e.scope==="inject"&&e.host){const a=e.host.attrs,c=t.name,s=[];for(const d of Object.keys(a)){if(d===c||d.startsWith(c+"-"))continue;const i=a[d];s.push(i===""?d:`${d}="${String(i).replace(/"/g,"&quot;")}"`)}s.push(c);for(const d of t.attrs){const i=e.fieldValues[d.name];if(i===void 0||i===""||i===null||i===d.default)continue;const n=d.type==="checkbox"?i?"true":"false":String(i).replace(/"/g,"&quot;");s.push(`${c}-${d.name}="${n}"`)}return`[${e.host.name} ${s.join(" ")}]\u2026[/${e.host.name}]`}return t.selfClosing?`[${t.name}${o}${r} /]`:`[${t.name}${o}${r}]
1
+ import{EFFECTS as A,effectsByCategory as V,effectsInjectableInto as R,getEffect as u,getEffectAttrs as g}from"./effect-defs.js";import{insertAtCursor as j,replaceRange as v}from"./markdown-toolbar.js";let h=null;function L(e,t,n=14){e.textContent="";const r=document.createElement("span");return r.setAttribute("data-icon",t),r.setAttribute("data-icon-size",String(n)),r.style.cssText="display:inline-flex;align-items:center;vertical-align:middle;",e.appendChild(r),I.scan(e),e}function O(e,t,n,r=14){return L(e,t,r),e.appendChild(document.createTextNode(" "+n)),e}export const EffectsBuilder={open(e){if(h)return;const t={textarea:e,activeEffect:S.get("effects-builder-last")||"reveal",scope:"standalone",host:null,editing:null,fieldValues:{}};if(t.host=D(e),t.host&&R(t.host.name).map(c=>c.name).includes(t.activeEffect)&&!(t.activeEffect in t.host.attrs)&&(t.scope="inject"),t.editing=P(e),t.editing){t.activeEffect=t.editing.name,t.scope="standalone";const i=u(t.editing.name);for(const c of g(i,t.fieldValues))if(c.name in t.editing.attrs){const s=t.editing.attrs[c.name];c.type==="number"?t.fieldValues[c.name]=Number(s):c.type==="checkbox"?t.fieldValues[c.name]=s==="true":t.fieldValues[c.name]=s}}const n=E.slideover({title:"Effects",size:"lg",position:"right",onClose:()=>{h=null}}),r=N(t,n);n.element.appendChild(r),n.open(),I.scan(r),h={state:t,slideover:n}},close(){h&&(h.slideover.close(),h=null)}};function N(e,t){const n=document.createElement("div");n.style.cssText="display:grid;grid-template-columns:170px 1fr;height:100%;min-height:520px;";const r=document.createElement("div");r.className="effects-rail",r.style.cssText="background:var(--dm-background-alt,#15152a);border-right:1px solid var(--dm-border,#2a2a40);padding:.5rem .35rem;font-size:.8rem;overflow-y:auto;";const i=document.createElement("div");i.className="effects-pane",i.style.cssText="padding:1rem;display:flex;flex-direction:column;gap:.75rem;overflow-y:auto;";function c(){K(i,e,t)}return y(r,e,c),c(),n.appendChild(r),n.appendChild(i),n}function y(e,t,n){e.replaceChildren();for(const r of V()){const i=document.createElement("div");i.style.cssText="color:var(--dm-text-muted,#888);text-transform:uppercase;font-size:.65rem;letter-spacing:.05em;margin:.4rem .35rem .25rem;",i.textContent=r.category,e.appendChild(i);for(const c of r.effects){const s=document.createElement("button");s.type="button",s.dataset.effect=c.name,s.style.cssText="display:block;width:100%;text-align:left;padding:.3rem .5rem;border:none;background:transparent;color:var(--dm-text,#ddd);border-radius:3px;font-size:.8rem;cursor:pointer;margin-bottom:.1rem;",s.textContent=c.label,c.name===t.activeEffect&&(s.style.background="var(--dm-primary,#4338ca)",s.style.color="var(--dm-primary-text,#fff)"),s.addEventListener("click",()=>{t.activeEffect=c.name,S.set("effects-builder-last",c.name),t.fieldValues={},y(e,t,n),n()}),e.appendChild(s)}}}function B(e,t){if(e.type==="select"){const r=document.createElement("select");r.style.cssText="width:100%;padding:.35rem .5rem;background:var(--dm-input-bg,#1a1a1a);border:1px solid var(--dm-border,#333);border-radius:3px;color:var(--dm-text,#eee);font-size:.85rem;";for(const[i,c]of e.options){const s=document.createElement("option");s.value=i,s.textContent=c,r.appendChild(s)}return r.value=t??e.default??"",r}if(e.type==="checkbox"){const r=document.createElement("input");return r.type="checkbox",r.checked=t??e.default??!1,r}const n=document.createElement("input");return n.type=e.type==="number"?"number":e.type==="colour"?"color":"text",n.value=t??e.default??"",e.placeholder&&(n.placeholder=e.placeholder),e.type==="number"&&(e.min!=null&&(n.min=e.min),e.max!=null&&(n.max=e.max),e.step!=null&&(n.step=e.step)),n.style.cssText="width:100%;padding:.35rem .5rem;background:var(--dm-input-bg,#1a1a1a);border:1px solid var(--dm-border,#333);border-radius:3px;color:var(--dm-text,#eee);font-size:.85rem;",n}function C(e,t){return t.type==="checkbox"?e.checked:t.type==="number"?e.value===""?"":Number(e.value):e.value}function x(e,t,n){const r=u(t.activeEffect);if(e.replaceChildren(),r.attrs.length===0){const c=document.createElement("div");c.style.cssText="color:var(--dm-text-muted,#888);font-size:.8rem;font-style:italic;",c.textContent="This effect takes no parameters.",e.appendChild(c);return}const i=document.createElement("div");i.style.cssText="display:grid;grid-template-columns:1fr 1fr;gap:.6rem .7rem;";for(const c of g(r,t.fieldValues)){const s=document.createElement("div"),l=document.createElement("label");l.style.cssText="display:block;color:var(--dm-text-muted,#888);font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.2rem;",l.textContent=c.label,s.appendChild(l);const d=B(c,t.fieldValues[c.name]);if(d.addEventListener("input",()=>{t.fieldValues[c.name]=C(d,c),n(),c.name==="theme"&&x(e,t,n)}),d.addEventListener("change",()=>{t.fieldValues[c.name]=C(d,c),n(),c.name==="theme"&&x(e,t,n)}),s.appendChild(d),c.hint){const o=document.createElement("div");o.style.cssText="color:var(--dm-text-muted,#666);font-size:.7rem;margin-top:.15rem;",o.textContent=c.hint,s.appendChild(o)}i.appendChild(s)}e.appendChild(i)}function k(e){const t={},n=/(\w[\w-]*)(?:\s*=\s*"([^"]*)"|\s*=\s*'([^']*)'|\s*=\s*(\S+))?/g;for(const r of e.matchAll(n)){const i=r[1],c=r[2]??r[3]??r[4]??"";t[i]=c}return t}const q=A.filter(e=>!e.selfClosing).map(e=>e.name);function P(e){const t=e.value,n=e.selectionStart;for(const r of q){const i=new RegExp(`\\[${r}([^\\]]*)\\]`,"g"),c=new RegExp(`\\[/${r}\\]`,"g"),s=[];for(const o of t.matchAll(i)){if(o.index>=n)break;s.push({type:"open",start:o.index,end:o.index+o[0].length,attrStr:o[1]})}for(const o of t.matchAll(c)){if(o.index>=n)break;s.push({type:"close",start:o.index,end:o.index+o[0].length})}s.sort((o,a)=>o.start-a.start);let l=0,d=null;for(const o of s)o.type==="open"?(l++,d=o):(l--,l<0&&(l=0,d=null));if(l>0&&d){const o=new RegExp(`\\[/${r}\\]`,"g"),a=t.slice(n),m=a.match(o);if(m){const f=n+a.indexOf(m[0]);return{name:r,blockStart:d.start,blockEnd:f+m[0].length,openTagEnd:d.end,attrs:k(d.attrStr)}}}}return null}const F=["card","box","hero","banner","col","button"];function D(e){const t=e.value,n=e.selectionStart;for(const r of F){const i=new RegExp(`\\[${r}([^\\]]*)\\]`,"g"),c=new RegExp(`\\[/${r}\\]`,"g"),s=[];for(const o of t.matchAll(i)){if(o.index>=n)break;s.push({type:"open",start:o.index,end:o.index+o[0].length,attrStr:o[1]})}for(const o of t.matchAll(c)){if(o.index>=n)break;s.push({type:"close",start:o.index,end:o.index+o[0].length})}s.sort((o,a)=>o.start-a.start);let l=0,d=null;for(const o of s)o.type==="open"?(l++,d=o):(l--,l<0&&(l=0,d=null));if(l>0&&d){const o=new RegExp(`\\[/${r}\\]`,"g"),a=t.slice(n),m=a.match(o);if(m){const f=n+a.indexOf(m[0]);return{name:r,openTagStart:d.start,openTagEnd:d.end,closeTagStart:f,closeTagEnd:f+m[0].length,attrs:k(d.attrStr)}}}}return null}function T(e,t){const n=[];for(const r of g(t,e.fieldValues)){const i=e.fieldValues[r.name];i===void 0||i===""||i===null||i!==r.default&&(r.type==="checkbox"?n.push(`${r.name}="${i?"true":"false"}"`):n.push(`${r.name}="${String(i).replace(/"/g,"&quot;")}"`))}return n.join(" ")}function $(e){const t=u(e.activeEffect),n=T(e,t),r=n?" ":"";if(e.scope==="inject"&&e.host){const i=e.host.attrs,c=t.name,s=[];for(const l of Object.keys(i)){if(l===c||l.startsWith(c+"-"))continue;const d=i[l];s.push(d===""?l:`${l}="${String(d).replace(/"/g,"&quot;")}"`)}s.push(c);for(const l of g(t,e.fieldValues)){const d=e.fieldValues[l.name];if(d===void 0||d===""||d===null||d===l.default)continue;const o=l.type==="checkbox"?d?"true":"false":String(d).replace(/"/g,"&quot;");s.push(`${c}-${l.name}="${o}"`)}return`[${e.host.name} ${s.join(" ")}]\u2026[/${e.host.name}]`}return t.selfClosing?`[${t.name}${r}${n} /]`:`[${t.name}${r}${n}]
2
2
  Content
3
- [/${t.name}]`}function T(e,t){e.replaceChildren();const r=document.createElement("div");r.style.cssText="color:var(--dm-text-muted,#888);font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem;",r.textContent="Shortcode (live)",e.appendChild(r);const o=document.createElement("pre");o.style.cssText="background:var(--dm-code-bg,#000);border:1px solid var(--dm-border,#333);padding:.5rem .65rem;border-radius:3px;font-family:var(--dm-font-mono,monospace);font-size:.75rem;color:var(--dm-code-text,#7c6af7);line-height:1.5;white-space:pre-wrap;word-break:break-all;margin:0;",o.textContent=k(t),e.appendChild(o)}function D(e,t,r){if(e.replaceChildren(),t.editing)return;const o=u(t.activeEffect),a=t.host&&o.hosts.includes(t.host.name),c=document.createElement("div");c.style.cssText="background:var(--dm-background-alt,#15152a);border:1px solid var(--dm-border,#2a2a40);border-radius:4px;padding:.5rem .6rem;";const s=document.createElement("div");if(s.style.cssText="color:var(--dm-text-muted,#888);text-transform:uppercase;font-size:.65rem;letter-spacing:.05em;margin-bottom:.3rem;",s.textContent="Apply as",c.appendChild(s),!a){const n=document.createElement("div");n.style.cssText="color:var(--dm-text-muted,#888);font-size:.75rem;font-style:italic;",t.host?n.textContent=`Inserted as a standalone block (${o.label} cannot be injected into [${t.host.name}]).`:n.textContent="Inserted as a standalone block.",c.appendChild(n),e.appendChild(c),t.scope="standalone";return}const d=document.createElement("div");d.style.cssText="display:flex;gap:.3rem;";function i(n,l){const m=document.createElement("button");return m.type="button",m.style.cssText="flex:1;padding:.35rem;border:1px solid var(--dm-border,#333);border-radius:3px;font-size:.75rem;font-weight:600;cursor:pointer;",t.scope===n?(m.style.background="var(--dm-primary,#4338ca)",m.style.color="var(--dm-primary-text,#fff)",m.style.borderColor="var(--dm-primary,#4338ca)"):(m.style.background="transparent",m.style.color="var(--dm-text-muted,#bbb)"),m.textContent=l,m.addEventListener("click",()=>{t.scope=n,r()}),m}d.appendChild(i("standalone","Standalone block")),d.appendChild(i("inject",`Inject into [${t.host.name}]`)),c.appendChild(d),e.appendChild(c)}let h=0;function $(e){return e.replace(/-([a-z])/g,(t,r)=>r.toUpperCase())}function w(e,t){e.replaceChildren();const r=u(t.activeEffect),o=document.createElement("div");o.style.cssText="display:flex;justify-content:space-between;align-items:center;margin-bottom:.25rem;";const a=document.createElement("div");if(a.style.cssText="color:var(--dm-text-muted,#888);text-transform:uppercase;font-size:.65rem;letter-spacing:.05em;",a.textContent="Live preview",o.appendChild(a),!r.previewable){const i=document.createElement("div");i.style.cssText="background:var(--dm-background-alt,#15152a);border:1px solid var(--dm-border,#2a2a40);border-radius:4px;padding:.6rem;color:var(--dm-text-muted,#888);font-size:.8rem;font-style:italic;text-align:center;",i.textContent="Runs full-page on the published site - no inline preview.",e.appendChild(i);return}const c=document.createElement("button");c.type="button",c.style.cssText="background:transparent;border:1px solid var(--dm-border,#333);color:var(--dm-text-muted,#888);font-size:.65rem;padding:.15rem .4rem;border-radius:2px;cursor:pointer;",L(c,"refresh-cw","Replay",12),o.appendChild(c),e.appendChild(o);const s=document.createElement("div");s.style.cssText="background:var(--dm-background-alt,#1a1a2e);border:1px solid var(--dm-border,#2a2a40);border-radius:4px;padding:1rem;text-align:center;min-height:80px;position:relative;overflow:hidden;",e.appendChild(s);function d(){h++;const i=h;s.replaceChildren();const n=document.createElement("div");n.style.cssText="background:linear-gradient(135deg,#6366f1,#a855f7);color:#fff;padding:.6rem;border-radius:4px;font-size:.8rem;font-weight:600;",n.textContent="Sample content",s.appendChild(n);const l=window.Domma&&window.Domma.effects;if(!l)return;const m={};for(const f of r.attrs){const p=t.fieldValues[f.name];p===void 0||p===""||p===null||(m[$(f.name)]=p)}try{const f=l[$(r.name)];if(typeof f=="function"){if(i!==h)return;f(n,m)}}catch(f){console.warn("Effects preview failed:",f)}}c.addEventListener("click",d),d()}function H(e,t,r){const o=u(t.activeEffect);e.replaceChildren();const a=document.createElement("h3");a.style.cssText="margin:0;font-size:1rem;color:var(--dm-text,#eee);",a.textContent=o.label,e.appendChild(a);const c=document.createElement("p");c.style.cssText="margin:0;color:var(--dm-text-muted,#999);font-size:.8rem;line-height:1.4;",c.textContent=o.description,e.appendChild(c);const s=document.createElement("div");e.appendChild(s);const d=document.createElement("div");e.appendChild(d);const i=document.createElement("div");e.appendChild(i);const n=document.createElement("div");e.appendChild(n);function l(){D(s,t,l),N(d,t,()=>{T(i,t),w(n,t)}),T(i,t),w(n,t)}l();const m=document.createElement("div");m.style.cssText="display:flex;gap:.5rem;margin-top:auto;padding-top:.75rem;border-top:1px solid var(--dm-border,#2a2a40);";const f=document.createElement("button");f.type="button",f.className="btn btn-ghost",f.textContent="Cancel",f.addEventListener("click",()=>r.close()),m.appendChild(f);const p=document.createElement("button");p.type="button",p.className="btn btn-primary",p.style.flex="2",p.textContent=t.editing?"Update":"Insert effect",p.addEventListener("click",()=>K(t,r)),m.appendChild(p),e.appendChild(m)}function K(e,t){const r=e.textarea,o=k(e);if(e.editing){const a=u(e.editing.name),c=r.value,s=`[/${a.name}]`.length,d=e.editing.openTagEnd,i=e.editing.blockEnd-s,n=c.substring(d,i),l=C(e,a),m=l?" ":"",f=a.selfClosing?`[${a.name}${m}${l} /]`:`[${a.name}${m}${l}]${n}[/${a.name}]`;x(r,e.editing.blockStart,e.editing.blockEnd,f);const p=a.selfClosing?e.editing.blockStart+f.length:e.editing.blockStart+f.indexOf("]")+1;r.setSelectionRange(p,p),t.close();return}if(e.scope==="inject"&&e.host){const a=u(e.activeEffect),c=e.host.attrs,s=a.name,d=[];for(const n of Object.keys(c)){if(n===s||n.startsWith(s+"-"))continue;const l=c[n];d.push(l===""?n:`${n}="${String(l).replace(/"/g,"&quot;")}"`)}d.push(s);for(const n of a.attrs){const l=e.fieldValues[n.name];if(l===void 0||l===""||l===null||l===n.default)continue;const m=n.type==="checkbox"?l?"true":"false":String(l).replace(/"/g,"&quot;");d.push(`${s}-${n.name}="${m}"`)}const i=`[${e.host.name} ${d.join(" ")}]`;x(r,e.host.openTagStart,e.host.openTagEnd,i),t.close();return}R(r,o),t.close()}
3
+ [/${t.name}]`}function w(e,t){e.replaceChildren();const n=document.createElement("div");n.style.cssText="color:var(--dm-text-muted,#888);font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem;",n.textContent="Shortcode (live)",e.appendChild(n);const r=document.createElement("pre");r.style.cssText="background:var(--dm-code-bg,#000);border:1px solid var(--dm-border,#333);padding:.5rem .65rem;border-radius:3px;font-family:var(--dm-font-mono,monospace);font-size:.75rem;color:var(--dm-code-text,#7c6af7);line-height:1.5;white-space:pre-wrap;word-break:break-all;margin:0;",r.textContent=$(t),e.appendChild(r)}function H(e,t,n){if(e.replaceChildren(),t.editing)return;const r=u(t.activeEffect),i=t.host&&r.hosts.includes(t.host.name),c=document.createElement("div");c.style.cssText="background:var(--dm-background-alt,#15152a);border:1px solid var(--dm-border,#2a2a40);border-radius:4px;padding:.5rem .6rem;";const s=document.createElement("div");if(s.style.cssText="color:var(--dm-text-muted,#888);text-transform:uppercase;font-size:.65rem;letter-spacing:.05em;margin-bottom:.3rem;",s.textContent="Apply as",c.appendChild(s),!i){const o=document.createElement("div");o.style.cssText="color:var(--dm-text-muted,#888);font-size:.75rem;font-style:italic;",t.host?o.textContent=`Inserted as a standalone block (${r.label} cannot be injected into [${t.host.name}]).`:o.textContent="Inserted as a standalone block.",c.appendChild(o),e.appendChild(c),t.scope="standalone";return}const l=document.createElement("div");l.style.cssText="display:flex;gap:.3rem;";function d(o,a){const m=document.createElement("button");return m.type="button",m.style.cssText="flex:1;padding:.35rem;border:1px solid var(--dm-border,#333);border-radius:3px;font-size:.75rem;font-weight:600;cursor:pointer;",t.scope===o?(m.style.background="var(--dm-primary,#4338ca)",m.style.color="var(--dm-primary-text,#fff)",m.style.borderColor="var(--dm-primary,#4338ca)"):(m.style.background="transparent",m.style.color="var(--dm-text-muted,#bbb)"),m.textContent=a,m.addEventListener("click",()=>{t.scope=o,n()}),m}l.appendChild(d("standalone","Standalone block")),l.appendChild(d("inject",`Inject into [${t.host.name}]`)),c.appendChild(l),e.appendChild(c)}let b=0;function _(e){return e.replace(/-([a-z])/g,(t,n)=>n.toUpperCase())}function z(e,t){e.replaceChildren();const n=u(t.activeEffect),r=document.createElement("div");r.style.cssText="display:flex;justify-content:space-between;align-items:center;margin-bottom:.25rem;";const i=document.createElement("div");if(i.style.cssText="color:var(--dm-text-muted,#888);text-transform:uppercase;font-size:.65rem;letter-spacing:.05em;",i.textContent="Live preview",r.appendChild(i),!n.previewable){const d=document.createElement("div");d.style.cssText="background:var(--dm-background-alt,#15152a);border:1px solid var(--dm-border,#2a2a40);border-radius:4px;padding:.6rem;color:var(--dm-text-muted,#888);font-size:.8rem;font-style:italic;text-align:center;",d.textContent="Runs full-page on the published site - no inline preview.",e.appendChild(d);return}const c=document.createElement("button");c.type="button",c.style.cssText="background:transparent;border:1px solid var(--dm-border,#333);color:var(--dm-text-muted,#888);font-size:.65rem;padding:.15rem .4rem;border-radius:2px;cursor:pointer;",O(c,"refresh-cw","Replay",12),r.appendChild(c),e.appendChild(r);const s=document.createElement("div");s.style.cssText="background:var(--dm-background-alt,#1a1a2e);border:1px solid var(--dm-border,#2a2a40);border-radius:4px;padding:1rem;text-align:center;min-height:80px;position:relative;overflow:hidden;",e.appendChild(s);function l(){b++;const d=b;s.replaceChildren();const o=document.createElement("div");o.style.cssText="background:linear-gradient(135deg,#6366f1,#a855f7);color:#fff;padding:.6rem;border-radius:4px;font-size:.8rem;font-weight:600;",o.textContent="Sample content",s.appendChild(o);const a=window.Domma&&window.Domma.effects;if(!a)return;const m={};for(const f of g(n,t.fieldValues)){const p=t.fieldValues[f.name];p===void 0||p===""||p===null||(m[_(f.name)]=p)}try{const f=a[_(n.name)];if(typeof f=="function"){if(d!==b)return;f(o,m)}}catch(f){console.warn("Effects preview failed:",f)}}c.addEventListener("click",l),l()}function K(e,t,n){const r=u(t.activeEffect);e.replaceChildren();const i=document.createElement("h3");i.style.cssText="margin:0;font-size:1rem;color:var(--dm-text,#eee);",i.textContent=r.label,e.appendChild(i);const c=document.createElement("p");c.style.cssText="margin:0;color:var(--dm-text-muted,#999);font-size:.8rem;line-height:1.4;",c.textContent=r.description,e.appendChild(c);const s=document.createElement("div");e.appendChild(s);const l=document.createElement("div");e.appendChild(l);const d=document.createElement("div");e.appendChild(d);const o=document.createElement("div");e.appendChild(o);function a(){H(s,t,a),x(l,t,()=>{w(d,t),z(o,t)}),w(d,t),z(o,t)}a();const m=document.createElement("div");m.style.cssText="display:flex;gap:.5rem;margin-top:auto;padding-top:.75rem;border-top:1px solid var(--dm-border,#2a2a40);";const f=document.createElement("button");f.type="button",f.className="btn btn-ghost",f.textContent="Cancel",f.addEventListener("click",()=>n.close()),m.appendChild(f);const p=document.createElement("button");p.type="button",p.className="btn btn-primary",p.style.flex="2",p.textContent=t.editing?"Update":"Insert effect",p.addEventListener("click",()=>M(t,n)),m.appendChild(p),e.appendChild(m)}function M(e,t){const n=e.textarea,r=$(e);if(e.editing){const i=u(e.editing.name),c=n.value,s=`[/${i.name}]`.length,l=e.editing.openTagEnd,d=e.editing.blockEnd-s,o=c.substring(l,d),a=T(e,i),m=a?" ":"",f=i.selfClosing?`[${i.name}${m}${a} /]`:`[${i.name}${m}${a}]${o}[/${i.name}]`;v(n,e.editing.blockStart,e.editing.blockEnd,f);const p=i.selfClosing?e.editing.blockStart+f.length:e.editing.blockStart+f.indexOf("]")+1;n.setSelectionRange(p,p),t.close();return}if(e.scope==="inject"&&e.host){const i=u(e.activeEffect),c=e.host.attrs,s=i.name,l=[];for(const o of Object.keys(c)){if(o===s||o.startsWith(s+"-"))continue;const a=c[o];l.push(a===""?o:`${o}="${String(a).replace(/"/g,"&quot;")}"`)}l.push(s);for(const o of g(i,e.fieldValues)){const a=e.fieldValues[o.name];if(a===void 0||a===""||a===null||a===o.default)continue;const m=o.type==="checkbox"?a?"true":"false":String(a).replace(/"/g,"&quot;");l.push(`${s}-${o.name}="${m}"`)}const d=`[${e.host.name} ${l.join(" ")}]`;v(n,e.host.openTagStart,e.host.openTagEnd,d),t.close();return}j(n,r),t.close()}
@@ -1,3 +1,3 @@
1
- import{SHORTCODES as $}from"./shortcode-registry.js";import{EFFECTS as C}from"./effect-defs.js";import{boolState as V,boolValue as G,findEnclosingShortcode as M,setAttr as N,setAttrs as X,effectAttrKeys as q}from"./shortcode-tags.js";import{insertAtCursor as J}from"./markdown-toolbar.js";import{clearLookupCache as Q,lookupOptions as Z}from"./shortcode-lookups.js";import{insertMenuGroups as ee,insertShortcodeByName as te}from"./shortcode-insert.js";import{openShortcodeForm as R}from"./shortcode-form.js";function ne(e){return Z(e,c?.span?.attrs||{})}const oe=$.map(e=>({name:e.name,selfClosing:!!e.selfClosing})),se=new Map($.map(e=>[e.name,e])),ie=new Map(C.map(e=>[e.name,e])),ce=C.map(e=>({name:e.name,selfClosing:!!e.selfClosing})),z=[...oe,...ce],D=new Set(["card","box","hero","banner","col","button"]);let c=null;const P="position:fixed;z-index:10000;min-width:210px;max-height:70vh;overflow-y:auto;background:var(--dm-surface,#1c1c30);border:1px solid var(--dm-border,#2a2a40);border-radius:6px;box-shadow:var(--dm-shadow-lg);padding:.3rem;font-size:.83rem;color:var(--dm-text,#e5e5e5);",p="display:flex;align-items:center;gap:.5rem;width:100%;text-align:left;padding:.35rem .55rem;border:none;background:transparent;color:inherit;border-radius:4px;cursor:pointer;font-size:.83rem;",S="padding:.3rem .55rem;color:var(--dm-text-muted,#8a8a9a);font-size:.68rem;text-transform:uppercase;letter-spacing:.06em;",H="flex:1;min-width:0;padding:.2rem .4rem;background:var(--dm-input-bg,#12121e);border:1px solid var(--dm-border,#333);border-radius:3px;color:var(--dm-text,#eee);font-size:.8rem;",ae="height:1px;background:var(--dm-border,#2a2a40);margin:.3rem .2rem;";function m(e){return e.addEventListener("mouseenter",()=>{e.style.background="var(--dm-primary,#4338ca)",e.style.color="var(--dm-primary-text,#fff)"}),e.addEventListener("mouseleave",()=>{e.style.background="transparent",e.style.color="inherit"}),e}export function attachContextMenu(e){return typeof Domma>"u"||!Domma.elements?.contextMenu?(e.addEventListener("contextmenu",n=>{n.preventDefault(),f();const t=M(e,z);F(e,t,n.clientX,n.clientY)}),null):Domma.elements.contextMenu(e,{inherit:!1,cursor:!1,render:n=>{f();const t=M(e,z);F(e,t,n.x,n.y)}})}function f(){c&&(v(),c.root.remove(),document.removeEventListener("mousedown",W,!0),document.removeEventListener("keydown",j,!0),window.removeEventListener("scroll",Y,!0),c=null)}const W=e=>{c&&!c.root.contains(e.target)&&!(c.submenu&&c.submenu.contains(e.target))&&f()},Y=e=>{c&&!c.root.contains(e.target)&&!(c.submenu&&c.submenu.contains(e.target))&&f()},j=e=>{e.key==="Escape"&&f()};function F(e,n,t,o){Q();const s=document.createElement("div");s.className="dm-sc-menu",s.style.cssText=P,c={root:s,textarea:e,span:n,submenu:null};const i=n&&(se.get(n.name)||ie.get(n.name)),l=n&&D.has(n.name)?C.filter(r=>r.name in n.attrs):[];i&&re(s,i);for(const r of l)xe(s,r);(i||l.length)&&s.appendChild(le()),s.appendChild(y("Add effect\u2026",()=>be())),s.appendChild(y("Insert\u2026",()=>ye()));const a=e.selectionStart!==e.selectionEnd;(n||a)&&s.appendChild(y(a?"Wrap selection in\u2026":"Wrap in\u2026",()=>Ee())),document.body.appendChild(s),U(s,t,o),document.addEventListener("mousedown",W,!0),document.addEventListener("keydown",j,!0),window.addEventListener("scroll",Y,!0),window.I&&I.scan(s)}function le(){const e=document.createElement("div");return e.style.cssText=ae,e}function k(e){const n=document.createElement("div");return n.style.cssText=S,n.textContent=e,n}function re(e,n){e.appendChild(k(`${n.label} options`));const t=m(document.createElement("button"));t.type="button",t.style.cssText=p,t.innerHTML='<span data-icon="edit" data-icon-size="13" style="display:inline-flex;align-items:center;"></span>',t.appendChild(document.createTextNode(`Edit ${n.label}\u2026`)),t.addEventListener("click",()=>{const{textarea:o,span:s}=c;f(),R(o,n,{span:s})}),e.appendChild(t);for(const o of n.attrs||[])e.appendChild(O(o))}function O(e,n=e.name){const t=c.span.attrs[n];switch(e.type){case"checkbox":return de(e,n,V(t,e.default));case"select":return me(e,n,t);case"lookup":return he(e,n,t);case"colour":return pe(e,n,t);case"icon":return fe(e,n,t);default:return ue(e,n,t)}}function b(e,n){c.span=N(c.textarea,c.span,e,n)}function de(e,n,t){const o=m(document.createElement("label"));o.style.cssText=p;const s=document.createElement("input");return s.type="checkbox",s.checked=t,s.addEventListener("change",()=>b(n,G(e,s.checked))),o.appendChild(s),o.appendChild(document.createTextNode(e.label)),o}function ue(e,n,t){const o=document.createElement("div");o.style.cssText=p+"cursor:default;";const s=document.createElement("span");s.textContent=e.label,s.style.cssText="white-space:nowrap;color:var(--dm-text-muted,#9a9aac);";const i=document.createElement("input");i.type=e.type==="number"?"number":"text",i.value=t===!0||t==null?"":t,e.default!=null&&e.default!==""&&(i.placeholder=String(e.default)),i.style.cssText=H;let l=null;return i.addEventListener("input",()=>{clearTimeout(l),l=setTimeout(()=>b(n,i.value.trim()),180)}),i.addEventListener("mousedown",a=>a.stopPropagation()),o.appendChild(s),o.appendChild(i),o}function pe(e,n,t){const o=document.createElement("div");o.style.cssText=p+"cursor:default;";const s=document.createElement("span");s.textContent=e.label,s.style.flex="1";const i=document.createElement("input");return i.type="color",typeof t=="string"&&/^#/.test(t)?i.value=t:typeof e.default=="string"&&/^#/.test(e.default)&&(i.value=e.default),i.addEventListener("input",()=>b(n,i.value)),o.appendChild(s),o.appendChild(i),o}function me(e,n,t){const o=t==null||t===!0?e.default??"":t;return y(`${e.label}${o!==""?`: ${o}`:""}`,()=>{const s=g();for(const[i,l]of e.options||[]){const a=m(document.createElement("button"));a.type="button",a.style.cssText=p,a.textContent=l,i===o&&(a.style.fontWeight="700"),a.addEventListener("click",()=>{b(n,i),v()}),s.appendChild(a)}return s})}function fe(e,n,t){const o=t==null||t===!0?"":String(t),s=o?`${e.label}: ${o}`:e.label;return y(s,()=>{const i=g();i.style.width="232px";const l=document.createElement("input");l.type="text",l.placeholder="Search icons\u2026",l.style.cssText=H+"margin:6px;width:calc(100% - 12px);",l.addEventListener("mousedown",T=>T.stopPropagation()),i.appendChild(l);const a=m(document.createElement("button"));a.type="button",a.style.cssText=p,a.textContent="- none -",o||(a.style.fontWeight="700"),a.addEventListener("click",()=>{b(n,null),v()}),i.appendChild(a);const r=document.createElement("div");r.style.cssText="display:flex;flex-wrap:wrap;gap:2px;padding:4px 6px;max-height:220px;overflow-y:auto;",i.appendChild(r);const u=96,w=()=>{const T=typeof I<"u"&&typeof I.list=="function"?I.list():[],A=l.value.trim().toLowerCase(),_=A?T.filter(d=>d.toLowerCase().includes(A)):T;r.replaceChildren();for(const d of _.slice(0,u)){const x=m(document.createElement("button"));x.type="button",x.title=d,x.style.cssText="width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid transparent;border-radius:4px;cursor:pointer;color:inherit;",d===o&&(x.style.borderColor="var(--dm-primary,#5b8cff)");const L=document.createElement("span");L.setAttribute("data-icon",d),L.setAttribute("data-icon-size","15"),L.style.cssText="display:inline-flex;align-items:center;",x.appendChild(L),x.addEventListener("click",()=>{b(n,d),v()}),r.appendChild(x)}if(_.length>u){const d=document.createElement("div");d.style.cssText=S,d.textContent=`${_.length-u} more - refine search`,r.appendChild(d)}if(!_.length){const d=document.createElement("div");d.style.cssText=S,d.textContent="No icons match",r.appendChild(d)}window.I&&I.scan(r)};let h=null;return l.addEventListener("input",()=>{clearTimeout(h),h=setTimeout(w,120)}),w(),setTimeout(()=>l.focus(),0),i})}function he(e,n,t){const o=t==null||t===!0?e.default??"":t;return y(`${e.label}${o!==""?`: ${o}`:""}`,()=>{const s=g();s.style.minHeight="48px";const i=typeof E<"u"&&E.loader?E.loader(s,{type:"dots"}):null;return ne(e.source).then(l=>{if(i&&i.destroy(),!c||c.submenu!==s)return;if(s.style.minHeight="",!l.length){const u=document.createElement("div");u.style.cssText=S,u.textContent="Nothing found",s.appendChild(u)}const a=l.some(([u])=>u===o),r=[["","- none -"],...l];o!==""&&!a&&r.push([o,`${o} (missing)`]);for(const[u,w]of r){const h=m(document.createElement("button"));h.type="button",h.style.cssText=p,h.textContent=w,u===o&&(h.style.fontWeight="700"),h.addEventListener("click",()=>{b(n,u===""?null:u),v()}),s.appendChild(h)}}),s})}function xe(e,n){e.appendChild(k(`Effect: ${n.label}`));for(const o of n.attrs||[])e.appendChild(O(o,`${n.name}-${o.name}`));const t=m(document.createElement("button"));t.type="button",t.style.cssText=p,t.innerHTML='<span data-icon="trash" data-icon-size="13" style="display:inline-flex;align-items:center;"></span> Remove effect',t.addEventListener("click",()=>{const o={};for(const s of q(c.span.attrs,n.name))o[s]=null;X(c.textarea,c.span,o),f()}),e.appendChild(t)}function be(){const e=g(),n=c.span,t=n&&D.has(n.name)?n.name:null;for(const o of[...new Set(C.map(s=>s.category))]){const s=C.filter(i=>i.category===o);if(s.length){e.appendChild(k(o));for(const i of s){const l=t&&(i.hosts||[]).includes(t)&&!(i.name in n.attrs),a=m(document.createElement("button"));if(a.type="button",a.style.cssText=p+"justify-content:space-between;",a.appendChild(document.createTextNode(i.label)),l){const r=document.createElement("span");r.textContent=`inject \u2192 [${t}]`,r.style.cssText="opacity:.6;font-size:.7rem;",a.appendChild(r)}a.addEventListener("click",()=>{if(l)N(c.textarea,c.span,i.name,!0);else{const r=i.selfClosing?`[${i.name} /]`:`[${i.name}]
1
+ import{SHORTCODES as $}from"./shortcode-registry.js";import{EFFECTS as w,CELEBRATION_THEMES as J,getEffectAttrs as R}from"./effect-defs.js";import{boolState as Q,boolValue as Z,findEnclosingShortcode as z,setAttr as P,setAttrs as ee,effectAttrKeys as te}from"./shortcode-tags.js";import{insertAtCursor as D}from"./markdown-toolbar.js";import{clearLookupCache as ne,lookupOptions as oe}from"./shortcode-lookups.js";import{insertMenuGroups as se,insertShortcodeByName as ie}from"./shortcode-insert.js";import{openShortcodeForm as A}from"./shortcode-form.js";function ce(e){return oe(e,c?.span?.attrs||{})}const ae=$.map(e=>({name:e.name,selfClosing:!!e.selfClosing})),le=new Map($.map(e=>[e.name,e])),H=new Map(w.map(e=>[e.name,e])),re=w.map(e=>({name:e.name,selfClosing:!!e.selfClosing})),O=[...ae,...re],j=new Set(["card","box","hero","banner","col","button"]);let c=null;const W="position:fixed;z-index:10000;min-width:210px;max-height:70vh;overflow-y:auto;background:var(--dm-surface,#1c1c30);border:1px solid var(--dm-border,#2a2a40);border-radius:6px;box-shadow:var(--dm-shadow-lg);padding:.3rem;font-size:.83rem;color:var(--dm-text,#e5e5e5);",p="display:flex;align-items:center;gap:.5rem;width:100%;text-align:left;padding:.35rem .55rem;border:none;background:transparent;color:inherit;border-radius:4px;cursor:pointer;font-size:.83rem;",k="padding:.3rem .55rem;color:var(--dm-text-muted,#8a8a9a);font-size:.68rem;text-transform:uppercase;letter-spacing:.06em;",Y="flex:1;min-width:0;padding:.2rem .4rem;background:var(--dm-input-bg,#12121e);border:1px solid var(--dm-border,#333);border-radius:3px;color:var(--dm-text,#eee);font-size:.8rem;",de="height:1px;background:var(--dm-border,#2a2a40);margin:.3rem .2rem;";function f(e){return e.addEventListener("mouseenter",()=>{e.style.background="var(--dm-primary,#4338ca)",e.style.color="var(--dm-primary-text,#fff)"}),e.addEventListener("mouseleave",()=>{e.style.background="transparent",e.style.color="inherit"}),e}export function attachContextMenu(e){return typeof Domma>"u"||!Domma.elements?.contextMenu?(e.addEventListener("contextmenu",n=>{n.preventDefault(),m();const t=z(e,O);M(e,t,n.clientX,n.clientY)}),null):Domma.elements.contextMenu(e,{inherit:!1,cursor:!1,render:n=>{m();const t=z(e,O);M(e,t,n.x,n.y)}})}function m(){c&&(v(),c.root.remove(),document.removeEventListener("mousedown",B,!0),document.removeEventListener("keydown",K,!0),window.removeEventListener("scroll",F,!0),c=null)}const B=e=>{c&&!c.root.contains(e.target)&&!(c.submenu&&c.submenu.contains(e.target))&&m()},F=e=>{c&&!c.root.contains(e.target)&&!(c.submenu&&c.submenu.contains(e.target))&&m()},K=e=>{e.key==="Escape"&&m()};function M(e,n,t,o){ne();const s=document.createElement("div");s.className="dm-sc-menu",s.style.cssText=W,c={root:s,textarea:e,span:n,submenu:null};const i=n&&(le.get(n.name)||H.get(n.name)),l=n&&j.has(n.name)?w.filter(r=>r.name in n.attrs):[];i&&ue(s,i);for(const r of l)ye(s,r);(i||l.length)&&s.appendChild(U()),s.appendChild(y("Celebrations\u2026",()=>Ee())),s.appendChild(y("Add effect\u2026",()=>ge())),s.appendChild(y("Insert\u2026",()=>Ce()));const a=e.selectionStart!==e.selectionEnd;(n||a)&&s.appendChild(y(a?"Wrap selection in\u2026":"Wrap in\u2026",()=>ve())),document.body.appendChild(s),q(s,t,o),document.addEventListener("mousedown",B,!0),document.addEventListener("keydown",K,!0),window.addEventListener("scroll",F,!0),window.I&&I.scan(s)}function U(){const e=document.createElement("div");return e.style.cssText=de,e}function C(e){const n=document.createElement("div");return n.style.cssText=k,n.textContent=e,n}function ue(e,n){e.appendChild(C(`${n.label} options`));const t=f(document.createElement("button"));t.type="button",t.style.cssText=p,t.innerHTML='<span data-icon="edit" data-icon-size="13" style="display:inline-flex;align-items:center;"></span>',t.appendChild(document.createTextNode(`Edit ${n.label}\u2026`)),t.addEventListener("click",()=>{const{textarea:o,span:s}=c;m(),A(o,{...n,attrs:R(n,s.attrs)},{span:s})}),e.appendChild(t);for(const o of R(n,c.span.attrs))e.appendChild(V(o))}function V(e,n=e.name){const t=c.span.attrs[n];switch(e.type){case"checkbox":return pe(e,n,Q(t,e.default));case"select":return he(e,n,t);case"lookup":return xe(e,n,t);case"colour":return fe(e,n,t);case"icon":return be(e,n,t);default:return me(e,n,t)}}function x(e,n){if(c.span=P(c.textarea,c.span,e,n),e==="theme"){const{textarea:t,span:o,root:s}=c,i=s.getBoundingClientRect();m(),M(t,o,i.left,i.top)}}function pe(e,n,t){const o=f(document.createElement("label"));o.style.cssText=p;const s=document.createElement("input");return s.type="checkbox",s.checked=t,s.addEventListener("change",()=>x(n,Z(e,s.checked))),o.appendChild(s),o.appendChild(document.createTextNode(e.label)),o}function me(e,n,t){const o=document.createElement("div");o.style.cssText=p+"cursor:default;";const s=document.createElement("span");s.textContent=e.label,s.style.cssText="white-space:nowrap;color:var(--dm-text-muted,#9a9aac);";const i=document.createElement("input");i.type=e.type==="number"?"number":"text",i.value=t===!0||t==null?"":t,e.default!=null&&e.default!==""&&(i.placeholder=String(e.default)),i.style.cssText=Y;let l=null;return i.addEventListener("input",()=>{clearTimeout(l),l=setTimeout(()=>x(n,i.value.trim()),180)}),i.addEventListener("mousedown",a=>a.stopPropagation()),o.appendChild(s),o.appendChild(i),o}function fe(e,n,t){const o=document.createElement("div");o.style.cssText=p+"cursor:default;";const s=document.createElement("span");s.textContent=e.label,s.style.flex="1";const i=document.createElement("input");return i.type="color",typeof t=="string"&&/^#/.test(t)?i.value=t:typeof e.default=="string"&&/^#/.test(e.default)&&(i.value=e.default),i.addEventListener("input",()=>x(n,i.value)),o.appendChild(s),o.appendChild(i),o}function he(e,n,t){const o=t==null||t===!0?e.default??"":t;return y(`${e.label}${o!==""?`: ${o}`:""}`,()=>{const s=g();for(const[i,l]of e.options||[]){const a=f(document.createElement("button"));a.type="button",a.style.cssText=p,a.textContent=l,i===o&&(a.style.fontWeight="700"),a.addEventListener("click",()=>{x(n,i),v()}),s.appendChild(a)}return s})}function be(e,n,t){const o=t==null||t===!0?"":String(t),s=o?`${e.label}: ${o}`:e.label;return y(s,()=>{const i=g();i.style.width="232px";const l=document.createElement("input");l.type="text",l.placeholder="Search icons\u2026",l.style.cssText=Y+"margin:6px;width:calc(100% - 12px);",l.addEventListener("mousedown",L=>L.stopPropagation()),i.appendChild(l);const a=f(document.createElement("button"));a.type="button",a.style.cssText=p,a.textContent="- none -",o||(a.style.fontWeight="700"),a.addEventListener("click",()=>{x(n,null),v()}),i.appendChild(a);const r=document.createElement("div");r.style.cssText="display:flex;flex-wrap:wrap;gap:2px;padding:4px 6px;max-height:220px;overflow-y:auto;",i.appendChild(r);const u=96,T=()=>{const L=typeof I<"u"&&typeof I.list=="function"?I.list():[],N=l.value.trim().toLowerCase(),_=N?L.filter(d=>d.toLowerCase().includes(N)):L;r.replaceChildren();for(const d of _.slice(0,u)){const b=f(document.createElement("button"));b.type="button",b.title=d,b.style.cssText="width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid transparent;border-radius:4px;cursor:pointer;color:inherit;",d===o&&(b.style.borderColor="var(--dm-primary,#5b8cff)");const S=document.createElement("span");S.setAttribute("data-icon",d),S.setAttribute("data-icon-size","15"),S.style.cssText="display:inline-flex;align-items:center;",b.appendChild(S),b.addEventListener("click",()=>{x(n,d),v()}),r.appendChild(b)}if(_.length>u){const d=document.createElement("div");d.style.cssText=k,d.textContent=`${_.length-u} more - refine search`,r.appendChild(d)}if(!_.length){const d=document.createElement("div");d.style.cssText=k,d.textContent="No icons match",r.appendChild(d)}window.I&&I.scan(r)};let h=null;return l.addEventListener("input",()=>{clearTimeout(h),h=setTimeout(T,120)}),T(),setTimeout(()=>l.focus(),0),i})}function xe(e,n,t){const o=t==null||t===!0?e.default??"":t;return y(`${e.label}${o!==""?`: ${o}`:""}`,()=>{const s=g();s.style.minHeight="48px";const i=typeof E<"u"&&E.loader?E.loader(s,{type:"dots"}):null;return ce(e.source).then(l=>{if(i&&i.destroy(),!c||c.submenu!==s)return;if(s.style.minHeight="",!l.length){const u=document.createElement("div");u.style.cssText=k,u.textContent="Nothing found",s.appendChild(u)}const a=l.some(([u])=>u===o),r=[["","- none -"],...l];o!==""&&!a&&r.push([o,`${o} (missing)`]);for(const[u,T]of r){const h=f(document.createElement("button"));h.type="button",h.style.cssText=p,h.textContent=T,u===o&&(h.style.fontWeight="700"),h.addEventListener("click",()=>{x(n,u===""?null:u),v()}),s.appendChild(h)}}),s})}function ye(e,n){e.appendChild(C(`Effect: ${n.label}`));for(const o of n.attrs||[])e.appendChild(V(o,`${n.name}-${o.name}`));const t=f(document.createElement("button"));t.type="button",t.style.cssText=p,t.innerHTML='<span data-icon="trash" data-icon-size="13" style="display:inline-flex;align-items:center;"></span> Remove effect',t.addEventListener("click",()=>{const o={};for(const s of te(c.span.attrs,n.name))o[s]=null;ee(c.textarea,c.span,o),m()}),e.appendChild(t)}function Ee(){const e=g();e.appendChild(C("Seasonal themes"));for(const[n,t]of[["auto","Auto (date-detected)"],...Object.entries(J).map(([o,s])=>[o,s.label])]){const o=f(document.createElement("button"));o.type="button",o.style.cssText=p,o.textContent=t,o.addEventListener("click",()=>{D(c.textarea,`[celebrate theme="${n}" /]`),m()}),e.appendChild(o)}e.appendChild(U()),e.appendChild(C("Celebration effects"));for(const n of["butterflies","ticker-tape","firework","fireworks"]){const t=H.get(n),o=f(document.createElement("button"));o.type="button",o.style.cssText=p,o.textContent=t.label,o.addEventListener("click",()=>{const s=c.textarea;m(),A(s,t)}),e.appendChild(o)}return e}function ge(){const e=g(),n=c.span,t=n&&j.has(n.name)?n.name:null;for(const o of[...new Set(w.map(s=>s.category))]){const s=w.filter(i=>i.category===o);if(s.length){e.appendChild(C(o));for(const i of s){const l=t&&(i.hosts||[]).includes(t)&&!(i.name in n.attrs),a=f(document.createElement("button"));if(a.type="button",a.style.cssText=p+"justify-content:space-between;",a.appendChild(document.createTextNode(i.label)),l){const r=document.createElement("span");r.textContent=`inject \u2192 [${t}]`,r.style.cssText="opacity:.6;font-size:.7rem;",a.appendChild(r)}a.addEventListener("click",()=>{if(l)P(c.textarea,c.span,i.name,!0);else{const r=i.selfClosing?`[${i.name} /]`:`[${i.name}]
2
2
  Content
3
- [/${i.name}]`;J(c.textarea,r)}f()}),e.appendChild(a)}}}return e}function y(e,n){const t=m(document.createElement("button"));t.type="button",t.style.cssText=p+"justify-content:space-between;",t.appendChild(document.createTextNode(e));const o=document.createElement("span");return o.setAttribute("data-icon","chevron-right"),o.setAttribute("data-icon-size","12"),o.style.cssText="opacity:.6;display:inline-flex;align-items:center;flex-shrink:0;",t.appendChild(o),t.addEventListener("mouseenter",()=>B(t,n)),t.addEventListener("click",s=>{s.stopPropagation(),B(t,n)}),t}function g(){const e=document.createElement("div");return e.className="dm-sc-submenu",e.style.cssText=P,e}function B(e,n){v();const t=n();c.submenu=t,document.body.appendChild(t);const o=e.getBoundingClientRect();U(t,o.right+2,o.top,o.left-2),window.I&&I.scan(t)}function v(){c&&c.submenu&&(c.submenu.remove(),c.submenu=null)}function K(e){const n=m(document.createElement("button"));if(n.type="button",n.style.cssText=p,e.icon){const t=document.createElement("span");t.setAttribute("data-icon",e.icon),t.setAttribute("data-icon-size","14"),t.style.cssText="opacity:.75;display:inline-flex;align-items:center;flex-shrink:0;",n.appendChild(t)}return n.appendChild(document.createTextNode(e.label)),n}function ye(){const e=g();for(const{category:n,items:t}of ee()){e.appendChild(k(n));for(const o of t){const s=K(o);s.addEventListener("click",()=>{const i=c.textarea;f(),te(o.name,i)}),e.appendChild(s)}}return e}function Ee(){const e=g();for(const n of $.filter(t=>t.wrappable||t.container)){const t=K(n);t.addEventListener("click",()=>{const o=c.textarea;f(),R(o,n)}),e.appendChild(t)}return e}function U(e,n,t,o){e.style.left="0px",e.style.top="0px";const s=e.getBoundingClientRect();let i=n;n+s.width>window.innerWidth-8&&(i=o!=null?o-s.width:n-s.width);let l=t;t+s.height>window.innerHeight-8&&(l=Math.max(8,window.innerHeight-s.height-8)),e.style.left=`${Math.max(8,i)}px`,e.style.top=`${Math.max(8,l)}px`}
3
+ [/${i.name}]`;D(c.textarea,r)}m()}),e.appendChild(a)}}}return e}function y(e,n){const t=f(document.createElement("button"));t.type="button",t.style.cssText=p+"justify-content:space-between;",t.appendChild(document.createTextNode(e));const o=document.createElement("span");return o.setAttribute("data-icon","chevron-right"),o.setAttribute("data-icon-size","12"),o.style.cssText="opacity:.6;display:inline-flex;align-items:center;flex-shrink:0;",t.appendChild(o),t.addEventListener("mouseenter",()=>G(t,n)),t.addEventListener("click",s=>{s.stopPropagation(),G(t,n)}),t}function g(){const e=document.createElement("div");return e.className="dm-sc-submenu",e.style.cssText=W,e}function G(e,n){v();const t=n();c.submenu=t,document.body.appendChild(t);const o=e.getBoundingClientRect();q(t,o.right+2,o.top,o.left-2),window.I&&I.scan(t)}function v(){c&&c.submenu&&(c.submenu.remove(),c.submenu=null)}function X(e){const n=f(document.createElement("button"));if(n.type="button",n.style.cssText=p,e.icon){const t=document.createElement("span");t.setAttribute("data-icon",e.icon),t.setAttribute("data-icon-size","14"),t.style.cssText="opacity:.75;display:inline-flex;align-items:center;flex-shrink:0;",n.appendChild(t)}return n.appendChild(document.createTextNode(e.label)),n}function Ce(){const e=g();for(const{category:n,items:t}of se()){e.appendChild(C(n));for(const o of t){const s=X(o);s.addEventListener("click",()=>{const i=c.textarea;m(),ie(o.name,i)}),e.appendChild(s)}}return e}function ve(){const e=g();for(const n of $.filter(t=>t.wrappable||t.container)){const t=X(n);t.addEventListener("click",()=>{const o=c.textarea;m(),A(o,n)}),e.appendChild(t)}return e}function q(e,n,t,o){e.style.left="0px",e.style.top="0px";const s=e.getBoundingClientRect();let i=n;n+s.width>window.innerWidth-8&&(i=o!=null?o-s.width:n-s.width);let l=t;t+s.height>window.innerHeight-8&&(l=Math.max(8,window.innerHeight-s.height-8)),e.style.left=`${Math.max(8,i)}px`,e.style.top=`${Math.max(8,l)}px`}
@@ -41,6 +41,13 @@
41
41
  <strong>Calls</strong> list shows how many messages it has - click that to read them (only
42
42
  you, as the one who made the call, or an admin). If you open the call again while it lasts,
43
43
  the earlier messages are still there.</p>
44
+ <p><strong>Add people</strong> while the call is on: as the host, click <strong>Add people</strong>
45
+ in the call's control bar, search your contacts and the team, or type a name and email, and
46
+ click <strong>Invite</strong>. Each person gets a link of their own - by email, and team
47
+ members also as a notification and, when Messages is on, a message from you. They knock and
48
+ you let them in. <strong>Copy link</strong> gives you a link to pass on yourself. The same
49
+ button is in online bookings' calls (Booking Pro) and online meetings (Calendar); in a
50
+ Messages call it rings in another team member.</p>
44
51
  <p>Who may start a call is the <strong>Calls</strong> permission under <strong>System &gt;
45
52
  Roles</strong>; Admin is given it once, the first time a Tool with calls runs. A site without
46
53
  any of those Tools has no instant calls at all.</p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.106.0",
3
+ "version": "0.107.1",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -84,7 +84,7 @@
84
84
  "@fastify/rate-limit": "^10.3.0",
85
85
  "@fastify/static": "9.1.1",
86
86
  "bcryptjs": "^3.0.3",
87
- "domma-js": "0.51.4",
87
+ "domma-js": "0.51.5",
88
88
  "dotenv": "^17.2.3",
89
89
  "fastify": "5.8.5",
90
90
  "gray-matter": "^4.0.3",
@@ -0,0 +1,172 @@
1
+ /**
2
+ * "Add people" to a call that is under way - the pure parts every Tool shares.
3
+ *
4
+ * routes.js mounts the two routes (host only, see there) and calls the Tool's
5
+ * `invite.search(room, q, ctx)` / `invite.send(room, people, ctx)`. What goes out
6
+ * (the Join email, a notification, a Messages direct message) is core's:
7
+ * `server/services/callInvites.js`. This file holds what needs no service:
8
+ * the request's shape, the invite pass, the search's matching, and limits.
9
+ *
10
+ * The invite pass: an extra guest link for people added to a call. It never
11
+ * reuses the host's key or a guest's own token (those carry a name and an
12
+ * identity - someone invited with the customer's link would take the
13
+ * customer's place in the call). A pass is `<nonce>.<sig>`, the HMAC of
14
+ * `room|nonce` under a key the Tool keeps per call (`newInviteKey()`), so the
15
+ * key itself never leaves the server and each pass is its own identity: two
16
+ * invitees never replace each other, and one turned away cannot knock again
17
+ * with the same link. Every invite (and every "Copy link") gets a new pass.
18
+ *
19
+ * @module _lib/call/invite
20
+ */
21
+
22
+ import {createHmac, randomBytes, timingSafeEqual} from 'node:crypto';
23
+
24
+ /** People in one invite request. */
25
+ export const MAX_TARGETS = 10;
26
+ /** People one caller may invite in a minute (a "Copy link" counts as one). */
27
+ export const INVITES_PER_MINUTE = 20;
28
+ /** Rows a search answers with. */
29
+ export const SEARCH_MAX = 12;
30
+ export const EMAIL_RE = /^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/;
31
+ const ID_RE = /^[\w-]{1,64}$/;
32
+ const PASS_RE = /^([\w-]{8,24})\.([\w-]{16,64})$/;
33
+
34
+ /** A new per-call key for invite passes (kept by the Tool, never sent anywhere). */
35
+ export const newInviteKey = () => randomBytes(24).toString('base64url');
36
+
37
+ const sign = (key, room, nonce) => createHmac('sha256', String(key)).update(`${room}|${nonce}`).digest('base64url').slice(0, 32);
38
+
39
+ /** A new invite pass for `room` under the call's `key`. */
40
+ export function invitePass(key, room, nonce = randomBytes(9).toString('base64url')) {
41
+ if (!key) throw new Error('invitePass needs the call\'s invite key');
42
+ return `${nonce}.${sign(key, String(room), nonce)}`;
43
+ }
44
+
45
+ /** The pass's nonce when it was made for this room under this key, else null. */
46
+ export function checkInvitePass(key, room, pass) {
47
+ const m = PASS_RE.exec(String(pass || ''));
48
+ if (!key || !m) return null;
49
+ const want = Buffer.from(sign(key, String(room), m[1]));
50
+ const got = Buffer.from(m[2]);
51
+ return want.length === got.length && timingSafeEqual(want, got) ? m[1] : null;
52
+ }
53
+
54
+ /** "jo@example.com" -> "j•••@example.com": enough to tell two Jos apart, not enough to harvest. */
55
+ export function maskEmail(email) {
56
+ const s = String(email || '').trim();
57
+ const at = s.lastIndexOf('@');
58
+ if (at < 1) return '';
59
+ return `${s[0]}•••${s.slice(at)}`;
60
+ }
61
+
62
+ const bool = (v) => v === true;
63
+ const text = (v, n) => String(v ?? '').trim().replace(/\s+/g, ' ').slice(0, n);
64
+ /** One line of plain text, at most `n` characters (names in subjects, notices and messages). */
65
+ export const cleanText = (v, n = 120) => text(v, n);
66
+
67
+ /**
68
+ * The invite request, checked: `{people: [{kind: 'contact'|'user', id} | {kind: 'email', name, email}], via}` or
69
+ * `{error}`. Duplicates are dropped. An empty `people` is allowed (the caller wants a link to copy).
70
+ *
71
+ * @param {object} body - {people: [{kind, id} | {name, email}], via: {email, notify, message}}
72
+ */
73
+ export function cleanInviteRequest(body = {}) {
74
+ const raw = Array.isArray(body?.people) ? body.people : [];
75
+ if (raw.length > MAX_TARGETS) return {error: `Add at most ${MAX_TARGETS} people at a time.`};
76
+ const people = [];
77
+ const seen = new Set();
78
+ for (const p of raw) {
79
+ if (!p || typeof p !== 'object') return {error: 'Someone in the list is not a person.'};
80
+ if (p.kind === 'contact' || p.kind === 'user') {
81
+ const id = String(p.id ?? '');
82
+ if (!ID_RE.test(id)) return {error: 'Someone in the list is not a person.'};
83
+ const k = `${p.kind}:${id}`;
84
+ if (!seen.has(k)) { seen.add(k); people.push({kind: p.kind, id}); }
85
+ continue;
86
+ }
87
+ const email = text(p.email, 200).toLowerCase();
88
+ if (!EMAIL_RE.test(email)) return {error: `"${text(p.email, 60) || 'An email'}" does not look like an email address.`};
89
+ const name = text(p.name, 120) || email.split('@')[0];
90
+ const k = `email:${email}`;
91
+ if (!seen.has(k)) { seen.add(k); people.push({kind: 'email', name, email}); }
92
+ }
93
+ const via = body?.via && typeof body.via === 'object' ? body.via : {};
94
+ return {people, via: {email: bool(via.email), notify: bool(via.notify), message: bool(via.message)}};
95
+ }
96
+
97
+ /**
98
+ * Match a search against a host's contacts and the team. Contacts' emails are masked (the rows reach a browser
99
+ * holding only a call link) and matched only WHOLE - a part of an address would let someone rebuild a masked one
100
+ * letter by letter; names match on any part. Team members show no email at all.
101
+ *
102
+ * @param {{q?: string, contacts?: {id, name, email}[], users?: {id, name}[], excludeUserIds?: string[],
103
+ * canMessage?: boolean, limit?: number}} opts - `canMessage`: a direct message can reach team members
104
+ */
105
+ export function matchPeople({q = '', contacts = [], users = [], excludeUserIds = [], canMessage = false, limit = SEARCH_MAX} = {}) {
106
+ const want = text(q, 100).toLowerCase();
107
+ const hit = (...vals) => !want || vals.some(v => String(v || '').toLowerCase().includes(want));
108
+ const skip = new Set(excludeUserIds.map(String));
109
+ const team = users.filter(u => u && u.id && !skip.has(String(u.id)) && hit(u.name))
110
+ .map(u => ({kind: 'user', id: String(u.id), name: text(u.name, 120) || 'Team member', canNotify: true, canMessage: canMessage === true}));
111
+ // Contacts need something typed: an empty search offers the team only.
112
+ const known = want ? contacts.filter(c => c && c.id && c.email && (hit(c.name) || String(c.email).trim().toLowerCase() === want))
113
+ .map(c => ({kind: 'contact', id: String(c.id), name: text(c.name, 120) || c.email.split('@')[0], email: maskEmail(c.email),
114
+ canNotify: false, canMessage: false})) : [];
115
+ const byName = (a, b) => a.name.localeCompare(b.name);
116
+ return [...team.sort(byName), ...known.sort(byName)].slice(0, Math.max(1, limit));
117
+ }
118
+
119
+ /** A Tool's search rows, as the browser may have them. */
120
+ export function cleanPeople(list) {
121
+ return (Array.isArray(list) ? list : []).slice(0, SEARCH_MAX * 2).filter(p => p && (p.kind === 'contact' || p.kind === 'user') && ID_RE.test(String(p.id)))
122
+ .map(p => ({kind: p.kind, id: String(p.id), name: text(p.name, 120) || 'Someone', ...(p.email ? {email: text(p.email, 200)} : {}),
123
+ canNotify: p.canNotify === true, canMessage: p.canMessage === true}));
124
+ }
125
+
126
+ /** A Tool's send results, as the browser may have them. */
127
+ export function cleanResults(list) {
128
+ return (Array.isArray(list) ? list : []).slice(0, MAX_TARGETS).map(r => ({name: text(r?.name, 120) || 'Someone',
129
+ emailed: r?.emailed === true, notified: r?.notified === true, messaged: r?.messaged === true, ...(r?.error ? {error: text(r.error, 200)} : {})}));
130
+ }
131
+
132
+ /**
133
+ * Counts per key in a sliding window: `take(key, n)` is true (and counts n) while the key has room left.
134
+ *
135
+ * @param {{max?: number, windowMs?: number, now?: () => number}} [opts]
136
+ */
137
+ export function createLimiter({max = INVITES_PER_MINUTE, windowMs = 60000, now = Date.now} = {}) {
138
+ const seen = new Map(); // key -> [times]
139
+ return {
140
+ take(key, n = 1) {
141
+ const t = now();
142
+ const recent = (seen.get(key) || []).filter(x => x > t - windowMs);
143
+ if (recent.length + n > max) { seen.set(key, recent); return false; }
144
+ for (let i = 0; i < n; i++) recent.push(t);
145
+ seen.set(key, recent);
146
+ if (seen.size > 5000) for (const [k, l] of seen) if (!l.some(x => x > t - windowMs)) seen.delete(k);
147
+ return true;
148
+ },
149
+ reset: () => seen.clear()
150
+ };
151
+ }
152
+
153
+ /** The words of a notification / direct message: "Alex is calling you - Join". */
154
+ export const inviteTitle = (hostName) => `${text(hostName, 80) || 'Someone'} is calling you - Join`;
155
+ export const inviteMessage = (hostName, title, link) =>
156
+ `${text(hostName, 80) || 'Someone'} is inviting you to join a video call${title ? ` (${text(title, 120)})` : ''} now. Join here: ${link}`;
157
+
158
+ /**
159
+ * What "Add people" offers in a call, as the join tells the page (`inviteOptions`):
160
+ * search - search the team (and the host's contacts, when `typed`)
161
+ * typed - contacts and "type a name and email"
162
+ * copyLink - Copy link (a new pass, nothing sent)
163
+ * A Tool's resolve() gives `inviteOptions` (any of the three), or the older `inviteExternal: false` (team only).
164
+ * `external` stays for pages that read it: typed || copyLink.
165
+ */
166
+ export function inviteOptionsOf(r = {}) {
167
+ const given = r.inviteOptions && typeof r.inviteOptions === 'object' ? r.inviteOptions : {};
168
+ const team = r.inviteExternal === false;
169
+ const flag = (v, dflt) => (typeof v === 'boolean' ? v : dflt);
170
+ const o = {search: flag(given.search, true), typed: flag(given.typed, !team), copyLink: flag(given.copyLink, !team)};
171
+ return {...o, external: o.typed || o.copyLink};
172
+ }
@@ -133,7 +133,7 @@ export function createRooms({now = Date.now, pollMs = POLL_MS, silentMs = SILENT
133
133
  tell(admitted(room).filter(p => p !== person), {type: 'joined', peer: pub(person), ...(item && {item})});
134
134
  // `chat` is the history so far (their own "joined" line included), so a late joiner sees what was said.
135
135
  push(person, {type: 'admitted', peers: admitted(room).filter(p => p !== person).map(pub), recording: room.recording,
136
- chat: room.chat.map(c => ({...c}))});
136
+ chat: room.chat.filter(c => person.seeHistory || c.seq > person.fromSeq).map(c => ({...c}))});
137
137
  tell(hosts(room), {type: 'waiting', waiting: waitingList(room)});
138
138
  }
139
139
 
@@ -144,10 +144,14 @@ export function createRooms({now = Date.now, pollMs = POLL_MS, silentMs = SILENT
144
144
  *
145
145
  * @param {string} key - the room, as the Tool names it ("booking:<id>")
146
146
  * @param {{name: string, host?: boolean, title?: string, max?: number, recordingAllowed?: boolean, autoAdmit?: boolean,
147
- * identity?: string, history?: object[]}} who - `history` seeds the chat of a room this join CREATES (earlier
147
+ * identity?: string, history?: object[], canInvite?: boolean}} who - `history` seeds the chat of a room this join CREATES (earlier
148
148
  * items from storage); it is ignored when the room already exists, and never handed to the listeners.
149
+ * `canInvite`: this person may use the Tool's "Add people" (routes.js decides; see member()), with
150
+ * `inviteOptions` (what of it). `seeHistory: false`: when let in they get only the chat from their own
151
+ * "joined" line on - not what was said before (someone added to a conversation's call is not shown it).
149
152
  */
150
- join(key, {name, host = false, title = '', max = MAX_PEOPLE, recordingAllowed = false, autoAdmit = false, identity = '', history}) {
153
+ join(key, {name, host = false, title = '', max = MAX_PEOPLE, recordingAllowed = false, autoAdmit = false, identity = '', history, canInvite = false,
154
+ inviteOptions = null, seeHistory = true}) {
151
155
  let room = rooms.get(key);
152
156
  if (!room) {
153
157
  const seeded = (Array.isArray(history) ? history : []).map(cleanItem).filter(Boolean).slice(-HISTORY_MAX);
@@ -173,7 +177,7 @@ export function createRooms({now = Date.now, pollMs = POLL_MS, silentMs = SILENT
173
177
  if (admitted(room).length >= room.max && (host || autoAdmit)) throw new CallError('full', `This call is full (${room.max} people).`);
174
178
  const id = newId(9);
175
179
  const person = {id, secret: newId(24), identity: String(identity || ''), who: whoOf(identity, id), name: String(name || '').trim().slice(0, 60) || (host ? 'Host' : 'Guest'),
176
- host: Boolean(host), state: 'waiting', seq: 0, queue: [], waiters: [], seen: now()};
180
+ host: Boolean(host), canInvite: canInvite === true, inviteOptions, seeHistory: seeHistory !== false, fromSeq: room.chatSeq, state: 'waiting', seq: 0, queue: [], waiters: [], seen: now()};
177
181
  room.people.set(person.id, person);
178
182
  if (host || autoAdmit) {
179
183
  // admit() also tells every host - this one included - who is already waiting.
@@ -183,7 +187,17 @@ export function createRooms({now = Date.now, pollMs = POLL_MS, silentMs = SILENT
183
187
  tell(hosts(room), {type: 'waiting', waiting: waitingList(room)});
184
188
  }
185
189
  return {id: person.id, secret: person.secret, who: person.who, name: person.name, host: person.host, state: person.state, title: room.title,
186
- max: room.max, recordingAllowed: room.recordingAllowed};
190
+ max: room.max, recordingAllowed: room.recordingAllowed, canInvite: person.canInvite};
191
+ },
192
+
193
+ /**
194
+ * Who a participant is, checked by their secret (throws 'gone' like every other action): for a Tool's
195
+ * own actions on the call, such as "Add people". `identity` is the join's - server side only.
196
+ */
197
+ member(key, pid, secret) {
198
+ const {room, person} = me(key, pid, secret);
199
+ return {id: person.id, name: person.name, host: person.host, identity: person.identity, canInvite: person.canInvite,
200
+ inviteOptions: person.inviteOptions, state: person.state, title: room.title};
187
201
  },
188
202
 
189
203
  /** Events after `since`, waiting up to pollMs for the first one. */