@michaelyagi/shoji 0.1.0-beta.13 → 0.1.0-beta.14

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.
@@ -2,6 +2,23 @@
2
2
  typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, global.Shoji = global.Shoji || {}, global.Shoji.Fullscreen = factory());
3
3
  })(this, (function() {
4
4
  "use strict";
5
+ function createIconSwap(iconOff, iconOn) {
6
+ const el = document.createElement("span");
7
+ el.className = "shoji-icon-swap";
8
+ const off = document.createElement("span");
9
+ off.className = "shoji-icon-swap-icon shoji-icon-swap-icon--off";
10
+ off.innerHTML = iconOff;
11
+ const on = document.createElement("span");
12
+ on.className = "shoji-icon-swap-icon shoji-icon-swap-icon--on";
13
+ on.innerHTML = iconOn;
14
+ el.append(off, on);
15
+ return {
16
+ el,
17
+ setState(state) {
18
+ el.classList.toggle("shoji-icon-swap--on", state);
19
+ }
20
+ };
21
+ }
5
22
  const EXPAND_ICON = '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg>';
6
23
  const COMPRESS_ICON = '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg>';
7
24
  function isSupported() {
@@ -35,7 +52,8 @@
35
52
  const button = document.createElement("button");
36
53
  button.type = "button";
37
54
  button.className = "shoji-toolbar-button";
38
- button.innerHTML = EXPAND_ICON;
55
+ const iconSwap = createIconSwap(EXPAND_ICON, COMPRESS_ICON);
56
+ button.appendChild(iconSwap.el);
39
57
  button.setAttribute("aria-label", enterLabel);
40
58
  button.title = enterLabel;
41
59
  button.setAttribute("aria-pressed", "false");
@@ -43,7 +61,7 @@
43
61
  return currentFullscreenElement() === outer;
44
62
  }
45
63
  function setState(active) {
46
- button.innerHTML = active ? COMPRESS_ICON : EXPAND_ICON;
64
+ iconSwap.setState(active);
47
65
  button.setAttribute("aria-label", active ? exitLabel : enterLabel);
48
66
  button.title = active ? exitLabel : enterLabel;
49
67
  button.setAttribute("aria-pressed", String(active));
@@ -1 +1 @@
1
- {"version":3,"file":"fullscreen.js","sources":["../../src/plugins/fullscreen/icons.ts","../../src/plugins/fullscreen/index.ts"],"sourcesContent":["/** DESIGN.md §9 — inline SVG, stroke = currentColor, matches src/core/icons.ts's convention. Four corner-arrow glyphs, a generic \"expand\"/\"collapse\" pair, not tied to any particular icon set. */\nexport const EXPAND_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5\"/></svg>';\n\nexport const COMPRESS_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5\"/></svg>';\n","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\nimport { COMPRESS_ICON, EXPAND_ICON } from './icons';\n\n/**\n * Vendor-prefixed Fullscreen API surface (older Safari) — not in standard\n * `lib.dom.d.ts`, so this is a documented interop boundary (CLAUDE.md: `any`\n * only there). Everything else in this file uses the standard,\n * already-typed `Element.requestFullscreen`/`Document.exitFullscreen`/\n * `Document.fullscreenElement`/`Document.fullscreenEnabled`.\n */\ninterface WebkitFullscreenElement extends HTMLElement {\n webkitRequestFullscreen?: () => void;\n}\ninterface WebkitFullscreenDocument extends Document {\n webkitExitFullscreen?: () => void;\n webkitFullscreenElement?: Element | null;\n webkitFullscreenEnabled?: boolean;\n}\n\nfunction isSupported(): boolean {\n return !!(\n document.fullscreenEnabled || (document as WebkitFullscreenDocument).webkitFullscreenEnabled\n );\n}\n\nfunction currentFullscreenElement(): Element | null {\n return (\n document.fullscreenElement ??\n (document as WebkitFullscreenDocument).webkitFullscreenElement ??\n null\n );\n}\n\nfunction requestFullscreen(el: HTMLElement): void {\n const target = el as WebkitFullscreenElement;\n if (target.requestFullscreen) target.requestFullscreen().catch(() => {});\n else target.webkitRequestFullscreen?.();\n}\n\nfunction exitFullscreen(): void {\n const doc = document as WebkitFullscreenDocument;\n if (document.exitFullscreen) document.exitFullscreen().catch(() => {});\n else doc.webkitExitFullscreen?.();\n}\n\n/**\n * DESIGN.md §4 — native Fullscreen API toggle, `.shoji-outer` (`ctx.ui.outer()`,\n * the whole lightbox — backdrop, dialog, controls) is what goes fullscreen,\n * not just the slide media. No toolbar button at all — not a disabled one —\n * on a browser with no Fullscreen API support (checked once at `init()`;\n * historically Safari on iOS), rather than shipping a dead control.\n *\n * `fullscreenchange` (plus the `webkitfullscreenchange` vendor variant) is\n * the single source of truth for button state, not the click handler\n * directly — a real Fullscreen API `request`/`exit` call can be rejected\n * (permission, another element already fullscreen, etc.), and the browser's\n * own Escape-key fullscreen exit doesn't go through this plugin's click\n * handler at all. Reacting to the event instead of the request keeps the\n * button correct regardless of *why* fullscreen state changed.\n */\nexport const Fullscreen: ShojiPlugin = {\n name: 'fullscreen',\n\n init(ctx: PluginContext): (() => void) | void {\n if (!isSupported()) return;\n\n const outer = ctx.ui.outer();\n const locale = (ctx.gallery.options.locale ?? {}) as Record<string, string>;\n const enterLabel = locale.enterFullscreen ?? 'Enter fullscreen';\n const exitLabel = locale.exitFullscreen ?? 'Exit fullscreen';\n\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'shoji-toolbar-button';\n button.innerHTML = EXPAND_ICON;\n button.setAttribute('aria-label', enterLabel);\n button.title = enterLabel;\n button.setAttribute('aria-pressed', 'false');\n\n function isActive(): boolean {\n return currentFullscreenElement() === outer;\n }\n\n function setState(active: boolean): void {\n button.innerHTML = active ? COMPRESS_ICON : EXPAND_ICON;\n button.setAttribute('aria-label', active ? exitLabel : enterLabel);\n button.title = active ? exitLabel : enterLabel;\n button.setAttribute('aria-pressed', String(active));\n }\n\n /** Shared by the toolbar button and `requestFullscreenToggle` below. */\n function toggleFullscreen(): void {\n if (isActive()) exitFullscreen();\n else requestFullscreen(outer);\n }\n button.addEventListener('click', toggleFullscreen);\n\n const onChange = (): void => {\n const active = isActive();\n setState(active);\n ctx.emit('fullscreenChange', { fullscreen: active });\n };\n document.addEventListener('fullscreenchange', onChange);\n document.addEventListener('webkitfullscreenchange', onChange);\n\n // 'right' — clusters immediately before the close button (DESIGN.md §3.1).\n const removeButton = ctx.ui.toolbar('right', button);\n\n // Browser-level Escape-to-exit-fullscreen behavior is inconsistent\n // across browsers (some exit fullscreen and still deliver the page its\n // own Escape keydown, some don't) — closing the gallery explicitly\n // exits fullscreen too, rather than risking the page getting stuck\n // fullscreen-on-nothing after the lightbox itself is gone.\n const offClose = ctx.on('close', () => {\n if (isActive()) exitFullscreen();\n });\n /**\n * A generic command surface, requested directly (DESIGN.md §4.4), so a\n * *custom* (host-authored) plugin's own button can toggle fullscreen\n * without importing this plugin at all — same \"events over\n * inheritance\" decoupling `close` above already uses.\n * `GalleryEvents` (`core/types.ts`) already extends `Record<string,\n * unknown>`, so `ctx.emit('requestFullscreenToggle', {})` from any\n * plugin — official or custom — type-checks with zero core changes;\n * this is just the listening half.\n */\n const offRequestToggle = ctx.on('requestFullscreenToggle', toggleFullscreen);\n\n return () => {\n document.removeEventListener('fullscreenchange', onChange);\n document.removeEventListener('webkitfullscreenchange', onChange);\n removeButton();\n offClose();\n offRequestToggle();\n if (isActive()) exitFullscreen();\n };\n },\n};\n"],"names":[],"mappings":";;;;AACO,QAAM,cACX;AAEK,QAAM,gBACX;ACcF,WAAS,cAAuB;AAC9B,WAAO,CAAC,EACN,SAAS,qBAAsB,SAAsC;AAAA,EAEzE;AAEA,WAAS,2BAA2C;AAClD,WACE,SAAS,qBACR,SAAsC,2BACvC;AAAA,EAEJ;AAEA,WAAS,kBAAkB,IAAuB;;AAChD,UAAM,SAAS;AACf,QAAI,OAAO,kBAAmB,QAAO,kBAAA,EAAoB,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,sBAC3D;EACd;AAEA,WAAS,iBAAuB;;AAC9B,UAAM,MAAM;AACZ,QAAI,SAAS,eAAgB,UAAS,eAAA,EAAiB,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,mBAC5D;EACX;AAiBO,QAAM,aAA0B;AAAA,IACrC,MAAM;AAAA,IAEN,KAAK,KAAyC;AAC5C,UAAI,CAAC,cAAe;AAEpB,YAAM,QAAQ,IAAI,GAAG,MAAA;AACrB,YAAM,SAAU,IAAI,QAAQ,QAAQ,UAAU,CAAA;AAC9C,YAAM,aAAa,OAAO,mBAAmB;AAC7C,YAAM,YAAY,OAAO,kBAAkB;AAE3C,YAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,aAAO,OAAO;AACd,aAAO,YAAY;AACnB,aAAO,YAAY;AACnB,aAAO,aAAa,cAAc,UAAU;AAC5C,aAAO,QAAQ;AACf,aAAO,aAAa,gBAAgB,OAAO;AAE3C,eAAS,WAAoB;AAC3B,eAAO,+BAA+B;AAAA,MACxC;AAEA,eAAS,SAAS,QAAuB;AACvC,eAAO,YAAY,SAAS,gBAAgB;AAC5C,eAAO,aAAa,cAAc,SAAS,YAAY,UAAU;AACjE,eAAO,QAAQ,SAAS,YAAY;AACpC,eAAO,aAAa,gBAAgB,OAAO,MAAM,CAAC;AAAA,MACpD;AAGA,eAAS,mBAAyB;AAChC,YAAI,SAAA,EAAY,gBAAA;AAAA,+BACO,KAAK;AAAA,MAC9B;AACA,aAAO,iBAAiB,SAAS,gBAAgB;AAEjD,YAAM,WAAW,MAAY;AAC3B,cAAM,SAAS,SAAA;AACf,iBAAS,MAAM;AACf,YAAI,KAAK,oBAAoB,EAAE,YAAY,QAAQ;AAAA,MACrD;AACA,eAAS,iBAAiB,oBAAoB,QAAQ;AACtD,eAAS,iBAAiB,0BAA0B,QAAQ;AAG5D,YAAM,eAAe,IAAI,GAAG,QAAQ,SAAS,MAAM;AAOnD,YAAM,WAAW,IAAI,GAAG,SAAS,MAAM;AACrC,YAAI,SAAA,EAAY,gBAAA;AAAA,MAClB,CAAC;AAWD,YAAM,mBAAmB,IAAI,GAAG,2BAA2B,gBAAgB;AAE3E,aAAO,MAAM;AACX,iBAAS,oBAAoB,oBAAoB,QAAQ;AACzD,iBAAS,oBAAoB,0BAA0B,QAAQ;AAC/D,qBAAA;AACA,iBAAA;AACA,yBAAA;AACA,YAAI,SAAA,EAAY,gBAAA;AAAA,MAClB;AAAA,IACF;AAAA,EACF;;;"}
1
+ {"version":3,"file":"fullscreen.js","sources":["../../src/core/iconSwap.ts","../../src/plugins/fullscreen/icons.ts","../../src/plugins/fullscreen/index.ts"],"sourcesContent":["/**\n * DESIGN.md §9 — a toolbar button whose icon reflects live state (Fullscreen's\n * enter/exit, Autoplay's play/pause, Zoom's actual-size expand/contract) used\n * to just replace `button.innerHTML` outright — an instant cut, no\n * transition, and each plugin reimplementing the same two-icon swap\n * independently. Requested directly: make every one of these a smooth\n * cross-fade, and make them all go through one shared mechanism rather than\n * three slightly different ones. Both icons are always in the DOM at once,\n * stacked via `position: absolute` inside a fixed-size wrapper\n * (`.shoji-icon-swap`, `shoji.css`) — swapping is just toggling which one is\n * `opacity: 1` via a single modifier class, so there's nothing to actually\n * replace, no risk of a flash-of-no-icon between the old node being removed\n * and the new one's fill/stroke painting in.\n */\nexport interface IconSwap {\n /** Append this into the button — it *is* the button's visible icon content. */\n el: HTMLSpanElement;\n /** `false` shows `iconOff`, `true` shows `iconOn` — cross-fades between them via CSS, respecting `prefers-reduced-motion` the same way every other transition in this codebase does (shoji.css zeroes `--shoji-icon-swap-duration` there). */\n setState(on: boolean): void;\n}\n\nexport function createIconSwap(iconOff: string, iconOn: string): IconSwap {\n const el = document.createElement('span');\n el.className = 'shoji-icon-swap';\n\n const off = document.createElement('span');\n off.className = 'shoji-icon-swap-icon shoji-icon-swap-icon--off';\n off.innerHTML = iconOff;\n\n const on = document.createElement('span');\n on.className = 'shoji-icon-swap-icon shoji-icon-swap-icon--on';\n on.innerHTML = iconOn;\n\n el.append(off, on);\n\n return {\n el,\n setState(state: boolean): void {\n el.classList.toggle('shoji-icon-swap--on', state);\n },\n };\n}\n","/** DESIGN.md §9 — inline SVG, stroke = currentColor, matches src/core/icons.ts's convention. Four corner-arrow glyphs, a generic \"expand\"/\"collapse\" pair, not tied to any particular icon set. */\nexport const EXPAND_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5\"/></svg>';\n\nexport const COMPRESS_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5\"/></svg>';\n","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\nimport { createIconSwap } from '../../core/iconSwap';\nimport { COMPRESS_ICON, EXPAND_ICON } from './icons';\n\n/**\n * Vendor-prefixed Fullscreen API surface (older Safari) — not in standard\n * `lib.dom.d.ts`, so this is a documented interop boundary (CLAUDE.md: `any`\n * only there). Everything else in this file uses the standard,\n * already-typed `Element.requestFullscreen`/`Document.exitFullscreen`/\n * `Document.fullscreenElement`/`Document.fullscreenEnabled`.\n */\ninterface WebkitFullscreenElement extends HTMLElement {\n webkitRequestFullscreen?: () => void;\n}\ninterface WebkitFullscreenDocument extends Document {\n webkitExitFullscreen?: () => void;\n webkitFullscreenElement?: Element | null;\n webkitFullscreenEnabled?: boolean;\n}\n\nfunction isSupported(): boolean {\n return !!(\n document.fullscreenEnabled || (document as WebkitFullscreenDocument).webkitFullscreenEnabled\n );\n}\n\nfunction currentFullscreenElement(): Element | null {\n return (\n document.fullscreenElement ??\n (document as WebkitFullscreenDocument).webkitFullscreenElement ??\n null\n );\n}\n\nfunction requestFullscreen(el: HTMLElement): void {\n const target = el as WebkitFullscreenElement;\n if (target.requestFullscreen) target.requestFullscreen().catch(() => {});\n else target.webkitRequestFullscreen?.();\n}\n\nfunction exitFullscreen(): void {\n const doc = document as WebkitFullscreenDocument;\n if (document.exitFullscreen) document.exitFullscreen().catch(() => {});\n else doc.webkitExitFullscreen?.();\n}\n\n/**\n * DESIGN.md §4 — native Fullscreen API toggle, `.shoji-outer` (`ctx.ui.outer()`,\n * the whole lightbox — backdrop, dialog, controls) is what goes fullscreen,\n * not just the slide media. No toolbar button at all — not a disabled one —\n * on a browser with no Fullscreen API support (checked once at `init()`;\n * historically Safari on iOS), rather than shipping a dead control.\n *\n * `fullscreenchange` (plus the `webkitfullscreenchange` vendor variant) is\n * the single source of truth for button state, not the click handler\n * directly — a real Fullscreen API `request`/`exit` call can be rejected\n * (permission, another element already fullscreen, etc.), and the browser's\n * own Escape-key fullscreen exit doesn't go through this plugin's click\n * handler at all. Reacting to the event instead of the request keeps the\n * button correct regardless of *why* fullscreen state changed.\n */\nexport const Fullscreen: ShojiPlugin = {\n name: 'fullscreen',\n\n init(ctx: PluginContext): (() => void) | void {\n if (!isSupported()) return;\n\n const outer = ctx.ui.outer();\n const locale = (ctx.gallery.options.locale ?? {}) as Record<string, string>;\n const enterLabel = locale.enterFullscreen ?? 'Enter fullscreen';\n const exitLabel = locale.exitFullscreen ?? 'Exit fullscreen';\n\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'shoji-toolbar-button';\n const iconSwap = createIconSwap(EXPAND_ICON, COMPRESS_ICON);\n button.appendChild(iconSwap.el);\n button.setAttribute('aria-label', enterLabel);\n button.title = enterLabel;\n button.setAttribute('aria-pressed', 'false');\n\n function isActive(): boolean {\n return currentFullscreenElement() === outer;\n }\n\n function setState(active: boolean): void {\n iconSwap.setState(active);\n button.setAttribute('aria-label', active ? exitLabel : enterLabel);\n button.title = active ? exitLabel : enterLabel;\n button.setAttribute('aria-pressed', String(active));\n }\n\n /** Shared by the toolbar button and `requestFullscreenToggle` below. */\n function toggleFullscreen(): void {\n if (isActive()) exitFullscreen();\n else requestFullscreen(outer);\n }\n button.addEventListener('click', toggleFullscreen);\n\n const onChange = (): void => {\n const active = isActive();\n setState(active);\n ctx.emit('fullscreenChange', { fullscreen: active });\n };\n document.addEventListener('fullscreenchange', onChange);\n document.addEventListener('webkitfullscreenchange', onChange);\n\n // 'right' — clusters immediately before the close button (DESIGN.md §3.1).\n const removeButton = ctx.ui.toolbar('right', button);\n\n // Browser-level Escape-to-exit-fullscreen behavior is inconsistent\n // across browsers (some exit fullscreen and still deliver the page its\n // own Escape keydown, some don't) — closing the gallery explicitly\n // exits fullscreen too, rather than risking the page getting stuck\n // fullscreen-on-nothing after the lightbox itself is gone.\n const offClose = ctx.on('close', () => {\n if (isActive()) exitFullscreen();\n });\n /**\n * A generic command surface, requested directly (DESIGN.md §4.4), so a\n * *custom* (host-authored) plugin's own button can toggle fullscreen\n * without importing this plugin at all — same \"events over\n * inheritance\" decoupling `close` above already uses.\n * `GalleryEvents` (`core/types.ts`) already extends `Record<string,\n * unknown>`, so `ctx.emit('requestFullscreenToggle', {})` from any\n * plugin — official or custom — type-checks with zero core changes;\n * this is just the listening half.\n */\n const offRequestToggle = ctx.on('requestFullscreenToggle', toggleFullscreen);\n\n return () => {\n document.removeEventListener('fullscreenchange', onChange);\n document.removeEventListener('webkitfullscreenchange', onChange);\n removeButton();\n offClose();\n offRequestToggle();\n if (isActive()) exitFullscreen();\n };\n },\n};\n"],"names":[],"mappings":";;;;AAqBO,WAAS,eAAe,SAAiB,QAA0B;AACxE,UAAM,KAAK,SAAS,cAAc,MAAM;AACxC,OAAG,YAAY;AAEf,UAAM,MAAM,SAAS,cAAc,MAAM;AACzC,QAAI,YAAY;AAChB,QAAI,YAAY;AAEhB,UAAM,KAAK,SAAS,cAAc,MAAM;AACxC,OAAG,YAAY;AACf,OAAG,YAAY;AAEf,OAAG,OAAO,KAAK,EAAE;AAEjB,WAAO;AAAA,MACL;AAAA,MACA,SAAS,OAAsB;AAC7B,WAAG,UAAU,OAAO,uBAAuB,KAAK;AAAA,MAClD;AAAA,IAAA;AAAA,EAEJ;ACxCO,QAAM,cACX;AAEK,QAAM,gBACX;ACeF,WAAS,cAAuB;AAC9B,WAAO,CAAC,EACN,SAAS,qBAAsB,SAAsC;AAAA,EAEzE;AAEA,WAAS,2BAA2C;AAClD,WACE,SAAS,qBACR,SAAsC,2BACvC;AAAA,EAEJ;AAEA,WAAS,kBAAkB,IAAuB;;AAChD,UAAM,SAAS;AACf,QAAI,OAAO,kBAAmB,QAAO,kBAAA,EAAoB,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,sBAC3D;EACd;AAEA,WAAS,iBAAuB;;AAC9B,UAAM,MAAM;AACZ,QAAI,SAAS,eAAgB,UAAS,eAAA,EAAiB,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,mBAC5D;EACX;AAiBO,QAAM,aAA0B;AAAA,IACrC,MAAM;AAAA,IAEN,KAAK,KAAyC;AAC5C,UAAI,CAAC,cAAe;AAEpB,YAAM,QAAQ,IAAI,GAAG,MAAA;AACrB,YAAM,SAAU,IAAI,QAAQ,QAAQ,UAAU,CAAA;AAC9C,YAAM,aAAa,OAAO,mBAAmB;AAC7C,YAAM,YAAY,OAAO,kBAAkB;AAE3C,YAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,aAAO,OAAO;AACd,aAAO,YAAY;AACnB,YAAM,WAAW,eAAe,aAAa,aAAa;AAC1D,aAAO,YAAY,SAAS,EAAE;AAC9B,aAAO,aAAa,cAAc,UAAU;AAC5C,aAAO,QAAQ;AACf,aAAO,aAAa,gBAAgB,OAAO;AAE3C,eAAS,WAAoB;AAC3B,eAAO,+BAA+B;AAAA,MACxC;AAEA,eAAS,SAAS,QAAuB;AACvC,iBAAS,SAAS,MAAM;AACxB,eAAO,aAAa,cAAc,SAAS,YAAY,UAAU;AACjE,eAAO,QAAQ,SAAS,YAAY;AACpC,eAAO,aAAa,gBAAgB,OAAO,MAAM,CAAC;AAAA,MACpD;AAGA,eAAS,mBAAyB;AAChC,YAAI,SAAA,EAAY,gBAAA;AAAA,+BACO,KAAK;AAAA,MAC9B;AACA,aAAO,iBAAiB,SAAS,gBAAgB;AAEjD,YAAM,WAAW,MAAY;AAC3B,cAAM,SAAS,SAAA;AACf,iBAAS,MAAM;AACf,YAAI,KAAK,oBAAoB,EAAE,YAAY,QAAQ;AAAA,MACrD;AACA,eAAS,iBAAiB,oBAAoB,QAAQ;AACtD,eAAS,iBAAiB,0BAA0B,QAAQ;AAG5D,YAAM,eAAe,IAAI,GAAG,QAAQ,SAAS,MAAM;AAOnD,YAAM,WAAW,IAAI,GAAG,SAAS,MAAM;AACrC,YAAI,SAAA,EAAY,gBAAA;AAAA,MAClB,CAAC;AAWD,YAAM,mBAAmB,IAAI,GAAG,2BAA2B,gBAAgB;AAE3E,aAAO,MAAM;AACX,iBAAS,oBAAoB,oBAAoB,QAAQ;AACzD,iBAAS,oBAAoB,0BAA0B,QAAQ;AAC/D,qBAAA;AACA,iBAAA;AACA,yBAAA;AACA,YAAI,SAAA,EAAY,gBAAA;AAAA,MAClB;AAAA,IACF;AAAA,EACF;;;"}
@@ -1,2 +1,2 @@
1
- (function(l,o){typeof exports=="object"&&typeof module<"u"?module.exports=o():typeof define=="function"&&define.amd?define(o):(l=typeof globalThis<"u"?globalThis:l||self,l.Shoji=l.Shoji||{},l.Shoji.Fullscreen=o())})(this,(function(){"use strict";const l='<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg>',o='<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg>';function h(){return!!(document.fullscreenEnabled||document.webkitFullscreenEnabled)}function m(){return document.fullscreenElement??document.webkitFullscreenElement??null}function b(e){var s;const t=e;t.requestFullscreen?t.requestFullscreen().catch(()=>{}):(s=t.webkitRequestFullscreen)==null||s.call(t)}function d(){var t;const e=document;document.exitFullscreen?document.exitFullscreen().catch(()=>{}):(t=e.webkitExitFullscreen)==null||t.call(e)}return{name:"fullscreen",init(e){if(!h())return;const t=e.ui.outer(),s=e.gallery.options.locale??{},u=s.enterFullscreen??"Enter fullscreen",f=s.exitFullscreen??"Exit fullscreen",n=document.createElement("button");n.type="button",n.className="shoji-toolbar-button",n.innerHTML=l,n.setAttribute("aria-label",u),n.title=u,n.setAttribute("aria-pressed","false");function i(){return m()===t}function v(r){n.innerHTML=r?o:l,n.setAttribute("aria-label",r?f:u),n.title=r?f:u,n.setAttribute("aria-pressed",String(r))}function a(){i()?d():b(t)}n.addEventListener("click",a);const c=()=>{const r=i();v(r),e.emit("fullscreenChange",{fullscreen:r})};document.addEventListener("fullscreenchange",c),document.addEventListener("webkitfullscreenchange",c);const g=e.ui.toolbar("right",n),F=e.on("close",()=>{i()&&d()}),p=e.on("requestFullscreenToggle",a);return()=>{document.removeEventListener("fullscreenchange",c),document.removeEventListener("webkitfullscreenchange",c),g(),F(),p(),i()&&d()}}}}));
1
+ (function(l,r){typeof exports=="object"&&typeof module<"u"?module.exports=r():typeof define=="function"&&define.amd?define(r):(l=typeof globalThis<"u"?globalThis:l||self,l.Shoji=l.Shoji||{},l.Shoji.Fullscreen=r())})(this,(function(){"use strict";function l(e,n){const o=document.createElement("span");o.className="shoji-icon-swap";const s=document.createElement("span");s.className="shoji-icon-swap-icon shoji-icon-swap-icon--off",s.innerHTML=e;const i=document.createElement("span");return i.className="shoji-icon-swap-icon shoji-icon-swap-icon--on",i.innerHTML=n,o.append(s,i),{el:o,setState(t){o.classList.toggle("shoji-icon-swap--on",t)}}}const r='<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg>',m='<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg>';function p(){return!!(document.fullscreenEnabled||document.webkitFullscreenEnabled)}function b(){return document.fullscreenElement??document.webkitFullscreenElement??null}function g(e){var o;const n=e;n.requestFullscreen?n.requestFullscreen().catch(()=>{}):(o=n.webkitRequestFullscreen)==null||o.call(n)}function d(){var n;const e=document;document.exitFullscreen?document.exitFullscreen().catch(()=>{}):(n=e.webkitExitFullscreen)==null||n.call(e)}return{name:"fullscreen",init(e){if(!p())return;const n=e.ui.outer(),o=e.gallery.options.locale??{},s=o.enterFullscreen??"Enter fullscreen",i=o.exitFullscreen??"Exit fullscreen",t=document.createElement("button");t.type="button",t.className="shoji-toolbar-button";const f=l(r,m);t.appendChild(f.el),t.setAttribute("aria-label",s),t.title=s,t.setAttribute("aria-pressed","false");function u(){return b()===n}function w(c){f.setState(c),t.setAttribute("aria-label",c?i:s),t.title=c?i:s,t.setAttribute("aria-pressed",String(c))}function h(){u()?d():g(n)}t.addEventListener("click",h);const a=()=>{const c=u();w(c),e.emit("fullscreenChange",{fullscreen:c})};document.addEventListener("fullscreenchange",a),document.addEventListener("webkitfullscreenchange",a);const v=e.ui.toolbar("right",t),E=e.on("close",()=>{u()&&d()}),F=e.on("requestFullscreenToggle",h);return()=>{document.removeEventListener("fullscreenchange",a),document.removeEventListener("webkitfullscreenchange",a),v(),E(),F(),u()&&d()}}}}));
2
2
  //# sourceMappingURL=fullscreen.min.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fullscreen.min.js","sources":["../../src/plugins/fullscreen/icons.ts","../../src/plugins/fullscreen/index.ts"],"sourcesContent":["/** DESIGN.md §9 — inline SVG, stroke = currentColor, matches src/core/icons.ts's convention. Four corner-arrow glyphs, a generic \"expand\"/\"collapse\" pair, not tied to any particular icon set. */\nexport const EXPAND_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5\"/></svg>';\n\nexport const COMPRESS_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5\"/></svg>';\n","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\nimport { COMPRESS_ICON, EXPAND_ICON } from './icons';\n\n/**\n * Vendor-prefixed Fullscreen API surface (older Safari) — not in standard\n * `lib.dom.d.ts`, so this is a documented interop boundary (CLAUDE.md: `any`\n * only there). Everything else in this file uses the standard,\n * already-typed `Element.requestFullscreen`/`Document.exitFullscreen`/\n * `Document.fullscreenElement`/`Document.fullscreenEnabled`.\n */\ninterface WebkitFullscreenElement extends HTMLElement {\n webkitRequestFullscreen?: () => void;\n}\ninterface WebkitFullscreenDocument extends Document {\n webkitExitFullscreen?: () => void;\n webkitFullscreenElement?: Element | null;\n webkitFullscreenEnabled?: boolean;\n}\n\nfunction isSupported(): boolean {\n return !!(\n document.fullscreenEnabled || (document as WebkitFullscreenDocument).webkitFullscreenEnabled\n );\n}\n\nfunction currentFullscreenElement(): Element | null {\n return (\n document.fullscreenElement ??\n (document as WebkitFullscreenDocument).webkitFullscreenElement ??\n null\n );\n}\n\nfunction requestFullscreen(el: HTMLElement): void {\n const target = el as WebkitFullscreenElement;\n if (target.requestFullscreen) target.requestFullscreen().catch(() => {});\n else target.webkitRequestFullscreen?.();\n}\n\nfunction exitFullscreen(): void {\n const doc = document as WebkitFullscreenDocument;\n if (document.exitFullscreen) document.exitFullscreen().catch(() => {});\n else doc.webkitExitFullscreen?.();\n}\n\n/**\n * DESIGN.md §4 — native Fullscreen API toggle, `.shoji-outer` (`ctx.ui.outer()`,\n * the whole lightbox — backdrop, dialog, controls) is what goes fullscreen,\n * not just the slide media. No toolbar button at all — not a disabled one —\n * on a browser with no Fullscreen API support (checked once at `init()`;\n * historically Safari on iOS), rather than shipping a dead control.\n *\n * `fullscreenchange` (plus the `webkitfullscreenchange` vendor variant) is\n * the single source of truth for button state, not the click handler\n * directly — a real Fullscreen API `request`/`exit` call can be rejected\n * (permission, another element already fullscreen, etc.), and the browser's\n * own Escape-key fullscreen exit doesn't go through this plugin's click\n * handler at all. Reacting to the event instead of the request keeps the\n * button correct regardless of *why* fullscreen state changed.\n */\nexport const Fullscreen: ShojiPlugin = {\n name: 'fullscreen',\n\n init(ctx: PluginContext): (() => void) | void {\n if (!isSupported()) return;\n\n const outer = ctx.ui.outer();\n const locale = (ctx.gallery.options.locale ?? {}) as Record<string, string>;\n const enterLabel = locale.enterFullscreen ?? 'Enter fullscreen';\n const exitLabel = locale.exitFullscreen ?? 'Exit fullscreen';\n\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'shoji-toolbar-button';\n button.innerHTML = EXPAND_ICON;\n button.setAttribute('aria-label', enterLabel);\n button.title = enterLabel;\n button.setAttribute('aria-pressed', 'false');\n\n function isActive(): boolean {\n return currentFullscreenElement() === outer;\n }\n\n function setState(active: boolean): void {\n button.innerHTML = active ? COMPRESS_ICON : EXPAND_ICON;\n button.setAttribute('aria-label', active ? exitLabel : enterLabel);\n button.title = active ? exitLabel : enterLabel;\n button.setAttribute('aria-pressed', String(active));\n }\n\n /** Shared by the toolbar button and `requestFullscreenToggle` below. */\n function toggleFullscreen(): void {\n if (isActive()) exitFullscreen();\n else requestFullscreen(outer);\n }\n button.addEventListener('click', toggleFullscreen);\n\n const onChange = (): void => {\n const active = isActive();\n setState(active);\n ctx.emit('fullscreenChange', { fullscreen: active });\n };\n document.addEventListener('fullscreenchange', onChange);\n document.addEventListener('webkitfullscreenchange', onChange);\n\n // 'right' — clusters immediately before the close button (DESIGN.md §3.1).\n const removeButton = ctx.ui.toolbar('right', button);\n\n // Browser-level Escape-to-exit-fullscreen behavior is inconsistent\n // across browsers (some exit fullscreen and still deliver the page its\n // own Escape keydown, some don't) — closing the gallery explicitly\n // exits fullscreen too, rather than risking the page getting stuck\n // fullscreen-on-nothing after the lightbox itself is gone.\n const offClose = ctx.on('close', () => {\n if (isActive()) exitFullscreen();\n });\n /**\n * A generic command surface, requested directly (DESIGN.md §4.4), so a\n * *custom* (host-authored) plugin's own button can toggle fullscreen\n * without importing this plugin at all — same \"events over\n * inheritance\" decoupling `close` above already uses.\n * `GalleryEvents` (`core/types.ts`) already extends `Record<string,\n * unknown>`, so `ctx.emit('requestFullscreenToggle', {})` from any\n * plugin — official or custom — type-checks with zero core changes;\n * this is just the listening half.\n */\n const offRequestToggle = ctx.on('requestFullscreenToggle', toggleFullscreen);\n\n return () => {\n document.removeEventListener('fullscreenchange', onChange);\n document.removeEventListener('webkitfullscreenchange', onChange);\n removeButton();\n offClose();\n offRequestToggle();\n if (isActive()) exitFullscreen();\n };\n },\n};\n"],"names":["EXPAND_ICON","COMPRESS_ICON","isSupported","currentFullscreenElement","requestFullscreen","el","target","exitFullscreen","doc","ctx","outer","locale","enterLabel","exitLabel","button","isActive","setState","active","toggleFullscreen","onChange","removeButton","offClose","offRequestToggle"],"mappings":"sPACO,MAAMA,EACX,6MAEWC,EACX,6MCcF,SAASC,GAAuB,CAC9B,MAAO,CAAC,EACN,SAAS,mBAAsB,SAAsC,wBAEzE,CAEA,SAASC,GAA2C,CAClD,OACE,SAAS,mBACR,SAAsC,yBACvC,IAEJ,CAEA,SAASC,EAAkBC,EAAuB,OAChD,MAAMC,EAASD,EACXC,EAAO,kBAAmBA,EAAO,kBAAA,EAAoB,MAAM,IAAM,CAAC,CAAC,OAC3D,yCACd,CAEA,SAASC,GAAuB,OAC9B,MAAMC,EAAM,SACR,SAAS,eAAgB,SAAS,eAAA,EAAiB,MAAM,IAAM,CAAC,CAAC,OAC5D,sCACX,OAiBuC,CACrC,KAAM,aAEN,KAAKC,EAAyC,CAC5C,GAAI,CAACP,IAAe,OAEpB,MAAMQ,EAAQD,EAAI,GAAG,MAAA,EACfE,EAAUF,EAAI,QAAQ,QAAQ,QAAU,CAAA,EACxCG,EAAaD,EAAO,iBAAmB,mBACvCE,EAAYF,EAAO,gBAAkB,kBAErCG,EAAS,SAAS,cAAc,QAAQ,EAC9CA,EAAO,KAAO,SACdA,EAAO,UAAY,uBACnBA,EAAO,UAAYd,EACnBc,EAAO,aAAa,aAAcF,CAAU,EAC5CE,EAAO,MAAQF,EACfE,EAAO,aAAa,eAAgB,OAAO,EAE3C,SAASC,GAAoB,CAC3B,OAAOZ,MAA+BO,CACxC,CAEA,SAASM,EAASC,EAAuB,CACvCH,EAAO,UAAYG,EAAShB,EAAgBD,EAC5Cc,EAAO,aAAa,aAAcG,EAASJ,EAAYD,CAAU,EACjEE,EAAO,MAAQG,EAASJ,EAAYD,EACpCE,EAAO,aAAa,eAAgB,OAAOG,CAAM,CAAC,CACpD,CAGA,SAASC,GAAyB,CAC5BH,EAAA,EAAYR,EAAA,IACOG,CAAK,CAC9B,CACAI,EAAO,iBAAiB,QAASI,CAAgB,EAEjD,MAAMC,EAAW,IAAY,CAC3B,MAAMF,EAASF,EAAA,EACfC,EAASC,CAAM,EACfR,EAAI,KAAK,mBAAoB,CAAE,WAAYQ,EAAQ,CACrD,EACA,SAAS,iBAAiB,mBAAoBE,CAAQ,EACtD,SAAS,iBAAiB,yBAA0BA,CAAQ,EAG5D,MAAMC,EAAeX,EAAI,GAAG,QAAQ,QAASK,CAAM,EAO7CO,EAAWZ,EAAI,GAAG,QAAS,IAAM,CACjCM,EAAA,GAAYR,EAAA,CAClB,CAAC,EAWKe,EAAmBb,EAAI,GAAG,0BAA2BS,CAAgB,EAE3E,MAAO,IAAM,CACX,SAAS,oBAAoB,mBAAoBC,CAAQ,EACzD,SAAS,oBAAoB,yBAA0BA,CAAQ,EAC/DC,EAAA,EACAC,EAAA,EACAC,EAAA,EACIP,EAAA,GAAYR,EAAA,CAClB,CACF,CACF"}
1
+ {"version":3,"file":"fullscreen.min.js","sources":["../../src/core/iconSwap.ts","../../src/plugins/fullscreen/icons.ts","../../src/plugins/fullscreen/index.ts"],"sourcesContent":["/**\n * DESIGN.md §9 — a toolbar button whose icon reflects live state (Fullscreen's\n * enter/exit, Autoplay's play/pause, Zoom's actual-size expand/contract) used\n * to just replace `button.innerHTML` outright — an instant cut, no\n * transition, and each plugin reimplementing the same two-icon swap\n * independently. Requested directly: make every one of these a smooth\n * cross-fade, and make them all go through one shared mechanism rather than\n * three slightly different ones. Both icons are always in the DOM at once,\n * stacked via `position: absolute` inside a fixed-size wrapper\n * (`.shoji-icon-swap`, `shoji.css`) — swapping is just toggling which one is\n * `opacity: 1` via a single modifier class, so there's nothing to actually\n * replace, no risk of a flash-of-no-icon between the old node being removed\n * and the new one's fill/stroke painting in.\n */\nexport interface IconSwap {\n /** Append this into the button — it *is* the button's visible icon content. */\n el: HTMLSpanElement;\n /** `false` shows `iconOff`, `true` shows `iconOn` — cross-fades between them via CSS, respecting `prefers-reduced-motion` the same way every other transition in this codebase does (shoji.css zeroes `--shoji-icon-swap-duration` there). */\n setState(on: boolean): void;\n}\n\nexport function createIconSwap(iconOff: string, iconOn: string): IconSwap {\n const el = document.createElement('span');\n el.className = 'shoji-icon-swap';\n\n const off = document.createElement('span');\n off.className = 'shoji-icon-swap-icon shoji-icon-swap-icon--off';\n off.innerHTML = iconOff;\n\n const on = document.createElement('span');\n on.className = 'shoji-icon-swap-icon shoji-icon-swap-icon--on';\n on.innerHTML = iconOn;\n\n el.append(off, on);\n\n return {\n el,\n setState(state: boolean): void {\n el.classList.toggle('shoji-icon-swap--on', state);\n },\n };\n}\n","/** DESIGN.md §9 — inline SVG, stroke = currentColor, matches src/core/icons.ts's convention. Four corner-arrow glyphs, a generic \"expand\"/\"collapse\" pair, not tied to any particular icon set. */\nexport const EXPAND_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5\"/></svg>';\n\nexport const COMPRESS_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5\"/></svg>';\n","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\nimport { createIconSwap } from '../../core/iconSwap';\nimport { COMPRESS_ICON, EXPAND_ICON } from './icons';\n\n/**\n * Vendor-prefixed Fullscreen API surface (older Safari) — not in standard\n * `lib.dom.d.ts`, so this is a documented interop boundary (CLAUDE.md: `any`\n * only there). Everything else in this file uses the standard,\n * already-typed `Element.requestFullscreen`/`Document.exitFullscreen`/\n * `Document.fullscreenElement`/`Document.fullscreenEnabled`.\n */\ninterface WebkitFullscreenElement extends HTMLElement {\n webkitRequestFullscreen?: () => void;\n}\ninterface WebkitFullscreenDocument extends Document {\n webkitExitFullscreen?: () => void;\n webkitFullscreenElement?: Element | null;\n webkitFullscreenEnabled?: boolean;\n}\n\nfunction isSupported(): boolean {\n return !!(\n document.fullscreenEnabled || (document as WebkitFullscreenDocument).webkitFullscreenEnabled\n );\n}\n\nfunction currentFullscreenElement(): Element | null {\n return (\n document.fullscreenElement ??\n (document as WebkitFullscreenDocument).webkitFullscreenElement ??\n null\n );\n}\n\nfunction requestFullscreen(el: HTMLElement): void {\n const target = el as WebkitFullscreenElement;\n if (target.requestFullscreen) target.requestFullscreen().catch(() => {});\n else target.webkitRequestFullscreen?.();\n}\n\nfunction exitFullscreen(): void {\n const doc = document as WebkitFullscreenDocument;\n if (document.exitFullscreen) document.exitFullscreen().catch(() => {});\n else doc.webkitExitFullscreen?.();\n}\n\n/**\n * DESIGN.md §4 — native Fullscreen API toggle, `.shoji-outer` (`ctx.ui.outer()`,\n * the whole lightbox — backdrop, dialog, controls) is what goes fullscreen,\n * not just the slide media. No toolbar button at all — not a disabled one —\n * on a browser with no Fullscreen API support (checked once at `init()`;\n * historically Safari on iOS), rather than shipping a dead control.\n *\n * `fullscreenchange` (plus the `webkitfullscreenchange` vendor variant) is\n * the single source of truth for button state, not the click handler\n * directly — a real Fullscreen API `request`/`exit` call can be rejected\n * (permission, another element already fullscreen, etc.), and the browser's\n * own Escape-key fullscreen exit doesn't go through this plugin's click\n * handler at all. Reacting to the event instead of the request keeps the\n * button correct regardless of *why* fullscreen state changed.\n */\nexport const Fullscreen: ShojiPlugin = {\n name: 'fullscreen',\n\n init(ctx: PluginContext): (() => void) | void {\n if (!isSupported()) return;\n\n const outer = ctx.ui.outer();\n const locale = (ctx.gallery.options.locale ?? {}) as Record<string, string>;\n const enterLabel = locale.enterFullscreen ?? 'Enter fullscreen';\n const exitLabel = locale.exitFullscreen ?? 'Exit fullscreen';\n\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'shoji-toolbar-button';\n const iconSwap = createIconSwap(EXPAND_ICON, COMPRESS_ICON);\n button.appendChild(iconSwap.el);\n button.setAttribute('aria-label', enterLabel);\n button.title = enterLabel;\n button.setAttribute('aria-pressed', 'false');\n\n function isActive(): boolean {\n return currentFullscreenElement() === outer;\n }\n\n function setState(active: boolean): void {\n iconSwap.setState(active);\n button.setAttribute('aria-label', active ? exitLabel : enterLabel);\n button.title = active ? exitLabel : enterLabel;\n button.setAttribute('aria-pressed', String(active));\n }\n\n /** Shared by the toolbar button and `requestFullscreenToggle` below. */\n function toggleFullscreen(): void {\n if (isActive()) exitFullscreen();\n else requestFullscreen(outer);\n }\n button.addEventListener('click', toggleFullscreen);\n\n const onChange = (): void => {\n const active = isActive();\n setState(active);\n ctx.emit('fullscreenChange', { fullscreen: active });\n };\n document.addEventListener('fullscreenchange', onChange);\n document.addEventListener('webkitfullscreenchange', onChange);\n\n // 'right' — clusters immediately before the close button (DESIGN.md §3.1).\n const removeButton = ctx.ui.toolbar('right', button);\n\n // Browser-level Escape-to-exit-fullscreen behavior is inconsistent\n // across browsers (some exit fullscreen and still deliver the page its\n // own Escape keydown, some don't) — closing the gallery explicitly\n // exits fullscreen too, rather than risking the page getting stuck\n // fullscreen-on-nothing after the lightbox itself is gone.\n const offClose = ctx.on('close', () => {\n if (isActive()) exitFullscreen();\n });\n /**\n * A generic command surface, requested directly (DESIGN.md §4.4), so a\n * *custom* (host-authored) plugin's own button can toggle fullscreen\n * without importing this plugin at all — same \"events over\n * inheritance\" decoupling `close` above already uses.\n * `GalleryEvents` (`core/types.ts`) already extends `Record<string,\n * unknown>`, so `ctx.emit('requestFullscreenToggle', {})` from any\n * plugin — official or custom — type-checks with zero core changes;\n * this is just the listening half.\n */\n const offRequestToggle = ctx.on('requestFullscreenToggle', toggleFullscreen);\n\n return () => {\n document.removeEventListener('fullscreenchange', onChange);\n document.removeEventListener('webkitfullscreenchange', onChange);\n removeButton();\n offClose();\n offRequestToggle();\n if (isActive()) exitFullscreen();\n };\n },\n};\n"],"names":["createIconSwap","iconOff","iconOn","el","off","on","state","EXPAND_ICON","COMPRESS_ICON","isSupported","currentFullscreenElement","requestFullscreen","target","exitFullscreen","doc","ctx","outer","locale","enterLabel","exitLabel","button","iconSwap","isActive","setState","active","toggleFullscreen","onChange","removeButton","offClose","offRequestToggle"],"mappings":"sPAqBO,SAASA,EAAeC,EAAiBC,EAA0B,CACxE,MAAMC,EAAK,SAAS,cAAc,MAAM,EACxCA,EAAG,UAAY,kBAEf,MAAMC,EAAM,SAAS,cAAc,MAAM,EACzCA,EAAI,UAAY,iDAChBA,EAAI,UAAYH,EAEhB,MAAMI,EAAK,SAAS,cAAc,MAAM,EACxC,OAAAA,EAAG,UAAY,gDACfA,EAAG,UAAYH,EAEfC,EAAG,OAAOC,EAAKC,CAAE,EAEV,CACL,GAAAF,EACA,SAASG,EAAsB,CAC7BH,EAAG,UAAU,OAAO,sBAAuBG,CAAK,CAClD,CAAA,CAEJ,CCxCO,MAAMC,EACX,6MAEWC,EACX,6MCeF,SAASC,GAAuB,CAC9B,MAAO,CAAC,EACN,SAAS,mBAAsB,SAAsC,wBAEzE,CAEA,SAASC,GAA2C,CAClD,OACE,SAAS,mBACR,SAAsC,yBACvC,IAEJ,CAEA,SAASC,EAAkBR,EAAuB,OAChD,MAAMS,EAAST,EACXS,EAAO,kBAAmBA,EAAO,kBAAA,EAAoB,MAAM,IAAM,CAAC,CAAC,OAC3D,yCACd,CAEA,SAASC,GAAuB,OAC9B,MAAMC,EAAM,SACR,SAAS,eAAgB,SAAS,eAAA,EAAiB,MAAM,IAAM,CAAC,CAAC,OAC5D,sCACX,OAiBuC,CACrC,KAAM,aAEN,KAAKC,EAAyC,CAC5C,GAAI,CAACN,IAAe,OAEpB,MAAMO,EAAQD,EAAI,GAAG,MAAA,EACfE,EAAUF,EAAI,QAAQ,QAAQ,QAAU,CAAA,EACxCG,EAAaD,EAAO,iBAAmB,mBACvCE,EAAYF,EAAO,gBAAkB,kBAErCG,EAAS,SAAS,cAAc,QAAQ,EAC9CA,EAAO,KAAO,SACdA,EAAO,UAAY,uBACnB,MAAMC,EAAWrB,EAAeO,EAAaC,CAAa,EAC1DY,EAAO,YAAYC,EAAS,EAAE,EAC9BD,EAAO,aAAa,aAAcF,CAAU,EAC5CE,EAAO,MAAQF,EACfE,EAAO,aAAa,eAAgB,OAAO,EAE3C,SAASE,GAAoB,CAC3B,OAAOZ,MAA+BM,CACxC,CAEA,SAASO,EAASC,EAAuB,CACvCH,EAAS,SAASG,CAAM,EACxBJ,EAAO,aAAa,aAAcI,EAASL,EAAYD,CAAU,EACjEE,EAAO,MAAQI,EAASL,EAAYD,EACpCE,EAAO,aAAa,eAAgB,OAAOI,CAAM,CAAC,CACpD,CAGA,SAASC,GAAyB,CAC5BH,EAAA,EAAYT,EAAA,IACOG,CAAK,CAC9B,CACAI,EAAO,iBAAiB,QAASK,CAAgB,EAEjD,MAAMC,EAAW,IAAY,CAC3B,MAAMF,EAASF,EAAA,EACfC,EAASC,CAAM,EACfT,EAAI,KAAK,mBAAoB,CAAE,WAAYS,EAAQ,CACrD,EACA,SAAS,iBAAiB,mBAAoBE,CAAQ,EACtD,SAAS,iBAAiB,yBAA0BA,CAAQ,EAG5D,MAAMC,EAAeZ,EAAI,GAAG,QAAQ,QAASK,CAAM,EAO7CQ,EAAWb,EAAI,GAAG,QAAS,IAAM,CACjCO,EAAA,GAAYT,EAAA,CAClB,CAAC,EAWKgB,EAAmBd,EAAI,GAAG,0BAA2BU,CAAgB,EAE3E,MAAO,IAAM,CACX,SAAS,oBAAoB,mBAAoBC,CAAQ,EACzD,SAAS,oBAAoB,yBAA0BA,CAAQ,EAC/DC,EAAA,EACAC,EAAA,EACAC,EAAA,EACIP,EAAA,GAAYT,EAAA,CAClB,CACF,CACF"}
@@ -2,6 +2,23 @@
2
2
  typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, global.Shoji = global.Shoji || {}, global.Shoji.Zoom = factory());
3
3
  })(this, (function() {
4
4
  "use strict";
5
+ function createIconSwap(iconOff, iconOn) {
6
+ const el = document.createElement("span");
7
+ el.className = "shoji-icon-swap";
8
+ const off = document.createElement("span");
9
+ off.className = "shoji-icon-swap-icon shoji-icon-swap-icon--off";
10
+ off.innerHTML = iconOff;
11
+ const on = document.createElement("span");
12
+ on.className = "shoji-icon-swap-icon shoji-icon-swap-icon--on";
13
+ on.innerHTML = iconOn;
14
+ el.append(off, on);
15
+ return {
16
+ el,
17
+ setState(state) {
18
+ el.classList.toggle("shoji-icon-swap--on", state);
19
+ }
20
+ };
21
+ }
5
22
  function parseCssTime(value) {
6
23
  var _a;
7
24
  const first = ((_a = value.split(",")[0]) == null ? void 0 : _a.trim()) ?? "";
@@ -26,7 +43,8 @@
26
43
  }
27
44
  const ZOOM_IN_ICON = '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="10" cy="10" r="7"/><path d="M21 21l-5.5-5.5M10 7v6M7 10h6"/></svg>';
28
45
  const ZOOM_OUT_ICON = '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="10" cy="10" r="7"/><path d="M21 21l-5.5-5.5M7 10h6"/></svg>';
29
- const ZOOM_ACTUAL_SIZE_ICON = '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="1"/><path d="M9 15l6-6M9 9h0M15 15h0"/></svg>';
46
+ const ZOOM_ACTUAL_SIZE_EXPAND_ICON = '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 9L3 3M3 8V3h5M15 15l6 6M21 16v5h-5"/></svg>';
47
+ const ZOOM_ACTUAL_SIZE_CONTRACT_ICON = '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3l6 6M9 3v6H3M21 21l-6-6M15 21v-6h6"/></svg>';
30
48
  function clampScale(scale, min, max) {
31
49
  return Math.min(max, Math.max(min, scale));
32
50
  }
@@ -188,6 +206,10 @@
188
206
  }
189
207
  function emitChange() {
190
208
  ctx.emit("zoomChange", { index: gallery.currentIndex, scale });
209
+ updateActualSizeIcon();
210
+ }
211
+ function updateActualSizeIcon() {
212
+ actualSizeIconSwap.setState(scale > ZOOM_EPSILON);
191
213
  }
192
214
  function withTransition(img, run, afterEnd) {
193
215
  img.style.transition = "transform var(--shoji-duration) var(--shoji-easing)";
@@ -230,6 +252,7 @@
230
252
  natural = null;
231
253
  container = null;
232
254
  originOffset = null;
255
+ updateActualSizeIcon();
233
256
  if (wasEngaged) emitChange();
234
257
  const img = getImg();
235
258
  if (!img) return;
@@ -358,7 +381,16 @@
358
381
  }
359
382
  const zoomInBtn = buildButton(ZOOM_IN_ICON, zoomInLabel);
360
383
  const zoomOutBtn = buildButton(ZOOM_OUT_ICON, zoomOutLabel);
361
- const actualSizeBtn = buildButton(ZOOM_ACTUAL_SIZE_ICON, actualSizeLabel);
384
+ const actualSizeBtn = document.createElement("button");
385
+ actualSizeBtn.type = "button";
386
+ actualSizeBtn.className = "shoji-toolbar-button";
387
+ const actualSizeIconSwap = createIconSwap(
388
+ ZOOM_ACTUAL_SIZE_EXPAND_ICON,
389
+ ZOOM_ACTUAL_SIZE_CONTRACT_ICON
390
+ );
391
+ actualSizeBtn.appendChild(actualSizeIconSwap.el);
392
+ actualSizeBtn.setAttribute("aria-label", actualSizeLabel);
393
+ actualSizeBtn.title = actualSizeLabel;
362
394
  zoomInBtn.addEventListener("click", zoomInStep);
363
395
  zoomOutBtn.addEventListener("click", zoomOutStep);
364
396
  actualSizeBtn.addEventListener("click", actualSizeToggle);