@gemboss/ui 0.12.0 → 0.21.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.
@@ -0,0 +1,188 @@
1
+ "use client";
2
+ // The settings PANEL's pieces (0.17.0, step 3 of 5 of the editor kit): the tab rows, the panel header, the collapsible
3
+ // group and the field row. The admin draws its Configure column, its element inspector and its schema form with these;
4
+ // the look is editor.css (`.gemboss-ec-tabs`, `.gemboss-ei-*`), unchanged by the move. Stateless except
5
+ // InspectorGroup's own open/closed when the caller does not hold it. Controls (Seg, Color, Range, EdgeBox, Image) are
6
+ // controls.tsx: their own module, because only a screen that edits values draws them.
7
+ import { useState } from "react";
8
+ import type React from "react";
9
+
10
+ // ── PanelTabs ───────────────────────────────────────────────────────────────────────────────────────────────────────
11
+
12
+ export interface PanelTab<T extends string = string> {
13
+ id: T;
14
+ label: React.ReactNode;
15
+ /** Drawn before the label (the column tabs' 14px line icons). */
16
+ icon?: React.ReactNode;
17
+ /** The id of the panel this tab shows: `aria-controls` (and editor-plain.js shows it, hiding its siblings'). */
18
+ controls?: string;
19
+ }
20
+
21
+ export interface PanelTabsProps<T extends string = string> {
22
+ tabs: ReadonlyArray<PanelTab<T>>;
23
+ value: T;
24
+ onChange: (id: T) => void;
25
+ /**
26
+ * `column`: the left column's row (`.gemboss-ec-tabs`, Configure · Chat; a selected tab has a 2px primary underline).
27
+ * `inspector`: the settings panel's segmented row (`.gemboss-ei-tabs`, Content · Design · Advanced · Elements).
28
+ */
29
+ variant?: "column" | "inspector";
30
+ /** The tablist's accessible name. */
31
+ label?: string;
32
+ /**
33
+ * Roving focus (default on): only the selected tab is in the Tab order, ←/→ move the selection and the focus with
34
+ * it, wrapping at the ends. Off, every tab is a plain stop and arrows do nothing (the admin's Configure · Chat row,
35
+ * kept exactly as it was in step 3).
36
+ */
37
+ roving?: boolean;
38
+ }
39
+
40
+ /** A row of `role="tab"` buttons. The caller owns the selection and draws the panel under it. */
41
+ export function PanelTabs<T extends string = string>({ tabs, value, onChange, variant = "column", label, roving = true }: PanelTabsProps<T>) {
42
+ const inspector = variant === "inspector";
43
+ const onKeyDown = roving ? (e: React.KeyboardEvent<HTMLDivElement>) => {
44
+ if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
45
+ e.preventDefault();
46
+ const i = tabs.findIndex((t) => t.id === value);
47
+ const ni = e.key === "ArrowRight" ? (i + 1) % tabs.length : (i - 1 + tabs.length) % tabs.length;
48
+ onChange(tabs[ni].id);
49
+ // Focus follows the selection (roving tabindex).
50
+ (e.currentTarget.children[ni] as HTMLButtonElement | undefined)?.focus();
51
+ } : undefined;
52
+ return (
53
+ <div className={inspector ? "gemboss-ei-tabs" : "gemboss-ec-tabs"} role="tablist" aria-label={label} onKeyDown={onKeyDown}>
54
+ {tabs.map((t) => {
55
+ const on = t.id === value;
56
+ return (
57
+ // data-value: what a page without React reads back (editor-plain.js), since the label can be anything.
58
+ <button key={t.id} type="button" role="tab" data-value={t.id}
59
+ className={inspector ? `gemboss-ei-tab${on ? " on" : ""}` : "gemboss-ec-tab"}
60
+ aria-selected={on} aria-controls={t.controls} tabIndex={roving ? (on ? 0 : -1) : undefined} onClick={() => onChange(t.id)}>
61
+ {t.icon}{t.label}
62
+ </button>
63
+ );
64
+ })}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ // ── PanelHeader ─────────────────────────────────────────────────────────────────────────────────────────────────────
70
+
71
+ export interface PanelHeaderProps {
72
+ /** What the panel edits (a section's name). */
73
+ title: React.ReactNode;
74
+ /** The line under it, monospaced and muted (the section's type). Drawn whenever it is not undefined. */
75
+ subtitle?: React.ReactNode;
76
+ /** Icon buttons after the title: `PanelAction`s. The first sits at the right edge's start (margin-left: auto). */
77
+ actions?: React.ReactNode;
78
+ /** Draws ✕ last. Omit it for a docked panel (nothing to close). */
79
+ onClose?: () => void;
80
+ /** The close button's accessible name. Default "Close". */
81
+ closeLabel?: string;
82
+ /** A floating panel's grab handle: adds `.gemboss-ei-drag` (grab cursor) and takes the mouse-down that starts a drag. */
83
+ onDragStart?: React.MouseEventHandler<HTMLElement>;
84
+ }
85
+
86
+ /** `header.gemboss-ei-head`: title (+ subtitle) · actions · ✕. */
87
+ export function PanelHeader({ title, subtitle, actions, onClose, closeLabel = "Close", onDragStart }: PanelHeaderProps) {
88
+ return (
89
+ <header className={onDragStart ? "gemboss-ei-head gemboss-ei-drag" : "gemboss-ei-head"} onMouseDown={onDragStart}>
90
+ <span className="gemboss-ei-title">
91
+ {title}
92
+ {subtitle !== undefined ? <small>{subtitle}</small> : null}
93
+ </span>
94
+ {actions}
95
+ {onClose ? <button type="button" className="gemboss-ei-x" title="Close" aria-label={closeLabel} onClick={onClose}>✕</button> : null}
96
+ </header>
97
+ );
98
+ }
99
+
100
+ /** A 26px icon button for `PanelHeader`'s actions (`.gemboss-ei-act`). `label` is both its name and its tooltip unless `title` differs. */
101
+ export function PanelAction({ label, title, disabled, onClick, children }: {
102
+ label: string; title?: string; disabled?: boolean; onClick?: React.MouseEventHandler<HTMLButtonElement>; children: React.ReactNode;
103
+ }) {
104
+ return (
105
+ <button type="button" className="gemboss-ei-act" title={title ?? label} aria-label={label} disabled={disabled} onClick={onClick}>
106
+ {children}
107
+ </button>
108
+ );
109
+ }
110
+
111
+ // ── InspectorGroup ──────────────────────────────────────────────────────────────────────────────────────────────────
112
+
113
+ export interface InspectorGroupProps {
114
+ /** The header: 11px / 700, uppercase, muted (`.gemboss-ei-gh`). */
115
+ title?: React.ReactNode;
116
+ /** A short aside after the title, in the header's own line ("· all screens"). Collapsible groups only. */
117
+ note?: React.ReactNode;
118
+ /** Controlled: the caller holds whether the group is open (and keeps it across tabs, say). */
119
+ open?: boolean;
120
+ /** Uncontrolled: where it starts. Default open. */
121
+ defaultOpen?: boolean;
122
+ onOpenChange?: (open: boolean) => void;
123
+ /**
124
+ * Default true: the inspector's group, `.gemboss-ei-grp` with a header BUTTON (▾ / ▸, `aria-expanded`); closed, CSS
125
+ * hides everything after the header (`.gemboss-ei-gh[data-open="closed"] ~ *`), so the body stays mounted.
126
+ * `false`: the schema form's static group, `section.gemboss-ei-group` with an `h4.gemboss-ei-grouphead` (no toggle).
127
+ */
128
+ collapsible?: boolean;
129
+ /** A muted line after the body (`p.gemboss-ei-hint`). */
130
+ hint?: React.ReactNode;
131
+ children?: React.ReactNode;
132
+ }
133
+
134
+ export function InspectorGroup({ title, note, open, defaultOpen = true, onOpenChange, collapsible = true, hint, children }: InspectorGroupProps) {
135
+ const [own, setOwn] = useState(defaultOpen);
136
+ const hintNode = hint ? <p className="gemboss-ei-hint">{hint}</p> : null;
137
+ if (!collapsible) {
138
+ return (
139
+ <section className="gemboss-ei-group">
140
+ {title ? <h4 className="gemboss-ei-grouphead">{title}</h4> : null}
141
+ {children}
142
+ {hintNode}
143
+ </section>
144
+ );
145
+ }
146
+ const isOpen = open ?? own;
147
+ return (
148
+ <div className="gemboss-ei-grp">
149
+ <button type="button" className="gemboss-ei-gh" data-open={isOpen ? "open" : "closed"} aria-expanded={isOpen}
150
+ onClick={() => { if (open === undefined) setOwn(!isOpen); onOpenChange?.(!isOpen); }}>
151
+ <span className="gemboss-ei-chev" aria-hidden>{isOpen ? "▾" : "▸"}</span>{title}
152
+ {note ? <small className="gemboss-ei-gh-note">· {note}</small> : null}
153
+ </button>
154
+ {children}
155
+ {hintNode}
156
+ </div>
157
+ );
158
+ }
159
+
160
+ // ── Field ───────────────────────────────────────────────────────────────────────────────────────────────────────────
161
+
162
+ export interface FieldProps {
163
+ /** 13px / 500 above the control. `undefined` / `null` draws no label (a control named by its own aria-label). */
164
+ label?: React.ReactNode;
165
+ /** The control's id, so a click on the label focuses it. */
166
+ htmlFor?: string;
167
+ /** A value shown at the label's right edge, bold and tabular (a range's current value: `.gemboss-ei-rangeval`). */
168
+ labelValue?: React.ReactNode;
169
+ /** A muted line under the control (`p.gemboss-ei-hint`). Put it in `children` instead to place it elsewhere. */
170
+ hint?: React.ReactNode;
171
+ /** Added to `.gemboss-ei-fld` (the admin's `togrow`). */
172
+ className?: string;
173
+ style?: React.CSSProperties;
174
+ children?: React.ReactNode;
175
+ }
176
+
177
+ /** One labelled row of a panel: `div.gemboss-ei-fld` › label · control · hint. 12px under each row but the last. */
178
+ export function Field({ label, htmlFor, labelValue, hint, className, style, children }: FieldProps) {
179
+ return (
180
+ <div className={className ? `gemboss-ei-fld ${className}` : "gemboss-ei-fld"} style={style}>
181
+ {label != null ? (
182
+ <label htmlFor={htmlFor}>{label}{labelValue != null ? <span className="gemboss-ei-rangeval">{labelValue}</span> : null}</label>
183
+ ) : null}
184
+ {children}
185
+ {hint ? <p className="gemboss-ei-hint">{hint}</p> : null}
186
+ </div>
187
+ );
188
+ }
@@ -0,0 +1,44 @@
1
+ import type { EditorPane } from "./frame";
2
+
3
+ // The two pane controls only an editor with a collapsible left column and a phone pane switch draws (the admin's theme
4
+ // editor): their own module, so the page and section editors do not carry them.
5
+
6
+ /** The panel icon that shows or hides the left column (EditorTopbar's `chatToggle`): the left third is filled while the
7
+ * column is open, hollow and pressed when it is collapsed. */
8
+ export function EditorChatToggle({ collapsed, onToggle }: { collapsed?: boolean; onToggle: () => void }) {
9
+ return (
10
+ <button
11
+ className={`gemboss-editor-chat-toggle${collapsed ? " collapsed" : ""}`}
12
+ onClick={onToggle}
13
+ aria-pressed={collapsed}
14
+ title={collapsed ? "Show chat panel" : "Hide chat — full-width preview"}
15
+ aria-label={collapsed ? "Show chat panel" : "Hide chat panel"}
16
+ >
17
+ {/* A real SVG panel icon (geometrically centred): the ▭/◫ box glyphs' ink sits high in the em box. */}
18
+ <svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round" aria-hidden="true">
19
+ <rect x="1.75" y="2.75" width="12.5" height="10.5" rx="2" />
20
+ <line x1="6.25" y1="2.75" x2="6.25" y2="13.25" />
21
+ {!collapsed && <rect x="1.75" y="2.75" width="4.5" height="10.5" fill="currentColor" stroke="none" />}
22
+ </svg>
23
+ </button>
24
+ );
25
+ }
26
+
27
+ /**
28
+ * The phone's Chat | Preview switch (`.gemboss-editor-mobile-tabs`; CSS shows it ≤760px only, wider screens show both
29
+ * panes side by side). Put it FIRST in EditorFrame's `children` and give the frame the same `mobilePane`.
30
+ */
31
+ export function EditorPaneTabs({ value, onChange, labels }: {
32
+ value: EditorPane; onChange: (pane: EditorPane) => void; labels?: { chat?: string; preview?: string };
33
+ }) {
34
+ return (
35
+ <div className="gemboss-editor-mobile-tabs" role="tablist" aria-label="Editor panes">
36
+ <button type="button" role="tab" aria-selected={value === "chat"}
37
+ className={`gemboss-editor-mobile-tab${value === "chat" ? " active" : ""}`}
38
+ onClick={() => onChange("chat")}>{labels?.chat ?? "Chat"}</button>
39
+ <button type="button" role="tab" aria-selected={value === "preview"}
40
+ className={`gemboss-editor-mobile-tab${value === "preview" ? " active" : ""}`}
41
+ onClick={() => onChange("preview")}>{labels?.preview ?? "Preview"}</button>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,45 @@
1
+ // What a colour swatch can and cannot show (moved from the admin's components/ui/color-field-value.ts in 0.17.0; the
2
+ // admin re-exports it from here, so the colour control and the admin's other colour boxes keep ONE answer).
3
+ //
4
+ // `<input type="color">` holds exactly one shape: a 7-character hex. Anything else it SANITISES to
5
+ // `#000000` — silently, with no error and no visual difference from a value someone actually chose.
6
+ //
7
+ // gemsite's `cssValue` decides whether a background paints, and it accepts five shapes. Measured:
8
+ //
9
+ // #ff0000 ✔ renders ✔ the swatch can show it
10
+ // #f00 ✔ renders ✘ shown as #000000 — even a 3-digit hex
11
+ // rgba(0,0,0,.5) ✔ renders ✘ shown as #000000
12
+ // var(--gem-color-accent) ✔ renders ✘ shown as #000000
13
+ // linear-gradient(90deg,#f00,#00f) ✔ renders ✘ shown as #000000
14
+ //
15
+ // So four of five were BOTH invisible and destructible: the panel showed black for a value that is
16
+ // not black, and touching the swatch overwrote it with the black it was pretending to show. A seller
17
+ // could lose a gradient by opening the panel to look at it.
18
+ //
19
+ // None of this is hypothetical. `gem_background` renders a gradient today, measured in chromium, and
20
+ // every one of these shapes is something the CLONE pipeline can read off a real storefront.
21
+ //
22
+ // Plain TypeScript, no JSX: the admin's node tests import it through the admin's re-export.
23
+
24
+ /** The only shape `<input type="color">` can hold without rewriting it. */
25
+ const SWATCHABLE = /^#[0-9a-fA-F]{6}$/;
26
+
27
+ export type SwatchState = {
28
+ /** What to put in the swatch. Never a colour the author did not choose. */
29
+ swatch: string;
30
+ /** False when the swatch would misrepresent the value — the text box is then the only truth. */
31
+ usable: boolean;
32
+ };
33
+
34
+ /**
35
+ * `whenEmpty` is the swatch shown for a BLANK value, and it is a parameter rather than a constant on
36
+ * purpose. Three surfaces call this now and they disagree: the element inspector and the schema form
37
+ * open an empty swatch on black, the page editor's "Background color" opens on white. That difference
38
+ * is harmless — an empty value misrepresents nothing, which is why `usable` stays true — and folding
39
+ * it into one answer would repaint a live control while fixing an unrelated bug. The lie is what gets
40
+ * consolidated here; the taste does not.
41
+ */
42
+ export function swatchState(value: string, whenEmpty = '#000000'): SwatchState {
43
+ if (!value) return { swatch: whenEmpty, usable: true };
44
+ return SWATCHABLE.test(value) ? { swatch: value, usable: true } : { swatch: '#000000', usable: false };
45
+ }
@@ -0,0 +1 @@
1
+ (function(S){"use strict";var b=S.gembossUI;if(!b){S.console&&S.console.error("@gemboss/ui editor-plain.js: load kit-plain.js first (it defines gembossUI)");return}if(b.initEditorFrame)return;var k=5,z=360,p=typeof WeakSet=="function"?new WeakSet:null,l=function(t,e,r){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:r}))},w=function(t,e){var r=Number(t);return isFinite(r)&&r>0?r:e};function c(t){return t.querySelector(".gemboss-editor-body")}function g(t){var e=c(t);return e&&e.querySelector(":scope > .gemboss-editor-resizer")}function v(t){var e=g(t);return{min:w(e&&e.getAttribute("aria-valuemin"),300),cap:w(e&&(e.getAttribute("data-gb-max")||e.getAttribute("aria-valuemax")),680)}}function x(t){var e=v(t),r=c(t),i=r?r.getBoundingClientRect().width:0;return i>0?Math.max(e.min,Math.min(e.cap,i-z-k)):e.cap}var h=typeof WeakMap=="function"?new WeakMap:null;function q(t){var e=h?h.get(t):t._gbWant;return e??y(t)}function C(t,e){h?h.set(t,e):t._gbWant=e}function d(t,e,r,i){var n=c(t),a=g(t);if(n){var s=i??x(t),f=v(t),o=Math.round(Math.min(s,Math.max(f.min,e)));if(n.style.setProperty("--gemboss-chat-w",o+"px"),a&&(a.setAttribute("aria-valuenow",String(o)),a.setAttribute("aria-valuemax",String(s))),r){C(t,o);var E=n.getAttribute("data-gb-storage");if(E)try{localStorage.setItem(E,String(o))}catch{}l(t,"gemboss:editor-split",{width:o})}return o}}function y(t){var e=g(t);return w(e&&e.getAttribute("aria-valuenow"),384)}function F(t){var e=c(t),r=g(t);if(!(!e||!r)){r.hasAttribute("data-gb-max")||r.setAttribute("data-gb-max",r.getAttribute("aria-valuemax")||"680");var i=e.getAttribute("data-gb-storage"),n=y(t);if(i)try{var a=Number(localStorage.getItem(i));a>=v(t).min&&a<=v(t).cap&&(n=a)}catch{}C(t,n),e.classList.contains("is-chat-collapsed")||d(t,n,!1)}}function O(t,e){var r=c(t);if(r&&!(e.button!==0||e.isPrimary===!1)){e.preventDefault();var i=r.getBoundingClientRect().left,n=x(t),a=0,s=y(t);document.body.classList.add("ui-resizing");var f=function(E){s=E.clientX-i-k,a||(a=requestAnimationFrame(function(){a=0,d(t,s,!1,n)}))},o=function(){a&&cancelAnimationFrame(a),document.body.classList.remove("ui-resizing"),removeEventListener("pointermove",f),removeEventListener("pointerup",o),removeEventListener("pointercancel",o),removeEventListener("blur",o),d(t,s,!0,n)};addEventListener("pointermove",f),addEventListener("pointerup",o),addEventListener("pointercancel",o),addEventListener("blur",o)}}function D(t,e){if(!(e.altKey||e.metaKey||e.ctrlKey)){var r=e.shiftKey?64:16,i=y(t),n=v(t);if(e.key==="ArrowLeft")i-=r;else if(e.key==="ArrowRight")i+=r;else if(e.key==="Home")i=n.min;else if(e.key==="End")i=x(t);else return;e.preventDefault(),d(t,i,!0)}}function T(t,e){var r=c(t);if(r){var i=!r.classList.contains("is-chat-collapsed");r.classList.toggle("is-chat-collapsed",i),i?r.style.setProperty("--gemboss-chat-w","0px"):d(t,q(t),!1),e.classList.toggle("collapsed",i),e.setAttribute("aria-pressed",String(i)),e.setAttribute("aria-label",i?"Show chat panel":"Hide chat panel"),e.title=i?"Show chat panel":"Hide chat \u2014 full-width preview";var n=e.querySelector("rect[fill='currentColor']");n&&(n.style.display=i?"none":""),l(t,"gemboss:editor-chat",{collapsed:i})}}function U(t,e){var r=e.closest(".gemboss-pv-devices");r&&(r.querySelectorAll(".gemboss-pv-device").forEach(function(i){var n=i===e;i.classList.toggle("active",n),i.setAttribute("aria-pressed",String(n))}),l(t,"gemboss:editor-device",{device:e.getAttribute("data-device")||(e.title||"").toLowerCase()}))}function K(t,e){var r=e.getAttribute("data-pane")||(Array.prototype.indexOf.call(e.parentNode.children,e)===0?"chat":"preview"),i=c(t);i&&(i.setAttribute("data-mobile-pane",r),e.parentNode.querySelectorAll(".gemboss-editor-mobile-tab").forEach(function(n){var a=n===e;n.classList.toggle("active",a),n.setAttribute("aria-selected",String(a))}),l(t,"gemboss:editor-pane",{pane:r}))}var u=null;function M(t){var e=t.trigger.getBoundingClientRect();t.menu.style.top=e.bottom+6+"px",t.menu.style.right=Math.max(8,innerWidth-e.right)+"px";var r=t.menu.getBoundingClientRect(),i=e.right-r.right;r.left+i<8&&(i=8-r.left),Math.abs(i)>=.5&&(t.menu.style.right=parseFloat(t.menu.style.right)-i+"px")}function m(t){var e=u;e&&(u=null,e.menu.hidden=!0,e.trigger.setAttribute("aria-expanded","false"),e.trigger.classList.remove("active"),t&&e.trigger.focus(),l(e.root,"gemboss:editor-menu",{open:!1,menu:e.menu}))}function H(t,e){var r=e.nextElementSibling;if(!r||!r.classList.contains("gemboss-editor-menu"))return!1;var i=u&&u.trigger===e;return m(!1),i||(u={root:t,trigger:e,menu:r},r.hidden=!1,e.setAttribute("aria-expanded","true"),e.classList.add("active"),M(u),l(t,"gemboss:editor-menu",{open:!0,menu:r})),!0}var P=!1;function j(){P||(P=!0,document.addEventListener("pointerdown",function(t){u&&!u.menu.contains(t.target)&&!u.trigger.contains(t.target)&&m(!1)}),addEventListener("keydown",function(t){t.key!=="Escape"||!u||t.isComposing||t.keyCode===229||(t.preventDefault(),t.stopImmediatePropagation(),m(!0))},!0),addEventListener("resize",function(){u&&M(u)}),addEventListener("blur",function(){m(!1)}))}function G(t){var e=t&&t.classList&&t.classList.contains("gemboss-editor")?[t]:Array.prototype.slice.call((t||document).querySelectorAll(".gemboss-editor"));return j(),e.forEach(function(r){if(!(p?p.has(r):r._gbEditor)){p?p.add(r):r._gbEditor=!0,F(r);var i=c(r);i&&typeof ResizeObserver=="function"&&new ResizeObserver(function(){i.classList.contains("is-chat-collapsed")||d(r,q(r),!1)}).observe(i),r.addEventListener("pointerdown",function(n){var a=n.target.closest(".gemboss-editor-resizer");a&&a===g(r)&&O(r,n)}),r.addEventListener("keydown",function(n){n.target.classList&&n.target.classList.contains("gemboss-editor-resizer")&&n.target===g(r)&&D(r,n)}),r.addEventListener("click",function(n){var a=n.target,s;if(s=a.closest(".gemboss-editor-chat-toggle"))return T(r,s);if(s=a.closest(".gemboss-pv-devices .gemboss-pv-device"))return U(r,s);if(s=a.closest(".gemboss-editor-mobile-tab"))return K(r,s);if((s=a.closest('[aria-haspopup="menu"]'))&&H(r,s)){n.stopPropagation();return}u&&(s=a.closest(".gemboss-editor-menu-item"))&&u.menu.contains(s)&&m(!0)})}}),e.length}b.initEditorFrame=G;var V=/^#[0-9a-fA-F]{6}$/,I=function(t){var e=t.getAttribute("data-value");return e??(t.textContent||"").trim()},N=function(t){return Array.prototype.filter.call(t.children,function(e){return e.getAttribute("role")==="tab"})};function W(t,e){var r=t.parentNode,i=N(r),n=i.some(function(a){return a.hasAttribute("tabindex")});i.forEach(function(a){var s=a===t;a.setAttribute("aria-selected",String(s)),a.classList.contains("gemboss-ei-tab")&&a.classList.toggle("on",s),n&&a.setAttribute("tabindex",s?"0":"-1");var f=a.getAttribute("aria-controls"),o=f&&document.getElementById(f);o&&(o.hidden=!s)}),e&&t.focus(),l(t,"gemboss:editor-tab",{tab:I(t),index:i.indexOf(t)})}function $(t,e){if(!(e.key!=="ArrowRight"&&e.key!=="ArrowLeft")){var r=N(t);if(r.some(function(a){return a.hasAttribute("tabindex")})){var i=r.findIndex(function(a){return a.getAttribute("aria-selected")==="true"}),n=e.key==="ArrowRight"?(i+1)%r.length:(i-1+r.length)%r.length;e.preventDefault(),W(r[n],!0)}}}function X(t){var e=t.getAttribute("data-open")!=="open";t.setAttribute("data-open",e?"open":"closed"),t.setAttribute("aria-expanded",String(e));var r=t.querySelector(".gemboss-ei-chev");r&&(r.textContent=e?"\u25BE":"\u25B8"),l(t,"gemboss:editor-group",{open:e})}function J(t){var e=t.closest(".gemboss-ei-seg");Array.prototype.forEach.call(e.querySelectorAll("button"),function(r){var i=r===t;r.classList.toggle("active",i),r.classList.toggle("on",i),r.hasAttribute("aria-pressed")&&r.setAttribute("aria-pressed",String(i))}),l(e,"gemboss:editor-seg",{value:I(t)})}function _(t){var e=t.querySelector(".gemboss-ei-swatch"),r=t.querySelector("input:not([type=color])");if(!(!e||!r)){var i=r.value,n=!i||V.test(i);e.value=i&&n?i:"#000000",e.disabled=r.disabled||!n,n?e.removeAttribute("title"):e.title=i+" \u2014 not a plain 6-digit hex; edit it in the box below"}}function Q(t){for(var e=t.labels||[],r=null,i=0;i<e.length&&!r;i++)r=e[i].querySelector(".gemboss-ei-rangeval");r&&(r.textContent=t.value+(r.getAttribute("data-suffix")||""))}function Y(t){var e={},r=["t","r","b","l"];return Array.prototype.forEach.call(t.querySelectorAll("input"),function(i,n){e[r[n]]=i.value.trim()}),e}function B(t){return'url("'+t.replace(/["\\]/g,"\\$&").replace(/[\u0000-\u001f\u007f]/g,function(e){return"\\"+e.charCodeAt(0).toString(16)+" "})+'")'}b.cssUrl=B;function Z(t,e){var r=t.querySelector(".gemboss-ei-img-thumb");r&&(r.style.backgroundImage=e?B(e):"",r.textContent=e?"":"\u2014")}function R(t,e){var r=t.getAttribute("data-gb-value")!=null?t.getAttribute("data-gb-value"):e.defaultValue;e.value!==r&&(t.setAttribute("data-gb-value",e.value),Z(t,e.value),l(t,"gemboss:editor-image",{value:e.value}))}var A=typeof WeakSet=="function"?new WeakSet:null,L=function(t){return t.__gbPanel?!1:(t.__gbPanel=!0,!0)};function ee(t){var e=t||document;return(A?A.has(e):e._gbPanel)?0:(A?A.add(e):e._gbPanel=!0,Array.prototype.forEach.call(e.querySelectorAll(".gemboss-ei-color"),_),e.addEventListener("click",function(r){var i=r.target,n;if(!(!i.closest||!L(r))){if(n=i.closest('.gemboss-ec-tabs > [role="tab"], .gemboss-ei-tabs > [role="tab"]'))return W(n,!1);if(n=i.closest(".gemboss-ei-gh"))return X(n);if((n=i.closest(".gemboss-ei-seg button"))&&!n.disabled)return J(n);if((n=i.closest("[data-gb-image-clear]"))&&!n.disabled){var a=n.closest(".gemboss-ei-img"),s=a&&a.querySelector(".gemboss-ei-img-ctl input");s&&(s.value="",R(a,s))}}}),e.addEventListener("keydown",function(r){var i=r.target;if(!(!i.closest||!L(r))){var n=i.closest('.gemboss-ec-tabs[role="tablist"], .gemboss-ei-tabs[role="tablist"]');if(n&&i.getAttribute("role")==="tab")return $(n,r);r.key==="Enter"&&i.tagName==="INPUT"&&i.type!=="color"&&i.type!=="range"&&i.closest(".gemboss-ei-color, .gemboss-ei-edgebox, .gemboss-ei-img")&&(r.preventDefault(),i.blur())}}),e.addEventListener("input",function(r){var i=r.target;if(!(!i.closest||!L(r))){if(i.classList.contains("gemboss-ei-range")){Q(i),l(i,"gemboss:editor-range",{value:Number(i.value)});return}if(i.classList.contains("gemboss-ei-swatch")){var n=i.closest(".gemboss-ei-color"),a=n&&n.querySelector("input:not([type=color])");a&&(a.value=i.value),l(n||i,"gemboss:editor-color",{value:i.value})}}}),e.addEventListener("focusout",function(r){var i=r.target;if(!(!i.closest||i.tagName!=="INPUT"||!L(r))){var n;if(i.type!=="color"&&(n=i.closest(".gemboss-ei-color"))){_(n),l(n,"gemboss:editor-color",{value:i.value});return}if(n=i.closest(".gemboss-ei-edgebox")){l(n,"gemboss:editor-edges",Y(n));return}(n=i.closest(".gemboss-ei-img"))&&R(n,i)}}),1)}b.initEditorPanel=ee})(typeof globalThis<"u"?globalThis:this);