smartrte-react 1.0.0-beta.8 → 1.1.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,30 @@
1
+ import { type BorderStyle } from "./TableBorderPopover.js";
2
+ export interface BlockquoteBorderDraft {
3
+ style: BorderStyle;
4
+ widthPx: number;
5
+ hex: string;
6
+ }
7
+ export interface BlockquoteBorderPopoverProps {
8
+ x: number;
9
+ y: number;
10
+ initial: BlockquoteBorderDraft;
11
+ /** Up to 4 previously-committed border colours, most-recent first - same pattern as the other colour buckets. */
12
+ recentColors?: readonly string[];
13
+ /** Fires on every change (style/width picked, or a colour drag frame) - the caller live-previews it on the real blockquote. */
14
+ onPreview: (draft: BlockquoteBorderDraft) => void;
15
+ /** Fires once, only from the explicit Apply button - matches TableBorderPopover's own reasoning (a composite dialog commits on a deliberate action, not on any dismissal). */
16
+ onApply: (draft: BlockquoteBorderDraft) => void;
17
+ /** Fires from Cancel, Escape, or an outside click - reverts to `initial`. */
18
+ onCancel: () => void;
19
+ }
20
+ /**
21
+ * "Blockquote border options" - style/width/colour for the one border a
22
+ * blockquote ever shows (its left border). Deliberately not
23
+ * TableBorderPopover with a "sides" toggle locked to left-only: there is
24
+ * only ever one side here, so the whole sides-diagram/"All sides"/"No
25
+ * border" UI that control exists for has nothing to select between -
26
+ * dropping it entirely rather than threading a single-side mode through
27
+ * the table-specific component keeps each popover's UI matching what it
28
+ * actually controls.
29
+ */
30
+ export declare function BlockquoteBorderPopover({ x, y, initial, recentColors, onPreview, onApply, onCancel }: BlockquoteBorderPopoverProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,115 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import { ColorPickerPopover } from "./ColorPickerPopover.js";
4
+ import { getFixedPositioningOrigin, getPositioningBounds } from "./fixedPositioning.js";
5
+ import { BORDER_WIDTH_PRESETS } from "./TableBorderPopover.js";
6
+ const STYLE_OPTIONS = ["solid", "dashed", "dotted"];
7
+ const buttonStyle = {
8
+ minHeight: 32, padding: "0 10px", border: "1px solid var(--srte-input-border)", borderRadius: 8,
9
+ background: "var(--srte-input-bg)", color: "var(--srte-menu-text)", cursor: "pointer", fontWeight: 500, fontSize: 13,
10
+ };
11
+ const selectStyle = {
12
+ width: "100%", height: 32, boxSizing: "border-box", padding: "0 8px", borderRadius: 8,
13
+ border: "1px solid var(--srte-input-border)", background: "var(--srte-input-bg)", color: "var(--srte-input-text)",
14
+ font: "inherit", fontSize: 13, cursor: "pointer",
15
+ };
16
+ const fieldLabelStyle = { fontSize: 11, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.04em", opacity: 0.7, marginBottom: 4 };
17
+ /**
18
+ * "Blockquote border options" - style/width/colour for the one border a
19
+ * blockquote ever shows (its left border). Deliberately not
20
+ * TableBorderPopover with a "sides" toggle locked to left-only: there is
21
+ * only ever one side here, so the whole sides-diagram/"All sides"/"No
22
+ * border" UI that control exists for has nothing to select between -
23
+ * dropping it entirely rather than threading a single-side mode through
24
+ * the table-specific component keeps each popover's UI matching what it
25
+ * actually controls.
26
+ */
27
+ export function BlockquoteBorderPopover({ x, y, initial, recentColors, onPreview, onApply, onCancel }) {
28
+ const [draft, setDraft] = useState(initial);
29
+ const [colorPickerAnchor, setColorPickerAnchor] = useState(null);
30
+ const hexBeforeNestedPickerRef = useRef(initial.hex);
31
+ const rootRef = useRef(null);
32
+ const swatchRef = useRef(null);
33
+ const [placement, setPlacement] = useState(null);
34
+ const onCancelRef = useRef(onCancel);
35
+ onCancelRef.current = onCancel;
36
+ const onPreviewRef = useRef(onPreview);
37
+ onPreviewRef.current = onPreview;
38
+ useLayoutEffect(() => {
39
+ const el = rootRef.current;
40
+ if (!el)
41
+ return;
42
+ const margin = 8;
43
+ const { width, height } = el.getBoundingClientRect();
44
+ const bounds = getPositioningBounds(el);
45
+ const overflowsRight = x + width > bounds.right - margin;
46
+ const left = overflowsRight ? x - width : x;
47
+ const clampedLeft = Math.min(Math.max(bounds.left + margin, left), Math.max(bounds.left + margin, bounds.right - width - margin));
48
+ const overflowsBottom = y + height > bounds.bottom - margin;
49
+ const top = overflowsBottom ? y - height : y;
50
+ const clampedTop = Math.min(Math.max(bounds.top + margin, top), Math.max(bounds.top + margin, bounds.bottom - margin));
51
+ const origin = getFixedPositioningOrigin(el);
52
+ setPlacement({ left: clampedLeft - origin.left, top: clampedTop - origin.top });
53
+ }, [x, y]);
54
+ useEffect(() => {
55
+ const dismissIfOutside = (event) => {
56
+ const target = event.target;
57
+ if (rootRef.current?.contains(target))
58
+ return;
59
+ // The nested colour picker is a DOM sibling (both are position:fixed
60
+ // overlays), not a descendant of rootRef - without this, clicking
61
+ // inside it would look like an "outside" click on this popover.
62
+ if (colorPickerAnchor && document.querySelector('[data-srte-color-popover="true"]')?.contains(target))
63
+ return;
64
+ onCancelRef.current();
65
+ };
66
+ window.addEventListener("pointerdown", dismissIfOutside, true);
67
+ window.addEventListener("mousedown", dismissIfOutside, true);
68
+ return () => {
69
+ window.removeEventListener("pointerdown", dismissIfOutside, true);
70
+ window.removeEventListener("mousedown", dismissIfOutside, true);
71
+ };
72
+ }, [colorPickerAnchor]);
73
+ useEffect(() => {
74
+ const onWindowKeyDown = (event) => {
75
+ if (event.key !== "Escape")
76
+ return;
77
+ // While the nested colour picker is open, let its own Escape handler
78
+ // commit/dismiss just that picker first - a second Escape closes this
79
+ // whole dialog.
80
+ if (colorPickerAnchor)
81
+ return;
82
+ onCancelRef.current();
83
+ };
84
+ window.addEventListener("keydown", onWindowKeyDown);
85
+ return () => window.removeEventListener("keydown", onWindowKeyDown);
86
+ }, [colorPickerAnchor]);
87
+ const update = (patch) => {
88
+ const next = { ...draft, ...patch };
89
+ setDraft(next);
90
+ onPreviewRef.current(next);
91
+ };
92
+ return (_jsxs("div", { ref: rootRef, "data-srte-blockquote-border-popover": "true", role: "dialog", "aria-label": "Blockquote border options", style: {
93
+ position: "fixed",
94
+ left: placement?.left ?? x,
95
+ top: placement?.top ?? y,
96
+ visibility: placement ? "visible" : "hidden",
97
+ zIndex: 70,
98
+ width: 240,
99
+ boxSizing: "border-box",
100
+ background: "var(--srte-menu-bg)",
101
+ color: "var(--srte-menu-text)",
102
+ border: "1px solid var(--srte-border)",
103
+ borderRadius: 12,
104
+ boxShadow: "var(--srte-menu-shadow)",
105
+ padding: 14,
106
+ display: "flex",
107
+ flexDirection: "column",
108
+ gap: 12,
109
+ }, children: [_jsx("div", { style: { fontWeight: 650 }, children: "Blockquote border options" }), _jsxs("div", { children: [_jsx("div", { style: fieldLabelStyle, children: "Style" }), _jsx("select", { "aria-label": "Border style", value: draft.style, onChange: (event) => update({ style: event.target.value }), style: selectStyle, children: STYLE_OPTIONS.map((style) => _jsxs("option", { value: style, children: [style[0].toUpperCase(), style.slice(1)] }, style)) })] }), _jsxs("div", { children: [_jsx("div", { style: fieldLabelStyle, children: "Width" }), _jsx("select", { "aria-label": "Border width", value: BORDER_WIDTH_PRESETS.find((preset) => preset.px === draft.widthPx)?.id ?? BORDER_WIDTH_PRESETS[0].id, onChange: (event) => { const preset = BORDER_WIDTH_PRESETS.find((entry) => entry.id === event.target.value); if (preset)
110
+ update({ widthPx: preset.px }); }, style: selectStyle, children: BORDER_WIDTH_PRESETS.map((preset) => _jsx("option", { value: preset.id, children: preset.label }, preset.id)) })] }), _jsxs("div", { children: [_jsx("div", { style: fieldLabelStyle, children: "Colour" }), _jsxs("button", { ref: swatchRef, type: "button", "aria-label": "Border colour", onMouseDown: (event) => event.preventDefault(), onClick: () => {
111
+ hexBeforeNestedPickerRef.current = draft.hex;
112
+ const rect = swatchRef.current?.getBoundingClientRect();
113
+ setColorPickerAnchor({ x: rect?.left ?? x, y: (rect?.bottom ?? y) + 4 });
114
+ }, style: { ...buttonStyle, width: "100%", display: "flex", alignItems: "center", gap: 8, justifyContent: "flex-start" }, children: [_jsx("span", { style: { width: 18, height: 18, borderRadius: 4, border: "1px solid var(--srte-input-border)", background: draft.hex }, "aria-hidden": "true" }), draft.hex] })] }), _jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8 }, children: [_jsx("button", { type: "button", style: buttonStyle, onClick: onCancel, children: "Cancel" }), _jsx("button", { type: "button", onClick: () => onApply(draft), style: { ...buttonStyle, borderColor: "var(--srte-primary)", background: "var(--srte-primary)", color: "var(--srte-on-primary)" }, children: "Apply" })] }), colorPickerAnchor && _jsx(ColorPickerPopover, { x: colorPickerAnchor.x, y: colorPickerAnchor.y, label: "Border colour", initialValue: draft.hex, recentColors: recentColors, onPreview: (hex) => update({ hex }), onApply: (hex) => { update({ hex }); setColorPickerAnchor(null); }, onCancel: () => { update({ hex: hexBeforeNestedPickerRef.current }); setColorPickerAnchor(null); } })] }));
115
+ }