@gemboss/gem-framework 0.18.16 → 0.19.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 (53) hide show
  1. package/README.md +80 -1
  2. package/dist/SOURCE.json +3 -3
  3. package/dist/gem-framework.css +23 -1
  4. package/dist/gem-runtime.js +94 -54
  5. package/dist/index.js +1668 -43
  6. package/dist/motion/core.d.ts +208 -0
  7. package/dist/motion/core.js +1 -0
  8. package/dist/motion/orbit.d.ts +4 -0
  9. package/dist/motion/orbit.js +1 -0
  10. package/dist/motion/quick-view.d.ts +4 -0
  11. package/dist/motion/quick-view.js +1 -0
  12. package/dist/motion/rail.d.ts +5 -0
  13. package/dist/motion/rail.js +1 -0
  14. package/dist/orbit-base.css +39 -0
  15. package/dist/plugins/before_after.js +1 -1
  16. package/dist/plugins/bundle.js +1 -1
  17. package/dist/plugins/carousel.js +1 -1
  18. package/dist/plugins/cart-drawer.js +1 -1
  19. package/dist/plugins/context-adapter.js +1 -1
  20. package/dist/plugins/countdown.js +1 -1
  21. package/dist/plugins/disclosure.js +1 -1
  22. package/dist/plugins/hero-slideshow.js +1 -1
  23. package/dist/plugins/img-in.js +1 -1
  24. package/dist/plugins/localization.js +1 -1
  25. package/dist/plugins/media-gallery.js +1 -1
  26. package/dist/plugins/mobile-menu.js +1 -1
  27. package/dist/plugins/offer-engine.js +1 -1
  28. package/dist/plugins/orbit.d.ts +9 -0
  29. package/dist/plugins/orbit.js +1 -0
  30. package/dist/plugins/predictive-search.js +1 -1
  31. package/dist/plugins/promo-popup.js +1 -1
  32. package/dist/plugins/quantity-input.js +1 -1
  33. package/dist/plugins/quick-view.js +1 -69
  34. package/dist/plugins/rail.d.ts +9 -0
  35. package/dist/plugins/rail.js +1 -0
  36. package/dist/plugins/recently-viewed.js +1 -1
  37. package/dist/plugins/reveal-on-scroll.js +1 -1
  38. package/dist/plugins/reviews.js +1 -1
  39. package/dist/plugins/runtime-slot-loader.js +1 -1
  40. package/dist/plugins/seasonal-effects.js +1 -1
  41. package/dist/plugins/size-guide.d.ts +9 -0
  42. package/dist/plugins/size-guide.js +38 -0
  43. package/dist/plugins/sticky-atc.js +1 -1
  44. package/dist/plugins/tabs.js +1 -1
  45. package/dist/plugins/variant-selects.js +1 -1
  46. package/dist/popup-shell.css +15 -0
  47. package/dist/primitives.css +4 -1
  48. package/dist/rail-base.css +48 -0
  49. package/dist/runtime-core.d.ts +2 -0
  50. package/dist/runtime-core.js +26 -15
  51. package/dist/section-gate.js +2368 -36
  52. package/dist/tokens.css +4 -0
  53. package/package.json +11 -1
@@ -1,69 +1 @@
1
- // @gemboss/gem-framework/plugins/quick-view: the gem-runtime plugin "quick-view" as an ES module. Generated from
2
- // packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.
3
- import { mountPlugin } from "../runtime-core.js";
4
-
5
- export const key = "quick-view";
6
- export const readyEvent = "gem:quick-view:ready";
7
-
8
- function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
9
- registerPlugin('quick-view', ({ api }) => {
10
- const triggers = document.querySelectorAll('[data-gem-quick-view], [data-gem-action="quick-view"]');
11
- const surface = document.querySelector('[data-gem-quick-view-surface], .gem-quick-view, .gem-modal[data-gem-component="quick-view"]');
12
- let bound = 0;
13
- const close = (reason = 'close-control') => {
14
- if (surface) {
15
- surface.dataset.gemQuickViewState = 'closed';
16
- surface.removeAttribute && surface.removeAttribute('open');
17
- surface.setAttribute && surface.setAttribute('aria-hidden', 'true');
18
- }
19
- api.emit('gem:quick-view:close', { source: 'gem-runtime', reason });
20
- };
21
- const openSurface = (detail) => {
22
- if (surface) {
23
- surface.dataset.gemQuickViewState = 'open';
24
- surface.dataset.gemProductHandle = detail.productHandle || '';
25
- surface.dataset.gemProductUrl = detail.productUrl || '';
26
- surface.setAttribute && surface.setAttribute('open', '');
27
- surface.setAttribute && surface.setAttribute('aria-hidden', 'false');
28
- surface.setAttribute && surface.setAttribute('role', 'dialog');
29
- surface.setAttribute && surface.setAttribute('aria-modal', 'true');
30
- const content = surface.querySelector && surface.querySelector('[data-gem-quick-view-content]');
31
- if (content && content.textContent !== undefined) content.textContent = 'Quick view for ' + (detail.productHandle || 'product');
32
- else if (surface.textContent !== undefined) surface.textContent = 'Quick view for ' + (detail.productHandle || 'product');
33
- const closeButton = surface.querySelector && surface.querySelector('[data-gem-quick-view-close]');
34
- if (closeButton && closeButton.focus) closeButton.focus();
35
- }
36
- api.emit('gem:quick-view:open', { source: 'gem-runtime', ...detail, hasSurface: Boolean(surface) });
37
- };
38
- triggers.forEach((trigger) => {
39
- if (!mark(trigger, 'quick-view')) return;
40
- if (!canListen(trigger)) return;
41
- __gemListen(trigger, 'click', (event) => {
42
- const productUrl = trigger.getAttribute && (trigger.getAttribute('data-gem-product-url') || trigger.getAttribute('href'));
43
- const productHandle = trigger.getAttribute && trigger.getAttribute('data-gem-product-handle');
44
- if (!productUrl && !productHandle) return;
45
- event.preventDefault && event.preventDefault();
46
- openSurface({ productUrl, productHandle });
47
- });
48
- bound += 1;
49
- });
50
- if (surface && mark(surface, 'quick-view-surface')) {
51
- surface.dataset.gemQuickViewState = 'closed';
52
- surface.setAttribute && surface.setAttribute('aria-hidden', 'true');
53
- if (canListen(surface)) __gemListen(surface, 'click', (event) => {
54
- const target = event.target;
55
- if (target && target.closest && target.closest('[data-gem-quick-view-close]')) close('close-control');
56
- });
57
- if (canListen(surface)) __gemListen(surface, 'keydown', (event) => { if (event.key === 'Escape') close('escape'); });
58
- }
59
- api.quickView = { open: (detail = {}) => openSurface({ ...detail }), close };
60
- api.emit('gem:quick-view:ready', { source: 'gem-runtime', count: bound, hasSurface: Boolean(surface) });
61
- });
62
- }
63
-
64
- /** Bind every "quick-view" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
65
- export function mount(root, options) {
66
- return mountPlugin(key, install, root, options);
67
- }
68
-
69
- export default mount;
1
+ import{mountPlugin as w}from"../runtime-core.js";import{mountEffects as b}from"../motion/core.js";import{quickViewEffect as h}from"../motion/quick-view.js";const _="quick-view",E="gem:quick-view:ready";function q({document:a,window:u,target:y,mark:m,canListen:c,listen:n,registerPlugin:v,registerNoopPlugin:A,onUnmount:f}){v("quick-view",({api:o})=>{const k=a.querySelectorAll('[data-gem-quick-view], [data-gem-action="quick-view"]'),e=a.querySelector('[data-gem-quick-view-surface], .gem-quick-view, .gem-modal[data-gem-component="quick-view"]');let l=0;const g=(t="close-control")=>{e&&(e.dataset.gemQuickViewState="closed",e.removeAttribute&&e.removeAttribute("open"),e.setAttribute&&e.setAttribute("aria-hidden","true")),o.emit("gem:quick-view:close",{source:"gem-runtime",reason:t})},d=t=>{if(e){e.dataset.gemQuickViewState="open",e.dataset.gemProductHandle=t.productHandle||"",e.dataset.gemProductUrl=t.productUrl||"",e.setAttribute&&e.setAttribute("open",""),e.setAttribute&&e.setAttribute("aria-hidden","false"),e.setAttribute&&e.setAttribute("role","dialog"),e.setAttribute&&e.setAttribute("aria-modal","true");const i=e.querySelector&&e.querySelector("[data-gem-quick-view-content]");i&&i.textContent!==void 0?i.textContent="Quick view for "+(t.productHandle||"product"):e.textContent!==void 0&&(e.textContent="Quick view for "+(t.productHandle||"product"));const r=e.querySelector&&e.querySelector("[data-gem-quick-view-close]");r&&r.focus&&r.focus()}o.emit("gem:quick-view:open",{source:"gem-runtime",...t,hasSurface:!!e})};k.forEach(t=>{m(t,"quick-view")&&c(t)&&(n(t,"click",i=>{const r=t.getAttribute&&(t.getAttribute("data-gem-product-url")||t.getAttribute("href")),s=t.getAttribute&&t.getAttribute("data-gem-product-handle");!r&&!s||(i.preventDefault&&i.preventDefault(),d({productUrl:r,productHandle:s}))}),l+=1)}),e&&m(e,"quick-view-surface")&&(e.dataset.gemQuickViewState="closed",e.setAttribute&&e.setAttribute("aria-hidden","true"),c(e)&&n(e,"click",t=>{const i=t.target;i&&i.closest&&i.closest("[data-gem-quick-view-close]")&&g("close-control")}),c(e)&&n(e,"keydown",t=>{t.key==="Escape"&&g("escape")})),(e||!o.quickView)&&(o.quickView={open:(t={})=>d({...t}),close:g});const p=b(h,{window:u,document:a,on:(t,i,r,s)=>{c(t)&&n(t,i,r,s)},mark:m,api:o,css:"[data-gem-quick-view-trigger]{--gem-quick-view-css:1}.gem-quick-view__shell{display:contents}.gem-quick-view__shell .gem-quick-view__dialog{overflow:visible;isolation:isolate;color:var(--gem-color-text);background:transparent;box-shadow:none;user-select:text;-webkit-user-select:text}.gem-quick-view__surface{position:absolute;inset:0;z-index:-1;border-radius:inherit;box-shadow:var(--gem-shadow-md);background:color-mix(in srgb,var(--gem-motion-tint,var(--gem-color-background)) 45%,var(--gem-color-background));transition:background-color var(--gem-motion-slow,0.5s) var(--gem-ease-soft,ease)}.gem-quick-view__shell .gem-quick-view__media{overflow:visible;background:transparent;min-height:0}.gem-quick-view__image{position:absolute;inset:6% 4%;width:92%;height:88%;object-fit:contain;transform-origin:50% 50%;will-change:transform}.gem-quick-view__image--next{opacity:0;pointer-events:none}.gem-quick-view__image--next:not([src]){display:none}.gem-quick-view__shell .gem-quick-view__panel{justify-content:flex-start;gap:var(--gem-space-sm)}.gem-quick-view__shell [hidden]{display:none}.gem-quick-view__bar{display:flex;align-items:center;gap:var(--gem-space-xs);font-family:var(--gem-font-mono);font-size:var(--gem-text-xs);font-variant-numeric:tabular-nums}.gem-quick-view__icon{display:inline-grid;place-items:center;width:var(--gem-tap-min);height:var(--gem-tap-min);padding:0;cursor:pointer;border:var(--gem-stroke,1px) solid color-mix(in srgb,currentColor 25%,transparent);border-radius:var(--gem-radius-pill);color:inherit;background:transparent;font:inherit;font-size:var(--gem-h5);line-height:1}.gem-quick-view__icon:hover{border-color:currentColor}.gem-quick-view__icon:focus-visible,.gem-quick-view__swatch:focus-visible,.gem-quick-view__size:focus-visible,.gem-quick-view__details:focus-visible{outline:var(--gem-focus-ring);outline-offset:var(--gem-focus-offset)}.gem-quick-view__eyebrow{margin:var(--gem-space-md) 0 0;color:var(--gem-color-muted);font-family:var(--gem-font-mono);font-size:var(--gem-text-xs);letter-spacing:0.12em;line-height:1.3;text-transform:uppercase}.gem-quick-view__title{margin:0;font-family:var(--gem-font-heading);font-size:var(--gem-h2);line-height:1.05}.gem-quick-view__price{margin:0;font-size:var(--gem-h5);font-variant-numeric:tabular-nums}.gem-quick-view__compare{margin:calc(-1 * var(--gem-space-xs)) 0 0;color:var(--gem-color-muted);font-size:var(--gem-text-sm)}.gem-quick-view__swatches,.gem-quick-view__sizes{display:flex;flex-wrap:wrap;gap:var(--gem-space-xs)}.gem-quick-view__swatch{position:relative;width:var(--gem-tap-min);height:var(--gem-tap-min);padding:0;border:0;color:inherit;background:transparent;cursor:pointer}.gem-quick-view__swatch::before{content:'';position:absolute;inset:8px;border-radius:var(--gem-radius-pill);background:var(--gem-quick-view-swatch,var(--gem-color-surface));box-shadow:inset 0 0 0 var(--gem-stroke,1px) color-mix(in srgb,var(--gem-color-text) 15%,transparent)}.gem-quick-view__swatch[aria-checked='true']::after{content:'';position:absolute;inset:3px;border:var(--gem-stroke-thick,2px) solid currentColor;border-radius:var(--gem-radius-pill)}.gem-quick-view__size{display:inline-grid;place-items:center;min-width:var(--gem-tap-min);height:var(--gem-tap-min);padding:0 var(--gem-space-sm);cursor:pointer;border:var(--gem-stroke,1px) solid color-mix(in srgb,currentColor 22%,transparent);border-radius:var(--gem-radius-pill);color:inherit;background:transparent;font:inherit;font-size:var(--gem-text-sm)}.gem-quick-view__size[aria-checked='true']{color:var(--gem-color-background);background:var(--gem-color-text);border-color:var(--gem-color-text)}.gem-quick-view__size:disabled{opacity:0.4;text-decoration:line-through;cursor:not-allowed}.gem-quick-view__add{width:100%;margin:var(--gem-space-xs) 0 0;font-variant-numeric:tabular-nums}.gem-quick-view__status{margin:0;min-height:1.2em;color:var(--gem-color-muted);font-size:var(--gem-text-sm)}.gem-quick-view__description{margin:0;max-width:46ch;font-size:var(--gem-text-sm);line-height:1.55}.gem-quick-view__details{color:inherit;font-size:var(--gem-text-sm);text-underline-offset:3px}[data-gem-quick-view-source]{visibility:hidden}@keyframes gem-quick-view-fade{from{opacity:0}to{opacity:1}}.gem-quick-view__dialog[data-gem-state='open']::backdrop{animation:gem-quick-view-fade var(--gem-motion-base,0.35s) ease both}.gem-quick-view__dialog[data-gem-state='closing']::backdrop{animation:gem-quick-view-fade var(--gem-motion-base,0.35s) ease reverse both}@media (max-width:749px){.gem-quick-view__shell .gem-quick-view__media{height:34dvh}}@media (prefers-reduced-motion:reduce){.gem-quick-view__dialog::backdrop{animation:none}.gem-quick-view__surface{transition:none}}"},f);o.emit("gem:quick-view:ready",{source:"gem-runtime",count:l+p,hasSurface:!!e})})}function x(a,u){return w(_,q,a,u)}var H=x;export{H as default,_ as key,x as mount,E as readyEvent};
@@ -0,0 +1,9 @@
1
+ // @gemboss/gem-framework/plugins/rail: types of the ES module next to this file. Generated; do not edit here.
2
+ import type { MountOptions, Unmount } from "../runtime-core.js";
3
+
4
+ export type { MountOptions, Unmount };
5
+ export declare const key: "rail";
6
+ export declare const readyEvent: "gem:rail:ready";
7
+ /** Bind every "rail" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
8
+ export declare function mount(root?: ParentNode | null, options?: MountOptions): Unmount;
9
+ export default mount;
@@ -0,0 +1 @@
1
+ import{mountPlugin as d}from"../runtime-core.js";import{mountEffects as f}from"../motion/core.js";import{railEffect as p}from"../motion/rail.js";const _="rail",P="gem:rail:ready";function v({document:e,window:t,target:x,mark:i,canListen:o,listen:n,registerPlugin:m,registerNoopPlugin:y,onUnmount:s}){m("rail",({api:a})=>{const g=f(p,{window:t,document:e,on:(r,l,c,u)=>{o(r)&&n(r,l,c,u)},mark:i,api:a,css:".gem-rail{--gem-rail-css:1;user-select:none;-webkit-user-select:none;transition:background-color var(--gem-motion-slow,0.6s) var(--gem-ease-soft,ease)}.gem-rail[data-gem-state] .gem-rail__item{will-change:transform}.gem-rail[data-gem-state] .gem-rail__stage{cursor:pointer}.gem-rail[data-gem-state] .gem-rail__hit{pointer-events:none;transition:opacity var(--gem-motion-base,0.35s) var(--gem-ease-soft,ease)}@container (max-width:699px){.gem-rail[data-gem-state] .gem-rail__hit{pointer-events:auto}}.gem-rail[data-gem-state='open'] .gem-rail__hit{opacity:0.15}@media (prefers-reduced-motion:reduce){.gem-rail,.gem-rail[data-gem-state] .gem-rail__hit{transition:none}}"},s);a.emit("gem:rail:ready",{source:"gem-runtime",count:g})})}function h(e,t){return d(_,v,e,t)}var N=h;export{N as default,_ as key,h as mount,P as readyEvent};
@@ -5,7 +5,7 @@ import { mountPlugin } from "../runtime-core.js";
5
5
  export const key = "recently-viewed";
6
6
  export const readyEvent = "gem:recently-viewed:ready";
7
7
 
8
- function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin, onUnmount }) {
9
9
  registerPlugin('recently-viewed', ({ api }) => {
10
10
  const storageKey = 'gem:recently-viewed-products';
11
11
  const readItems = () => {
@@ -5,7 +5,7 @@ import { mountPlugin } from "../runtime-core.js";
5
5
  export const key = "reveal-on-scroll";
6
6
  export const readyEvent = "gem:reveal:ready";
7
7
 
8
- function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin, onUnmount }) {
9
9
  registerPlugin('reveal-on-scroll', ({ api }) => {
10
10
  const nodes = document.querySelectorAll('[data-gem-reveal], .gem-reveal');
11
11
  let bound = 0;
@@ -5,7 +5,7 @@ import { mountPlugin } from "../runtime-core.js";
5
5
  export const key = "reviews";
6
6
  export const readyEvent = "gem:reviews:ready";
7
7
 
8
- function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin, onUnmount }) {
9
9
  registerPlugin('reviews', ({ api }) => {
10
10
  let boundReviews = 0;
11
11
  document.querySelectorAll('[data-rv-root]').forEach((root) => {
@@ -5,7 +5,7 @@ import { mountPlugin } from "../runtime-core.js";
5
5
  export const key = "runtime-slot-loader";
6
6
  export const readyEvent = "gem:slot:ready";
7
7
 
8
- function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin, onUnmount }) {
9
9
  registerPlugin('runtime-slot-loader', ({ api }) => {
10
10
  const slots = document.querySelectorAll('[data-gem-runtime-slot], [data-gem-footer-block-builder], .gem-navigation-shell__layout');
11
11
  let bound = 0;
@@ -5,7 +5,7 @@ import { mountPlugin } from "../runtime-core.js";
5
5
  export const key = "seasonal-effects";
6
6
  export const readyEvent = "gem:seasonal:ready";
7
7
 
8
- function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin, onUnmount }) {
9
9
  registerNoopPlugin("seasonal-effects", "gem:seasonal:ready");
10
10
  }
11
11
 
@@ -0,0 +1,9 @@
1
+ // @gemboss/gem-framework/plugins/size-guide: types of the ES module next to this file. Generated; do not edit here.
2
+ import type { MountOptions, Unmount } from "../runtime-core.js";
3
+
4
+ export type { MountOptions, Unmount };
5
+ export declare const key: "size-guide";
6
+ export declare const readyEvent: "gem:size-guide:ready";
7
+ /** Bind every "size-guide" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
8
+ export declare function mount(root?: ParentNode | null, options?: MountOptions): Unmount;
9
+ export default mount;
@@ -0,0 +1,38 @@
1
+ // @gemboss/gem-framework/plugins/size-guide: the gem-runtime plugin "size-guide" as an ES module. Generated from
2
+ // packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.
3
+ import { mountPlugin } from "../runtime-core.js";
4
+
5
+ export const key = "size-guide";
6
+ export const readyEvent = "gem:size-guide:ready";
7
+
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin, onUnmount }) {
9
+ registerPlugin('size-guide', ({ api }) => {
10
+ const page = document.documentElement && typeof document.documentElement.querySelectorAll === 'function' ? document.documentElement : document;
11
+ const guides = Array.prototype.slice.call(page.querySelectorAll('[data-gem-size-guide]'));
12
+ const opens = (target) => guides.some((guide) => !target || guide.getAttribute('data-gem-size-guide') === target || guide.id === target);
13
+ let bound = 0;
14
+ document.querySelectorAll('[data-gem-size-guide-trigger]').forEach((trigger) => {
15
+ const target = trigger.getAttribute('data-gem-size-guide-trigger') || '';
16
+ if (!opens(target)) { trigger.hidden = true; return; }
17
+ if (!mark(trigger, 'size-guide')) return;
18
+ trigger.hidden = false;
19
+ if (!canListen(trigger)) return;
20
+ __gemListen(trigger, 'click', (event) => {
21
+ event.preventDefault && event.preventDefault();
22
+ const holder = trigger.closest ? trigger.closest('[data-product-id]') : null;
23
+ const detail = { source: 'gem-runtime', sectionId: target || null, productId: holder ? holder.getAttribute('data-product-id') : null };
24
+ if (typeof window.CustomEvent === 'function') window.dispatchEvent(new window.CustomEvent('gem:size-guide:open', { detail }));
25
+ api.emit('gem:size-guide:open', detail);
26
+ });
27
+ bound += 1;
28
+ });
29
+ api.emit('gem:size-guide:ready', { source: 'gem-runtime', count: bound, guides: guides.length });
30
+ });
31
+ }
32
+
33
+ /** Bind every "size-guide" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
34
+ export function mount(root, options) {
35
+ return mountPlugin(key, install, root, options);
36
+ }
37
+
38
+ export default mount;
@@ -5,7 +5,7 @@ import { mountPlugin } from "../runtime-core.js";
5
5
  export const key = "sticky-atc";
6
6
  export const readyEvent = "gem:sticky-atc:ready";
7
7
 
8
- function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin, onUnmount }) {
9
9
  registerPlugin('sticky-atc', ({ api }) => {
10
10
  const bars = document.querySelectorAll('[data-gem-sticky-atc], .gem-sticky-atc');
11
11
  let bound = 0;
@@ -5,7 +5,7 @@ import { mountPlugin } from "../runtime-core.js";
5
5
  export const key = "tabs";
6
6
  export const readyEvent = "gem:tabs:ready";
7
7
 
8
- function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin, onUnmount }) {
9
9
  registerPlugin('tabs', ({ api }) => {
10
10
  const roots = document.querySelectorAll('[data-gem-tabs]');
11
11
  let bound = 0;
@@ -5,7 +5,7 @@ import { mountPlugin } from "../runtime-core.js";
5
5
  export const key = "variant-selects";
6
6
  export const readyEvent = "gem:variant:ready";
7
7
 
8
- function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin, onUnmount }) {
9
9
  registerPlugin('variant-selects', ({ api }) => {
10
10
  const selects = document.querySelectorAll('[data-gem-variant-select], select[name="id"]');
11
11
  let bound = 0;
@@ -58,6 +58,19 @@
58
58
  .gem-popup-shell[data-gem-popup-media='background']:is([data-gem-popup-desktop='split-left'], [data-gem-popup-desktop='split-right'], [data-gem-popup-desktop='card']) .gem-popup-shell__close { color: var(--gem-color-text); }
59
59
  .gem-popup-shell[data-gem-popup-media='background']:is([data-gem-popup-desktop='split-left'], [data-gem-popup-desktop='split-right'], [data-gem-popup-desktop='card']) .gem-popup-shell__media::after { content: none; }
60
60
  }
61
+ @keyframes gem-popup-drawer-in { from { translate: calc(100% * var(--gem-popup-drawer-dir, 1)) 0; } }
62
+ .gem-popup-shell__dialog:dir(rtl) { --gem-popup-drawer-dir: -1; }
63
+ @media (min-width: 750px) {
64
+ .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__dialog {
65
+ width: min(var(--gem-popup-drawer-width, 640px), 100vw); max-width: 100vw; height: 100dvh; max-height: 100dvh;
66
+ margin-block: 0; margin-inline: auto 0; border-radius: 0;
67
+ animation: gem-popup-drawer-in var(--gem-motion-base, 180ms) var(--gem-ease-enter, ease-out);
68
+ }
69
+ .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__frame { grid-template-columns: 1fr; align-content: start; min-height: 100%; }
70
+ .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__media { max-height: 240px; }
71
+ .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__panel { justify-content: flex-start; align-items: stretch; text-align: start; }
72
+ }
73
+ @media (prefers-reduced-motion: reduce) { .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__dialog { animation: none; } }
61
74
  @media (max-width: 749px) {
62
75
  .gem-popup-shell__dialog { width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; margin: 0; border-radius: 0; }
63
76
  .gem-popup-shell[data-gem-popup-mobile='sheet'] .gem-popup-shell__dialog { height: auto; max-height: 88dvh; margin: auto 0 0; border-radius: var(--gem-radius-lg) var(--gem-radius-lg) 0 0; }
@@ -81,6 +94,8 @@
81
94
  @media (min-width: 750px) {
82
95
  :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] { padding: 0; }
83
96
  :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] .gem-popup-shell__dialog { height: 720px; max-height: none; }
97
+ :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='drawer-end'] { padding-inline: 0; }
98
+ :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='drawer-end'] .gem-popup-shell__dialog { height: 720px; max-height: none; animation: none; }
84
99
  }
85
100
  @media (max-width: 749px) {
86
101
  :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'] { padding: 0; }
@@ -103,6 +103,9 @@ a {
103
103
  cursor: not-allowed;
104
104
  opacity: 0.55;
105
105
  }
106
+ [data-gem-button-shape='square'] { --gem-radius-button: 0px; }
107
+ [data-gem-button-shape='soft'] { --gem-radius-button: var(--gem-radius-sm); }
108
+ [data-gem-button-shape='pill'] { --gem-radius-button: 999px; }
106
109
  .gem-input:disabled, .gem-select:disabled, .gem-textarea:disabled, .gem-quantity-input__button:disabled {
107
110
  cursor: not-allowed;
108
111
  opacity: 0.6;
@@ -1068,7 +1071,7 @@ a {
1068
1071
  :where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }
1069
1072
  :where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }
1070
1073
  .gem-mark-link { color: var(--gem-color-accent); text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }
1071
- .gem-mark-link:hover { text-decoration-thickness: 2px; }
1074
+ .gem-mark-link:hover { text-decoration-thickness: var(--gem-underline-thickness, 2px); }
1072
1075
  .gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }
1073
1076
  @keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }
1074
1077
  @keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
@@ -0,0 +1,48 @@
1
+ .gem-rail { display: block; position: relative; container-type: inline-size; background-color: var(--gem-motion-tint, transparent); }
2
+ .gem-rail__stage { position: relative; height: var(--gem-rail-stage-height, clamp(380px, 62vh, 640px)); overflow: hidden; container-type: size; }
3
+ .gem-rail__rod { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
4
+ .gem-rail__rod path { fill: none; stroke: var(--gem-rail-rod, currentColor); stroke-width: 5; stroke-linecap: round; vector-effect: non-scaling-stroke; opacity: 0.85; }
5
+ .gem-rail__track { list-style: none; margin: 0; padding: 0; position: absolute; inset: 0; }
6
+ .gem-rail__item {
7
+ --gem-rail-t: calc(0.04 + 0.92 * (var(--gem-rail-i, 0) + 0.5) / var(--gem-rail-n, 1));
8
+ position: absolute; left: 0; top: 0; width: 0; height: 0; z-index: 10;
9
+ transform: translate3d(calc(var(--gem-rail-t) * 100cqw), calc((0.14 + 4 * var(--gem-rail-sag, 0.07) * var(--gem-rail-t) * (1 - var(--gem-rail-t))) * 100cqh), 0);
10
+ }
11
+ .gem-rail__hit {
12
+ position: absolute; left: 0; top: -6px; width: calc(var(--gem-rail-height, 0.7) * 78cqh); height: calc(var(--gem-rail-height, 0.7) * 100cqh + 34px);
13
+ padding: 0; border: 0; color: inherit; background: transparent; text-decoration: none; cursor: pointer; transform: translateX(-50%);
14
+ }
15
+ .gem-rail__hit:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); border-radius: var(--gem-radius-md); }
16
+ .gem-rail__body { position: absolute; inset: 0; display: block; transform-origin: 50% 0; }
17
+ .gem-rail__hook {
18
+ position: absolute; left: calc(50% - 8px); top: 0; width: 16px; height: 22px; opacity: 0.8;
19
+ border: var(--gem-stroke-thick, 2px) solid var(--gem-rail-rod, currentColor); border-bottom: 0; border-radius: var(--gem-radius-md) var(--gem-radius-md) 0 0;
20
+ }
21
+ .gem-rail__hook::after { content: ''; position: absolute; left: calc(50% - 23px); top: 18px; width: 46px; height: var(--gem-stroke-thick, 2px); background: var(--gem-rail-rod, currentColor); border-radius: var(--gem-radius-sm); }
22
+ .gem-rail__tag {
23
+ position: absolute; left: calc(50% + 14px); top: 18px; z-index: 2; padding: var(--gem-space-2xs) var(--gem-space-xs); transform: rotate(4deg);
24
+ font-family: var(--gem-font-mono); font-size: var(--gem-text-xs); font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums;
25
+ color: var(--gem-color-text); background: var(--gem-color-background); border-radius: var(--gem-radius-sm); box-shadow: var(--gem-shadow-sm);
26
+ }
27
+ .gem-rail .gem-turn { position: absolute; inset: 26px 0 0; width: auto; height: auto; }
28
+ .gem-rail__caption { margin: var(--gem-space-xs) 0 0; min-height: 1.5em; text-align: center; font-variant-numeric: tabular-nums; }
29
+ @container (max-width: 699px) {
30
+ .gem-rail__stage { height: auto; overflow: visible; container-type: normal; }
31
+ .gem-rail__rod { display: none; }
32
+ .gem-rail__track { position: static; display: flex; gap: var(--gem-space-2xs); overflow-x: auto; scroll-snap-type: x mandatory; padding: 0 20cqw var(--gem-space-xs); scrollbar-width: none; }
33
+ .gem-rail__track::-webkit-scrollbar { display: none; }
34
+ .gem-rail__item { position: relative; flex: 0 0 60cqw; width: auto; height: calc(72cqw + 34px); transform: none; scroll-snap-align: center; border-top: var(--gem-stroke-thick, 4px) solid var(--gem-rail-rod, currentColor); }
35
+ .gem-rail__hit { left: 50%; width: 100%; height: 100%; }
36
+ .gem-rail__item:first-child .gem-turn__side { opacity: 0; transform: perspective(900px) rotateY(-82deg); }
37
+ .gem-rail__item:first-child .gem-turn:has(> .gem-turn__side) > .gem-turn__front { opacity: 1; transform: scale(1); }
38
+ .gem-rail__item:first-child .gem-turn:not(:has(> .gem-turn__side)) > .gem-turn__front { transform: perspective(900px) rotateY(0deg) scale(1); }
39
+ .gem-rail__item:first-child .gem-turn__sheet { transform: translate(calc(-100% / var(--gem-turn-cols, 1) * mod(var(--gem-turn-frames, 1) - 1, var(--gem-turn-cols, 1))), calc(-100% / var(--gem-turn-rows, 1) * round(down, (var(--gem-turn-frames, 1) - 1) / var(--gem-turn-cols, 1)))); }
40
+ }
41
+ .gem-turn { position: relative; display: block; width: 100%; height: 100%; }
42
+ .gem-turn[data-gem-turn-frames] { overflow: hidden; }
43
+ .gem-turn__side, .gem-turn__front { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: contain; object-position: 50% 0; pointer-events: none; }
44
+ .gem-turn__side { transform: perspective(900px) rotateY(0deg); backface-visibility: hidden; }
45
+ .gem-turn:has(> .gem-turn__side) > .gem-turn__front { opacity: 0; transform: scale(0.9); }
46
+ .gem-turn:not(:has(> .gem-turn__side)) > .gem-turn__front { transform: perspective(900px) rotateY(-74deg) scale(0.86); }
47
+ .gem-turn__sheet { position: absolute; left: 0; top: 0; display: block; max-width: none; pointer-events: none; width: calc(var(--gem-turn-cols, 1) * 100%); height: calc(var(--gem-turn-rows, 1) * 100%); }
48
+ .gem-turn__sheet + .gem-turn__sheet { opacity: 0; }
@@ -38,6 +38,8 @@ export interface PluginInstallScope {
38
38
  listen(element: EventTarget, type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
39
39
  registerPlugin(key: string, setup: (context: { api: GemApi; config: unknown; reason: string }) => void): void;
40
40
  registerNoopPlugin(key: string, readyEvent?: string): void;
41
+ /** Run `cleanup` at unmount (0.19.0), for what is not a listener, timer or observer. */
42
+ onUnmount(cleanup: () => void): void;
41
43
  }
42
44
 
43
45
  export declare function createGemApi(doc?: Document): GemApi;
@@ -4,7 +4,8 @@
4
4
  // that body the names it expects (`document`, `window`, `target`, `mark`, `canListen`, `registerPlugin`) scoped to
5
5
  // one root, and keeps a ledger of what the body did so unmount() can undo it:
6
6
  // - listeners (the module wraps every `x.addEventListener(` of the body as `__gemListen(x, `), timers,
7
- // animation frames and IntersectionObservers made through `window` / `target`;
7
+ // animation frames and Intersection / Resize observers made through `window` / `target`;
8
+ // - what the body hands to onUnmount(fn) (0.19.0);
8
9
  // - the `data-gem-bound-<key>` marks it set, so the same root can be mounted again after an unmount.
9
10
  // Importing this file does nothing: no global is read or written until mount() is called.
10
11
 
@@ -52,6 +53,21 @@ function scopedDocument(root, doc) {
52
53
  });
53
54
  }
54
55
 
56
+ // An observer class whose instances the ledger disconnects at unmount.
57
+ function trackedObserver(Base, ledger) {
58
+ if (typeof Base !== "function") return undefined;
59
+ return class extends Base {
60
+ constructor(callback, init) {
61
+ super(callback, init);
62
+ ledger.observers.add(this);
63
+ }
64
+ disconnect() {
65
+ ledger.observers.delete(this);
66
+ super.disconnect();
67
+ }
68
+ };
69
+ }
70
+
55
71
  // `window` / `target` for a plugin: the real window, except that timers, frames and observers made through it are
56
72
  // remembered so unmount() can stop them.
57
73
  function trackedWindow(win, ledger) {
@@ -75,18 +91,9 @@ function trackedWindow(win, ledger) {
75
91
  return id;
76
92
  }
77
93
  : undefined,
78
- IntersectionObserver: typeof win.IntersectionObserver === "function"
79
- ? class extends win.IntersectionObserver {
80
- constructor(callback, init) {
81
- super(callback, init);
82
- ledger.observers.add(this);
83
- }
84
- disconnect() {
85
- ledger.observers.delete(this);
86
- super.disconnect();
87
- }
88
- }
89
- : undefined,
94
+ IntersectionObserver: trackedObserver(win.IntersectionObserver, ledger),
95
+ // 0.19.0: the motion effects watch their root's size.
96
+ ResizeObserver: trackedObserver(win.ResizeObserver, ledger),
90
97
  };
91
98
  return new Proxy(win, {
92
99
  get: (real, prop) => (Object.prototype.hasOwnProperty.call(own, prop) ? own[prop] : bindMethod(real, Reflect.get(real, prop), prop)),
@@ -116,7 +123,7 @@ export function mountPlugin(key, install, root, options = {}, guard) {
116
123
  const doc = scope.nodeType === 9 ? scope : scope.ownerDocument || globalThis.document;
117
124
  const win = options.window || (doc && doc.defaultView) || globalThis;
118
125
  const api = options.api || (doc === globalThis.document ? gemApi() : createGemApi(doc));
119
- const ledger = { listeners: [], marks: [], timeouts: new Set(), intervals: new Set(), frames: new Set(), observers: new Set() };
126
+ const ledger = { listeners: [], marks: [], timeouts: new Set(), intervals: new Set(), frames: new Set(), observers: new Set(), cleanups: [] };
120
127
  const window = trackedWindow(win, ledger);
121
128
  const document = scope === doc ? doc : scopedDocument(scope, doc);
122
129
  // `data-gem-bound-<key>` with ANY value means bound, including one the markup carries to claim an element for its own
@@ -140,7 +147,11 @@ export function mountPlugin(key, install, root, options = {}, guard) {
140
147
  };
141
148
  const unguarded = guard ? Array.from(document.querySelectorAll(guard.selector)).filter((element) => !element[guard.prop]) : [];
142
149
 
150
+ // 0.19.0: a plugin hands back what is not a listener, timer or observer (the motion effects' scheduler tasks, host API).
151
+ const onUnmount = (fn) => { if (typeof fn === "function") ledger.cleanups.push(fn); };
152
+
143
153
  const teardown = () => {
154
+ ledger.cleanups.splice(0).reverse().forEach((fn) => fn());
144
155
  ledger.timeouts.forEach((id) => win.clearTimeout(id));
145
156
  ledger.intervals.forEach((id) => win.clearInterval(id));
146
157
  ledger.frames.forEach((id) => win.cancelAnimationFrame && win.cancelAnimationFrame(id));
@@ -150,7 +161,7 @@ export function mountPlugin(key, install, root, options = {}, guard) {
150
161
  if (guard) unguarded.forEach((element) => { delete element[guard.prop]; });
151
162
  };
152
163
  try {
153
- install({ document, window, target: window, mark, canListen, listen, registerPlugin, registerNoopPlugin });
164
+ install({ document, window, target: window, mark, canListen, listen, registerPlugin, registerNoopPlugin, onUnmount });
154
165
  if (typeof setup === "function") {
155
166
  setup({ api, config: { planId: "gem-plugin-module", plugins: [{ key, loadStrategy: "mount", dependsOn: [] }] }, reason: "mount" });
156
167
  }