tempest-react-sdk 0.73.0 → 0.75.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 +6 -4
- package/dist/br/BrazilMap.module.cjs.map +1 -1
- package/dist/br/BrazilMap.module.js.map +1 -1
- package/dist/br/MapMarkers.cjs +1 -1
- package/dist/br/MapMarkers.cjs.map +1 -1
- package/dist/br/MapMarkers.js +19 -22
- package/dist/br/MapMarkers.js.map +1 -1
- package/dist/br/MunicipalitySearch.cjs +1 -1
- package/dist/br/MunicipalitySearch.cjs.map +1 -1
- package/dist/br/MunicipalitySearch.js +66 -48
- package/dist/br/MunicipalitySearch.js.map +1 -1
- package/dist/br/MunicipalitySearch.module.cjs +1 -1
- package/dist/br/MunicipalitySearch.module.cjs.map +1 -1
- package/dist/br/MunicipalitySearch.module.js +4 -3
- package/dist/br/MunicipalitySearch.module.js.map +1 -1
- package/dist/br.d.ts +12 -0
- package/dist/components/AIChat/AIChat.module.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.module.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
- package/dist/components/AIChat/AIChatTurn.js +1 -1
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/AppBar/AppBar.cjs +1 -1
- package/dist/components/AppBar/AppBar.cjs.map +1 -1
- package/dist/components/AppBar/AppBar.js +1 -0
- package/dist/components/AppBar/AppBar.js.map +1 -1
- package/dist/components/AppBar/AppBar.module.cjs +1 -1
- package/dist/components/AppBar/AppBar.module.cjs.map +1 -1
- package/dist/components/AppBar/AppBar.module.js +12 -11
- package/dist/components/AppBar/AppBar.module.js.map +1 -1
- package/dist/components/AppShell/AppShell.module.cjs.map +1 -1
- package/dist/components/AppShell/AppShell.module.js.map +1 -1
- package/dist/components/Button/Button.cjs +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js +41 -36
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Button/Button.module.cjs.map +1 -1
- package/dist/components/Button/Button.module.js.map +1 -1
- package/dist/components/Button/ButtonContent.cjs +2 -0
- package/dist/components/Button/ButtonContent.cjs.map +1 -0
- package/dist/components/Button/ButtonContent.js +34 -0
- package/dist/components/Button/ButtonContent.js.map +1 -0
- package/dist/components/Button/ButtonSlot.cjs +2 -0
- package/dist/components/Button/ButtonSlot.cjs.map +1 -0
- package/dist/components/Button/ButtonSlot.js +36 -0
- package/dist/components/Button/ButtonSlot.js.map +1 -0
- package/dist/components/Button/button-class.cjs +2 -0
- package/dist/components/Button/button-class.cjs.map +1 -0
- package/dist/components/Button/button-class.js +13 -0
- package/dist/components/Button/button-class.js.map +1 -0
- package/dist/components/Button/link-rel.cjs +2 -0
- package/dist/components/Button/link-rel.cjs.map +1 -0
- package/dist/components/Button/link-rel.js +11 -0
- package/dist/components/Button/link-rel.js.map +1 -0
- package/dist/components/Button/slot.cjs +2 -0
- package/dist/components/Button/slot.cjs.map +1 -0
- package/dist/components/Button/slot.js +41 -0
- package/dist/components/Button/slot.js.map +1 -0
- package/dist/components/Carousel/Carousel.module.cjs.map +1 -1
- package/dist/components/Carousel/Carousel.module.js.map +1 -1
- package/dist/components/Chat/Chat.module.cjs.map +1 -1
- package/dist/components/Chat/Chat.module.js.map +1 -1
- package/dist/components/Collapsible/Collapsible.cjs +1 -1
- package/dist/components/Collapsible/Collapsible.cjs.map +1 -1
- package/dist/components/Collapsible/Collapsible.js +1 -1
- package/dist/components/Collapsible/Collapsible.js.map +1 -1
- package/dist/components/Combobox/Combobox.cjs +1 -1
- package/dist/components/Combobox/Combobox.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.js +18 -23
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Combobox/use-listbox-dismiss.cjs +2 -0
- package/dist/components/Combobox/use-listbox-dismiss.cjs.map +1 -0
- package/dist/components/Combobox/use-listbox-dismiss.js +34 -0
- package/dist/components/Combobox/use-listbox-dismiss.js.map +1 -0
- package/dist/components/Dropzone/Dropzone.module.cjs.map +1 -1
- package/dist/components/Dropzone/Dropzone.module.js.map +1 -1
- package/dist/components/FilterBar/FilterBar.module.cjs.map +1 -1
- package/dist/components/FilterBar/FilterBar.module.js.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.module.cjs.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.module.js.map +1 -1
- package/dist/components/Form/Form.cjs.map +1 -1
- package/dist/components/Form/Form.js.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.module.cjs.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.module.js.map +1 -1
- package/dist/components/InstallButton/InstallButton.cjs.map +1 -1
- package/dist/components/InstallButton/InstallButton.js.map +1 -1
- package/dist/components/ListTile/ListTile.module.cjs.map +1 -1
- package/dist/components/ListTile/ListTile.module.js.map +1 -1
- package/dist/components/Markdown/Markdown.module.cjs.map +1 -1
- package/dist/components/Markdown/Markdown.module.js.map +1 -1
- package/dist/components/Menubar/Menubar.module.cjs.map +1 -1
- package/dist/components/Menubar/Menubar.module.js.map +1 -1
- package/dist/components/Modal/Modal.cjs +1 -1
- package/dist/components/Modal/Modal.cjs.map +1 -1
- package/dist/components/Modal/Modal.js +1 -1
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/Modal/Modal.module.cjs +1 -1
- package/dist/components/Modal/Modal.module.cjs.map +1 -1
- package/dist/components/Modal/Modal.module.js +1 -1
- package/dist/components/Modal/Modal.module.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.cjs +1 -1
- package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.js +55 -54
- package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
- package/dist/components/Navbar/Navbar.cjs +1 -1
- package/dist/components/Navbar/Navbar.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.js +1 -0
- package/dist/components/Navbar/Navbar.js.map +1 -1
- package/dist/components/Navbar/Navbar.module.cjs +1 -1
- package/dist/components/Navbar/Navbar.module.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.module.js +9 -8
- package/dist/components/Navbar/Navbar.module.js.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.module.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.module.js.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.module.cjs.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.module.js.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.module.cjs.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.module.js.map +1 -1
- package/dist/components/Page/Page.cjs +1 -1
- package/dist/components/Page/Page.cjs.map +1 -1
- package/dist/components/Page/Page.js +23 -39
- package/dist/components/Page/Page.js.map +1 -1
- package/dist/components/Page/Page.module.cjs +1 -1
- package/dist/components/Page/Page.module.cjs.map +1 -1
- package/dist/components/Page/Page.module.js +5 -11
- package/dist/components/Page/Page.module.js.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.module.cjs.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.module.js.map +1 -1
- package/dist/components/PinInput/PinInput.module.cjs.map +1 -1
- package/dist/components/PinInput/PinInput.module.js.map +1 -1
- package/dist/components/Reveal/Reveal.cjs +2 -0
- package/dist/components/Reveal/Reveal.cjs.map +1 -0
- package/dist/components/Reveal/Reveal.js +56 -0
- package/dist/components/Reveal/Reveal.js.map +1 -0
- package/dist/components/Reveal/Reveal.module.cjs +3 -0
- package/dist/components/Reveal/Reveal.module.cjs.map +1 -0
- package/dist/components/Reveal/Reveal.module.js +7 -0
- package/dist/components/Reveal/Reveal.module.js.map +1 -0
- package/dist/components/Reveal/reveal-threshold.cjs +2 -0
- package/dist/components/Reveal/reveal-threshold.cjs.map +1 -0
- package/dist/components/Reveal/reveal-threshold.js +13 -0
- package/dist/components/Reveal/reveal-threshold.js.map +1 -0
- package/dist/components/SectionHeader/SectionHeader.cjs +2 -0
- package/dist/components/SectionHeader/SectionHeader.cjs.map +1 -0
- package/dist/components/SectionHeader/SectionHeader.js +66 -0
- package/dist/components/SectionHeader/SectionHeader.js.map +1 -0
- package/dist/components/SectionHeader/SectionHeader.module.cjs +3 -0
- package/dist/components/SectionHeader/SectionHeader.module.cjs.map +1 -0
- package/dist/components/SectionHeader/SectionHeader.module.js +15 -0
- package/dist/components/SectionHeader/SectionHeader.module.js.map +1 -0
- package/dist/components/Timeline/Timeline.cjs +1 -1
- package/dist/components/Timeline/Timeline.cjs.map +1 -1
- package/dist/components/Timeline/Timeline.js +22 -15
- package/dist/components/Timeline/Timeline.js.map +1 -1
- package/dist/components/Timeline/Timeline.module.cjs +1 -1
- package/dist/components/Timeline/Timeline.module.cjs.map +1 -1
- package/dist/components/Timeline/Timeline.module.js +1 -1
- package/dist/components/Timeline/Timeline.module.js.map +1 -1
- package/dist/components/Tour/Tour.module.cjs.map +1 -1
- package/dist/components/Tour/Tour.module.js.map +1 -1
- package/dist/components/Transfer/Transfer.module.cjs.map +1 -1
- package/dist/components/Transfer/Transfer.module.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.module.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.module.js.map +1 -1
- package/dist/geo/TrajectoryMap.module.cjs.map +1 -1
- package/dist/geo/TrajectoryMap.module.js.map +1 -1
- package/dist/icons/IconPicker.module.cjs.map +1 -1
- package/dist/icons/IconPicker.module.js.map +1 -1
- package/dist/styles/Reveal.css +2 -0
- package/dist/styles/SectionHeader.css +2 -0
- package/dist/styles/actions.css +26 -24
- package/dist/styles/base.css +3 -1
- package/dist/styles/br.css +3 -3
- package/dist/styles/chat.css +5 -5
- package/dist/styles/component/AIChat.css +3 -3
- package/dist/styles/component/AppBar.css +1 -0
- package/dist/styles/component/AppShell.css +1 -1
- package/dist/styles/component/BrazilMap.css +2 -2
- package/dist/styles/component/Button.css +25 -23
- package/dist/styles/component/Carousel.css +3 -3
- package/dist/styles/component/Chat.css +2 -2
- package/dist/styles/component/Dropzone.css +2 -1
- package/dist/styles/component/FilterBar.css +2 -2
- package/dist/styles/component/FloatingActionButton.css +1 -1
- package/dist/styles/component/IconPicker.css +1 -1
- package/dist/styles/component/ImageCropper.css +2 -2
- package/dist/styles/component/ListTile.css +1 -1
- package/dist/styles/component/Markdown.css +1 -1
- package/dist/styles/component/Menubar.css +1 -1
- package/dist/styles/component/Modal.css +2 -2
- package/dist/styles/component/MunicipalitySearch.css +1 -1
- package/dist/styles/component/Navbar.css +1 -0
- package/dist/styles/component/NavigationMenu.css +1 -1
- package/dist/styles/component/NavigationRail.css +1 -1
- package/dist/styles/component/NotificationCenter.css +3 -3
- package/dist/styles/component/Page.css +0 -6
- package/dist/styles/component/PasswordInput.css +1 -1
- package/dist/styles/component/PinInput.css +1 -1
- package/dist/styles/component/Reveal.css +9 -0
- package/dist/styles/component/SectionHeader.css +9 -0
- package/dist/styles/component/Timeline.css +5 -5
- package/dist/styles/component/Tour.css +2 -2
- package/dist/styles/component/TrajectoryMap.css +1 -1
- package/dist/styles/component/Transfer.css +2 -2
- package/dist/styles/component/VirtualTable.css +2 -2
- package/dist/styles/core.css +3 -1
- package/dist/styles/data.css +15 -15
- package/dist/styles/feedback.css +3 -3
- package/dist/styles/forms.css +6 -5
- package/dist/styles/geo.css +1 -1
- package/dist/styles/icons.css +1 -1
- package/dist/styles/inverse.css +372 -0
- package/dist/styles/layout.css +9 -7
- package/dist/styles/manifest.json +14 -1
- package/dist/styles/navigation.css +8 -6
- package/dist/styles/overlay.css +2 -2
- package/dist/styles/utility.css +8 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +473 -11
- package/dist/tempest-react-sdk.js +350 -346
- package/dist/theme/create-theme.cjs +3 -3
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js +72 -55
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/theme/inverse-render.cjs +3 -0
- package/dist/theme/inverse-render.cjs.map +1 -0
- package/dist/theme/inverse-render.js +10 -0
- package/dist/theme/inverse-render.js.map +1 -0
- package/dist/theme/inverse-restore.cjs +5 -0
- package/dist/theme/inverse-restore.cjs.map +1 -0
- package/dist/theme/inverse-restore.js +30 -0
- package/dist/theme/inverse-restore.js.map +1 -0
- package/dist/theme/inverse-surface.cjs +2 -0
- package/dist/theme/inverse-surface.cjs.map +1 -0
- package/dist/theme/inverse-surface.js +255 -0
- package/dist/theme/inverse-surface.js.map +1 -0
- package/dist/theme/status-tokens.cjs +2 -0
- package/dist/theme/status-tokens.cjs.map +1 -0
- package/dist/theme/status-tokens.js +15 -0
- package/dist/theme/status-tokens.js.map +1 -0
- package/dist/vite/create-vite-config.cjs +1 -1
- package/dist/vite/create-vite-config.cjs.map +1 -1
- package/dist/vite/create-vite-config.js +6 -4
- package/dist/vite/create-vite-config.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { cloneElement as t, isValidElement as n, useMemo as r, version as i } from "react";
|
|
3
|
+
//#region src/components/Button/slot.ts
|
|
4
|
+
var a = Number.parseInt(i, 10);
|
|
5
|
+
function o(...e) {
|
|
6
|
+
return (t) => {
|
|
7
|
+
for (let n of e) typeof n == "function" ? n(t) : n && (n.current = t);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function s(e) {
|
|
11
|
+
return a >= 19 ? e.props.ref : e.ref;
|
|
12
|
+
}
|
|
13
|
+
function c(t, n) {
|
|
14
|
+
return typeof n == "function" ? (...r) => e(t, n(...r)) : e(t, n);
|
|
15
|
+
}
|
|
16
|
+
function l(e, t) {
|
|
17
|
+
let n = {
|
|
18
|
+
...t,
|
|
19
|
+
...e
|
|
20
|
+
};
|
|
21
|
+
for (let r of Object.keys(e)) {
|
|
22
|
+
let i = e[r], a = t[r];
|
|
23
|
+
/^on[A-Z]/.test(r) && typeof i == "function" && typeof a == "function" && (n[r] = (e) => {
|
|
24
|
+
i(e), e.defaultPrevented || a(e);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return n.className = c(e.className, t.className), e.style && (n.style = {
|
|
28
|
+
...t.style,
|
|
29
|
+
...e.style
|
|
30
|
+
}), n;
|
|
31
|
+
}
|
|
32
|
+
function u({ child: e, slotProps: i, slotRef: a, content: c }) {
|
|
33
|
+
let u = n(e) ? s(e) : void 0, d = r(() => o(u, a), [u, a]);
|
|
34
|
+
if (!n(e)) throw Error("ButtonSlot expects exactly one React element as its child.");
|
|
35
|
+
let f = l(i, e.props);
|
|
36
|
+
return f.ref = d, t(e, f, c);
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { u as Slot, o as composeRefs, s as getElementRef, l as mergeSlotProps };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=slot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot.js","names":[],"sources":["../../../src/components/Button/slot.ts"],"sourcesContent":["import { cloneElement, isValidElement, useMemo, version } from \"react\";\nimport type { CSSProperties, ReactElement, ReactNode, Ref, RefCallback, RefObject } from \"react\";\nimport { cn } from \"@/utils/cn\";\n\n/** Props a slot hands to the element it renders through. */\nexport type SlotProps = Record<string, unknown> & {\n className?: string;\n style?: CSSProperties;\n};\n\n/**\n * Major version of the React running the page.\n *\n * React 19 moved an element's `ref` into `element.props.ref` and turned the old\n * `element.ref` into a getter that logs a deprecation warning; React 18 only has\n * `element.ref`. Reading the version once picks the right field without touching\n * the deprecated one.\n */\nconst REACT_MAJOR: number = Number.parseInt(version, 10);\n\n/**\n * Combine several refs into one callback ref, so a single DOM node reaches all of\n * them — the ref the consumer put on the child and the one forwarded to `Button`.\n *\n * @param refs - Callback refs, ref objects, `null` or `undefined`, in any mix.\n * @returns A callback ref that assigns the node to every ref it was given.\n */\nexport function composeRefs<T>(...refs: Array<Ref<T> | undefined>): RefCallback<T> {\n return (node: T | null): void => {\n for (const ref of refs) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as RefObject<T | null>).current = node;\n }\n };\n}\n\n/**\n * Read the `ref` a React element was created with, on React 18 and 19 alike.\n *\n * @param element - The element whose ref is wanted.\n * @returns The element's ref, or `undefined` when it has none.\n */\nexport function getElementRef(element: ReactElement): Ref<unknown> | undefined {\n if (REACT_MAJOR >= 19) return (element.props as { ref?: Ref<unknown> }).ref;\n return (element as unknown as { ref?: Ref<unknown> }).ref;\n}\n\n/**\n * Join the slot's class with the child's, which may be a function of state.\n *\n * react-router's `NavLink` takes `className` as `({ isActive }) => string`.\n * Stringifying that would drop the active class, so a function stays a function\n * and gets the slot's class prepended to whatever it returns.\n *\n * @param slotClass - The slot's class string.\n * @param childClass - The child's `className`: a string, a function or absent.\n * @returns A string, or a function of the same shape as the child's.\n */\nfunction mergeClassName(slotClass: string | undefined, childClass: unknown): unknown {\n if (typeof childClass === \"function\") {\n return (...args: unknown[]): string =>\n cn(slotClass, childClass(...args) as string | undefined);\n }\n return cn(slotClass, childClass as string | undefined);\n}\n\n/**\n * Merge the slot's props over the child's own props.\n *\n * The rules, and why each one is that way round:\n *\n * - **Plain props: the slot wins.** The slot is `ButtonSlot`, and the props it adds\n * carry its invariants — `aria-disabled`, `tabIndex={-1}` and the click blocker\n * of a disabled link. A child prop overriding them would make a disabled link\n * navigate, which is the bug the props exist to prevent.\n * - **`className` is joined**, slot first, so the child keeps its own classes.\n * A function `className` (react-router's `NavLink`) stays a function.\n * - **`style` is merged** when the slot sets one, slot keys over child keys, for\n * the same reason as the plain props. Without a slot `style` the child's stays\n * untouched, function or not.\n * - **Event handlers (`on[A-Z]…`) both run**: the slot's first, then the child's,\n * unless the slot's handler called `preventDefault()`. That is what lets a\n * disabled `ButtonSlot` stop a react-router `Link`: the `Link` runs the `onClick` it\n * was given and then skips its own navigation when the event is already\n * prevented.\n *\n * @param slotProps - Props the slot applies.\n * @param childProps - Props the child element was created with.\n * @returns The merged props for `cloneElement`.\n */\nexport function mergeSlotProps(\n slotProps: SlotProps,\n childProps: Record<string, unknown>,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...childProps, ...slotProps };\n\n for (const key of Object.keys(slotProps)) {\n const slotValue = slotProps[key];\n const childValue = childProps[key];\n if (\n /^on[A-Z]/.test(key) &&\n typeof slotValue === \"function\" &&\n typeof childValue === \"function\"\n ) {\n merged[key] = (event: { defaultPrevented?: boolean }): void => {\n slotValue(event);\n if (!event.defaultPrevented) childValue(event);\n };\n }\n }\n\n merged.className = mergeClassName(slotProps.className, childProps.className);\n if (slotProps.style) {\n merged.style = { ...(childProps.style as CSSProperties | undefined), ...slotProps.style };\n }\n return merged;\n}\n\n/** Props of {@link Slot}. */\nexport interface SlotComponentProps {\n /** The single element the consumer passed as `children`. */\n child: ReactNode;\n /** Props to merge over the child's own (see {@link mergeSlotProps}). */\n slotProps: SlotProps;\n /** Ref forwarded to the slot, composed with the child's own ref. */\n slotRef?: Ref<HTMLElement>;\n /** What to render inside the child, in place of its children. */\n content: ReactNode;\n}\n\n/**\n * Render `child` with the slot's props, ref and content — the `asChild` pattern behind `ButtonSlot`.\n *\n * Kept local to `Button` instead of taking `@radix-ui/react-slot`: the whole\n * mechanism is a prop merge and a ref composition, and a dependency for that\n * would put its version bounds on every consumer of the SDK.\n *\n * The composed ref is memoised on the two refs, so a re-render with the same refs\n * does not detach and re-attach them.\n *\n * @param props - See {@link SlotComponentProps}.\n * @returns The cloned element.\n * @throws {Error} When `child` is not a single React element.\n */\nexport function Slot({ child, slotProps, slotRef, content }: SlotComponentProps): ReactElement {\n const childRef = isValidElement(child)\n ? (getElementRef(child) as Ref<HTMLElement> | undefined)\n : undefined;\n const ref = useMemo(() => composeRefs(childRef, slotRef), [childRef, slotRef]);\n if (!isValidElement(child)) {\n throw new Error(\"ButtonSlot expects exactly one React element as its child.\");\n }\n const merged = mergeSlotProps(slotProps, child.props as Record<string, unknown>);\n merged.ref = ref;\n return cloneElement(child, merged, content);\n}\n"],"mappings":";;;AAkBA,IAAM,IAAsB,OAAO,SAAS,GAAS,EAAE;AASvD,SAAgB,EAAe,GAAG,GAAiD;CAC/E,QAAQ,MAAyB;EAC7B,KAAK,IAAM,KAAO,GACd,AAAI,OAAO,KAAQ,aAAY,EAAI,CAAI,IAC9B,MAAK,EAA6B,UAAU;CAE7D;AACJ;AAQA,SAAgB,EAAc,GAAiD;CAE3E,OADI,KAAe,KAAY,EAAQ,MAAiC,MAChE,EAA8C;AAC1D;AAaA,SAAS,EAAe,GAA+B,GAA8B;CAKjF,OAJI,OAAO,KAAe,cACd,GAAG,MACP,EAAG,GAAW,EAAW,GAAG,CAAI,CAAuB,IAExD,EAAG,GAAW,CAAgC;AACzD;AA0BA,SAAgB,EACZ,GACA,GACuB;CACvB,IAAM,IAAkC;EAAE,GAAG;EAAY,GAAG;CAAU;CAEtE,KAAK,IAAM,KAAO,OAAO,KAAK,CAAS,GAAG;EACtC,IAAM,IAAY,EAAU,IACtB,IAAa,EAAW;EAC9B,AACI,WAAW,KAAK,CAAG,KACnB,OAAO,KAAc,cACrB,OAAO,KAAe,eAEtB,EAAO,MAAQ,MAAgD;GAE3D,AADA,EAAU,CAAK,GACV,EAAM,oBAAkB,EAAW,CAAK;EACjD;CAER;CAMA,OAJA,EAAO,YAAY,EAAe,EAAU,WAAW,EAAW,SAAS,GACvE,EAAU,UACV,EAAO,QAAQ;EAAE,GAAI,EAAW;EAAqC,GAAG,EAAU;CAAM,IAErF;AACX;AA4BA,SAAgB,EAAK,EAAE,UAAO,cAAW,YAAS,cAA6C;CAC3F,IAAM,IAAW,EAAe,CAAK,IAC9B,EAAc,CAAK,IACpB,KAAA,GACA,IAAM,QAAc,EAAY,GAAU,CAAO,GAAG,CAAC,GAAU,CAAO,CAAC;CAC7E,IAAI,CAAC,EAAe,CAAK,GACrB,MAAU,MAAM,4DAA4D;CAEhF,IAAM,IAAS,EAAe,GAAW,EAAM,KAAgC;CAE/E,OADA,EAAO,MAAM,GACN,EAAa,GAAO,GAAQ,CAAO;AAC9C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Carousel.module.cjs","names":[],"sources":["../../../src/components/Carousel/Carousel.module.css"],"sourcesContent":[".root {\n position: relative;\n width: 100%;\n font-family: var(--tempest-font-sans);\n}\n\n.root:focus-visible {\n outline:
|
|
1
|
+
{"version":3,"file":"Carousel.module.cjs","names":[],"sources":["../../../src/components/Carousel/Carousel.module.css"],"sourcesContent":[".root {\n position: relative;\n width: 100%;\n font-family: var(--tempest-font-sans);\n}\n\n.root:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: var(--tempest-focus-ring-offset);\n}\n\n.viewport {\n overflow: hidden;\n border-radius: var(--tempest-radius-lg);\n}\n\n.track {\n display: flex;\n transition: transform var(--tempest-duration-base) var(--tempest-ease-out);\n}\n\n.slide {\n flex: 0 0 100%;\n min-width: 0;\n}\n\n.arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full);\n box-shadow: var(--tempest-shadow-md);\n color: var(--tempest-text);\n font-size: var(--tempest-text-xl);\n line-height: 1;\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .arrow:hover:not(:disabled) {\n background-color: var(--tempest-surface);\n }\n}\n\n.arrow:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: var(--tempest-focus-ring-offset);\n}\n\n.arrow:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.prev {\n left: var(--tempest-space-2);\n}\n\n.next {\n right: var(--tempest-space-2);\n}\n\n.dots {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--tempest-space-2);\n margin-top: var(--tempest-space-3);\n}\n\n.dot {\n width: 0.5rem;\n height: 0.5rem;\n padding: 0;\n background-color: var(--tempest-border);\n border: none;\n border-radius: var(--tempest-radius-full);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n.dotActive {\n background-color: var(--tempest-primary);\n}\n\n.dot:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: var(--tempest-focus-ring-offset);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .track {\n transition: none;\n }\n}\n"],"mappings":";"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Carousel.module.js","names":[],"sources":["../../../src/components/Carousel/Carousel.module.css"],"sourcesContent":[".root {\n position: relative;\n width: 100%;\n font-family: var(--tempest-font-sans);\n}\n\n.root:focus-visible {\n outline:
|
|
1
|
+
{"version":3,"file":"Carousel.module.js","names":[],"sources":["../../../src/components/Carousel/Carousel.module.css"],"sourcesContent":[".root {\n position: relative;\n width: 100%;\n font-family: var(--tempest-font-sans);\n}\n\n.root:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: var(--tempest-focus-ring-offset);\n}\n\n.viewport {\n overflow: hidden;\n border-radius: var(--tempest-radius-lg);\n}\n\n.track {\n display: flex;\n transition: transform var(--tempest-duration-base) var(--tempest-ease-out);\n}\n\n.slide {\n flex: 0 0 100%;\n min-width: 0;\n}\n\n.arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full);\n box-shadow: var(--tempest-shadow-md);\n color: var(--tempest-text);\n font-size: var(--tempest-text-xl);\n line-height: 1;\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .arrow:hover:not(:disabled) {\n background-color: var(--tempest-surface);\n }\n}\n\n.arrow:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: var(--tempest-focus-ring-offset);\n}\n\n.arrow:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.prev {\n left: var(--tempest-space-2);\n}\n\n.next {\n right: var(--tempest-space-2);\n}\n\n.dots {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--tempest-space-2);\n margin-top: var(--tempest-space-3);\n}\n\n.dot {\n width: 0.5rem;\n height: 0.5rem;\n padding: 0;\n background-color: var(--tempest-border);\n border: none;\n border-radius: var(--tempest-radius-full);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n.dotActive {\n background-color: var(--tempest-primary);\n}\n\n.dot:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: var(--tempest-focus-ring-offset);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .track {\n transition: none;\n }\n}\n"],"mappings":";"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chat.module.cjs","names":[],"sources":["../../../src/components/Chat/Chat.module.css"],"sourcesContent":[".panel {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n height: var(--tempest-chat-height, 100%);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.header {\n flex: 0 0 auto;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.thread {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--tempest-space-3);\n min-height: 0;\n padding: var(--tempest-space-4);\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.thread:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -2px;\n}\n\n.day {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n}\n\n.day::before,\n.day::after {\n flex: 1 1 auto;\n height: 1px;\n background-color: var(--tempest-border);\n content: \"\";\n}\n\n.dayLabel {\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n text-transform: capitalize;\n}\n\n.run {\n display: flex;\n gap: var(--tempest-space-3);\n align-items: flex-start;\n max-width: 100%;\n}\n\n/*\n * Own messages mirror the row instead of moving to a second column: the avatar,\n * the name and the bubbles keep the same source order, so a screen reader still\n * reads \"who, then what\" while the eye reads it from the other side.\n */\n.ownRun {\n flex-direction: row-reverse;\n}\n\n.avatar {\n flex: 0 0 auto;\n}\n\n.runBody {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n min-width: 0;\n max-width: min(78%, 42rem);\n}\n\n.ownRun .runBody {\n align-items: flex-end;\n}\n\n.author {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n}\n\n.bubbles {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.bubbleRow {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.ownRun .bubbleRow {\n flex-direction: row-reverse;\n}\n\n.bubble {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n}\n\n.ownBubble {\n border-color: transparent;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n}\n\n/*\n * A failed message keeps its text readable and states the failure at the border\n * and in the meta row. Tinting the whole bubble danger-red would make the message\n * itself hard to read — and re-reading it is exactly what somebody does before\n * deciding to retry.\n */\n.failedBubble {\n border-color: var(--tempest-danger);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-text);\n}\n\n.body {\n overflow-wrap: anywhere;\n white-space: pre-wrap;\n}\n\n/*\n * The timestamp uses the muted token, not the subtle one, and switches to the\n * bubble's own foreground when the bubble is tinted.\n *\n * `--tempest-text-subtle` is solved against the page and surface backgrounds. On\n * `--tempest-primary-soft` it is a different composite, and the real browser\n * measured it below the 4.5:1 text floor — the same trap the syntax tokens hit.\n * Size is what de-emphasises the meta row here; colour still has to pass.\n */\n.meta {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.ownBubble .meta {\n color: var(--tempest-primary-on-soft);\n}\n\n.status {\n display: inline-flex;\n align-items: center;\n}\n\n.statusFailed {\n color: var(--tempest-danger);\n font-weight: var(--tempest-weight-bold);\n}\n\n/*\n * A bubble row carries more than a bubble now: the actions button sits beside it\n * and the reaction chips hang under it, so the row wraps and the chips are given\n * the whole width to fall onto.\n */\n.bubbleRow {\n flex-wrap: wrap;\n}\n\n.reactions {\n display: flex;\n flex-basis: 100%;\n flex-wrap: wrap;\n gap: var(--tempest-space-1);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.ownRun .reactions {\n justify-content: flex-end;\n}\n\n.reaction {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-1);\n padding: 0 var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full, 999px);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-xs);\n line-height: 1.8;\n cursor: pointer;\n}\n\n.reaction:disabled {\n cursor: default;\n}\n\n/*\n * Your own reaction is marked by a tinted chip and its own foreground, never by\n * `--tempest-text-subtle` over the tint: that token is solved against the page\n * background and fails the text floor on a tinted surface.\n */\n.reactionMine {\n border-color: transparent;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-weight: var(--tempest-weight-medium);\n}\n\n/*\n * The ⋮ button is always in the DOM — on touch there is no hover to reveal it,\n * and a control that only appears on hover is a control a phone does not have.\n * A fine pointer gets it faded until the row is hovered or it is focused.\n */\n.actionsButton {\n flex: 0 0 auto;\n padding: 0 var(--tempest-space-1);\n border: none;\n background: none;\n color: var(--tempest-text-muted);\n font: inherit;\n line-height: 1;\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .actionsButton {\n opacity: 0;\n transition: var(--tempest-transition-opacity, opacity 120ms ease);\n }\n\n .bubbleRow:hover .actionsButton,\n .actionsButton:focus-visible {\n opacity: 1;\n }\n}\n\n/* The reply stub: a tinted strip with a bar on the side it is anchored to. */\n.quote {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border-left: 3px solid var(--tempest-primary);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n font-size: var(--tempest-text-xs);\n text-align: left;\n}\n\n.quoteButton {\n border-top: none;\n border-right: none;\n border-bottom: none;\n font: inherit;\n font-size: var(--tempest-text-xs);\n cursor: pointer;\n}\n\n/*\n * The author of a quote is text, so it is coloured like text.\n *\n * `--tempest-primary` here measured **4.35:1** against `--tempest-surface-2` in a\n * real browser — below the 4.5:1 floor for a 12px label, and invisible to the\n * jsdom axe sweep, which disables `color-contrast` because nothing is painted.\n * The brand still marks the quote: it is the bar down the side, which is a\n * non-text indicator and only owes 3:1.\n */\n.quoteAuthor {\n color: var(--tempest-text);\n font-weight: var(--tempest-weight-medium);\n}\n\n.quoteText {\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n line-clamp: 2;\n overflow-wrap: anywhere;\n}\n\n/*\n * A revoked quote reads as absent, which is the point: the text of a deleted\n * message must not survive inside the reply to it.\n */\n.quoteRevoked {\n color: var(--tempest-text-muted);\n font-style: italic;\n}\n\n.tombstone {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-style: italic;\n}\n\n.deletedBubble {\n border-style: dashed;\n background-color: transparent;\n}\n\n.edited {\n font-style: italic;\n}\n\n/* Read by everyone is the only state that turns the ticks. */\n.statusRead {\n color: var(--tempest-info, var(--tempest-primary));\n}\n\n.attachments {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n max-width: 320px;\n}\n\n/* Holds the row's height while the attachments chunk is still loading. */\n.attachmentsFallback {\n min-height: var(--tempest-space-8, 2rem);\n}\n\n.imageAttachment {\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-md);\n background: none;\n line-height: 0;\n cursor: pointer;\n}\n\n.imageAttachment img {\n max-width: 100%;\n max-height: 260px;\n border-radius: var(--tempest-radius-md);\n object-fit: cover;\n}\n\n.videoAttachment {\n max-width: 100%;\n border-radius: var(--tempest-radius-md);\n}\n\n.voiceAttachment {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n}\n\n/*\n * The waveform is the difference between a voice note and a grey rectangle: it\n * is what tells somebody whether this is a word or a two-minute monologue.\n */\n.waveform {\n display: flex;\n align-items: flex-end;\n gap: 2px;\n height: 28px;\n}\n\n.waveform span {\n flex: 1 1 auto;\n min-width: 2px;\n border-radius: 1px;\n background-color: var(--tempest-primary);\n opacity: 0.7;\n}\n\n.fileAttachment {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n color: var(--tempest-text);\n text-decoration: none;\n}\n\n.fileName {\n overflow-wrap: anywhere;\n font-weight: var(--tempest-weight-medium);\n}\n\n.fileMeta {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--tempest-danger);\n font: inherit;\n font-size: var(--tempest-text-xs);\n text-decoration: underline;\n cursor: pointer;\n}\n\n.typing {\n flex: 0 0 auto;\n margin: 0;\n padding: 0 var(--tempest-space-4) var(--tempest-space-2);\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n}\n\n.composer {\n display: flex;\n flex: 0 0 auto;\n align-items: flex-end;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n\n.field {\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-bg);\n color: inherit;\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: none;\n}\n\n.field:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -1px;\n}\n\n.field:disabled {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.composerActions {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.send {\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.send:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n@media (max-width: 480px) {\n .runBody {\n max-width: 88%;\n }\n}\n"],"mappings":";"}
|
|
1
|
+
{"version":3,"file":"Chat.module.cjs","names":[],"sources":["../../../src/components/Chat/Chat.module.css"],"sourcesContent":[".panel {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n height: var(--tempest-chat-height, 100%);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.header {\n flex: 0 0 auto;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.thread {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--tempest-space-3);\n min-height: 0;\n padding: var(--tempest-space-4);\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.thread:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: calc(var(--tempest-focus-ring-width) * -1);\n}\n\n.day {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n}\n\n.day::before,\n.day::after {\n flex: 1 1 auto;\n height: 1px;\n background-color: var(--tempest-border);\n content: \"\";\n}\n\n.dayLabel {\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n text-transform: capitalize;\n}\n\n.run {\n display: flex;\n gap: var(--tempest-space-3);\n align-items: flex-start;\n max-width: 100%;\n}\n\n/*\n * Own messages mirror the row instead of moving to a second column: the avatar,\n * the name and the bubbles keep the same source order, so a screen reader still\n * reads \"who, then what\" while the eye reads it from the other side.\n */\n.ownRun {\n flex-direction: row-reverse;\n}\n\n.avatar {\n flex: 0 0 auto;\n}\n\n.runBody {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n min-width: 0;\n max-width: min(78%, 42rem);\n}\n\n.ownRun .runBody {\n align-items: flex-end;\n}\n\n.author {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n}\n\n.bubbles {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.bubbleRow {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.ownRun .bubbleRow {\n flex-direction: row-reverse;\n}\n\n.bubble {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n}\n\n.ownBubble {\n border-color: transparent;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n}\n\n/*\n * A failed message keeps its text readable and states the failure at the border\n * and in the meta row. Tinting the whole bubble danger-red would make the message\n * itself hard to read — and re-reading it is exactly what somebody does before\n * deciding to retry.\n */\n.failedBubble {\n border-color: var(--tempest-danger);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-text);\n}\n\n.body {\n overflow-wrap: anywhere;\n white-space: pre-wrap;\n}\n\n/*\n * The timestamp uses the muted token, not the subtle one, and switches to the\n * bubble's own foreground when the bubble is tinted.\n *\n * `--tempest-text-subtle` is solved against the page and surface backgrounds. On\n * `--tempest-primary-soft` it is a different composite, and the real browser\n * measured it below the 4.5:1 text floor — the same trap the syntax tokens hit.\n * Size is what de-emphasises the meta row here; colour still has to pass.\n */\n.meta {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.ownBubble .meta {\n color: var(--tempest-primary-on-soft);\n}\n\n.status {\n display: inline-flex;\n align-items: center;\n}\n\n.statusFailed {\n color: var(--tempest-danger);\n font-weight: var(--tempest-weight-bold);\n}\n\n/*\n * A bubble row carries more than a bubble now: the actions button sits beside it\n * and the reaction chips hang under it, so the row wraps and the chips are given\n * the whole width to fall onto.\n */\n.bubbleRow {\n flex-wrap: wrap;\n}\n\n.reactions {\n display: flex;\n flex-basis: 100%;\n flex-wrap: wrap;\n gap: var(--tempest-space-1);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.ownRun .reactions {\n justify-content: flex-end;\n}\n\n.reaction {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-1);\n padding: 0 var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full, 999px);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-xs);\n line-height: 1.8;\n cursor: pointer;\n}\n\n.reaction:disabled {\n cursor: default;\n}\n\n/*\n * Your own reaction is marked by a tinted chip and its own foreground, never by\n * `--tempest-text-subtle` over the tint: that token is solved against the page\n * background and fails the text floor on a tinted surface.\n */\n.reactionMine {\n border-color: transparent;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-weight: var(--tempest-weight-medium);\n}\n\n/*\n * The ⋮ button is always in the DOM — on touch there is no hover to reveal it,\n * and a control that only appears on hover is a control a phone does not have.\n * A fine pointer gets it faded until the row is hovered or it is focused.\n */\n.actionsButton {\n flex: 0 0 auto;\n padding: 0 var(--tempest-space-1);\n border: none;\n background: none;\n color: var(--tempest-text-muted);\n font: inherit;\n line-height: 1;\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .actionsButton {\n opacity: 0;\n transition: var(--tempest-transition-opacity, opacity 120ms ease);\n }\n\n .bubbleRow:hover .actionsButton,\n .actionsButton:focus-visible {\n opacity: 1;\n }\n}\n\n/* The reply stub: a tinted strip with a bar on the side it is anchored to. */\n.quote {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border-left: 3px solid var(--tempest-primary);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n font-size: var(--tempest-text-xs);\n text-align: left;\n}\n\n.quoteButton {\n border-top: none;\n border-right: none;\n border-bottom: none;\n font: inherit;\n font-size: var(--tempest-text-xs);\n cursor: pointer;\n}\n\n/*\n * The author of a quote is text, so it is coloured like text.\n *\n * `--tempest-primary` here measured **4.35:1** against `--tempest-surface-2` in a\n * real browser — below the 4.5:1 floor for a 12px label, and invisible to the\n * jsdom axe sweep, which disables `color-contrast` because nothing is painted.\n * The brand still marks the quote: it is the bar down the side, which is a\n * non-text indicator and only owes 3:1.\n */\n.quoteAuthor {\n color: var(--tempest-text);\n font-weight: var(--tempest-weight-medium);\n}\n\n.quoteText {\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n line-clamp: 2;\n overflow-wrap: anywhere;\n}\n\n/*\n * A revoked quote reads as absent, which is the point: the text of a deleted\n * message must not survive inside the reply to it.\n */\n.quoteRevoked {\n color: var(--tempest-text-muted);\n font-style: italic;\n}\n\n.tombstone {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-style: italic;\n}\n\n.deletedBubble {\n border-style: dashed;\n background-color: transparent;\n}\n\n.edited {\n font-style: italic;\n}\n\n/* Read by everyone is the only state that turns the ticks. */\n.statusRead {\n color: var(--tempest-info, var(--tempest-primary));\n}\n\n.attachments {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n max-width: 320px;\n}\n\n/* Holds the row's height while the attachments chunk is still loading. */\n.attachmentsFallback {\n min-height: var(--tempest-space-8, 2rem);\n}\n\n.imageAttachment {\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-md);\n background: none;\n line-height: 0;\n cursor: pointer;\n}\n\n.imageAttachment img {\n max-width: 100%;\n max-height: 260px;\n border-radius: var(--tempest-radius-md);\n object-fit: cover;\n}\n\n.videoAttachment {\n max-width: 100%;\n border-radius: var(--tempest-radius-md);\n}\n\n.voiceAttachment {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n}\n\n/*\n * The waveform is the difference between a voice note and a grey rectangle: it\n * is what tells somebody whether this is a word or a two-minute monologue.\n */\n.waveform {\n display: flex;\n align-items: flex-end;\n gap: 2px;\n height: 28px;\n}\n\n.waveform span {\n flex: 1 1 auto;\n min-width: 2px;\n border-radius: 1px;\n background-color: var(--tempest-primary);\n opacity: 0.7;\n}\n\n.fileAttachment {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n color: var(--tempest-text);\n text-decoration: none;\n}\n\n.fileName {\n overflow-wrap: anywhere;\n font-weight: var(--tempest-weight-medium);\n}\n\n.fileMeta {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--tempest-danger);\n font: inherit;\n font-size: var(--tempest-text-xs);\n text-decoration: underline;\n cursor: pointer;\n}\n\n.typing {\n flex: 0 0 auto;\n margin: 0;\n padding: 0 var(--tempest-space-4) var(--tempest-space-2);\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n}\n\n.composer {\n display: flex;\n flex: 0 0 auto;\n align-items: flex-end;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n\n.field {\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-bg);\n color: inherit;\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: none;\n}\n\n.field:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: calc(var(--tempest-focus-ring-width) * -1);\n}\n\n.field:disabled {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.composerActions {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.send {\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.send:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n@media (max-width: 480px) {\n .runBody {\n max-width: 88%;\n }\n}\n"],"mappings":";"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chat.module.js","names":[],"sources":["../../../src/components/Chat/Chat.module.css"],"sourcesContent":[".panel {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n height: var(--tempest-chat-height, 100%);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.header {\n flex: 0 0 auto;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.thread {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--tempest-space-3);\n min-height: 0;\n padding: var(--tempest-space-4);\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.thread:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -2px;\n}\n\n.day {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n}\n\n.day::before,\n.day::after {\n flex: 1 1 auto;\n height: 1px;\n background-color: var(--tempest-border);\n content: \"\";\n}\n\n.dayLabel {\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n text-transform: capitalize;\n}\n\n.run {\n display: flex;\n gap: var(--tempest-space-3);\n align-items: flex-start;\n max-width: 100%;\n}\n\n/*\n * Own messages mirror the row instead of moving to a second column: the avatar,\n * the name and the bubbles keep the same source order, so a screen reader still\n * reads \"who, then what\" while the eye reads it from the other side.\n */\n.ownRun {\n flex-direction: row-reverse;\n}\n\n.avatar {\n flex: 0 0 auto;\n}\n\n.runBody {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n min-width: 0;\n max-width: min(78%, 42rem);\n}\n\n.ownRun .runBody {\n align-items: flex-end;\n}\n\n.author {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n}\n\n.bubbles {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.bubbleRow {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.ownRun .bubbleRow {\n flex-direction: row-reverse;\n}\n\n.bubble {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n}\n\n.ownBubble {\n border-color: transparent;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n}\n\n/*\n * A failed message keeps its text readable and states the failure at the border\n * and in the meta row. Tinting the whole bubble danger-red would make the message\n * itself hard to read — and re-reading it is exactly what somebody does before\n * deciding to retry.\n */\n.failedBubble {\n border-color: var(--tempest-danger);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-text);\n}\n\n.body {\n overflow-wrap: anywhere;\n white-space: pre-wrap;\n}\n\n/*\n * The timestamp uses the muted token, not the subtle one, and switches to the\n * bubble's own foreground when the bubble is tinted.\n *\n * `--tempest-text-subtle` is solved against the page and surface backgrounds. On\n * `--tempest-primary-soft` it is a different composite, and the real browser\n * measured it below the 4.5:1 text floor — the same trap the syntax tokens hit.\n * Size is what de-emphasises the meta row here; colour still has to pass.\n */\n.meta {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.ownBubble .meta {\n color: var(--tempest-primary-on-soft);\n}\n\n.status {\n display: inline-flex;\n align-items: center;\n}\n\n.statusFailed {\n color: var(--tempest-danger);\n font-weight: var(--tempest-weight-bold);\n}\n\n/*\n * A bubble row carries more than a bubble now: the actions button sits beside it\n * and the reaction chips hang under it, so the row wraps and the chips are given\n * the whole width to fall onto.\n */\n.bubbleRow {\n flex-wrap: wrap;\n}\n\n.reactions {\n display: flex;\n flex-basis: 100%;\n flex-wrap: wrap;\n gap: var(--tempest-space-1);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.ownRun .reactions {\n justify-content: flex-end;\n}\n\n.reaction {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-1);\n padding: 0 var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full, 999px);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-xs);\n line-height: 1.8;\n cursor: pointer;\n}\n\n.reaction:disabled {\n cursor: default;\n}\n\n/*\n * Your own reaction is marked by a tinted chip and its own foreground, never by\n * `--tempest-text-subtle` over the tint: that token is solved against the page\n * background and fails the text floor on a tinted surface.\n */\n.reactionMine {\n border-color: transparent;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-weight: var(--tempest-weight-medium);\n}\n\n/*\n * The ⋮ button is always in the DOM — on touch there is no hover to reveal it,\n * and a control that only appears on hover is a control a phone does not have.\n * A fine pointer gets it faded until the row is hovered or it is focused.\n */\n.actionsButton {\n flex: 0 0 auto;\n padding: 0 var(--tempest-space-1);\n border: none;\n background: none;\n color: var(--tempest-text-muted);\n font: inherit;\n line-height: 1;\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .actionsButton {\n opacity: 0;\n transition: var(--tempest-transition-opacity, opacity 120ms ease);\n }\n\n .bubbleRow:hover .actionsButton,\n .actionsButton:focus-visible {\n opacity: 1;\n }\n}\n\n/* The reply stub: a tinted strip with a bar on the side it is anchored to. */\n.quote {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border-left: 3px solid var(--tempest-primary);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n font-size: var(--tempest-text-xs);\n text-align: left;\n}\n\n.quoteButton {\n border-top: none;\n border-right: none;\n border-bottom: none;\n font: inherit;\n font-size: var(--tempest-text-xs);\n cursor: pointer;\n}\n\n/*\n * The author of a quote is text, so it is coloured like text.\n *\n * `--tempest-primary` here measured **4.35:1** against `--tempest-surface-2` in a\n * real browser — below the 4.5:1 floor for a 12px label, and invisible to the\n * jsdom axe sweep, which disables `color-contrast` because nothing is painted.\n * The brand still marks the quote: it is the bar down the side, which is a\n * non-text indicator and only owes 3:1.\n */\n.quoteAuthor {\n color: var(--tempest-text);\n font-weight: var(--tempest-weight-medium);\n}\n\n.quoteText {\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n line-clamp: 2;\n overflow-wrap: anywhere;\n}\n\n/*\n * A revoked quote reads as absent, which is the point: the text of a deleted\n * message must not survive inside the reply to it.\n */\n.quoteRevoked {\n color: var(--tempest-text-muted);\n font-style: italic;\n}\n\n.tombstone {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-style: italic;\n}\n\n.deletedBubble {\n border-style: dashed;\n background-color: transparent;\n}\n\n.edited {\n font-style: italic;\n}\n\n/* Read by everyone is the only state that turns the ticks. */\n.statusRead {\n color: var(--tempest-info, var(--tempest-primary));\n}\n\n.attachments {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n max-width: 320px;\n}\n\n/* Holds the row's height while the attachments chunk is still loading. */\n.attachmentsFallback {\n min-height: var(--tempest-space-8, 2rem);\n}\n\n.imageAttachment {\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-md);\n background: none;\n line-height: 0;\n cursor: pointer;\n}\n\n.imageAttachment img {\n max-width: 100%;\n max-height: 260px;\n border-radius: var(--tempest-radius-md);\n object-fit: cover;\n}\n\n.videoAttachment {\n max-width: 100%;\n border-radius: var(--tempest-radius-md);\n}\n\n.voiceAttachment {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n}\n\n/*\n * The waveform is the difference between a voice note and a grey rectangle: it\n * is what tells somebody whether this is a word or a two-minute monologue.\n */\n.waveform {\n display: flex;\n align-items: flex-end;\n gap: 2px;\n height: 28px;\n}\n\n.waveform span {\n flex: 1 1 auto;\n min-width: 2px;\n border-radius: 1px;\n background-color: var(--tempest-primary);\n opacity: 0.7;\n}\n\n.fileAttachment {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n color: var(--tempest-text);\n text-decoration: none;\n}\n\n.fileName {\n overflow-wrap: anywhere;\n font-weight: var(--tempest-weight-medium);\n}\n\n.fileMeta {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--tempest-danger);\n font: inherit;\n font-size: var(--tempest-text-xs);\n text-decoration: underline;\n cursor: pointer;\n}\n\n.typing {\n flex: 0 0 auto;\n margin: 0;\n padding: 0 var(--tempest-space-4) var(--tempest-space-2);\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n}\n\n.composer {\n display: flex;\n flex: 0 0 auto;\n align-items: flex-end;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n\n.field {\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-bg);\n color: inherit;\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: none;\n}\n\n.field:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -1px;\n}\n\n.field:disabled {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.composerActions {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.send {\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.send:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n@media (max-width: 480px) {\n .runBody {\n max-width: 88%;\n }\n}\n"],"mappings":";"}
|
|
1
|
+
{"version":3,"file":"Chat.module.js","names":[],"sources":["../../../src/components/Chat/Chat.module.css"],"sourcesContent":[".panel {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n height: var(--tempest-chat-height, 100%);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.header {\n flex: 0 0 auto;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.thread {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--tempest-space-3);\n min-height: 0;\n padding: var(--tempest-space-4);\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.thread:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: calc(var(--tempest-focus-ring-width) * -1);\n}\n\n.day {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n}\n\n.day::before,\n.day::after {\n flex: 1 1 auto;\n height: 1px;\n background-color: var(--tempest-border);\n content: \"\";\n}\n\n.dayLabel {\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n text-transform: capitalize;\n}\n\n.run {\n display: flex;\n gap: var(--tempest-space-3);\n align-items: flex-start;\n max-width: 100%;\n}\n\n/*\n * Own messages mirror the row instead of moving to a second column: the avatar,\n * the name and the bubbles keep the same source order, so a screen reader still\n * reads \"who, then what\" while the eye reads it from the other side.\n */\n.ownRun {\n flex-direction: row-reverse;\n}\n\n.avatar {\n flex: 0 0 auto;\n}\n\n.runBody {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n min-width: 0;\n max-width: min(78%, 42rem);\n}\n\n.ownRun .runBody {\n align-items: flex-end;\n}\n\n.author {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n}\n\n.bubbles {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.bubbleRow {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.ownRun .bubbleRow {\n flex-direction: row-reverse;\n}\n\n.bubble {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n}\n\n.ownBubble {\n border-color: transparent;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n}\n\n/*\n * A failed message keeps its text readable and states the failure at the border\n * and in the meta row. Tinting the whole bubble danger-red would make the message\n * itself hard to read — and re-reading it is exactly what somebody does before\n * deciding to retry.\n */\n.failedBubble {\n border-color: var(--tempest-danger);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-text);\n}\n\n.body {\n overflow-wrap: anywhere;\n white-space: pre-wrap;\n}\n\n/*\n * The timestamp uses the muted token, not the subtle one, and switches to the\n * bubble's own foreground when the bubble is tinted.\n *\n * `--tempest-text-subtle` is solved against the page and surface backgrounds. On\n * `--tempest-primary-soft` it is a different composite, and the real browser\n * measured it below the 4.5:1 text floor — the same trap the syntax tokens hit.\n * Size is what de-emphasises the meta row here; colour still has to pass.\n */\n.meta {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.ownBubble .meta {\n color: var(--tempest-primary-on-soft);\n}\n\n.status {\n display: inline-flex;\n align-items: center;\n}\n\n.statusFailed {\n color: var(--tempest-danger);\n font-weight: var(--tempest-weight-bold);\n}\n\n/*\n * A bubble row carries more than a bubble now: the actions button sits beside it\n * and the reaction chips hang under it, so the row wraps and the chips are given\n * the whole width to fall onto.\n */\n.bubbleRow {\n flex-wrap: wrap;\n}\n\n.reactions {\n display: flex;\n flex-basis: 100%;\n flex-wrap: wrap;\n gap: var(--tempest-space-1);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.ownRun .reactions {\n justify-content: flex-end;\n}\n\n.reaction {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-1);\n padding: 0 var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full, 999px);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-xs);\n line-height: 1.8;\n cursor: pointer;\n}\n\n.reaction:disabled {\n cursor: default;\n}\n\n/*\n * Your own reaction is marked by a tinted chip and its own foreground, never by\n * `--tempest-text-subtle` over the tint: that token is solved against the page\n * background and fails the text floor on a tinted surface.\n */\n.reactionMine {\n border-color: transparent;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-weight: var(--tempest-weight-medium);\n}\n\n/*\n * The ⋮ button is always in the DOM — on touch there is no hover to reveal it,\n * and a control that only appears on hover is a control a phone does not have.\n * A fine pointer gets it faded until the row is hovered or it is focused.\n */\n.actionsButton {\n flex: 0 0 auto;\n padding: 0 var(--tempest-space-1);\n border: none;\n background: none;\n color: var(--tempest-text-muted);\n font: inherit;\n line-height: 1;\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .actionsButton {\n opacity: 0;\n transition: var(--tempest-transition-opacity, opacity 120ms ease);\n }\n\n .bubbleRow:hover .actionsButton,\n .actionsButton:focus-visible {\n opacity: 1;\n }\n}\n\n/* The reply stub: a tinted strip with a bar on the side it is anchored to. */\n.quote {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border-left: 3px solid var(--tempest-primary);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n font-size: var(--tempest-text-xs);\n text-align: left;\n}\n\n.quoteButton {\n border-top: none;\n border-right: none;\n border-bottom: none;\n font: inherit;\n font-size: var(--tempest-text-xs);\n cursor: pointer;\n}\n\n/*\n * The author of a quote is text, so it is coloured like text.\n *\n * `--tempest-primary` here measured **4.35:1** against `--tempest-surface-2` in a\n * real browser — below the 4.5:1 floor for a 12px label, and invisible to the\n * jsdom axe sweep, which disables `color-contrast` because nothing is painted.\n * The brand still marks the quote: it is the bar down the side, which is a\n * non-text indicator and only owes 3:1.\n */\n.quoteAuthor {\n color: var(--tempest-text);\n font-weight: var(--tempest-weight-medium);\n}\n\n.quoteText {\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n line-clamp: 2;\n overflow-wrap: anywhere;\n}\n\n/*\n * A revoked quote reads as absent, which is the point: the text of a deleted\n * message must not survive inside the reply to it.\n */\n.quoteRevoked {\n color: var(--tempest-text-muted);\n font-style: italic;\n}\n\n.tombstone {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-style: italic;\n}\n\n.deletedBubble {\n border-style: dashed;\n background-color: transparent;\n}\n\n.edited {\n font-style: italic;\n}\n\n/* Read by everyone is the only state that turns the ticks. */\n.statusRead {\n color: var(--tempest-info, var(--tempest-primary));\n}\n\n.attachments {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n max-width: 320px;\n}\n\n/* Holds the row's height while the attachments chunk is still loading. */\n.attachmentsFallback {\n min-height: var(--tempest-space-8, 2rem);\n}\n\n.imageAttachment {\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-md);\n background: none;\n line-height: 0;\n cursor: pointer;\n}\n\n.imageAttachment img {\n max-width: 100%;\n max-height: 260px;\n border-radius: var(--tempest-radius-md);\n object-fit: cover;\n}\n\n.videoAttachment {\n max-width: 100%;\n border-radius: var(--tempest-radius-md);\n}\n\n.voiceAttachment {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n}\n\n/*\n * The waveform is the difference between a voice note and a grey rectangle: it\n * is what tells somebody whether this is a word or a two-minute monologue.\n */\n.waveform {\n display: flex;\n align-items: flex-end;\n gap: 2px;\n height: 28px;\n}\n\n.waveform span {\n flex: 1 1 auto;\n min-width: 2px;\n border-radius: 1px;\n background-color: var(--tempest-primary);\n opacity: 0.7;\n}\n\n.fileAttachment {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n color: var(--tempest-text);\n text-decoration: none;\n}\n\n.fileName {\n overflow-wrap: anywhere;\n font-weight: var(--tempest-weight-medium);\n}\n\n.fileMeta {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--tempest-danger);\n font: inherit;\n font-size: var(--tempest-text-xs);\n text-decoration: underline;\n cursor: pointer;\n}\n\n.typing {\n flex: 0 0 auto;\n margin: 0;\n padding: 0 var(--tempest-space-4) var(--tempest-space-2);\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n}\n\n.composer {\n display: flex;\n flex: 0 0 auto;\n align-items: flex-end;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n\n.field {\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-bg);\n color: inherit;\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: none;\n}\n\n.field:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: calc(var(--tempest-focus-ring-width) * -1);\n}\n\n.field:disabled {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.composerActions {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.send {\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.send:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n@media (max-width: 480px) {\n .runBody {\n max-width: 88%;\n }\n}\n"],"mappings":";"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../utils/cn.cjs"),t=require("./Collapsible.module.cjs");let n=require("react"),r=require("react/jsx-runtime");function i({open:i,defaultOpen:a=!1,onOpenChange:o,trigger:s,className:c,children:l,...u}){let d=(0,n.useId)(),f=`${d}-content`,p=`${d}-trigger`,m=i!==void 0,[h,g]=(0,n.useState)(a),_=m?i:h,v=(0,n.useCallback)(()=>{let e=!_;m||g(e),o?.(e)},[_,m,o]);return(0,r.jsxs)(`div`,{className:e.cn(t.default.collapsible,
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("./Collapsible.module.cjs");let n=require("react"),r=require("react/jsx-runtime");function i({open:i,defaultOpen:a=!1,onOpenChange:o,trigger:s,className:c,children:l,...u}){let d=(0,n.useId)(),f=`${d}-content`,p=`${d}-trigger`,m=i!==void 0,[h,g]=(0,n.useState)(a),_=m?i:h,v=(0,n.useCallback)(()=>{let e=!_;m||g(e),o?.(e)},[_,m,o]);return(0,r.jsxs)(`div`,{className:e.cn(t.default.collapsible,c),...u,children:[(0,r.jsx)(`button`,{type:`button`,id:p,className:t.default.trigger,"aria-expanded":_,"aria-controls":f,onClick:v,children:s}),(0,r.jsx)(`div`,{id:f,role:`region`,"aria-labelledby":p,hidden:!_,className:t.default.content,children:l})]})}exports.Collapsible=i;
|
|
2
2
|
//# sourceMappingURL=Collapsible.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Collapsible.cjs","names":[],"sources":["../../../src/components/Collapsible/Collapsible.tsx"],"sourcesContent":["import { useCallback, useId, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Collapsible.module.css\";\n\nexport interface CollapsibleProps extends HTMLAttributes<HTMLDivElement> {\n /** Controlled open state. When provided the component is controlled. */\n open?: boolean;\n /** Initial open state for the uncontrolled variant. Default `false`. */\n defaultOpen?: boolean;\n /** Fired with the next open state whenever the trigger is activated. */\n onOpenChange?: (open: boolean) => void;\n /** Content rendered inside the trigger button. */\n trigger: ReactNode;\n /** Collapsible content, hidden while closed. */\n children: ReactNode;\n}\n\n/**\n * A single expand/collapse region — a lighter alternative to `Accordion` for\n * one toggleable block. The trigger is a `<button aria-expanded aria-controls>`\n * wired to a content region sharing the same id; the region is `hidden` while\n * closed. Works controlled (`open` + `onOpenChange`) or uncontrolled\n * (`defaultOpen`).\n *\n * @param props - {@link CollapsibleProps}.\n * @returns The collapsible container.\n */\nexport function Collapsible({\n open,\n defaultOpen = false,\n onOpenChange,\n trigger,\n className,\n children,\n ...props\n}: CollapsibleProps) {\n const reactId = useId();\n const contentId = `${reactId}-content`;\n const triggerId = `${reactId}-trigger`;\n\n const isControlled = open !== undefined;\n const [internalOpen, setInternalOpen] = useState<boolean>(defaultOpen);\n const isOpen = isControlled ? open : internalOpen;\n\n const handleToggle = useCallback((): void => {\n const next = !isOpen;\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n }, [isOpen, isControlled, onOpenChange]);\n\n return (\n <div className={cn(styles.collapsible,
|
|
1
|
+
{"version":3,"file":"Collapsible.cjs","names":[],"sources":["../../../src/components/Collapsible/Collapsible.tsx"],"sourcesContent":["import { useCallback, useId, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Collapsible.module.css\";\n\nexport interface CollapsibleProps extends HTMLAttributes<HTMLDivElement> {\n /** Controlled open state. When provided the component is controlled. */\n open?: boolean;\n /** Initial open state for the uncontrolled variant. Default `false`. */\n defaultOpen?: boolean;\n /** Fired with the next open state whenever the trigger is activated. */\n onOpenChange?: (open: boolean) => void;\n /** Content rendered inside the trigger button. */\n trigger: ReactNode;\n /** Collapsible content, hidden while closed. */\n children: ReactNode;\n}\n\n/**\n * A single expand/collapse region — a lighter alternative to `Accordion` for\n * one toggleable block. The trigger is a `<button aria-expanded aria-controls>`\n * wired to a content region sharing the same id; the region is `hidden` while\n * closed. Works controlled (`open` + `onOpenChange`) or uncontrolled\n * (`defaultOpen`).\n *\n * @param props - {@link CollapsibleProps}.\n * @returns The collapsible container.\n */\nexport function Collapsible({\n open,\n defaultOpen = false,\n onOpenChange,\n trigger,\n className,\n children,\n ...props\n}: CollapsibleProps) {\n const reactId = useId();\n const contentId = `${reactId}-content`;\n const triggerId = `${reactId}-trigger`;\n\n const isControlled = open !== undefined;\n const [internalOpen, setInternalOpen] = useState<boolean>(defaultOpen);\n const isOpen = isControlled ? open : internalOpen;\n\n const handleToggle = useCallback((): void => {\n const next = !isOpen;\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n }, [isOpen, isControlled, onOpenChange]);\n\n return (\n <div className={cn(styles.collapsible, className)} {...props}>\n <button\n type=\"button\"\n id={triggerId}\n className={styles.trigger}\n aria-expanded={isOpen}\n aria-controls={contentId}\n onClick={handleToggle}\n >\n {trigger}\n </button>\n <div\n id={contentId}\n role=\"region\"\n aria-labelledby={triggerId}\n hidden={!isOpen}\n className={styles.content}\n >\n {children}\n </div>\n </div>\n );\n}\n"],"mappings":"kIA4BA,SAAgB,EAAY,CACxB,OACA,cAAc,GACd,eACA,UACA,YACA,WACA,GAAG,GACc,CACjB,IAAM,GAAA,EAAU,EAAA,MAAA,CAAM,EAChB,EAAY,GAAG,EAAQ,UACvB,EAAY,GAAG,EAAQ,UAEvB,EAAe,IAAS,IAAA,GACxB,CAAC,EAAc,IAAA,EAAmB,EAAA,SAAA,CAAkB,CAAW,EAC/D,EAAS,EAAe,EAAO,EAE/B,GAAA,EAAe,EAAA,YAAA,KAAwB,CACzC,IAAM,EAAO,CAAC,EACT,GAAc,EAAgB,CAAI,EACvC,IAAe,CAAI,CACvB,EAAG,CAAC,EAAQ,EAAc,CAAY,CAAC,EAEvC,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,YAAa,CAAS,EAAG,GAAI,EAAvD,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,GAAI,EACJ,UAAW,EAAA,QAAO,QAClB,gBAAe,EACf,gBAAe,EACf,QAAS,EAER,SAAA,CACG,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,GAAI,EACJ,KAAK,SACL,kBAAiB,EACjB,OAAQ,CAAC,EACT,UAAW,EAAA,QAAO,QAEjB,UACA,CAAA,CACJ,GAEb"}
|
|
@@ -13,7 +13,7 @@ function s({ open: s, defaultOpen: c = !1, onOpenChange: l, trigger: u, classNam
|
|
|
13
13
|
l
|
|
14
14
|
]);
|
|
15
15
|
return /* @__PURE__ */ o("div", {
|
|
16
|
-
className: e(t.collapsible,
|
|
16
|
+
className: e(t.collapsible, d),
|
|
17
17
|
...p,
|
|
18
18
|
children: [/* @__PURE__ */ a("button", {
|
|
19
19
|
type: "button",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Collapsible.js","names":[],"sources":["../../../src/components/Collapsible/Collapsible.tsx"],"sourcesContent":["import { useCallback, useId, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Collapsible.module.css\";\n\nexport interface CollapsibleProps extends HTMLAttributes<HTMLDivElement> {\n /** Controlled open state. When provided the component is controlled. */\n open?: boolean;\n /** Initial open state for the uncontrolled variant. Default `false`. */\n defaultOpen?: boolean;\n /** Fired with the next open state whenever the trigger is activated. */\n onOpenChange?: (open: boolean) => void;\n /** Content rendered inside the trigger button. */\n trigger: ReactNode;\n /** Collapsible content, hidden while closed. */\n children: ReactNode;\n}\n\n/**\n * A single expand/collapse region — a lighter alternative to `Accordion` for\n * one toggleable block. The trigger is a `<button aria-expanded aria-controls>`\n * wired to a content region sharing the same id; the region is `hidden` while\n * closed. Works controlled (`open` + `onOpenChange`) or uncontrolled\n * (`defaultOpen`).\n *\n * @param props - {@link CollapsibleProps}.\n * @returns The collapsible container.\n */\nexport function Collapsible({\n open,\n defaultOpen = false,\n onOpenChange,\n trigger,\n className,\n children,\n ...props\n}: CollapsibleProps) {\n const reactId = useId();\n const contentId = `${reactId}-content`;\n const triggerId = `${reactId}-trigger`;\n\n const isControlled = open !== undefined;\n const [internalOpen, setInternalOpen] = useState<boolean>(defaultOpen);\n const isOpen = isControlled ? open : internalOpen;\n\n const handleToggle = useCallback((): void => {\n const next = !isOpen;\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n }, [isOpen, isControlled, onOpenChange]);\n\n return (\n <div className={cn(styles.collapsible,
|
|
1
|
+
{"version":3,"file":"Collapsible.js","names":[],"sources":["../../../src/components/Collapsible/Collapsible.tsx"],"sourcesContent":["import { useCallback, useId, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Collapsible.module.css\";\n\nexport interface CollapsibleProps extends HTMLAttributes<HTMLDivElement> {\n /** Controlled open state. When provided the component is controlled. */\n open?: boolean;\n /** Initial open state for the uncontrolled variant. Default `false`. */\n defaultOpen?: boolean;\n /** Fired with the next open state whenever the trigger is activated. */\n onOpenChange?: (open: boolean) => void;\n /** Content rendered inside the trigger button. */\n trigger: ReactNode;\n /** Collapsible content, hidden while closed. */\n children: ReactNode;\n}\n\n/**\n * A single expand/collapse region — a lighter alternative to `Accordion` for\n * one toggleable block. The trigger is a `<button aria-expanded aria-controls>`\n * wired to a content region sharing the same id; the region is `hidden` while\n * closed. Works controlled (`open` + `onOpenChange`) or uncontrolled\n * (`defaultOpen`).\n *\n * @param props - {@link CollapsibleProps}.\n * @returns The collapsible container.\n */\nexport function Collapsible({\n open,\n defaultOpen = false,\n onOpenChange,\n trigger,\n className,\n children,\n ...props\n}: CollapsibleProps) {\n const reactId = useId();\n const contentId = `${reactId}-content`;\n const triggerId = `${reactId}-trigger`;\n\n const isControlled = open !== undefined;\n const [internalOpen, setInternalOpen] = useState<boolean>(defaultOpen);\n const isOpen = isControlled ? open : internalOpen;\n\n const handleToggle = useCallback((): void => {\n const next = !isOpen;\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n }, [isOpen, isControlled, onOpenChange]);\n\n return (\n <div className={cn(styles.collapsible, className)} {...props}>\n <button\n type=\"button\"\n id={triggerId}\n className={styles.trigger}\n aria-expanded={isOpen}\n aria-controls={contentId}\n onClick={handleToggle}\n >\n {trigger}\n </button>\n <div\n id={contentId}\n role=\"region\"\n aria-labelledby={triggerId}\n hidden={!isOpen}\n className={styles.content}\n >\n {children}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AA4BA,SAAgB,EAAY,EACxB,SACA,iBAAc,IACd,iBACA,YACA,cACA,aACA,GAAG,KACc;CACjB,IAAM,IAAU,EAAM,GAChB,IAAY,GAAG,EAAQ,WACvB,IAAY,GAAG,EAAQ,WAEvB,IAAe,MAAS,KAAA,GACxB,CAAC,GAAc,KAAmB,EAAkB,CAAW,GAC/D,IAAS,IAAe,IAAO,GAE/B,IAAe,QAAwB;EACzC,IAAM,IAAO,CAAC;EAEd,AADK,KAAc,EAAgB,CAAI,GACvC,IAAe,CAAI;CACvB,GAAG;EAAC;EAAQ;EAAc;CAAY,CAAC;CAEvC,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,aAAa,CAAS;EAAG,GAAI;EAAvD,UAAA,CACI,kBAAC,UAAD;GACI,MAAK;GACL,IAAI;GACJ,WAAW,EAAO;GAClB,iBAAe;GACf,iBAAe;GACf,SAAS;GAER,UAAA;EACG,CAAA,GACR,kBAAC,OAAD;GACI,IAAI;GACJ,MAAK;GACL,mBAAiB;GACjB,QAAQ,CAAC;GACT,WAAW,EAAO;GAEjB;EACA,CAAA,CACJ;;AAEb"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../utils/cn.cjs"),t=require("../Portal/Portal.cjs"),n=require("../Portal/anchor-position.cjs"),r=require("../Select/CaretIcon.cjs"),i=require("./Combobox.module.cjs");let
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../Portal/Portal.cjs"),n=require("../Portal/anchor-position.cjs"),r=require("../Select/CaretIcon.cjs"),i=require("./Combobox.module.cjs"),a=require("./use-listbox-dismiss.cjs");let o=require("react"),s=require("react/jsx-runtime");var c=4;function l(e,t){return!t||e.label.toLowerCase().includes(t.toLowerCase())}function u({options:u,value:d,onChange:f,label:p,placeholder:m=`Selecione`,helperText:h,error:g,disabled:_,filter:v=l,emptyMessage:y=`Nenhuma opção encontrada`,portal:b=!0,className:x}){let S=(0,o.useId)(),C=(0,o.useRef)(null),w=(0,o.useRef)(null),[T,E]=(0,o.useState)(!1),[D,O]=(0,o.useState)(``),[k,A]=(0,o.useState)(0),[j,M]=(0,o.useState)(null),[N,P]=(0,o.useState)(null),F=n.useAnchorPosition({anchor:j,floating:N,side:`bottom`,align:`start`,offset:c,enabled:b&&T,matchWidth:!0}),I=(0,o.useMemo)(()=>u.find(e=>e.value===d)?.label??``,[u,d]),L=(0,o.useMemo)(()=>u.filter(e=>v(e,D)),[u,v,D]),R=(0,o.useCallback)(()=>{E(!1),O(``),A(0)},[]),z=(0,o.useCallback)(e=>{e.disabled||(f(e.value),R())},[f,R]),{onInputBlur:B,onListMouseDown:V}=a.useListboxDismiss({open:T,rootRef:C,listNode:N,onDismiss:R}),H=e=>`${S}-option-${e}`,U=T?L[k]:void 0,W=e=>{if(e.key===`ArrowDown`)e.preventDefault(),E(!0),A(e=>Math.min(L.length-1,e+1));else if(e.key===`ArrowUp`)e.preventDefault(),A(e=>Math.max(0,e-1));else if(e.key===`Enter`){e.preventDefault();let t=L[k];t&&z(t)}else e.key===`Escape`&&(T&&e.preventDefault(),R())},G=(0,s.jsx)(`ul`,{ref:P,id:`${S}-listbox`,role:`listbox`,className:e.cn(i.default.menu,b&&i.default.portalled),style:F,onMouseDown:V,children:L.length===0?(0,s.jsx)(`li`,{className:i.default.empty,children:y}):L.map((t,n)=>(0,s.jsx)(`li`,{id:H(n),role:`option`,"aria-selected":t.value===d,className:e.cn(i.default.option,n===k&&i.default.active,t.value===d&&i.default.selected),onMouseEnter:()=>A(n),onMouseDown:()=>z(t),children:t.label},t.value))});return(0,s.jsxs)(`div`,{ref:C,className:e.cn(i.default.wrapper,g&&i.default.error,x),children:[p&&(0,s.jsx)(`label`,{htmlFor:S,className:i.default.label,children:p}),(0,s.jsxs)(`div`,{ref:M,className:i.default.field,children:[(0,s.jsx)(`input`,{id:S,ref:w,type:`text`,role:`combobox`,"aria-expanded":T,"aria-controls":`${S}-listbox`,"aria-autocomplete":`list`,"aria-activedescendant":U?H(k):void 0,className:i.default.input,placeholder:m,disabled:_,value:T?D:I,onFocus:()=>E(!0),onChange:e=>{O(e.target.value),E(!0),A(0)},onKeyDown:W,onBlur:B}),(0,s.jsx)(`span`,{className:i.default.caret,"aria-hidden":!0,children:(0,s.jsx)(r.CaretIcon,{})})]}),T&&(b?(0,s.jsx)(t.Portal,{children:G}):G),g?(0,s.jsx)(`span`,{className:i.default.errorText,children:g}):h?(0,s.jsx)(`span`,{className:i.default.helper,children:h}):null]})}exports.Combobox=u;
|
|
2
2
|
//# sourceMappingURL=Combobox.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Combobox.cjs","names":[],"sources":["../../../src/components/Combobox/Combobox.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — options/value/onChange\n * plus the shared field contract (label, placeholder, helperText, error, disabled,\n * className) and the combobox-specific knobs: filter, emptyMessage, portal. The body\n * is the listbox — open state, active descendant, type-ahead and the ARIA wiring —\n * and those cannot be separated without passing the same active index back and\n * forth.\n */\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { Portal } from \"@/components/Portal\";\nimport { useAnchorPosition } from \"@/components/Portal/anchor-position\";\nimport { CaretIcon } from \"@/components/Select/CaretIcon\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Combobox.module.css\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface ComboboxProps {\n options: ComboboxOption[];\n value: string;\n onChange: (value: string) => void;\n label?: string;\n placeholder?: string;\n helperText?: string;\n error?: string;\n disabled?: boolean;\n /** Custom filter — return true to keep the option. Default is case-insensitive substring match on label. */\n filter?: (option: ComboboxOption, query: string) => boolean;\n /** Message shown when no option matches. */\n emptyMessage?: string;\n /**\n * Render the list in a portal, positioned under the field. Default `true`.\n *\n * In flow, an ancestor with `overflow` other than `visible` clips the list —\n * in the last row of a `DataTable` none of it was visible (measured in Chrome\n * at 1440 px). In a portal it escapes the clip, takes the field's width,\n * flips above the field when there is no room below, and follows it on\n * scroll, resize and when the field or the list changes size.\n */\n portal?: boolean;\n className?: string;\n}\n\n/** Gap between field and list, in px — the in-flow CSS uses the same 4 px. */\nconst LIST_OFFSET = 4;\n\nfunction defaultFilter(option: ComboboxOption, query: string): boolean {\n if (!query) return true;\n return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/**\n * Combobox — text input with a filterable dropdown of options.\n *\n * Selecting an option fires `onChange(value)`. Typing filters the list.\n * Keyboard: ArrowUp/ArrowDown to navigate, Enter to select, Esc to close.\n */\nexport function Combobox({\n options,\n value,\n onChange,\n label,\n placeholder = \"Selecione\",\n helperText,\n error,\n disabled,\n filter = defaultFilter,\n emptyMessage = \"Nenhuma opção encontrada\",\n portal = true,\n className,\n}: ComboboxProps) {\n const id = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n const [fieldNode, setFieldNode] = useState<HTMLDivElement | null>(null);\n const [listNode, setListNode] = useState<HTMLUListElement | null>(null);\n const floatingStyle = useAnchorPosition({\n anchor: fieldNode,\n floating: listNode,\n side: \"bottom\",\n align: \"start\",\n offset: LIST_OFFSET,\n enabled: portal && open,\n matchWidth: true,\n });\n\n const selectedLabel = useMemo(\n () => options.find((o) => o.value === value)?.label ?? \"\",\n [options, value],\n );\n\n const filtered = useMemo(\n () => options.filter((o) => filter(o, query)),\n [options, filter, query],\n );\n\n const closeAndReset = useCallback((): void => {\n setOpen(false);\n setQuery(\"\");\n setActiveIndex(0);\n }, []);\n\n const handleSelect = useCallback(\n (option: ComboboxOption): void => {\n if (option.disabled) return;\n onChange(option.value);\n closeAndReset();\n },\n [onChange, closeAndReset],\n );\n\n /**\n * Close on a press outside both the field and the list.\n *\n * The list is checked on its own because in a portal it is not inside the\n * field's wrapper: a press on its scrollbar, or on the empty message, would\n * otherwise count as outside and close it.\n */\n useEffect(() => {\n if (!open) return;\n const onDown = (event: MouseEvent): void => {\n const target = event.target as Node;\n if (rootRef.current?.contains(target) || listNode?.contains(target)) return;\n closeAndReset();\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open, closeAndReset, listNode]);\n\n /**\n * Keyboard model of the input.\n *\n * `Escape` calls `preventDefault()` only while the list is open: that marks the\n * key as consumed, so an enclosing `Modal` or `Drawer` stays open and the next\n * `Escape` reaches it. With the list closed the key is left alone and dismisses\n * the enclosing layer, as the user expects.\n */\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setOpen(true);\n setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const option = filtered[activeIndex];\n if (option) handleSelect(option);\n } else if (event.key === \"Escape\") {\n if (open) event.preventDefault();\n closeAndReset();\n }\n };\n\n const list = (\n <ul\n ref={setListNode}\n id={`${id}-listbox`}\n role=\"listbox\"\n className={cn(styles.menu, portal && styles.portalled)}\n style={floatingStyle}\n >\n {filtered.length === 0 ? (\n <li className={styles.empty}>{emptyMessage}</li>\n ) : (\n filtered.map((option, index) => (\n <li\n key={option.value}\n role=\"option\"\n aria-selected={option.value === value}\n className={cn(\n styles.option,\n index === activeIndex && styles.active,\n option.value === value && styles.selected,\n )}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseDown={(event) => {\n event.preventDefault();\n handleSelect(option);\n }}\n >\n {option.label}\n </li>\n ))\n )}\n </ul>\n );\n\n return (\n <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n {label && (\n <label htmlFor={id} className={styles.label}>\n {label}\n </label>\n )}\n <div ref={setFieldNode} className={styles.field}>\n <input\n id={id}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={`${id}-listbox`}\n aria-autocomplete=\"list\"\n className={styles.input}\n placeholder={placeholder}\n disabled={disabled}\n value={open ? query : selectedLabel}\n onFocus={() => setOpen(true)}\n onChange={(event) => {\n setQuery(event.target.value);\n setOpen(true);\n setActiveIndex(0);\n }}\n onKeyDown={handleKeyDown}\n />\n <span className={styles.caret} aria-hidden>\n <CaretIcon />\n </span>\n </div>\n {open && (portal ? <Portal>{list}</Portal> : list)}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Combobox.cjs","names":[],"sources":["../../../src/components/Combobox/Combobox.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — options/value/onChange\n * plus the shared field contract (label, placeholder, helperText, error, disabled,\n * className) and the combobox-specific knobs: filter, emptyMessage, portal. The body\n * is the listbox — open state, active descendant, type-ahead and the ARIA wiring —\n * and those cannot be separated without passing the same active index back and\n * forth.\n */\nimport { useCallback, useId, useMemo, useRef, useState } from \"react\";\nimport { Portal } from \"@/components/Portal\";\nimport { useAnchorPosition } from \"@/components/Portal/anchor-position\";\nimport { CaretIcon } from \"@/components/Select/CaretIcon\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Combobox.module.css\";\nimport { useListboxDismiss } from \"./use-listbox-dismiss\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface ComboboxProps {\n options: ComboboxOption[];\n value: string;\n onChange: (value: string) => void;\n label?: string;\n placeholder?: string;\n helperText?: string;\n error?: string;\n disabled?: boolean;\n /** Custom filter — return true to keep the option. Default is case-insensitive substring match on label. */\n filter?: (option: ComboboxOption, query: string) => boolean;\n /** Message shown when no option matches. */\n emptyMessage?: string;\n /**\n * Render the list in a portal, positioned under the field. Default `true`.\n *\n * In flow, an ancestor with `overflow` other than `visible` clips the list —\n * in the last row of a `DataTable` none of it was visible (measured in Chrome\n * at 1440 px). In a portal it escapes the clip, takes the field's width,\n * flips above the field when there is no room below, and follows it on\n * scroll, resize and when the field or the list changes size.\n */\n portal?: boolean;\n className?: string;\n}\n\n/** Gap between field and list, in px — the in-flow CSS uses the same 4 px. */\nconst LIST_OFFSET = 4;\n\nfunction defaultFilter(option: ComboboxOption, query: string): boolean {\n if (!query) return true;\n return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/**\n * Combobox — text input with a filterable dropdown of options.\n *\n * Selecting an option fires `onChange(value)`. Typing filters the list.\n * Keyboard: ArrowUp/ArrowDown to navigate — the active option is announced\n * through `aria-activedescendant`, focus stays on the input — Enter to select,\n * Esc to close, and Tab/Shift+Tab leave the field closing the list.\n */\nexport function Combobox({\n options,\n value,\n onChange,\n label,\n placeholder = \"Selecione\",\n helperText,\n error,\n disabled,\n filter = defaultFilter,\n emptyMessage = \"Nenhuma opção encontrada\",\n portal = true,\n className,\n}: ComboboxProps) {\n const id = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n const [fieldNode, setFieldNode] = useState<HTMLDivElement | null>(null);\n const [listNode, setListNode] = useState<HTMLUListElement | null>(null);\n const floatingStyle = useAnchorPosition({\n anchor: fieldNode,\n floating: listNode,\n side: \"bottom\",\n align: \"start\",\n offset: LIST_OFFSET,\n enabled: portal && open,\n matchWidth: true,\n });\n\n const selectedLabel = useMemo(\n () => options.find((o) => o.value === value)?.label ?? \"\",\n [options, value],\n );\n\n const filtered = useMemo(\n () => options.filter((o) => filter(o, query)),\n [options, filter, query],\n );\n\n const closeAndReset = useCallback((): void => {\n setOpen(false);\n setQuery(\"\");\n setActiveIndex(0);\n }, []);\n\n const handleSelect = useCallback(\n (option: ComboboxOption): void => {\n if (option.disabled) return;\n onChange(option.value);\n closeAndReset();\n },\n [onChange, closeAndReset],\n );\n\n const { onInputBlur, onListMouseDown } = useListboxDismiss({\n open,\n rootRef,\n listNode,\n onDismiss: closeAndReset,\n });\n const optionId = (index: number): string => `${id}-option-${index}`;\n const activeOption = open ? filtered[activeIndex] : undefined;\n\n /**\n * Keyboard model of the input.\n *\n * `Escape` calls `preventDefault()` only while the list is open: that marks the\n * key as consumed, so an enclosing `Modal` or `Drawer` stays open and the next\n * `Escape` reaches it. With the list closed the key is left alone and dismisses\n * the enclosing layer, as the user expects.\n */\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setOpen(true);\n setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const option = filtered[activeIndex];\n if (option) handleSelect(option);\n } else if (event.key === \"Escape\") {\n if (open) event.preventDefault();\n closeAndReset();\n }\n };\n\n const list = (\n <ul\n ref={setListNode}\n id={`${id}-listbox`}\n role=\"listbox\"\n className={cn(styles.menu, portal && styles.portalled)}\n style={floatingStyle}\n onMouseDown={onListMouseDown}\n >\n {filtered.length === 0 ? (\n <li className={styles.empty}>{emptyMessage}</li>\n ) : (\n filtered.map((option, index) => (\n <li\n key={option.value}\n id={optionId(index)}\n role=\"option\"\n aria-selected={option.value === value}\n className={cn(\n styles.option,\n index === activeIndex && styles.active,\n option.value === value && styles.selected,\n )}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseDown={() => handleSelect(option)}\n >\n {option.label}\n </li>\n ))\n )}\n </ul>\n );\n\n return (\n <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n {label && (\n <label htmlFor={id} className={styles.label}>\n {label}\n </label>\n )}\n <div ref={setFieldNode} className={styles.field}>\n <input\n id={id}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={`${id}-listbox`}\n aria-autocomplete=\"list\"\n aria-activedescendant={activeOption ? optionId(activeIndex) : undefined}\n className={styles.input}\n placeholder={placeholder}\n disabled={disabled}\n value={open ? query : selectedLabel}\n onFocus={() => setOpen(true)}\n onChange={(event) => {\n setQuery(event.target.value);\n setOpen(true);\n setActiveIndex(0);\n }}\n onKeyDown={handleKeyDown}\n onBlur={onInputBlur}\n />\n <span className={styles.caret} aria-hidden>\n <CaretIcon />\n </span>\n </div>\n {open && (portal ? <Portal>{list}</Portal> : list)}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,8 @@ import { Portal as t } from "../Portal/Portal.js";
|
|
|
3
3
|
import { useAnchorPosition as n } from "../Portal/anchor-position.js";
|
|
4
4
|
import { CaretIcon as r } from "../Select/CaretIcon.js";
|
|
5
5
|
import i from "./Combobox.module.js";
|
|
6
|
-
import {
|
|
6
|
+
import { useListboxDismiss as a } from "./use-listbox-dismiss.js";
|
|
7
|
+
import { useCallback as o, useId as s, useMemo as c, useRef as l, useState as u } from "react";
|
|
7
8
|
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
8
9
|
//#region src/components/Combobox/Combobox.tsx
|
|
9
10
|
var p = 4;
|
|
@@ -23,24 +24,16 @@ function h({ options: h, value: g, onChange: _, label: v, placeholder: y = "Sele
|
|
|
23
24
|
h,
|
|
24
25
|
C,
|
|
25
26
|
M
|
|
26
|
-
]), U =
|
|
27
|
+
]), U = o(() => {
|
|
27
28
|
j(!1), N(""), F(0);
|
|
28
|
-
}, []), W =
|
|
29
|
+
}, []), W = o((e) => {
|
|
29
30
|
e.disabled || (_(e.value), U());
|
|
30
|
-
}, [_, U])
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
};
|
|
37
|
-
return window.addEventListener("mousedown", e), () => window.removeEventListener("mousedown", e);
|
|
38
|
-
}, [
|
|
39
|
-
A,
|
|
40
|
-
U,
|
|
41
|
-
R
|
|
42
|
-
]);
|
|
43
|
-
let G = (e) => {
|
|
31
|
+
}, [_, U]), { onInputBlur: G, onListMouseDown: K } = a({
|
|
32
|
+
open: A,
|
|
33
|
+
rootRef: O,
|
|
34
|
+
listNode: R,
|
|
35
|
+
onDismiss: U
|
|
36
|
+
}), q = (e) => `${D}-option-${e}`, J = A ? H[P] : void 0, Y = (e) => {
|
|
44
37
|
if (e.key === "ArrowDown") e.preventDefault(), j(!0), F((e) => Math.min(H.length - 1, e + 1));
|
|
45
38
|
else if (e.key === "ArrowUp") e.preventDefault(), F((e) => Math.max(0, e - 1));
|
|
46
39
|
else if (e.key === "Enter") {
|
|
@@ -48,23 +41,23 @@ function h({ options: h, value: g, onChange: _, label: v, placeholder: y = "Sele
|
|
|
48
41
|
let t = H[P];
|
|
49
42
|
t && W(t);
|
|
50
43
|
} else e.key === "Escape" && (A && e.preventDefault(), U());
|
|
51
|
-
},
|
|
44
|
+
}, X = /* @__PURE__ */ d("ul", {
|
|
52
45
|
ref: z,
|
|
53
46
|
id: `${D}-listbox`,
|
|
54
47
|
role: "listbox",
|
|
55
48
|
className: e(i.menu, T && i.portalled),
|
|
56
49
|
style: B,
|
|
50
|
+
onMouseDown: K,
|
|
57
51
|
children: H.length === 0 ? /* @__PURE__ */ d("li", {
|
|
58
52
|
className: i.empty,
|
|
59
53
|
children: w
|
|
60
54
|
}) : H.map((t, n) => /* @__PURE__ */ d("li", {
|
|
55
|
+
id: q(n),
|
|
61
56
|
role: "option",
|
|
62
57
|
"aria-selected": t.value === g,
|
|
63
58
|
className: e(i.option, n === P && i.active, t.value === g && i.selected),
|
|
64
59
|
onMouseEnter: () => F(n),
|
|
65
|
-
onMouseDown: (
|
|
66
|
-
e.preventDefault(), W(t);
|
|
67
|
-
},
|
|
60
|
+
onMouseDown: () => W(t),
|
|
68
61
|
children: t.label
|
|
69
62
|
}, t.value))
|
|
70
63
|
});
|
|
@@ -88,6 +81,7 @@ function h({ options: h, value: g, onChange: _, label: v, placeholder: y = "Sele
|
|
|
88
81
|
"aria-expanded": A,
|
|
89
82
|
"aria-controls": `${D}-listbox`,
|
|
90
83
|
"aria-autocomplete": "list",
|
|
84
|
+
"aria-activedescendant": J ? q(P) : void 0,
|
|
91
85
|
className: i.input,
|
|
92
86
|
placeholder: y,
|
|
93
87
|
disabled: S,
|
|
@@ -96,14 +90,15 @@ function h({ options: h, value: g, onChange: _, label: v, placeholder: y = "Sele
|
|
|
96
90
|
onChange: (e) => {
|
|
97
91
|
N(e.target.value), j(!0), F(0);
|
|
98
92
|
},
|
|
99
|
-
onKeyDown:
|
|
93
|
+
onKeyDown: Y,
|
|
94
|
+
onBlur: G
|
|
100
95
|
}), /* @__PURE__ */ d("span", {
|
|
101
96
|
className: i.caret,
|
|
102
97
|
"aria-hidden": !0,
|
|
103
98
|
children: /* @__PURE__ */ d(r, {})
|
|
104
99
|
})]
|
|
105
100
|
}),
|
|
106
|
-
A && (T ? /* @__PURE__ */ d(t, { children:
|
|
101
|
+
A && (T ? /* @__PURE__ */ d(t, { children: X }) : X),
|
|
107
102
|
x ? /* @__PURE__ */ d("span", {
|
|
108
103
|
className: i.errorText,
|
|
109
104
|
children: x
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Combobox.js","names":[],"sources":["../../../src/components/Combobox/Combobox.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — options/value/onChange\n * plus the shared field contract (label, placeholder, helperText, error, disabled,\n * className) and the combobox-specific knobs: filter, emptyMessage, portal. The body\n * is the listbox — open state, active descendant, type-ahead and the ARIA wiring —\n * and those cannot be separated without passing the same active index back and\n * forth.\n */\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { Portal } from \"@/components/Portal\";\nimport { useAnchorPosition } from \"@/components/Portal/anchor-position\";\nimport { CaretIcon } from \"@/components/Select/CaretIcon\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Combobox.module.css\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface ComboboxProps {\n options: ComboboxOption[];\n value: string;\n onChange: (value: string) => void;\n label?: string;\n placeholder?: string;\n helperText?: string;\n error?: string;\n disabled?: boolean;\n /** Custom filter — return true to keep the option. Default is case-insensitive substring match on label. */\n filter?: (option: ComboboxOption, query: string) => boolean;\n /** Message shown when no option matches. */\n emptyMessage?: string;\n /**\n * Render the list in a portal, positioned under the field. Default `true`.\n *\n * In flow, an ancestor with `overflow` other than `visible` clips the list —\n * in the last row of a `DataTable` none of it was visible (measured in Chrome\n * at 1440 px). In a portal it escapes the clip, takes the field's width,\n * flips above the field when there is no room below, and follows it on\n * scroll, resize and when the field or the list changes size.\n */\n portal?: boolean;\n className?: string;\n}\n\n/** Gap between field and list, in px — the in-flow CSS uses the same 4 px. */\nconst LIST_OFFSET = 4;\n\nfunction defaultFilter(option: ComboboxOption, query: string): boolean {\n if (!query) return true;\n return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/**\n * Combobox — text input with a filterable dropdown of options.\n *\n * Selecting an option fires `onChange(value)`. Typing filters the list.\n * Keyboard: ArrowUp/ArrowDown to navigate, Enter to select, Esc to close.\n */\nexport function Combobox({\n options,\n value,\n onChange,\n label,\n placeholder = \"Selecione\",\n helperText,\n error,\n disabled,\n filter = defaultFilter,\n emptyMessage = \"Nenhuma opção encontrada\",\n portal = true,\n className,\n}: ComboboxProps) {\n const id = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n const [fieldNode, setFieldNode] = useState<HTMLDivElement | null>(null);\n const [listNode, setListNode] = useState<HTMLUListElement | null>(null);\n const floatingStyle = useAnchorPosition({\n anchor: fieldNode,\n floating: listNode,\n side: \"bottom\",\n align: \"start\",\n offset: LIST_OFFSET,\n enabled: portal && open,\n matchWidth: true,\n });\n\n const selectedLabel = useMemo(\n () => options.find((o) => o.value === value)?.label ?? \"\",\n [options, value],\n );\n\n const filtered = useMemo(\n () => options.filter((o) => filter(o, query)),\n [options, filter, query],\n );\n\n const closeAndReset = useCallback((): void => {\n setOpen(false);\n setQuery(\"\");\n setActiveIndex(0);\n }, []);\n\n const handleSelect = useCallback(\n (option: ComboboxOption): void => {\n if (option.disabled) return;\n onChange(option.value);\n closeAndReset();\n },\n [onChange, closeAndReset],\n );\n\n /**\n * Close on a press outside both the field and the list.\n *\n * The list is checked on its own because in a portal it is not inside the\n * field's wrapper: a press on its scrollbar, or on the empty message, would\n * otherwise count as outside and close it.\n */\n useEffect(() => {\n if (!open) return;\n const onDown = (event: MouseEvent): void => {\n const target = event.target as Node;\n if (rootRef.current?.contains(target) || listNode?.contains(target)) return;\n closeAndReset();\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open, closeAndReset, listNode]);\n\n /**\n * Keyboard model of the input.\n *\n * `Escape` calls `preventDefault()` only while the list is open: that marks the\n * key as consumed, so an enclosing `Modal` or `Drawer` stays open and the next\n * `Escape` reaches it. With the list closed the key is left alone and dismisses\n * the enclosing layer, as the user expects.\n */\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setOpen(true);\n setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const option = filtered[activeIndex];\n if (option) handleSelect(option);\n } else if (event.key === \"Escape\") {\n if (open) event.preventDefault();\n closeAndReset();\n }\n };\n\n const list = (\n <ul\n ref={setListNode}\n id={`${id}-listbox`}\n role=\"listbox\"\n className={cn(styles.menu, portal && styles.portalled)}\n style={floatingStyle}\n >\n {filtered.length === 0 ? (\n <li className={styles.empty}>{emptyMessage}</li>\n ) : (\n filtered.map((option, index) => (\n <li\n key={option.value}\n role=\"option\"\n aria-selected={option.value === value}\n className={cn(\n styles.option,\n index === activeIndex && styles.active,\n option.value === value && styles.selected,\n )}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseDown={(event) => {\n event.preventDefault();\n handleSelect(option);\n }}\n >\n {option.label}\n </li>\n ))\n )}\n </ul>\n );\n\n return (\n <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n {label && (\n <label htmlFor={id} className={styles.label}>\n {label}\n </label>\n )}\n <div ref={setFieldNode} className={styles.field}>\n <input\n id={id}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={`${id}-listbox`}\n aria-autocomplete=\"list\"\n className={styles.input}\n placeholder={placeholder}\n disabled={disabled}\n value={open ? query : selectedLabel}\n onFocus={() => setOpen(true)}\n onChange={(event) => {\n setQuery(event.target.value);\n setOpen(true);\n setActiveIndex(0);\n }}\n onKeyDown={handleKeyDown}\n />\n <span className={styles.caret} aria-hidden>\n <CaretIcon />\n </span>\n </div>\n {open && (portal ? <Portal>{list}</Portal> : list)}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Combobox.js","names":[],"sources":["../../../src/components/Combobox/Combobox.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — options/value/onChange\n * plus the shared field contract (label, placeholder, helperText, error, disabled,\n * className) and the combobox-specific knobs: filter, emptyMessage, portal. The body\n * is the listbox — open state, active descendant, type-ahead and the ARIA wiring —\n * and those cannot be separated without passing the same active index back and\n * forth.\n */\nimport { useCallback, useId, useMemo, useRef, useState } from \"react\";\nimport { Portal } from \"@/components/Portal\";\nimport { useAnchorPosition } from \"@/components/Portal/anchor-position\";\nimport { CaretIcon } from \"@/components/Select/CaretIcon\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Combobox.module.css\";\nimport { useListboxDismiss } from \"./use-listbox-dismiss\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface ComboboxProps {\n options: ComboboxOption[];\n value: string;\n onChange: (value: string) => void;\n label?: string;\n placeholder?: string;\n helperText?: string;\n error?: string;\n disabled?: boolean;\n /** Custom filter — return true to keep the option. Default is case-insensitive substring match on label. */\n filter?: (option: ComboboxOption, query: string) => boolean;\n /** Message shown when no option matches. */\n emptyMessage?: string;\n /**\n * Render the list in a portal, positioned under the field. Default `true`.\n *\n * In flow, an ancestor with `overflow` other than `visible` clips the list —\n * in the last row of a `DataTable` none of it was visible (measured in Chrome\n * at 1440 px). In a portal it escapes the clip, takes the field's width,\n * flips above the field when there is no room below, and follows it on\n * scroll, resize and when the field or the list changes size.\n */\n portal?: boolean;\n className?: string;\n}\n\n/** Gap between field and list, in px — the in-flow CSS uses the same 4 px. */\nconst LIST_OFFSET = 4;\n\nfunction defaultFilter(option: ComboboxOption, query: string): boolean {\n if (!query) return true;\n return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/**\n * Combobox — text input with a filterable dropdown of options.\n *\n * Selecting an option fires `onChange(value)`. Typing filters the list.\n * Keyboard: ArrowUp/ArrowDown to navigate — the active option is announced\n * through `aria-activedescendant`, focus stays on the input — Enter to select,\n * Esc to close, and Tab/Shift+Tab leave the field closing the list.\n */\nexport function Combobox({\n options,\n value,\n onChange,\n label,\n placeholder = \"Selecione\",\n helperText,\n error,\n disabled,\n filter = defaultFilter,\n emptyMessage = \"Nenhuma opção encontrada\",\n portal = true,\n className,\n}: ComboboxProps) {\n const id = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n const [fieldNode, setFieldNode] = useState<HTMLDivElement | null>(null);\n const [listNode, setListNode] = useState<HTMLUListElement | null>(null);\n const floatingStyle = useAnchorPosition({\n anchor: fieldNode,\n floating: listNode,\n side: \"bottom\",\n align: \"start\",\n offset: LIST_OFFSET,\n enabled: portal && open,\n matchWidth: true,\n });\n\n const selectedLabel = useMemo(\n () => options.find((o) => o.value === value)?.label ?? \"\",\n [options, value],\n );\n\n const filtered = useMemo(\n () => options.filter((o) => filter(o, query)),\n [options, filter, query],\n );\n\n const closeAndReset = useCallback((): void => {\n setOpen(false);\n setQuery(\"\");\n setActiveIndex(0);\n }, []);\n\n const handleSelect = useCallback(\n (option: ComboboxOption): void => {\n if (option.disabled) return;\n onChange(option.value);\n closeAndReset();\n },\n [onChange, closeAndReset],\n );\n\n const { onInputBlur, onListMouseDown } = useListboxDismiss({\n open,\n rootRef,\n listNode,\n onDismiss: closeAndReset,\n });\n const optionId = (index: number): string => `${id}-option-${index}`;\n const activeOption = open ? filtered[activeIndex] : undefined;\n\n /**\n * Keyboard model of the input.\n *\n * `Escape` calls `preventDefault()` only while the list is open: that marks the\n * key as consumed, so an enclosing `Modal` or `Drawer` stays open and the next\n * `Escape` reaches it. With the list closed the key is left alone and dismisses\n * the enclosing layer, as the user expects.\n */\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setOpen(true);\n setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const option = filtered[activeIndex];\n if (option) handleSelect(option);\n } else if (event.key === \"Escape\") {\n if (open) event.preventDefault();\n closeAndReset();\n }\n };\n\n const list = (\n <ul\n ref={setListNode}\n id={`${id}-listbox`}\n role=\"listbox\"\n className={cn(styles.menu, portal && styles.portalled)}\n style={floatingStyle}\n onMouseDown={onListMouseDown}\n >\n {filtered.length === 0 ? (\n <li className={styles.empty}>{emptyMessage}</li>\n ) : (\n filtered.map((option, index) => (\n <li\n key={option.value}\n id={optionId(index)}\n role=\"option\"\n aria-selected={option.value === value}\n className={cn(\n styles.option,\n index === activeIndex && styles.active,\n option.value === value && styles.selected,\n )}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseDown={() => handleSelect(option)}\n >\n {option.label}\n </li>\n ))\n )}\n </ul>\n );\n\n return (\n <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n {label && (\n <label htmlFor={id} className={styles.label}>\n {label}\n </label>\n )}\n <div ref={setFieldNode} className={styles.field}>\n <input\n id={id}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={`${id}-listbox`}\n aria-autocomplete=\"list\"\n aria-activedescendant={activeOption ? optionId(activeIndex) : undefined}\n className={styles.input}\n placeholder={placeholder}\n disabled={disabled}\n value={open ? query : selectedLabel}\n onFocus={() => setOpen(true)}\n onChange={(event) => {\n setQuery(event.target.value);\n setOpen(true);\n setActiveIndex(0);\n }}\n onKeyDown={handleKeyDown}\n onBlur={onInputBlur}\n />\n <span className={styles.caret} aria-hidden>\n <CaretIcon />\n </span>\n </div>\n {open && (portal ? <Portal>{list}</Portal> : list)}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require("react");function t(e){e.preventDefault()}function n({open:n,rootRef:r,listNode:i,onDismiss:a}){return(0,e.useEffect)(()=>{if(!n)return;let e=e=>{let t=e.target;r.current?.contains(t)||i?.contains(t)||a()};return window.addEventListener(`mousedown`,e),()=>window.removeEventListener(`mousedown`,e)},[n,r,i,a]),{onInputBlur:(0,e.useCallback)(e=>{let t=e.relatedTarget;n&&t instanceof Node&&!i?.contains(t)&&a()},[n,i,a]),onListMouseDown:t}}exports.useListboxDismiss=n;
|
|
2
|
+
//# sourceMappingURL=use-listbox-dismiss.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-listbox-dismiss.cjs","names":[],"sources":["../../../src/components/Combobox/use-listbox-dismiss.ts"],"sourcesContent":["import { useCallback, useEffect, type FocusEvent, type MouseEvent, type RefObject } from \"react\";\n\n/** Inputs of {@link useListboxDismiss}. */\nexport interface ListboxDismissOptions {\n /** Whether the list is open; nothing is listened to while it is closed. */\n open: boolean;\n /** The component's wrapper — a press inside it is not a press outside. */\n rootRef: RefObject<HTMLElement | null>;\n /** The list node, checked on its own because in a portal it lives outside `rootRef`. */\n listNode: HTMLElement | null;\n /** Closes the list. Keep it stable (`useCallback`), it is an effect dependency. */\n onDismiss: () => void;\n}\n\n/** Handlers {@link useListboxDismiss} hands back to wire on the input and the list. */\nexport interface ListboxDismissHandlers {\n /** `onBlur` of the combobox input. */\n onInputBlur: (event: FocusEvent<HTMLInputElement>) => void;\n /** `onMouseDown` of the list element. */\n onListMouseDown: (event: MouseEvent<HTMLElement>) => void;\n}\n\n/**\n * Keeps focus on the input when a press lands anywhere on the list — an option,\n * the empty message, the padding or the scrollbar.\n *\n * @param event - The `mousedown` on the list.\n */\nfunction keepInputFocus(event: MouseEvent<HTMLElement>): void {\n event.preventDefault();\n}\n\n/**\n * Closes the list of an input-driven listbox (`Combobox`, `MultiSelect`) when the\n * user leaves it, by pointer or by keyboard.\n *\n * Pointer: a press outside both the wrapper and the list closes it. The list is\n * checked on its own because in a portal it is not inside the wrapper.\n *\n * Keyboard: the list closes when focus moves from the input to an element outside\n * the list. Before this (#430, measured in Chromium headless on `main`), `Tab` and\n * `Shift+Tab` took focus to the neighbouring control and left the list open on top\n * of it — `aria-expanded=\"true\"` on a field that no longer had focus — until a\n * press outside or `Escape` on the input, which no longer received keys. A blur\n * whose `relatedTarget` is `null` (focus went to a non-focusable spot or out of\n * the page) is left to the pointer rule, so a press on the label or the helper\n * text keeps behaving as a press inside.\n *\n * A press on the list calls `preventDefault()`, so it never blurs the input: the\n * option's handler runs with focus still on the field, and a press on the empty\n * message or the scrollbar no longer drops focus to `<body>`.\n *\n * @param options - See {@link ListboxDismissOptions}.\n * @returns The input `onBlur` and the list `onMouseDown` handlers.\n */\nexport function useListboxDismiss({\n open,\n rootRef,\n listNode,\n onDismiss,\n}: ListboxDismissOptions): ListboxDismissHandlers {\n useEffect(() => {\n if (!open) return;\n const onDown = (event: globalThis.MouseEvent): void => {\n const target = event.target as Node;\n if (rootRef.current?.contains(target) || listNode?.contains(target)) return;\n onDismiss();\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open, rootRef, listNode, onDismiss]);\n\n const onInputBlur = useCallback(\n (event: FocusEvent<HTMLInputElement>): void => {\n const next = event.relatedTarget;\n if (!open || !(next instanceof Node) || listNode?.contains(next)) return;\n onDismiss();\n },\n [open, listNode, onDismiss],\n );\n\n return { onInputBlur, onListMouseDown: keepInputFocus };\n}\n"],"mappings":"uBA4BA,SAAS,EAAe,EAAsC,CAC1D,EAAM,eAAe,CACzB,CAyBA,SAAgB,EAAkB,CAC9B,OACA,UACA,WACA,aAC8C,CAqB9C,OApBA,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAM,OACX,IAAM,EAAU,GAAuC,CACnD,IAAM,EAAS,EAAM,OACjB,EAAQ,SAAS,SAAS,CAAM,GAAK,GAAU,SAAS,CAAM,GAClE,EAAU,CACd,EAEA,OADA,OAAO,iBAAiB,YAAa,CAAM,MAC9B,OAAO,oBAAoB,YAAa,CAAM,CAC/D,EAAG,CAAC,EAAM,EAAS,EAAU,CAAS,CAAC,EAWhC,CAAE,aAAA,EATW,EAAA,YAAA,CACf,GAA8C,CAC3C,IAAM,EAAO,EAAM,cACd,GAAU,aAAgB,MAAS,IAAU,SAAS,CAAI,GAC/D,EAAU,CACd,EACA,CAAC,EAAM,EAAU,CAAS,CAGrB,EAAa,gBAAiB,CAAe,CAC1D"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useCallback as e, useEffect as t } from "react";
|
|
2
|
+
//#region src/components/Combobox/use-listbox-dismiss.ts
|
|
3
|
+
function n(e) {
|
|
4
|
+
e.preventDefault();
|
|
5
|
+
}
|
|
6
|
+
function r({ open: r, rootRef: i, listNode: a, onDismiss: o }) {
|
|
7
|
+
return t(() => {
|
|
8
|
+
if (!r) return;
|
|
9
|
+
let e = (e) => {
|
|
10
|
+
let t = e.target;
|
|
11
|
+
i.current?.contains(t) || a?.contains(t) || o();
|
|
12
|
+
};
|
|
13
|
+
return window.addEventListener("mousedown", e), () => window.removeEventListener("mousedown", e);
|
|
14
|
+
}, [
|
|
15
|
+
r,
|
|
16
|
+
i,
|
|
17
|
+
a,
|
|
18
|
+
o
|
|
19
|
+
]), {
|
|
20
|
+
onInputBlur: e((e) => {
|
|
21
|
+
let t = e.relatedTarget;
|
|
22
|
+
r && t instanceof Node && !a?.contains(t) && o();
|
|
23
|
+
}, [
|
|
24
|
+
r,
|
|
25
|
+
a,
|
|
26
|
+
o
|
|
27
|
+
]),
|
|
28
|
+
onListMouseDown: n
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { r as useListboxDismiss };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=use-listbox-dismiss.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-listbox-dismiss.js","names":[],"sources":["../../../src/components/Combobox/use-listbox-dismiss.ts"],"sourcesContent":["import { useCallback, useEffect, type FocusEvent, type MouseEvent, type RefObject } from \"react\";\n\n/** Inputs of {@link useListboxDismiss}. */\nexport interface ListboxDismissOptions {\n /** Whether the list is open; nothing is listened to while it is closed. */\n open: boolean;\n /** The component's wrapper — a press inside it is not a press outside. */\n rootRef: RefObject<HTMLElement | null>;\n /** The list node, checked on its own because in a portal it lives outside `rootRef`. */\n listNode: HTMLElement | null;\n /** Closes the list. Keep it stable (`useCallback`), it is an effect dependency. */\n onDismiss: () => void;\n}\n\n/** Handlers {@link useListboxDismiss} hands back to wire on the input and the list. */\nexport interface ListboxDismissHandlers {\n /** `onBlur` of the combobox input. */\n onInputBlur: (event: FocusEvent<HTMLInputElement>) => void;\n /** `onMouseDown` of the list element. */\n onListMouseDown: (event: MouseEvent<HTMLElement>) => void;\n}\n\n/**\n * Keeps focus on the input when a press lands anywhere on the list — an option,\n * the empty message, the padding or the scrollbar.\n *\n * @param event - The `mousedown` on the list.\n */\nfunction keepInputFocus(event: MouseEvent<HTMLElement>): void {\n event.preventDefault();\n}\n\n/**\n * Closes the list of an input-driven listbox (`Combobox`, `MultiSelect`) when the\n * user leaves it, by pointer or by keyboard.\n *\n * Pointer: a press outside both the wrapper and the list closes it. The list is\n * checked on its own because in a portal it is not inside the wrapper.\n *\n * Keyboard: the list closes when focus moves from the input to an element outside\n * the list. Before this (#430, measured in Chromium headless on `main`), `Tab` and\n * `Shift+Tab` took focus to the neighbouring control and left the list open on top\n * of it — `aria-expanded=\"true\"` on a field that no longer had focus — until a\n * press outside or `Escape` on the input, which no longer received keys. A blur\n * whose `relatedTarget` is `null` (focus went to a non-focusable spot or out of\n * the page) is left to the pointer rule, so a press on the label or the helper\n * text keeps behaving as a press inside.\n *\n * A press on the list calls `preventDefault()`, so it never blurs the input: the\n * option's handler runs with focus still on the field, and a press on the empty\n * message or the scrollbar no longer drops focus to `<body>`.\n *\n * @param options - See {@link ListboxDismissOptions}.\n * @returns The input `onBlur` and the list `onMouseDown` handlers.\n */\nexport function useListboxDismiss({\n open,\n rootRef,\n listNode,\n onDismiss,\n}: ListboxDismissOptions): ListboxDismissHandlers {\n useEffect(() => {\n if (!open) return;\n const onDown = (event: globalThis.MouseEvent): void => {\n const target = event.target as Node;\n if (rootRef.current?.contains(target) || listNode?.contains(target)) return;\n onDismiss();\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open, rootRef, listNode, onDismiss]);\n\n const onInputBlur = useCallback(\n (event: FocusEvent<HTMLInputElement>): void => {\n const next = event.relatedTarget;\n if (!open || !(next instanceof Node) || listNode?.contains(next)) return;\n onDismiss();\n },\n [open, listNode, onDismiss],\n );\n\n return { onInputBlur, onListMouseDown: keepInputFocus };\n}\n"],"mappings":";;AA4BA,SAAS,EAAe,GAAsC;CAC1D,EAAM,eAAe;AACzB;AAyBA,SAAgB,EAAkB,EAC9B,SACA,YACA,aACA,gBAC8C;CAqB9C,OApBA,QAAgB;EACZ,IAAI,CAAC,GAAM;EACX,IAAM,KAAU,MAAuC;GACnD,IAAM,IAAS,EAAM;GACjB,EAAQ,SAAS,SAAS,CAAM,KAAK,GAAU,SAAS,CAAM,KAClE,EAAU;EACd;EAEA,OADA,OAAO,iBAAiB,aAAa,CAAM,SAC9B,OAAO,oBAAoB,aAAa,CAAM;CAC/D,GAAG;EAAC;EAAM;EAAS;EAAU;CAAS,CAAC,GAWhC;EAAE,aATW,GACf,MAA8C;GAC3C,IAAM,IAAO,EAAM;GACf,AAAC,KAAU,aAAgB,QAAS,IAAU,SAAS,CAAI,KAC/D,EAAU;EACd,GACA;GAAC;GAAM;GAAU;EAAS,CAGrB;EAAa,iBAAiB;CAAe;AAC1D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dropzone.module.cjs","names":[],"sources":["../../../src/components/Dropzone/Dropzone.module.css"],"sourcesContent":[".zone {\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n min-height: 120px;\n padding: var(--tempest-space-6, 24px);\n border: 2px dashed var(--tempest-border);\n border-radius: var(--tempest-radius-lg, 12px);\n background: var(--tempest-surface);\n color: var(--tempest-text-muted);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n transition:\n border-color var(--tempest-duration-fast, 120ms) var(--tempest-ease-out, ease-out),\n background var(--tempest-duration-fast, 120ms) var(--tempest-ease-out, ease-out);\n}\n\n.zone:hover,\n.zone:focus-visible {\n border-color: var(--tempest-primary);\n color: var(--tempest-text);\n outline:
|
|
1
|
+
{"version":3,"file":"Dropzone.module.cjs","names":[],"sources":["../../../src/components/Dropzone/Dropzone.module.css"],"sourcesContent":[".zone {\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n min-height: 120px;\n padding: var(--tempest-space-6, 24px);\n border: 2px dashed var(--tempest-border);\n border-radius: var(--tempest-radius-lg, 12px);\n background: var(--tempest-surface);\n color: var(--tempest-text-muted);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n transition:\n border-color var(--tempest-duration-fast, 120ms) var(--tempest-ease-out, ease-out),\n background var(--tempest-duration-fast, 120ms) var(--tempest-ease-out, ease-out);\n}\n\n.zone:hover,\n.zone:focus-visible {\n border-color: var(--tempest-primary);\n color: var(--tempest-text);\n}\n\n.zone:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: var(--tempest-focus-ring-offset);\n}\n\n.dragging {\n border-color: var(--tempest-primary);\n background: var(--tempest-primary-soft);\n color: var(--tempest-text);\n}\n\n.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.input {\n display: none;\n}\n\n.prompt {\n line-height: 1.4;\n}\n"],"mappings":";"}
|