@stll/ui 0.0.1-placeholder.0 → 0.2.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 +84 -0
- package/dist/components/accordion.d.ts +8 -0
- package/dist/components/accordion.js +38 -0
- package/dist/components/alert-dialog.d.ts +32 -0
- package/dist/components/alert-dialog.js +75 -0
- package/dist/components/avatar.d.ts +7 -0
- package/dist/components/avatar.js +22 -0
- package/dist/components/bidi-text.d.ts +24 -0
- package/dist/components/bidi-text.js +38 -0
- package/dist/components/brand-icons.d.ts +7 -0
- package/dist/components/brand-icons.js +18 -0
- package/dist/components/breadcrumb.d.ts +12 -0
- package/dist/components/breadcrumb.js +61 -0
- package/dist/components/button-variants.d.ts +17 -0
- package/dist/components/button-variants.js +43 -0
- package/dist/components/button.d.ts +20 -0
- package/dist/components/button.js +53 -0
- package/dist/components/checkbox.d.ts +5 -0
- package/dist/components/checkbox.js +44 -0
- package/dist/components/color-picker.d.ts +47 -0
- package/dist/components/color-picker.js +249 -0
- package/dist/components/combobox.d.ts +40 -0
- package/dist/components/combobox.js +208 -0
- package/dist/components/command.d.ts +34 -0
- package/dist/components/command.js +100 -0
- package/dist/components/date-picker-popover.d.ts +29 -0
- package/dist/components/date-picker-popover.js +548 -0
- package/dist/components/date-picker-popover.logic.d.ts +14 -0
- package/dist/components/date-picker-popover.logic.js +26 -0
- package/dist/components/destructive-action-confirmation.d.ts +21 -0
- package/dist/components/destructive-action-confirmation.js +60 -0
- package/dist/components/destructive-confirm-dialog.d.ts +17 -0
- package/dist/components/destructive-confirm-dialog.js +49 -0
- package/dist/components/dialog.d.ts +37 -0
- package/dist/components/dialog.js +92 -0
- package/dist/components/directional-icon.d.ts +30 -0
- package/dist/components/directional-icon.js +12 -0
- package/dist/components/field.d.ts +11 -0
- package/dist/components/field.js +34 -0
- package/dist/components/form.d.ts +5 -0
- package/dist/components/form.js +12 -0
- package/dist/components/frame.d.ts +10 -0
- package/dist/components/frame.js +35 -0
- package/dist/components/hex-color-picker.d.ts +11 -0
- package/dist/components/hex-color-picker.js +366 -0
- package/dist/components/input-group.d.ts +15 -0
- package/dist/components/input-group.js +53 -0
- package/dist/components/input-otp.d.ts +13 -0
- package/dist/components/input-otp.js +39 -0
- package/dist/components/input.d.ts +12 -0
- package/dist/components/input.js +46 -0
- package/dist/components/label.d.ts +5 -0
- package/dist/components/label.js +18 -0
- package/dist/components/menu.d.ts +44 -0
- package/dist/components/menu.js +152 -0
- package/dist/components/outline-rail.d.ts +39 -0
- package/dist/components/outline-rail.js +405 -0
- package/dist/components/pagination.d.ts +17 -0
- package/dist/components/pagination.js +78 -0
- package/dist/components/popover.d.ts +34 -0
- package/dist/components/popover.js +67 -0
- package/dist/components/preview-card.d.ts +12 -0
- package/dist/components/preview-card.js +25 -0
- package/dist/components/preview-pane.d.ts +23 -0
- package/dist/components/preview-pane.js +33 -0
- package/dist/components/scroll-area.d.ts +19 -0
- package/dist/components/scroll-area.js +38 -0
- package/dist/components/scroll-to-top.d.ts +12 -0
- package/dist/components/scroll-to-top.js +43 -0
- package/dist/components/secret-input.d.ts +9 -0
- package/dist/components/secret-input.js +38 -0
- package/dist/components/segmented-icon-toggle.d.ts +22 -0
- package/dist/components/segmented-icon-toggle.js +30 -0
- package/dist/components/select.d.ts +25 -0
- package/dist/components/select.js +196 -0
- package/dist/components/separator.d.ts +16 -0
- package/dist/components/separator.js +26 -0
- package/dist/components/sheet.d.ts +29 -0
- package/dist/components/sheet.js +87 -0
- package/dist/components/skeleton.d.ts +4 -0
- package/dist/components/skeleton.js +10 -0
- package/dist/components/stella-mark.d.ts +5 -0
- package/dist/components/stella-mark.js +12 -0
- package/dist/components/stella-wordmark.d.ts +5 -0
- package/dist/components/stella-wordmark.js +21 -0
- package/dist/components/table.d.ts +20 -0
- package/dist/components/table.js +80 -0
- package/dist/components/tabs.d.ts +11 -0
- package/dist/components/tabs.js +46 -0
- package/dist/components/textarea.d.ts +9 -0
- package/dist/components/textarea.js +31 -0
- package/dist/components/toast.d.ts +60 -0
- package/dist/components/toast.js +261 -0
- package/dist/components/tooltip-trigger-helper.d.ts +11 -0
- package/dist/components/tooltip-trigger-helper.js +14 -0
- package/dist/components/tooltip.d.ts +15 -0
- package/dist/components/tooltip.js +32 -0
- package/dist/hooks/use-contained-handler.d.ts +55 -0
- package/dist/hooks/use-contained-handler.js +54 -0
- package/dist/hooks/use-content-dir.d.ts +48 -0
- package/dist/hooks/use-content-dir.js +45 -0
- package/dist/hooks/use-mobile.d.ts +4 -0
- package/dist/hooks/use-mobile.js +16 -0
- package/dist/hooks/use-viewport-width.d.ts +5 -0
- package/dist/hooks/use-viewport-width.js +14 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +59 -0
- package/dist/inspector/chrome.d.ts +72 -0
- package/dist/inspector/chrome.js +153 -0
- package/dist/inspector/dock.d.ts +48 -0
- package/dist/inspector/dock.js +36 -0
- package/dist/inspector/index.d.ts +6 -0
- package/dist/inspector/index.js +6 -0
- package/dist/inspector/layout-tokens.d.ts +28 -0
- package/dist/inspector/layout-tokens.js +28 -0
- package/dist/inspector/pane-width.d.ts +64 -0
- package/dist/inspector/pane-width.js +53 -0
- package/dist/inspector/use-pane-width.d.ts +99 -0
- package/dist/inspector/use-pane-width.js +149 -0
- package/dist/lib/button-disposition.d.ts +63 -0
- package/dist/lib/button-disposition.js +52 -0
- package/dist/lib/overlay-layer.d.ts +22 -0
- package/dist/lib/overlay-layer.js +21 -0
- package/dist/lib/tab-order.d.ts +15 -0
- package/dist/lib/tab-order.js +15 -0
- package/dist/lib/tooltip-content.d.ts +13 -0
- package/dist/lib/tooltip-content.js +12 -0
- package/dist/lib/utils.d.ts +20 -0
- package/dist/lib/utils.js +41 -0
- package/dist/lib/week.d.ts +25 -0
- package/dist/lib/week.js +40 -0
- package/dist/styles/theme.css +1209 -0
- package/package.json +506 -2
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { useContentDir } from "../hooks/use-content-dir.js";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
6
|
+
import { Field } from "@base-ui/react/field";
|
|
7
|
+
//#region src/components/textarea.tsx
|
|
8
|
+
const Textarea = ({ className, size = "default", unstyled = false, dir, onChange, ...props }) => {
|
|
9
|
+
const contentDir = useContentDir({
|
|
10
|
+
dir,
|
|
11
|
+
value: props.value,
|
|
12
|
+
defaultValue: props.defaultValue
|
|
13
|
+
});
|
|
14
|
+
const handleChange = (event) => {
|
|
15
|
+
contentDir.trackValue(event.currentTarget.value);
|
|
16
|
+
onChange?.(event);
|
|
17
|
+
};
|
|
18
|
+
return /* @__PURE__ */ jsx("span", {
|
|
19
|
+
className: cn(!unstyled && "border-input bg-background text-foreground ring-ring/24 has-focus-visible:border-ring has-aria-invalid:border-destructive/36 has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 dark:bg-input/32 dark:has-aria-invalid:ring-destructive/24 relative inline-flex w-full rounded-lg border text-base shadow-xs/5 transition-shadow not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:ring-[3px] has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none sm:text-sm dark:not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]", className) || void 0,
|
|
20
|
+
"data-size": size,
|
|
21
|
+
"data-slot": "textarea-control",
|
|
22
|
+
children: /* @__PURE__ */ jsx(Field.Control, { render: (defaultProps) => /* @__PURE__ */ jsx("textarea", {
|
|
23
|
+
className: cn("field-sizing-content min-h-17.5 w-full rounded-[inherit] px-[calc(--spacing(3)-1px)] py-[calc(--spacing(1.5)-1px)] outline-none max-sm:min-h-20.5", size === "sm" && "min-h-16.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)] max-sm:min-h-19.5", size === "lg" && "min-h-18.5 py-[calc(--spacing(2)-1px)] max-sm:min-h-21.5"),
|
|
24
|
+
"data-slot": "textarea",
|
|
25
|
+
...mergeProps(defaultProps, props, { onChange: handleChange }),
|
|
26
|
+
dir: contentDir.dir
|
|
27
|
+
}) })
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { Textarea };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React$1 from "react";
|
|
2
|
+
import { Toast, ToastManagerAddOptions } from "@base-ui/react/toast";
|
|
3
|
+
//#region src/components/toast.d.ts
|
|
4
|
+
type ToastData = {
|
|
5
|
+
tooltipStyle?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare const TOAST_RIGHT_OFFSET_VAR = "--stella-toast-right-offset";
|
|
8
|
+
declare const TOAST_ICONS: {
|
|
9
|
+
readonly error: React$1.ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & React$1.RefAttributes<SVGSVGElement>>;
|
|
10
|
+
readonly info: React$1.ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & React$1.RefAttributes<SVGSVGElement>>;
|
|
11
|
+
readonly loading: React$1.ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & React$1.RefAttributes<SVGSVGElement>>;
|
|
12
|
+
readonly neutral: null;
|
|
13
|
+
readonly success: React$1.ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & React$1.RefAttributes<SVGSVGElement>>;
|
|
14
|
+
readonly warning: React$1.ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & React$1.RefAttributes<SVGSVGElement>>;
|
|
15
|
+
};
|
|
16
|
+
type ToastType = keyof typeof TOAST_ICONS;
|
|
17
|
+
type ToastAction = {
|
|
18
|
+
label: React$1.ReactNode;
|
|
19
|
+
onClick?: React$1.MouseEventHandler<HTMLButtonElement>;
|
|
20
|
+
};
|
|
21
|
+
type ToastActionProps = ToastManagerAddOptions<ToastData>["actionProps"];
|
|
22
|
+
type ToastOptions = Omit<ToastManagerAddOptions<ToastData>, "actionProps" | "title" | "type"> & {
|
|
23
|
+
action?: ToastAction;
|
|
24
|
+
actionProps?: ToastActionProps;
|
|
25
|
+
};
|
|
26
|
+
type ToastUpdateOptions = Partial<ToastOptions> & {
|
|
27
|
+
title?: React$1.ReactNode | undefined;
|
|
28
|
+
type?: ToastType | undefined;
|
|
29
|
+
};
|
|
30
|
+
type ToastPromiseMessage = React$1.ReactNode | ToastUpdateOptions;
|
|
31
|
+
type ToastPromiseOptions<Value> = {
|
|
32
|
+
error: ToastPromiseMessage | ((error: unknown) => ToastPromiseMessage);
|
|
33
|
+
loading: ToastPromiseMessage;
|
|
34
|
+
success: ToastPromiseMessage | ((value: Value) => ToastPromiseMessage);
|
|
35
|
+
};
|
|
36
|
+
type ToastApi = {
|
|
37
|
+
(title: React$1.ReactNode, options?: ToastOptions): string;
|
|
38
|
+
add: (options: ToastAddOptions) => string;
|
|
39
|
+
close: (id?: string) => void;
|
|
40
|
+
dismiss: (id?: string) => void;
|
|
41
|
+
error: (title: React$1.ReactNode, options?: ToastOptions) => string;
|
|
42
|
+
info: (title: React$1.ReactNode, options?: ToastOptions) => string;
|
|
43
|
+
loading: (title: React$1.ReactNode, options?: ToastOptions) => string;
|
|
44
|
+
promise: <Value>(promiseValue: Promise<Value>, options: ToastPromiseOptions<Value>) => Promise<Value>;
|
|
45
|
+
success: (title: React$1.ReactNode, options?: ToastOptions) => string;
|
|
46
|
+
update: (id: string, options: ToastUpdateOptions) => void;
|
|
47
|
+
warning: (title: React$1.ReactNode, options?: ToastOptions) => string;
|
|
48
|
+
};
|
|
49
|
+
type ToastAddOptions = ToastUpdateOptions & {
|
|
50
|
+
title: React$1.ReactNode;
|
|
51
|
+
};
|
|
52
|
+
type ToastPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
|
|
53
|
+
type ToastProviderProps = {
|
|
54
|
+
position?: ToastPosition;
|
|
55
|
+
} & Toast.Provider.Props;
|
|
56
|
+
declare const stellaToast: ToastApi;
|
|
57
|
+
declare const ToastProvider: ({ children, position, ...props }: ToastProviderProps) => React$1.JSX.Element;
|
|
58
|
+
declare const AnchoredToastProvider: ({ children, ...props }: Toast.Provider.Props) => React$1.JSX.Element;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { AnchoredToastProvider, TOAST_RIGHT_OFFSET_VAR, type ToastPosition, ToastProvider, stellaToast };
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { buttonVariants } from "./button-variants.js";
|
|
4
|
+
import { CircleAlertIcon, CircleCheckIcon, InfoIcon, LoaderCircleIcon, TriangleAlertIcon, XIcon } from "lucide-react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import * as React$1 from "react";
|
|
7
|
+
import { Toast } from "@base-ui/react/toast";
|
|
8
|
+
//#region src/components/toast.tsx
|
|
9
|
+
const toastManager = Toast.createToastManager();
|
|
10
|
+
const anchoredToastManager = Toast.createToastManager();
|
|
11
|
+
const TOAST_RIGHT_OFFSET_VAR = "--stella-toast-right-offset";
|
|
12
|
+
const TOAST_ICONS = {
|
|
13
|
+
error: CircleAlertIcon,
|
|
14
|
+
info: InfoIcon,
|
|
15
|
+
loading: LoaderCircleIcon,
|
|
16
|
+
neutral: null,
|
|
17
|
+
success: CircleCheckIcon,
|
|
18
|
+
warning: TriangleAlertIcon
|
|
19
|
+
};
|
|
20
|
+
const stellaToast = Object.assign((title, options) => addToast(title, void 0, options), {
|
|
21
|
+
add: (options) => toastManager.add(normalizeToastAddOptions(options)),
|
|
22
|
+
close: (id) => {
|
|
23
|
+
toastManager.close(id);
|
|
24
|
+
},
|
|
25
|
+
dismiss: (id) => {
|
|
26
|
+
if (id === void 0) {
|
|
27
|
+
toastManager.close();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
toastManager.close(id);
|
|
31
|
+
},
|
|
32
|
+
error: (title, options) => addToast(title, "error", options),
|
|
33
|
+
info: (title, options) => addToast(title, "info", options),
|
|
34
|
+
loading: (title, options) => addToast(title, "loading", options),
|
|
35
|
+
promise: async (promiseValue, options) => {
|
|
36
|
+
const toastId = stellaToast.loading(getPromiseToastTitle(options.loading), getPromiseToastOptions(options.loading));
|
|
37
|
+
try {
|
|
38
|
+
const value = await promiseValue;
|
|
39
|
+
stellaToast.update(toastId, getPromiseToastUpdateOptions(options.success, value, "success"));
|
|
40
|
+
return value;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
stellaToast.update(toastId, getPromiseToastUpdateOptions(options.error, error, "error"));
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
success: (title, options) => addToast(title, "success", options),
|
|
47
|
+
update: (id, options) => {
|
|
48
|
+
toastManager.update(id, normalizeToastUpdateOptions(options));
|
|
49
|
+
},
|
|
50
|
+
warning: (title, options) => addToast(title, "warning", options)
|
|
51
|
+
});
|
|
52
|
+
const ToastProvider = ({ children, position = "bottom-right", ...props }) => /* @__PURE__ */ jsxs(Toast.Provider, {
|
|
53
|
+
toastManager,
|
|
54
|
+
...props,
|
|
55
|
+
children: [children, /* @__PURE__ */ jsx(Toasts, { position })]
|
|
56
|
+
});
|
|
57
|
+
const Toasts = ({ position }) => {
|
|
58
|
+
const { toasts } = Toast.useToastManager();
|
|
59
|
+
const isTop = position.startsWith("top");
|
|
60
|
+
return /* @__PURE__ */ jsx(Toast.Portal, {
|
|
61
|
+
"data-slot": "toast-portal",
|
|
62
|
+
children: /* @__PURE__ */ jsx(Toast.Viewport, {
|
|
63
|
+
className: cn("fixed z-[90] mx-auto flex w-[calc(100%-var(--toast-inset)*2)] max-w-90 [--toast-inset:--spacing(4)] sm:[--toast-inset:--spacing(8)]", "md:data-[position*=right]:w-[calc(100%-var(--toast-inset)*2-min(var(--stella-toast-right-offset,0px),calc(100vw-var(--toast-inset)*2-10rem)))]", "data-[position*=top]:top-(--toast-inset)", "data-[position*=bottom]:bottom-(--toast-inset)", "data-[position*=left]:start-(--toast-inset)", "data-[position*=right]:end-(--toast-inset)", "md:data-[position*=right]:end-[calc(var(--toast-inset)+min(var(--stella-toast-right-offset,0px),calc(100vw-var(--toast-inset)*2-10rem)))]", "data-[position*=center]:left-1/2 data-[position*=center]:-translate-x-1/2"),
|
|
64
|
+
"data-position": position,
|
|
65
|
+
"data-slot": "toast-viewport",
|
|
66
|
+
children: toasts.map((toastItem) => {
|
|
67
|
+
const Icon = getToastIcon(toastItem.type);
|
|
68
|
+
return /* @__PURE__ */ jsx(Toast.Root, {
|
|
69
|
+
className: cn("bg-popover text-popover-foreground absolute z-[calc(9999-var(--toast-index))] h-(--toast-calc-height) w-full rounded-lg border shadow-lg/5 [transition:transform_.5s_cubic-bezier(.22,1,.36,1),opacity_.5s,height_.15s] not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", "data-[position*=right]:start-auto data-[position*=right]:end-0", "data-[position*=left]:start-0 data-[position*=left]:end-auto", "data-[position*=center]:start-0 data-[position*=center]:end-0", "data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-top", "data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-bottom", "after:absolute after:start-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full", "data-[position*=top]:after:top-full", "data-[position*=bottom]:after:bottom-full", "[--toast-calc-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*.1)))] [--toast-shrink:calc(1-var(--toast-scale))]", "data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]", "data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+var(--toast-index)*var(--toast-gap)*-1+var(--toast-swipe-movement-y))]", "data-[position*=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]", "data-[position*=bottom]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]", "data-limited:opacity-0", "data-expanded:h-(--toast-height)", "data-position:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))]", "data-[position*=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]", "data-[position*=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]", "data-ending-style:opacity-0", "data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateY(calc(100%+var(--toast-inset)))]", "data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]", "data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]", "data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]", "data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]", "data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]", "data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]", "data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]", "data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]"),
|
|
70
|
+
"data-position": position,
|
|
71
|
+
swipeDirection: (() => {
|
|
72
|
+
if (position.includes("center")) return [isTop ? "up" : "down"];
|
|
73
|
+
if (position.includes("left")) return ["left", isTop ? "up" : "down"];
|
|
74
|
+
return ["right", isTop ? "up" : "down"];
|
|
75
|
+
})(),
|
|
76
|
+
toast: toastItem,
|
|
77
|
+
children: /* @__PURE__ */ jsxs(Toast.Content, {
|
|
78
|
+
className: "pointer-events-auto flex items-center gap-3 overflow-hidden px-3.5 py-3 text-sm transition-opacity duration-250 select-text data-behind:opacity-0 data-behind:not-data-expanded:pointer-events-none data-expanded:opacity-100",
|
|
79
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
80
|
+
className: "flex min-w-0 flex-1 gap-2",
|
|
81
|
+
children: [Icon && /* @__PURE__ */ jsx("div", {
|
|
82
|
+
className: "select-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&>svg]:h-lh [&>svg]:w-4",
|
|
83
|
+
"data-slot": "toast-icon",
|
|
84
|
+
children: /* @__PURE__ */ jsx(Icon, { className: "in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:animate-spin in-data-[type=loading]:opacity-80" })
|
|
85
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
86
|
+
className: "flex min-w-0 flex-col gap-0.5 select-text",
|
|
87
|
+
children: [/* @__PURE__ */ jsx(Toast.Title, {
|
|
88
|
+
className: "truncate font-medium",
|
|
89
|
+
"data-slot": "toast-title"
|
|
90
|
+
}), /* @__PURE__ */ jsx(Toast.Description, {
|
|
91
|
+
className: "text-muted-foreground break-words",
|
|
92
|
+
"data-slot": "toast-description"
|
|
93
|
+
})]
|
|
94
|
+
})]
|
|
95
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
96
|
+
className: "ms-auto flex shrink-0 items-center gap-2",
|
|
97
|
+
children: [toastItem.actionProps && /* @__PURE__ */ jsx(Toast.Action, {
|
|
98
|
+
className: cn(buttonVariants({ size: "xs" }), "select-none"),
|
|
99
|
+
"data-slot": "toast-action",
|
|
100
|
+
children: toastItem.actionProps.children
|
|
101
|
+
}), /* @__PURE__ */ jsx(Toast.Close, {
|
|
102
|
+
className: "text-muted-foreground hover:text-foreground shrink-0 cursor-pointer rounded p-0.5 transition-colors select-none",
|
|
103
|
+
"data-slot": "toast-close",
|
|
104
|
+
"aria-label": "Close notification",
|
|
105
|
+
children: /* @__PURE__ */ jsx(XIcon, { className: "size-4" })
|
|
106
|
+
})]
|
|
107
|
+
})]
|
|
108
|
+
})
|
|
109
|
+
}, toastItem.id);
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
const AnchoredToastProvider = ({ children, ...props }) => /* @__PURE__ */ jsxs(Toast.Provider, {
|
|
115
|
+
toastManager: anchoredToastManager,
|
|
116
|
+
...props,
|
|
117
|
+
children: [children, /* @__PURE__ */ jsx(AnchoredToasts, {})]
|
|
118
|
+
});
|
|
119
|
+
const AnchoredToasts = () => {
|
|
120
|
+
const { toasts } = Toast.useToastManager();
|
|
121
|
+
return /* @__PURE__ */ jsx(Toast.Portal, {
|
|
122
|
+
"data-slot": "toast-portal-anchored",
|
|
123
|
+
children: /* @__PURE__ */ jsx(Toast.Viewport, {
|
|
124
|
+
className: "outline-none",
|
|
125
|
+
"data-slot": "toast-viewport-anchored",
|
|
126
|
+
children: toasts.map((toastItem) => {
|
|
127
|
+
const Icon = getToastIcon(toastItem.type);
|
|
128
|
+
const tooltipStyle = toastItem.data?.tooltipStyle ?? false;
|
|
129
|
+
const positionerProps = toastItem.positionerProps;
|
|
130
|
+
if (!positionerProps?.anchor) return null;
|
|
131
|
+
return /* @__PURE__ */ jsx(Toast.Positioner, {
|
|
132
|
+
className: "z-50 max-w-[min(--spacing(64),var(--available-width))]",
|
|
133
|
+
"data-slot": "toast-positioner",
|
|
134
|
+
sideOffset: positionerProps.sideOffset ?? 4,
|
|
135
|
+
toast: toastItem,
|
|
136
|
+
children: /* @__PURE__ */ jsx(Toast.Root, {
|
|
137
|
+
className: cn("bg-popover text-popover-foreground relative border text-xs text-balance transition-[scale,opacity] not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-ending-style:opacity-0 data-starting-style:scale-98 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", tooltipStyle ? "rounded-md shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]" : "rounded-lg shadow-lg/5 before:rounded-[calc(var(--radius-lg)-1px)]"),
|
|
138
|
+
"data-slot": "toast-popup",
|
|
139
|
+
toast: toastItem,
|
|
140
|
+
children: tooltipStyle ? /* @__PURE__ */ jsx(Toast.Content, {
|
|
141
|
+
className: "pointer-events-auto px-2 py-1 select-text",
|
|
142
|
+
children: /* @__PURE__ */ jsx(Toast.Title, { "data-slot": "toast-title" })
|
|
143
|
+
}) : /* @__PURE__ */ jsxs(Toast.Content, {
|
|
144
|
+
className: "pointer-events-auto flex items-center gap-3 overflow-hidden px-3.5 py-3 text-sm select-text",
|
|
145
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
146
|
+
className: "flex min-w-0 flex-1 gap-2",
|
|
147
|
+
children: [Icon && /* @__PURE__ */ jsx("div", {
|
|
148
|
+
className: "select-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&>svg]:h-lh [&>svg]:w-4",
|
|
149
|
+
"data-slot": "toast-icon",
|
|
150
|
+
children: /* @__PURE__ */ jsx(Icon, { className: "in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:animate-spin in-data-[type=loading]:opacity-80" })
|
|
151
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
152
|
+
className: "flex min-w-0 flex-col gap-0.5 select-text",
|
|
153
|
+
children: [/* @__PURE__ */ jsx(Toast.Title, {
|
|
154
|
+
className: "truncate font-medium",
|
|
155
|
+
"data-slot": "toast-title"
|
|
156
|
+
}), /* @__PURE__ */ jsx(Toast.Description, {
|
|
157
|
+
className: "text-muted-foreground break-words",
|
|
158
|
+
"data-slot": "toast-description"
|
|
159
|
+
})]
|
|
160
|
+
})]
|
|
161
|
+
}), toastItem.actionProps && /* @__PURE__ */ jsx(Toast.Action, {
|
|
162
|
+
className: cn(buttonVariants({ size: "xs" }), "ms-auto shrink-0 select-none"),
|
|
163
|
+
"data-slot": "toast-action",
|
|
164
|
+
children: toastItem.actionProps.children
|
|
165
|
+
})]
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
}, toastItem.id);
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
function addToast(title, type, options) {
|
|
174
|
+
return toastManager.add(normalizeToastAddOptions({
|
|
175
|
+
...options,
|
|
176
|
+
title,
|
|
177
|
+
type
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
function normalizeToastAddOptions({ action, actionProps, timeout, type, ...options }) {
|
|
181
|
+
return {
|
|
182
|
+
...options,
|
|
183
|
+
...normalizeToastActionProps({
|
|
184
|
+
action,
|
|
185
|
+
actionProps
|
|
186
|
+
}),
|
|
187
|
+
timeout: normalizeToastTimeout(timeout ?? getDefaultToastTimeout(type)),
|
|
188
|
+
type
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function normalizeToastUpdateOptions(options) {
|
|
192
|
+
const { action, actionProps, timeout, type, ...updateOptions } = options;
|
|
193
|
+
const normalizedTimeout = timeout === void 0 ? normalizeDefaultToastUpdateTimeout(type) : normalizeToastTimeout(timeout);
|
|
194
|
+
return {
|
|
195
|
+
...updateOptions,
|
|
196
|
+
...normalizeToastActionProps({
|
|
197
|
+
action,
|
|
198
|
+
actionProps,
|
|
199
|
+
includeEmptyActionProps: Object.hasOwn(options, "action") || Object.hasOwn(options, "actionProps")
|
|
200
|
+
}),
|
|
201
|
+
...type === void 0 ? {} : { type },
|
|
202
|
+
...normalizedTimeout === void 0 ? {} : { timeout: normalizedTimeout }
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function normalizeDefaultToastUpdateTimeout(type) {
|
|
206
|
+
if (type === void 0) return;
|
|
207
|
+
return normalizeToastTimeout(getDefaultToastTimeout(type));
|
|
208
|
+
}
|
|
209
|
+
function normalizeToastActionProps({ action, actionProps, includeEmptyActionProps = false }) {
|
|
210
|
+
if (action === void 0) return actionProps === void 0 && !includeEmptyActionProps ? {} : { actionProps };
|
|
211
|
+
return { actionProps: {
|
|
212
|
+
...actionProps,
|
|
213
|
+
children: action.label,
|
|
214
|
+
onClick: action.onClick
|
|
215
|
+
} };
|
|
216
|
+
}
|
|
217
|
+
function getDefaultToastTimeout(type) {
|
|
218
|
+
if (type === "loading") return 0;
|
|
219
|
+
if (type === "error" || type === "warning") return 6e3;
|
|
220
|
+
return 4e3;
|
|
221
|
+
}
|
|
222
|
+
function normalizeToastTimeout(timeout) {
|
|
223
|
+
if (timeout === Number.POSITIVE_INFINITY) return 0;
|
|
224
|
+
return timeout;
|
|
225
|
+
}
|
|
226
|
+
function getPromiseToastTitle(option) {
|
|
227
|
+
return isToastUpdateOptions(option) ? option.title : option;
|
|
228
|
+
}
|
|
229
|
+
function getPromiseToastOptions(option) {
|
|
230
|
+
return isToastUpdateOptions(option) ? option : void 0;
|
|
231
|
+
}
|
|
232
|
+
function getPromiseToastUpdateOptions(option, value, type) {
|
|
233
|
+
const resolved = typeof option === "function" ? option(value) : option;
|
|
234
|
+
if (isToastUpdateOptions(resolved)) {
|
|
235
|
+
const hasReplacementAction = resolved.action !== void 0 || Object.hasOwn(resolved, "actionProps");
|
|
236
|
+
return {
|
|
237
|
+
...resolved,
|
|
238
|
+
...hasReplacementAction ? {} : { actionProps: void 0 },
|
|
239
|
+
type
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
actionProps: void 0,
|
|
244
|
+
title: resolved,
|
|
245
|
+
type
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function isToastUpdateOptions(value) {
|
|
249
|
+
if (typeof value !== "object" || value === null) return false;
|
|
250
|
+
if (Array.isArray(value) || React$1.isValidElement(value)) return false;
|
|
251
|
+
return !(Symbol.iterator in value);
|
|
252
|
+
}
|
|
253
|
+
function getToastIcon(type) {
|
|
254
|
+
if (!isToastType(type)) return null;
|
|
255
|
+
return TOAST_ICONS[type];
|
|
256
|
+
}
|
|
257
|
+
function isToastType(type) {
|
|
258
|
+
return typeof type === "string" && type in TOAST_ICONS;
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
261
|
+
export { AnchoredToastProvider, TOAST_RIGHT_OFFSET_VAR, ToastProvider, stellaToast };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { OverlayLayer } from "../lib/overlay-layer.js";
|
|
2
|
+
import * as React$1 from "react";
|
|
3
|
+
//#region src/components/tooltip-trigger-helper.d.ts
|
|
4
|
+
type TooltipTriggerOptions = {
|
|
5
|
+
trigger: React$1.ReactElement;
|
|
6
|
+
tooltip: React$1.ReactNode;
|
|
7
|
+
layer?: OverlayLayer | undefined;
|
|
8
|
+
};
|
|
9
|
+
declare const renderTooltipTrigger: ({ trigger, tooltip, layer }: TooltipTriggerOptions) => React$1.JSX.Element;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { renderTooltipTrigger };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { hasTooltipContent } from "../lib/tooltip-content.js";
|
|
3
|
+
import { Tooltip, TooltipContent as TooltipPopup, TooltipTrigger } from "./tooltip.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/components/tooltip-trigger-helper.tsx
|
|
6
|
+
const renderTooltipTrigger = ({ trigger, tooltip, layer }) => {
|
|
7
|
+
if (!hasTooltipContent(tooltip)) return trigger;
|
|
8
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, { render: trigger }), /* @__PURE__ */ jsx(TooltipPopup, {
|
|
9
|
+
...layer === void 0 ? {} : { layer },
|
|
10
|
+
children: tooltip
|
|
11
|
+
})] });
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { renderTooltipTrigger };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { OverlayLayer } from "../lib/overlay-layer.js";
|
|
2
|
+
import { Tooltip as Tooltip$1 } from "@base-ui/react/tooltip";
|
|
3
|
+
//#region src/components/tooltip.d.ts
|
|
4
|
+
declare const TooltipCreateHandle: typeof Tooltip$1.createHandle;
|
|
5
|
+
declare const TooltipProvider: import("react").FC<import("@base-ui/react").TooltipProviderProps>;
|
|
6
|
+
declare const Tooltip: <Payload>(props: Tooltip$1.Root.Props<Payload>) => import("react").JSX.Element;
|
|
7
|
+
declare const TooltipTrigger: (props: Tooltip$1.Trigger.Props) => import("react").JSX.Element;
|
|
8
|
+
declare const TooltipPopup: ({ className, align, sideOffset, side, layer, children, ...props }: Tooltip$1.Popup.Props & {
|
|
9
|
+
align?: Tooltip$1.Positioner.Props["align"];
|
|
10
|
+
side?: Tooltip$1.Positioner.Props["side"];
|
|
11
|
+
sideOffset?: Tooltip$1.Positioner.Props["sideOffset"];
|
|
12
|
+
layer?: OverlayLayer;
|
|
13
|
+
}) => import("react").JSX.Element;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { Tooltip, TooltipPopup as TooltipContent, TooltipPopup, TooltipCreateHandle, TooltipProvider, TooltipTrigger };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { OVERLAY_LAYER_CLASS_NAMES } from "../lib/overlay-layer.js";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { Tooltip as Tooltip$1 } from "@base-ui/react/tooltip";
|
|
6
|
+
//#region src/components/tooltip.tsx
|
|
7
|
+
const TooltipCreateHandle = Tooltip$1.createHandle;
|
|
8
|
+
const TooltipProvider = Tooltip$1.Provider;
|
|
9
|
+
const Tooltip = Tooltip$1.Root;
|
|
10
|
+
const TooltipTrigger = (props) => /* @__PURE__ */ jsx(Tooltip$1.Trigger, {
|
|
11
|
+
"data-slot": "tooltip-trigger",
|
|
12
|
+
...props
|
|
13
|
+
});
|
|
14
|
+
const TooltipPopup = ({ className, align = "center", sideOffset = 4, side = "top", layer = "default", children, ...props }) => /* @__PURE__ */ jsx(Tooltip$1.Portal, { children: /* @__PURE__ */ jsx(Tooltip$1.Positioner, {
|
|
15
|
+
align,
|
|
16
|
+
className: cn("h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] data-instant:transition-none", OVERLAY_LAYER_CLASS_NAMES[layer]),
|
|
17
|
+
"data-slot": "tooltip-positioner",
|
|
18
|
+
side,
|
|
19
|
+
sideOffset,
|
|
20
|
+
children: /* @__PURE__ */ jsx(Tooltip$1.Popup, {
|
|
21
|
+
className: cn("bg-popover text-popover-foreground relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-md border text-xs text-balance shadow-md/5 transition-[width,height,scale,opacity] not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-ending-style:opacity-0 data-instant:duration-0 data-starting-style:scale-98 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", className),
|
|
22
|
+
"data-slot": "tooltip-popup",
|
|
23
|
+
...props,
|
|
24
|
+
children: /* @__PURE__ */ jsx(Tooltip$1.Viewport, {
|
|
25
|
+
className: "relative size-full overflow-clip px-(--viewport-inline-padding) py-1 [--viewport-inline-padding:--spacing(2)] **:data-current:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-current:opacity-100 **:data-current:transition-opacity **:data-current:data-ending-style:opacity-0 data-instant:transition-none **:data-previous:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-previous:truncate **:data-previous:opacity-100 **:data-previous:transition-opacity **:data-previous:data-ending-style:opacity-0 **:data-current:data-starting-style:opacity-0 **:data-previous:data-starting-style:opacity-0",
|
|
26
|
+
"data-slot": "tooltip-viewport",
|
|
27
|
+
children
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
}) });
|
|
31
|
+
//#endregion
|
|
32
|
+
export { Tooltip, TooltipPopup as TooltipContent, TooltipPopup, TooltipCreateHandle, TooltipProvider, TooltipTrigger };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React$1 from "react";
|
|
2
|
+
//#region src/hooks/use-contained-handler.d.ts
|
|
3
|
+
type ContainmentRoot = {
|
|
4
|
+
contains: (target: Node) => boolean;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Wrap a React event handler so it only fires when the event target
|
|
8
|
+
* is a DOM descendant of the given ref.
|
|
9
|
+
*
|
|
10
|
+
* React forwards synthetic events through the parent React tree even
|
|
11
|
+
* when descendants are rendered via createPortal. That makes it unsafe
|
|
12
|
+
* for a parent element to attach `onMouseDown`, `onClick`, `onFocus`,
|
|
13
|
+
* etc. that side-effect (focus steal, preventDefault, selection changes)
|
|
14
|
+
* under the assumption "the target lives inside me" — a portaled popup
|
|
15
|
+
* (Dialog, Combobox, Tooltip) silently bypasses the DOM boundary and
|
|
16
|
+
* triggers the side effect, dismissing itself.
|
|
17
|
+
*
|
|
18
|
+
* Only use for event types whose `target` is the meaningful subject of
|
|
19
|
+
* the event: pointer/mouse/touch/click and `focus`. Do **not** wrap
|
|
20
|
+
* `onBlur`: blur's `target` is the element losing focus, not the new
|
|
21
|
+
* focus destination, so the containment test cannot answer "is focus
|
|
22
|
+
* leaving for a portaled child?". For that case test `relatedTarget`
|
|
23
|
+
* directly inside the handler.
|
|
24
|
+
*
|
|
25
|
+
* The `require-contained-handler` oxlint rule enforces this on any JSX
|
|
26
|
+
* element carrying both `ref={…}` and one of the watched handler props;
|
|
27
|
+
* `containedEventHandler` covers elements that do not otherwise need a ref.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const barRef = useRef<HTMLDivElement>(null);
|
|
31
|
+
* return (
|
|
32
|
+
* <div
|
|
33
|
+
* ref={barRef}
|
|
34
|
+
* onMouseDown={containedHandler(barRef, (e) => {
|
|
35
|
+
* e.preventDefault();
|
|
36
|
+
* focusEditor();
|
|
37
|
+
* })}
|
|
38
|
+
* >
|
|
39
|
+
* ...
|
|
40
|
+
* </div>
|
|
41
|
+
* );
|
|
42
|
+
*/
|
|
43
|
+
declare const containedHandler: <E extends {
|
|
44
|
+
target: unknown;
|
|
45
|
+
}>(ref: React$1.RefObject<ContainmentRoot | null> | null | undefined, handler: ((event: E) => void) | undefined) => (event: E) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Contain a synchronous JSX handler to its own DOM subtree without a ref.
|
|
48
|
+
* Prefer this form when the element does not otherwise need a ref.
|
|
49
|
+
*/
|
|
50
|
+
declare const containedEventHandler: <E extends {
|
|
51
|
+
currentTarget: unknown;
|
|
52
|
+
target: unknown;
|
|
53
|
+
}>(handler: (event: E) => void) => (event: E) => void;
|
|
54
|
+
//#endregion
|
|
55
|
+
export { containedEventHandler, containedHandler };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/hooks/use-contained-handler.ts
|
|
2
|
+
/**
|
|
3
|
+
* Wrap a React event handler so it only fires when the event target
|
|
4
|
+
* is a DOM descendant of the given ref.
|
|
5
|
+
*
|
|
6
|
+
* React forwards synthetic events through the parent React tree even
|
|
7
|
+
* when descendants are rendered via createPortal. That makes it unsafe
|
|
8
|
+
* for a parent element to attach `onMouseDown`, `onClick`, `onFocus`,
|
|
9
|
+
* etc. that side-effect (focus steal, preventDefault, selection changes)
|
|
10
|
+
* under the assumption "the target lives inside me" — a portaled popup
|
|
11
|
+
* (Dialog, Combobox, Tooltip) silently bypasses the DOM boundary and
|
|
12
|
+
* triggers the side effect, dismissing itself.
|
|
13
|
+
*
|
|
14
|
+
* Only use for event types whose `target` is the meaningful subject of
|
|
15
|
+
* the event: pointer/mouse/touch/click and `focus`. Do **not** wrap
|
|
16
|
+
* `onBlur`: blur's `target` is the element losing focus, not the new
|
|
17
|
+
* focus destination, so the containment test cannot answer "is focus
|
|
18
|
+
* leaving for a portaled child?". For that case test `relatedTarget`
|
|
19
|
+
* directly inside the handler.
|
|
20
|
+
*
|
|
21
|
+
* The `require-contained-handler` oxlint rule enforces this on any JSX
|
|
22
|
+
* element carrying both `ref={…}` and one of the watched handler props;
|
|
23
|
+
* `containedEventHandler` covers elements that do not otherwise need a ref.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* const barRef = useRef<HTMLDivElement>(null);
|
|
27
|
+
* return (
|
|
28
|
+
* <div
|
|
29
|
+
* ref={barRef}
|
|
30
|
+
* onMouseDown={containedHandler(barRef, (e) => {
|
|
31
|
+
* e.preventDefault();
|
|
32
|
+
* focusEditor();
|
|
33
|
+
* })}
|
|
34
|
+
* >
|
|
35
|
+
* ...
|
|
36
|
+
* </div>
|
|
37
|
+
* );
|
|
38
|
+
*/
|
|
39
|
+
const containedHandler = (ref, handler) => (event) => {
|
|
40
|
+
if (handler === void 0) return;
|
|
41
|
+
const container = ref?.current ?? null;
|
|
42
|
+
if (container !== null && event.target instanceof Node && !container.contains(event.target)) return;
|
|
43
|
+
handler(event);
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Contain a synchronous JSX handler to its own DOM subtree without a ref.
|
|
47
|
+
* Prefer this form when the element does not otherwise need a ref.
|
|
48
|
+
*/
|
|
49
|
+
const containedEventHandler = (handler) => (event) => {
|
|
50
|
+
if (event.currentTarget instanceof Node && event.target instanceof Node && !event.currentTarget.contains(event.target)) return;
|
|
51
|
+
handler(event);
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { containedEventHandler, containedHandler };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/hooks/use-content-dir.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the `dir` for a free-text field so the caret and alignment follow
|
|
4
|
+
* the ambient UI direction while the field is empty, then switch to the typed
|
|
5
|
+
* content's own direction once the user starts writing.
|
|
6
|
+
*
|
|
7
|
+
* `dir="auto"` on its own is wrong for this: the bidi heuristic reads the
|
|
8
|
+
* field's *value*, and an empty value has no strong directional character, so
|
|
9
|
+
* it falls back to LTR — leaving the caret on the left even under an RTL UI.
|
|
10
|
+
* Here an empty field omits `dir` entirely (so it inherits the document
|
|
11
|
+
* direction: caret on the right in Arabic), and a non-empty field uses
|
|
12
|
+
* `dir="auto"` (Arabic content → RTL, Latin content → LTR, re-evaluated live).
|
|
13
|
+
*
|
|
14
|
+
* An explicit `dir="ltr"`/`dir="rtl"` is treated as a forced direction and
|
|
15
|
+
* returned unchanged (e.g. always-LTR numeric fields).
|
|
16
|
+
*
|
|
17
|
+
* Empty↔non-empty is the only transition that changes direction, so the
|
|
18
|
+
* uncontrolled path stores a boolean and the state setter no-ops (no re-render)
|
|
19
|
+
* on every keystroke that doesn't cross that boundary.
|
|
20
|
+
*/
|
|
21
|
+
type FieldValue = string | number | readonly string[] | undefined;
|
|
22
|
+
type UseContentDirArgs = {
|
|
23
|
+
dir: string | undefined;
|
|
24
|
+
value: FieldValue;
|
|
25
|
+
defaultValue: FieldValue;
|
|
26
|
+
};
|
|
27
|
+
type UseContentDirResult = {
|
|
28
|
+
dir: "ltr" | "rtl" | "auto" | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Feed the field's current text on every change so the uncontrolled path can
|
|
31
|
+
* detect the empty↔non-empty transition. No-op for controlled fields, whose
|
|
32
|
+
* direction is derived from `value` on render.
|
|
33
|
+
*/
|
|
34
|
+
trackValue: (text: string) => void;
|
|
35
|
+
};
|
|
36
|
+
declare const isStructuredInputType: (type: string | undefined) => boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Content-derived direction for a controlled field value: `"auto"` when there
|
|
39
|
+
* is content, `undefined` when empty (so the field inherits the ambient UI
|
|
40
|
+
* direction — caret on the RTL side under Arabic). Use this for a raw
|
|
41
|
+
* `<input>`/`<textarea>` whose value you already control; the shared
|
|
42
|
+
* `<Input>`/`<Textarea>` apply it for you via `useContentDir`, which also
|
|
43
|
+
* tracks the empty↔non-empty transition for uncontrolled fields.
|
|
44
|
+
*/
|
|
45
|
+
declare const contentDir: (value: FieldValue) => "auto" | undefined;
|
|
46
|
+
declare const useContentDir: ({ dir, value, defaultValue }: UseContentDirArgs) => UseContentDirResult;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { contentDir, isStructuredInputType, useContentDir };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
//#region src/hooks/use-content-dir.ts
|
|
3
|
+
const STRUCTURED_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
4
|
+
"email",
|
|
5
|
+
"url",
|
|
6
|
+
"tel",
|
|
7
|
+
"number",
|
|
8
|
+
"password",
|
|
9
|
+
"date",
|
|
10
|
+
"datetime-local",
|
|
11
|
+
"time",
|
|
12
|
+
"month",
|
|
13
|
+
"week"
|
|
14
|
+
]);
|
|
15
|
+
const isStructuredInputType = (type) => type !== void 0 && STRUCTURED_INPUT_TYPES.has(type);
|
|
16
|
+
const hasText = (value) => {
|
|
17
|
+
if (typeof value === "string") return value.length > 0;
|
|
18
|
+
if (typeof value === "number") return true;
|
|
19
|
+
return Array.isArray(value) && value.length > 0;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Content-derived direction for a controlled field value: `"auto"` when there
|
|
23
|
+
* is content, `undefined` when empty (so the field inherits the ambient UI
|
|
24
|
+
* direction — caret on the RTL side under Arabic). Use this for a raw
|
|
25
|
+
* `<input>`/`<textarea>` whose value you already control; the shared
|
|
26
|
+
* `<Input>`/`<Textarea>` apply it for you via `useContentDir`, which also
|
|
27
|
+
* tracks the empty↔non-empty transition for uncontrolled fields.
|
|
28
|
+
*/
|
|
29
|
+
const contentDir = (value) => hasText(value) ? "auto" : void 0;
|
|
30
|
+
const useContentDir = ({ dir, value, defaultValue }) => {
|
|
31
|
+
const forcedDir = dir === "ltr" || dir === "rtl" ? dir : void 0;
|
|
32
|
+
const isControlled = value !== void 0;
|
|
33
|
+
const [uncontrolledHasText, setUncontrolledHasText] = useState(() => hasText(defaultValue));
|
|
34
|
+
const contentPresent = isControlled ? hasText(value) : uncontrolledHasText;
|
|
35
|
+
return {
|
|
36
|
+
dir: forcedDir ?? (contentPresent ? "auto" : void 0),
|
|
37
|
+
trackValue: (text) => {
|
|
38
|
+
if (isControlled) return;
|
|
39
|
+
const next = text.length > 0;
|
|
40
|
+
setUncontrolledHasText((previous) => previous === next ? previous : next);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { contentDir, isStructuredInputType, useContentDir };
|