@panphora/clayjs 1.5.3 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +5 -3
  2. package/THIRD-PARTY-NOTICES.md +6 -0
  3. package/dist/clay.standalone.js +14965 -10714
  4. package/package.json +9 -4
  5. package/src/core/conflict-download.js +91 -0
  6. package/src/core/conflict-notice.js +720 -0
  7. package/src/core/conflict-presentation.js +301 -0
  8. package/src/core/save-conflict-notice.js +2 -192
  9. package/src/core/save-feedback.js +6 -0
  10. package/src/core/save.js +53 -1
  11. package/src/core/snapshot.js +21 -0
  12. package/src/core/stale-host-notice.js +21 -24
  13. package/src/core/unsaved-warning.js +35 -10
  14. package/src/lib/content-dom.js +4 -2
  15. package/src/lib/dirty-gate.js +9 -0
  16. package/src/lib/unsaved-state.js +55 -0
  17. package/src/loader-logic.js +26 -15
  18. package/src/loader.js +4 -3
  19. package/src/plugins/ai-edit.js +489 -179
  20. package/src/plugins/cms.js +12 -0
  21. package/src/plugins/indicator.js +35 -16
  22. package/src/plugins/quickcrop.js +103 -0
  23. package/src/plugins/richclay.js +21 -0
  24. package/src/plugins/source.js +13 -6
  25. package/src/plugins/wire.js +4 -0
  26. package/src/sync/conflict-revert.js +1138 -0
  27. package/src/sync/conflicts.js +159 -0
  28. package/src/sync/live-sync.js +130 -109
  29. package/src/sync/presence.js +42 -36
  30. package/src/sync/section-notice.js +53 -28
  31. package/src/ui/bevel-controls.js +277 -0
  32. package/src/ui/bevel-dialog.js +112 -0
  33. package/src/ui/bevel.js +247 -0
  34. package/src/ui/dialogs.js +63 -86
  35. package/src/ui/index.js +22 -11
  36. package/src/ui/modal.js +80 -387
  37. package/src/ui/skins/cms-toggle.js +15 -0
  38. package/src/ui/skins/cms.js +4 -0
  39. package/src/ui/skins/quickcrop.js +4 -0
  40. package/src/ui/skins/richclay.js +4 -0
  41. package/src/ui/toast.js +116 -401
  42. package/src/ui/vendor-skin.js +65 -0
  43. package/src/vendor/hyper-morph.vendor.js +4 -4
  44. package/src/vendor/quickcrop.vendor.js +14 -8
  45. package/src/vendor/richclay.vendor.js +22 -41
@@ -0,0 +1,4 @@
1
+ // GENERATED from src/vendor/richclay.vendor.js and bevel/integrations/richclay.css by scripts/build-vendor-skins.mjs. Run `npm run build:skins`.
2
+ export const SOURCE_SHA256 = "91b0339a9c534d4aa94d5b7179e5e9174fa61383f57fa2406cdac56b3719581c";
3
+ export const ROOTS = [".richclay-toolbar",".richclay-float",".richclay-dialog"];
4
+ export const CSS = "@layer clay-skin{\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog),:is(.richclay-toolbar, .richclay-float, .richclay-dialog) *{--richclay-surface:light-dark(#FFFCF6, #11131E) !important;--richclay-surface-alt:light-dark(#F4ECDF, #1A1D2C) !important;--richclay-text:light-dark(#2B241B, #ECEAF2) !important;--richclay-muted:light-dark(#6B5942, #8A90AB) !important;--richclay-border:light-dark(#D8C8AF, #353B52) !important;--richclay-accent:light-dark(#8F6310, #D9A445) !important;--richclay-accent-ink:light-dark(#FDF8F0, #0B0C13) !important;--richclay-focus:light-dark(#8F6310, #D9A445) !important;--richclay-danger:light-dark(#A83A2C, #FF5566) !important;--richclay-radius:0px !important;--richclay-control-size:32px !important;--richclay-shadow:0 1px 2px rgba(0,0,0,.12),0 10px 28px -12px rgba(0,0,0,.35) !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog){font-family:system-ui,-apple-system,\"Segoe UI\",sans-serif !important}\n:where(.richclay-float, .richclay-dialog):is(.richclay-toolbar, .richclay-float, .richclay-dialog){color:light-dark(#2B241B, #ECEAF2) !important;background:light-dark(#FFFCF6, #11131E) !important;border:1px solid light-dark(#D8C8AF, #353B52) !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-toolbar{gap:3px !important;margin:0 !important;padding:8px !important;border:1px solid light-dark(#D8C8AF, #353B52) !important;border-radius:0 !important;background:light-dark(#F4ECDF, #1A1D2C) !important;color:light-dark(#2B241B, #ECEAF2) !important}\n.richclay-toolbar:is(.richclay-toolbar, .richclay-float, .richclay-dialog){gap:3px !important;padding:8px !important;border:1px solid light-dark(#D8C8AF, #353B52) !important;border-radius:0 !important;background:light-dark(#F4ECDF, #1A1D2C) !important;color:light-dark(#2B241B, #ECEAF2) !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-button{min-width:32px !important;height:32px !important;padding:0 7px !important;gap:6px !important;border:2px solid !important;border-color:light-dark(#FFF9ED, #3C4260) light-dark(#D6C3A5, #1B2033) light-dark(#D6C3A5, #1B2033) light-dark(#FFF9ED, #3C4260) !important;border-radius:0 !important;background:light-dark(#F1E7D4, #22273E) !important;font:14px system-ui,-apple-system,\"Segoe UI\",sans-serif !important;cursor:pointer !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-button:is([aria-pressed=\"true\"], [aria-expanded=\"true\"]){background:light-dark(#E3D0B3, #28304D) !important;border-color:light-dark(#B89F7B, #1F2339) light-dark(#F0E1C9, #4D587A) light-dark(#F0E1C9, #4D587A) light-dark(#B89F7B, #1F2339) !important;box-shadow:none !important;color:light-dark(#2B241B, #ECEAF2) !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-button[aria-pressed=\"true\"] > svg{translate:1px 1px !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-button:disabled{opacity:.45 !important;cursor:not-allowed !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-button:hover:not([aria-pressed=\"true\"], [aria-expanded=\"true\"], :disabled){border-color:light-dark(#FFF9ED, #3C4260) light-dark(#D6C3A5, #1B2033) light-dark(#D6C3A5, #1B2033) light-dark(#FFF9ED, #3C4260) !important;background:color-mix(in srgb, light-dark(#F1E7D4, #22273E), light-dark(#FFF9ED, #3C4260) 45%) !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-separator{align-self:center !important;height:22px !important;margin:0 5px !important;background:light-dark(#D8C8AF, #353B52) !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-menu{inset-block-start:38px !important;min-width:210px !important;padding:5px !important;border-radius:0 !important;box-shadow:0 1px 2px rgba(0,0,0,.12),0 10px 28px -12px rgba(0,0,0,.35) !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-menu-item{padding:8px 10px !important;border:0 !important;border-radius:0 !important;font:14px system-ui,-apple-system,\"Segoe UI\",sans-serif !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-menu-item[aria-checked=\"true\"]{background:light-dark(#F4ECDF, #1A1D2C) !important;box-shadow:inset 2px 0 light-dark(#8F6310, #D9A445) !important}\n.richclay-dialog:is(.richclay-toolbar, .richclay-float, .richclay-dialog){width:min(100%, 360px) !important;padding:14px 16px 12px !important;border:1px solid light-dark(#D8C8AF, #353B52) !important;border-radius:0 !important;background:light-dark(#FFFCF6, #11131E) !important;color:light-dark(#2B241B, #ECEAF2) !important;box-shadow:0 1px 2px rgba(0,0,0,.12),0 10px 28px -12px rgba(0,0,0,.35) !important}\n.richclay-dialog:is(.richclay-toolbar, .richclay-float, .richclay-dialog)::before{content:\"\" !important;position:absolute !important;left:calc(var(--richclay-dialog-arrow, 28px) - 6px) !important;top:-7px !important;width:12px !important;height:12px !important;background:light-dark(#FFFCF6, #11131E) !important;border-top:1px solid light-dark(#D8C8AF, #353B52) !important;border-left:1px solid light-dark(#D8C8AF, #353B52) !important;transform:rotate(45deg) !important}\n.richclay-dialog:is(.richclay-toolbar, .richclay-float, .richclay-dialog)[data-richclay-side=\"above\"]::before{top:auto !important;bottom:-7px !important;border:0 !important;border-right:1px solid light-dark(#D8C8AF, #353B52) !important;border-bottom:1px solid light-dark(#D8C8AF, #353B52) !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-dialog-title{margin-bottom:2px !important;font:500 10px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace !important;text-transform:uppercase !important;letter-spacing:.12em !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-field{gap:5px !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-field > span{color:light-dark(#4A4033, #C9CDE0) !important;font-size:12px !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-input{min-height:30px !important;padding:4px 8px !important;border:2px solid !important;border-color:light-dark(#B89F7B, #1F2339) light-dark(#F0E1C9, #4D587A) light-dark(#F0E1C9, #4D587A) light-dark(#B89F7B, #1F2339) !important;border-radius:0 !important;background:light-dark(#F4ECDF, #1A1D2C) !important;color:light-dark(#2B241B, #ECEAF2) !important;font:13px system-ui,-apple-system,\"Segoe UI\",sans-serif !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-dialog-actions{margin-top:4px !important;padding-top:12px !important;border-top:1px solid light-dark(#E8DCC6, #262B42) !important;flex-wrap:wrap !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) :is(.richclay-primary, .richclay-secondary){min-height:30px !important;padding:4px 10px !important;border:2px solid !important;border-color:light-dark(#FFF9ED, #3C4260) light-dark(#D6C3A5, #1B2033) light-dark(#D6C3A5, #1B2033) light-dark(#FFF9ED, #3C4260) !important;border-radius:0 !important;background:light-dark(#F1E7D4, #22273E) !important;color:light-dark(#2B241B, #ECEAF2) !important;font:13px system-ui,-apple-system,\"Segoe UI\",sans-serif !important}\n:is(.richclay-toolbar, .richclay-float, .richclay-dialog) .richclay-primary{background:light-dark(#2B241B, #ECEAF2) !important;color:light-dark(#FDF8F0, #0B0C13) !important;border-color:color-mix(in srgb, light-dark(#2B241B, #ECEAF2), white 24%) color-mix(in srgb, light-dark(#2B241B, #ECEAF2), black 32%) color-mix(in srgb, light-dark(#2B241B, #ECEAF2), black 32%) color-mix(in srgb, light-dark(#2B241B, #ECEAF2), white 24%) !important}\n.richclay-float:is(.richclay-toolbar, .richclay-float, .richclay-dialog){border-radius:0 !important;box-shadow:0 1px 2px rgba(0,0,0,.12),0 10px 28px -12px rgba(0,0,0,.35) !important}\n.richclay-float:is(.richclay-toolbar, .richclay-float, .richclay-dialog) > .richclay-toolbar{padding:5px !important}\n.richclay-toolbar{align-items:center !important;background:var(--richclay-surface-alt) !important;border:1px solid var(--richclay-border) !important;border-radius:var(--richclay-radius) !important;color:var(--richclay-text) !important;display:flex !important;flex-wrap:wrap !important;gap:2px !important;margin-block:0 6px !important;padding:4px !important}\n.richclay-toolbar:has(+ .richclay-editor){margin-block-end:0 !important;border-bottom-left-radius:0 !important;border-bottom-right-radius:0 !important}\n.richclay-button{align-items:center !important;appearance:none !important;background:transparent !important;border:1px solid transparent !important;border-radius:5px !important;color:currentColor !important;display:inline-flex !important;height:var(--richclay-control-size) !important;justify-content:center !important;margin:0 !important;min-width:var(--richclay-control-size) !important;padding:0 !important;position:relative !important}\n.richclay-button:hover,.richclay-button[aria-expanded=\"true\"],.richclay-menu-item:hover{background:var(--richclay-surface) !important;border-color:var(--richclay-border) !important}\n.richclay-button.is-active,.richclay-button[aria-pressed=\"true\"],.richclay-menu-item.is-active{background:rgb(0 0 0 / 0.13) !important;border-color:rgb(0 0 0 / 0.22) !important;color:var(--richclay-text) !important;box-shadow:inset 0 1px 3px rgb(0 0 0 / 0.26) !important}\n.richclay-button:focus-visible,.richclay-menu-item:focus-visible,.richclay-input:focus-visible,.richclay-primary:focus-visible,.richclay-secondary:focus-visible{outline:3px solid var(--richclay-focus) !important;outline-offset:2px !important}\n.richclay-button:disabled{color:var(--richclay-muted) !important;cursor:not-allowed !important;opacity:0.55 !important}\n.richclay-separator{align-self:stretch !important;background:var(--richclay-border) !important;display:inline-block !important;margin:5px 4px !important;width:1px !important}\n.richclay-menu-wrap{display:inline-flex !important;position:relative !important}\n.richclay-menu{background:var(--richclay-surface) !important;border:1px solid var(--richclay-border) !important;border-radius:var(--richclay-radius) !important;box-shadow:var(--richclay-shadow) !important;color:var(--richclay-text) !important;display:grid !important;gap:2px !important;inset-block-start:calc(100% + 4px) !important;inset-inline-start:0 !important;min-width:160px !important;padding:4px !important;position:absolute !important;z-index:1000 !important}\n.richclay-menu[hidden]{display:none !important}\n.richclay-menu-item{appearance:none !important;background:transparent !important;border:1px solid transparent !important;border-radius:4px !important;color:inherit !important;font:inherit !important;min-height:32px !important;padding:5px 9px !important;text-align:start !important}\n.richclay-float{left:0 !important;position:fixed !important;top:0 !important;will-change:transform !important;z-index:99999 !important}\n.richclay-float .richclay-toolbar{box-shadow:var(--richclay-shadow) !important;margin-block:0 !important}\n.richclay-float-rail .richclay-toolbar{--richclay-control-size:28px !important;flex-direction:column !important;flex-wrap:nowrap !important}\n.richclay-float-rail .richclay-toolbar svg{height:15px !important;width:15px !important}\n.richclay-float-rail .richclay-separator{align-self:stretch !important;height:1px !important;margin:4px 5px !important;width:auto !important}\n.richclay-float-rail .richclay-menu{inset-block-start:0 !important;inset-inline-start:calc(100% + 4px) !important}\n.richclay-dialog{background:var(--richclay-surface) !important;border:1px solid var(--richclay-border) !important;border-radius:var(--richclay-radius) !important;box-shadow:var(--richclay-shadow) !important;color:var(--richclay-text) !important;display:grid !important;gap:10px !important;box-sizing:border-box !important;width:min(420px, calc(100vw - 16px)) !important;z-index:99999 !important;padding:12px !important}\n.richclay-dialog-title{font-weight:650 !important}\n.richclay-field{display:grid !important;gap:4px !important}\n.richclay-field span{color:var(--richclay-muted) !important;font-size:0.875rem !important}\n.richclay-input{background:var(--richclay-surface) !important;border:1px solid var(--richclay-border) !important;border-radius:4px !important;color:var(--richclay-text) !important;font:inherit !important;min-height:36px !important;padding:6px 8px !important}\n.richclay-dialog-actions{display:flex !important;gap:8px !important;justify-content:flex-end !important}\n.richclay-primary,.richclay-secondary{appearance:none !important;border:1px solid var(--richclay-border) !important;border-radius:4px !important;font:inherit !important;min-height:34px !important;padding:5px 10px !important}\n.richclay-primary{background:var(--richclay-accent) !important;border-color:var(--richclay-accent) !important;color:var(--richclay-accent-ink) !important}\n.richclay-secondary{background:var(--richclay-surface-alt) !important;color:var(--richclay-text) !important}\n.richclay-sr-only{border:0 !important;clip:rect(0 0 0 0) !important;clip-path:inset(50%) !important;height:1px !important;margin:-1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;white-space:nowrap !important;width:1px !important}\n@media (forced-colors: active){\n.richclay-toolbar,.richclay-menu,.richclay-input{border-color:CanvasText !important}\n.richclay-dialog{border-color:CanvasText !important}\n.richclay-button.is-active,.richclay-button[aria-pressed=\"true\"],.richclay-menu-item.is-active,.richclay-primary{background:Highlight !important;color:HighlightText !important}\n.richclay-button:focus-visible,.richclay-menu-item:focus-visible,.richclay-input:focus-visible{outline-color:Highlight !important}\n}\n.richclay-toolbar[hidden]{display:none !important}\n.richclay-button[hidden]{display:none !important}\n.richclay-separator[hidden]{display:none !important}\n.richclay-menu-wrap[hidden]{display:none !important}\n.richclay-menu[hidden]{display:none !important}\n.richclay-dialog[hidden]{display:none !important}\n.richclay-field[hidden]{display:none !important}\n.richclay-dialog-actions[hidden]{display:none !important}\n}";
package/src/ui/toast.js CHANGED
@@ -1,434 +1,149 @@
1
1
  // a nice, simple alert
2
2
  // ❗️ don't use too much text!
3
-
4
- // Default modern icons (normalized to 48x48 viewBox)
5
- const defaultIcons = {
6
- success: `<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.9404 23.9475L21.9099 31.224L35.1906 15.9045M3 4.5H44.9804V44.309H3V4.5Z" stroke="#33D131" stroke-width="4.3"/></svg>`,
7
- error: `<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M32.7383 14.4045L14 33.1429M32.7451 33.1429L14.0068 14.4046M3.01 4H44.99V43.809H3.01V4Z" stroke="#FF4450" stroke-width="4"/></svg>`,
8
- warning: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke="#F5A623" stroke-width="2" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"/></svg>`,
9
- info: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke="#4A90D9" stroke-width="2" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"/></svg>`
10
- };
11
-
12
- // Hyperclay icons
13
- export const hyperclayIcons = {
14
- success: `<svg viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M32 1h-5v3.5h-2.5V8h-2v3.5H20V15h-2.5v3.5h-2V22H13v3.5H9V22H7v-3.5H6V15H1v3.5h1V22h2v3.5h1.5V29H7v3.5h5V29h3.5v-3.5H18V22h2.5v-3.5h2V15H25v-3.5h2.5V8h2V4.5H32V1Z" fill="#76C824"/></svg>`,
15
- error: `<svg viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M33 1h-5v3.5h-3.5V8H21v3.5h-3.5V15h-2v-3.5H12V8H8.5V4.5H5V1H0v3.5h3.5V8H7v3.5h3.5V15H14v3.5h-3.5V22H7v3.5H3.5V29H0v3.5h5V29h3.5v-3.5H12V22h3.5v-3.5h2V22H21v3.5h3.5V29H28v3.5h5V29h-3.5v-3.5H26V22h-3.5v-3.5H19V15h3.5v-3.5H26V8h3.5V4.5H33V1Z" fill="#DD304F"/></svg>`,
16
- warning: `<svg viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M18 1h-3v3h-2v3h-2v3H9v3H7v3H5v3H3v3H1v6h31v-6h-2v-3h-2v-3h-2v-3h-2v-3h-2V7h-2V4h-2V1zm-2 8h1v10h-1V9zm0 13h1v2h-1v-2z" fill="#F5A623"/></svg>`,
17
- info: `<svg viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 1v3H9v3H6v3H3v6h3v3h3v3h3v3h9v-3h3v-3h3v-3h3v-6h-3V7h-3V4h-3V1H12zm4 7h1v2h-1V8zm0 5h1v9h-1v-9z" fill="#4A90D9"/></svg>`
18
- };
19
-
20
- // Default templates
21
- const defaultTemplates = {
22
- toast: {
23
- success: `
24
- <div class="toast hide success noise-texture">
25
- <div class="toast-icon">{icon}</div>
26
- <div class="toast-message">{message}</div>
27
- </div>
28
- `,
29
- error: `
30
- <div class="toast hide error noise-texture">
31
- <div class="toast-icon">{icon}</div>
32
- <div class="toast-message">{message}</div>
33
- </div>
34
- `,
35
- warning: `
36
- <div class="toast hide warning noise-texture">
37
- <div class="toast-icon">{icon}</div>
38
- <div class="toast-message">{message}</div>
39
- </div>
40
- `,
41
- info: `
42
- <div class="toast hide info noise-texture">
43
- <div class="toast-icon">{icon}</div>
44
- <div class="toast-message">{message}</div>
45
- </div>
46
- `
47
- }
3
+ //
4
+ // The Hyperclay dashboard's toast (Bevel's React toast with the dashboard's overrides),
5
+ // drawn inline: a 3px tone edge, a 16px Phosphor glyph, 13.5px semibold text, a quiet close.
6
+ import { set } from "../lib/hostile-css.js";
7
+ import { bevelBox, bevelSurface, bevelText, pageScheme, protectIcon } from "./bevel-controls.js";
8
+ import { TOKENS, RULES } from "./bevel.js";
9
+
10
+ const icon = (d) =>
11
+ `<svg viewBox="0 0 256 256" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="${d}"/></svg>`;
12
+
13
+ const CHECK = "M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z";
14
+ const WARNING = "M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z";
15
+ const INFO = "M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z";
16
+ const X = "M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z";
17
+
18
+ const TONES = {
19
+ success: { edge: TOKENS.teal, icon: icon(CHECK) },
20
+ error: { edge: TOKENS.ox, icon: icon(WARNING) },
21
+ warning: { edge: TOKENS.brass, icon: icon(WARNING) },
22
+ info: { edge: TOKENS.brass, icon: icon(INFO) },
48
23
  };
49
24
 
50
- // Hyperclay templates
51
- export const hyperclayTemplates = {
52
- toast: {
53
- success: `
54
- <div class="toast hide success">
55
- {icon}
56
- <span class="message">{message}</span>
57
- </div>
58
- `,
59
- error: `
60
- <div class="toast hide error">
61
- {icon}
62
- <span class="message">{message}</span>
63
- </div>
64
- `,
65
- warning: `
66
- <div class="toast hide warning">
67
- {icon}
68
- <span class="message">{message}</span>
69
- </div>
70
- `,
71
- info: `
72
- <div class="toast hide info">
73
- {icon}
74
- <span class="message">{message}</span>
75
- </div>
76
- `
77
- }
78
- };
79
-
80
- // Modern styles (scoped by data-toast-theme="modern")
81
- const modernStyles = `
82
- [data-toast-theme="modern"].toast-container {
83
- z-index: 9999;
84
- position: fixed;
85
- top: 20px;
86
- right: 20px;
87
- display: flex;
88
- flex-direction: column;
89
- align-items: end;
90
- gap: 18px;
91
- }
92
-
93
- [data-toast-theme="modern"] .toast {
94
- position: relative;
95
- right: 0;
96
- cursor: pointer;
97
- transition: right 0.5s ease-in-out, opacity 0.5s ease-in-out;
98
- display: flex;
99
- align-items: center;
100
- gap: 12px;
101
- padding: 16px 24px 16px 16px;
102
- font-size: var(--hyperclay-toast-font-size, 16px);
103
- font-family: var(--hyperclay-toast-font-family, monospace);
104
- font-weight: bold;
105
- color: white;
106
- border-width: 1px;
107
- border-style: solid;
108
- }
109
-
110
- [data-toast-theme="modern"] .toast.hide {
111
- right: -400px;
112
- opacity: 0;
113
- }
114
-
115
- [data-toast-theme="modern"] .toast.success {
116
- border-color: #358234;
117
- background: radial-gradient(85.86% 68.42% at 50% 68.42%, #142419 0%, #1D3927 100%);
118
- }
119
-
120
- [data-toast-theme="modern"] .toast.error {
121
- border-color: #992930;
122
- background: radial-gradient(85.86% 68.42% at 50% 68.42%, #240A13 0%, #481826 100%);
123
- }
124
-
125
- [data-toast-theme="modern"] .toast.warning {
126
- border-color: #B8860B;
127
- background: radial-gradient(85.86% 68.42% at 50% 68.42%, #2A2010 0%, #3D3018 100%);
128
- }
129
-
130
- [data-toast-theme="modern"] .toast.info {
131
- border-color: #2E6B9E;
132
- background: radial-gradient(85.86% 68.42% at 50% 68.42%, #0A1A24 0%, #162D3D 100%);
133
- }
134
-
135
- [data-toast-theme="modern"] .toast-icon {
136
- position: relative;
137
- top: -1px;
138
- }
25
+ const CLOSE_ICON = icon(X);
139
26
 
140
- [data-toast-theme="modern"] .toast-icon svg {
141
- display: block;
142
- width: 22px;
143
- height: 22px;
144
- }
145
-
146
- [data-toast-theme="modern"] .toast.info .toast-icon svg {
147
- width: 24px;
148
- height: 24px;
149
- }
27
+ const HIDDEN = ["opacity:0", "transform:translateX(24px)"];
150
28
 
151
- [data-toast-theme="modern"] .toast-message {
152
- position: relative;
153
- top: -1px;
154
- }
155
-
156
- [data-toast-theme="modern"] .noise-texture {
157
- position: relative;
158
- }
159
-
160
- [data-toast-theme="modern"] .noise-texture::before {
161
- content: "";
162
- position: absolute;
163
- top: 0;
164
- left: 0;
165
- width: 100%;
166
- height: 100%;
167
- opacity: 0.12;
168
- pointer-events: none;
169
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.2' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
170
- background-repeat: repeat;
171
- }
172
- `;
173
-
174
- // Hyperclay styles (scoped by data-toast-theme="hyperclay")
175
- export const hyperclayStyles = `
176
- [data-toast-theme="hyperclay"].toast-container {
177
- z-index: 9999;
178
- position: fixed;
179
- top: 20px;
180
- right: 20px;
181
- display: flex;
182
- flex-direction: column;
183
- align-items: end;
184
- }
185
-
186
- [data-toast-theme="hyperclay"].toast-container > * + * {
187
- margin-top: 18px;
188
- }
189
-
190
- [data-toast-theme="hyperclay"] .toast {
191
- position: relative;
192
- right: 0;
193
- display: flex;
194
- align-items: center;
195
- padding: 10px 19px 11px 17px;
196
- cursor: pointer;
197
- color: rgba(255,255,255,.8);
198
- background-color: #0B0C12;
199
- border: 2px dashed rgba(255,255,255,.6);
200
- transition: right 0.5s ease-in-out;
201
- font-size: var(--hyperclay-toast-font-size, inherit);
202
- font-family: var(--hyperclay-toast-font-family, inherit);
203
- }
204
-
205
- [data-toast-theme="hyperclay"] .toast svg {
206
- position: relative;
207
- top: -1px;
208
- width: 17px;
209
- height: 17px;
210
- margin-right: 13px;
211
- }
212
-
213
- [data-toast-theme="hyperclay"] .toast.hide {
214
- right: -300px;
215
- }
216
-
217
- [data-toast-theme="hyperclay"] .toast.success {
218
- color: #76C824;
219
- border: 2px dashed #589E11;
220
- }
221
-
222
- [data-toast-theme="hyperclay"] .toast.error {
223
- color: #DD304F;
224
- border: 2px dashed #CD2140;
225
- }
226
-
227
- [data-toast-theme="hyperclay"] .toast.warning {
228
- color: #F5A623;
229
- border: 2px dashed #D4900E;
230
- }
231
-
232
- [data-toast-theme="hyperclay"] .toast.info {
233
- color: #4A90D9;
234
- border: 2px dashed #3A7BBF;
235
- }
236
- `;
237
-
238
- // Global toast configuration (can be overridden by toast-hyperclay module)
239
- let toastConfig = {
240
- styles: modernStyles,
241
- templates: defaultTemplates,
242
- icons: defaultIcons,
243
- theme: 'modern'
244
- };
245
-
246
- // Allow other modules (like toast-hyperclay) to override default toast styling
247
- export function setToastTheme(config) {
248
- Object.assign(toastConfig, config);
29
+ function motion() {
30
+ const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
31
+ return reduced ? "none" : "opacity .5s ease-in-out, transform .5s ease-in-out";
249
32
  }
250
33
 
251
- // Helper function to inject styles for a theme (additive, not replacing)
252
- export function injectToastStyles(styles, theme) {
253
- if (document.querySelector(`style.toast-styles-${theme}`)) return;
254
-
255
- const styleSheet = document.createElement('style');
256
- styleSheet.className = `toast-styles-${theme}`;
257
- styleSheet.setAttribute('clay', 'no-save no-snapshot');
258
- styleSheet.textContent = styles;
259
- document.head.appendChild(styleSheet);
34
+ // Looked up on every call, never cached: a live-sync morph or a caller can empty the
35
+ // body, and a detached stack would swallow every toast after it.
36
+ function stack() {
37
+ const existing = document.querySelector("[data-clay-toasts]");
38
+ if (existing) return existing;
39
+ const box = bevelBox("div", [
40
+ "position:fixed", "top:18px", "right:18px", "z-index:9999",
41
+ "display:flex", "flex-direction:column", "align-items:flex-end", "gap:10px",
42
+ "width:min(330px, calc(100vw - 36px))", "pointer-events:none",
43
+ ]);
44
+ box.setAttribute("data-clay-toasts", "");
45
+ box.setAttribute("aria-live", "polite");
46
+ document.body.append(box);
47
+ return box;
260
48
  }
261
49
 
262
- // Core toast function (used by both toast and toastHyperclay)
263
- export function toastCore(message, messageType = "success", config = {}) {
264
- const templates = config.templates || defaultTemplates;
265
- const icons = config.icons || defaultIcons;
266
- const theme = config.theme || 'modern';
267
-
268
- // Get or create container for this theme
269
- let toastContainer = document.querySelector(`.toast-container[data-toast-theme="${theme}"]`);
270
- if (!toastContainer) {
271
- toastContainer = document.createElement('div');
272
- toastContainer.className = 'toast-container';
273
- toastContainer.setAttribute('data-toast-theme', theme);
274
- toastContainer.setAttribute('clay', 'no-save no-snapshot');
275
- document.body.append(toastContainer);
276
- }
277
-
278
- // Create toast element
279
- const icon = icons[messageType] || icons.success;
280
- const template = templates.toast[messageType] || templates.toast.success;
281
- const toastHtml = template
282
- .replace('{icon}', icon)
283
- .replace('{message}', '');
284
-
285
- const tempDiv = document.createElement('div');
286
- tempDiv.innerHTML = toastHtml.trim();
287
- const toastElement = tempDiv.firstElementChild;
50
+ function build(message, type) {
51
+ const tone = TONES[type];
52
+ const el = bevelSurface("div", [
53
+ "display:flex", "align-items:flex-start", ...RULES.toast, "width:100%",
54
+ `border-inline-start:3px solid ${tone.edge}`,
55
+ "cursor:pointer", "pointer-events:auto", `color-scheme:${pageScheme()}`,
56
+ ...HIDDEN, `transition:${motion()}`,
57
+ ]);
58
+ el.setAttribute("data-clay-toast", type);
59
+ const glyph = bevelText("span", ["display:inline-grid", "flex:none", "padding-top:1px", `color:${tone.edge}`]);
60
+ glyph.innerHTML = tone.icon;
61
+ protectIcon(glyph.firstElementChild, 16);
288
62
  // A message can carry a filename, and a filename is whatever whoever wrote the
289
- // file chose. Assign it as text so markup in a name can never become markup here.
290
- toastElement.querySelector('.toast-message, .message').textContent = message;
63
+ // file chose. It goes in as text so markup in a name can never become markup here.
64
+ const text = bevelText("span", ["flex:1 1 auto", "min-width:120px", ...RULES.toastTitle, "overflow-wrap:anywhere"], message);
65
+ el.append(glyph, text);
66
+ return el;
67
+ }
291
68
 
292
- // Add click handler
293
- toastElement.addEventListener('click', () => {
294
- toastElement.classList.add('hide');
295
- setTimeout(() => toastElement.remove(), 500);
296
- });
69
+ // The dashboard's toast close: a bare 24px square in the muted ink, the sunk ground on hover.
70
+ function closeButton(onClick) {
71
+ const b = bevelBox("button", [
72
+ "flex:none", "display:inline-grid", "place-items:center", ...RULES.toastClose,
73
+ "cursor:pointer",
74
+ ]);
75
+ b.type = "button";
76
+ b.setAttribute("aria-label", "Dismiss");
77
+ b.title = "Dismiss";
78
+ b.innerHTML = CLOSE_ICON;
79
+ protectIcon(b.firstElementChild, 14);
80
+ const paint = (lit) => {
81
+ set(b, "color", lit ? TOKENS.ink : TOKENS.muted);
82
+ set(b, "background", lit ? TOKENS.sunk : "transparent");
83
+ };
84
+ b.addEventListener("pointerenter", () => paint(true));
85
+ b.addEventListener("pointerleave", () => paint(false));
86
+ b.addEventListener("focus", () => { paint(true); set(b, "outline", `2px solid ${TOKENS.brass}`); set(b, "outline-offset", "1px"); });
87
+ b.addEventListener("blur", () => { paint(false); set(b, "outline", "none"); });
88
+ b.addEventListener("click", onClick);
89
+ return b;
90
+ }
297
91
 
298
- // Add to container and animate in
299
- toastContainer.append(toastElement);
300
- setTimeout(() => toastElement.classList.remove('hide'), 10);
92
+ function reveal(el) {
93
+ stack().append(el);
94
+ setTimeout(() => {
95
+ set(el, "opacity", "1");
96
+ set(el, "transform", "none");
97
+ }, 10);
98
+ }
301
99
 
302
- // Auto-hide after timeout
100
+ function dismiss(el, after) {
101
+ set(el, "opacity", "0");
102
+ set(el, "transform", "translateX(24px)");
303
103
  setTimeout(() => {
304
- toastElement.classList.add('hide');
305
- setTimeout(() => toastElement.remove(), 500);
306
- }, 6600);
104
+ el.remove();
105
+ after?.();
106
+ }, 500);
307
107
  }
308
108
 
309
- // Main toast function - uses configured theme (default: modern, or hyperclay if toast-hyperclay loaded)
310
109
  function toast(message, messageType = "success") {
311
- injectToastStyles(toastConfig.styles, toastConfig.theme);
312
- toastCore(message, messageType, {
313
- templates: toastConfig.templates,
314
- icons: toastConfig.icons,
315
- theme: toastConfig.theme
316
- });
110
+ const el = build(message, TONES[messageType] ? messageType : "success");
111
+ el.addEventListener("click", () => dismiss(el));
112
+ reveal(el);
113
+ setTimeout(() => dismiss(el), 6600);
317
114
  }
318
115
 
319
- // Close button (button with pixel art X icon inside)
320
- const CLOSE_BUTTON_HTML = `<button class="toast-close" type="button" aria-label="Close"><svg viewBox="0 0 558 558" xmlns="http://www.w3.org/2000/svg"><path d="M0,0h62.02v62.02h-62.02Z M62,0h62.02v62.02h-62.02Z M434,0h62.02v62.02h-62.02Z M496,0h62.02v62.02h-62.02Z M0,62h62.02v62.02h-62.02Z M62,62h62.02v62.02h-62.02Z M124,62h62.02v62.02h-62.02Z M372,62h62.02v62.02h-62.02Z M434,62h62.02v62.02h-62.02Z M496,62h62.02v62.02h-62.02Z M62,124h62.02v62.02h-62.02Z M124,124h62.02v62.02h-62.02Z M186,124h62.02v62.02h-62.02Z M310,124h62.02v62.02h-62.02Z M372,124h62.02v62.02h-62.02Z M434,124h62.02v62.02h-62.02Z M124,186h62.02v62.02h-62.02Z M186,186h62.02v62.02h-62.02Z M248,186h62.02v62.02h-62.02Z M310,186h62.02v62.02h-62.02Z M372,186h62.02v62.02h-62.02Z M186,248h62.02v62.02h-62.02Z M248,248h62.02v62.02h-62.02Z M310,248h62.02v62.02h-62.02Z M124,310h62.02v62.02h-62.02Z M186,310h62.02v62.02h-62.02Z M248,310h62.02v62.02h-62.02Z M310,310h62.02v62.02h-62.02Z M372,310h62.02v62.02h-62.02Z M62,372h62.02v62.02h-62.02Z M124,372h62.02v62.02h-62.02Z M186,372h62.02v62.02h-62.02Z M310,372h62.02v62.02h-62.02Z M372,372h62.02v62.02h-62.02Z M434,372h62.02v62.02h-62.02Z M0,434h62.02v62.02h-62.02Z M62,434h62.02v62.02h-62.02Z M124,434h62.02v62.02h-62.02Z M372,434h62.02v62.02h-62.02Z M434,434h62.02v62.02h-62.02Z M496,434h62.02v62.02h-62.02Z M0,496h62.02v62.02h-62.02Z M62,496h62.02v62.02h-62.02Z M434,496h62.02v62.02h-62.02Z M496,496h62.02v62.02h-62.02Z"/></svg></button>`;
321
-
322
- // Persistent toast styles (added once)
323
- const persistentToastStyles = `
324
- .toast.toast-persistent {
325
- position: relative;
326
- align-items: center;
327
- padding-right: 18px;
328
- }
329
- .toast-persistent .toast-close {
330
- background: none;
331
- border: none;
332
- padding: 0;
333
- margin-left: 3px;
334
- position: relative;
335
- top: -2px;
336
- cursor: pointer;
337
- opacity: 0.7;
338
- transition: opacity 0.2s;
339
- display: flex;
340
- align-items: center;
341
- justify-content: center;
342
- color: white;
343
- width: 22px;
344
- height: 22px;
345
- }
346
- .toast-persistent .toast-close:hover {
347
- opacity: 1;
348
- }
349
- .toast-persistent .toast-close svg {
350
- width: 11px;
351
- height: 11px;
352
- fill: currentColor;
353
- }
354
- `;
355
-
356
116
  // Track active persistent toasts by message
357
117
  const activePersistentToasts = new Map();
358
118
 
359
119
  // Persistent toast function - doesn't auto-dismiss, requires click to close
360
120
  function toastPersistent(message, messageType = "warning") {
361
- injectToastStyles(toastConfig.styles, toastConfig.theme);
362
-
363
- // Inject persistent-specific styles once
364
- if (!document.querySelector('style.toast-styles-persistent')) {
365
- const styleSheet = document.createElement('style');
366
- styleSheet.className = 'toast-styles-persistent';
367
- styleSheet.setAttribute('clay', 'no-save no-snapshot');
368
- styleSheet.textContent = persistentToastStyles;
369
- document.head.appendChild(styleSheet);
370
- }
371
-
372
- const templates = toastConfig.templates;
373
- const icons = toastConfig.icons;
374
- const theme = toastConfig.theme;
375
-
376
- // Get or create container for this theme
377
- let toastContainer = document.querySelector(`.toast-container[data-toast-theme="${theme}"]`);
378
- if (!toastContainer) {
379
- toastContainer = document.createElement('div');
380
- toastContainer.className = 'toast-container';
381
- toastContainer.setAttribute('data-toast-theme', theme);
382
- toastContainer.setAttribute('clay', 'no-save no-snapshot');
383
- document.body.append(toastContainer);
384
- }
385
-
386
- // If same message already showing, animate it out first
387
- const existingToast = activePersistentToasts.get(message);
388
- if (existingToast) {
389
- existingToast.classList.add('hide');
390
- setTimeout(() => existingToast.remove(), 500);
121
+ const existing = activePersistentToasts.get(message);
122
+ if (existing) {
123
+ dismiss(existing);
391
124
  activePersistentToasts.delete(message);
392
125
  }
393
126
 
394
- // Create toast element
395
- const icon = icons[messageType] || icons.warning;
396
- const template = templates.toast[messageType] || templates.toast.warning;
397
- const toastHtml = template
398
- .replace('{icon}', icon)
399
- .replace('{message}', '');
400
-
401
- const tempDiv = document.createElement('div');
402
- tempDiv.innerHTML = toastHtml.trim();
403
- const toastElement = tempDiv.firstElementChild;
404
- // A message can carry a filename, and a filename is whatever whoever wrote the
405
- // file chose. Assign it as text so markup in a name can never become markup here.
406
- toastElement.querySelector('.toast-message, .message').textContent = message;
407
- toastElement.classList.add('toast-persistent');
408
-
409
- // Add close button as sibling (not inside message)
410
- toastElement.insertAdjacentHTML('beforeend', CLOSE_BUTTON_HTML);
411
-
412
- // Close handler - only on close button click
413
- const closeBtn = toastElement.querySelector('.toast-close');
414
- if (closeBtn) {
415
- closeBtn.addEventListener('click', (e) => {
416
- e.stopPropagation();
417
- toastElement.classList.add('hide');
418
- setTimeout(() => {
419
- toastElement.remove();
420
- activePersistentToasts.delete(message);
421
- }, 500);
422
- });
423
- }
127
+ const el = build(message, TONES[messageType] ? messageType : "warning");
128
+ set(el, "cursor", "default");
129
+ const close = closeButton((e) => {
130
+ e.stopPropagation();
131
+ // A newer toast with the same message may have taken this entry since.
132
+ dismiss(el, () => { if (activePersistentToasts.get(message) === el) activePersistentToasts.delete(message); });
133
+ });
134
+ el.append(close);
424
135
 
425
- // Track this toast
426
- activePersistentToasts.set(message, toastElement);
136
+ activePersistentToasts.set(message, el);
137
+ reveal(el);
138
+ }
427
139
 
428
- // Add to container and animate in
429
- toastContainer.append(toastElement);
430
- setTimeout(() => toastElement.classList.remove('hide'), 10);
140
+ // Takes down the persistent toast showing this message, if one is up.
141
+ function dismissPersistent(message) {
142
+ const el = activePersistentToasts.get(message);
143
+ if (!el) return;
144
+ activePersistentToasts.delete(message);
145
+ dismiss(el);
431
146
  }
432
147
 
433
- export { toastPersistent };
148
+ export { toastPersistent, dismissPersistent };
434
149
  export default toast;
@@ -0,0 +1,65 @@
1
+ // Puts a generated vendor skin (src/ui/skins/*.js) on the page.
2
+ //
3
+ // A skin is one `@layer clay-skin { ... }` block whose declarations are all
4
+ // !important. Important declarations in the earliest cascade layer beat the host
5
+ // page's unlayered !important rules and every later layer, and a layer's position is
6
+ // fixed where its name first appears, so the skin goes in ahead of every other
7
+ // stylesheet in <head>. It is runtime-only: never saved, never watched, never sent to
8
+ // a peer, and a second call for the same name reuses the element.
9
+ import { RUNTIME_ONLY, pageScheme } from './bevel-controls.js';
10
+
11
+ const isSkin = (node) => node.nodeType === 1 && node.hasAttribute('data-clay-skin');
12
+
13
+ function firstNonSkin(head) {
14
+ for (const node of head.childNodes) if (!isSkin(node)) return node;
15
+ return null;
16
+ }
17
+
18
+ function placed(head, el) {
19
+ if (el.parentNode !== head) return false;
20
+ for (let node = el.previousSibling; node; node = node.previousSibling) if (!isSkin(node)) return false;
21
+ return true;
22
+ }
23
+
24
+ const installed = new Map();
25
+
26
+ // A vendor that sets no color-scheme of its own inherits the page's, and a page that
27
+ // declares none computes `normal`, which resolves every light-dark() pin to light.
28
+ // ClayJS's own surfaces follow the reader's preference there (pageScheme), so a skin
29
+ // installed with its roots gives them the same scheme.
30
+ function schemeRule(roots) {
31
+ return `\n@layer clay-skin{:is(${roots.join(', ')}){color-scheme:${pageScheme()} !important}}`;
32
+ }
33
+
34
+ /**
35
+ * Install or refresh one skin.
36
+ * @param {string} name one element per name
37
+ * @param {string} css the skin module's CSS export
38
+ * @param {{ roots?: string[] }} [options] roots to give the page's scheme
39
+ * @returns {HTMLStyleElement}
40
+ */
41
+ export function installSkin(name, css, { roots } = {}) {
42
+ installed.set(name, { css, roots });
43
+ const head = document.head || document.documentElement;
44
+ let el = null;
45
+ for (const node of head.querySelectorAll('style[data-clay-skin]')) {
46
+ if (node.getAttribute('data-clay-skin') === name) el = node;
47
+ }
48
+ if (!el) {
49
+ el = document.createElement('style');
50
+ el.setAttribute('clay', RUNTIME_ONLY);
51
+ el.setAttribute('data-clay-skin', name);
52
+ }
53
+ const text = roots ? css + schemeRule(roots) : css;
54
+ if (el.textContent !== text) el.textContent = text;
55
+ if (!placed(head, el)) head.insertBefore(el, firstNonSkin(head));
56
+ return el;
57
+ }
58
+
59
+ // A live-sync merge can put a peer's stylesheet ahead of a skin, and a page can change
60
+ // its scheme; after every applied frame each skin goes back in front, re-read.
61
+ if (typeof document !== 'undefined') {
62
+ document.addEventListener('clay:sync-applied', () => {
63
+ for (const [name, { css, roots }] of installed) installSkin(name, css, { roots });
64
+ });
65
+ }