tempest-react-sdk 0.63.0 → 0.64.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/README.md +1 -1
- package/dist/components/Chat/Chat.cjs +1 -1
- package/dist/components/Chat/Chat.cjs.map +1 -1
- package/dist/components/Chat/Chat.js +71 -96
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/Chat/Chat.module.cjs +1 -1
- package/dist/components/Chat/Chat.module.cjs.map +1 -1
- package/dist/components/Chat/Chat.module.js +30 -8
- package/dist/components/Chat/Chat.module.js.map +1 -1
- package/dist/components/Chat/ChatActionsMenu.cjs +2 -0
- package/dist/components/Chat/ChatActionsMenu.cjs.map +1 -0
- package/dist/components/Chat/ChatActionsMenu.js +14 -0
- package/dist/components/Chat/ChatActionsMenu.js.map +1 -0
- package/dist/components/Chat/ChatAttachments.cjs +2 -0
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -0
- package/dist/components/Chat/ChatAttachments.js +80 -0
- package/dist/components/Chat/ChatAttachments.js.map +1 -0
- package/dist/components/Chat/ChatBubble.cjs +2 -0
- package/dist/components/Chat/ChatBubble.cjs.map +1 -0
- package/dist/components/Chat/ChatBubble.js +155 -0
- package/dist/components/Chat/ChatBubble.js.map +1 -0
- package/dist/components/Chat/chat-groups.cjs +1 -1
- package/dist/components/Chat/chat-groups.cjs.map +1 -1
- package/dist/components/Chat/chat-groups.js +42 -2
- package/dist/components/Chat/chat-groups.js.map +1 -1
- package/dist/components/Chat/chat-receipt.cjs +2 -0
- package/dist/components/Chat/chat-receipt.cjs.map +1 -0
- package/dist/components/Chat/chat-receipt.js +14 -0
- package/dist/components/Chat/chat-receipt.js.map +1 -0
- package/dist/components/ContextMenu/ContextMenu.cjs +1 -1
- package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.js +100 -48
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.module.cjs.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.module.js.map +1 -1
- package/dist/styles/advanced.css +2 -0
- package/dist/styles/chat.css +28 -0
- package/dist/styles/component/Chat.css +28 -0
- package/dist/styles/component/ContextMenu.css +2 -0
- package/dist/styles/manifest.json +12 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +253 -10
- package/dist/tempest-react-sdk.js +169 -167
- package/dist/theme/create-theme.cjs +3 -3
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js +109 -57
- package/dist/theme/create-theme.js.map +1 -1
- package/loader/css-loader-hooks.mjs +37 -0
- package/loader/css-loader.mjs +46 -0
- package/package.json +3 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextMenu.cjs","names":[],"sources":["../../../src/components/ContextMenu/ContextMenu.tsx"],"sourcesContent":["/**\n * @tempest-limits function-lines — the body positions the menu against the pointer,\n * flips it inside the viewport, and owns roving focus for the keyboard path — one\n * geometry computed from one anchor point.\n */\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Portal } from \"@/components/Portal\";\nimport styles from \"./ContextMenu.module.css\";\n\nexport type ContextMenuItem =\n | {\n label: ReactNode;\n onSelect?: () => void;\n disabled?: boolean;\n danger?: boolean;\n }\n | { separator: true };\n\nexport interface ContextMenuProps {\n /** Menu entries — selectable items and separators. */\n items: ContextMenuItem[];\n /** Trigger area. Right-clicking anywhere within opens the menu at the cursor. */\n children: ReactNode;\n /** Extra class names forwarded to the menu element. */\n className?: string;\n}\n\ninterface Position {\n x: number;\n y: number;\n}\n\nfunction isSeparator(item: ContextMenuItem): item is { separator: true } {\n return \"separator\" in item && item.separator === true;\n}\n\n/**\n * Right-click context menu.\n *\n * - Opens at the cursor position on `onContextMenu` (default browser menu suppressed).\n * - Rendered through a {@link Portal} so it escapes parent overflow/stacking contexts.\n * - Closes on outside click, Escape, or item selection.\n * - Arrow Up/Down move focus across selectable items; Enter activates the focused item.\n *\n * @param props - The context menu props.\n * @returns The trigger wrapper plus the portalled menu when open.\n */\nexport function ContextMenu({ items, children, className }: ContextMenuProps) {\n const [open, setOpen] = useState(false);\n const [position, setPosition] = useState<Position>({ x: 0, y: 0 });\n const [activeIndex, setActiveIndex] = useState<number>(-1);\n const id = useId();\n const menuRef = useRef<HTMLUListElement>(null);\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n const selectableIndexes = items\n .map((item, index) => (!isSeparator(item) && !item.disabled ? index : -1))\n .filter((i) => i !== -1);\n\n const close = useCallback((): void => {\n setOpen(false);\n setActiveIndex(-1);\n }, []);\n\n const handleContextMenu = (event: React.MouseEvent): void => {\n event.preventDefault();\n setPosition({ x: event.clientX, y: event.clientY });\n setActiveIndex(-1);\n setOpen(true);\n };\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n close();\n return;\n }\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n const current = selectableIndexes.indexOf(activeIndex);\n const next = selectableIndexes[(current + 1) % selectableIndexes.length] ?? -1;\n setActiveIndex(next);\n itemRefs.current[next]?.focus();\n }\n if (event.key === \"ArrowUp\") {\n event.preventDefault();\n const current = selectableIndexes.indexOf(activeIndex);\n const prev =\n selectableIndexes[\n (current - 1 + selectableIndexes.length) % selectableIndexes.length\n ] ?? -1;\n setActiveIndex(prev);\n itemRefs.current[prev]?.focus();\n }\n };\n const onDown = (event: MouseEvent): void => {\n if (menuRef.current && !menuRef.current.contains(event.target as Node)) close();\n };\n window.addEventListener(\"keydown\", onKey);\n window.addEventListener(\"mousedown\", onDown);\n return () => {\n window.removeEventListener(\"keydown\", onKey);\n window.removeEventListener(\"mousedown\", onDown);\n };\n }, [open, activeIndex, selectableIndexes, close]);\n\n const handleSelect = (item: Extract<ContextMenuItem, { label: ReactNode }>): void => {\n item.onSelect?.();\n close();\n };\n\n return (\n <>\n <span className={styles.root} onContextMenu={handleContextMenu}>\n {children}\n </span>\n {open && (\n <Portal>\n <ul\n ref={menuRef}\n id={id}\n role=\"menu\"\n className={cn(styles.menu, className)}\n style={{ top: position.y, left: position.x }}\n >\n {items.map((item, index) => {\n if (isSeparator(item)) {\n return (\n <li\n key={`separator-${index}`}\n role=\"separator\"\n className={styles.separator}\n aria-hidden\n />\n );\n }\n return (\n <li key={`item-${index}`} role=\"none\">\n <button\n ref={(el) => {\n itemRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"menuitem\"\n className={cn(\n styles.item,\n item.danger && styles.danger,\n activeIndex === index && styles.active,\n )}\n disabled={item.disabled}\n onClick={() => handleSelect(item)}\n onMouseEnter={() => setActiveIndex(index)}\n >\n {item.label}\n </button>\n </li>\n );\n })}\n </ul>\n </Portal>\n )}\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ContextMenu.cjs","names":[],"sources":["../../../src/components/ContextMenu/ContextMenu.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines function-lines — the body owns one anchor point and\n * everything that reads from it: the three ways a menu opens (right click, long\n * press, click), the clamp that keeps it inside the viewport, and roving focus\n * for the keyboard path. Splitting them would hand the same geometry to three\n * files.\n */\nimport { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport type { PointerEvent as ReactPointerEvent, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Portal } from \"@/components/Portal\";\nimport styles from \"./ContextMenu.module.css\";\n\nexport type ContextMenuItem =\n | {\n label: ReactNode;\n onSelect?: () => void;\n disabled?: boolean;\n danger?: boolean;\n }\n | { separator: true };\n\n/** How the menu is opened by a pointer. Touch always gets the long press. */\nexport type ContextMenuTrigger = \"contextmenu\" | \"click\" | \"both\";\n\nexport interface ContextMenuProps {\n /** Menu entries — selectable items and separators. */\n items: ContextMenuItem[];\n /** Trigger area. Right-clicking anywhere within opens the menu at the cursor. */\n children: ReactNode;\n /** Extra class names forwarded to the menu element. */\n className?: string;\n /**\n * Which pointer gesture opens the menu. Default `\"contextmenu\"`.\n *\n * `\"click\"` is the `⋮` button case, where a right click is the wrong gesture\n * on the desktop and impossible on touch. `\"both\"` keeps the right click and\n * adds the left one.\n */\n trigger?: ContextMenuTrigger;\n /**\n * Hold, in ms, that opens the menu from a coarse pointer. Default `500`.\n *\n * Set `0` to turn the long press off. It is on by default because touch has\n * no right click: measured in Chrome with touch emulation (Pixel 7 and\n * iPhone 13 profiles), a 900 ms hold fires `pointerdown`, `touchstart`,\n * `pointerup`, `touchend` and `click` — and no `contextmenu` at all. Every\n * action behind this menu was unreachable on a phone.\n */\n longPressDelay?: number;\n /**\n * Gap, in px, kept between the menu and the edge of the viewport. Default `8`.\n */\n viewportMargin?: number;\n}\n\ninterface Position {\n x: number;\n y: number;\n}\n\n/** Movement, in px, that reads as a scroll rather than a hold. */\nconst LONG_PRESS_MOVE_TOLERANCE = 10;\n\nfunction isSeparator(item: ContextMenuItem): item is { separator: true } {\n return \"separator\" in item && item.separator === true;\n}\n\n/**\n * Context menu — right click, long press, or click.\n *\n * - Opens at the pointer on `contextmenu`, and on a long press from touch or pen,\n * which is the gesture those pointers have instead of a right click. `trigger`\n * adds or replaces the mouse gesture; the long press is independent of it.\n * - Clamped inside the viewport after it mounts, so a menu opened near an edge\n * moves in rather than overflowing — measured before the clamp: a 180 px menu\n * opened at `x=235` in a 320 px window ran 95 px off screen, cutting the right\n * edge off every label.\n * - Rendered through a {@link Portal} so it escapes parent overflow/stacking\n * contexts.\n * - Closes on outside click, Escape, scroll, resize, or item selection.\n * - Arrow Up/Down move focus across selectable items and wrap; Home/End jump to\n * the ends; Enter activates the focused item. The menu itself takes focus when\n * it opens, so a screen reader announces it instead of staying on the trigger.\n *\n * @param props - The context menu props.\n * @returns The trigger wrapper plus the portalled menu when open.\n */\nexport function ContextMenu({\n items,\n children,\n className,\n trigger = \"contextmenu\",\n longPressDelay = 500,\n viewportMargin = 8,\n}: ContextMenuProps) {\n const [open, setOpen] = useState(false);\n const [position, setPosition] = useState<Position>({ x: 0, y: 0 });\n const [activeIndex, setActiveIndex] = useState<number>(-1);\n const id = useId();\n const [menuNode, setMenuNode] = useState<HTMLUListElement | null>(null);\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);\n const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const longPressOrigin = useRef<Position | null>(null);\n\n const selectableIndexes = items\n .map((item, index) => (!isSeparator(item) && !item.disabled ? index : -1))\n .filter((i) => i !== -1);\n\n const close = useCallback((): void => {\n setOpen(false);\n setActiveIndex(-1);\n }, []);\n\n const openAt = useCallback((point: Position): void => {\n setPosition(point);\n setActiveIndex(-1);\n setOpen(true);\n }, []);\n\n const cancelLongPress = useCallback((): void => {\n if (longPressTimer.current !== null) {\n clearTimeout(longPressTimer.current);\n longPressTimer.current = null;\n }\n longPressOrigin.current = null;\n }, []);\n\n useEffect(() => cancelLongPress, [cancelLongPress]);\n\n const handleContextMenu = (event: React.MouseEvent): void => {\n if (trigger === \"click\") return;\n event.preventDefault();\n openAt({ x: event.clientX, y: event.clientY });\n };\n\n const handleClick = (event: React.MouseEvent): void => {\n if (trigger === \"contextmenu\") return;\n openAt({ x: event.clientX, y: event.clientY });\n };\n\n /**\n * Start the hold that stands in for a right click on a coarse pointer.\n *\n * A mouse is excluded: it has `contextmenu`, and holding the left button is\n * how a drag starts.\n */\n const handlePointerDown = (event: ReactPointerEvent): void => {\n if (longPressDelay <= 0 || event.pointerType === \"mouse\") return;\n const point = { x: event.clientX, y: event.clientY };\n longPressOrigin.current = point;\n cancelLongPress();\n longPressOrigin.current = point;\n longPressTimer.current = setTimeout(() => {\n longPressTimer.current = null;\n longPressOrigin.current = null;\n suppressNextClick();\n openAt(point);\n }, longPressDelay);\n };\n\n /**\n * Cancel the hold once the finger travels — otherwise every scroll that\n * starts on the trigger opens a menu, which is the difference between a\n * usable long press and an infuriating one.\n */\n const handlePointerMove = (event: ReactPointerEvent): void => {\n const origin = longPressOrigin.current;\n if (!origin || longPressTimer.current === null) return;\n if (\n Math.hypot(event.clientX - origin.x, event.clientY - origin.y) >\n LONG_PRESS_MOVE_TOLERANCE\n ) {\n cancelLongPress();\n }\n };\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n close();\n return;\n }\n const move = (next: number): void => {\n event.preventDefault();\n setActiveIndex(next);\n itemRefs.current[next]?.focus();\n };\n if (selectableIndexes.length === 0) return;\n const current = selectableIndexes.indexOf(activeIndex);\n if (event.key === \"ArrowDown\") {\n move(selectableIndexes[(current + 1) % selectableIndexes.length] ?? -1);\n }\n if (event.key === \"ArrowUp\") {\n move(\n selectableIndexes[\n (current - 1 + selectableIndexes.length) % selectableIndexes.length\n ] ?? -1,\n );\n }\n if (event.key === \"Home\") {\n move(selectableIndexes[0] ?? -1);\n }\n if (event.key === \"End\") {\n move(selectableIndexes[selectableIndexes.length - 1] ?? -1);\n }\n };\n const onDown = (event: MouseEvent): void => {\n if (menuNode && !menuNode.contains(event.target as Node)) close();\n };\n window.addEventListener(\"keydown\", onKey);\n window.addEventListener(\"mousedown\", onDown);\n window.addEventListener(\"resize\", close);\n window.addEventListener(\"scroll\", close, true);\n return () => {\n window.removeEventListener(\"keydown\", onKey);\n window.removeEventListener(\"mousedown\", onDown);\n window.removeEventListener(\"resize\", close);\n window.removeEventListener(\"scroll\", close, true);\n };\n }, [open, activeIndex, selectableIndexes, close, menuNode]);\n\n /**\n * Pull the menu inside the viewport, and take focus.\n *\n * Keyed off the node rather than off `open`, because {@link Portal} renders\n * `null` on its first pass and mounts on an effect — an effect that reads a\n * ref when `open` flips runs while the menu does not exist yet, measures\n * nothing and focuses nothing. A callback ref in state is what makes this\n * run on the render that has the element.\n *\n * `useLayoutEffect` runs before paint, so the menu is never painted at the\n * overflowing position first.\n *\n * The size comes from `offsetWidth`/`offsetHeight` rather than from\n * `getBoundingClientRect`, which reports the **transformed** box: the menu\n * enters at `scale(0.96)`, so the rect is 4% small and the clamp lets the\n * grown menu back over the edge. Measured in Chrome at 320 px wide, a 180 px\n * menu came back at `right: 316` against a margin that asked for 312.\n */\n useLayoutEffect(() => {\n if (!open || !menuNode) return;\n menuNode.focus({ preventScroll: true });\n const { offsetWidth, offsetHeight } = menuNode;\n if (offsetWidth === 0 && offsetHeight === 0) return;\n const maxLeft = window.innerWidth - offsetWidth - viewportMargin;\n const maxTop = window.innerHeight - offsetHeight - viewportMargin;\n const left = Math.max(viewportMargin, Math.min(position.x, maxLeft));\n const top = Math.max(viewportMargin, Math.min(position.y, maxTop));\n if (left !== position.x || top !== position.y) setPosition({ x: left, y: top });\n }, [open, menuNode, position.x, position.y, viewportMargin]);\n\n const handleSelect = (item: Extract<ContextMenuItem, { label: ReactNode }>): void => {\n item.onSelect?.();\n close();\n };\n\n return (\n <>\n <span\n className={styles.root}\n onContextMenu={handleContextMenu}\n onClick={handleClick}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={cancelLongPress}\n onPointerCancel={cancelLongPress}\n >\n {children}\n </span>\n {open && (\n <Portal>\n <ul\n ref={setMenuNode}\n id={id}\n role=\"menu\"\n aria-orientation=\"vertical\"\n tabIndex={-1}\n className={cn(styles.menu, className)}\n style={{ top: position.y, left: position.x }}\n >\n {items.map((item, index) => {\n if (isSeparator(item)) {\n return (\n <li\n key={`separator-${index}`}\n role=\"separator\"\n className={styles.separator}\n aria-hidden\n />\n );\n }\n return (\n <li key={`item-${index}`} role=\"none\">\n <button\n ref={(el) => {\n itemRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"menuitem\"\n className={cn(\n styles.item,\n item.danger && styles.danger,\n activeIndex === index && styles.active,\n )}\n disabled={item.disabled}\n onClick={() => handleSelect(item)}\n onMouseEnter={() => setActiveIndex(index)}\n >\n {item.label}\n </button>\n </li>\n );\n })}\n </ul>\n </Portal>\n )}\n </>\n );\n}\n\n/**\n * Swallow the click a finger leaves behind after a long press.\n *\n * The hold fires while the finger is still down, so the browser goes on to\n * deliver `pointerup` and then `click` to whatever is under it — measured, that\n * is the same element the menu was just opened from. Without this, opening the\n * menu on a chat bubble also opens the bubble.\n *\n * Capture phase and `once`, so the listener never outlives the gesture that\n * installed it, and a later genuine click is untouched.\n */\nfunction suppressNextClick(): void {\n if (typeof window === \"undefined\") return;\n window.addEventListener(\n \"click\",\n (event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n },\n { capture: true, once: true },\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,91 +1,143 @@
|
|
|
1
1
|
import { cn as e } from "../../utils/cn.js";
|
|
2
2
|
import { Portal as t } from "../Portal/Portal.js";
|
|
3
3
|
import n from "./ContextMenu.module.js";
|
|
4
|
-
import { useCallback as r, useEffect as i, useId as a,
|
|
5
|
-
import { Fragment as
|
|
4
|
+
import { useCallback as r, useEffect as i, useId as a, useLayoutEffect as o, useRef as s, useState as c } from "react";
|
|
5
|
+
import { Fragment as l, jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
6
6
|
//#region src/components/ContextMenu/ContextMenu.tsx
|
|
7
|
-
|
|
7
|
+
var f = 10;
|
|
8
|
+
function p(e) {
|
|
8
9
|
return "separator" in e && e.separator === !0;
|
|
9
10
|
}
|
|
10
|
-
function
|
|
11
|
-
let [
|
|
11
|
+
function m({ items: m, children: g, className: _, trigger: v = "contextmenu", longPressDelay: y = 500, viewportMargin: b = 8 }) {
|
|
12
|
+
let [x, S] = c(!1), [C, w] = c({
|
|
12
13
|
x: 0,
|
|
13
14
|
y: 0
|
|
14
|
-
}), [
|
|
15
|
-
|
|
16
|
-
}, []),
|
|
17
|
-
e
|
|
15
|
+
}), [T, E] = c(-1), D = a(), [O, k] = c(null), A = s([]), j = s(null), M = s(null), N = m.map((e, t) => !p(e) && !e.disabled ? t : -1).filter((e) => e !== -1), P = r(() => {
|
|
16
|
+
S(!1), E(-1);
|
|
17
|
+
}, []), F = r((e) => {
|
|
18
|
+
w(e), E(-1), S(!0);
|
|
19
|
+
}, []), I = r(() => {
|
|
20
|
+
j.current !== null && (clearTimeout(j.current), j.current = null), M.current = null;
|
|
21
|
+
}, []);
|
|
22
|
+
i(() => I, [I]);
|
|
23
|
+
let L = (e) => {
|
|
24
|
+
v !== "click" && (e.preventDefault(), F({
|
|
18
25
|
x: e.clientX,
|
|
19
26
|
y: e.clientY
|
|
20
|
-
})
|
|
27
|
+
}));
|
|
28
|
+
}, R = (e) => {
|
|
29
|
+
v !== "contextmenu" && F({
|
|
30
|
+
x: e.clientX,
|
|
31
|
+
y: e.clientY
|
|
32
|
+
});
|
|
33
|
+
}, z = (e) => {
|
|
34
|
+
if (y <= 0 || e.pointerType === "mouse") return;
|
|
35
|
+
let t = {
|
|
36
|
+
x: e.clientX,
|
|
37
|
+
y: e.clientY
|
|
38
|
+
};
|
|
39
|
+
M.current = t, I(), M.current = t, j.current = setTimeout(() => {
|
|
40
|
+
j.current = null, M.current = null, h(), F(t);
|
|
41
|
+
}, y);
|
|
42
|
+
}, B = (e) => {
|
|
43
|
+
let t = M.current;
|
|
44
|
+
t && j.current !== null && Math.hypot(e.clientX - t.x, e.clientY - t.y) > f && I();
|
|
21
45
|
};
|
|
22
46
|
i(() => {
|
|
23
|
-
if (!
|
|
47
|
+
if (!x) return;
|
|
24
48
|
let e = (e) => {
|
|
25
49
|
if (e.key === "Escape") {
|
|
26
|
-
|
|
50
|
+
P();
|
|
27
51
|
return;
|
|
28
52
|
}
|
|
29
|
-
|
|
30
|
-
e.preventDefault();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
e.preventDefault();
|
|
36
|
-
let t = w.indexOf(y), n = w[(t - 1 + w.length) % w.length] ?? -1;
|
|
37
|
-
b(n), C.current[n]?.focus();
|
|
38
|
-
}
|
|
53
|
+
let t = (t) => {
|
|
54
|
+
e.preventDefault(), E(t), A.current[t]?.focus();
|
|
55
|
+
};
|
|
56
|
+
if (N.length === 0) return;
|
|
57
|
+
let n = N.indexOf(T);
|
|
58
|
+
e.key === "ArrowDown" && t(N[(n + 1) % N.length] ?? -1), e.key === "ArrowUp" && t(N[(n - 1 + N.length) % N.length] ?? -1), e.key === "Home" && t(N[0] ?? -1), e.key === "End" && t(N[N.length - 1] ?? -1);
|
|
39
59
|
}, t = (e) => {
|
|
40
|
-
|
|
60
|
+
O && !O.contains(e.target) && P();
|
|
41
61
|
};
|
|
42
|
-
return window.addEventListener("keydown", e), window.addEventListener("mousedown", t), () => {
|
|
43
|
-
window.removeEventListener("keydown", e), window.removeEventListener("mousedown", t);
|
|
62
|
+
return window.addEventListener("keydown", e), window.addEventListener("mousedown", t), window.addEventListener("resize", P), window.addEventListener("scroll", P, !0), () => {
|
|
63
|
+
window.removeEventListener("keydown", e), window.removeEventListener("mousedown", t), window.removeEventListener("resize", P), window.removeEventListener("scroll", P, !0);
|
|
44
64
|
};
|
|
45
65
|
}, [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
x,
|
|
67
|
+
T,
|
|
68
|
+
N,
|
|
69
|
+
P,
|
|
70
|
+
O
|
|
71
|
+
]), o(() => {
|
|
72
|
+
if (!x || !O) return;
|
|
73
|
+
O.focus({ preventScroll: !0 });
|
|
74
|
+
let { offsetWidth: e, offsetHeight: t } = O;
|
|
75
|
+
if (e === 0 && t === 0) return;
|
|
76
|
+
let n = window.innerWidth - e - b, r = window.innerHeight - t - b, i = Math.max(b, Math.min(C.x, n)), a = Math.max(b, Math.min(C.y, r));
|
|
77
|
+
(i !== C.x || a !== C.y) && w({
|
|
78
|
+
x: i,
|
|
79
|
+
y: a
|
|
80
|
+
});
|
|
81
|
+
}, [
|
|
82
|
+
x,
|
|
83
|
+
O,
|
|
84
|
+
C.x,
|
|
85
|
+
C.y,
|
|
86
|
+
b
|
|
50
87
|
]);
|
|
51
|
-
let
|
|
52
|
-
e.onSelect?.(),
|
|
88
|
+
let V = (e) => {
|
|
89
|
+
e.onSelect?.(), P();
|
|
53
90
|
};
|
|
54
|
-
return /* @__PURE__ */
|
|
91
|
+
return /* @__PURE__ */ d(l, { children: [/* @__PURE__ */ u("span", {
|
|
55
92
|
className: n.root,
|
|
56
|
-
onContextMenu:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
93
|
+
onContextMenu: L,
|
|
94
|
+
onClick: R,
|
|
95
|
+
onPointerDown: z,
|
|
96
|
+
onPointerMove: B,
|
|
97
|
+
onPointerUp: I,
|
|
98
|
+
onPointerCancel: I,
|
|
99
|
+
children: g
|
|
100
|
+
}), x && /* @__PURE__ */ u(t, { children: /* @__PURE__ */ u("ul", {
|
|
101
|
+
ref: k,
|
|
102
|
+
id: D,
|
|
61
103
|
role: "menu",
|
|
62
|
-
|
|
104
|
+
"aria-orientation": "vertical",
|
|
105
|
+
tabIndex: -1,
|
|
106
|
+
className: e(n.menu, _),
|
|
63
107
|
style: {
|
|
64
|
-
top:
|
|
65
|
-
left:
|
|
108
|
+
top: C.y,
|
|
109
|
+
left: C.x
|
|
66
110
|
},
|
|
67
|
-
children:
|
|
111
|
+
children: m.map((t, r) => p(t) ? /* @__PURE__ */ u("li", {
|
|
68
112
|
role: "separator",
|
|
69
113
|
className: n.separator,
|
|
70
114
|
"aria-hidden": !0
|
|
71
|
-
}, `separator-${r}`) : /* @__PURE__ */
|
|
115
|
+
}, `separator-${r}`) : /* @__PURE__ */ u("li", {
|
|
72
116
|
role: "none",
|
|
73
|
-
children: /* @__PURE__ */
|
|
117
|
+
children: /* @__PURE__ */ u("button", {
|
|
74
118
|
ref: (e) => {
|
|
75
|
-
|
|
119
|
+
A.current[r] = e;
|
|
76
120
|
},
|
|
77
121
|
type: "button",
|
|
78
122
|
role: "menuitem",
|
|
79
|
-
className: e(n.item, t.danger && n.danger,
|
|
123
|
+
className: e(n.item, t.danger && n.danger, T === r && n.active),
|
|
80
124
|
disabled: t.disabled,
|
|
81
|
-
onClick: () =>
|
|
82
|
-
onMouseEnter: () =>
|
|
125
|
+
onClick: () => V(t),
|
|
126
|
+
onMouseEnter: () => E(r),
|
|
83
127
|
children: t.label
|
|
84
128
|
})
|
|
85
129
|
}, `item-${r}`))
|
|
86
130
|
}) })] });
|
|
87
131
|
}
|
|
132
|
+
function h() {
|
|
133
|
+
typeof window > "u" || window.addEventListener("click", (e) => {
|
|
134
|
+
e.preventDefault(), e.stopPropagation();
|
|
135
|
+
}, {
|
|
136
|
+
capture: !0,
|
|
137
|
+
once: !0
|
|
138
|
+
});
|
|
139
|
+
}
|
|
88
140
|
//#endregion
|
|
89
|
-
export {
|
|
141
|
+
export { m as ContextMenu };
|
|
90
142
|
|
|
91
143
|
//# sourceMappingURL=ContextMenu.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextMenu.js","names":[],"sources":["../../../src/components/ContextMenu/ContextMenu.tsx"],"sourcesContent":["/**\n * @tempest-limits function-lines — the body positions the menu against the pointer,\n * flips it inside the viewport, and owns roving focus for the keyboard path — one\n * geometry computed from one anchor point.\n */\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Portal } from \"@/components/Portal\";\nimport styles from \"./ContextMenu.module.css\";\n\nexport type ContextMenuItem =\n | {\n label: ReactNode;\n onSelect?: () => void;\n disabled?: boolean;\n danger?: boolean;\n }\n | { separator: true };\n\nexport interface ContextMenuProps {\n /** Menu entries — selectable items and separators. */\n items: ContextMenuItem[];\n /** Trigger area. Right-clicking anywhere within opens the menu at the cursor. */\n children: ReactNode;\n /** Extra class names forwarded to the menu element. */\n className?: string;\n}\n\ninterface Position {\n x: number;\n y: number;\n}\n\nfunction isSeparator(item: ContextMenuItem): item is { separator: true } {\n return \"separator\" in item && item.separator === true;\n}\n\n/**\n * Right-click context menu.\n *\n * - Opens at the cursor position on `onContextMenu` (default browser menu suppressed).\n * - Rendered through a {@link Portal} so it escapes parent overflow/stacking contexts.\n * - Closes on outside click, Escape, or item selection.\n * - Arrow Up/Down move focus across selectable items; Enter activates the focused item.\n *\n * @param props - The context menu props.\n * @returns The trigger wrapper plus the portalled menu when open.\n */\nexport function ContextMenu({ items, children, className }: ContextMenuProps) {\n const [open, setOpen] = useState(false);\n const [position, setPosition] = useState<Position>({ x: 0, y: 0 });\n const [activeIndex, setActiveIndex] = useState<number>(-1);\n const id = useId();\n const menuRef = useRef<HTMLUListElement>(null);\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n const selectableIndexes = items\n .map((item, index) => (!isSeparator(item) && !item.disabled ? index : -1))\n .filter((i) => i !== -1);\n\n const close = useCallback((): void => {\n setOpen(false);\n setActiveIndex(-1);\n }, []);\n\n const handleContextMenu = (event: React.MouseEvent): void => {\n event.preventDefault();\n setPosition({ x: event.clientX, y: event.clientY });\n setActiveIndex(-1);\n setOpen(true);\n };\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n close();\n return;\n }\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n const current = selectableIndexes.indexOf(activeIndex);\n const next = selectableIndexes[(current + 1) % selectableIndexes.length] ?? -1;\n setActiveIndex(next);\n itemRefs.current[next]?.focus();\n }\n if (event.key === \"ArrowUp\") {\n event.preventDefault();\n const current = selectableIndexes.indexOf(activeIndex);\n const prev =\n selectableIndexes[\n (current - 1 + selectableIndexes.length) % selectableIndexes.length\n ] ?? -1;\n setActiveIndex(prev);\n itemRefs.current[prev]?.focus();\n }\n };\n const onDown = (event: MouseEvent): void => {\n if (menuRef.current && !menuRef.current.contains(event.target as Node)) close();\n };\n window.addEventListener(\"keydown\", onKey);\n window.addEventListener(\"mousedown\", onDown);\n return () => {\n window.removeEventListener(\"keydown\", onKey);\n window.removeEventListener(\"mousedown\", onDown);\n };\n }, [open, activeIndex, selectableIndexes, close]);\n\n const handleSelect = (item: Extract<ContextMenuItem, { label: ReactNode }>): void => {\n item.onSelect?.();\n close();\n };\n\n return (\n <>\n <span className={styles.root} onContextMenu={handleContextMenu}>\n {children}\n </span>\n {open && (\n <Portal>\n <ul\n ref={menuRef}\n id={id}\n role=\"menu\"\n className={cn(styles.menu, className)}\n style={{ top: position.y, left: position.x }}\n >\n {items.map((item, index) => {\n if (isSeparator(item)) {\n return (\n <li\n key={`separator-${index}`}\n role=\"separator\"\n className={styles.separator}\n aria-hidden\n />\n );\n }\n return (\n <li key={`item-${index}`} role=\"none\">\n <button\n ref={(el) => {\n itemRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"menuitem\"\n className={cn(\n styles.item,\n item.danger && styles.danger,\n activeIndex === index && styles.active,\n )}\n disabled={item.disabled}\n onClick={() => handleSelect(item)}\n onMouseEnter={() => setActiveIndex(index)}\n >\n {item.label}\n </button>\n </li>\n );\n })}\n </ul>\n </Portal>\n )}\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ContextMenu.js","names":[],"sources":["../../../src/components/ContextMenu/ContextMenu.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines function-lines — the body owns one anchor point and\n * everything that reads from it: the three ways a menu opens (right click, long\n * press, click), the clamp that keeps it inside the viewport, and roving focus\n * for the keyboard path. Splitting them would hand the same geometry to three\n * files.\n */\nimport { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport type { PointerEvent as ReactPointerEvent, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Portal } from \"@/components/Portal\";\nimport styles from \"./ContextMenu.module.css\";\n\nexport type ContextMenuItem =\n | {\n label: ReactNode;\n onSelect?: () => void;\n disabled?: boolean;\n danger?: boolean;\n }\n | { separator: true };\n\n/** How the menu is opened by a pointer. Touch always gets the long press. */\nexport type ContextMenuTrigger = \"contextmenu\" | \"click\" | \"both\";\n\nexport interface ContextMenuProps {\n /** Menu entries — selectable items and separators. */\n items: ContextMenuItem[];\n /** Trigger area. Right-clicking anywhere within opens the menu at the cursor. */\n children: ReactNode;\n /** Extra class names forwarded to the menu element. */\n className?: string;\n /**\n * Which pointer gesture opens the menu. Default `\"contextmenu\"`.\n *\n * `\"click\"` is the `⋮` button case, where a right click is the wrong gesture\n * on the desktop and impossible on touch. `\"both\"` keeps the right click and\n * adds the left one.\n */\n trigger?: ContextMenuTrigger;\n /**\n * Hold, in ms, that opens the menu from a coarse pointer. Default `500`.\n *\n * Set `0` to turn the long press off. It is on by default because touch has\n * no right click: measured in Chrome with touch emulation (Pixel 7 and\n * iPhone 13 profiles), a 900 ms hold fires `pointerdown`, `touchstart`,\n * `pointerup`, `touchend` and `click` — and no `contextmenu` at all. Every\n * action behind this menu was unreachable on a phone.\n */\n longPressDelay?: number;\n /**\n * Gap, in px, kept between the menu and the edge of the viewport. Default `8`.\n */\n viewportMargin?: number;\n}\n\ninterface Position {\n x: number;\n y: number;\n}\n\n/** Movement, in px, that reads as a scroll rather than a hold. */\nconst LONG_PRESS_MOVE_TOLERANCE = 10;\n\nfunction isSeparator(item: ContextMenuItem): item is { separator: true } {\n return \"separator\" in item && item.separator === true;\n}\n\n/**\n * Context menu — right click, long press, or click.\n *\n * - Opens at the pointer on `contextmenu`, and on a long press from touch or pen,\n * which is the gesture those pointers have instead of a right click. `trigger`\n * adds or replaces the mouse gesture; the long press is independent of it.\n * - Clamped inside the viewport after it mounts, so a menu opened near an edge\n * moves in rather than overflowing — measured before the clamp: a 180 px menu\n * opened at `x=235` in a 320 px window ran 95 px off screen, cutting the right\n * edge off every label.\n * - Rendered through a {@link Portal} so it escapes parent overflow/stacking\n * contexts.\n * - Closes on outside click, Escape, scroll, resize, or item selection.\n * - Arrow Up/Down move focus across selectable items and wrap; Home/End jump to\n * the ends; Enter activates the focused item. The menu itself takes focus when\n * it opens, so a screen reader announces it instead of staying on the trigger.\n *\n * @param props - The context menu props.\n * @returns The trigger wrapper plus the portalled menu when open.\n */\nexport function ContextMenu({\n items,\n children,\n className,\n trigger = \"contextmenu\",\n longPressDelay = 500,\n viewportMargin = 8,\n}: ContextMenuProps) {\n const [open, setOpen] = useState(false);\n const [position, setPosition] = useState<Position>({ x: 0, y: 0 });\n const [activeIndex, setActiveIndex] = useState<number>(-1);\n const id = useId();\n const [menuNode, setMenuNode] = useState<HTMLUListElement | null>(null);\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);\n const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const longPressOrigin = useRef<Position | null>(null);\n\n const selectableIndexes = items\n .map((item, index) => (!isSeparator(item) && !item.disabled ? index : -1))\n .filter((i) => i !== -1);\n\n const close = useCallback((): void => {\n setOpen(false);\n setActiveIndex(-1);\n }, []);\n\n const openAt = useCallback((point: Position): void => {\n setPosition(point);\n setActiveIndex(-1);\n setOpen(true);\n }, []);\n\n const cancelLongPress = useCallback((): void => {\n if (longPressTimer.current !== null) {\n clearTimeout(longPressTimer.current);\n longPressTimer.current = null;\n }\n longPressOrigin.current = null;\n }, []);\n\n useEffect(() => cancelLongPress, [cancelLongPress]);\n\n const handleContextMenu = (event: React.MouseEvent): void => {\n if (trigger === \"click\") return;\n event.preventDefault();\n openAt({ x: event.clientX, y: event.clientY });\n };\n\n const handleClick = (event: React.MouseEvent): void => {\n if (trigger === \"contextmenu\") return;\n openAt({ x: event.clientX, y: event.clientY });\n };\n\n /**\n * Start the hold that stands in for a right click on a coarse pointer.\n *\n * A mouse is excluded: it has `contextmenu`, and holding the left button is\n * how a drag starts.\n */\n const handlePointerDown = (event: ReactPointerEvent): void => {\n if (longPressDelay <= 0 || event.pointerType === \"mouse\") return;\n const point = { x: event.clientX, y: event.clientY };\n longPressOrigin.current = point;\n cancelLongPress();\n longPressOrigin.current = point;\n longPressTimer.current = setTimeout(() => {\n longPressTimer.current = null;\n longPressOrigin.current = null;\n suppressNextClick();\n openAt(point);\n }, longPressDelay);\n };\n\n /**\n * Cancel the hold once the finger travels — otherwise every scroll that\n * starts on the trigger opens a menu, which is the difference between a\n * usable long press and an infuriating one.\n */\n const handlePointerMove = (event: ReactPointerEvent): void => {\n const origin = longPressOrigin.current;\n if (!origin || longPressTimer.current === null) return;\n if (\n Math.hypot(event.clientX - origin.x, event.clientY - origin.y) >\n LONG_PRESS_MOVE_TOLERANCE\n ) {\n cancelLongPress();\n }\n };\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n close();\n return;\n }\n const move = (next: number): void => {\n event.preventDefault();\n setActiveIndex(next);\n itemRefs.current[next]?.focus();\n };\n if (selectableIndexes.length === 0) return;\n const current = selectableIndexes.indexOf(activeIndex);\n if (event.key === \"ArrowDown\") {\n move(selectableIndexes[(current + 1) % selectableIndexes.length] ?? -1);\n }\n if (event.key === \"ArrowUp\") {\n move(\n selectableIndexes[\n (current - 1 + selectableIndexes.length) % selectableIndexes.length\n ] ?? -1,\n );\n }\n if (event.key === \"Home\") {\n move(selectableIndexes[0] ?? -1);\n }\n if (event.key === \"End\") {\n move(selectableIndexes[selectableIndexes.length - 1] ?? -1);\n }\n };\n const onDown = (event: MouseEvent): void => {\n if (menuNode && !menuNode.contains(event.target as Node)) close();\n };\n window.addEventListener(\"keydown\", onKey);\n window.addEventListener(\"mousedown\", onDown);\n window.addEventListener(\"resize\", close);\n window.addEventListener(\"scroll\", close, true);\n return () => {\n window.removeEventListener(\"keydown\", onKey);\n window.removeEventListener(\"mousedown\", onDown);\n window.removeEventListener(\"resize\", close);\n window.removeEventListener(\"scroll\", close, true);\n };\n }, [open, activeIndex, selectableIndexes, close, menuNode]);\n\n /**\n * Pull the menu inside the viewport, and take focus.\n *\n * Keyed off the node rather than off `open`, because {@link Portal} renders\n * `null` on its first pass and mounts on an effect — an effect that reads a\n * ref when `open` flips runs while the menu does not exist yet, measures\n * nothing and focuses nothing. A callback ref in state is what makes this\n * run on the render that has the element.\n *\n * `useLayoutEffect` runs before paint, so the menu is never painted at the\n * overflowing position first.\n *\n * The size comes from `offsetWidth`/`offsetHeight` rather than from\n * `getBoundingClientRect`, which reports the **transformed** box: the menu\n * enters at `scale(0.96)`, so the rect is 4% small and the clamp lets the\n * grown menu back over the edge. Measured in Chrome at 320 px wide, a 180 px\n * menu came back at `right: 316` against a margin that asked for 312.\n */\n useLayoutEffect(() => {\n if (!open || !menuNode) return;\n menuNode.focus({ preventScroll: true });\n const { offsetWidth, offsetHeight } = menuNode;\n if (offsetWidth === 0 && offsetHeight === 0) return;\n const maxLeft = window.innerWidth - offsetWidth - viewportMargin;\n const maxTop = window.innerHeight - offsetHeight - viewportMargin;\n const left = Math.max(viewportMargin, Math.min(position.x, maxLeft));\n const top = Math.max(viewportMargin, Math.min(position.y, maxTop));\n if (left !== position.x || top !== position.y) setPosition({ x: left, y: top });\n }, [open, menuNode, position.x, position.y, viewportMargin]);\n\n const handleSelect = (item: Extract<ContextMenuItem, { label: ReactNode }>): void => {\n item.onSelect?.();\n close();\n };\n\n return (\n <>\n <span\n className={styles.root}\n onContextMenu={handleContextMenu}\n onClick={handleClick}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={cancelLongPress}\n onPointerCancel={cancelLongPress}\n >\n {children}\n </span>\n {open && (\n <Portal>\n <ul\n ref={setMenuNode}\n id={id}\n role=\"menu\"\n aria-orientation=\"vertical\"\n tabIndex={-1}\n className={cn(styles.menu, className)}\n style={{ top: position.y, left: position.x }}\n >\n {items.map((item, index) => {\n if (isSeparator(item)) {\n return (\n <li\n key={`separator-${index}`}\n role=\"separator\"\n className={styles.separator}\n aria-hidden\n />\n );\n }\n return (\n <li key={`item-${index}`} role=\"none\">\n <button\n ref={(el) => {\n itemRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"menuitem\"\n className={cn(\n styles.item,\n item.danger && styles.danger,\n activeIndex === index && styles.active,\n )}\n disabled={item.disabled}\n onClick={() => handleSelect(item)}\n onMouseEnter={() => setActiveIndex(index)}\n >\n {item.label}\n </button>\n </li>\n );\n })}\n </ul>\n </Portal>\n )}\n </>\n );\n}\n\n/**\n * Swallow the click a finger leaves behind after a long press.\n *\n * The hold fires while the finger is still down, so the browser goes on to\n * deliver `pointerup` and then `click` to whatever is under it — measured, that\n * is the same element the menu was just opened from. Without this, opening the\n * menu on a chat bubble also opens the bubble.\n *\n * Capture phase and `once`, so the listener never outlives the gesture that\n * installed it, and a later genuine click is untouched.\n */\nfunction suppressNextClick(): void {\n if (typeof window === \"undefined\") return;\n window.addEventListener(\n \"click\",\n (event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n },\n { capture: true, once: true },\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextMenu.module.cjs","names":[],"sources":["../../../src/components/ContextMenu/ContextMenu.module.css"],"sourcesContent":[".root {\n display: contents;\n}\n\n.menu {\n position: fixed;\n z-index: var(--tempest-z-popover);\n min-width: 180px;\n max-width: 320px;\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n padding: var(--tempest-space-1);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n list-style: none;\n margin: 0;\n animation: tempest-context-menu-in var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: none;\n border: none;\n border-radius: var(--tempest-radius-sm);\n color: var(--tempest-text);\n font-family: inherit;\n font-size: inherit;\n line-height: var(--tempest-leading-snug);\n text-align: left;\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .item:hover:not(:disabled) {\n background-color: var(--tempest-surface);\n }\n}\n\n.item:focus,\n.item.active {\n outline: none;\n background-color: var(--tempest-surface);\n}\n\n.item:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n\n.item.danger {\n color: var(--tempest-danger);\n}\n\n.separator {\n height: 1px;\n background-color: var(--tempest-border);\n margin: var(--tempest-space-1) 0;\n}\n\n@keyframes tempest-context-menu-in {\n from {\n opacity: 0;\n transform: scale(0.96);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .menu {\n animation: none;\n }\n}\n"],"mappings":";"}
|
|
1
|
+
{"version":3,"file":"ContextMenu.module.cjs","names":[],"sources":["../../../src/components/ContextMenu/ContextMenu.module.css"],"sourcesContent":[".root {\n display: contents;\n}\n\n/*\n * A hold on a coarse pointer is this component's right click, and the platform\n * has its own ideas about what a hold means: iOS raises the callout and both\n * platforms start a text selection. Scoped to `pointer: coarse` so a mouse user\n * keeps ordinary selection over the same content.\n */\n@media (pointer: coarse) {\n .root {\n -webkit-touch-callout: none;\n user-select: none;\n }\n}\n\n/*\n * The menu takes focus when it opens so a screen reader announces it, and that\n * focus gets no ring of its own: measured in Chrome, focusing it from a long\n * press still matches `:focus-visible`, which drew a heavy ring around the whole\n * menu after every touch. The indicator that matters is on the focused item,\n * which is where the arrow keys move next.\n */\n.menu:focus {\n outline: none;\n}\n\n.menu {\n position: fixed;\n z-index: var(--tempest-z-popover);\n min-width: 180px;\n max-width: 320px;\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n padding: var(--tempest-space-1);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n list-style: none;\n margin: 0;\n animation: tempest-context-menu-in var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: none;\n border: none;\n border-radius: var(--tempest-radius-sm);\n color: var(--tempest-text);\n font-family: inherit;\n font-size: inherit;\n line-height: var(--tempest-leading-snug);\n text-align: left;\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .item:hover:not(:disabled) {\n background-color: var(--tempest-surface);\n }\n}\n\n.item:focus,\n.item.active {\n outline: none;\n background-color: var(--tempest-surface);\n}\n\n.item:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n\n.item.danger {\n color: var(--tempest-danger);\n}\n\n.separator {\n height: 1px;\n background-color: var(--tempest-border);\n margin: var(--tempest-space-1) 0;\n}\n\n@keyframes tempest-context-menu-in {\n from {\n opacity: 0;\n transform: scale(0.96);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .menu {\n animation: none;\n }\n}\n"],"mappings":";"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextMenu.module.js","names":[],"sources":["../../../src/components/ContextMenu/ContextMenu.module.css"],"sourcesContent":[".root {\n display: contents;\n}\n\n.menu {\n position: fixed;\n z-index: var(--tempest-z-popover);\n min-width: 180px;\n max-width: 320px;\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n padding: var(--tempest-space-1);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n list-style: none;\n margin: 0;\n animation: tempest-context-menu-in var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: none;\n border: none;\n border-radius: var(--tempest-radius-sm);\n color: var(--tempest-text);\n font-family: inherit;\n font-size: inherit;\n line-height: var(--tempest-leading-snug);\n text-align: left;\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .item:hover:not(:disabled) {\n background-color: var(--tempest-surface);\n }\n}\n\n.item:focus,\n.item.active {\n outline: none;\n background-color: var(--tempest-surface);\n}\n\n.item:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n\n.item.danger {\n color: var(--tempest-danger);\n}\n\n.separator {\n height: 1px;\n background-color: var(--tempest-border);\n margin: var(--tempest-space-1) 0;\n}\n\n@keyframes tempest-context-menu-in {\n from {\n opacity: 0;\n transform: scale(0.96);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .menu {\n animation: none;\n }\n}\n"],"mappings":";"}
|
|
1
|
+
{"version":3,"file":"ContextMenu.module.js","names":[],"sources":["../../../src/components/ContextMenu/ContextMenu.module.css"],"sourcesContent":[".root {\n display: contents;\n}\n\n/*\n * A hold on a coarse pointer is this component's right click, and the platform\n * has its own ideas about what a hold means: iOS raises the callout and both\n * platforms start a text selection. Scoped to `pointer: coarse` so a mouse user\n * keeps ordinary selection over the same content.\n */\n@media (pointer: coarse) {\n .root {\n -webkit-touch-callout: none;\n user-select: none;\n }\n}\n\n/*\n * The menu takes focus when it opens so a screen reader announces it, and that\n * focus gets no ring of its own: measured in Chrome, focusing it from a long\n * press still matches `:focus-visible`, which drew a heavy ring around the whole\n * menu after every touch. The indicator that matters is on the focused item,\n * which is where the arrow keys move next.\n */\n.menu:focus {\n outline: none;\n}\n\n.menu {\n position: fixed;\n z-index: var(--tempest-z-popover);\n min-width: 180px;\n max-width: 320px;\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n padding: var(--tempest-space-1);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n list-style: none;\n margin: 0;\n animation: tempest-context-menu-in var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: none;\n border: none;\n border-radius: var(--tempest-radius-sm);\n color: var(--tempest-text);\n font-family: inherit;\n font-size: inherit;\n line-height: var(--tempest-leading-snug);\n text-align: left;\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .item:hover:not(:disabled) {\n background-color: var(--tempest-surface);\n }\n}\n\n.item:focus,\n.item.active {\n outline: none;\n background-color: var(--tempest-surface);\n}\n\n.item:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n\n.item.danger {\n color: var(--tempest-danger);\n}\n\n.separator {\n height: 1px;\n background-color: var(--tempest-border);\n margin: var(--tempest-space-1) 0;\n}\n\n@keyframes tempest-context-menu-in {\n from {\n opacity: 0;\n transform: scale(0.96);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .menu {\n animation: none;\n }\n}\n"],"mappings":";"}
|
package/dist/styles/advanced.css
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
@keyframes tempest_tempest-modal-in_uDcF6{0%{opacity:0;transform:translateY(-12px)scale(.98)}to{opacity:1;transform:translateY(0)scale(1)}}
|
|
25
25
|
@media (prefers-reduced-motion:reduce){.tempest_overlay_drnPW,.tempest_dialog_lPl6k{animation:none}}
|
|
26
26
|
.tempest_root_OxRHj{display:contents}
|
|
27
|
+
@media (pointer:coarse){.tempest_root_OxRHj{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}}
|
|
28
|
+
.tempest_menu_MDn4t:focus{outline:none}
|
|
27
29
|
.tempest_menu_MDn4t{z-index:var(--tempest-z-popover);background-color:var(--tempest-bg);border:1px solid var(--tempest-border);border-radius:var(--tempest-radius-lg);min-width:180px;max-width:320px;box-shadow:var(--tempest-shadow-lg);padding:var(--tempest-space-1);font-family:var(--tempest-font-sans);font-size:var(--tempest-text-base);animation:tempest_tempest-context-menu-in_fNuOl var(--tempest-duration-fast) var(--tempest-ease-out);margin:0;list-style:none;position:fixed}
|
|
28
30
|
.tempest_item_PoiMY{align-items:center;gap:var(--tempest-space-2);width:100%;padding:var(--tempest-space-2) var(--tempest-space-3);border-radius:var(--tempest-radius-sm);color:var(--tempest-text);font-family:inherit;font-size:inherit;line-height:var(--tempest-leading-snug);text-align:left;cursor:pointer;transition:var(--tempest-transition-color);background:0 0;border:none;display:flex}
|
|
29
31
|
@media (hover:hover) and (pointer:fine){.tempest_item_PoiMY:hover:not(:disabled){background-color:var(--tempest-surface)}}
|
package/dist/styles/chat.css
CHANGED
|
@@ -101,6 +101,34 @@
|
|
|
101
101
|
.tempest_ownBubble_agOMW .tempest_meta_3fNx8{color:var(--tempest-primary-on-soft)}
|
|
102
102
|
.tempest_status_EeiUt{align-items:center;display:inline-flex}
|
|
103
103
|
.tempest_statusFailed_Bfro1{color:var(--tempest-danger);font-weight:var(--tempest-weight-bold)}
|
|
104
|
+
.tempest_bubbleRow_QSc41{flex-wrap:wrap}
|
|
105
|
+
.tempest_reactions_D4iGE{gap:var(--tempest-space-1);flex-wrap:wrap;flex-basis:100%;margin:0;padding:0;list-style:none;display:flex}
|
|
106
|
+
.tempest_ownRun_3XTG5 .tempest_reactions_D4iGE{justify-content:flex-end}
|
|
107
|
+
.tempest_reaction_WkkNQ{align-items:center;gap:var(--tempest-space-1);padding:0 var(--tempest-space-2);border:1px solid var(--tempest-border);border-radius:var(--tempest-radius-full,999px);background-color:var(--tempest-surface);color:var(--tempest-text);font:inherit;font-size:var(--tempest-text-xs);cursor:pointer;line-height:1.8;display:inline-flex}
|
|
108
|
+
.tempest_reaction_WkkNQ:disabled{cursor:default}
|
|
109
|
+
.tempest_reactionMine_81mXj{background-color:var(--tempest-primary-soft);color:var(--tempest-primary-on-soft);font-weight:var(--tempest-weight-medium);border-color:#0000}
|
|
110
|
+
.tempest_actionsButton_v8oqH{padding:0 var(--tempest-space-1);color:var(--tempest-text-muted);font:inherit;cursor:pointer;background:0 0;border:none;flex:none;line-height:1}
|
|
111
|
+
@media (hover:hover) and (pointer:fine){.tempest_actionsButton_v8oqH{opacity:0;transition:var(--tempest-transition-opacity,opacity .12s ease)}.tempest_bubbleRow_QSc41:hover .tempest_actionsButton_v8oqH,.tempest_actionsButton_v8oqH:focus-visible{opacity:1}}
|
|
112
|
+
.tempest_quote_1muxm{width:100%;padding:var(--tempest-space-1) var(--tempest-space-2);border-left:3px solid var(--tempest-primary);border-radius:var(--tempest-radius-sm);background-color:var(--tempest-surface-2);color:var(--tempest-text);font-size:var(--tempest-text-xs);text-align:left;flex-direction:column;gap:2px;display:flex}
|
|
113
|
+
.tempest_quoteButton_eD0Dx{font:inherit;font-size:var(--tempest-text-xs);cursor:pointer;border-top:none;border-bottom:none;border-right:none}
|
|
114
|
+
.tempest_quoteAuthor_Jpx15{color:var(--tempest-text);font-weight:var(--tempest-weight-medium)}
|
|
115
|
+
.tempest_quoteText_9KI-m{-webkit-line-clamp:2;line-clamp:2;overflow-wrap:anywhere;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}
|
|
116
|
+
.tempest_quoteRevoked_RQ-5u{color:var(--tempest-text-muted);font-style:italic}
|
|
117
|
+
.tempest_tombstone_7Iiw-{align-items:center;gap:var(--tempest-space-1);color:var(--tempest-text-muted);font-style:italic;display:flex}
|
|
118
|
+
.tempest_deletedBubble_dZQFE{background-color:#0000;border-style:dashed}
|
|
119
|
+
.tempest_edited_kTire{font-style:italic}
|
|
120
|
+
.tempest_statusRead_Wj77L{color:var(--tempest-info,var(--tempest-primary))}
|
|
121
|
+
.tempest_attachments_h8wIJ{gap:var(--tempest-space-1);flex-direction:column;max-width:320px;display:flex}
|
|
122
|
+
.tempest_attachmentsFallback_YT9RT{min-height:var(--tempest-space-8,2rem)}
|
|
123
|
+
.tempest_imageAttachment_PggzU{border-radius:var(--tempest-radius-md);cursor:pointer;background:0 0;border:none;padding:0;line-height:0}
|
|
124
|
+
.tempest_imageAttachment_PggzU img{border-radius:var(--tempest-radius-md);object-fit:cover;max-width:100%;max-height:260px}
|
|
125
|
+
.tempest_videoAttachment_0TsUC{border-radius:var(--tempest-radius-md);max-width:100%}
|
|
126
|
+
.tempest_voiceAttachment_1EUcu{gap:var(--tempest-space-1);flex-direction:column;display:flex}
|
|
127
|
+
.tempest_waveform_5K-qs{align-items:flex-end;gap:2px;height:28px;display:flex}
|
|
128
|
+
.tempest_waveform_5K-qs span{background-color:var(--tempest-primary);opacity:.7;border-radius:1px;flex:auto;min-width:2px}
|
|
129
|
+
.tempest_fileAttachment_H803b{padding:var(--tempest-space-2);border:1px solid var(--tempest-border);border-radius:var(--tempest-radius-md);color:var(--tempest-text);flex-direction:column;gap:2px;text-decoration:none;display:flex}
|
|
130
|
+
.tempest_fileName_2Fwix{overflow-wrap:anywhere;font-weight:var(--tempest-weight-medium)}
|
|
131
|
+
.tempest_fileMeta_T1fdS{color:var(--tempest-text-muted);font-size:var(--tempest-text-2xs);font-variant-numeric:tabular-nums}
|
|
104
132
|
.tempest_retry_DAbIj{color:var(--tempest-danger);font:inherit;font-size:var(--tempest-text-xs);cursor:pointer;background:0 0;border:none;padding:0;text-decoration:underline}
|
|
105
133
|
.tempest_typing_zdp3O{padding:0 var(--tempest-space-4) var(--tempest-space-2);color:var(--tempest-text-subtle);font-size:var(--tempest-text-xs);flex:none;margin:0}
|
|
106
134
|
.tempest_composer_krOGl{align-items:flex-end;gap:var(--tempest-space-2);padding:var(--tempest-space-3) var(--tempest-space-4);border-top:1px solid var(--tempest-border);flex:none;display:flex}
|
|
@@ -23,6 +23,34 @@
|
|
|
23
23
|
.tempest_ownBubble_agOMW .tempest_meta_3fNx8{color:var(--tempest-primary-on-soft)}
|
|
24
24
|
.tempest_status_EeiUt{align-items:center;display:inline-flex}
|
|
25
25
|
.tempest_statusFailed_Bfro1{color:var(--tempest-danger);font-weight:var(--tempest-weight-bold)}
|
|
26
|
+
.tempest_bubbleRow_QSc41{flex-wrap:wrap}
|
|
27
|
+
.tempest_reactions_D4iGE{gap:var(--tempest-space-1);flex-wrap:wrap;flex-basis:100%;margin:0;padding:0;list-style:none;display:flex}
|
|
28
|
+
.tempest_ownRun_3XTG5 .tempest_reactions_D4iGE{justify-content:flex-end}
|
|
29
|
+
.tempest_reaction_WkkNQ{align-items:center;gap:var(--tempest-space-1);padding:0 var(--tempest-space-2);border:1px solid var(--tempest-border);border-radius:var(--tempest-radius-full,999px);background-color:var(--tempest-surface);color:var(--tempest-text);font:inherit;font-size:var(--tempest-text-xs);cursor:pointer;line-height:1.8;display:inline-flex}
|
|
30
|
+
.tempest_reaction_WkkNQ:disabled{cursor:default}
|
|
31
|
+
.tempest_reactionMine_81mXj{background-color:var(--tempest-primary-soft);color:var(--tempest-primary-on-soft);font-weight:var(--tempest-weight-medium);border-color:#0000}
|
|
32
|
+
.tempest_actionsButton_v8oqH{padding:0 var(--tempest-space-1);color:var(--tempest-text-muted);font:inherit;cursor:pointer;background:0 0;border:none;flex:none;line-height:1}
|
|
33
|
+
@media (hover:hover) and (pointer:fine){.tempest_actionsButton_v8oqH{opacity:0;transition:var(--tempest-transition-opacity,opacity .12s ease)}.tempest_bubbleRow_QSc41:hover .tempest_actionsButton_v8oqH,.tempest_actionsButton_v8oqH:focus-visible{opacity:1}}
|
|
34
|
+
.tempest_quote_1muxm{width:100%;padding:var(--tempest-space-1) var(--tempest-space-2);border-left:3px solid var(--tempest-primary);border-radius:var(--tempest-radius-sm);background-color:var(--tempest-surface-2);color:var(--tempest-text);font-size:var(--tempest-text-xs);text-align:left;flex-direction:column;gap:2px;display:flex}
|
|
35
|
+
.tempest_quoteButton_eD0Dx{font:inherit;font-size:var(--tempest-text-xs);cursor:pointer;border-top:none;border-bottom:none;border-right:none}
|
|
36
|
+
.tempest_quoteAuthor_Jpx15{color:var(--tempest-text);font-weight:var(--tempest-weight-medium)}
|
|
37
|
+
.tempest_quoteText_9KI-m{-webkit-line-clamp:2;line-clamp:2;overflow-wrap:anywhere;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}
|
|
38
|
+
.tempest_quoteRevoked_RQ-5u{color:var(--tempest-text-muted);font-style:italic}
|
|
39
|
+
.tempest_tombstone_7Iiw-{align-items:center;gap:var(--tempest-space-1);color:var(--tempest-text-muted);font-style:italic;display:flex}
|
|
40
|
+
.tempest_deletedBubble_dZQFE{background-color:#0000;border-style:dashed}
|
|
41
|
+
.tempest_edited_kTire{font-style:italic}
|
|
42
|
+
.tempest_statusRead_Wj77L{color:var(--tempest-info,var(--tempest-primary))}
|
|
43
|
+
.tempest_attachments_h8wIJ{gap:var(--tempest-space-1);flex-direction:column;max-width:320px;display:flex}
|
|
44
|
+
.tempest_attachmentsFallback_YT9RT{min-height:var(--tempest-space-8,2rem)}
|
|
45
|
+
.tempest_imageAttachment_PggzU{border-radius:var(--tempest-radius-md);cursor:pointer;background:0 0;border:none;padding:0;line-height:0}
|
|
46
|
+
.tempest_imageAttachment_PggzU img{border-radius:var(--tempest-radius-md);object-fit:cover;max-width:100%;max-height:260px}
|
|
47
|
+
.tempest_videoAttachment_0TsUC{border-radius:var(--tempest-radius-md);max-width:100%}
|
|
48
|
+
.tempest_voiceAttachment_1EUcu{gap:var(--tempest-space-1);flex-direction:column;display:flex}
|
|
49
|
+
.tempest_waveform_5K-qs{align-items:flex-end;gap:2px;height:28px;display:flex}
|
|
50
|
+
.tempest_waveform_5K-qs span{background-color:var(--tempest-primary);opacity:.7;border-radius:1px;flex:auto;min-width:2px}
|
|
51
|
+
.tempest_fileAttachment_H803b{padding:var(--tempest-space-2);border:1px solid var(--tempest-border);border-radius:var(--tempest-radius-md);color:var(--tempest-text);flex-direction:column;gap:2px;text-decoration:none;display:flex}
|
|
52
|
+
.tempest_fileName_2Fwix{overflow-wrap:anywhere;font-weight:var(--tempest-weight-medium)}
|
|
53
|
+
.tempest_fileMeta_T1fdS{color:var(--tempest-text-muted);font-size:var(--tempest-text-2xs);font-variant-numeric:tabular-nums}
|
|
26
54
|
.tempest_retry_DAbIj{color:var(--tempest-danger);font:inherit;font-size:var(--tempest-text-xs);cursor:pointer;background:0 0;border:none;padding:0;text-decoration:underline}
|
|
27
55
|
.tempest_typing_zdp3O{padding:0 var(--tempest-space-4) var(--tempest-space-2);color:var(--tempest-text-subtle);font-size:var(--tempest-text-xs);flex:none;margin:0}
|
|
28
56
|
.tempest_composer_krOGl{align-items:flex-end;gap:var(--tempest-space-2);padding:var(--tempest-space-3) var(--tempest-space-4);border-top:1px solid var(--tempest-border);flex:none;display:flex}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* Generated by scripts/split-css.mjs — do not edit. */
|
|
2
2
|
.tempest_root_OxRHj{display:contents}
|
|
3
|
+
@media (pointer:coarse){.tempest_root_OxRHj{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}}
|
|
4
|
+
.tempest_menu_MDn4t:focus{outline:none}
|
|
3
5
|
.tempest_menu_MDn4t{z-index:var(--tempest-z-popover);background-color:var(--tempest-bg);border:1px solid var(--tempest-border);border-radius:var(--tempest-radius-lg);min-width:180px;max-width:320px;box-shadow:var(--tempest-shadow-lg);padding:var(--tempest-space-1);font-family:var(--tempest-font-sans);font-size:var(--tempest-text-base);animation:tempest_tempest-context-menu-in_fNuOl var(--tempest-duration-fast) var(--tempest-ease-out);margin:0;list-style:none;position:fixed}
|
|
4
6
|
.tempest_item_PoiMY{align-items:center;gap:var(--tempest-space-2);width:100%;padding:var(--tempest-space-2) var(--tempest-space-3);border-radius:var(--tempest-radius-sm);color:var(--tempest-text);font-family:inherit;font-size:inherit;line-height:var(--tempest-leading-snug);text-align:left;cursor:pointer;transition:var(--tempest-transition-color);background:0 0;border:none;display:flex}
|
|
5
7
|
@media (hover:hover) and (pointer:fine){.tempest_item_PoiMY:hover:not(:disabled){background-color:var(--tempest-surface)}}
|
|
@@ -97,8 +97,20 @@
|
|
|
97
97
|
"component/Center.css"
|
|
98
98
|
],
|
|
99
99
|
"Chat": [
|
|
100
|
+
"component/AudioPlayer.css",
|
|
100
101
|
"component/Chat.css",
|
|
102
|
+
"component/ContextMenu.css",
|
|
101
103
|
"component/EmptyState.css",
|
|
104
|
+
"component/Lightbox.css",
|
|
105
|
+
"component/VideoPlayer.css",
|
|
106
|
+
"component/VisuallyHidden.css"
|
|
107
|
+
],
|
|
108
|
+
"ChatBubble": [
|
|
109
|
+
"component/AudioPlayer.css",
|
|
110
|
+
"component/Chat.css",
|
|
111
|
+
"component/ContextMenu.css",
|
|
112
|
+
"component/Lightbox.css",
|
|
113
|
+
"component/VideoPlayer.css",
|
|
102
114
|
"component/VisuallyHidden.css"
|
|
103
115
|
],
|
|
104
116
|
"ChatComposer": [
|