@gemboss/ui 0.41.0 → 0.42.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.
- package/CHANGELOG.md +59 -0
- package/README.md +7 -3
- package/catalog/editor.html +9 -7
- package/catalog/index.html +11 -4
- package/gemboss-ui.css +1 -1
- package/package.json +1 -1
- package/parts/components.css +1 -1
- package/parts/core.css +1 -1
- package/parts/shell.css +1 -1
- package/src/editor/controls.tsx +52 -8
- package/src/editor/index.ts +2 -2
- package/src/editor/swatch.ts +133 -9
- package/src/editor-plain.js +1 -1
- package/src/index.ts +2 -2
- package/src/portal.tsx +12 -6
- package/src/reactions-data.ts +13 -0
- package/src/reactions.tsx +5 -5
- package/src/team-chat-data.ts +46 -13
- package/src/team-chat-plain.js +1 -1
package/src/editor/index.ts
CHANGED
|
@@ -37,9 +37,9 @@ export {
|
|
|
37
37
|
} from "./panel";
|
|
38
38
|
export {
|
|
39
39
|
ChipButton, ColorControl, EdgeBox, ImageControl, RangeControl, Seg, UnitField, ValueControl,
|
|
40
|
-
type ColorControlProps, type EdgeBoxProps, type EdgeSide, type ImageControlProps, type RangeControlProps, type SegOption, type SegProps, type UnitFieldProps, type ValueControlProps,
|
|
40
|
+
type ColorChoice, type ColorControlProps, type EdgeBoxProps, type EdgeSide, type ImageControlProps, type RangeControlProps, type SegOption, type SegProps, type UnitFieldProps, type ValueControlProps,
|
|
41
41
|
} from "./controls";
|
|
42
|
-
export { swatchState, swatchWhy, type SwatchState } from "./swatch";
|
|
42
|
+
export { opacityOf, parseColor, pickSwatch, swatchState, swatchWhy, themeColorChoices, withOpacity, type Rgba, type SwatchState } from "./swatch";
|
|
43
43
|
export { cssUrl } from "./css-url";
|
|
44
44
|
// Step 4 (0.22.0): the page's layers and the drawer (`layers`, stateless; the page editor draws them), a list whose rows
|
|
45
45
|
// reorder (`sortable`, "use client": a drag holds state; no admin screen uses it), and the element outline (`tree`, the
|
package/src/editor/swatch.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// `<input type="color">` holds exactly one shape: a 7-character hex. Anything else it SANITISES to
|
|
5
5
|
// `#000000` — silently, with no error and no visual difference from a value someone actually chose.
|
|
6
6
|
//
|
|
7
|
-
// gemsite's `cssValue`
|
|
7
|
+
// gemsite's `cssValue` paints any safe string, so a field holds every shape CSS has. Measured:
|
|
8
8
|
//
|
|
9
9
|
// #ff0000 ✔ renders ✔ the swatch can show it
|
|
10
10
|
// #f00 ✔ renders ✘ shown as #000000 — even a 3-digit hex
|
|
@@ -19,11 +19,15 @@
|
|
|
19
19
|
// None of this is hypothetical. `gem_background` renders a gradient today, measured in chromium, and
|
|
20
20
|
// every one of these shapes is something the CLONE pipeline can read off a real storefront.
|
|
21
21
|
//
|
|
22
|
+
// THE SWATCH READS EVERY COLOUR, NOT ONLY HEX (A4, 21/10/2026). Measured on 169 element pages of gemboss-dev: of 3,283 colour
|
|
23
|
+
// values, 66% are `rgb()`, 17% `rgba()`, 9% a 6-digit hex, ~3% `oklch()` (cloned pages carry what the source's CSS computed), 0.3%
|
|
24
|
+
// a theme token. Only the hex 9% had a swatch: nine colours in ten showed a locked black square. A colour is now read (parseColor:
|
|
25
|
+
// hex of 3, 4, 6 or 8 digits, rgb/rgba, hsl/hsla, oklch) and the swatch shows it; a pick keeps the value's transparency and its way
|
|
26
|
+
// of writing (pickSwatch), so turning the swatch on can never drop an alpha. A token, a gradient or a scheme id is still not a colour
|
|
27
|
+
// the swatch can stand for: off, and saying why.
|
|
28
|
+
//
|
|
22
29
|
// Plain TypeScript, no JSX: the admin's node tests import it through the admin's re-export.
|
|
23
30
|
|
|
24
|
-
/** The only shape `<input type="color">` can hold without rewriting it. */
|
|
25
|
-
const SWATCHABLE = /^#[0-9a-fA-F]{6}$/;
|
|
26
|
-
|
|
27
31
|
export type SwatchState = {
|
|
28
32
|
/** What to put in the swatch. Never a colour the author did not choose. */
|
|
29
33
|
swatch: string;
|
|
@@ -31,6 +35,95 @@ export type SwatchState = {
|
|
|
31
35
|
usable: boolean;
|
|
32
36
|
};
|
|
33
37
|
|
|
38
|
+
/** A colour as the swatch and the opacity box hold it: 0-255 channels and an alpha of 0-1. */
|
|
39
|
+
export type Rgba = { r: number; g: number; b: number; a: number };
|
|
40
|
+
|
|
41
|
+
const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));
|
|
42
|
+
const NUM = "[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:e[+-]?\\d+)?";
|
|
43
|
+
/** A number, a percentage (of `pct`), or `none` (0): one CSS colour argument. */
|
|
44
|
+
function arg(s: string, pct: number): number | null {
|
|
45
|
+
if (s === "none") return 0;
|
|
46
|
+
const m = new RegExp(`^(${NUM})(%?)$`, "i").exec(s);
|
|
47
|
+
if (!m) return null;
|
|
48
|
+
const n = Number(m[1]);
|
|
49
|
+
return m[2] ? (n / 100) * pct : n;
|
|
50
|
+
}
|
|
51
|
+
/** `f(a b c / d)`, `f(a, b, c)` or `f(a, b, c, d)`: the arguments, or null for anything else. */
|
|
52
|
+
function args(v: string, fn: RegExp): string[] | null {
|
|
53
|
+
const m = new RegExp(`^(?:${fn.source})\\(\\s*([^()]*?)\\s*\\)$`, "i").exec(v);
|
|
54
|
+
if (!m) return null;
|
|
55
|
+
const body = m[1]!;
|
|
56
|
+
const [main, slash, ...more] = body.split("/");
|
|
57
|
+
if (more.length) return null;
|
|
58
|
+
const parts = main!.includes(",") ? main!.split(",").map((s) => s.trim()) : main!.trim().split(/\s+/);
|
|
59
|
+
if (slash !== undefined) parts.push(slash.trim());
|
|
60
|
+
return parts.every(Boolean) ? parts : null;
|
|
61
|
+
}
|
|
62
|
+
const alphaOf = (s: string | undefined): number | null => (s === undefined ? 1 : arg(s, 1));
|
|
63
|
+
const hue = (s: string): number | null => arg(s.replace(/deg$/i, ""), 360);
|
|
64
|
+
|
|
65
|
+
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
66
|
+
const k = (n: number) => (n + h / 30) % 12;
|
|
67
|
+
const a = s * Math.min(l, 1 - l);
|
|
68
|
+
const f = (n: number) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
69
|
+
return [f(0) * 255, f(8) * 255, f(4) * 255];
|
|
70
|
+
}
|
|
71
|
+
function oklchToRgb(L: number, C: number, H: number): [number, number, number] {
|
|
72
|
+
const a = C * Math.cos((H * Math.PI) / 180), b = C * Math.sin((H * Math.PI) / 180);
|
|
73
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
74
|
+
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
75
|
+
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
76
|
+
const lin = [4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
77
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s];
|
|
78
|
+
const gamma = (c: number) => (c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055);
|
|
79
|
+
return lin.map((c) => clamp(gamma(c), 0, 1) * 255) as [number, number, number];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The colour a value names, or null for anything that is not one colour (a token, a gradient, a scheme id, a padded value). */
|
|
83
|
+
export function parseColor(value: string): Rgba | null {
|
|
84
|
+
if (!value || value !== value.trim()) return null;
|
|
85
|
+
const hex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(value)?.[1];
|
|
86
|
+
if (hex) {
|
|
87
|
+
const full = hex.length <= 4 ? hex.split("").map((c) => c + c).join("") : hex;
|
|
88
|
+
const n = (i: number) => parseInt(full.slice(i, i + 2), 16);
|
|
89
|
+
return { r: n(0), g: n(2), b: n(4), a: full.length === 8 ? Math.round((n(6) / 255) * 1000) / 1000 : 1 };
|
|
90
|
+
}
|
|
91
|
+
const rgb = args(value, /rgba?/);
|
|
92
|
+
if (rgb && (rgb.length === 3 || rgb.length === 4)) {
|
|
93
|
+
const [r, g, b] = rgb.slice(0, 3).map((s) => arg(s, 255));
|
|
94
|
+
const a = alphaOf(rgb[3]);
|
|
95
|
+
if (r == null || g == null || b == null || a == null) return null;
|
|
96
|
+
return { r: clamp(Math.round(r), 0, 255), g: clamp(Math.round(g), 0, 255), b: clamp(Math.round(b), 0, 255), a: clamp(a, 0, 1) };
|
|
97
|
+
}
|
|
98
|
+
const hsl = args(value, /hsla?/);
|
|
99
|
+
if (hsl && (hsl.length === 3 || hsl.length === 4)) {
|
|
100
|
+
const h = hue(hsl[0]!), s = arg(hsl[1]!, 1), l = arg(hsl[2]!, 1), a = alphaOf(hsl[3]);
|
|
101
|
+
// `hsl(0 50 50)` (no %) reads the numbers as percentages, as CSS Color 4 does.
|
|
102
|
+
const pc = (x: number, raw: string) => (raw.endsWith("%") ? x : x / 100);
|
|
103
|
+
if (h == null || s == null || l == null || a == null) return null;
|
|
104
|
+
const [r, g, b] = hslToRgb(((h % 360) + 360) % 360, clamp(pc(s, hsl[1]!), 0, 1), clamp(pc(l, hsl[2]!), 0, 1));
|
|
105
|
+
return { r: Math.round(r), g: Math.round(g), b: Math.round(b), a: clamp(a, 0, 1) };
|
|
106
|
+
}
|
|
107
|
+
const ok = args(value, /oklch/);
|
|
108
|
+
if (ok && (ok.length === 3 || ok.length === 4)) {
|
|
109
|
+
const L = arg(ok[0]!, 1), C = arg(ok[1]!, 0.4), H = hue(ok[2]!), a = alphaOf(ok[3]);
|
|
110
|
+
if (L == null || C == null || H == null || a == null) return null;
|
|
111
|
+
const [r, g, b] = oklchToRgb(L, C, H);
|
|
112
|
+
return { r: Math.round(r), g: Math.round(g), b: Math.round(b), a: clamp(a, 0, 1) };
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const hex2 = (n: number) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0");
|
|
118
|
+
const hexOf = (c: Pick<Rgba, "r" | "g" | "b">) => `#${hex2(c.r)}${hex2(c.g)}${hex2(c.b)}`;
|
|
119
|
+
const alphaText = (a: number) => String(Math.round(a * 100) / 100);
|
|
120
|
+
|
|
121
|
+
/** A colour written as the field already writes it: a hex stays a hex, anything else becomes `rgb()`; with transparency `rgba()`. */
|
|
122
|
+
function writeLike(previous: string, c: Rgba): string {
|
|
123
|
+
if (c.a >= 1) return /^#/.test(previous) || !previous ? hexOf(c) : `rgb(${c.r}, ${c.g}, ${c.b})`;
|
|
124
|
+
return `rgba(${c.r}, ${c.g}, ${c.b}, ${alphaText(c.a)})`;
|
|
125
|
+
}
|
|
126
|
+
|
|
34
127
|
/**
|
|
35
128
|
* `whenEmpty` is the swatch shown for a BLANK value, and it is a parameter rather than a constant on
|
|
36
129
|
* purpose. Three surfaces call this now and they disagree: the element inspector and the schema form
|
|
@@ -41,7 +134,29 @@ export type SwatchState = {
|
|
|
41
134
|
*/
|
|
42
135
|
export function swatchState(value: string, whenEmpty = '#000000'): SwatchState {
|
|
43
136
|
if (!value) return { swatch: whenEmpty, usable: true };
|
|
44
|
-
|
|
137
|
+
const c = parseColor(value);
|
|
138
|
+
return c ? { swatch: hexOf(c), usable: true } : { swatch: '#000000', usable: false };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The value after `hex` is picked on its swatch: the value's transparency and its way of writing kept (a pick never drops an alpha). */
|
|
142
|
+
export function pickSwatch(value: string, hex: string): string {
|
|
143
|
+
const picked = parseColor(hex);
|
|
144
|
+
if (!picked) return value;
|
|
145
|
+
const now = parseColor(value);
|
|
146
|
+
return writeLike(value, { ...picked, a: now ? now.a : 1 });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A value's opacity in whole percent (100 for an opaque colour), or null when it is not one colour. */
|
|
150
|
+
export function opacityOf(value: string): number | null {
|
|
151
|
+
const c = parseColor(value);
|
|
152
|
+
return c ? Math.round(c.a * 100) : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The value with its opacity set to `percent` (0-100), written as the field writes it; unchanged when it is not one colour. */
|
|
156
|
+
export function withOpacity(value: string, percent: number): string {
|
|
157
|
+
const c = parseColor(value);
|
|
158
|
+
if (!c || !Number.isFinite(percent)) return value;
|
|
159
|
+
return writeLike(value, { ...c, a: clamp(Math.round(percent), 0, 100) / 100 });
|
|
45
160
|
}
|
|
46
161
|
|
|
47
162
|
/** The theme's colour tokens a value may name (`var(--gem-color-accent)`), as a seller reads them. */
|
|
@@ -49,17 +164,26 @@ const TOKEN_NAMES: Record<string, string> = { text: "Text", accent: "Accent", "a
|
|
|
49
164
|
surface: "Surface", border: "Border", success: "Success", warning: "Warning", danger: "Danger" };
|
|
50
165
|
const tokenName = (t: string) => TOKEN_NAMES[t] ?? t.charAt(0).toUpperCase() + t.slice(1).replace(/-/g, " ");
|
|
51
166
|
|
|
167
|
+
/** The theme's colours as ColorControl chips (A4b), in the theme's own order and names, for the roles `palette` has: each writes
|
|
168
|
+
* `var(--gem-color-<role>)` and paints the colour the page draws for it (a frame's palette, gemsite InspectorBridge). A role the
|
|
169
|
+
* palette does not say, or says as something that is not one colour, has no chip. */
|
|
170
|
+
export function themeColorChoices(palette: Readonly<Record<string, string>> | null | undefined): Array<{ value: string; label: string; swatch: string }> {
|
|
171
|
+
if (!palette) return [];
|
|
172
|
+
return Object.keys(TOKEN_NAMES).filter((t) => palette[t] && parseColor(palette[t]!.trim()))
|
|
173
|
+
.map((t) => ({ value: `var(--gem-color-${t})`, label: TOKEN_NAMES[t]!, swatch: palette[t]!.trim() }));
|
|
174
|
+
}
|
|
175
|
+
|
|
52
176
|
/**
|
|
53
177
|
* WHY THE SWATCH IS OFF, said where a seller sees it (Claude Design: a disabled control says why). The editors' selection toolbar
|
|
54
178
|
* writes the theme's own colours as tokens (`var(--gem-color-accent)`), so every colour picked there turned its field's swatch grey,
|
|
55
179
|
* with only a tooltip a finger or the keyboard never reaches (inventory of 19/10/2026). Null when the swatch shows the value.
|
|
56
180
|
*/
|
|
57
181
|
export function swatchWhy(value: string): string | null {
|
|
58
|
-
// Exactly when the swatch is off (swatchState's one rule): a padded
|
|
182
|
+
// Exactly when the swatch is off (swatchState's one rule): a padded value is off too, and says so (review of #4636).
|
|
59
183
|
if (swatchState(value).usable) return null;
|
|
60
184
|
const v = value.trim();
|
|
61
185
|
const token = /^var\(\s*--gem-color-([a-z-]+)\s*\)$/i.exec(v)?.[1]?.toLowerCase();
|
|
62
|
-
if (token) return `Theme color: ${tokenName(token)}. Type a
|
|
63
|
-
if (/^var\(/i.test(v)) return "A theme value. Type a
|
|
64
|
-
return "Kept as written: the swatch
|
|
186
|
+
if (token) return `Theme color: ${tokenName(token)}. Type a color to use your own.`;
|
|
187
|
+
if (/^var\(/i.test(v)) return "A theme value. Type a color to use your own.";
|
|
188
|
+
return "Kept as written: the swatch shows one color (hex, rgb, hsl or oklch), not a gradient or a name.";
|
|
65
189
|
}
|
package/src/editor-plain.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(E){"use strict";var v=E.gembossUI;if(!v){E.console&&E.console.error("@gemboss/ui editor-plain.js: load kit-plain.js first (it defines gembossUI)");return}if(v.initEditorFrame)return;var k=5,R=360,p=typeof WeakSet=="function"?new WeakSet:null,u=function(t,e,r){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:r}))},S=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 b(t){var e=g(t);return{min:S(e&&e.getAttribute("aria-valuemin"),300),cap:S(e&&(e.getAttribute("data-gb-max")||e.getAttribute("aria-valuemax")),680)}}function x(t){var e=b(t),r=c(t),i=r?r.getBoundingClientRect().width:0;return i>0?Math.max(e.min,Math.min(e.cap,i-R-k)):e.cap}var h=typeof WeakMap=="function"?new WeakMap:null;function C(t){var e=h?h.get(t):t._gbWant;return e??y(t)}function q(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=b(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){q(t,o);var L=n.getAttribute("data-gb-storage");if(L)try{localStorage.setItem(L,String(o))}catch{}u(t,"gemboss:editor-split",{width:o})}return o}}function y(t){var e=g(t);return S(e&&e.getAttribute("aria-valuenow"),384)}function z(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>=b(t).min&&a<=b(t).cap&&(n=a)}catch{}q(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(L){s=L.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=b(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 F(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,C(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":""),u(t,"gemboss:editor-chat",{collapsed:i})}}function K(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))}),u(t,"gemboss:editor-device",{device:e.getAttribute("data-device")||(e.title||"").toLowerCase()}))}function U(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))}),u(t,"gemboss:editor-pane",{pane:r}))}var l=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=l;e&&(l=null,e.menu.hidden=!0,e.trigger.setAttribute("aria-expanded","false"),e.trigger.classList.remove("active"),t&&e.trigger.focus(),u(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=l&&l.trigger===e;return m(!1),i||(l={root:t,trigger:e,menu:r},r.hidden=!1,e.setAttribute("aria-expanded","true"),e.classList.add("active"),M(l),u(t,"gemboss:editor-menu",{open:!0,menu:r})),!0}var N=!1;function $(){N||(N=!0,document.addEventListener("pointerdown",function(t){l&&!l.menu.contains(t.target)&&!l.trigger.contains(t.target)&&m(!1)}),addEventListener("keydown",function(t){t.key!=="Escape"||!l||t.isComposing||t.keyCode===229||(t.preventDefault(),t.stopImmediatePropagation(),m(!0))},!0),addEventListener("resize",function(){l&&M(l)}),addEventListener("blur",function(){m(!1)}))}function j(t){var e=t&&t.classList&&t.classList.contains("gemboss-editor")?[t]:Array.prototype.slice.call((t||document).querySelectorAll(".gemboss-editor"));return $(),e.forEach(function(r){if(!(p?p.has(r):r._gbEditor)){p?p.add(r):r._gbEditor=!0,z(r);var i=c(r);i&&typeof ResizeObserver=="function"&&new ResizeObserver(function(){i.classList.contains("is-chat-collapsed")||d(r,C(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 F(r,s);if(s=a.closest(".gemboss-pv-devices .gemboss-pv-device"))return K(r,s);if(s=a.closest(".gemboss-editor-mobile-tab"))return U(r,s);if((s=a.closest('[aria-haspopup="menu"]'))&&H(r,s)){n.stopPropagation();return}l&&(s=a.closest(".gemboss-editor-menu-item"))&&l.menu.contains(s)&&m(!0)})}}),e.length}v.initEditorFrame=j;var G=/^#[0-9a-fA-F]{6}$/,P=function(t){var e=t.getAttribute("data-value");return e??(t.textContent||"").trim()},W=function(t){return Array.prototype.filter.call(t.children,function(e){return e.getAttribute("role")==="tab"})};function I(t,e){var r=t.parentNode,i=W(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(),u(t,"gemboss:editor-tab",{tab:P(t),index:i.indexOf(t)})}function V(t,e){if(!(e.key!=="ArrowRight"&&e.key!=="ArrowLeft")){var r=W(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(),I(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"),u(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))}),u(e,"gemboss:editor-seg",{value:P(t)})}var Q={text:"Text",accent:"Accent","accent-text":"Accent text",muted:"Muted",background:"Background",surface:"Surface",border:"Border",success:"Success",warning:"Warning",danger:"Danger"},Y=0;function Z(t){var e=t.trim(),r=/^var\(\s*--gem-color-([a-z-]+)\s*\)$/i.exec(e);if(r){var i=r[1].toLowerCase();return"Theme color: "+(Q[i]||i.charAt(0).toUpperCase()+i.slice(1).replace(/-/g," "))+". Type a hex to use your own."}return/^var\(/i.test(e)?"A theme value. Type a hex to use your own.":"Kept as written: the swatch picks 6-digit hex colors only."}function B(t){var e=t.querySelector(".gemboss-ei-swatch"),r=t.querySelector("input:not([type=color])");if(!(!e||!r)){var i=r.value,n=!i||G.test(i);e.value=i&&n?i:"#000000",e.disabled=r.disabled||!n;var a=t.querySelector(".gemboss-ei-color-why");if(n){e.removeAttribute("title"),e.removeAttribute("aria-describedby"),a&&a.parentNode.removeChild(a);return}var s=Z(i);e.title=s,a||(a=document.createElement("p"),a.className="gemboss-ei-color-why",t.appendChild(a)),a.id||(a.id="gemboss-ei-color-why-"+ ++Y),a.textContent=s,e.setAttribute("aria-describedby",a.id)}}function ee(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 te(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 T(t){return'url("'+t.replace(/["\\]/g,"\\$&").replace(/[\u0000-\u001f\u007f]/g,function(e){return"\\"+e.charCodeAt(0).toString(16)+" "})+'")'}v.cssUrl=T;function re(t,e){var r=t.querySelector(".gemboss-ei-img-thumb");r&&(r.style.backgroundImage=e?T(e):"",r.textContent=e?"":"\u2014")}function _(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),re(t,e.value),u(t,"gemboss:editor-image",{value:e.value}))}var A=typeof WeakSet=="function"?new WeakSet:null,w=function(t){return t.__gbPanel?!1:(t.__gbPanel=!0,!0)};function ie(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"),B),e.addEventListener("click",function(r){var i=r.target,n;if(!(!i.closest||!w(r))){if(n=i.closest('.gemboss-ec-tabs > [role="tab"], .gemboss-ei-tabs > [role="tab"]'))return I(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="",_(a,s))}}}),e.addEventListener("keydown",function(r){var i=r.target;if(!(!i.closest||!w(r))){var n=i.closest('.gemboss-ec-tabs[role="tablist"], .gemboss-ei-tabs[role="tablist"]');if(n&&i.getAttribute("role")==="tab")return V(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||!w(r))){if(i.classList.contains("gemboss-ei-range")){ee(i),u(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),u(n||i,"gemboss:editor-color",{value:i.value})}}}),e.addEventListener("focusout",function(r){var i=r.target;if(!(!i.closest||i.tagName!=="INPUT"||!w(r))){var n;if(i.type!=="color"&&(n=i.closest(".gemboss-ei-color"))){B(n),u(n,"gemboss:editor-color",{value:i.value});return}if(n=i.closest(".gemboss-ei-edgebox")){u(n,"gemboss:editor-edges",te(n));return}(n=i.closest(".gemboss-ei-img"))&&_(n,i)}}),1)}v.initEditorPanel=ie})(typeof globalThis<"u"?globalThis:this);
|
|
1
|
+
(function(k){"use strict";var g=k.gembossUI;if(!g){k.console&&k.console.error("@gemboss/ui editor-plain.js: load kit-plain.js first (it defines gembossUI)");return}if(g.initEditorFrame)return;var N=5,K=360,A=typeof WeakSet=="function"?new WeakSet:null,u=function(t,e,r){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:r}))},M=function(t,e){var r=Number(t);return isFinite(r)&&r>0?r:e};function d(t){return t.querySelector(".gemboss-editor-body")}function b(t){var e=d(t);return e&&e.querySelector(":scope > .gemboss-editor-resizer")}function p(t){var e=b(t);return{min:M(e&&e.getAttribute("aria-valuemin"),300),cap:M(e&&(e.getAttribute("data-gb-max")||e.getAttribute("aria-valuemax")),680)}}function C(t){var e=p(t),r=d(t),i=r?r.getBoundingClientRect().width:0;return i>0?Math.max(e.min,Math.min(e.cap,i-K-N)):e.cap}var w=typeof WeakMap=="function"?new WeakMap:null;function q(t){var e=w?w.get(t):t._gbWant;return e??S(t)}function O(t,e){w?w.set(t,e):t._gbWant=e}function m(t,e,r,i){var a=d(t),n=b(t);if(a){var s=i??C(t),o=p(t),l=Math.round(Math.min(s,Math.max(o.min,e)));if(a.style.setProperty("--gemboss-chat-w",l+"px"),n&&(n.setAttribute("aria-valuenow",String(l)),n.setAttribute("aria-valuemax",String(s))),r){O(t,l);var f=a.getAttribute("data-gb-storage");if(f)try{localStorage.setItem(f,String(l))}catch{}u(t,"gemboss:editor-split",{width:l})}return l}}function S(t){var e=b(t);return M(e&&e.getAttribute("aria-valuenow"),384)}function U(t){var e=d(t),r=b(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"),a=S(t);if(i)try{var n=Number(localStorage.getItem(i));n>=p(t).min&&n<=p(t).cap&&(a=n)}catch{}O(t,a),e.classList.contains("is-chat-collapsed")||m(t,a,!1)}}function j(t,e){var r=d(t);if(r&&!(e.button!==0||e.isPrimary===!1)){e.preventDefault();var i=r.getBoundingClientRect().left,a=C(t),n=0,s=S(t);document.body.classList.add("ui-resizing");var o=function(f){s=f.clientX-i-N,n||(n=requestAnimationFrame(function(){n=0,m(t,s,!1,a)}))},l=function(){n&&cancelAnimationFrame(n),document.body.classList.remove("ui-resizing"),removeEventListener("pointermove",o),removeEventListener("pointerup",l),removeEventListener("pointercancel",l),removeEventListener("blur",l),m(t,s,!0,a)};addEventListener("pointermove",o),addEventListener("pointerup",l),addEventListener("pointercancel",l),addEventListener("blur",l)}}function H(t,e){if(!(e.altKey||e.metaKey||e.ctrlKey)){var r=e.shiftKey?64:16,i=S(t),a=p(t);if(e.key==="ArrowLeft")i-=r;else if(e.key==="ArrowRight")i+=r;else if(e.key==="Home")i=a.min;else if(e.key==="End")i=C(t);else return;e.preventDefault(),m(t,i,!0)}}function G(t,e){var r=d(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"):m(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 a=e.querySelector("rect[fill='currentColor']");a&&(a.style.display=i?"none":""),u(t,"gemboss:editor-chat",{collapsed:i})}}function V(t,e){var r=e.closest(".gemboss-pv-devices");r&&(r.querySelectorAll(".gemboss-pv-device").forEach(function(i){var a=i===e;i.classList.toggle("active",a),i.setAttribute("aria-pressed",String(a))}),u(t,"gemboss:editor-device",{device:e.getAttribute("data-device")||(e.title||"").toLowerCase()}))}function X(t,e){var r=e.getAttribute("data-pane")||(Array.prototype.indexOf.call(e.parentNode.children,e)===0?"chat":"preview"),i=d(t);i&&(i.setAttribute("data-mobile-pane",r),e.parentNode.querySelectorAll(".gemboss-editor-mobile-tab").forEach(function(a){var n=a===e;a.classList.toggle("active",n),a.setAttribute("aria-selected",String(n))}),u(t,"gemboss:editor-pane",{pane:r}))}var c=null;function I(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 h(t){var e=c;e&&(c=null,e.menu.hidden=!0,e.trigger.setAttribute("aria-expanded","false"),e.trigger.classList.remove("active"),t&&e.trigger.focus(),u(e.root,"gemboss:editor-menu",{open:!1,menu:e.menu}))}function J(t,e){var r=e.nextElementSibling;if(!r||!r.classList.contains("gemboss-editor-menu"))return!1;var i=c&&c.trigger===e;return h(!1),i||(c={root:t,trigger:e,menu:r},r.hidden=!1,e.setAttribute("aria-expanded","true"),e.classList.add("active"),I(c),u(t,"gemboss:editor-menu",{open:!0,menu:r})),!0}var P=!1;function Q(){P||(P=!0,document.addEventListener("pointerdown",function(t){c&&!c.menu.contains(t.target)&&!c.trigger.contains(t.target)&&h(!1)}),addEventListener("keydown",function(t){t.key!=="Escape"||!c||t.isComposing||t.keyCode===229||(t.preventDefault(),t.stopImmediatePropagation(),h(!0))},!0),addEventListener("resize",function(){c&&I(c)}),addEventListener("blur",function(){h(!1)}))}function Y(t){var e=t&&t.classList&&t.classList.contains("gemboss-editor")?[t]:Array.prototype.slice.call((t||document).querySelectorAll(".gemboss-editor"));return Q(),e.forEach(function(r){if(!(A?A.has(r):r._gbEditor)){A?A.add(r):r._gbEditor=!0,U(r);var i=d(r);i&&typeof ResizeObserver=="function"&&new ResizeObserver(function(){i.classList.contains("is-chat-collapsed")||m(r,q(r),!1)}).observe(i),r.addEventListener("pointerdown",function(a){var n=a.target.closest(".gemboss-editor-resizer");n&&n===b(r)&&j(r,a)}),r.addEventListener("keydown",function(a){a.target.classList&&a.target.classList.contains("gemboss-editor-resizer")&&a.target===b(r)&&H(r,a)}),r.addEventListener("click",function(a){var n=a.target,s;if(s=n.closest(".gemboss-editor-chat-toggle"))return G(r,s);if(s=n.closest(".gemboss-pv-devices .gemboss-pv-device"))return V(r,s);if(s=n.closest(".gemboss-editor-mobile-tab"))return X(r,s);if((s=n.closest('[aria-haspopup="menu"]'))&&J(r,s)){a.stopPropagation();return}c&&(s=n.closest(".gemboss-editor-menu-item"))&&c.menu.contains(s)&&h(!0)})}}),e.length}g.initEditorFrame=Y;function y(t){if(!t||t!==t.trim())return null;var e=/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(t),r,i,a,n;if(e){var s=e[1].length<=4?e[1].replace(/./g,"$&$&"):e[1];return n=function(ue){return parseInt(s.substr(ue,2),16)},{r:n(0),g:n(2),b:n(4),a:s.length===8?Math.round(n(6)/255*1e3)/1e3:1}}if(r=/^rgba?\(\s*([^()]*?)\s*\)$/i.exec(t),!r)return null;var o=r[1].split("/");if(i=o[0].indexOf(",")>=0?o[0].split(","):o[0].trim().split(/\s+/),o.length===2&&i.push(o[1]),o.length>2||i.length<3||i.length>4)return null;var l=[];for(a=0;a<i.length;a++){var f=i[a].trim(),v=/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?%?$/i.test(f)?parseFloat(f):f==="none"?0:NaN;if(isNaN(v))return null;/%$/.test(f)&&(v=v/100*(a<3?255:1)),l.push(a<3?Math.min(255,Math.max(0,Math.round(v))):Math.min(1,Math.max(0,v)))}return{r:l[0],g:l[1],b:l[2],a:l.length===4?l[3]:1}}function W(t){return"#"+[t.r,t.g,t.b].map(function(e){return(e<16?"0":"")+e.toString(16)}).join("")}function B(t,e,r){return r<1?"rgba("+e.r+", "+e.g+", "+e.b+", "+Math.round(r*100)/100+")":/^#/.test(t)||!t?W(e):"rgb("+e.r+", "+e.g+", "+e.b+")"}function _(t,e){var r=y(e),i=y(t);return r?B(t,r,i?i.a:1):t}function T(t,e){var r=y(t);return r&&isFinite(e)?B(t,r,Math.min(100,Math.max(0,Math.round(e)))/100):t}g.colorOf=y,g.pickSwatch=_,g.withOpacity=T;var F=function(t){var e=t.getAttribute("data-value");return e??(t.textContent||"").trim()},R=function(t){return Array.prototype.filter.call(t.children,function(e){return e.getAttribute("role")==="tab"})};function z(t,e){var r=t.parentNode,i=R(r),a=i.some(function(n){return n.hasAttribute("tabindex")});i.forEach(function(n){var s=n===t;n.setAttribute("aria-selected",String(s)),n.classList.contains("gemboss-ei-tab")&&n.classList.toggle("on",s),a&&n.setAttribute("tabindex",s?"0":"-1");var o=n.getAttribute("aria-controls"),l=o&&document.getElementById(o);l&&(l.hidden=!s)}),e&&t.focus(),u(t,"gemboss:editor-tab",{tab:F(t),index:i.indexOf(t)})}function Z(t,e){if(!(e.key!=="ArrowRight"&&e.key!=="ArrowLeft")){var r=R(t);if(r.some(function(n){return n.hasAttribute("tabindex")})){var i=r.findIndex(function(n){return n.getAttribute("aria-selected")==="true"}),a=e.key==="ArrowRight"?(i+1)%r.length:(i-1+r.length)%r.length;e.preventDefault(),z(r[a],!0)}}}function ee(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"),u(t,"gemboss:editor-group",{open:e})}function te(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))}),u(e,"gemboss:editor-seg",{value:F(t)})}var re={text:"Text",accent:"Accent","accent-text":"Accent text",muted:"Muted",background:"Background",surface:"Surface",border:"Border",success:"Success",warning:"Warning",danger:"Danger"},ie=0;function ae(t){var e=t.trim(),r=/^var\(\s*--gem-color-([a-z-]+)\s*\)$/i.exec(e);if(r){var i=r[1].toLowerCase();return"Theme color: "+(re[i]||i.charAt(0).toUpperCase()+i.slice(1).replace(/-/g," "))+". Type a color to use your own."}return/^var\(/i.test(e)?"A theme value. Type a color to use your own.":"Kept as written: the swatch shows one color (hex, rgb, hsl or oklch), not a gradient or a name."}function L(t){var e=t.querySelector(".gemboss-ei-swatch"),r=t.querySelector("input:not([type=color]):not([data-alpha])");if(!(!e||!r)){var i=r.value,a=y(i),n=!i||!!a;e.value=a?W(a):"#000000";var s=t.querySelector("input[data-alpha]");a&&!s&&(s=document.createElement("input"),s.className="gemboss-field gemboss-ei-inp",s.setAttribute("data-alpha",""),s.setAttribute("inputmode","numeric"),s.setAttribute("aria-label",(e.getAttribute("aria-label")||"Color swatch").replace(/ swatch$/,"")+" opacity"),s.style.flex="0 0 64px",r.parentNode.insertBefore(s,r.nextSibling)),s&&(s.hidden=!a,a&&(s.value=Math.round(a.a*100)+"%"),s.disabled=r.disabled),e.disabled=r.disabled||!n;var o=t.querySelector(".gemboss-ei-color-why");if(n){e.removeAttribute("title"),e.removeAttribute("aria-describedby"),o&&o.parentNode.removeChild(o);return}var l=ae(i);e.title=l,o||(o=document.createElement("p"),o.className="gemboss-ei-color-why",t.appendChild(o)),o.id||(o.id="gemboss-ei-color-why-"+ ++ie),o.textContent=l,e.setAttribute("aria-describedby",o.id)}}function ne(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 se(t){var e={},r=["t","r","b","l"];return Array.prototype.forEach.call(t.querySelectorAll("input"),function(i,a){e[r[a]]=i.value.trim()}),e}function $(t){return'url("'+t.replace(/["\\]/g,"\\$&").replace(/[\u0000-\u001f\u007f]/g,function(e){return"\\"+e.charCodeAt(0).toString(16)+" "})+'")'}g.cssUrl=$;function oe(t,e){var r=t.querySelector(".gemboss-ei-img-thumb");r&&(r.style.backgroundImage=e?$(e):"",r.textContent=e?"":"\u2014")}function D(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),oe(t,e.value),u(t,"gemboss:editor-image",{value:e.value}))}var E=typeof WeakSet=="function"?new WeakSet:null,x=function(t){return t.__gbPanel?!1:(t.__gbPanel=!0,!0)};function le(t){var e=t||document;return(E?E.has(e):e._gbPanel)?0:(E?E.add(e):e._gbPanel=!0,Array.prototype.forEach.call(e.querySelectorAll(".gemboss-ei-color"),L),e.addEventListener("click",function(r){var i=r.target,a;if(!(!i.closest||!x(r))){if(a=i.closest('.gemboss-ec-tabs > [role="tab"], .gemboss-ei-tabs > [role="tab"]'))return z(a,!1);if(a=i.closest(".gemboss-ei-gh"))return ee(a);if((a=i.closest(".gemboss-ei-seg button"))&&!a.disabled)return te(a);if((a=i.closest("[data-gb-image-clear]"))&&!a.disabled){var n=a.closest(".gemboss-ei-img"),s=n&&n.querySelector(".gemboss-ei-img-ctl input");s&&(s.value="",D(n,s))}}}),e.addEventListener("keydown",function(r){var i=r.target;if(!(!i.closest||!x(r))){var a=i.closest('.gemboss-ec-tabs[role="tablist"], .gemboss-ei-tabs[role="tablist"]');if(a&&i.getAttribute("role")==="tab")return Z(a,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||!x(r))){if(i.classList.contains("gemboss-ei-range")){ne(i),u(i,"gemboss:editor-range",{value:Number(i.value)});return}if(i.classList.contains("gemboss-ei-swatch")){var a=i.closest(".gemboss-ei-color"),n=a&&a.querySelector("input:not([type=color]):not([data-alpha])"),s=n?_(n.value,i.value):i.value;n&&(n.value=s),u(a||i,"gemboss:editor-color",{value:s})}}}),e.addEventListener("focusout",function(r){var i=r.target;if(!(!i.closest||i.tagName!=="INPUT"||!x(r))){var a;if(i.hasAttribute("data-alpha")&&(a=i.closest(".gemboss-ei-color"))){var n=a.querySelector("input:not([type=color]):not([data-alpha])"),s=Number(String(i.value).replace(/%/g,"").trim()),o=n&&String(i.value).trim()&&isFinite(s)?T(n.value,s):null;o&&o!==n.value?(n.value=o,L(a),u(a,"gemboss:editor-color",{value:o})):L(a);return}if(i.type!=="color"&&(a=i.closest(".gemboss-ei-color"))){L(a),u(a,"gemboss:editor-color",{value:i.value});return}if(a=i.closest(".gemboss-ei-edgebox")){u(a,"gemboss:editor-edges",se(a));return}(a=i.closest(".gemboss-ei-img"))&&D(a,i)}}),1)}g.initEditorPanel=le})(typeof globalThis<"u"?globalThis:this);
|
package/src/index.ts
CHANGED
|
@@ -57,7 +57,7 @@ export { ThemeToggle } from "./theme";
|
|
|
57
57
|
export { THEME_KEY, themeScript } from "./theme-script";
|
|
58
58
|
export { Reactions, ReactionPicker, type ReactionsProps } from "./reactions";
|
|
59
59
|
export { ActionCard, ActionCardItem, ActionCardSection, ActionChange, type ActionCardItemProps, type ActionCardOutcome, type ActionCardProps } from "./action-card";
|
|
60
|
-
export { isReactionEmoji, QUICK_REACTIONS, REACTION_EMOJI, REACTION_GROUPS, reactionLabel, reactionWords, searchReactions, toggleReaction, type Reaction } from "./reactions-data";
|
|
60
|
+
export { FREQUENT_MAX, frequentReactions, isReactionEmoji, QUICK_REACTIONS, REACTION_EMOJI, REACTION_GROUPS, reactionLabel, reactionWords, searchReactions, SUGGESTED_REACTIONS, toggleReaction, type Reaction } from "./reactions-data";
|
|
61
61
|
// The team chat layer (20/10/2026, README "The team chat layer"): GemCare's channel chat for every surface, with
|
|
62
62
|
// team-chat.css its own entry. The row rules (turns, day separators, the "New messages" line) are plain data.
|
|
63
63
|
export {
|
|
@@ -66,4 +66,4 @@ export {
|
|
|
66
66
|
type ChannelItemProps, type ChannelKind, type ChannelRailProps, type ChatHeaderProps, type ComposerProps, type MessageListProps, type MessageProps,
|
|
67
67
|
type PeoplePanelProps, type PersonRowProps, type Presence, type TeamChatLabels, type TeamChatPanel, type TeamChatProps, type TeamChatView, type ThreadPanelProps,
|
|
68
68
|
} from "./team-chat";
|
|
69
|
-
export { chatDayKey, chatDayWord, teamChatRows, type ChatRow, type ChatRowMessage, type ChatRowOptions } from "./team-chat-data";
|
|
69
|
+
export { chatDayISO, chatDayKey, chatDayWord, teamChatRows, type ChatRow, type ChatRowMessage, type ChatRowOptions } from "./team-chat-data";
|
package/src/portal.tsx
CHANGED
|
@@ -93,13 +93,15 @@ export type LeaderRow = {
|
|
|
93
93
|
|
|
94
94
|
const MEDALS = ["🥇", "🥈", "🥉"];
|
|
95
95
|
const PODIUM_SLOT = ["gb-podium-slot--1", "gb-podium-slot--2", "gb-podium-slot--3"];
|
|
96
|
+
/** What screen readers hear before a name ("Rank 1"); pass `rankLabel` in the page's language ("Hạng 1", "第 1 名"). */
|
|
97
|
+
const RANK_LABEL = (rank: number) => `Rank ${rank}`;
|
|
96
98
|
|
|
97
99
|
/**
|
|
98
100
|
* A ranked list of people. Rows are links when they carry `href`; `unit` is read after the count by
|
|
99
101
|
* screen readers ("163 votes") and can be shown with `showUnit`. Empty → nothing (a "top" list of
|
|
100
102
|
* zeros looks dead; GemCommunity hides it the same way).
|
|
101
103
|
*/
|
|
102
|
-
export function Leaderboard({ title, label, caption, rows, unit, showUnit = false, viewAll, as: L = "a", className, headingLevel = 2, id, currentLabel = "you" }: {
|
|
104
|
+
export function Leaderboard({ title, label, caption, rows, unit, showUnit = false, viewAll, as: L = "a", className, headingLevel = 2, id, currentLabel = "you", rankLabel = RANK_LABEL }: {
|
|
103
105
|
/** The visible heading. Leave it out on a page whose own <h1> already names the board; give `label`. */
|
|
104
106
|
title?: React.ReactNode; label?: string; caption?: React.ReactNode; rows: LeaderRow[]; unit?: string; showUnit?: boolean;
|
|
105
107
|
viewAll?: { href: string; label: React.ReactNode }; as?: LinkLike; className?: string; headingLevel?: 2 | 3;
|
|
@@ -107,6 +109,8 @@ export function Leaderboard({ title, label, caption, rows, unit, showUnit = fals
|
|
|
107
109
|
id?: string;
|
|
108
110
|
/** Read by screen readers after the viewer's own name ("Minh, you"). "" when the name already says it on screen. */
|
|
109
111
|
currentLabel?: string;
|
|
112
|
+
/** Read by screen readers before each name: `(n) => "Rank " + n` unless given. */
|
|
113
|
+
rankLabel?: (rank: number) => string;
|
|
110
114
|
}) {
|
|
111
115
|
if (!rows.length) return null;
|
|
112
116
|
const H = `h${headingLevel}` as "h2" | "h3";
|
|
@@ -125,7 +129,7 @@ export function Leaderboard({ title, label, caption, rows, unit, showUnit = fals
|
|
|
125
129
|
const inner = (
|
|
126
130
|
<>
|
|
127
131
|
<span className={cx("gb-leaderboard-rank", !MEDALS[i] && "is-number")}>
|
|
128
|
-
<span aria-hidden="true">{MEDALS[i] ?? i + 1}</span><span className="gb-sr-only">
|
|
132
|
+
<span aria-hidden="true">{MEDALS[i] ?? i + 1}</span><span className="gb-sr-only">{rankLabel(i + 1)}: </span>
|
|
129
133
|
</span>
|
|
130
134
|
<Avatar name={r.name} src={r.avatar} />
|
|
131
135
|
<span className="gb-leaderboard-name">{r.name}{r.current && currentLabel ? <span className="gb-sr-only">, {currentLabel}</span> : null}</span>
|
|
@@ -147,17 +151,19 @@ export function Leaderboard({ title, label, caption, rows, unit, showUnit = fals
|
|
|
147
151
|
);
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
/** The first three as a podium, drawn second · first · third. */
|
|
151
|
-
export function Podium({ rows, unit, as: L = "a", className
|
|
154
|
+
/** The first three as a podium, drawn second · first · third. `label` names the list for screen readers ("Top three"). */
|
|
155
|
+
export function Podium({ rows, unit, as: L = "a", className, label = "Top three", rankLabel = RANK_LABEL }: {
|
|
156
|
+
rows: LeaderRow[]; unit?: string; as?: LinkLike; className?: string; label?: string; rankLabel?: (rank: number) => string;
|
|
157
|
+
}) {
|
|
152
158
|
if (!rows.length) return null;
|
|
153
159
|
return (
|
|
154
|
-
<ol className={cx("gb-podium", className)} aria-label=
|
|
160
|
+
<ol className={cx("gb-podium", className)} aria-label={label}>
|
|
155
161
|
{[1, 0, 2].map((idx) => {
|
|
156
162
|
const r = rows[idx];
|
|
157
163
|
if (!r) return <li key={idx} className="gb-podium-slot is-empty" aria-hidden="true" />;
|
|
158
164
|
const body = (
|
|
159
165
|
<>
|
|
160
|
-
<span className="gb-podium-medal"><span aria-hidden="true">{MEDALS[idx]}</span><span className="gb-sr-only">
|
|
166
|
+
<span className="gb-podium-medal"><span aria-hidden="true">{MEDALS[idx]}</span><span className="gb-sr-only">{rankLabel(idx + 1)}: </span></span>
|
|
161
167
|
<Avatar name={r.name} src={r.avatar} size={56} />
|
|
162
168
|
<span className="gb-podium-name">{r.name}</span>
|
|
163
169
|
<span className="gb-podium-value">{r.value}{unit ? <span className="gb-podium-unit"> {unit}</span> : null}</span>
|
package/src/reactions-data.ts
CHANGED
|
@@ -23,6 +23,19 @@ const KEYS = new Map<string, string>(REACTION_GROUPS.flatMap(([, items]) => item
|
|
|
23
23
|
/** The quick pair shown on hover next to "Add reaction" (GemCare's 👍 Like and ✅ Done). */
|
|
24
24
|
export const QUICK_REACTIONS: readonly string[] = ["👍", "✅"];
|
|
25
25
|
|
|
26
|
+
/** The picker's "Frequently used" row holds this many. */
|
|
27
|
+
export const FREQUENT_MAX = 9;
|
|
28
|
+
/** What fills "Frequently used" until this browser has used that many (Chris 20/10/2026: GemCare's row always held nine, and a
|
|
29
|
+
* row of one or two for a new person read as empty). The quick pair first, then the reactions a team reaches for most. */
|
|
30
|
+
export const SUGGESTED_REACTIONS: readonly string[] = ["👍", "✅", "🙏", "🎉", "😂", "❤️", "👀", "🙌", "🔥"];
|
|
31
|
+
|
|
32
|
+
/** "Frequently used": what this browser used, most recent first, then the suggestions it has not used, up to FREQUENT_MAX. */
|
|
33
|
+
export function frequentReactions(recent: readonly string[]): string[] {
|
|
34
|
+
const out: string[] = [];
|
|
35
|
+
for (const e of [...recent, ...SUGGESTED_REACTIONS]) if (isReactionEmoji(e) && !out.includes(e) && out.length < FREQUENT_MAX) out.push(e);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
export function isReactionEmoji(e: unknown): e is string {
|
|
27
40
|
return typeof e === "string" && KEYS.has(e);
|
|
28
41
|
}
|
package/src/reactions.tsx
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* count does not), the viewer's own outlined and filled (not only coloured: two chips that differ only in hue look the
|
|
11
11
|
* same to a colour-blind reader). No chips, no row.
|
|
12
12
|
* - "Add reaction": opens the picker IN FLOW under the row (not floating: a layer in a scrolling column has to fight the
|
|
13
|
-
* bottom edge; a row in flow is scrolled to by the browser). Frequently used first (this browser's,
|
|
14
|
-
*
|
|
13
|
+
* bottom edge; a row in flow is scrolled to by the browser). Frequently used first (this browser's, filled up to nine
|
|
14
|
+
* with SUGGESTED_REACTIONS), then the groups; search by words; arrows move in the grid, Enter in the search picks the first, Escape closes
|
|
15
15
|
* and gives focus back to the button.
|
|
16
16
|
*
|
|
17
17
|
* Each target is 44px (a control, CLAUDE.md §9.1) while the visible pill is 26px: transparent padding carries the rest,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import "./reactions.css"; // its own stylesheet travels with it (0.23.0): a surface that never renders it never loads it
|
|
21
21
|
import React, { useEffect, useId, useRef, useState } from "react";
|
|
22
22
|
import { Icon } from "./icons";
|
|
23
|
-
import {
|
|
23
|
+
import { frequentReactions, REACTION_GROUPS, reactionLabel, reactionWords, searchReactions, type Reaction } from "./reactions-data";
|
|
24
24
|
|
|
25
25
|
const RECENT_KEY = "gemboss-ui:reactions-recent";
|
|
26
26
|
const RECENT_MAX = 9;
|
|
@@ -91,11 +91,11 @@ export function Reactions({ items, onToggle, target = "this", canReact = true, o
|
|
|
91
91
|
|
|
92
92
|
export function ReactionPicker({ items, onPick, onClose, id }: { items: readonly Reaction[]; onPick: (emoji: string) => void; onClose: () => void; id?: string }) {
|
|
93
93
|
const [q, setQ] = useState("");
|
|
94
|
-
const [recent, setRecent] = useState<readonly string[]>(
|
|
94
|
+
const [recent, setRecent] = useState<readonly string[]>(() => frequentReactions([]));
|
|
95
95
|
const search = useRef<HTMLInputElement>(null);
|
|
96
96
|
const body = useRef<HTMLDivElement>(null);
|
|
97
97
|
// Storage is read after mount: the server render and the first client render must draw the same picker.
|
|
98
|
-
useEffect(() => { const r = readRecent(); if (r.length) setRecent(r); search.current?.focus(); }, []);
|
|
98
|
+
useEffect(() => { const r = readRecent(); if (r.length) setRecent(frequentReactions(r)); search.current?.focus(); }, []);
|
|
99
99
|
const mine = (e: string) => items.some((r) => r.emoji === e && r.mine);
|
|
100
100
|
const btn = (e: string) => (
|
|
101
101
|
<button key={e} type="button" className="ui-reaction-picker__btn" title={reactionWords(e).split(" ")[0] || e}
|
package/src/team-chat-data.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* The rules (GemCare origin/main, 20/10/2026):
|
|
8
8
|
* - A message is GROUPED under the one before it (no avatar, no name line) when the same person wrote both, on the same
|
|
9
9
|
* day, less than 5 minutes apart, and it is not a reply (a reply starts a turn of its own: it answers someone else).
|
|
10
|
-
* - A day separator comes before the first message and before the first message of each new day
|
|
11
|
-
*
|
|
10
|
+
* - A day separator comes before the first message and before the first message of each new day: the viewer's local day,
|
|
11
|
+
* or the calendar day of `timeZone` when given. A new day also breaks a group: two lines either side of midnight are
|
|
12
|
+
* two turns.
|
|
12
13
|
* - The "New messages" line comes before the first message someone ELSE wrote that the viewer has not read. The
|
|
13
14
|
* viewer's own messages after the mark are not "new". The line also breaks a group, so the first new message
|
|
14
15
|
* always shows who wrote it.
|
|
@@ -36,23 +37,55 @@ export type ChatRowOptions<M> = {
|
|
|
36
37
|
isUnread?: (message: M) => boolean;
|
|
37
38
|
/** Messages closer than this are one turn. Default 5 minutes, as GemCare. */
|
|
38
39
|
groupWithinMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* An IANA zone ("Asia/Ho_Chi_Minh"): days are that zone's calendar days instead of the process's local ones. Give it on
|
|
42
|
+
* a screen the server renders first: the server's zone (UTC on Railway) is not the member's, so a message at 00:30 in
|
|
43
|
+
* Hanoi fell on "yesterday" on the server and "today" in the browser, and the two drew different day separators (React
|
|
44
|
+
* #418, GemCommunity /chat 20/10/2026). An unknown name falls back to the local day.
|
|
45
|
+
*/
|
|
46
|
+
timeZone?: string;
|
|
39
47
|
};
|
|
40
48
|
|
|
41
49
|
const toDate = (at: string | number | Date) => (at instanceof Date ? at : new Date(at));
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
const ZONE_DAY = new Map<string, Intl.DateTimeFormat | null>();
|
|
52
|
+
/** [year, month 1–12, day] of `d` in `timeZone`, or in local time when there is no zone, it is unknown, or `d` is invalid. */
|
|
53
|
+
function dayParts(at: string | number | Date, timeZone?: string): [number, number, number] {
|
|
45
54
|
const d = toDate(at);
|
|
46
|
-
|
|
55
|
+
if (timeZone) {
|
|
56
|
+
let f = ZONE_DAY.get(timeZone);
|
|
57
|
+
if (f === undefined) {
|
|
58
|
+
try { f = new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", month: "numeric", day: "numeric" }); } catch { f = null; }
|
|
59
|
+
ZONE_DAY.set(timeZone, f);
|
|
60
|
+
}
|
|
61
|
+
if (f && !Number.isNaN(d.getTime())) {
|
|
62
|
+
const p: Record<string, number> = {};
|
|
63
|
+
for (const x of f.formatToParts(d)) if (x.type !== "literal") p[x.type] = Number(x.value);
|
|
64
|
+
return [p.year, p.month, p.day];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [d.getFullYear(), d.getMonth() + 1, d.getDate()];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The calendar day (local, or of `timeZone`), as a key: two dates with the same key are the same day. */
|
|
71
|
+
export function chatDayKey(at: string | number | Date, timeZone?: string): string {
|
|
72
|
+
const [y, m, d] = dayParts(at, timeZone);
|
|
73
|
+
return `${y}-${m}-${d}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The same day as ISO 8601 ("2026-10-21"), for a day separator's `<time dateTime>`. */
|
|
77
|
+
export function chatDayISO(at: string | number | Date, timeZone?: string): string {
|
|
78
|
+
const [y, m, d] = dayParts(at, timeZone);
|
|
79
|
+
return `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
|
47
80
|
}
|
|
48
81
|
|
|
49
82
|
/** "Today", "Yesterday", or null (the caller formats older days with its own locale, e.g. "Mon, Sep 28"). */
|
|
50
|
-
export function chatDayWord(at: string | number | Date, now: Date = new Date()): "Today" | "Yesterday" | null {
|
|
51
|
-
const key = chatDayKey(at);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return key ===
|
|
83
|
+
export function chatDayWord(at: string | number | Date, now: Date = new Date(), timeZone?: string): "Today" | "Yesterday" | null {
|
|
84
|
+
const key = chatDayKey(at, timeZone);
|
|
85
|
+
const [y, m, d] = dayParts(now, timeZone);
|
|
86
|
+
if (key === `${y}-${m}-${d}`) return "Today";
|
|
87
|
+
const p = new Date(Date.UTC(y, m - 1, d - 1)); // calendar arithmetic: the day before, whatever the zone
|
|
88
|
+
return key === `${p.getUTCFullYear()}-${p.getUTCMonth() + 1}-${p.getUTCDate()}` ? "Yesterday" : null;
|
|
56
89
|
}
|
|
57
90
|
|
|
58
91
|
/** The rows to draw, in order: day separators, the "New messages" line, and each message with its `grouped` flag. */
|
|
@@ -65,8 +98,8 @@ export function teamChatRows<M extends ChatRowMessage>(messages: readonly M[], o
|
|
|
65
98
|
messages.forEach((m, i) => {
|
|
66
99
|
const prev = i > 0 ? messages[i - 1] : undefined;
|
|
67
100
|
const at = toDate(m.at);
|
|
68
|
-
const newDay = !prev || chatDayKey(prev.at) !== chatDayKey(at);
|
|
69
|
-
if (newDay) rows.push({ kind: "day", key: chatDayKey(at), date: at });
|
|
101
|
+
const newDay = !prev || chatDayKey(prev.at, opts.timeZone) !== chatDayKey(at, opts.timeZone);
|
|
102
|
+
if (newDay) rows.push({ kind: "day", key: chatDayKey(at, opts.timeZone), date: at });
|
|
70
103
|
const isFirstNew = i === firstNew;
|
|
71
104
|
if (isFirstNew) rows.push({ kind: "new" });
|
|
72
105
|
const grouped = Boolean(
|