@muten/shadcn 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +69 -1
- package/README.md +7 -0
- package/kit.css +2406 -24
- package/motion.css +15 -0
- package/package.json +3 -2
- package/registry/accordion.muten +10 -0
- package/registry/agenda.js +72 -24
- package/registry/agenda.muten +30 -2
- package/registry/band.muten +1 -1
- package/registry/bio-play.muten +16 -0
- package/registry/booking-play.muten +43 -0
- package/registry/button.muten +3 -2
- package/registry/chat-play.muten +77 -0
- package/registry/day-plan.muten +40 -0
- package/registry/device-frame.muten +25 -7
- package/registry/gradient-blinds.js +88 -89
- package/registry/gradient-blinds.muten +11 -4
- package/registry/heatmap.muten +30 -0
- package/registry/inbox-play.muten +56 -0
- package/registry/input-group.muten +3 -1
- package/registry/kit/collections.js +10 -2
- package/registry/kit/core.js +519 -99
- package/registry/kit/structure.js +3 -3
- package/registry/kit-widget.js +1 -1
- package/registry/legal-doc.muten +27 -0
- package/registry/modal-behavior.js +11 -0
- package/registry/modal.muten +1 -1
- package/registry/order-play.muten +33 -0
- package/registry/permission-matrix.muten +31 -5
- package/registry/phone-bar.muten +18 -0
- package/registry/phone-shell.muten +1 -1
- package/registry/product-play.muten +20 -0
- package/registry/progress.muten +5 -2
- package/registry/rank-item.muten +32 -0
- package/registry/screen-tour.js +59 -0
- package/registry/screen-tour.muten +10 -0
- package/registry/share-bar.muten +22 -0
- package/registry/sidebar.muten +9 -0
- package/registry/site-footer.muten +37 -0
- package/registry/stacked-bars.js +75 -0
- package/registry/stacked-bars.muten +13 -0
- package/registry/stamp-card.muten +5 -2
- package/registry/stat-band.muten +5 -1
- package/registry/stat.muten +19 -7
- package/registry/veil.muten +18 -0
- package/registry/zoom-control.muten +3 -3
- package/registry.json +116 -0
- package/scripts/reuse-audit.mjs +37 -0
package/registry/kit/core.js
CHANGED
|
@@ -15,8 +15,96 @@ const sprite = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); sp
|
|
|
15
15
|
sprite.innerHTML = `<defs><symbol id="b-whatsapp" viewBox="0 0 24 24"><path fill="currentColor" d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"/></symbol><symbol id="b-instagram" viewBox="0 0 24 24"><path fill="currentColor" d="M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077"/></symbol><symbol id="b-facebook" viewBox="0 0 24 24"><path fill="currentColor" d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></symbol><symbol id="b-x" viewBox="0 0 24 24"><path fill="currentColor" d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/></symbol><symbol id="b-telegram" viewBox="0 0 24 24"><path fill="currentColor" d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></symbol><symbol id="b-gmail" viewBox="0 0 24 24"><path fill="currentColor" d="M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z"/></symbol><symbol id="b-tiktok" viewBox="0 0 24 24"><path fill="currentColor" d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/></symbol><symbol id="x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></symbol><symbol id="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></symbol><symbol id="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></symbol><symbol id="chevup" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></symbol><symbol id="updown" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5M7 9l5-5 5 5"/></symbol><symbol id="right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></symbol><symbol id="left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></symbol><symbol id="arrowup" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 7-7 7 7M12 19V5"/></symbol><symbol id="arrowleft" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7M19 12H5"/></symbol><symbol id="more" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></symbol><symbol id="search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></symbol><symbol id="okc" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></symbol><symbol id="info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></symbol><symbol id="folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></symbol><symbol id="cal" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></symbol><symbol id="smile" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01"/></symbol><symbol id="calc" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><path d="M8 6h8M16 14v4M8 10h.01M12 10h.01M16 10h.01M8 14h.01M12 14h.01M8 18h.01M12 18h.01"/></symbol><symbol id="user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/></symbol><symbol id="card" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20"/></symbol><symbol id="gear" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></symbol><symbol id="loader" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></symbol><symbol id="bold" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"/></symbol><symbol id="italic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 4h-9M14 20H5M15 4 9 20"/></symbol><symbol id="under" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4v6a6 6 0 0 0 12 0V4M4 20h16"/></symbol><symbol id="bookmark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/></symbol><symbol id="ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h10v10M7 17 17 7"/></symbol><symbol id="badgecheck" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></symbol><symbol id="plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></symbol><symbol id="grip" viewBox="0 0 24 24" fill="currentColor"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></symbol><symbol id="scissors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M20 4 8.12 15.88M14.47 14.48 20 20M8.12 8.12 12 12"/></symbol><symbol id="store" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9h18l-1.5-5h-15z"/><path d="M5 9v11h14V9M9 20v-6h6v6"/></symbol><symbol id="clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol><symbol id="pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.85 0 0 1 4 4L7.5 20.5 2 22l1.5-5.5z"/></symbol><symbol id="trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></symbol><symbol id="caloff" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18M9.5 14.5l5 5M14.5 14.5l-5 5"/></symbol><symbol id="copyi" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></symbol><symbol id="mappin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0z"/><circle cx="12" cy="10" r="3"/></symbol><symbol id="quote" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></symbol><symbol id="truck" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2M15 18H9M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/></symbol><symbol id="call" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 2.1 4.2 2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1.9.4 1.8.7 2.7a2 2 0 0 1-.5 2.1L8 9.8a16 16 0 0 0 6 6l1.3-1.3a2 2 0 0 1 2.1-.4c.9.3 1.8.6 2.7.7a2 2 0 0 1 1.7 2z"/></symbol><symbol id="pause" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></symbol><symbol id="play" viewBox="0 0 24 24" fill="currentColor"><path d="M7 4.5v15l13-7.5z"/></symbol><symbol id="filter" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5h18M6 12h12M10 19h4"/></symbol><symbol id="upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/></symbol><symbol id="image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></symbol><symbol id="sort" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5M7 9l5-5 5 5"/></symbol><symbol id="arrowdown" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M19 12l-7 7-7-7"/></symbol><symbol id="share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 13.5 6.8 4M15.4 6.5l-6.8 4"/></symbol><symbol id="home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10.5 12 3l9 7.5V20a1 1 0 0 1-1 1h-5v-6h-6v6H4a1 1 0 0 1-1-1z"/></symbol><symbol id="bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4zM3 6h18M16 10a4 4 0 0 1-8 0"/></symbol><symbol id="menu-lines" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h16"/></symbol><symbol id="mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 6L2 7"/></symbol><symbol id="link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/></symbol><symbol id="qrc" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 14h3v3h-3zM20 14v.01M14 20h.01M17 20h4v-3"/></symbol><symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10.3 21a1.94 1.94 0 0 0 3.4 0"/></symbol><symbol id="i-clip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.4 11.1-9.2 9.2a6 6 0 0 1-8.5-8.5l9.2-9.2a4 4 0 0 1 5.7 5.7l-9.2 9.2a2 2 0 0 1-2.8-2.8l8.5-8.5"/></symbol><symbol id="i-send" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 2 11 13M22 2l-7 20-4-9-9-4z"/></symbol><symbol id="i-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></symbol><symbol id="i-eyeoff" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.9 4.2A10 10 0 0 1 12 4c6.5 0 10 7 10 7a17 17 0 0 1-2.2 3.2M6.6 6.6A17 17 0 0 0 2 12s3.5 7 10 7a10 10 0 0 0 5.4-1.6M2 2l20 20M14.1 14.1a3 3 0 0 1-4.2-4.2"/></symbol><symbol id="i-gift" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="8" width="18" height="4" rx="1"/><path d="M12 8v13M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7M7.5 8a2.5 2.5 0 0 1 0-5C10 3 12 8 12 8s2-5 4.5-5a2.5 2.5 0 0 1 0 5"/></symbol><symbol id="i-doc" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M8 13h8M8 17h5"/></symbol><symbol id="i-ticks" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 7 17l-5-5M22 10l-7.5 7.5L13 16"/></symbol><symbol id="i-tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></symbol><symbol id="i-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></symbol><symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></symbol><symbol id="i-archive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="5" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8M10 12h4"/></symbol><symbol id="i-bot" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect x="4" y="8" width="16" height="12" rx="2"/><path d="M2 14h2M20 14h2M15 13v2M9 13v2"/></symbol><symbol id="i-hand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 11V6a2 2 0 0 0-4 0v5M14 10V4a2 2 0 0 0-4 0v2M10 10.5V6a2 2 0 0 0-4 0v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></symbol><symbol id="i-inbox" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></symbol><symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h16"/></symbol><symbol id="i-panel" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M15 3v18"/></symbol><symbol id="i-mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 6L2 7"/></symbol></defs>`; document.body.prepend(sprite);
|
|
16
16
|
// follow the page's skin: the layer sits outside the app, so it copies data-skin from the nearest skinned element
|
|
17
17
|
// (only when it differs: writing the same value is still a mutation, and the observer would feed itself forever)
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// It copies the style too (style-nova…) and the density: the library's rules hang off them, and a modal on a layer
|
|
19
|
+
// without them drew its icons at their natural size and its fields bare.
|
|
20
|
+
const skinSync = () => {
|
|
21
|
+
const page = document.querySelector('[data-skin]:not(.cx-kit-layer)');
|
|
22
|
+
const s = page?.dataset.skin || '', d = page?.dataset.density || '';
|
|
23
|
+
const style = page ? [...page.classList].filter((c) => /^style-/.test(c)).join(' ') : '';
|
|
24
|
+
const want = `cx-kit-layer${style ? ` ${style}` : ''}`;
|
|
25
|
+
if (lib.className !== want) lib.className = want;
|
|
26
|
+
if ((lib.dataset.density || '') !== d) { if (d) lib.dataset.density = d; else delete lib.dataset.density; }
|
|
27
|
+
if ((lib.dataset.skin || '') === s) return; if (s) lib.dataset.skin = s; else delete lib.dataset.skin;
|
|
28
|
+
};
|
|
29
|
+
new MutationObserver((records) => { if (records.some((r) => r.target !== lib)) skinSync(); }).observe(document.body, { subtree: true, attributes: true, attributeFilter: ['data-skin', 'data-density'] }); skinSync();
|
|
30
|
+
|
|
31
|
+
// ── sheets: a button with data-sheet="open" (aria-controls = the sheet's id) opens a full-screen sheet such as the
|
|
32
|
+
// NavPill's phone menu (.cx-nav-sheet); data-sheet="close", a link inside it or Escape closes it. While one is open the
|
|
33
|
+
// page behind does not scroll. Delegated once, so a sheet rendered later works the same. ──
|
|
34
|
+
const sheetSet = (sheet, open) => {
|
|
35
|
+
if (!sheet) return;
|
|
36
|
+
if (open) sheet.setAttribute('data-open', ''); else sheet.removeAttribute('data-open');
|
|
37
|
+
document.documentElement.style.overflow = open ? 'hidden' : '';
|
|
38
|
+
document.querySelectorAll(`[data-sheet="open"][aria-controls="${sheet.id}"]`).forEach((b) => b.setAttribute('aria-expanded', open ? 'true' : 'false'));
|
|
39
|
+
};
|
|
40
|
+
document.addEventListener('click', (event) => {
|
|
41
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
42
|
+
if (!target) return;
|
|
43
|
+
const opener = target.closest('[data-sheet="open"]');
|
|
44
|
+
if (opener) { event.preventDefault(); sheetSet(document.getElementById(opener.getAttribute('aria-controls')), true); return; }
|
|
45
|
+
const sheet = target.closest('.cx-nav-sheet[data-open]');
|
|
46
|
+
if (!sheet) return;
|
|
47
|
+
if (target.closest('[data-sheet="close"]')) { event.preventDefault(); sheetSet(sheet, false); return; }
|
|
48
|
+
if (target.closest('a[href]')) sheetSet(sheet, false);
|
|
49
|
+
});
|
|
50
|
+
document.addEventListener('keydown', (event) => { if (event.key === 'Escape') document.querySelectorAll('.cx-nav-sheet[data-open]').forEach((s) => sheetSet(s, false)); });
|
|
51
|
+
|
|
52
|
+
// ── contents that follow the reading: in a navbar with data-spy="on", the link whose #section is on screen gets
|
|
53
|
+
// aria-current (the first one at the top of the page). Delegated: a page rendered later is found by the observer below. ──
|
|
54
|
+
const spyWatch = (nav) => {
|
|
55
|
+
if (nav.dataset.spied) return; nav.dataset.spied = '';
|
|
56
|
+
const links = [...nav.querySelectorAll('a[href^="#"]')]; const map = new Map();
|
|
57
|
+
links.forEach((a) => { const t = document.getElementById(decodeURIComponent(a.getAttribute('href').slice(1))); if (t) map.set(t, a); });
|
|
58
|
+
if (!map.size) return;
|
|
59
|
+
const mark = (a) => links.forEach((l) => { if (l === a) l.setAttribute('aria-current', 'true'); else l.removeAttribute('aria-current'); });
|
|
60
|
+
mark(links[0]);
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
const io = new IntersectionObserver((entries) => {
|
|
63
|
+
entries.forEach((e) => { if (e.isIntersecting) seen.add(e.target); else seen.delete(e.target); });
|
|
64
|
+
const first = [...map.keys()].find((t) => seen.has(t)); if (first) mark(map.get(first));
|
|
65
|
+
}, { rootMargin: '-20% 0px -65% 0px' });
|
|
66
|
+
map.forEach((a, t) => io.observe(t));
|
|
67
|
+
};
|
|
68
|
+
const spyScan = (root) => root.querySelectorAll?.('[data-spy="on"]').forEach(spyWatch);
|
|
69
|
+
new MutationObserver((records) => records.forEach((r) => r.addedNodes.forEach((n) => { if (n.nodeType === 1) spyScan(n.parentElement || document); }))).observe(document.body, { subtree: true, childList: true });
|
|
70
|
+
spyScan(document);
|
|
71
|
+
|
|
72
|
+
// ── plays (BookingPlay, OrderPlay, DayPlan, ChatPlay) loop only while on screen: data-in starts the loop from its
|
|
73
|
+
// first frame, leaving takes it off; off screen a play rests still (no work, and a checker never reads it mid-fade) ──
|
|
74
|
+
const playSeen = new IntersectionObserver((entries) => entries.forEach((e) => { if (e.isIntersecting) e.target.dataset.in = ''; else delete e.target.dataset.in; }), { threshold: 0.25 });
|
|
75
|
+
const watchPlays = (root) => root.querySelectorAll?.('.cx-play:not([data-watched])').forEach((el) => { el.dataset.watched = ''; playSeen.observe(el); });
|
|
76
|
+
new MutationObserver((records) => records.forEach((r) => r.addedNodes.forEach((n) => { if (n.nodeType === 1) { if (n.matches('.cx-play')) watchPlays(n.parentElement || document); else watchPlays(n); } }))).observe(document.body, { subtree: true, childList: true });
|
|
77
|
+
watchPlays(document);
|
|
78
|
+
|
|
79
|
+
// ── reveal-blur: under a .reveal-blur root, each block of the marketing system comes in from a blur as it enters the
|
|
80
|
+
// screen. What is on screen when it is found shows at once (never half-faded at the fold, never holding the first
|
|
81
|
+
// paint); without this script, or with reduced motion, nothing is ever hidden. Siblings enter a beat apart. ──
|
|
82
|
+
const RV = ['.cx-mk-head', '.cx-mk-sec > .cx-mk-h2', '.cx-mk-sec > .cx-mk-lead', '.cx-mk-row-copy', '.cx-mk-stage', '.cx-mk-tiles > *', '.cx-mk-feats > *', '.cx-mk-steps > *', '.cx-mk-fund > *', '.cx-mk-which > *', '.cx-mk-stores > *', '.cx-mk-vals-row > *', '.cx-mk-band-copy', '.cx-mk-frame', '.cx-mk-bring', '.cx-mk-cta', '.cx-mk-serp', '.cx-mk-vs-table > .cn-table-row', '.cn-accordion > *', '.cx-bento > *', '.cx-features > *', '.cx-wordline'].map((q) => `.reveal-blur ${q}`).join(',');
|
|
83
|
+
const rvStill = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
84
|
+
// The observer decides, never a measurement of ours (no forced layout while the page loads): its first word on a block
|
|
85
|
+
// is whether it is on screen now (shown whole) or not (hidden until it enters).
|
|
86
|
+
const rvSeen = new IntersectionObserver((entries) => entries.forEach((e) => {
|
|
87
|
+
const el = e.target;
|
|
88
|
+
if (e.isIntersecting) { el.dataset.rv = 'in'; rvSeen.unobserve(el); } else if (el.dataset.rv === undefined) el.dataset.rv = '';
|
|
89
|
+
}));
|
|
90
|
+
let rvPending = false;
|
|
91
|
+
const rvScan = () => {
|
|
92
|
+
rvPending = false;
|
|
93
|
+
document.querySelectorAll(RV).forEach((el) => {
|
|
94
|
+
if (el.dataset.rvWatched !== undefined) return;
|
|
95
|
+
el.dataset.rvWatched = '';
|
|
96
|
+
el.style.setProperty('--rv-i', String([...el.parentElement.children].indexOf(el) % 4));
|
|
97
|
+
rvSeen.observe(el);
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
// Scans are batched into an idle moment: a block not yet marked is simply shown, so waiting costs nothing.
|
|
101
|
+
const watchReveal = () => {
|
|
102
|
+
if (rvStill || rvPending) return;
|
|
103
|
+
rvPending = true;
|
|
104
|
+
(typeof requestIdleCallback === 'function' ? requestIdleCallback : (fn) => setTimeout(fn, 120))(rvScan, { timeout: 400 });
|
|
105
|
+
};
|
|
106
|
+
new MutationObserver(watchReveal).observe(document.body, { subtree: true, childList: true });
|
|
107
|
+
watchReveal();
|
|
20
108
|
|
|
21
109
|
// ── dates, money, text ──
|
|
22
110
|
const DAY = ['dom', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb'];
|
|
@@ -170,6 +258,7 @@ const CONTENT = {};
|
|
|
170
258
|
const focusables = () => $$('button:not([disabled]), input, textarea, select, [tabindex]:not([tabindex="-1"])', mdl).filter((x) => x.offsetParent !== null || x === mdl);
|
|
171
259
|
function openModal(kind, mode) {
|
|
172
260
|
if (open) return;
|
|
261
|
+
skinSync(); // the page on screen now, not the one the kit first saw
|
|
173
262
|
CONTENT[kind]();
|
|
174
263
|
if (kind === 'list') mdl.dataset.size = 'tall'; else delete mdl.dataset.size;
|
|
175
264
|
mdl.dataset.mode = mode === 'auto' ? (phone.matches ? 'drawer' : 'dialog') : mode;
|
|
@@ -333,23 +422,26 @@ let wheelsNow = [];
|
|
|
333
422
|
|
|
334
423
|
|
|
335
424
|
// ── Calendar: mode 'single' | 'range'; months 1 | 2; onChange(value) ──
|
|
336
|
-
kit.calendar = (root, { mode = 'single', months = 1, value = null, onChange = () => {}, min = null } = {}) => {
|
|
425
|
+
kit.calendar = (root, { mode = 'single', months = 1, value = null, onChange = () => {}, min = null, mon = MON, weekdays = null, prevLabel = 'Mes anterior', nextLabel = 'Mes siguiente', prevYear = 'Año anterior', nextYear = 'Año siguiente', pickLabel = 'Elegir mes' } = {}) => {
|
|
426
|
+
let picking = false; // the month's title opens a year of months: far dates in two taps
|
|
337
427
|
let view = new Date((mode === 'range' ? value?.[0] : value) || today); view.setDate(1);
|
|
338
428
|
let sel = value, hover = null, focusDay = startOfDay((mode === 'range' ? value?.[0] : value) || today);
|
|
339
|
-
const WD = ['lu', 'ma', 'mi', 'ju', 'vi', 'sá', 'do'];
|
|
429
|
+
const WD = weekdays || ['lu', 'ma', 'mi', 'ju', 'vi', 'sá', 'do'];
|
|
340
430
|
const inRange = (d) => mode === 'range' && sel?.[0] && (sel[1] || hover) && d > startOfDay(sel[0]) && d < startOfDay(sel[1] || hover) ;
|
|
341
431
|
const isEdge = (d) => mode === 'range' ? (sameDay(d, sel?.[0]) || sameDay(d, sel?.[1])) : sameDay(d, sel);
|
|
342
432
|
const month = (m) => {
|
|
343
433
|
const first = new Date(view.getFullYear(), view.getMonth() + m, 1), lead = (first.getDay() + 6) % 7, days = new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate();
|
|
344
434
|
const cells = Array.from({ length: 42 }, (_, i) => addDays(first, i - lead)).slice(0, Math.ceil((lead + days) / 7) * 7);
|
|
345
|
-
return `<div class="cx-cal-month" role="grid" aria-label="${
|
|
435
|
+
return `<div class="cx-cal-month" role="grid" aria-label="${mon[first.getMonth()]} ${first.getFullYear()}"><button type="button" class="cx-cal-title cx-cal-pick" data-cal-pick aria-label="${pickLabel}: ${mon[first.getMonth()]} ${first.getFullYear()}">${ui.text(`${mon[first.getMonth()][0].toUpperCase()}${mon[first.getMonth()].slice(1)} ${first.getFullYear()}`, 'heading')}${ui.icon('chev')}</button><div class="cx-cal-grid" role="row">${WD.map((w) => `<span class="cx-cal-wd" role="columnheader">${ui.text(w, 'caption')}</span>`).join('')}</div><div class="cx-cal-grid">${cells.map((d) => {
|
|
346
436
|
const out = d.getMonth() !== first.getMonth(), off = min && d < startOfDay(min);
|
|
347
437
|
const st = isEdge(d) ? 'edge' : inRange(d) ? 'mid' : '';
|
|
348
|
-
return `<button class="cn-button cn-button-variant-ghost cn-button-size-icon cx-cal-day" role="gridcell" data-day="${+d}" ${out ? 'data-out' : ''} ${sameDay(d, today) ? 'data-today' : ''} ${st ? `data-range="${st}"` : ''} aria-pressed="${st === 'edge'}" aria-label="${DAYLONG[d.getDay()]} ${d.getDate()} de ${
|
|
438
|
+
return `<button class="cn-button cn-button-variant-ghost cn-button-size-icon cx-cal-day" role="gridcell" data-day="${+d}" ${out ? 'data-out' : ''} ${sameDay(d, today) ? 'data-today' : ''} ${st ? `data-range="${st}"` : ''} aria-pressed="${st === 'edge'}" aria-label="${DAYLONG[d.getDay()]} ${d.getDate()} de ${mon[d.getMonth()]}" tabindex="${sameDay(d, focusDay) && !out ? 0 : -1}" ${off ? 'disabled' : ''}>${d.getDate()}</button>`;
|
|
349
439
|
}).join('')}</div></div>`;
|
|
350
440
|
};
|
|
441
|
+
const picker = () => `<div class="cx-cal cx-cal-picker"><div class="cx-cal-nav">${ui.button({ icon: 'left', variant: 'ghost', size: 'icon-sm', attrs: `data-cal-year="-1" aria-label="${prevYear}" title="${prevYear}"` })}${ui.button({ icon: 'right', variant: 'ghost', size: 'icon-sm', attrs: `data-cal-year="1" aria-label="${nextYear}" title="${nextYear}"` })}</div><div class="cx-cal-title">${ui.text(String(view.getFullYear()), 'heading')}</div><div class="cx-cal-mgrid">${mon.map((m, k) => `<button type="button" class="cn-button cn-button-variant-ghost cn-button-size-sm cx-cal-m" data-cal-month="${k}" ${k === view.getMonth() ? 'aria-current="true"' : ''} ${today.getFullYear() === view.getFullYear() && today.getMonth() === k ? 'data-today' : ''}>${m[0].toUpperCase()}${m.slice(1, 3)}</button>`).join('')}</div></div>`;
|
|
351
442
|
const paint = (focus) => {
|
|
352
|
-
|
|
443
|
+
if (picking) { root.innerHTML = picker(); root.querySelector('[aria-current="true"]')?.focus(); return; }
|
|
444
|
+
root.innerHTML = `<div class="cx-cal" data-months="${months}"><div class="cx-cal-nav">${ui.button({ icon: 'left', variant: 'ghost', size: 'icon-sm', attrs: `data-cal-nav="-1" aria-label="${prevLabel}" title="${prevLabel}"` })}${ui.button({ icon: 'right', variant: 'ghost', size: 'icon-sm', attrs: `data-cal-nav="1" aria-label="${nextLabel}" title="${nextLabel}"` })}</div><div class="cx-cal-months">${Array.from({ length: months }, (_, m) => month(m)).join('')}</div></div>`;
|
|
353
445
|
if (focus) root.querySelector(`[data-day="${+focusDay}"]:not([data-out])`)?.focus();
|
|
354
446
|
};
|
|
355
447
|
const pick = (d) => {
|
|
@@ -360,6 +452,9 @@ let wheelsNow = [];
|
|
|
360
452
|
};
|
|
361
453
|
root.addEventListener('click', (e) => {
|
|
362
454
|
const n = e.target.closest('[data-cal-nav]'); if (n) { view.setMonth(view.getMonth() + +n.dataset.calNav); paint(); return; }
|
|
455
|
+
if (e.target.closest('[data-cal-pick]')) { picking = true; paint(); return; }
|
|
456
|
+
const yr = e.target.closest('[data-cal-year]'); if (yr) { view.setFullYear(view.getFullYear() + +yr.dataset.calYear); paint(); return; }
|
|
457
|
+
const mo = e.target.closest('[data-cal-month]'); if (mo) { view = new Date(view.getFullYear(), +mo.dataset.calMonth, 1); picking = false; paint(); return; }
|
|
363
458
|
const b = e.target.closest('[data-day]'); if (b && !b.disabled) pick(new Date(+b.dataset.day));
|
|
364
459
|
});
|
|
365
460
|
root.addEventListener('pointerover', (e) => { const b = e.target.closest('[data-day]'); if (mode === 'range' && b && sel?.[0] && !sel[1]) { hover = new Date(+b.dataset.day); $$('[data-day]', root).forEach((x) => { const d = new Date(+x.dataset.day); x.toggleAttribute('data-range', false); if (isEdge(d)) x.dataset.range = 'edge'; else if (inRange(d)) x.dataset.range = 'mid'; }); } });
|
|
@@ -377,63 +472,264 @@ let wheelsNow = [];
|
|
|
377
472
|
return { set: (v) => { sel = v; const a = mode === 'range' ? v?.[0] : v; if (a) { view = new Date(a.getFullYear(), a.getMonth(), 1); focusDay = startOfDay(a); } paint(); } };
|
|
378
473
|
};
|
|
379
474
|
|
|
380
|
-
// ──
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
475
|
+
// ── AGENDA ──
|
|
476
|
+
// Day columns per person, the week, or a list (the phone keeps the day grid and the list). Two ways to use it:
|
|
477
|
+
// · a SHOWCASE (no app behind it): its own sheets open, book, block, move and cancel, all in memory;
|
|
478
|
+
// · an APP (sheet: 'app'): the app owns the data. The agenda paints what it is given and tells the app what
|
|
479
|
+
// happened: onOpen(event) when a cita is tapped, onCreate({resource, day, start}) on a free hour, onBlock(block)
|
|
480
|
+
// when a block is asked, onMove(change) when one is dragged, onRange({from, to, view}) when the days shown
|
|
481
|
+
// change (so the app loads them). The app answers with update(next): new data, same day, view and person.
|
|
482
|
+
// The free hours can come from the server (free: [{resource, day, start, end}]): then they are the ONLY free hours,
|
|
483
|
+
// so the owner never sees a slot the client's page would not offer. Without them they come from availability().
|
|
484
|
+
// Words, days and months follow `lang` ('es' | 'en'), any of them can be replaced through `words`; prices are
|
|
485
|
+
// written with `currency` + `locale` (Intl), in units or in cents (`cents: true`).
|
|
486
|
+
const AG_WORDS = {
|
|
487
|
+
es: {
|
|
488
|
+
item: 'cita', items: 'citas', resource: 'profesional', newItem: 'Nueva cita', theItem: 'la cita', It: 'Cita',
|
|
489
|
+
today: 'Hoy', todayShort: 'hoy', prevDay: 'Día anterior', nextDay: 'Día siguiente', prevWeek: 'Semana anterior', nextWeek: 'Semana siguiente',
|
|
490
|
+
view: 'Vista', vDay: 'Día', vWeek: 'Semana', vList: 'Lista', block: 'Bloquear', all: 'Todos', days: 'Días',
|
|
491
|
+
weekOf: (d) => `Semana del ${d}`, dayTitle: (wd, n, m) => `${wd} ${n} de ${m}`,
|
|
492
|
+
off: 'No trabaja este día', blocked: 'Bloqueado', minutes: 'minutos', with: 'con', notFree: 'no está libre', notFreeLong: 'Ese horario no está libre',
|
|
493
|
+
nowLasts: (m) => `Ahora dura ${m} min`, movedTo: (t, who) => `Movida a las ${t}${who ? ` con ${who}` : ''}`,
|
|
494
|
+
free: 'Libre', freeFromTo: (a, b) => `Libre de ${a} a ${b}, agendar`, until: 'a', noHours: 'No hay horario este día', freeSlot: 'Hora libre',
|
|
495
|
+
now: 'Ahora', waiting: (n) => (n === 1 ? '1 en espera' : `${n} en espera`), closed: 'Cerrado', countOf: (n) => (n === 1 ? '1 cita' : `${n} citas`),
|
|
496
|
+
move: 'Cambiar la hora', day: 'Día', current: 'Es su hora actual', noneFree: 'No queda una hora libre ese día.', back: 'Volver', saveMove: 'Guardar nueva hora',
|
|
497
|
+
cancelOf: 'Cancelar la cita', why: '¿Por qué?', reasons: ['El cliente pidió cancelar', 'No vino', 'Me surgió algo', 'Otro'], reasonLabel: 'Motivo', tellClient: 'Le avisamos al cliente con este motivo.',
|
|
498
|
+
close: 'Cerrar', finished: 'Terminada', proofShow: 'Ver comprobante', proofHide: 'Ocultar comprobante', proof: 'Comprobante', paidBtn: 'Pago recibido', openFirst: 'Ábrelo primero',
|
|
499
|
+
confirmFirst: 'Primero confirma el pago', write: 'Escribir', opening: (x) => `Abriendo ${x}`, paid: 'Pagado', cancelled: 'Cita cancelada', movedDay: (d, t) => `Movida al ${d} a las ${t}`,
|
|
500
|
+
newDesc: 'Elige el servicio, quién viene y a qué hora.', service: 'Servicio', client: 'Cliente', clientPh: 'Nombre y apellido', hour: 'Hora', cancel: 'Cancelar',
|
|
501
|
+
schedule: 'Agendar', scheduled: (t) => `Cita agendada · ${t}`,
|
|
502
|
+
blockTitle: 'Bloquear un horario', blockDesc: 'Nadie puede reservar en ese rato.', for: 'Para', wholeShop: 'Todo el negocio', from: 'Desde', to: 'Hasta',
|
|
503
|
+
reasonDefault: 'Almuerzo', onlyYou: 'Solo lo ves tú.', blockedOk: 'Horario bloqueado', loading: 'Cargando la agenda',
|
|
504
|
+
business: 'Negocio', team: 'Equipo', whose: 'Agenda de', goTo: 'Ir a un día', prevMonth: 'Mes anterior', nextMonth: 'Mes siguiente', prevYear: 'Año anterior', nextYear: 'Año siguiente', pickMonth: 'Elegir mes', oneDay: 'Un día', someDays: 'Varios días', noItems: 'Sin citas', rangeHint: 'Toca el primer día y luego el último.', full: 'A esa hora ya no cabe nadie más', clashOne: 'Se cruza', clash: (n) => `${n} citas se cruzan este día`, clashHow: 'Muévelas a otra hora o a otra persona.', clashSee: 'Ver cuáles',
|
|
505
|
+
capacityOf: (n) => `hasta ${n} a la vez`,
|
|
506
|
+
status: { awaiting_payment: 'Por revisar', confirmed: 'Confirmada', started: 'En curso', done: 'Cumplida', noshow: 'No vino' },
|
|
507
|
+
dayS: ['dom', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb'], dayL: ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado'],
|
|
508
|
+
mon: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
|
|
509
|
+
},
|
|
510
|
+
en: {
|
|
511
|
+
item: 'appointment', items: 'appointments', resource: 'team member', newItem: 'New appointment', theItem: 'the appointment', It: 'Appointment',
|
|
512
|
+
today: 'Today', todayShort: 'today', prevDay: 'Previous day', nextDay: 'Next day', prevWeek: 'Previous week', nextWeek: 'Next week',
|
|
513
|
+
view: 'View', vDay: 'Day', vWeek: 'Week', vList: 'List', block: 'Block', all: 'Everyone', days: 'Days',
|
|
514
|
+
weekOf: (d) => `Week of ${d}`, dayTitle: (wd, n, m) => `${wd}, ${m} ${n}`,
|
|
515
|
+
off: 'Not working this day', blocked: 'Blocked', minutes: 'minutes', with: 'with', notFree: 'not free', notFreeLong: 'That time is not free',
|
|
516
|
+
nowLasts: (m) => `Now ${m} min long`, movedTo: (t, who) => `Moved to ${t}${who ? ` with ${who}` : ''}`,
|
|
517
|
+
free: 'Free', freeFromTo: (a, b) => `Free from ${a} to ${b}, book`, until: 'to', noHours: 'No hours this day', freeSlot: 'Free time',
|
|
518
|
+
now: 'Now', waiting: (n) => `${n} waiting`, closed: 'Closed', countOf: (n) => (n === 1 ? '1 appointment' : `${n} appointments`),
|
|
519
|
+
move: 'Change the time', day: 'Day', current: 'Its current time', noneFree: 'No free time left that day.', back: 'Back', saveMove: 'Save new time',
|
|
520
|
+
cancelOf: 'Cancel the appointment', why: 'Why?', reasons: ['The client asked to cancel', 'No-show', 'Something came up', 'Other'], reasonLabel: 'Reason', tellClient: 'We tell the client with this reason.',
|
|
521
|
+
close: 'Close', finished: 'Finished', proofShow: 'See receipt', proofHide: 'Hide receipt', proof: 'Receipt', paidBtn: 'Payment received', openFirst: 'Open it first',
|
|
522
|
+
confirmFirst: 'Confirm the payment first', write: 'Message', opening: (x) => `Opening ${x}`, paid: 'Paid', cancelled: 'Appointment cancelled', movedDay: (d, t) => `Moved to the ${d} at ${t}`,
|
|
523
|
+
newDesc: 'Pick the service, who is coming and when.', service: 'Service', client: 'Client', clientPh: 'First and last name', hour: 'Time', cancel: 'Cancel',
|
|
524
|
+
schedule: 'Book', scheduled: (t) => `Appointment booked · ${t}`,
|
|
525
|
+
blockTitle: 'Block a time', blockDesc: 'Nobody can book in that window.', for: 'For', wholeShop: 'The whole business', from: 'From', to: 'To',
|
|
526
|
+
reasonDefault: 'Lunch', onlyYou: 'Only you see it.', blockedOk: 'Time blocked', loading: 'Loading the calendar',
|
|
527
|
+
business: 'Business', team: 'Team', whose: 'Calendar of', goTo: 'Go to a day', prevMonth: 'Previous month', nextMonth: 'Next month', prevYear: 'Previous year', nextYear: 'Next year', pickMonth: 'Pick a month', oneDay: 'One day', someDays: 'Several days', noItems: 'No appointments', rangeHint: 'Tap the first day, then the last.', full: 'No room left at that time', clashOne: 'Overlaps', clash: (n) => `${n} appointments overlap this day`, clashHow: 'Move them to another time or person.', clashSee: 'Show me',
|
|
528
|
+
capacityOf: (n) => `up to ${n} at once`,
|
|
529
|
+
status: { awaiting_payment: 'To review', confirmed: 'Confirmed', started: 'In progress', done: 'Done', noshow: 'No-show' },
|
|
530
|
+
dayS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], dayL: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
|
531
|
+
mon: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
// a cita's status as the agenda draws it: its tone (the colour family) and whether it still occupies its time
|
|
535
|
+
const AG_STATUS = { awaiting_payment: 'info', confirmed: 'ok', started: 'live', done: 'done', noshow: 'bad' };
|
|
536
|
+
|
|
537
|
+
let agSeq = 0, agOwner = 0; // several agendas on one page: each its own sheets, and only the one that opened the sheet answers it
|
|
538
|
+
kit.agenda = (root, opts) => {
|
|
539
|
+
const me = ++agSeq, K = (name) => `${name}${me}`;
|
|
540
|
+
const sheetOf = (name) => { agOwner = me; openModal(K(name), 'auto'); };
|
|
541
|
+
let { resources, events, blocks = [], availability = () => [[9 * 60, 19 * 60]], free: freeList = null, queue = [], closed = [], loading = false,
|
|
542
|
+
lang = 'es', words = {}, currency = '', locale = '', cents = false, overlap = 'auto', placing = null } = opts; // all of these can change later through update()
|
|
543
|
+
const { from = 9, to = 19, slot = 30, hourPx = 56, onCreate = () => {}, onChange = () => {}, onAction = () => {}, onOpen = null, onBlock = null, onMove = null, onRange = () => {},
|
|
544
|
+
actions = null, createSheet = false, services = [], view: startView = 'day', sheet: sheetMode = 'built-in', drag: dragOn = true,
|
|
545
|
+
resize: resizeMode = 'on', moveAcross = 'on',
|
|
546
|
+
start: startDay = null, phone: phoneMode = '', hour12 = false, timeZone = '', phoneView = '', blockScope = 'each' } = opts;
|
|
547
|
+
// THE SHOP'S CLOCK: with a timeZone, «today», «now» and «the past» are the shop's, not the browser's (an owner
|
|
548
|
+
// travelling, or a phone set to another zone, still sees the shop's day)
|
|
549
|
+
const clockNow = () => { if (!timeZone) return new Date(); try { const p = Object.fromEntries(new Intl.DateTimeFormat('en-US', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).formatToParts(new Date()).map((x) => [x.type, x.value])); return new Date(+p.year, +p.month - 1, +p.day, +p.hour % 24, +p.minute); } catch { return new Date(); } };
|
|
550
|
+
const today = (() => { const d = clockNow(); d.setHours(0, 0, 0, 0); return d; })();
|
|
551
|
+
const nowMin = () => { const d = clockNow(); return d.getHours() * 60 + d.getMinutes(); };
|
|
552
|
+
// how a time is WRITTEN (the data stays "HH:MM"): 24 h, or 12 h with AM/PM where the shop reads it that way
|
|
553
|
+
const clk = (hm) => { if (!hour12 || !hm || !/^\d{1,2}:\d{2}$/.test(hm)) return hm; const [h, m] = hm.split(':').map(Number); return `${((h + 11) % 12) + 1}:${String(m).padStart(2, '0')} ${h < 12 ? 'AM' : 'PM'}`; };
|
|
554
|
+
// the view on screen: the phone has no week, and an app can open the phone on its list (phoneView) until a view is picked
|
|
555
|
+
let picked = false, rangeSel = null; // rangeSel [from, to] while the range view is on
|
|
556
|
+
const eff = () => (view === 'range' && rangeSel ? 'range' : narrow ? (!picked && phoneView ? phoneView : view === 'week' ? 'day' : view) : view);
|
|
557
|
+
// phone "nav": on a narrow agenda the actions and the arrows leave its toolbar, because the app's phone bar carries
|
|
558
|
+
// them and calls run(id) (the same ids: new, block, prev, next, today, any action id; view:day|week|list)
|
|
559
|
+
if (phoneMode === 'nav') root.dataset.phone = 'nav';
|
|
560
|
+
let w = { ...(AG_WORDS[lang] || AG_WORDS.es), ...words };
|
|
561
|
+
const appMode = sheetMode === 'app';
|
|
562
|
+
const fmtMoney = (n) => { const v = cents ? n / 100 : n; if (!currency) return money0(v); try { return new Intl.NumberFormat(locale || (lang === 'en' ? 'en-US' : 'es'), { style: 'currency', currency, maximumFractionDigits: v % 1 ? 2 : 0 }).format(v); } catch { return money0(v); } };
|
|
563
|
+
const fmtD = (d) => (lang === 'en' ? `${w.mon[d.getMonth()].slice(0, 3)} ${d.getDate()}` : `${d.getDate()} ${w.mon[d.getMonth()].slice(0, 3)}`);
|
|
564
|
+
const It = (s) => s[0].toUpperCase() + s.slice(1);
|
|
565
|
+
// narrow (the phone layout) is known before the first paint, from the width the agenda already has: painting the
|
|
566
|
+
// wide layout first and switching a frame later made it jump.
|
|
386
567
|
const toMin = (t) => { const [h, m] = t.split(':').map(Number); return h * 60 + m; };
|
|
568
|
+
let day = startDay ? new Date(startDay) : new Date(today), view = ['day', 'week', 'list'].includes(startView) ? startView : 'day', who = 'all', narrow = (root.clientWidth || root.parentElement?.clientWidth || 999) < 700;
|
|
387
569
|
const toHM = (m) => `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
|
|
388
570
|
const y = (m) => ((m - from * 60) / 60) * hourPx;
|
|
389
571
|
const weekDays = () => { const mon = addDays(day, -((day.getDay() + 6) % 7)); return Array.from({ length: 7 }, (_, i) => addDays(mon, i)); };
|
|
390
|
-
|
|
391
|
-
|
|
572
|
+
// SEVERAL AGENDAS: a resource is an agenda (kind "business" for the shop's own, "person" for each worker, or any other
|
|
573
|
+
// kind: a room, a chair). The business goes first. A cita belongs to its `resource` and also takes every agenda in
|
|
574
|
+
// `with` (a worker AND the room). `capacity` says how many citas an agenda holds at once: 1 (the default) = they never
|
|
575
|
+
// overlap, 3 = a class of three, 0 = no limit. `overlap` "never" | "allow" overrides it for the whole agenda.
|
|
576
|
+
const kindOf = (r) => r?.kind || 'person';
|
|
577
|
+
const ordered = () => [...resources].sort((a, b) => (kindOf(a) === 'business' ? 0 : 1) - (kindOf(b) === 'business' ? 0 : 1));
|
|
578
|
+
const shown = () => (who === 'all' ? ordered() : who.startsWith('kind:') ? ordered().filter((r) => kindOf(r) === who.slice(5)) : resources.filter((r) => r.id === who));
|
|
579
|
+
// what an agenda is, under its name: its role («Barbero»), else its kind (the business, a room's group), plus how many fit
|
|
580
|
+
const roleOf = (r) => [r.role || (kindOf(r) === 'business' ? w.business : kindOf(r) !== 'person' ? r.group || '' : ''), capOf(r.id) > 1 ? w.capacityOf(capOf(r.id)) : ''].filter(Boolean).join(' · ');
|
|
581
|
+
// a resource with hideEmpty (the business's own agenda, which only holds what nobody was given) shows its column
|
|
582
|
+
// only on days it has something; the rest of the agendas always show
|
|
583
|
+
const bookable = (rid) => resources.find((r) => r.id === rid)?.bookable !== false;
|
|
584
|
+
const dayCols = () => { const rs = shown().filter((r) => !r.hideEmpty || evOn(day, r.id).length || who === r.id); return rs.length ? rs : shown(); };
|
|
585
|
+
const pick = () => (who === 'all' || who.startsWith('kind:') ? (shown()[0] || resources[0]).id : who);
|
|
586
|
+
const occ = (e) => [e.resource, ...(Array.isArray(e.with) ? e.with : [])];
|
|
587
|
+
const capOf = (rid) => { if (overlap === 'never') return 1; if (overlap === 'allow') return 0; const c = resources.find((r) => r.id === rid)?.capacity; return c === undefined || c === null || c === '' ? 1 : Math.max(0, Number(c) || 0); };
|
|
588
|
+
const evOn = (d, rid) => { const ids = rid ? [rid] : who === 'all' ? null : shown().map((r) => r.id); return events.filter((e) => sameDay(e.day, d) && e.status !== 'cancelled' && (!ids || occ(e).some((x) => ids.includes(x)))); };
|
|
589
|
+
// the most citas one agenda holds at once inside [st, st + dur) (except one, the cita being moved)
|
|
590
|
+
const peak = (rid, d, st, dur, except) => { const iv = events.filter((x) => x.id !== except && x.status !== 'cancelled' && sameDay(x.day, d) && occ(x).includes(rid)).map((x) => [toMin(x.start), toMin(x.start) + x.dur]).filter(([a, b]) => a < st + dur && b > st); return Math.max(0, ...[st, ...iv.map(([a]) => a).filter((a) => a > st)].map((pt) => iv.filter(([a, b]) => a <= pt && b > pt).length)); };
|
|
591
|
+
const roomFor = (rid, d, st, dur, except) => { const cap = capOf(rid); return cap === 0 || peak(rid, d, st, dur, except) < cap; };
|
|
592
|
+
// citas that break an agenda's capacity (data from the server that crossed): they say so, in words
|
|
593
|
+
let clash = new Set();
|
|
594
|
+
const findClashes = () => { const out = new Set(); resources.forEach((r) => { const cap = capOf(r.id); if (!cap) return; events.filter((x) => x.status !== 'cancelled' && occ(x).includes(r.id)).forEach((e) => { if (peak(r.id, e.day, toMin(e.start), e.dur, null) > cap) out.add(e.id); }); }); return out; };
|
|
392
595
|
const blOn = (d, rid) => blocks.filter((b) => sameDay(b.day, d) && (!rid || b.resource === rid || b.resource === 'all'));
|
|
393
596
|
const resName = (id) => resources.find((r) => r.id === id)?.name || '';
|
|
597
|
+
const toneOf = (e) => AG_STATUS[e.status] || e.tone || 'ok';
|
|
598
|
+
const paint1 = (e) => (e.tint ? ` data-tint style="--tint:${e.tint};--ink:${e.ink || 'inherit'};` : ' style="');
|
|
599
|
+
// the server's free windows of one person on one day, in minutes (only when the app sends them)
|
|
600
|
+
const freeOf = (rid, d) => (freeList || []).filter((f) => f.resource === rid && sameDay(f.day, d)).map((f) => [toMin(f.start), toMin(f.end)]);
|
|
601
|
+
const works = (rid, d) => (freeList ? true : availability(rid, d).length > 0);
|
|
602
|
+
const isClosed = (d) => closed.some((c) => sameDay(c, d)) || (!freeList && shown().every((r) => !availability(r.id, d).length));
|
|
394
603
|
// overlapping events share the column: each gets a lane and the lane count of its cluster
|
|
395
604
|
const lanes = (list) => { const s = [...list].sort((a, b) => toMin(a.start) - toMin(b.start)); const out = new Map(); let cluster = [], end = -1; const flush = () => { const lanesEnd = []; cluster.forEach((e) => { const st = toMin(e.start); let l = lanesEnd.findIndex((x) => x <= st); if (l < 0) { l = lanesEnd.length; lanesEnd.push(0); } lanesEnd[l] = st + e.dur; out.set(e.id, [l, 0]); }); cluster.forEach((e) => { out.get(e.id)[1] = lanesEnd.length; }); }; s.forEach((e) => { const st = toMin(e.start); if (st >= end && cluster.length) { flush(); cluster = []; } cluster.push(e); end = Math.max(end, st + e.dur); }); if (cluster.length) flush(); return out; };
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
// the
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
605
|
+
// an app's own words on a cita («A domicilio», «Llegó 10:02», «Confirmó»): said next to its time, never as colour
|
|
606
|
+
const tagsOf = (e) => (Array.isArray(e.tags) ? e.tags.filter(Boolean).join(' · ') : '');
|
|
607
|
+
const SAY = ['started', 'awaiting_payment', 'noshow']; // the statuses a cita SAYS in words (no stripes, no glow)
|
|
608
|
+
const evHtml = (e, ln, showWho, rid = null, gut = false) => { const [l, n] = ln.get(e.id) || [0, 1]; const short = e.dur < 45; const said = clash.has(e.id) ? w.clashOne : SAY.includes(e.status) && w.status[e.status] ? w.status[e.status] : ''; const others = rid ? occ(e).filter((x) => x !== rid).map(resName).filter(Boolean).join(', ') : ''; const tg = tagsOf(e);
|
|
609
|
+
const span = gut ? '(100% - 22px)' : '100%'; /* an agenda with no limit keeps a strip on the right to book beside */ return `<button class="cx-ag-ev" data-ev="${e.id}" data-tone="${toneOf(e)}" ${e.status ? `data-status="${e.status}"` : ''}${paint1(e)}top:${y(toMin(e.start))}px;height:${(e.dur / 60) * hourPx - 3}px;left:calc(${span} * ${l / n} + 3px);width:calc(${span} / ${n} - 6px)"${clash.has(e.id) ? ' data-conflict' : ''} aria-label="${e.title}, ${clk(e.start)}, ${e.dur} ${w.minutes}${tg ? ', ' + tg : ''}${showWho ? `, ${w.with} ${resName(e.resource)}` : ''}${others ? `, ${w.with} ${others}` : ''}${e.status && w.status[e.status] ? `, ${w.status[e.status]}` : ''}${clash.has(e.id) ? `, ${w.clashOne}` : ''}">${short ? ui.text(`${e.title} · ${said || tg || clk(e.start)}`, 'label') : `${ui.text(e.title, 'label')}${ui.text(`${said ? said + ' · ' : ''}${tg ? tg + ' · ' : ''}${clk(e.start)} · ${e.subtitle || ''}${showWho ? ' · ' + resName(e.resource) : ''}${others ? ' · ' + others : ''}`, 'caption')}`}${dragOn && resizeMode !== 'off' ? '<span class="cx-ag-grip" aria-hidden="true"></span>' : ''}</button>`; };
|
|
610
|
+
// the hours a person does not work: hatched. With server free hours, everything that is neither free nor booked.
|
|
611
|
+
const offHtml = (d, rid) => {
|
|
612
|
+
const av = freeList ? null : availability(rid, d);
|
|
613
|
+
if (av && !av.length) return `<div class="cx-ag-off" data-all style="top:0;height:100%">${ui.text(w.off, 'caption')}</div>`;
|
|
614
|
+
if (!av) return '';
|
|
615
|
+
const out = []; let cur = from * 60; av.forEach(([a, b]) => { if (a > cur) out.push([cur, a]); cur = Math.max(cur, b); }); if (cur < to * 60) out.push([cur, to * 60]);
|
|
616
|
+
return out.map(([a, b]) => `<div class="cx-ag-off" style="top:${y(a)}px;height:${y(b) - y(a)}px"></div>`).join('');
|
|
617
|
+
};
|
|
618
|
+
// the server's free windows, drawn as dashed «free» tiles you tap to book (they replace the bare slot buttons)
|
|
619
|
+
const freeHtml = (d, rid, label) => freeOf(rid, d).map(([a, b]) => `<button class="cx-ag-free" data-free="${a}" data-rid="${rid}" data-day="${+d}" style="top:${y(a)}px;height:${y(b) - y(a) - 2}px" aria-label="${w.freeFromTo(clk(toHM(a)), clk(toHM(b)))}, ${label}">${b - a >= 45 ? ui.text(`${w.free} · ${clk(toHM(a))}`, 'caption') : ''}</button>`).join('');
|
|
620
|
+
// PLACING: while the app's «new cita» sheet is open, where the cita will land, drawn dashed (placing {resource, day,
|
|
621
|
+
// start, dur, title, tint}); the app clears it when the sheet closes
|
|
622
|
+
// what already went by: today up to now, and whole past days, under a quiet veil (nothing to book there)
|
|
623
|
+
const goneHtml = (d) => { const k = new Date(d); k.setHours(0, 0, 0, 0); if (k > today) return ''; const h = k < today ? (to - from) * hourPx : Math.max(0, Math.min(y(nowMin()), (to - from) * hourPx)); return h ? `<div class="cx-ag-gone" style="height:${h}px" aria-hidden="true"></div>` : ''; };
|
|
624
|
+
const ghostHtml = (d, rid) => { const g = placing; if (!g || !g.start || !sameDay(g.day, d) || (rid && g.resource !== rid)) return ''; return `<div class="cx-ag-ghostnew" style="${g.tint ? `--tint:${g.tint};` : ''}top:${y(toMin(g.start))}px;height:${((Number(g.dur) || slot) / 60) * hourPx - 3}px" aria-hidden="true">${ui.text(`${g.title || w.newItem} · ${clk(g.start)}`, 'label')}</div>`; };
|
|
625
|
+
const blockHtml = (b) => `<div class="cx-ag-block" style="top:${y(toMin(b.start))}px;height:${(b.dur / 60) * hourPx - 2}px">${ui.text(`${b.reason || w.blocked} · ${clk(b.start)}`, 'caption')}</div>`;
|
|
626
|
+
// while loading, everything that would ask the server for something is shut (days, views, arrows, actions, hours)
|
|
627
|
+
const shut = () => (loading ? ' disabled aria-disabled="true"' : '');
|
|
628
|
+
const title = () => (eff() === 'range' ? `${fmtD(rangeSel[0])} → ${fmtD(rangeSel[1])}` : view === 'week' && !narrow ? w.weekOf(fmtD(weekDays()[0])) : w.dayTitle(w.dayL[day.getDay()], day.getDate(), w.mon[day.getMonth()]));
|
|
629
|
+
const toolbar = () => `<div class="cx-ag-bar">${ui.button({ label: w.today, size: 'sm', attrs: `data-ag="today"${shut()}` })}<div class="cx-hstack" style="gap:2px;flex-wrap:nowrap">${ui.button({ icon: 'left', variant: 'ghost', size: 'icon-sm', attrs: `data-ag="prev" data-ag-move aria-label="${view === 'week' && !narrow ? w.prevWeek : w.prevDay}"${shut()}` })}${ui.button({ icon: 'right', variant: 'ghost', size: 'icon-sm', attrs: `data-ag="next" data-ag-move aria-label="${view === 'week' && !narrow ? w.nextWeek : w.nextDay}"${shut()}` })}</div><button type="button" class="cx-ag-title" data-ag-date aria-haspopup="dialog" aria-expanded="false" aria-label="${w.goTo}: ${title()}"${shut()}>${ui.text(title(), 'heading')}${ui.icon('chev')}</button><span class="cx-grow"></span><div class="cx-tabs" role="tablist" aria-label="${w.view}">${[['day', w.vDay], ['week', w.vWeek], ['list', w.vList]].filter(([k]) => !narrow || k !== 'week').map(([k, l]) => `<button class="cn-button cn-button-variant-ghost cn-button-size-sm cx-tab" role="tab" aria-selected="${eff() === k}" data-ag-view="${k}"${shut()}>${l}</button>`).join('')}</div>${bar()}</div>`;
|
|
630
|
+
// the toolbar's own buttons are data: [{id, label, icon, primary}]; «block» and «new» keep their behaviour, any other id goes to onAction
|
|
631
|
+
function bar() { const list = Array.isArray(actions) && actions.length ? actions : [{ id: 'block', label: w.block, icon: 'i-lock' }, { id: 'new', label: w.newItem, icon: 'plus', primary: true }]; return list.map((x) => ui.button({ label: `${x.icon ? ui.icon(x.icon) : ''}${x.label}`, variant: x.primary ? 'default' : 'outline', size: 'sm', attrs: `data-ag="${x.id}" data-ag-act${shut()}` })).join(''); }
|
|
632
|
+
const personBtn = (r) => ui.button({ label: `${r.photo ? `<span class="cn-avatar cx-ag-face" style="background-image:url('${r.photo}')"></span>` : ui.avatar(r.name, { size: 'sm' })}${r.name}`, size: 'sm', variant: 'ghost', attrs: `role="radio" aria-checked="${who === r.id}" data-who="${r.id}"` });
|
|
633
|
+
const groupName = (k) => (k === 'business' ? w.business : k === 'person' ? w.team : resources.find((r) => kindOf(r) === k)?.group || k);
|
|
634
|
+
// the filter: everyone, then each kind of agenda (the business, the team, rooms…) with its own button when there is more than one kind
|
|
635
|
+
const people = () => {
|
|
636
|
+
if (loading) return `<div class="cx-hstack cx-ag-who" aria-hidden="true">${[64, 92, 92, 92].map((wd, i) => `<span class="cx-ag-skpill">${i ? '<s class="skel" style="width:20px;height:20px;border-radius:50%"></s>' : ''}<s class="skel" style="width:${i ? wd - 30 : wd - 20}px"></s></span>`).join('')}</div>`;
|
|
637
|
+
if (resources.length < 2) return '';
|
|
638
|
+
const kinds = [...new Set(ordered().map(kindOf))];
|
|
639
|
+
const all = ui.button({ label: w.all, size: 'sm', variant: 'ghost', attrs: `role="radio" aria-checked="${who === 'all'}" data-who="all"` });
|
|
640
|
+
if (kinds.length < 2) return `<div class="cx-hstack cx-ag-who" role="radiogroup" aria-label="${w.whose}"><span class="cx-ag-wholabel" aria-hidden="true">${w.whose}</span>${all}${resources.map(personBtn).join('')}</div>`;
|
|
641
|
+
return `<div class="cx-hstack cx-ag-who" role="radiogroup" aria-label="${w.whose}"><span class="cx-ag-wholabel" aria-hidden="true">${w.whose}</span>${all}${kinds.map((k) => { const rs = ordered().filter((r) => kindOf(r) === k); return `<span class="cx-ag-sep" aria-hidden="true"></span>${rs.length > 1 ? ui.button({ label: groupName(k), size: 'sm', variant: 'ghost', attrs: `role="radio" aria-checked="${who === 'kind:' + k}" data-who="kind:${k}"`, cls: 'cx-ag-group' }) : ''}${rs.map(personBtn).join('')}`; }).join('')}</div>`;
|
|
642
|
+
};
|
|
643
|
+
// the week strip: each day with how many citas it has (a plain number), closed days dimmed
|
|
644
|
+
const strip = () => `<div class="cx-ag-strip" role="group" aria-label="${w.days}">${weekDays().map((d) => { const evs = loading ? [] : evOn(d); const n = evs.length; const shut = isClosed(d); return `<button class="cn-button cn-button-variant-ghost cx-size-tile cx-ag-day" data-ag-day="${+d}" aria-pressed="${sameDay(d, day)}" ${sameDay(d, today) ? 'data-today' : ''} ${shut ? 'data-closed' : ''} aria-label="${w.dayL[d.getDay()]} ${d.getDate()}, ${shut && !n ? w.closed : w.countOf(n)}"${loading ? ' disabled aria-disabled="true"' : ''}><small>${sameDay(d, today) ? w.todayShort : w.dayS[d.getDay()]}</small><b>${d.getDate()}</b><span class="cx-ag-count" aria-hidden="true">${loading ? '<s class="skel"></s>' : n || ''}</span></button>`; }).join('')}</div>`;
|
|
645
|
+
// «Ahora»: on today, the cita in progress (started, or whose time is now), first thing, one tap to open it
|
|
646
|
+
const nowCard = () => {
|
|
647
|
+
if (!sameDay(day, today)) return '';
|
|
648
|
+
const nowM = nowMin();
|
|
649
|
+
const e = evOn(today).find((x) => x.status === 'started') || evOn(today).find((x) => !['done', 'noshow'].includes(x.status) && nowM >= toMin(x.start) && nowM < toMin(x.start) + x.dur);
|
|
650
|
+
if (!e) return '';
|
|
651
|
+
const left = Math.min(e.dur, Math.max(0, toMin(e.start) + e.dur - nowM)); // started early or ahead of the clock: never more than the cita itself
|
|
652
|
+
return `<button class="cx-ag-nowcard" data-ev="${e.id}"><span class="cx-vstack" style="gap:0;min-width:0">${ui.text(`${w.now} · ${clk(e.start)}–${clk(toHM(toMin(e.start) + e.dur))}`, 'caption')}${ui.text(e.title, 'heading')}${ui.text(`${e.subtitle || ''}${resources.length > 1 ? ' · ' + resName(e.resource) : ''}${tagsOf(e) ? ' · ' + tagsOf(e) : ''}`, 'muted')}</span><span class="cx-grow"></span>${ui.text(`${left} min`, 'strong')}</button>`;
|
|
653
|
+
};
|
|
654
|
+
// who waits for a free hour that day: two faces and how many (never grows with the line)
|
|
655
|
+
const queueLine = () => { const q = queue.filter((x) => sameDay(x.day, day)); if (!q.length) return ''; return `<div class="cx-ag-queue">${q.slice(0, 2).map((x) => (x.photo ? `<span class="cn-avatar cx-ag-face" style="background-image:url('${x.photo}')" aria-hidden="true"></span>` : ui.avatar(x.name, { size: 'sm' }))).join('')}${ui.text(w.waiting(q.length), 'caption')}</div>`; };
|
|
656
|
+
// the loading head keeps the real head's shape: the face, the name and, when the agenda has one, its role line
|
|
657
|
+
const skHead = (c) => { const r = resources.find((x) => x.id === c.rid); return v0() === 'week' ? `<span class="cx-ag-skhead" aria-hidden="true"><s class="skel" style="width:56px"></s></span>` : `<span class="cx-ag-skhead" aria-hidden="true"><s class="skel" style="width:24px;height:24px;border-radius:50%"></s><span class="cx-ag-colname"><b><s class="skel cx-ag-skline" style="width:64px"></s></b>${r && roleOf(r) ? `<small><s class="skel cx-ag-skline" style="width:48px"></s></small>` : ''}</span></span>`; };
|
|
658
|
+
const v0 = eff;
|
|
659
|
+
const grid = (cols) => { const hours = Array.from({ length: to - from }, (_, i) => from + i); const nowM = nowMin(); return `<div class="cx-ag-scroll cx-scroll"><div class="cx-ag" style="--cols:${cols.length};--h:${hourPx}px;grid-template-rows:auto auto"><div class="cx-ag-head"><span></span>${cols.map((c) => `<div class="cx-ag-colhead" ${c.today ? 'data-today' : ''} ${c.closed ? 'data-closed' : ''}>${loading ? skHead(c) : c.head}</div>`).join('')}</div><div class="cx-ag-body" style="height:${(to - from) * hourPx}px"><div class="cx-ag-times">${cols.some((k) => sameDay(k.day, today)) && nowM > from * 60 && nowM < to * 60 ? `<b class="cx-ag-nowtime" style="top:${y(nowM)}px">${clk(toHM(nowM))}</b>` : ''}${hours.map((h) => `<span style="top:${(h - from) * hourPx}px">${ui.text(hour12 ? `${((h + 11) % 12) + 1} ${h < 12 ? 'AM' : 'PM'}` : `${String(h).padStart(2, '0')}:00`, 'caption')}</span>`).join('')}</div>${cols.map((c, ci) => { const rid = c.rid === 'all' ? null : c.rid; const evs = evOn(c.day, rid); const cap = rid ? capOf(rid) : 1; const ln = lanes(evs); if (cap > 1) ln.forEach((v) => { v[1] = Math.max(v[1], Math.min(cap, 4)); }); const rids = rid ? [rid] : shown().map((r) => r.id); if (loading) return `<div class="cx-ag-col" data-day="${+c.day}" data-rid="${c.rid}" aria-hidden="true">${rid && !freeList ? offHtml(c.day, rid) : ''}${skelCol(ci)}</div>`;
|
|
660
|
+
return `<div class="cx-ag-col" data-day="${+c.day}" data-rid="${c.rid}" role="group" aria-label="${c.label}">${rid ? offHtml(c.day, rid) : ''}${!rids.some(bookable) ? '' : freeList ? rids.filter(bookable).map((r) => freeHtml(c.day, r, c.label)).join('') : Array.from({ length: ((to - from) * 60) / slot }, (_, i) => gone(c.day, from * 60 + i * slot) ? '' : `<button class="cx-ag-slot" data-slot-time="${from * 60 + i * slot}" style="top:${y(from * 60 + i * slot)}px;height:${(slot / 60) * hourPx}px" aria-label="${w.newItem}, ${c.label}, ${toHM(from * 60 + i * slot)}" tabindex="-1"></button>`).join('')}${blOn(c.day, rid).map(blockHtml).join('')}${evs.map((e) => evHtml(e, ln, c.rid === 'all' && resources.length > 1, rid, cap === 0)).join('')}${ghostHtml(c.day, rid)}${goneHtml(c.day)}${sameDay(c.day, today) && nowM > from * 60 && nowM < to * 60 ? `<div class="cx-ag-now" style="top:${y(nowM)}px" aria-hidden="true">${cols.findIndex((k) => sameDay(k.day, today)) === ci ? '<i></i>' : ''}</div>` : ''}</div>`; }).join('')}</div></div></div>`; };
|
|
661
|
+
// list = the phone's agenda: a solid row is sold, a dashed one is still free
|
|
662
|
+
// one cita as a row of the list (the day's list and the range's)
|
|
663
|
+
const evRow = (e) => `<button class="cx-ag-row" data-ev="${e.id}"${clash.has(e.id) ? ' data-conflict' : ''} data-tone="${toneOf(e)}" ${e.status ? `data-status="${e.status}"` : ''}${paint1(e)}"><span class="cx-ag-row-t">${ui.text(clk(e.start), 'strong')}${ui.text(`${e.dur} min`, 'caption')}</span>${e.photo ? `<span class="cn-avatar cx-ag-face cx-ag-rowface" style="background-image:url('${e.photo}')" aria-hidden="true"></span>` : ''}<span class="cx-vstack" style="gap:0;min-width:0">${ui.text(e.title, 'heading')}${ui.text(`${e.subtitle || ''}${resources.length > 1 ? ' · ' + resName(e.resource) : ''}${tagsOf(e) ? ' · ' + tagsOf(e) : ''}`, 'muted')}</span>${e.status && w.status[e.status] ? ui.badge(w.status[e.status], { tone: { info: 'info', live: 'wait', done: 'ok', bad: 'bad', ok: 'ok' }[toneOf(e)] || 'ok' }) : e.steps ? ui.badge(e.steps[e.at], { tone: e.tone === 'wait' ? 'wait' : e.tone === 'info' ? 'info' : 'ok' }) : ''}${e.price ? ui.text(fmtMoney(e.price), 'strong') : ''}</button>`;
|
|
664
|
+
// RANGE: the days picked in the calendar, one after another, each with its citas (a day with none says so)
|
|
665
|
+
const rangeList = () => { const [a, b] = rangeSel; const n = Math.min(62, Math.round((b - a) / 864e5) + 1); return `<div class="cx-ag-list cx-ag-range" role="list">${Array.from({ length: n }, (_, k) => { const d = addDays(a, k); const evs = evOn(d).sort((x, z) => toMin(x.start) - toMin(z.start)); return `<button type="button" class="cx-ag-rangeday" data-ag-day="${+d}" ${sameDay(d, today) ? 'data-today' : ''}>${ui.text(`${w.dayL[d.getDay()]} ${d.getDate()} ${w.mon[d.getMonth()]}`, 'strong')}${ui.text(isClosed(d) && !evs.length ? w.closed : evs.length ? w.countOf(evs.length) : w.noItems, 'caption')}</button>${evs.map(evRow).join('')}`; }).join('')}</div>`; };
|
|
407
666
|
const list = () => {
|
|
408
667
|
const rids = shown().map((r) => r.id), rows = [];
|
|
409
668
|
const evs = evOn(day).sort((a, b) => toMin(a.start) - toMin(b.start));
|
|
410
|
-
const busy = (rid, m) =>
|
|
669
|
+
const busy = (rid, m) => !roomFor(rid, day, m, slot) || blOn(day, rid).some((b) => m >= toMin(b.start) && m < toMin(b.start) + b.dur);
|
|
670
|
+
const freeAt = (rid, m) => !gone(day, m) && !closed.some((c) => sameDay(c, day)) && (freeList ? freeOf(rid, day).some(([a, b]) => m >= a && m + slot <= b) : availability(rid, day).some(([a, b]) => m >= a && m + slot <= b) && !busy(rid, m));
|
|
411
671
|
for (let m = from * 60; m < to * 60; m += slot) {
|
|
412
|
-
evs.filter((e) => toMin(e.start) >= m && toMin(e.start) < m + slot).forEach((e) => rows.push(
|
|
413
|
-
blOn(day).filter((b) => toMin(b.start) === m && (who === 'all' || b.resource === who || b.resource === 'all')).forEach((b) => rows.push(`<div class="cx-ag-row" data-kind="block"><span class="cx-ag-row-t">${ui.text(b.start, 'strong')}${ui.text(`${b.dur} min`, 'caption')}</span>${ui.text(`${b.reason ||
|
|
414
|
-
const
|
|
415
|
-
const key =
|
|
416
|
-
if (
|
|
672
|
+
evs.filter((e) => toMin(e.start) >= m && toMin(e.start) < m + slot).forEach((e) => rows.push(evRow(e)));
|
|
673
|
+
blOn(day).filter((b) => toMin(b.start) === m && (who === 'all' || b.resource === who || b.resource === 'all')).forEach((b) => rows.push(`<div class="cx-ag-row" data-kind="block"><span class="cx-ag-row-t">${ui.text(clk(b.start), 'strong')}${ui.text(`${b.dur} min`, 'caption')}</span>${ui.text(`${b.reason || w.blocked}${resources.length > 1 && b.resource !== 'all' ? ' · ' + resName(b.resource) : ''}`, 'muted')}</div>`));
|
|
674
|
+
const fr = rids.filter((rid) => bookable(rid) && freeAt(rid, m));
|
|
675
|
+
const key = fr.join(','), lastRow = rows.at(-1);
|
|
676
|
+
if (fr.length && !evs.some((e) => toMin(e.start) <= m && toMin(e.start) + e.dur > m && (who !== 'all' || rids.length === 1))) {
|
|
417
677
|
if (lastRow?.kind === 'free' && lastRow.key === key && lastRow.end === m) lastRow.end = m + slot;
|
|
418
|
-
else rows.push({ kind: 'free', key, start: m, end: m + slot, free });
|
|
678
|
+
else rows.push({ kind: 'free', key, start: m, end: m + slot, free: fr });
|
|
419
679
|
}
|
|
420
680
|
}
|
|
421
|
-
const html = rows.map((r) => (typeof r === 'string' ? r : `<button class="cx-ag-row" data-kind="free" data-free="${r.start}" data-rid="${r.free[0]}" aria-label="
|
|
422
|
-
return `<div class="cx-ag-list" role="list">${html || `<div class="cn-empty" style="padding:32px 12px"><div class="cn-empty-header"><div class="cn-empty-title"
|
|
681
|
+
const html = rows.map((r) => (typeof r === 'string' ? r : `<button class="cx-ag-row" data-kind="free" data-free="${r.start}" data-rid="${r.free[0]}" aria-label="${w.freeFromTo(clk(toHM(r.start)), clk(toHM(r.end)))}"><span class="cx-ag-row-t">${ui.text(clk(toHM(r.start)), 'strong')}${r.end - r.start > slot ? ui.text(`${w.until} ${clk(toHM(r.end))}`, 'caption') : ''}</span>${ui.text(`${w.free}${rids.length > 1 ? ` · ${r.free.map(resName).join(', ')}` : ''}`, 'muted')}<span class="cx-grow"></span>${ui.icon('plus')}</button>`)).join('');
|
|
682
|
+
return `<div class="cx-ag-list" role="list">${html || `<div class="cn-empty" style="padding:32px 12px"><div class="cn-empty-header"><div class="cn-empty-title">${isClosed(day) ? w.closed : w.noHours}</div></div></div>`}</div>`;
|
|
423
683
|
};
|
|
684
|
+
// LOADING keeps the very layout it is about to show: the same toolbar, strip, column heads, hours and height,
|
|
685
|
+
// with grey citas where the real ones will land. Nothing jumps when the data arrives, nothing shrinks while it loads.
|
|
686
|
+
const SKEL = [[0.5, 1], [2, 1.5], [4.5, 1], [6, 2], [8, 1]];
|
|
687
|
+
const skelCol = (ci) => SKEL.filter(([h], i) => (i + ci) % 3 !== 2 && h < to - from).map(([h, len]) => `<span class="cx-ag-skev skel" style="top:${h * hourPx + 2}px;height:${Math.min(len, to - from - h) * hourPx - 4}px"></span>`).join('');
|
|
688
|
+
const skelList = () => `<div class="cx-ag-list" aria-hidden="true">${Array.from({ length: Math.max(6, Math.round((to - from) * 60 / slot / 2)) }, (_, i) => `<div class="cx-ag-row cx-ag-skrow"><span class="cx-ag-row-t"><s class="skel" style="width:38px"></s><s class="skel" style="width:28px"></s></span><span class="cx-vstack" style="gap:6px;flex:1"><s class="skel" style="width:${[46, 38, 54, 42][i % 4]}%"></s><s class="skel" style="width:${[30, 26, 34, 22][i % 4]}%"></s></span></div>`).join('')}</div>`;
|
|
689
|
+
// the top band keeps what it had (the «Ahora» card, the queue) as grey shapes, so its height holds too
|
|
690
|
+
let hadNow = false, hadQueue = false;
|
|
691
|
+
const skelTop = () => `${hadNow && sameDay(day, today) ? '<div class="cx-ag-nowcard cx-ag-sknow" aria-hidden="true"><span class="cx-vstack" style="gap:0;flex:1">' + [[90, 'caption'], [160, 'heading'], [120, 'muted']].map(([wd, v]) => ui.text(`<s class="skel cx-ag-skline" style="width:${wd}px"></s>`, v)).join('') + '</span></div>' : ''}${hadQueue ? '<div class="cx-ag-queue" aria-hidden="true"><s class="skel" style="width:24px;height:24px;border-radius:50%"></s><s class="skel" style="width:24px;height:24px;border-radius:50%;margin-left:4px"></s><s class="skel" style="width:70px;margin-left:8px"></s></div>' : ''}`;
|
|
692
|
+
// the days shown, so an app can load exactly those (it is told once per change)
|
|
693
|
+
let lastRange = '';
|
|
694
|
+
// every toolbar command in one place: the toolbar's own buttons and an app's phone bar (api.run) go through here.
|
|
695
|
+
// While loading, nothing that would ask the server runs.
|
|
696
|
+
function run(k) {
|
|
697
|
+
if (loading || !k) return;
|
|
698
|
+
if (k.startsWith('view:')) { const v = k.slice(5); if (['day', 'week', 'list'].includes(v)) { view = v; picked = true; paint(); } return; }
|
|
699
|
+
if (k === 'today') { day = new Date(today); if (view === 'range') { view = 'day'; rangeSel = null; } }
|
|
700
|
+
else if ((k === 'prev' || k === 'next') && eff() === 'range') { const len = Math.round((rangeSel[1] - rangeSel[0]) / 864e5) + 1, s2 = (k === 'next' ? 1 : -1) * len; rangeSel = [addDays(rangeSel[0], s2), addDays(rangeSel[1], s2)]; day = new Date(rangeSel[0]); }
|
|
701
|
+
else if (k === 'prev' || k === 'next') day = addDays(day, (k === 'next' ? 1 : -1) * (view === 'week' && !narrow ? 7 : 1));
|
|
702
|
+
else if (k === 'new') return create({ resource: pick(), day, start: null });
|
|
703
|
+
else if (k === 'block') return openBlock();
|
|
704
|
+
else return onAction(k);
|
|
705
|
+
paint();
|
|
706
|
+
}
|
|
707
|
+
const tellRange = () => { const v = eff(); const a = v === 'range' ? rangeSel[0] : v === 'week' ? weekDays()[0] : day, b = v === 'range' ? rangeSel[1] : v === 'week' ? weekDays()[6] : day; const key = `${v}|${+a}|${+b}`; if (key === lastRange) return; lastRange = key; onRange({ from: a, to: b, view: v }); };
|
|
424
708
|
const paint = () => {
|
|
425
709
|
if (typeof drag !== 'undefined' && drag) { const d = drag; drag = null; clearTimeout(d.timer); cancelAnimationFrame(scrollRaf); d.ghost?.remove(); }
|
|
426
|
-
const v =
|
|
427
|
-
const cols = v === 'week' ? weekDays().map((d) => ({ day: d, rid: who === 'all' ? 'all' : who, head: `${
|
|
428
|
-
|
|
710
|
+
const v = eff(); // the phone keeps the calendar: day grid or list, the week needs a wide screen
|
|
711
|
+
const cols = v === 'week' ? weekDays().map((d) => ({ day: d, rid: who === 'all' || who.startsWith('kind:') ? 'all' : who, head: `${w.dayS[d.getDay()]} ${d.getDate()}`, label: `${w.dayL[d.getDay()]} ${d.getDate()}`, today: sameDay(d, today), closed: isClosed(d) })) : dayCols().map((r) => ({ day, rid: r.id, head: `${r.photo ? `<span class="cn-avatar cx-ag-face" style="background-image:url('${r.photo}')"></span>` : ui.avatar(r.name, { size: 'sm' })}<span class="cx-ag-colname"><b>${r.name}</b>${roleOf(r) ? `<small>${roleOf(r)}</small>` : ''}</span>`, label: r.name }));
|
|
712
|
+
clash = loading ? new Set() : findClashes();
|
|
713
|
+
const crossed = loading ? 0 : evOn(day).filter((e) => clash.has(e.id)).length;
|
|
714
|
+
const top = loading ? skelTop() : nowCard() + queueLine();
|
|
715
|
+
const alert = crossed ? ui.alert({ icon: 'info', title: w.clash(crossed), description: w.clashHow, action: ui.button({ label: w.clashSee, size: 'sm', variant: 'outline', attrs: 'data-ag-clash' }), tone: 'bad', attrs: 'data-ag-alert' }) : '';
|
|
716
|
+
if (!loading) { hadNow = top.includes('cx-ag-nowcard'); hadQueue = top.includes('cx-ag-queue'); }
|
|
717
|
+
root.innerHTML = toolbar() + `<div class="cx-ag-frame"${loading ? ` aria-busy="true" aria-label="${w.loading}"` : ''}><div class="cx-ag-top">${people()}${strip()}${top}${alert}</div>${v === 'range' ? `<div class="cx-ag-listwrap">${loading ? skelList() : rangeList()}</div>` : v === 'list' ? `<div class="cx-ag-listwrap">${loading ? skelList() : list()}</div>` : grid(cols)}</div>`;
|
|
718
|
+
root.toggleAttribute('data-loading', !!loading);
|
|
429
719
|
$$('.cx-tabs', root).forEach(initTabs);
|
|
430
720
|
const now = $('.cx-ag-now', root); if (now) $('.cx-ag-scroll', root).scrollTop = Math.max(0, parseFloat(now.style.top) - 120);
|
|
721
|
+
tellRange();
|
|
431
722
|
};
|
|
432
723
|
// drag to move · drag the bottom edge to resize (15 min). A ghost shows where it lands and turns red where the
|
|
433
724
|
// time is not free; the real event never moves until a valid drop. Mouse starts after 6 px, touch after a long press.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
725
|
+
// With server free hours, «free» means inside one of them or inside the cita's own time.
|
|
726
|
+
// the past and the closed days are never free, whatever the hours say
|
|
727
|
+
const gone = (d, st) => { const k = new Date(d); k.setHours(0, 0, 0, 0); return k < today || (sameDay(k, today) && st < nowMin()); };
|
|
728
|
+
const fits = (rid, d, st, dur, except) => {
|
|
729
|
+
if (gone(d, st) || closed.some((c) => sameDay(c, d))) return false;
|
|
730
|
+
if (freeList) { const own = events.find((x) => x.id === except); const wins = freeOf(rid, d); if (own && own.resource === rid && sameDay(own.day, d)) wins.push([toMin(own.start), toMin(own.start) + own.dur]); wins.sort((a, b) => a[0] - b[0]); let cur = st; for (const [a, b] of wins) { if (a <= cur && b > cur) cur = b; } return cur >= st + dur && !blOn(d, rid).some((b) => st < toMin(b.start) + b.dur && st + dur > toMin(b.start)); }
|
|
731
|
+
return availability(rid, d).some(([a, b]) => st >= a && st + dur <= b) && roomFor(rid, d, st, dur, except) && !blOn(d, rid).some((b) => st < toMin(b.start) + b.dur && st + dur > toMin(b.start));
|
|
732
|
+
};
|
|
437
733
|
let drag = null, justDragged = false, scrollRaf = 0;
|
|
438
734
|
const arm = () => {
|
|
439
735
|
const d = drag; d.armed = true; clearTimeout(d.timer);
|
|
@@ -446,16 +742,18 @@ let wheelsNow = [];
|
|
|
446
742
|
const d = drag; if (!d?.armed) return; d.lastX = cx; d.lastY = cy;
|
|
447
743
|
if (d.resize) { const top = d.ev.getBoundingClientRect().top; d.dur = Math.max(15, Math.min(to * 60 - toMin(d.it.start), Math.round(((cy - top) / hourPx) * 60 / 15) * 15)); d.target = { day: d.it.day, resource: d.it.resource, start: d.it.start }; }
|
|
448
744
|
else {
|
|
449
|
-
|
|
745
|
+
let col = document.elementsFromPoint(cx, cy).find((x) => x.classList?.contains('cx-ag-col')); if (!col) return;
|
|
746
|
+
// moveAcross "off": over another person's column the cita stays in its own (only its time changes)
|
|
747
|
+
if (moveAcross === 'off' && col.dataset.rid !== 'all' && col.dataset.rid !== d.it.resource) col = root.querySelector(`.cx-ag-col[data-rid="${d.it.resource}"][data-day="${col.dataset.day}"]`) || col;
|
|
450
748
|
const cr = col.getBoundingClientRect(), m = Math.round((((cy - d.dy - cr.top) / hourPx) * 60 + from * 60) / 15) * 15;
|
|
451
|
-
d.target = { day: new Date(+col.dataset.day), resource: col.dataset.rid === 'all' ? d.it.resource : col.dataset.rid, start: toHM(Math.max(from * 60, Math.min(to * 60 - d.it.dur, m))) };
|
|
749
|
+
d.target = { day: new Date(+col.dataset.day), resource: col.dataset.rid === 'all' || moveAcross === 'off' ? d.it.resource : col.dataset.rid, start: toHM(Math.max(from * 60, Math.min(to * 60 - d.it.dur, m))) };
|
|
452
750
|
if (d.ghost.parentElement !== col) { col.appendChild(d.ghost); d.ghost.style.left = '3px'; d.ghost.style.width = 'calc(100% - 6px)'; }
|
|
453
751
|
}
|
|
454
752
|
const st = toMin(d.target.start), dur = d.resize ? d.dur : d.it.dur;
|
|
455
|
-
d.ok =
|
|
753
|
+
d.ok = fits(d.target.resource, d.target.day, st, dur, d.it.id) && occ(d.it).filter((x) => x !== d.it.resource).every((x) => roomFor(x, d.target.day, st, dur, d.it.id));
|
|
456
754
|
d.ghost.style.top = y(st) + 'px'; d.ghost.style.height = (dur / 60) * hourPx - 3 + 'px';
|
|
457
755
|
d.ghost.toggleAttribute('data-invalid', !d.ok);
|
|
458
|
-
$('.cx-ag-ghost-t', d.ghost).textContent = `${d.target.start}–${toHM(st + dur)}${resources.length > 1 ? ' · ' + resName(d.target.resource) : ''}${d.ok ? '' : ' ·
|
|
756
|
+
$('.cx-ag-ghost-t', d.ghost).textContent = `${d.target.start}–${toHM(st + dur)}${resources.length > 1 ? ' · ' + resName(d.target.resource) : ''}${d.ok ? '' : ' · ' + w.notFree}`;
|
|
459
757
|
};
|
|
460
758
|
const stop = (commit) => {
|
|
461
759
|
const d = drag; drag = null; cancelAnimationFrame(scrollRaf); if (!d) return; clearTimeout(d.timer);
|
|
@@ -464,21 +762,23 @@ let wheelsNow = [];
|
|
|
464
762
|
delete d.ev.dataset.dragging;
|
|
465
763
|
const changed = d.target && (d.resize ? d.dur !== d.it.dur : d.target.start !== d.it.start || d.target.resource !== d.it.resource || !sameDay(d.target.day, d.it.day));
|
|
466
764
|
if (commit && d.ok && changed) {
|
|
765
|
+
// drawn where it landed at once; an app confirms with update() (or puts it back by sending the old data)
|
|
467
766
|
if (d.resize) d.it.dur = d.dur; else Object.assign(d.it, d.target);
|
|
468
|
-
paint(); onChange(events);
|
|
469
|
-
island('done', d.resize ?
|
|
767
|
+
paint(); onChange(events); onMove?.({ id: d.it.id, resource: d.it.resource, day: d.it.day, start: d.it.start, dur: d.it.dur });
|
|
768
|
+
if (!appMode) island('done', d.resize ? w.nowLasts(d.it.dur) : w.movedTo(d.it.start, resources.length > 1 ? resName(d.it.resource) : ''), 2000);
|
|
470
769
|
$(`.cx-ag-ev[data-ev="${d.it.id}"]`, root)?.animate([{ boxShadow: '0 0 0 2px var(--selected)' }, { boxShadow: '0 0 0 0 transparent' }], { duration: 700 });
|
|
471
770
|
return;
|
|
472
771
|
}
|
|
473
|
-
if (commit && !d.ok && changed) island('error',
|
|
772
|
+
if (commit && !d.ok && changed) island('error', w.notFreeLong, 2000);
|
|
474
773
|
// back to where it was: the ghost flies home, then goes
|
|
475
774
|
const home = d.ev.getBoundingClientRect(), g = d.ghost.getBoundingClientRect();
|
|
476
775
|
d.ghost.animate([{ transform: 'none', opacity: 1 }, { transform: `translate(${home.left - g.left}px, ${home.top - g.top}px)`, opacity: 0.4 }], { duration: 220, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'forwards' }).finished.then(() => d.ghost.remove());
|
|
477
776
|
};
|
|
478
777
|
root.addEventListener('pointerdown', (e) => {
|
|
778
|
+
if (!dragOn || loading) return;
|
|
479
779
|
const ev = e.target.closest('.cx-ag-ev'); if (!ev || e.button > 0 || drag) return;
|
|
480
|
-
const it = events.find((x) => x.id === ev.dataset.ev), r = ev.getBoundingClientRect();
|
|
481
|
-
drag = { ev, it, resize: !!e.target.closest('.cx-ag-grip'), dy: e.clientY - r.top, x0: e.clientX, y0: e.clientY, lastX: e.clientX, lastY: e.clientY, armed: false, touch: e.pointerType === 'touch', dur: it.dur, pid: e.pointerId };
|
|
780
|
+
const it = events.find((x) => x.id === ev.dataset.ev), r = ev.getBoundingClientRect(); if (!it || ['done', 'noshow', 'started'].includes(it.status)) return; // what happened, or is happening, stays where it is
|
|
781
|
+
drag = { ev, it, resize: resizeMode !== 'off' && !!e.target.closest('.cx-ag-grip'), dy: e.clientY - r.top, x0: e.clientX, y0: e.clientY, lastX: e.clientX, lastY: e.clientY, armed: false, touch: e.pointerType === 'touch', dur: it.dur, pid: e.pointerId };
|
|
482
782
|
if (drag.touch) drag.timer = setTimeout(() => { if (drag && !drag.armed) { arm(); move(drag.lastX, drag.lastY); } }, 320);
|
|
483
783
|
});
|
|
484
784
|
root.addEventListener('pointermove', (e) => {
|
|
@@ -492,85 +792,203 @@ let wheelsNow = [];
|
|
|
492
792
|
root.addEventListener('pointerup', (e) => { if (drag && e.pointerId === drag.pid) stop(true); });
|
|
493
793
|
root.addEventListener('pointercancel', () => stop(false));
|
|
494
794
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && drag?.armed) { e.preventDefault(); stop(false); } });
|
|
795
|
+
// DRAG TO PAN: when the agenda is smaller than its day (a narrow window, many people, the hours below the fold), a
|
|
796
|
+
// mouse drag on the empty grid moves it, in both directions, like a finger does on a phone (the finger keeps the
|
|
797
|
+
// native scroll). It never changes the day. Citas keep their own drag; the end of a pan is not a tap on an hour.
|
|
798
|
+
let pan = null, justSwiped = false;
|
|
799
|
+
root.addEventListener('pointerdown', (e) => {
|
|
800
|
+
if (e.pointerType !== 'mouse' || e.button > 0 || drag || e.target.closest('.cx-ag-ev, .cx-ag-nowcard, .cx-ag-bar, input')) return;
|
|
801
|
+
const el = e.target.closest('.cx-ag-scroll, .cx-ag-who'); if (!el) return;
|
|
802
|
+
if (el.scrollWidth <= el.clientWidth + 1 && el.scrollHeight <= el.clientHeight + 1) return; // nothing hidden: nothing to pan
|
|
803
|
+
pan = { el, x0: e.clientX, y0: e.clientY, sl: el.scrollLeft, st: el.scrollTop, on: false, pid: e.pointerId };
|
|
804
|
+
});
|
|
805
|
+
root.addEventListener('pointermove', (e) => {
|
|
806
|
+
const pn = pan; if (!pn || e.pointerId !== pn.pid || drag?.armed) return;
|
|
807
|
+
const dx = e.clientX - pn.x0, dy = e.clientY - pn.y0;
|
|
808
|
+
if (!pn.on) { if (Math.hypot(dx, dy) < 6) return; pn.on = true; pn.el.dataset.panning = ''; try { pn.el.setPointerCapture(e.pointerId); } catch { /* gone */ } }
|
|
809
|
+
pn.el.scrollLeft = pn.sl - dx; pn.el.scrollTop = pn.st - dy;
|
|
810
|
+
});
|
|
811
|
+
const endPan = () => { const pn = pan; pan = null; if (!pn?.on) return; delete pn.el.dataset.panning; justSwiped = true; setTimeout(() => { justSwiped = false; }, 0); };
|
|
812
|
+
root.addEventListener('pointerup', (e) => { if (pan && e.pointerId === pan.pid) endPan(); });
|
|
813
|
+
root.addEventListener('pointercancel', endPan);
|
|
495
814
|
root.addEventListener('click', (e) => {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
const
|
|
815
|
+
if (justSwiped) { e.preventDefault(); return; } // the end of a pan is not a tap on an hour
|
|
816
|
+
if (loading && !e.target.closest('[data-who]')) return; // only the person filter works while loading: it asks nothing
|
|
817
|
+
const evb = e.target.closest('.cx-ag-ev, .cx-ag-nowcard, .cx-ag-row[data-ev]'); if (evb) { if (!justDragged) openEvent(events.find((x) => x.id === evb.dataset.ev)); return; }
|
|
818
|
+
const a = e.target.closest('[data-ag]'); if (a) { run(a.dataset.ag); return; }
|
|
819
|
+
if (e.target.closest('[data-ag-date]')) { openDate(); return; }
|
|
820
|
+
if (e.target.closest('[data-ag-clash]')) { seeClash(); return; }
|
|
821
|
+
const v = e.target.closest('[data-ag-view]'); if (v) { view = v.dataset.agView; rangeSel = null; picked = true; paint(); return; }
|
|
822
|
+
const wh = e.target.closest('[data-who]'); if (wh) { who = wh.dataset.who; paint(); return; }
|
|
823
|
+
const dd = e.target.closest('[data-ag-day]'); if (dd) { day = new Date(+dd.dataset.agDay); if (view === 'range') { view = 'day'; rangeSel = null; } if (view === 'week' && !narrow) view = 'day'; paint(); return; }
|
|
824
|
+
const s = e.target.closest('.cx-ag-slot'); if (s) { const col = s.closest('.cx-ag-col'); const rid = col.dataset.rid === 'all' ? pick() : col.dataset.rid, d = new Date(+col.dataset.day), st = +s.dataset.slotTime;
|
|
825
|
+
if (!fits(rid, d, st, slot)) { island('error', capOf(rid) !== 1 && availability(rid, d).some(([a, b]) => st >= a && st + slot <= b) ? w.full : w.notFreeLong, 2000); return; } // a full agenda says so instead of opening a sheet
|
|
826
|
+
create({ resource: rid, day: d, start: toHM(st) }); return; }
|
|
827
|
+
const f = e.target.closest('[data-free]'); if (f) { create({ resource: f.dataset.rid, day: f.dataset.day ? new Date(+f.dataset.day) : day, start: toHM(+f.dataset.free) }); return; }
|
|
504
828
|
});
|
|
505
829
|
root.addEventListener('keydown', (e) => { const ev = e.target.closest('.cx-ag-ev'); if (ev && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); openEvent(events.find((x) => x.id === ev.dataset.ev)); } });
|
|
506
|
-
|
|
830
|
+
// A width change repaints on the NEXT frame: repainting inside the observer changes the agenda's own size in the frame
|
|
831
|
+
// it is being measured, and the browser reports a resize loop.
|
|
832
|
+
// the time keeps moving: every half minute the now line, its time and the veil over what went by follow the clock
|
|
833
|
+
const tick = setInterval(() => {
|
|
834
|
+
if (!root.isConnected) { clearInterval(tick); return; }
|
|
835
|
+
if (drag || loading) return; const m = nowMin(); if (m <= from * 60 || m >= to * 60) return;
|
|
836
|
+
$$('.cx-ag-now', root).forEach((x) => { x.style.top = y(m) + 'px'; }); const t = $('.cx-ag-nowtime', root); if (t) { t.style.top = y(m) + 'px'; t.textContent = clk(toHM(m)); }
|
|
837
|
+
$$('.cx-ag-col', root).forEach((c) => { if (!sameDay(new Date(+c.dataset.day), today)) return; const g = $('.cx-ag-gone', c); if (g) g.style.height = y(m) + 'px'; });
|
|
838
|
+
}, 30000);
|
|
839
|
+
new ResizeObserver(() => requestAnimationFrame(() => { const n = root.clientWidth < 700; if (n !== narrow) { narrow = n; paint(); } })).observe(root);
|
|
507
840
|
|
|
508
|
-
// ── the event sheet: detail · lifecycle · payment · move (second step) · cancel (second step)
|
|
841
|
+
// ── the event sheet (showcase): detail · lifecycle · payment · move (second step) · cancel (second step).
|
|
842
|
+
// In app mode a tapped cita goes to onOpen and the app opens its own sheet. ──
|
|
509
843
|
let open1 = null, step = 'detail', moveTo = null;
|
|
510
|
-
const openEvent = (ev) => { open1 = ev; step = 'detail'; moveTo = null;
|
|
844
|
+
const openEvent = (ev) => { if (!ev) return; if (appMode && onOpen) return onOpen(ev); open1 = ev; step = 'detail'; moveTo = null; sheetOf('agEvent'); };
|
|
511
845
|
const sheet = () => {
|
|
512
846
|
const e = open1;
|
|
513
847
|
if (step === 'move') {
|
|
514
|
-
const days = Array.from({ length: 6 }, (_, i) => addDays(today, i)).filter((d) =>
|
|
515
|
-
const md = moveTo?.day || e.day,
|
|
516
|
-
const nowMin = sameDay(md,
|
|
517
|
-
for (let m = from * 60; m + e.dur <= to * 60; m += slot) {
|
|
518
|
-
$('#mdl-title').textContent =
|
|
519
|
-
body.innerHTML = `<div class="cx-vstack" style="gap:14px">${ui.text(
|
|
520
|
-
foot.innerHTML = ui.button({ label: `${ui.icon('left')}
|
|
848
|
+
const days = Array.from({ length: 6 }, (_, i) => addDays(today, i)).filter((d) => works(e.resource, d));
|
|
849
|
+
const md = moveTo?.day || e.day, fr = [];
|
|
850
|
+
const nowMin = sameDay(md, today) ? nowMin() : -1;
|
|
851
|
+
for (let m = from * 60; m + e.dur <= to * 60; m += slot) { if (m > nowMin && fits(e.resource, md, m, e.dur, e.id)) fr.push(toHM(m)); }
|
|
852
|
+
$('#mdl-title').textContent = w.move; $('#mdl-desc').textContent = `${e.title} · ${e.dur} min${resources.length > 1 ? ` · ${resName(e.resource)}` : ''}`;
|
|
853
|
+
body.innerHTML = `<div class="cx-vstack" style="gap:14px">${ui.text(w.day, 'label')}<div class="cx-wz-days" style="grid-template-columns:repeat(${days.length},1fr)">${days.map((d) => `<button class="cn-button cn-button-variant-outline cx-size-tile cx-day" data-mv-day="${+d}" aria-pressed="${sameDay(d, md)}"><small>${sameDay(d, today) ? w.todayShort : w.dayS[d.getDay()]}</small><b>${d.getDate()}</b><i></i></button>`).join('')}</div>${ui.text(w.freeSlot, 'label')}${fr.length ? `<div class="cx-hour-grid">${fr.map((h) => (sameDay(md, e.day) && h === e.start ? ui.button({ label: h, size: 'sm', attrs: `disabled title="${w.current}" aria-label="${h}, ${w.current}"` }) : ui.button({ label: h, size: 'sm', attrs: `aria-pressed="${moveTo?.start === h}" data-mv-h="${h}"` }))).join('')}</div>` : ui.text(w.noneFree, 'muted', 'p')}</div>`;
|
|
854
|
+
foot.innerHTML = ui.button({ label: `${ui.icon('left')}${w.back}`, size: 'default', attrs: 'data-ev-back', cls: 'cx-exit' }) + ui.button({ label: w.saveMove, variant: 'default', size: 'default', attrs: `data-ev-move ${moveTo?.start ? '' : 'disabled'}`, cls: 'cx-go' });
|
|
521
855
|
return;
|
|
522
856
|
}
|
|
523
857
|
if (step === 'cancel') {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
foot.innerHTML = ui.button({ label: `${ui.icon('left')}Volver`, size: 'default', attrs: 'data-ev-back', cls: 'cx-exit' }) + ui.button({ label: `Cancelar la ${nouns.item}`, variant: 'destructive', size: 'default', attrs: 'data-ev-cancel-go', cls: 'cx-go' });
|
|
858
|
+
$('#mdl-title').textContent = w.cancelOf; $('#mdl-desc').textContent = `${e.title} · ${e.start}`;
|
|
859
|
+
body.innerHTML = `<div class="cx-vstack" role="radiogroup" aria-label="${w.reasonLabel}" style="gap:8px">${ui.text(w.why, 'label')}${w.reasons.map((r, i) => ui.choice({ title: r, checked: i === 0, attrs: `data-why="${r}"` })).join('')}</div>${ui.text(w.tellClient, 'caption', 'p')}`;
|
|
860
|
+
foot.innerHTML = ui.button({ label: `${ui.icon('left')}${w.back}`, size: 'default', attrs: 'data-ev-back', cls: 'cx-exit' }) + ui.button({ label: w.cancelOf, variant: 'destructive', size: 'default', attrs: 'data-ev-cancel-go', cls: 'cx-go' });
|
|
528
861
|
return;
|
|
529
862
|
}
|
|
530
863
|
const last = e.steps && e.at >= e.steps.length - 1, next = e.steps && !last ? e.steps[e.at + 1] : null;
|
|
531
|
-
$('#mdl-title').textContent = e.title; $('#mdl-desc').textContent = `${
|
|
864
|
+
$('#mdl-title').textContent = e.title; $('#mdl-desc').textContent = `${w.dayL[e.day.getDay()]} ${e.day.getDate()} · ${e.start} · ${e.dur} min${resources.length > 1 ? ` · ${resName(e.resource)}` : ''}`;
|
|
532
865
|
body.innerHTML = `${e.steps ? ui.stepper(e.steps, e.at) : ''}<div class="cn-item-group" style="gap:0">${[
|
|
533
|
-
ui.item({ media: ui.itemMedia(ui.icon('scissors')), title: e.subtitle, description: e.price ?
|
|
534
|
-
e.payment ? ui.item({ media: ui.itemMedia(ui.icon('card')), title: e.payment.label, description: e.payment.detail, actions: ui.badge(e.payment.state, { tone: e.payment.tone }), footer: e.payment.receipt ? `<div class="cx-vstack" style="gap:8px">${e.payment.seen ? `<span class="cx-rcpt-mini" style="background:linear-gradient(135deg,#e8e3d6,#cfc6b0)" role="img" aria-label="
|
|
535
|
-
e.contact ? ui.item({ media: ui.itemMedia(ui.icon('i-mail')), title: e.contact.label, description: e.contact.handle, actions: ui.button({ label:
|
|
536
|
-
].filter(Boolean).join(ui.itemSeparator())}</div><div class="cx-hstack">${ui.button({ label: `${ui.icon('clock')}
|
|
537
|
-
foot.innerHTML = ui.button({ label:
|
|
866
|
+
ui.item({ media: ui.itemMedia(ui.icon('scissors')), title: e.subtitle, description: e.price ? fmtMoney(e.price) : '' }),
|
|
867
|
+
e.payment ? ui.item({ media: ui.itemMedia(ui.icon('card')), title: e.payment.label, description: e.payment.detail, actions: ui.badge(e.payment.state, { tone: e.payment.tone }), footer: e.payment.receipt ? `<div class="cx-vstack" style="gap:8px">${e.payment.seen ? `<span class="cx-rcpt-mini" style="background:linear-gradient(135deg,#e8e3d6,#cfc6b0)" role="img" aria-label="${w.proof}"></span>` : ''}<div class="cx-hstack">${ui.button({ label: `${ui.icon('image')}${e.payment.seen ? w.proofHide : w.proofShow}`, size: 'sm', attrs: 'data-ev-proof' })}${e.payment.tone === 'info' ? ui.button({ label: w.paidBtn, size: 'sm', variant: 'default', attrs: `data-ev-paid ${e.payment.seen ? '' : `disabled title="${w.openFirst}"`}` }) : ''}</div></div>` : '' }) : '',
|
|
868
|
+
e.contact ? ui.item({ media: ui.itemMedia(ui.icon('i-mail')), title: e.contact.label, description: e.contact.handle, actions: ui.button({ label: w.write, size: 'xs', attrs: 'data-ev-write' }) }) : '',
|
|
869
|
+
].filter(Boolean).join(ui.itemSeparator())}</div><div class="cx-hstack">${ui.button({ label: `${ui.icon('clock')}${w.move}`, size: 'sm', variant: 'ghost', attrs: 'data-ev-step="move"' })}${ui.button({ label: `${ui.icon('caloff')}${w.cancel}`, size: 'sm', variant: 'ghost', attrs: 'data-ev-step="cancel"' })}</div>`;
|
|
870
|
+
foot.innerHTML = ui.button({ label: w.close, size: 'default', attrs: 'data-dismiss', cls: 'cx-exit' }) + (next ? ui.button({ label: `${e.actions?.[e.at] || next}${ui.icon('right')}`, variant: 'default', size: 'default', attrs: `data-ev-next ${e.payment && e.payment.tone === 'info' && e.at === 0 ? `disabled title="${w.confirmFirst}"` : ''}`, cls: 'cx-go' }) : ui.button({ label: `${ui.icon('check')}${w.finished}`, size: 'default', attrs: 'disabled', cls: 'cx-go' }));
|
|
538
871
|
};
|
|
539
|
-
CONTENT
|
|
872
|
+
CONTENT[K('agEvent')] = sheet;
|
|
540
873
|
const reSheet = () => easeHeight(mdl, sheet);
|
|
541
874
|
document.addEventListener('click', (e) => {
|
|
542
|
-
if (!open1 || !mdl.contains(e.target)) return;
|
|
875
|
+
if (agOwner !== me || !open1 || !mdl.contains(e.target)) return;
|
|
543
876
|
const st = e.target.closest('[data-ev-step]'); if (st) { step = st.dataset.evStep; moveTo = null; return reSheet(); }
|
|
544
877
|
if (e.target.closest('[data-ev-back]')) { step = 'detail'; return reSheet(); }
|
|
545
878
|
if (e.target.closest('[data-ev-proof]')) { open1.payment.seen = !open1.payment.seen; return reSheet(); }
|
|
546
|
-
if (e.target.closest('[data-ev-paid]')) { Object.assign(open1.payment, { state:
|
|
879
|
+
if (e.target.closest('[data-ev-paid]')) { Object.assign(open1.payment, { state: w.paid, tone: 'ok', receipt: false }); open1.tone = 'ok'; if (open1.status === 'awaiting_payment') open1.status = 'confirmed'; paint(); return reSheet(); }
|
|
547
880
|
if (e.target.closest('[data-ev-next]')) { open1.at++; if (open1.at >= open1.steps.length - 1) open1.tone = 'ok'; paint(); onChange(events); return reSheet(); }
|
|
548
881
|
const md = e.target.closest('[data-mv-day]'); if (md) { moveTo = { day: new Date(+md.dataset.mvDay), start: null }; return reSheet(); }
|
|
549
882
|
const mh = e.target.closest('[data-mv-h]'); if (mh) { moveTo = { day: moveTo?.day || open1.day, start: mh.dataset.mvH }; return sheet(); }
|
|
550
|
-
if (e.target.closest('[data-ev-move]')) { Object.assign(open1, moveTo); day = new Date(moveTo.day); paint(); onChange(events); closeModal(); island('done',
|
|
883
|
+
if (e.target.closest('[data-ev-move]')) { Object.assign(open1, moveTo); day = new Date(moveTo.day); paint(); onChange(events); closeModal(); island('done', w.movedDay(moveTo.day.getDate(), moveTo.start), 2200); return; }
|
|
551
884
|
const why = e.target.closest('[data-why]'); if (why) { $$('[data-why]', body).forEach((x) => x.setAttribute('aria-checked', x === why)); return; }
|
|
552
|
-
if (e.target.closest('[data-ev-cancel-go]')) { open1.status = 'cancelled'; paint(); onChange(events); closeModal(); island('done',
|
|
553
|
-
if (e.target.closest('[data-ev-write]')) island('info',
|
|
885
|
+
if (e.target.closest('[data-ev-cancel-go]')) { open1.status = 'cancelled'; paint(); onChange(events); closeModal(); island('done', w.cancelled, 2400); return; }
|
|
886
|
+
if (e.target.closest('[data-ev-write]')) island('info', w.opening(open1.contact.label), 1800);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
// ── a new appointment: the page's own form (onCreate), or, with createSheet, a short one built in: the service, the
|
|
890
|
+
// client and the time, and it lands on the agenda at once (a showcase agenda has no app behind it to open) ──
|
|
891
|
+
let draft = null;
|
|
892
|
+
const create = (x) => { if (!createSheet) return onCreate(x); draft = { ...x, start: x.start || toHM(from * 60 + 60) }; sheetOf('agNew'); };
|
|
893
|
+
const svcList = () => (services.length ? services : [{ name: w.It, dur: slot }]);
|
|
894
|
+
CONTENT[K('agNew')] = () => {
|
|
895
|
+
$('#mdl-title').textContent = w.newItem; $('#mdl-desc').textContent = w.newDesc;
|
|
896
|
+
body.innerHTML = `<div class="cx-vstack" style="gap:16px">`
|
|
897
|
+
+ ui.field({ id: 'nw-s', label: w.service, control: `<button type="button" class="cn-button cn-button-variant-outline cn-button-size-default cx-sel-btn cx-fill" id="nw-s" data-v="0"></button>` })
|
|
898
|
+
+ ui.field({ id: 'nw-c', label: w.client, control: ui.input({ id: 'nw-c', placeholder: w.clientPh, attrs: 'autocomplete="off"' }) })
|
|
899
|
+
+ `<div class="cx-hstack" style="gap:12px;align-items:end">`
|
|
900
|
+
+ ui.field({ id: 'nw-t', label: w.hour, control: kit.timeButton(draft.start, `id="nw-t" data-nwt="${w.hour}" style="width:100%;justify-content:flex-start"`) })
|
|
901
|
+
+ (resources.length > 1 ? ui.field({ id: 'nw-r', label: It(w.resource), control: `<button type="button" class="cn-button cn-button-variant-outline cn-button-size-default cx-sel-btn cx-fill" id="nw-r" data-v="${draft.resource}"></button>` }) : '')
|
|
902
|
+
+ `</div></div>`;
|
|
903
|
+
foot.innerHTML = ui.button({ label: w.cancel, size: 'default', attrs: 'data-dismiss', cls: 'cx-exit' }) + ui.button({ label: `${ui.icon('check')}${w.schedule}`, variant: 'default', size: 'default', attrs: 'data-nw-go', cls: 'cx-go' });
|
|
904
|
+
kit.selectMenu($('#nw-s', body), { label: w.service, value: '0', options: svcList().map((x, i) => ({ value: String(i), label: x.dur ? `${x.name} · ${x.dur} min` : x.name })), onChange: (v) => { $('#nw-s', body).dataset.v = v; } });
|
|
905
|
+
const nr = $('#nw-r', body); if (nr) kit.selectMenu(nr, { label: w.resource, value: draft.resource, options: resources.map((r) => ({ value: r.id, label: r.name })), onChange: (v) => { nr.dataset.v = v; } });
|
|
906
|
+
};
|
|
907
|
+
body.addEventListener('click', async (e) => { const b = e.target.closest('[data-nwt]'); if (agOwner !== me || !b || !draft) return; const v = await kit.pickTime({ value: b.querySelector('span').textContent, label: w.hour, from, to, step: slot }); if (v) b.querySelector('span').textContent = v; });
|
|
908
|
+
document.addEventListener('click', (e) => {
|
|
909
|
+
if (agOwner !== me || !e.target.closest('[data-nw-go]') || !draft) return;
|
|
910
|
+
const svc = svcList()[+($('#nw-s', body)?.dataset.v || 0)] || svcList()[0];
|
|
911
|
+
const name = $('#nw-c', body).value.trim();
|
|
912
|
+
if (!name) { $('#nw-c', body).setAttribute('aria-invalid', 'true'); $('#nw-c', body).focus(); return; }
|
|
913
|
+
const start = $('#nw-t span', body).textContent;
|
|
914
|
+
events.push({ id: 'n' + Date.now(), resource: $('#nw-r', body)?.dataset.v || draft.resource, day: new Date(draft.day), start, dur: svc.dur || slot, title: name, subtitle: svc.name, status: 'confirmed', tint: svc.tint, at: 0 });
|
|
915
|
+
draft = null; closeModal(); paint(); onChange(events); island('done', w.scheduled(start), 2400);
|
|
554
916
|
});
|
|
555
917
|
|
|
556
|
-
// ── block time ──
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
918
|
+
// ── block time: the form is the agenda's; the block itself is the app's when it listens (onBlock), else local ──
|
|
919
|
+
let blockOpen = false;
|
|
920
|
+
// THE TITLE OPENS A MONTH, floating under it (on a phone, the sheet from below): one day, or a range of days
|
|
921
|
+
// («Varios días»: the citas of each day one after another), and the month's title opens a year of months.
|
|
922
|
+
// «Ver cuáles»: each press scrolls to the next crossed cita and marks it for a moment
|
|
923
|
+
let clashAt = 0;
|
|
924
|
+
const seeClash = () => { const els = $$('.cx-ag-ev[data-conflict], .cx-ag-row[data-conflict]', root); if (!els.length) return; const el = els[clashAt++ % els.length]; const sc = $('.cx-ag-scroll', root);
|
|
925
|
+
if (sc && sc.contains(el)) { const a = sc.getBoundingClientRect(), b = el.getBoundingClientRect(); sc.scrollTo({ top: sc.scrollTop + b.top - a.top - 60, left: sc.scrollLeft + b.left - a.left - 60, behavior: 'smooth' }); } else el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
926
|
+
el.animate([{ outline: '2px solid var(--destructive)', outlineOffset: '2px' }, { outline: '2px solid transparent', outlineOffset: '2px' }], { duration: 1400, delay: 250 }); };
|
|
927
|
+
let pop = null, dateMode = 'day';
|
|
928
|
+
const calWords = () => ({ mon: w.mon, weekdays: w.dayS.slice(1).concat(w.dayS[0]).map((x) => x.slice(0, 2).toLowerCase()), prevLabel: w.prevMonth, nextLabel: w.nextMonth, prevYear: w.prevYear, nextYear: w.nextYear, pickLabel: w.pickMonth });
|
|
929
|
+
const dateUI = (host, done) => {
|
|
930
|
+
const draw = () => {
|
|
931
|
+
host.innerHTML = `<div class="cx-tabs cx-ag-datemode" role="tablist" aria-label="${w.goTo}">${[['day', w.oneDay], ['range', w.someDays]].map(([k, l]) => `<button type="button" class="cn-button cn-button-variant-ghost cn-button-size-sm cx-tab" role="tab" aria-selected="${dateMode === k}" data-date-mode="${k}">${l}</button>`).join('')}</div><div class="cx-ag-datecal"></div>${dateMode === 'range' ? `<p class="cx-ag-datehint">${w.rangeHint}</p>` : ''}<div class="cx-ag-datefoot">${ui.button({ label: w.today, size: 'sm', variant: 'outline', attrs: 'data-date-today' })}</div>`;
|
|
932
|
+
$$('.cx-tabs', host).forEach(initTabs);
|
|
933
|
+
const wide = dateMode === 'range' && innerWidth > 720;
|
|
934
|
+
kit.calendar($('.cx-ag-datecal', host), { ...calWords(), mode: dateMode === 'range' ? 'range' : 'single', months: wide ? 2 : 1, value: dateMode === 'range' ? (rangeSel ? [new Date(rangeSel[0]), new Date(rangeSel[1])] : null) : new Date(day),
|
|
935
|
+
onChange: (v) => { if (dateMode === 'single' || dateMode === 'day') { if (v) done({ day: v }); return; } if (v?.[0] && v?.[1]) done({ range: v }); } });
|
|
936
|
+
};
|
|
937
|
+
host.addEventListener('click', (e) => { const m = e.target.closest('[data-date-mode]'); if (m) { dateMode = m.dataset.dateMode; draw(); return; } if (e.target.closest('[data-date-today]')) done({ day: new Date(today), today: true }); });
|
|
938
|
+
draw();
|
|
939
|
+
};
|
|
940
|
+
const applyDate = (r) => { if (r.range) { rangeSel = [new Date(r.range[0]), new Date(r.range[1])]; view = 'range'; day = new Date(rangeSel[0]); picked = true; } else { day = new Date(r.day); if (view === 'range') { view = 'day'; rangeSel = null; } } paint(); };
|
|
941
|
+
const closePop = () => { if (!pop) return; pop.remove(); pop = null; document.removeEventListener('pointerdown', popAway, true); document.removeEventListener('keydown', popKey, true); $('[data-ag-date]', root)?.setAttribute('aria-expanded', 'false'); };
|
|
942
|
+
const popAway = (e) => { if (pop && !pop.contains(e.target) && !e.target.closest?.('[data-ag-date]')) closePop(); };
|
|
943
|
+
const popKey = (e) => { if (e.key === 'Escape' && pop) { e.stopPropagation(); closePop(); $('[data-ag-date]', root)?.focus(); } };
|
|
944
|
+
const openDate = () => {
|
|
945
|
+
if (pop) return closePop();
|
|
946
|
+
if (matchMedia('(max-width: 640px)').matches) return sheetOf('agDate');
|
|
947
|
+
const trig = $('[data-ag-date]', root);
|
|
948
|
+
skinSync(); pop = document.createElement('div'); pop.className = 'cn-popover-content pg-pop cx-ag-pop'; pop.dataset.open = ''; pop.setAttribute('role', 'dialog'); pop.setAttribute('aria-label', w.goTo);
|
|
949
|
+
lib.append(pop); dateUI(pop, (r) => { closePop(); applyDate(r); trig?.focus(); });
|
|
950
|
+
place(pop, trig, 'start', 6);
|
|
951
|
+
trig?.setAttribute('aria-expanded', 'true');
|
|
952
|
+
document.addEventListener('pointerdown', popAway, true); document.addEventListener('keydown', popKey, true);
|
|
953
|
+
$('.cx-cal-day[data-range="edge"], .cx-cal-day[aria-pressed="true"], .cx-cal-day[data-today]', pop)?.focus();
|
|
954
|
+
};
|
|
955
|
+
addEventListener('scroll', () => { if (pop) place(pop, $('[data-ag-date]', root), 'start', 6); }, { passive: true });
|
|
956
|
+
addEventListener('resize', () => closePop());
|
|
957
|
+
CONTENT[K('agDate')] = () => {
|
|
958
|
+
$('#mdl-title').textContent = w.goTo; $('#mdl-desc').textContent = ''; foot.innerHTML = '';
|
|
959
|
+
body.innerHTML = '<div class="cx-ag-datesheet"></div>';
|
|
960
|
+
dateUI($('.cx-ag-datesheet', body), (r) => { closeModal(); applyDate(r); });
|
|
961
|
+
};
|
|
962
|
+
const openBlock = () => { blockOpen = true; sheetOf('agBlock'); };
|
|
963
|
+
CONTENT[K('agBlock')] = () => {
|
|
964
|
+
$('#mdl-title').textContent = w.blockTitle; $('#mdl-desc').textContent = w.blockDesc;
|
|
560
965
|
const sel = (id, label, v) => ui.field({ id, label, control: kit.timeButton(v, `id="${id}" data-blt="${label}" style="width:100%;justify-content:flex-start"`) });
|
|
561
|
-
body.innerHTML = `${resources.length > 1 ? ui.field({ id: 'bl-r', label:
|
|
562
|
-
foot.innerHTML = ui.button({ label:
|
|
563
|
-
const br = $('#bl-r', body); if (br) kit.selectMenu(br, { label:
|
|
966
|
+
body.innerHTML = `${resources.length > 1 && blockScope !== 'all' ? ui.field({ id: 'bl-r', label: w.for, control: `<button type="button" class="cn-button cn-button-variant-outline cn-button-size-default cx-sel-btn cx-fill" id="bl-r" data-v="${who === 'all' ? 'all' : who}"></button>` }) : ''}<div class="cx-frange">${sel('bl-f', w.from, '13:00')}${sel('bl-t', w.to, '14:00')}</div>${ui.field({ id: 'bl-why', label: w.reasonLabel, control: ui.input({ id: 'bl-why', value: w.reasonDefault }), description: w.onlyYou })}`;
|
|
967
|
+
foot.innerHTML = ui.button({ label: w.cancel, size: 'default', attrs: 'data-dismiss', cls: 'cx-exit' }) + ui.button({ label: `${ui.icon('i-lock')}${w.block}`, variant: 'default', size: 'default', attrs: 'data-bl-go', cls: 'cx-go' });
|
|
968
|
+
const br = $('#bl-r', body); if (br) kit.selectMenu(br, { label: w.for, value: br.dataset.v, options: [{ value: 'all', label: w.wholeShop }, ...resources.map((r) => ({ value: r.id, label: r.name }))], onChange: (v) => { br.dataset.v = v; } });
|
|
564
969
|
};
|
|
565
|
-
body.addEventListener('click', async (e) => { const b = e.target.closest('[data-blt]'); if (!b) return; const v = await kit.pickTime({ value: b.querySelector('span').textContent, label: b.dataset.blt, from, to, step: 15 }); if (!v) return; b.querySelector('span').textContent = v; b.removeAttribute('aria-invalid'); const fb = $('#bl-f span', body), tb = $('#bl-t span', body); if (toMin(tb.textContent) <= toMin(fb.textContent)) tb.textContent = toHM(Math.min(toMin(fb.textContent) + 60, to * 60)); });
|
|
970
|
+
body.addEventListener('click', async (e) => { const b = e.target.closest('[data-blt]'); if (agOwner !== me || !b || !blockOpen) return; const v = await kit.pickTime({ value: b.querySelector('span').textContent, label: b.dataset.blt, from, to, step: 15 }); if (!v) return; b.querySelector('span').textContent = v; b.removeAttribute('aria-invalid'); const fb = $('#bl-f span', body), tb = $('#bl-t span', body); if (toMin(tb.textContent) <= toMin(fb.textContent)) tb.textContent = toHM(Math.min(toMin(fb.textContent) + 60, to * 60)); });
|
|
566
971
|
document.addEventListener('click', (e) => {
|
|
567
|
-
if (!e.target.closest('[data-bl-go]')) return;
|
|
972
|
+
if (agOwner !== me || !e.target.closest('[data-bl-go]') || !blockOpen) return;
|
|
568
973
|
const f = toMin($('#bl-f span', body).textContent), t2 = toMin($('#bl-t span', body).textContent); if (t2 <= f) { $('#bl-t', body).setAttribute('aria-invalid', 'true'); return; }
|
|
569
|
-
|
|
570
|
-
|
|
974
|
+
const b = { id: 'b' + Date.now(), resource: $('#bl-r', body)?.dataset.v || resources[0].id, day: new Date(day), start: toHM(f), dur: t2 - f, reason: $('#bl-why', body).value.trim() || w.blocked };
|
|
975
|
+
blockOpen = false; closeModal();
|
|
976
|
+
if (onBlock) { onBlock(b); return; }
|
|
977
|
+
blocks.push(b); paint(); onChange(events); island('done', w.blockedOk, 1800);
|
|
571
978
|
});
|
|
979
|
+
|
|
572
980
|
paint();
|
|
573
|
-
|
|
981
|
+
// the app's answer: new data (and/or loading off), same day, view and person on screen
|
|
982
|
+
const update = (next = {}) => {
|
|
983
|
+
if ('resources' in next) resources = next.resources; if ('events' in next) events = next.events; if ('blocks' in next) blocks = next.blocks;
|
|
984
|
+
if ('availability' in next) availability = next.availability; if ('free' in next) freeList = next.free; if ('queue' in next) queue = next.queue;
|
|
985
|
+
if ('closed' in next) closed = next.closed; if ('loading' in next) loading = next.loading;
|
|
986
|
+
if ('overlap' in next) overlap = next.overlap; if ('placing' in next) placing = next.placing; if ('currency' in next) currency = next.currency; if ('locale' in next) locale = next.locale; if ('cents' in next) cents = next.cents;
|
|
987
|
+
if ('lang' in next || 'words' in next) { lang = next.lang ?? lang; words = next.words ?? words; w = { ...(AG_WORDS[lang] || AG_WORDS.es), ...words }; }
|
|
988
|
+
if (who !== 'all' && !(who.startsWith('kind:') ? resources.some((r) => kindOf(r) === who.slice(5)) : resources.some((r) => r.id === who))) who = 'all';
|
|
989
|
+
if (!drag) paint();
|
|
990
|
+
};
|
|
991
|
+
return { refresh: paint, update, run, go: (d) => { day = new Date(d); paint(); } };
|
|
574
992
|
};
|
|
575
993
|
|
|
576
994
|
// ── Chat thread + composer (shared by the inbox and the chatbot) ──
|
|
@@ -853,7 +1271,7 @@ let wheelsNow = [];
|
|
|
853
1271
|
|
|
854
1272
|
// ── Checklist { title, steps [{title, desc, done, action}] } ──
|
|
855
1273
|
kit.checklist = (root, { title, steps }) => {
|
|
856
|
-
const paint = () => { const n = steps.filter((s) => s.done).length; root.innerHTML = `<div data-slot="card" data-size="sm" class="cn-card"><div class="cx-vstack" style="gap:8px;padding:14px 14px 4px">${ui.text(title, 'heading')}<div class="cx-hstack" style="flex-wrap:nowrap;gap:10px">${ui.progress((n / steps.length) * 100,
|
|
1274
|
+
const paint = () => { const n = steps.filter((s) => s.done).length; root.innerHTML = `<div data-slot="card" data-size="sm" class="cn-card"><div class="cx-vstack" style="gap:8px;padding:14px 14px 4px">${ui.text(title, 'heading')}<div class="cx-hstack" style="flex-wrap:nowrap;gap:10px">${ui.progress((n / steps.length) * 100, `$%n de ${steps.length} pasos`)}${ui.text(`$%n/${steps.length}`, 'caption')}</div></div><div class="cn-card-content" style="padding:0 12px 8px"><div class="cn-item-group">${steps.map((s, i) => ui.item({ media: `<div class="cn-item-media"><span class="cx-check-dot" ${s.done ? 'data-done' : ''}>${s.done ? ui.icon('i-tick') : i + 1}</span></div>`, title: s.title, description: s.desc, actions: s.done ? '' : ui.button({ label: s.action, size: 'xs', attrs: `data-step="${i}"` }) })).join(ui.itemSeparator())}</div></div></div>`; };
|
|
857
1275
|
root.addEventListener('click', (e) => { const b = e.target.closest('[data-step]'); if (!b) return; steps[+b.dataset.step].done = true; easeHeight(root, paint); });
|
|
858
1276
|
paint();
|
|
859
1277
|
};
|
|
@@ -942,8 +1360,10 @@ let wheelsNow = [];
|
|
|
942
1360
|
};
|
|
943
1361
|
|
|
944
1362
|
// ── Stamp card { owner, total, stamps, reward } ──
|
|
945
|
-
kit.stampCard = (root, { owner, total, stamps, reward, store }) => {
|
|
946
|
-
|
|
1363
|
+
kit.stampCard = (root, { owner, total, stamps, reward, store, title = 'Cartilla de %owner', progress = '%n de %total · faltan %left para %reward', complete = '¡Completa! Ganó %reward.', count = '%n de %total sellos', redeem = 'Canjear premio', stamp = 'Sellar visita' }) => {
|
|
1364
|
+
// the words are templates, so the card speaks the page's language: %owner %n %total %left %reward
|
|
1365
|
+
const say = (tpl) => String(tpl).replace(/%(owner|total|left|reward|n)/g, (_, k) => ({ owner, n: stamps, total, left: total - stamps, reward })[k]);
|
|
1366
|
+
const paint = (popIdx = -1) => { const done = stamps >= total; root.innerHTML = `<div data-slot="card" class="cn-card cx-stamps"><div class="cx-vstack" style="gap:2px;padding:16px 16px 0">${ui.text(store, 'caption')}${ui.text(say(title), 'heading')}</div><div class="cx-stamp-grid" role="img" aria-label="${say(count)}">${Array.from({ length: total }, (_, i) => `<span class="cx-stamp" ${i < stamps ? 'data-on' : ''} ${i === total - 1 ? 'data-gift' : ''} ${i === popIdx ? 'data-pop' : ''}>${i < stamps ? ui.icon('scissors') : i === total - 1 ? ui.icon('i-gift') : ''}</span>`).join('')}</div><div class="cx-vstack" style="gap:10px;padding:0 16px 16px">${ui.text(say(done ? complete : progress), done ? 'label' : 'muted', 'p')}${done ? ui.button({ label: `${ui.icon('i-gift')}${redeem}`, variant: 'default', size: 'default', cls: 'cx-fill', attrs: 'data-redeem' }) : ui.button({ label: `${ui.icon('plus')}${stamp}`, variant: 'outline', size: 'default', cls: 'cx-fill', attrs: 'data-stamp' })}</div></div>`; };
|
|
947
1367
|
root.addEventListener('click', (e) => { if (e.target.closest('[data-stamp]') && stamps < total) { stamps++; paint(stamps - 1); } if (e.target.closest('[data-redeem]')) { stamps = 0; paint(); } });
|
|
948
1368
|
paint();
|
|
949
1369
|
};
|