@-reddacted-/react-ui 0.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.
- package/dist/ColorPicker.d.ts +9 -0
- package/dist/ColorPicker.d.ts.map +1 -0
- package/dist/ColorPicker.js +62 -0
- package/dist/ConfirmDeleteButton.d.ts +11 -0
- package/dist/ConfirmDeleteButton.d.ts.map +1 -0
- package/dist/ConfirmDeleteButton.js +23 -0
- package/dist/Modal.d.ts +18 -0
- package/dist/Modal.d.ts.map +1 -0
- package/dist/Modal.js +162 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/package.json +30 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "./ColorPicker.css";
|
|
2
|
+
interface ColorPickerProps {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export default function ColorPicker({ value, onChange, className }: ColorPickerProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=ColorPicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ColorPicker.d.ts","sourceRoot":"","sources":["../src/ColorPicker.tsx"],"names":[],"mappings":"AAEA,OAAO,mBAAmB,CAAA;AAE1B,UAAU,gBAAgB;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAKD,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,gBAAgB,2CA2FnF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState, useEffect, useLayoutEffect } from "react";
|
|
3
|
+
import { HexColorPicker } from "react-colorful";
|
|
4
|
+
import "./ColorPicker.css";
|
|
5
|
+
const HEX_RE = /^#[0-9a-fA-F]{6}$/;
|
|
6
|
+
const safe = (v) => (HEX_RE.test(v) ? v : "#000000");
|
|
7
|
+
export default function ColorPicker({ value, onChange, className }) {
|
|
8
|
+
const [open, setOpen] = useState(false);
|
|
9
|
+
const [inputValue, setInputValue] = useState(value);
|
|
10
|
+
const [pos, setPos] = useState({ top: 0, left: 0 });
|
|
11
|
+
const [positioned, setPositioned] = useState(false);
|
|
12
|
+
const swatchRef = useRef(null);
|
|
13
|
+
const popoverRef = useRef(null);
|
|
14
|
+
useEffect(() => { setInputValue(value); }, [value]);
|
|
15
|
+
useLayoutEffect(() => {
|
|
16
|
+
if (!open || !popoverRef.current || !swatchRef.current) {
|
|
17
|
+
setPositioned(false);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const pr = popoverRef.current.getBoundingClientRect();
|
|
21
|
+
const sr = swatchRef.current.getBoundingClientRect();
|
|
22
|
+
const vw = window.innerWidth;
|
|
23
|
+
const vh = window.innerHeight;
|
|
24
|
+
const gap = 6;
|
|
25
|
+
const top = sr.bottom + gap + pr.height > vh
|
|
26
|
+
? Math.max(4, sr.top - pr.height - gap)
|
|
27
|
+
: sr.bottom + gap;
|
|
28
|
+
const left = sr.left + pr.width > vw
|
|
29
|
+
? Math.max(4, vw - pr.width - 8)
|
|
30
|
+
: sr.left;
|
|
31
|
+
setPos({ top, left });
|
|
32
|
+
setPositioned(true);
|
|
33
|
+
}, [open]);
|
|
34
|
+
const toggle = () => {
|
|
35
|
+
if (!open)
|
|
36
|
+
setInputValue(value);
|
|
37
|
+
setOpen(o => !o);
|
|
38
|
+
};
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!open)
|
|
41
|
+
return;
|
|
42
|
+
const handleMouseDown = (e) => {
|
|
43
|
+
if (!popoverRef.current?.contains(e.target) &&
|
|
44
|
+
!swatchRef.current?.contains(e.target))
|
|
45
|
+
setOpen(false);
|
|
46
|
+
};
|
|
47
|
+
document.addEventListener("mousedown", handleMouseDown);
|
|
48
|
+
return () => document.removeEventListener("mousedown", handleMouseDown);
|
|
49
|
+
}, [open]);
|
|
50
|
+
const handleHexInput = (e) => {
|
|
51
|
+
const v = e.target.value;
|
|
52
|
+
setInputValue(v);
|
|
53
|
+
if (HEX_RE.test(v))
|
|
54
|
+
onChange(v);
|
|
55
|
+
};
|
|
56
|
+
return (_jsxs("div", { className: `color-picker${className ? ` ${className}` : ""}`, children: [_jsx("button", { ref: swatchRef, type: "button", className: "color-picker__swatch", style: { background: safe(value) }, onClick: toggle, "aria-label": `Color: ${value}` }), open && (_jsxs("div", { ref: popoverRef, className: "color-picker__popover", style: {
|
|
57
|
+
position: "fixed",
|
|
58
|
+
top: pos.top,
|
|
59
|
+
left: pos.left,
|
|
60
|
+
visibility: positioned ? "visible" : "hidden",
|
|
61
|
+
}, onMouseDown: e => e.stopPropagation(), children: [_jsx(HexColorPicker, { color: safe(value), onChange: onChange }), _jsx("input", { type: "text", className: "color-picker__hex-input", value: inputValue, onChange: handleHexInput, maxLength: 7, spellCheck: false })] }))] }));
|
|
62
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
onConfirm: () => void;
|
|
3
|
+
label?: string;
|
|
4
|
+
confirmLabel?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
/** Delay in ms before the confirm state auto-resets (default: 10000) */
|
|
7
|
+
resetDelay?: number;
|
|
8
|
+
};
|
|
9
|
+
export default function ConfirmDeleteButton({ onConfirm, label, confirmLabel, className, resetDelay, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=ConfirmDeleteButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConfirmDeleteButton.d.ts","sourceRoot":"","sources":["../src/ConfirmDeleteButton.tsx"],"names":[],"mappings":"AAEA,KAAK,KAAK,GAAG;IACX,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,EAC1C,SAAS,EACT,KAAgB,EAChB,YAA8B,EAC9B,SAAS,EACT,UAAmB,GACpB,EAAE,KAAK,2CA4BP"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
export default function ConfirmDeleteButton({ onConfirm, label = "Delete", confirmLabel = "Are you sure?", className, resetDelay = 10000, }) {
|
|
4
|
+
const [confirming, setConfirming] = useState(false);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
if (!confirming)
|
|
7
|
+
return;
|
|
8
|
+
const timer = setTimeout(() => setConfirming(false), resetDelay);
|
|
9
|
+
return () => clearTimeout(timer);
|
|
10
|
+
}, [confirming, resetDelay]);
|
|
11
|
+
const handleClick = () => {
|
|
12
|
+
if (!confirming) {
|
|
13
|
+
setConfirming(true);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
onConfirm();
|
|
17
|
+
};
|
|
18
|
+
return (_jsx("button", { type: "button", onClick: handleClick, className: className, style: {
|
|
19
|
+
color: "var(--confirm-delete-danger-color, #e55)",
|
|
20
|
+
borderColor: "var(--confirm-delete-danger-color, #e55)",
|
|
21
|
+
background: confirming ? "var(--confirm-delete-danger-bg, rgba(220,50,50,0.15))" : undefined,
|
|
22
|
+
}, children: confirming ? confirmLabel : label }));
|
|
23
|
+
}
|
package/dist/Modal.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export declare function openModal(id: string): void;
|
|
4
|
+
export declare function closeModal(id: string): void;
|
|
5
|
+
type ModalProps = {
|
|
6
|
+
id: string;
|
|
7
|
+
width?: string | number;
|
|
8
|
+
height?: string | number;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
preventClose?: boolean;
|
|
13
|
+
ariaLabelledBy?: string;
|
|
14
|
+
ariaDescribedBy?: string;
|
|
15
|
+
};
|
|
16
|
+
export default function Modal({ id, width, height, children, className, style, preventClose, ariaLabelledBy, ariaDescribedBy, }: ModalProps): React.ReactPortal | null;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=Modal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../src/Modal.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAA;AAC1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAgBtC,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,QAGnC;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,QAEpC;AAED,KAAK,UAAU,GAAG;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACxB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;IAC3B,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,EAC5B,EAAE,EACF,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,EACT,KAAK,EACL,YAAoB,EACpB,cAAc,EACd,eAAe,GAChB,EAAE,UAAU,4BA+KZ"}
|
package/dist/Modal.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
const listeners = new Map();
|
|
5
|
+
function on(event, cb) {
|
|
6
|
+
if (!listeners.has(event))
|
|
7
|
+
listeners.set(event, new Set());
|
|
8
|
+
listeners.get(event).add(cb);
|
|
9
|
+
return () => listeners.get(event).delete(cb);
|
|
10
|
+
}
|
|
11
|
+
function emit(event, payload) {
|
|
12
|
+
listeners.get(event)?.forEach(cb => cb(payload));
|
|
13
|
+
}
|
|
14
|
+
export function openModal(id) {
|
|
15
|
+
emit("modal:closeAll");
|
|
16
|
+
emit("modal:open", id);
|
|
17
|
+
}
|
|
18
|
+
export function closeModal(id) {
|
|
19
|
+
emit("modal:close", id);
|
|
20
|
+
}
|
|
21
|
+
export default function Modal({ id, width, height, children, className, style, preventClose = false, ariaLabelledBy, ariaDescribedBy, }) {
|
|
22
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
23
|
+
const contentRef = useRef(null);
|
|
24
|
+
const lastFocusedEl = useRef(null);
|
|
25
|
+
const portalTarget = useRef(null);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const offOpen = on("modal:open", (targetId) => {
|
|
28
|
+
if (targetId === id) {
|
|
29
|
+
lastFocusedEl.current = document.activeElement ?? null;
|
|
30
|
+
setIsOpen(true);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const offClose = on("modal:close", (targetId) => {
|
|
34
|
+
if (targetId === id)
|
|
35
|
+
setIsOpen(false);
|
|
36
|
+
});
|
|
37
|
+
const offCloseAll = on("modal:closeAll", () => setIsOpen(false));
|
|
38
|
+
return () => { offOpen(); offClose(); offCloseAll(); };
|
|
39
|
+
}, [id]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
let el = document.getElementById("__modal-root__");
|
|
42
|
+
if (!el) {
|
|
43
|
+
el = document.createElement("div");
|
|
44
|
+
el.id = "__modal-root__";
|
|
45
|
+
document.body.appendChild(el);
|
|
46
|
+
}
|
|
47
|
+
portalTarget.current = el;
|
|
48
|
+
}, []);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!isOpen)
|
|
51
|
+
return;
|
|
52
|
+
const prevOverflow = document.body.style.overflow;
|
|
53
|
+
const prevPadRight = document.body.style.paddingRight;
|
|
54
|
+
const scrollbarW = window.innerWidth - document.documentElement.clientWidth;
|
|
55
|
+
if (scrollbarW > 0)
|
|
56
|
+
document.body.style.paddingRight = `${scrollbarW}px`;
|
|
57
|
+
document.body.style.overflow = "hidden";
|
|
58
|
+
const to = setTimeout(() => {
|
|
59
|
+
const container = contentRef.current;
|
|
60
|
+
if (!container)
|
|
61
|
+
return;
|
|
62
|
+
const focusables = container.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])');
|
|
63
|
+
(focusables[0] ?? container).focus();
|
|
64
|
+
}, 0);
|
|
65
|
+
const onKeyDown = (e) => {
|
|
66
|
+
if (e.key === "Escape" && !preventClose) {
|
|
67
|
+
closeModal(id);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (e.key !== "Tab")
|
|
71
|
+
return;
|
|
72
|
+
const container = contentRef.current;
|
|
73
|
+
if (!container)
|
|
74
|
+
return;
|
|
75
|
+
const focusables = Array.from(container.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])')).filter(el => !el.hasAttribute("disabled") && !el.getAttribute("aria-hidden"));
|
|
76
|
+
if (focusables.length === 0)
|
|
77
|
+
return;
|
|
78
|
+
const first = focusables[0];
|
|
79
|
+
const last = focusables[focusables.length - 1];
|
|
80
|
+
const active = document.activeElement;
|
|
81
|
+
if (e.shiftKey) {
|
|
82
|
+
if (active === first || !container.contains(active)) {
|
|
83
|
+
last.focus();
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
if (active === last || !container.contains(active)) {
|
|
89
|
+
first.focus();
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const onFocusIn = (e) => {
|
|
95
|
+
const container = contentRef.current;
|
|
96
|
+
if (!container)
|
|
97
|
+
return;
|
|
98
|
+
if (!container.contains(e.target))
|
|
99
|
+
container.focus();
|
|
100
|
+
};
|
|
101
|
+
document.addEventListener("keydown", onKeyDown);
|
|
102
|
+
document.addEventListener("focusin", onFocusIn);
|
|
103
|
+
return () => {
|
|
104
|
+
document.body.style.overflow = prevOverflow;
|
|
105
|
+
document.body.style.paddingRight = prevPadRight;
|
|
106
|
+
clearTimeout(to);
|
|
107
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
108
|
+
document.removeEventListener("focusin", onFocusIn);
|
|
109
|
+
lastFocusedEl.current?.focus?.();
|
|
110
|
+
lastFocusedEl.current = null;
|
|
111
|
+
};
|
|
112
|
+
}, [isOpen, id, preventClose]);
|
|
113
|
+
if (!isOpen || !portalTarget.current)
|
|
114
|
+
return null;
|
|
115
|
+
const w = typeof width === "number" ? `${width}px` : width ?? undefined;
|
|
116
|
+
const h = typeof height === "number" ? `${height}px` : height ?? undefined;
|
|
117
|
+
const prefersReducedMotion = typeof window !== "undefined" &&
|
|
118
|
+
window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
|
|
119
|
+
const contentStyles = {
|
|
120
|
+
position: "fixed",
|
|
121
|
+
inset: 0,
|
|
122
|
+
display: "grid",
|
|
123
|
+
placeItems: "center",
|
|
124
|
+
zIndex: 9999,
|
|
125
|
+
backdropFilter: "blur(2px)",
|
|
126
|
+
background: "rgba(0,0,0,0.35)",
|
|
127
|
+
animation: prefersReducedMotion ? "none" : "modalFade 150ms ease-out",
|
|
128
|
+
};
|
|
129
|
+
const cardStyles = {
|
|
130
|
+
maxWidth: "95vw",
|
|
131
|
+
maxHeight: "95vh",
|
|
132
|
+
width: w,
|
|
133
|
+
height: h,
|
|
134
|
+
overflow: "auto",
|
|
135
|
+
outline: "none",
|
|
136
|
+
background: "var(--modal-background, #1a1a1a)",
|
|
137
|
+
borderRadius: "10px",
|
|
138
|
+
};
|
|
139
|
+
const overlayStyles = {
|
|
140
|
+
position: "fixed",
|
|
141
|
+
inset: 0,
|
|
142
|
+
backdropFilter: "blur(2px)",
|
|
143
|
+
background: "rgba(0,0,0,0.35)",
|
|
144
|
+
zIndex: 9998,
|
|
145
|
+
animation: prefersReducedMotion ? "none" : "modalPop 160ms ease-out",
|
|
146
|
+
};
|
|
147
|
+
const innerWrapper = {
|
|
148
|
+
animation: prefersReducedMotion ? "none" : "modalPop 160ms ease-out",
|
|
149
|
+
};
|
|
150
|
+
const keyframes = (_jsx("style", { children: `
|
|
151
|
+
@keyframes modalFade {
|
|
152
|
+
from { opacity: 0; } to { opacity: 1; }
|
|
153
|
+
}
|
|
154
|
+
@keyframes modalPop {
|
|
155
|
+
from { opacity: 0; transform: translateY(4px) scale(0.98); }
|
|
156
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
157
|
+
}
|
|
158
|
+
` }));
|
|
159
|
+
return createPortal(_jsxs(_Fragment, { children: [keyframes, _jsx("div", { "aria-hidden": "true", style: overlayStyles, onClick: () => { if (!preventClose)
|
|
160
|
+
closeModal(id); } }), _jsx("div", { style: contentStyles, "aria-hidden": !isOpen, onMouseDown: e => { if (!preventClose && e.target === e.currentTarget)
|
|
161
|
+
closeModal(id); }, children: _jsx("div", { style: innerWrapper, children: _jsx("div", { ref: contentRef, role: "dialog", "aria-modal": "true", "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, tabIndex: -1, className: className ?? "", style: { ...cardStyles, ...style }, onMouseDown: e => e.stopPropagation(), children: children }) }) })] }), portalTarget.current);
|
|
162
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACjE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,uBAAuB,CAAA"}
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@-reddacted-/react-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Modal, ColorPicker and ConfirmDeleteButton React components",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist"],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"react": ">=18",
|
|
20
|
+
"react-dom": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"react-colorful": "^5.6.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/react": "^19.0.0",
|
|
27
|
+
"@types/react-dom": "^19.0.0",
|
|
28
|
+
"typescript": "~5.8.3"
|
|
29
|
+
}
|
|
30
|
+
}
|