unknown> = T extends (
+ props: infer P,
+) => unknown
+ ? Omit
+ : never;
+type DivProps = Omit, "className" | "style">;
+export type DrawerSide = "top" | "right" | "bottom" | "left";
+export interface DrawerRootProps
+ extends Omit {
+ side?: DrawerSide;
+}
+export interface DrawerPopupProps extends Props {
+ side?: DrawerSide;
+}
+
+function partProps(...values: stylex.StyleXStyles[]) {
+ const resolved = stylex.props(...values);
+ return { className: resolved.className, style: resolved.style };
+}
+
+const swipeDirection = {
+ top: "up",
+ right: "right",
+ bottom: "down",
+ left: "left",
+} as const;
+
+const DrawerSideContext = createContext("right");
+
+export const Drawer = {
+ Provider: BaseDrawer.Provider,
+ Root: function DrawerRoot({ side = "right", ...props }: DrawerRootProps) {
+ return (
+
+
+
+ );
+ },
+ Trigger: function DrawerTrigger(props: Props) {
+ return (
+
+ );
+ },
+ Portal: function DrawerPortal(props: Props) {
+ const theme = useContext(DowelThemeContext);
+ const resolved = stylex.props(themeStyles[theme]);
+ return (
+
+ );
+ },
+ Backdrop: function DrawerBackdrop(props: Props) {
+ return (
+
+ );
+ },
+ Viewport: function DrawerViewport(props: Props) {
+ return (
+
+ );
+ },
+ Popup: function DrawerPopup({
+ side: sideOverride,
+ ...props
+ }: DrawerPopupProps) {
+ const inheritedSide = useContext(DrawerSideContext);
+ const side = sideOverride ?? inheritedSide;
+ return (
+
+ );
+ },
+ Content: function DrawerContent(props: Props) {
+ return (
+
+ );
+ },
+ Handle: forwardRef(
+ function DrawerHandle(props, ref) {
+ return (
+
+ );
+ },
+ ),
+ Header: forwardRef(
+ function DrawerHeader(props, ref) {
+ return (
+
+ );
+ },
+ ),
+ Body: forwardRef(function DrawerBody(props, ref) {
+ return (
+
+ );
+ }),
+ Footer: forwardRef(
+ function DrawerFooter(props, ref) {
+ return (
+
+ );
+ },
+ ),
+ Title: function DrawerTitle(props: Props) {
+ return (
+
+ );
+ },
+ Description: function DrawerDescription(
+ props: Props,
+ ) {
+ return (
+
+ );
+ },
+ Close: function DrawerClose(props: Props) {
+ return (
+
+ );
+ },
+};
diff --git a/packages/dowel/src/components/file-upload/file-upload.stylex.ts b/packages/dowel/src/components/file-upload/file-upload.stylex.ts
new file mode 100644
index 0000000..e9a57d5
--- /dev/null
+++ b/packages/dowel/src/components/file-upload/file-upload.stylex.ts
@@ -0,0 +1,118 @@
+import * as stylex from "@stylexjs/stylex";
+import { tokens } from "../../theme/tokens.stylex";
+export const upload = stylex.create({
+ root: { display: "grid", gap: "0.5rem", width: "100%" },
+ dropzone: {
+ alignItems: "center",
+ backgroundColor: tokens["--dowel-bg-surface-2"],
+ borderColor: tokens["--dowel-border-default"],
+ borderRadius: tokens["--dowel-radius-md"],
+ borderStyle: "dashed",
+ borderWidth: "1px",
+ display: "flex",
+ flexDirection: "column",
+ gap: "0.375rem",
+ justifyContent: "center",
+ minHeight: "8rem",
+ padding: "1rem",
+ transitionDuration: tokens["--dowel-duration-fast"],
+ transitionProperty: "background-color, border-color",
+ },
+ dragging: {
+ backgroundColor: tokens["--dowel-bg-active"],
+ borderColor: tokens["--dowel-accent"],
+ },
+ disabled: { opacity: 0.55 },
+ input: {
+ border: 0,
+ clip: "rect(0 0 0 0)",
+ clipPath: "inset(50%)",
+ height: "1px",
+ margin: "-1px",
+ overflow: "hidden",
+ padding: 0,
+ position: "absolute",
+ whiteSpace: "nowrap",
+ width: "1px",
+ },
+ browse: {
+ alignItems: "center",
+ backgroundColor: "transparent",
+ borderRadius: tokens["--dowel-radius-sm"],
+ borderWidth: 0,
+ color: tokens["--dowel-text-primary"],
+ display: "inline-flex",
+ fontFamily: tokens["--dowel-font-sans"],
+ fontSize: "0.8125rem",
+ fontWeight: 500,
+ gap: "0.5rem",
+ minHeight: "2rem",
+ outline: "none",
+ paddingInline: "0.5rem",
+ ":hover": { backgroundColor: tokens["--dowel-bg-hover"] },
+ ":focus-visible": {
+ outlineColor: tokens["--dowel-focus-ring"],
+ outlineStyle: "solid",
+ outlineWidth: "1px",
+ },
+ },
+ description: {
+ color: tokens["--dowel-text-tertiary"],
+ fontFamily: tokens["--dowel-font-sans"],
+ fontSize: "0.75rem",
+ textAlign: "center",
+ },
+ list: {
+ display: "grid",
+ gap: "0.25rem",
+ listStyle: "none",
+ margin: 0,
+ padding: 0,
+ },
+ file: {
+ alignItems: "center",
+ backgroundColor: tokens["--dowel-bg-surface-2"],
+ borderRadius: tokens["--dowel-radius-sm"],
+ display: "flex",
+ gap: "0.5rem",
+ minHeight: "2rem",
+ padding: "0.25rem 0.375rem 0.25rem 0.5rem",
+ },
+ fileIcon: { color: tokens["--dowel-text-tertiary"], flexShrink: 0 },
+ fileName: {
+ color: tokens["--dowel-text-primary"],
+ flex: 1,
+ fontFamily: tokens["--dowel-font-sans"],
+ fontSize: "0.75rem",
+ minWidth: 0,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ },
+ fileSize: {
+ color: tokens["--dowel-text-tertiary"],
+ fontFamily: tokens["--dowel-font-mono"],
+ fontSize: "0.6875rem",
+ },
+ remove: {
+ alignItems: "center",
+ backgroundColor: "transparent",
+ borderRadius: tokens["--dowel-radius-sm"],
+ borderWidth: 0,
+ color: tokens["--dowel-text-tertiary"],
+ display: "inline-flex",
+ height: "1.5rem",
+ justifyContent: "center",
+ outline: "none",
+ width: "1.5rem",
+ ":hover": {
+ backgroundColor: tokens["--dowel-bg-hover"],
+ color: tokens["--dowel-text-primary"],
+ },
+ ":focus-visible": {
+ outlineColor: tokens["--dowel-focus-ring"],
+ outlineStyle: "solid",
+ outlineWidth: "1px",
+ },
+ },
+});
diff --git a/packages/dowel/src/components/file-upload/file-upload.test.tsx b/packages/dowel/src/components/file-upload/file-upload.test.tsx
new file mode 100644
index 0000000..fc7a2f7
--- /dev/null
+++ b/packages/dowel/src/components/file-upload/file-upload.test.tsx
@@ -0,0 +1,21 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi } from "vitest";
+import { FileUpload } from "./index";
+describe("FileUpload", () => {
+ it("accepts and removes a file", async () => {
+ const change = vi.fn();
+ const { container } = render( );
+ const input = container.querySelector(
+ "input[type=file]",
+ ) as HTMLInputElement;
+ const file = new File(["hello"], "notes.txt", { type: "text/plain" });
+ await userEvent.upload(input, file);
+ expect(screen.getByText("notes.txt")).toBeDefined();
+ await userEvent.click(
+ screen.getByRole("button", { name: "Remove notes.txt" }),
+ );
+ expect(screen.queryByText("notes.txt")).toBeNull();
+ expect(change).toHaveBeenLastCalledWith([]);
+ });
+});
diff --git a/packages/dowel/src/components/file-upload/index.tsx b/packages/dowel/src/components/file-upload/index.tsx
new file mode 100644
index 0000000..fac066c
--- /dev/null
+++ b/packages/dowel/src/components/file-upload/index.tsx
@@ -0,0 +1,182 @@
+import {
+ ArrowUpTrayIcon,
+ DocumentIcon,
+ XMarkIcon,
+} from "@heroicons/react/16/solid";
+import * as stylex from "@stylexjs/stylex";
+import { useRef, useState } from "react";
+import type { ChangeEvent, DragEvent, ReactNode } from "react";
+import * as styles from "./file-upload.stylex";
+
+export type FileRejectReason = "type" | "size" | "count";
+export interface RejectedFile {
+ file: File;
+ reason: FileRejectReason;
+}
+export interface FileUploadProps {
+ label?: ReactNode;
+ description?: ReactNode;
+ accept?: string;
+ multiple?: boolean;
+ maxFiles?: number;
+ maxSize?: number;
+ disabled?: boolean;
+ onFilesChange?: (files: File[]) => void;
+ onReject?: (rejected: RejectedFile[]) => void;
+}
+function sx(...v: stylex.StyleXStyles[]) {
+ const r = stylex.props(...v);
+ return { className: r.className, style: r.style };
+}
+function accepts(file: File, accept?: string) {
+ if (!accept) return true;
+ return accept
+ .split(",")
+ .map((part) => part.trim().toLowerCase())
+ .some((part) =>
+ part.startsWith(".")
+ ? file.name.toLowerCase().endsWith(part)
+ : part.endsWith("/*")
+ ? file.type.toLowerCase().startsWith(part.slice(0, -1))
+ : file.type.toLowerCase() === part,
+ );
+}
+function sizeLabel(bytes: number) {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
+}
+export function FileUpload({
+ label = "Drop files here or browse",
+ description,
+ accept,
+ multiple = false,
+ maxFiles = multiple ? Infinity : 1,
+ maxSize = Infinity,
+ disabled,
+ onFilesChange,
+ onReject,
+}: FileUploadProps) {
+ const [files, setFiles] = useState([]);
+ const [dragging, setDragging] = useState(false);
+ const inputRef = useRef(null);
+ function commit(incoming: File[]) {
+ const rejected: RejectedFile[] = [];
+ const accepted = incoming.filter((file) => {
+ if (!accepts(file, accept)) {
+ rejected.push({ file, reason: "type" });
+ return false;
+ }
+ if (file.size > maxSize) {
+ rejected.push({ file, reason: "size" });
+ return false;
+ }
+ return true;
+ });
+ const combined = multiple ? [...files, ...accepted] : accepted.slice(0, 1);
+ const unique = combined.filter(
+ (file, index, all) =>
+ all.findIndex(
+ (candidate) =>
+ candidate.name === file.name &&
+ candidate.size === file.size &&
+ candidate.lastModified === file.lastModified,
+ ) === index,
+ );
+ const next = unique.slice(0, maxFiles);
+ unique
+ .slice(maxFiles)
+ .forEach((file) => rejected.push({ file, reason: "count" }));
+ setFiles(next);
+ onFilesChange?.(next);
+ if (rejected.length) onReject?.(rejected);
+ }
+ function change(event: ChangeEvent) {
+ commit([...(event.target.files ?? [])]);
+ event.target.value = "";
+ }
+ function drop(event: DragEvent) {
+ event.preventDefault();
+ if (disabled) return;
+ setDragging(false);
+ commit([...event.dataTransfer.files]);
+ }
+ function remove(index: number) {
+ const next = files.filter((_, fileIndex) => fileIndex !== index);
+ setFiles(next);
+ onFilesChange?.(next);
+ }
+ return (
+
+
{
+ event.preventDefault();
+ if (!disabled) setDragging(true);
+ }}
+ onDragOver={(event) => event.preventDefault()}
+ onDragLeave={(event) => {
+ if (!event.currentTarget.contains(event.relatedTarget as Node))
+ setDragging(false);
+ }}
+ onDrop={drop}
+ {...sx(
+ styles.upload.dropzone,
+ dragging && styles.upload.dragging,
+ disabled && styles.upload.disabled,
+ )}
+ >
+
+
inputRef.current?.click()}
+ {...sx(styles.upload.browse)}
+ >
+
+ {label}
+
+ {description ? (
+
{description}
+ ) : null}
+
+ {files.length ? (
+
+ {files.map((file, index) => (
+
+
+ {file.name}
+
+ {sizeLabel(file.size)}
+
+ remove(index)}
+ {...sx(styles.upload.remove)}
+ >
+
+
+
+ ))}
+
+ ) : null}
+
+ );
+}
+export const Dropzone = FileUpload;
diff --git a/packages/dowel/src/components/list/index.tsx b/packages/dowel/src/components/list/index.tsx
new file mode 100644
index 0000000..8af8c79
--- /dev/null
+++ b/packages/dowel/src/components/list/index.tsx
@@ -0,0 +1,127 @@
+import * as stylex from "@stylexjs/stylex";
+import { forwardRef } from "react";
+import type { ComponentPropsWithoutRef } from "react";
+
+import * as styles from "./list.stylex";
+
+type UlProps = Omit, "className" | "style">;
+type LiProps = Omit, "className" | "style">;
+type DivProps = Omit, "className" | "style">;
+
+export interface ListProps extends UlProps {
+ divided?: boolean;
+}
+
+export interface ListRowProps extends LiProps {
+ density?: "compact" | "default";
+ selected?: boolean;
+ disabled?: boolean;
+}
+
+export interface ListCellProps extends DivProps {
+ grow?: boolean;
+ align?: "start" | "end";
+ tone?: "primary" | "secondary" | "tertiary";
+ truncate?: boolean;
+}
+
+export interface GroupHeaderProps extends LiProps {
+ sticky?: boolean;
+}
+
+export const List = forwardRef(function List(
+ { divided = false, ...props },
+ ref,
+) {
+ const resolved = stylex.props(
+ styles.list.root,
+ divided && styles.list.divided,
+ );
+ return (
+
+ );
+});
+
+export const ListRow = forwardRef(function ListRow(
+ { density = "default", selected = false, disabled = false, ...props },
+ ref,
+) {
+ const resolved = stylex.props(
+ styles.row.root,
+ density === "compact" && styles.row.compact,
+ selected && styles.row.selected,
+ disabled && styles.row.disabled,
+ );
+ return (
+
+ );
+});
+
+export const ListCell = forwardRef(
+ function ListCell(
+ {
+ grow = false,
+ align = "start",
+ tone = "primary",
+ truncate = false,
+ ...props
+ },
+ ref,
+ ) {
+ const resolved = stylex.props(
+ styles.cell.root,
+ grow && styles.cell.grow,
+ align === "end" && styles.cell.end,
+ tone === "secondary" && styles.cell.secondary,
+ tone === "tertiary" && styles.cell.tertiary,
+ truncate && styles.cell.truncate,
+ );
+ return (
+
+ );
+ },
+);
+
+export const GroupHeader = forwardRef(
+ function GroupHeader({ sticky = false, ...props }, ref) {
+ const resolved = stylex.props(
+ styles.groupHeader.root,
+ sticky && styles.groupHeader.sticky,
+ );
+ return (
+
+ );
+ },
+);
diff --git a/packages/dowel/src/components/list/list.stylex.ts b/packages/dowel/src/components/list/list.stylex.ts
new file mode 100644
index 0000000..4263ca0
--- /dev/null
+++ b/packages/dowel/src/components/list/list.stylex.ts
@@ -0,0 +1,83 @@
+import * as stylex from "@stylexjs/stylex";
+
+import { tokens } from "../../theme/tokens.stylex";
+
+export const list = stylex.create({
+ root: {
+ display: "flex",
+ flexDirection: "column",
+ listStyle: "none",
+ margin: 0,
+ minWidth: 0,
+ padding: 0,
+ },
+ divided: {
+ backgroundColor: tokens["--dowel-border-subtle"],
+ rowGap: tokens["--dowel-hairline"],
+ },
+});
+
+export const row = stylex.create({
+ root: {
+ alignItems: "center",
+ boxSizing: "border-box",
+ color: tokens["--dowel-text-primary"],
+ display: "flex",
+ gap: "0.75rem",
+ minHeight: tokens["--dowel-row-md"],
+ minWidth: 0,
+ padding: "0.5rem 0.75rem",
+ position: "relative",
+ backgroundColor: tokens["--dowel-bg-surface-1"],
+ },
+ compact: { minHeight: "2.25rem", paddingBlock: "0.375rem" },
+ selected: { backgroundColor: tokens["--dowel-bg-active"] },
+ disabled: { color: tokens["--dowel-text-disabled"], opacity: 0.72 },
+});
+
+export const cell = stylex.create({
+ root: {
+ alignItems: "center",
+ display: "flex",
+ fontFamily: tokens["--dowel-font-sans"],
+ fontSize: "0.8125rem",
+ gap: "0.5rem",
+ lineHeight: 1.4,
+ minWidth: 0,
+ },
+ grow: { flex: 1 },
+ end: {
+ justifyContent: "flex-end",
+ marginInlineStart: "auto",
+ textAlign: "end",
+ },
+ secondary: { color: tokens["--dowel-text-secondary"] },
+ tertiary: { color: tokens["--dowel-text-tertiary"] },
+ truncate: {
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ },
+});
+
+export const groupHeader = stylex.create({
+ root: {
+ alignItems: "center",
+ color: tokens["--dowel-text-tertiary"],
+ display: "flex",
+ fontFamily: tokens["--dowel-font-sans"],
+ fontSize: "0.6875rem",
+ fontWeight: 550,
+ gap: "0.5rem",
+ lineHeight: 1.4,
+ minHeight: "1.75rem",
+ padding: "0.5rem 0.75rem 0.25rem",
+ backgroundColor: tokens["--dowel-bg-surface-1"],
+ },
+ sticky: {
+ backgroundColor: tokens["--dowel-bg-surface-1"],
+ insetBlockStart: 0,
+ position: "sticky",
+ zIndex: 1,
+ },
+});
diff --git a/packages/dowel/src/components/list/list.test.tsx b/packages/dowel/src/components/list/list.test.tsx
new file mode 100644
index 0000000..d76c6ca
--- /dev/null
+++ b/packages/dowel/src/components/list/list.test.tsx
@@ -0,0 +1,36 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { expectNoA11yViolations } from "../../../test/setup";
+import { GroupHeader, List, ListCell, ListRow } from "./index";
+
+describe("List", () => {
+ it("renders semantic grouped rows and selection state", () => {
+ render(
+
+ Favorites
+
+
+ dowel
+
+ Public
+
+
,
+ );
+ expect(screen.getByRole("list", { name: "Repositories" })).toBeDefined();
+ expect(screen.getByText("dowel").closest("li")?.dataset.selected).toBe(
+ "true",
+ );
+ });
+
+ it("has no accessibility violations", async () => {
+ const { container } = render(
+
+
+ dowel
+
+
,
+ );
+ await expectNoA11yViolations(container);
+ });
+});
diff --git a/packages/dowel/src/components/pagination/index.tsx b/packages/dowel/src/components/pagination/index.tsx
new file mode 100644
index 0000000..c4f3166
--- /dev/null
+++ b/packages/dowel/src/components/pagination/index.tsx
@@ -0,0 +1,92 @@
+import {
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ EllipsisHorizontalIcon,
+} from "@heroicons/react/16/solid";
+import * as stylex from "@stylexjs/stylex";
+import * as styles from "./pagination.stylex";
+
+export interface PaginationProps {
+ page: number;
+ totalPages: number;
+ onPageChange: (page: number) => void;
+ siblingCount?: number;
+ ariaLabel?: string;
+}
+type Token = number | "ellipsis-start" | "ellipsis-end";
+function tokens(page: number, total: number, siblings: number): Token[] {
+ if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
+ const start = Math.max(2, page - siblings);
+ const end = Math.min(total - 1, page + siblings);
+ const out: Token[] = [1];
+ if (start > 2) out.push("ellipsis-start");
+ for (let n = start; n <= end; n++) out.push(n);
+ if (end < total - 1) out.push("ellipsis-end");
+ out.push(total);
+ return out;
+}
+function sx(...v: stylex.StyleXStyles[]) {
+ const r = stylex.props(...v);
+ return { className: r.className, style: r.style };
+}
+export function Pagination({
+ page,
+ totalPages,
+ onPageChange,
+ siblingCount = 1,
+ ariaLabel = "Pagination",
+}: PaginationProps) {
+ const safeTotal = Math.max(1, totalPages);
+ const safePage = Math.min(Math.max(1, page), safeTotal);
+ return (
+
+ onPageChange(safePage - 1)}
+ {...sx(styles.pagination.button)}
+ >
+
+
+ {tokens(safePage, safeTotal, siblingCount).map((token) =>
+ typeof token === "number" ? (
+ onPageChange(token)}
+ {...sx(
+ styles.pagination.button,
+ token === safePage && styles.pagination.active,
+ )}
+ >
+ {token}
+
+ ) : (
+
+
+
+ ),
+ )}
+ onPageChange(safePage + 1)}
+ {...sx(styles.pagination.button)}
+ >
+
+
+
+ );
+}
diff --git a/packages/dowel/src/components/pagination/pagination.stylex.ts b/packages/dowel/src/components/pagination/pagination.stylex.ts
new file mode 100644
index 0000000..639bb5e
--- /dev/null
+++ b/packages/dowel/src/components/pagination/pagination.stylex.ts
@@ -0,0 +1,43 @@
+import * as stylex from "@stylexjs/stylex";
+import { tokens } from "../../theme/tokens.stylex";
+export const pagination = stylex.create({
+ root: { alignItems: "center", display: "flex", gap: "0.125rem" },
+ button: {
+ alignItems: "center",
+ backgroundColor: "transparent",
+ borderRadius: tokens["--dowel-radius-sm"],
+ borderWidth: 0,
+ color: tokens["--dowel-text-secondary"],
+ display: "inline-flex",
+ fontFamily: tokens["--dowel-font-sans"],
+ fontSize: "0.75rem",
+ height: tokens["--dowel-control-md"],
+ justifyContent: "center",
+ minWidth: tokens["--dowel-control-md"],
+ outline: "none",
+ paddingInline: "0.375rem",
+ ":hover": {
+ backgroundColor: tokens["--dowel-bg-hover"],
+ color: tokens["--dowel-text-primary"],
+ },
+ ":focus-visible": {
+ outlineColor: tokens["--dowel-focus-ring"],
+ outlineStyle: "solid",
+ outlineWidth: "1px",
+ },
+ ":disabled": { color: tokens["--dowel-text-disabled"], opacity: 0.55 },
+ },
+ active: {
+ backgroundColor: tokens["--dowel-bg-surface-3"],
+ color: tokens["--dowel-text-primary"],
+ fontWeight: 600,
+ },
+ ellipsis: {
+ alignItems: "center",
+ color: tokens["--dowel-text-tertiary"],
+ display: "inline-flex",
+ height: tokens["--dowel-control-md"],
+ justifyContent: "center",
+ width: tokens["--dowel-control-md"],
+ },
+});
diff --git a/packages/dowel/src/components/pagination/pagination.test.tsx b/packages/dowel/src/components/pagination/pagination.test.tsx
new file mode 100644
index 0000000..f5f41b2
--- /dev/null
+++ b/packages/dowel/src/components/pagination/pagination.test.tsx
@@ -0,0 +1,12 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi } from "vitest";
+import { Pagination } from "./index";
+describe("Pagination", () => {
+ it("moves to another page", async () => {
+ const change = vi.fn();
+ render( );
+ await userEvent.click(screen.getByRole("button", { name: "Next page" }));
+ expect(change).toHaveBeenCalledWith(3);
+ });
+});
diff --git a/packages/dowel/src/components/progress/index.tsx b/packages/dowel/src/components/progress/index.tsx
new file mode 100644
index 0000000..6d8f3bc
--- /dev/null
+++ b/packages/dowel/src/components/progress/index.tsx
@@ -0,0 +1,60 @@
+import { Progress as BaseProgress } from "@base-ui/react/progress";
+import * as stylex from "@stylexjs/stylex";
+import type { ReactNode } from "react";
+
+import * as styles from "./progress.stylex";
+
+export interface ProgressProps {
+ label: ReactNode;
+ value: number | null;
+ min?: number;
+ max?: number;
+ size?: "sm" | "md" | "lg";
+ showValue?: boolean;
+ getAriaValueText?: (formattedValue: string, value: number | null) => string;
+}
+
+function sx(...values: stylex.StyleXStyles[]) {
+ const resolved = stylex.props(...values);
+ return { className: resolved.className, style: resolved.style };
+}
+
+export function Progress({
+ label,
+ value,
+ min = 0,
+ max = 100,
+ size = "md",
+ showValue = true,
+ getAriaValueText,
+}: ProgressProps) {
+ return (
+
+
+
+ {label}
+
+ {showValue ? (
+
+ ) : null}
+
+
+
+
+
+ );
+}
diff --git a/packages/dowel/src/components/progress/progress.stylex.ts b/packages/dowel/src/components/progress/progress.stylex.ts
new file mode 100644
index 0000000..5008d19
--- /dev/null
+++ b/packages/dowel/src/components/progress/progress.stylex.ts
@@ -0,0 +1,61 @@
+import * as stylex from "@stylexjs/stylex";
+
+import { tokens } from "../../theme/tokens.stylex";
+
+const REDUCED_MOTION = "@media (prefers-reduced-motion: reduce)";
+const slide = stylex.keyframes({
+ from: { transform: "translateX(-120%)" },
+ to: { transform: "translateX(260%)" },
+});
+
+export const progress = stylex.create({
+ root: { display: "grid", gap: "0.375rem", minWidth: 0, width: "100%" },
+ metadata: {
+ alignItems: "center",
+ display: "flex",
+ gap: "0.75rem",
+ justifyContent: "space-between",
+ },
+ label: {
+ color: tokens["--dowel-text-primary"],
+ fontFamily: tokens["--dowel-font-sans"],
+ fontSize: "0.8125rem",
+ lineHeight: 1.4,
+ },
+ value: {
+ color: tokens["--dowel-text-tertiary"],
+ fontFamily: tokens["--dowel-font-mono"],
+ fontSize: "0.75rem",
+ fontVariantNumeric: "tabular-nums",
+ lineHeight: 1.4,
+ },
+ track: {
+ backgroundColor: tokens["--dowel-bg-surface-3"],
+ borderRadius: tokens["--dowel-radius-pill"],
+ height: "0.375rem",
+ overflow: "hidden",
+ position: "relative",
+ width: "100%",
+ },
+ small: { height: "0.25rem" },
+ large: { height: "0.5rem" },
+ indicator: {
+ backgroundColor: tokens["--dowel-accent"],
+ borderRadius: "inherit",
+ height: "100%",
+ transitionDuration: {
+ default: tokens["--dowel-duration-normal"],
+ [REDUCED_MOTION]: "0.01ms",
+ },
+ transitionProperty: "width",
+ transitionTimingFunction: tokens["--dowel-ease-out"],
+ "[data-complete]": { backgroundColor: tokens["--dowel-success"] },
+ "[data-indeterminate]": {
+ animationDuration: "1.2s",
+ animationIterationCount: "infinite",
+ animationName: slide,
+ animationTimingFunction: "ease-in-out",
+ width: "40%",
+ },
+ },
+});
diff --git a/packages/dowel/src/components/progress/progress.test.tsx b/packages/dowel/src/components/progress/progress.test.tsx
new file mode 100644
index 0000000..c895ae1
--- /dev/null
+++ b/packages/dowel/src/components/progress/progress.test.tsx
@@ -0,0 +1,30 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { expectNoA11yViolations } from "../../../test/setup";
+import { Progress } from "./index";
+
+describe("Progress", () => {
+ it("announces and displays determinate progress", () => {
+ render( );
+ const progress = screen.getByRole("progressbar", {
+ name: "Upload artifacts",
+ });
+ expect(progress.getAttribute("aria-valuenow")).toBe("42");
+ expect(screen.getByText("42%")).toBeDefined();
+ });
+
+ it("supports indeterminate progress", () => {
+ render( );
+ expect(screen.getByRole("progressbar").hasAttribute("aria-valuenow")).toBe(
+ false,
+ );
+ });
+
+ it("has no accessibility violations", async () => {
+ const { container } = render(
+ ,
+ );
+ await expectNoA11yViolations(container);
+ });
+});
diff --git a/packages/dowel/src/components/slider/index.tsx b/packages/dowel/src/components/slider/index.tsx
new file mode 100644
index 0000000..c7b7f33
--- /dev/null
+++ b/packages/dowel/src/components/slider/index.tsx
@@ -0,0 +1,72 @@
+import { Slider as BaseSlider } from "@base-ui/react/slider";
+import * as stylex from "@stylexjs/stylex";
+import { forwardRef } from "react";
+import type { ComponentPropsWithoutRef, ElementType } from "react";
+import * as styles from "./slider.stylex";
+type P = Omit<
+ ComponentPropsWithoutRef,
+ "className" | "style"
+>;
+function sx(...v: stylex.StyleXStyles[]) {
+ const r = stylex.props(...v);
+ return { className: r.className, style: r.style };
+}
+const Root = forwardRef>(
+ function Root(props, ref) {
+ return (
+
+ );
+ },
+);
+const Label = forwardRef>(
+ function Label(props, ref) {
+ return (
+
+ );
+ },
+);
+const Value = forwardRef>(
+ function Value(props, ref) {
+ return (
+
+ );
+ },
+);
+const Control = forwardRef>(
+ function Control(props, ref) {
+ return (
+
+ );
+ },
+);
+const Track = forwardRef>(
+ function Track(props, ref) {
+ return (
+
+ );
+ },
+);
+const Indicator = forwardRef>(
+ function Indicator(props, ref) {
+ return (
+
+ );
+ },
+);
+const Thumb = forwardRef>(
+ function Thumb(props, ref) {
+ return (
+
+ );
+ },
+);
+export const Slider = { Root, Label, Value, Control, Track, Indicator, Thumb };
diff --git a/packages/dowel/src/components/slider/slider.stylex.ts b/packages/dowel/src/components/slider/slider.stylex.ts
new file mode 100644
index 0000000..c031930
--- /dev/null
+++ b/packages/dowel/src/components/slider/slider.stylex.ts
@@ -0,0 +1,64 @@
+import * as stylex from "@stylexjs/stylex";
+import { tokens } from "../../theme/tokens.stylex";
+export const slider = stylex.create({
+ root: { display: "grid", gap: "0.25rem", minWidth: 0, width: "100%" },
+ label: {
+ color: tokens["--dowel-text-primary"],
+ fontFamily: tokens["--dowel-font-sans"],
+ fontSize: "0.75rem",
+ fontWeight: 500,
+ },
+ value: {
+ color: tokens["--dowel-text-secondary"],
+ fontFamily: tokens["--dowel-font-mono"],
+ fontSize: "0.75rem",
+ gridColumn: "2",
+ gridRow: "1",
+ justifySelf: "end",
+ },
+ control: {
+ alignItems: "center",
+ display: "flex",
+ gridColumn: "1 / -1",
+ minHeight: "1.75rem",
+ touchAction: "none",
+ userSelect: "none",
+ width: "100%",
+ "[data-orientation=vertical]": {
+ height: "10rem",
+ justifyContent: "center",
+ minWidth: "1.75rem",
+ width: "auto",
+ },
+ },
+ track: {
+ backgroundColor: tokens["--dowel-bg-surface-3"],
+ borderRadius: tokens["--dowel-radius-pill"],
+ height: "0.25rem",
+ position: "relative",
+ width: "100%",
+ "[data-orientation=vertical]": { height: "100%", width: "0.25rem" },
+ },
+ indicator: {
+ backgroundColor: tokens["--dowel-accent"],
+ borderRadius: "inherit",
+ },
+ thumb: {
+ backgroundColor: tokens["--dowel-bg-elevated"],
+ borderColor: tokens["--dowel-border-strong"],
+ borderRadius: "50%",
+ borderStyle: "solid",
+ borderWidth: "1px",
+ boxShadow: tokens["--dowel-shadow-control"],
+ height: "1rem",
+ outline: "none",
+ width: "1rem",
+ ":has(:focus-visible)": {
+ outlineColor: tokens["--dowel-focus-ring"],
+ outlineOffset: "2px",
+ outlineStyle: "solid",
+ outlineWidth: "1px",
+ },
+ "[data-dragging]": { borderColor: tokens["--dowel-accent"] },
+ },
+});
diff --git a/packages/dowel/src/components/slider/slider.test.tsx b/packages/dowel/src/components/slider/slider.test.tsx
new file mode 100644
index 0000000..5b905fd
--- /dev/null
+++ b/packages/dowel/src/components/slider/slider.test.tsx
@@ -0,0 +1,23 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it } from "vitest";
+import { Slider } from "./index";
+describe("Slider", () => {
+ it("responds to keyboard changes", async () => {
+ render(
+
+ Volume
+
+
+
+
+
+
+ ,
+ );
+ const slider = screen.getByRole("slider", { name: "Volume" });
+ slider.focus();
+ await userEvent.keyboard("{ArrowRight}");
+ expect(slider.getAttribute("aria-valuenow")).toBe("26");
+ });
+});
diff --git a/packages/dowel/src/components/toast/index.tsx b/packages/dowel/src/components/toast/index.tsx
new file mode 100644
index 0000000..6c6dde4
--- /dev/null
+++ b/packages/dowel/src/components/toast/index.tsx
@@ -0,0 +1,178 @@
+import { Toast as BaseToast } from "@base-ui/react/toast";
+import {
+ CheckCircleIcon,
+ ExclamationCircleIcon,
+ ExclamationTriangleIcon,
+ InformationCircleIcon,
+ XMarkIcon,
+} from "@heroicons/react/16/solid";
+import * as stylex from "@stylexjs/stylex";
+import { useContext } from "react";
+import type { ReactNode } from "react";
+import type {
+ ToastManager,
+ ToastManagerAddOptions,
+ ToastManagerPromiseOptions,
+ ToastManagerUpdateOptions,
+ ToastRootToastObject,
+} from "@base-ui/react/toast";
+
+import { DowelThemeContext, themeStyles } from "../../theme/theme-provider";
+import * as styles from "./toast.stylex";
+
+export type ToastTone =
+ | "default"
+ | "info"
+ | "success"
+ | "warning"
+ | "danger"
+ | "loading";
+type ToastData = Record;
+export interface ToastOptions
+ extends Omit, "title" | "type"> {
+ description?: ReactNode;
+ tone?: ToastTone;
+}
+export interface ToastUpdateOptions
+ extends Omit, "type"> {
+ tone?: ToastTone;
+}
+export interface ToastProviderProps {
+ children?: ReactNode;
+ timeout?: number;
+ limit?: number;
+ manager?: ToastManager;
+}
+
+const globalManager = BaseToast.createToastManager();
+
+function addToast(title: ReactNode, options: ToastOptions = {}) {
+ const { tone = "default", ...rest } = options;
+ return globalManager.add({ ...rest, title, type: tone });
+}
+
+export const toast = Object.assign(addToast, {
+ success: (title: ReactNode, options: Omit = {}) =>
+ addToast(title, { ...options, tone: "success" }),
+ info: (title: ReactNode, options: Omit = {}) =>
+ addToast(title, { ...options, tone: "info" }),
+ warning: (title: ReactNode, options: Omit = {}) =>
+ addToast(title, { ...options, tone: "warning" }),
+ error: (title: ReactNode, options: Omit = {}) =>
+ addToast(title, {
+ ...options,
+ tone: "danger",
+ priority: options.priority ?? "high",
+ }),
+ loading: (title: ReactNode, options: Omit = {}) =>
+ addToast(title, {
+ ...options,
+ tone: "loading",
+ timeout: options.timeout ?? 0,
+ }),
+ dismiss: (id?: string) => globalManager.close(id),
+ update: (id: string, options: ToastUpdateOptions) => {
+ const { tone, ...rest } = options;
+ globalManager.update(id, tone ? { ...rest, type: tone } : rest);
+ },
+ promise: (
+ promise: Promise,
+ options: ToastManagerPromiseOptions,
+ ) => globalManager.promise(promise, options),
+});
+
+function sx(...values: stylex.StyleXStyles[]) {
+ const resolved = stylex.props(...values);
+ return { className: resolved.className, style: resolved.style };
+}
+
+function ToneIcon({ tone }: { tone: ToastTone }) {
+ const iconProps = { width: 16, height: 16 };
+ if (tone === "success") return ;
+ if (tone === "warning") return ;
+ if (tone === "danger") return ;
+ if (tone === "info") return ;
+ if (tone === "loading") return ;
+ return null;
+}
+
+function ToastItem({ item }: { item: ToastRootToastObject }) {
+ const tone = (
+ item.type === "error" ? "danger" : (item.type ?? "default")
+ ) as ToastTone;
+ return (
+
+
+ {tone !== "default" ? (
+
+
+
+ ) : null}
+
+
+
+
+
+ {item.actionProps ? (
+
+ ) : null}
+
+
+
+
+
+
+ );
+}
+
+function ToastViewport() {
+ const { toasts } = BaseToast.useToastManager();
+ const theme = useContext(DowelThemeContext);
+ const portalTheme = stylex.props(themeStyles[theme]);
+ return (
+
+
+ {toasts.map((item) => (
+
+ ))}
+
+
+ );
+}
+
+export function ToastProvider({
+ children,
+ timeout = 5000,
+ limit = 3,
+ manager = globalManager,
+}: ToastProviderProps) {
+ return (
+
+ {children}
+
+
+ );
+}
+
+export { globalManager as toastManager };
diff --git a/packages/dowel/src/components/toast/toast.stylex.ts b/packages/dowel/src/components/toast/toast.stylex.ts
new file mode 100644
index 0000000..1fe8089
--- /dev/null
+++ b/packages/dowel/src/components/toast/toast.stylex.ts
@@ -0,0 +1,220 @@
+import * as stylex from "@stylexjs/stylex";
+
+import { tokens } from "../../theme/tokens.stylex";
+
+const MOBILE = "@media (max-width: 639px)";
+const REDUCED_MOTION = "@media (prefers-reduced-motion: reduce)";
+
+export const toast = stylex.create({
+ viewport: {
+ bottom: { default: "1rem", [MOBILE]: "0.5rem" },
+ maxWidth: { default: "calc(100vw - 2rem)", [MOBILE]: "calc(100vw - 1rem)" },
+ outline: "none",
+ pointerEvents: "none",
+ position: "fixed",
+ right: { default: "1rem", [MOBILE]: "0.5rem" },
+ width: "22rem",
+ zIndex: 70,
+ },
+ root: {
+ "--toast-gap": "0.5rem",
+ "--toast-peek": "0.5rem",
+ "--toast-scale": "calc(max(0, 1 - (var(--toast-index) * 0.05)))",
+ "--toast-shrink": "calc(1 - var(--toast-scale))",
+ "--toast-stack-height":
+ "var(--toast-frontmost-height, var(--toast-height))",
+ "--toast-z-index": "calc(1000 - var(--toast-index))",
+ "--toast-expanded-y":
+ "calc((var(--toast-offset-y) * -1) + (var(--toast-index) * var(--toast-gap) * -1) + var(--toast-swipe-movement-y))",
+ backgroundColor: tokens["--dowel-bg-elevated"],
+ bottom: 0,
+ borderColor: tokens["--dowel-border-default"],
+ borderRadius: tokens["--dowel-radius-md"],
+ borderStyle: "solid",
+ borderWidth: tokens["--dowel-hairline"],
+ boxShadow: tokens["--dowel-shadow-popover"],
+ boxSizing: "border-box",
+ color: tokens["--dowel-text-primary"],
+ cursor: "default",
+ height: "var(--toast-stack-height)",
+ left: 0,
+ opacity: 1,
+ overflow: "hidden",
+ pointerEvents: "auto",
+ position: "absolute",
+ 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-stack-height)))) scale(var(--toast-scale))",
+ transformOrigin: "bottom center",
+ transitionDuration: {
+ default: "400ms, 200ms, 200ms, 200ms",
+ [REDUCED_MOTION]: "0.01ms",
+ },
+ transitionProperty: "transform, opacity, height, box-shadow",
+ transitionTimingFunction:
+ "cubic-bezier(0.22, 1, 0.36, 1), ease, cubic-bezier(0.22, 1, 0.36, 1), ease",
+ userSelect: "none",
+ width: "100%",
+ // CSS accepts a computed integer here; csstype only models literal integers.
+ zIndex: "var(--toast-z-index)" as unknown as number,
+ "::after": {
+ content: '""',
+ height: "calc(var(--toast-gap) + 1px)",
+ left: 0,
+ position: "absolute",
+ top: "100%",
+ width: "100%",
+ },
+ ":focus-visible": {
+ outlineColor: tokens["--dowel-focus-ring"],
+ outlineOffset: "-2px",
+ outlineStyle: "solid",
+ outlineWidth: "2px",
+ },
+ "[data-expanded]": {
+ height: "var(--toast-height)",
+ transform:
+ "translateX(var(--toast-swipe-movement-x)) translateY(var(--toast-expanded-y))",
+ },
+ "[data-starting-style]": {
+ opacity: 0,
+ transform: "translateY(calc(100% + 1rem)) scale(0.96)",
+ },
+ "[data-ending-style]": {
+ opacity: 0,
+ transform: "translateY(calc(100% + 1rem)) scale(0.96)",
+ },
+ "[data-ending-style][data-swipe-direction='up']": {
+ transform: "translateY(calc(var(--toast-swipe-movement-y) - 150%))",
+ },
+ "[data-ending-style][data-swipe-direction='down']": {
+ transform: "translateY(calc(var(--toast-swipe-movement-y) + 150%))",
+ },
+ "[data-ending-style][data-swipe-direction='left']": {
+ transform:
+ "translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--toast-expanded-y))",
+ },
+ "[data-ending-style][data-swipe-direction='right']": {
+ transform:
+ "translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--toast-expanded-y))",
+ },
+ "[data-swiping]": { transitionDuration: "0ms" },
+ "[data-limited]": { opacity: 0, pointerEvents: "none" },
+ },
+ content: {
+ alignItems: "flex-start",
+ display: "flex",
+ gap: "0.625rem",
+ minHeight: "3.25rem",
+ overflow: "hidden",
+ padding: "0.75rem",
+ transitionDuration: {
+ default: "250ms",
+ [REDUCED_MOTION]: "0.01ms",
+ },
+ transitionProperty: "opacity",
+ transitionTimingFunction: "cubic-bezier(0.22, 1, 0.36, 1)",
+ "[data-behind]": { opacity: 0 },
+ "[data-expanded]": { opacity: 1 },
+ },
+ icon: {
+ alignItems: "center",
+ animationDuration: {
+ default: "200ms",
+ [REDUCED_MOTION]: "0.01ms",
+ },
+ animationName: stylex.keyframes({
+ from: { opacity: 0, transform: "scale(0.8)" },
+ to: { opacity: 1, transform: "scale(1)" },
+ }),
+ animationTimingFunction: "cubic-bezier(0.22, 1, 0.36, 1)",
+ color: tokens["--dowel-text-tertiary"],
+ display: "inline-flex",
+ flexShrink: 0,
+ height: "1.25rem",
+ },
+ success: { color: tokens["--dowel-success"] },
+ warning: { color: tokens["--dowel-warning"] },
+ danger: { color: tokens["--dowel-danger"] },
+ info: { color: tokens["--dowel-accent"] },
+ spinner: {
+ animationDuration: "0.8s",
+ animationIterationCount: "infinite",
+ animationName: stylex.keyframes({ to: { transform: "rotate(360deg)" } }),
+ animationTimingFunction: "linear",
+ borderColor: tokens["--dowel-border-strong"],
+ borderRadius: "50%",
+ borderStyle: "solid",
+ borderTopColor: tokens["--dowel-accent"],
+ borderWidth: "2px",
+ height: "0.875rem",
+ width: "0.875rem",
+ },
+ text: {
+ display: "flex",
+ flex: 1,
+ flexDirection: "column",
+ gap: "0.125rem",
+ minWidth: 0,
+ },
+ title: {
+ color: tokens["--dowel-text-primary"],
+ fontFamily: tokens["--dowel-font-sans"],
+ fontSize: "0.8125rem",
+ fontWeight: 550,
+ lineHeight: 1.4,
+ margin: 0,
+ },
+ description: {
+ color: tokens["--dowel-text-secondary"],
+ fontFamily: tokens["--dowel-font-sans"],
+ fontSize: "0.75rem",
+ lineHeight: 1.45,
+ margin: 0,
+ },
+ controls: {
+ alignItems: "center",
+ display: "flex",
+ flexShrink: 0,
+ gap: "0.25rem",
+ },
+ action: {
+ backgroundColor: "transparent",
+ borderWidth: 0,
+ borderRadius: tokens["--dowel-radius-sm"],
+ color: tokens["--dowel-accent"],
+ fontFamily: tokens["--dowel-font-sans"],
+ fontSize: "0.75rem",
+ fontWeight: 550,
+ minHeight: "1.75rem",
+ padding: "0 0.375rem",
+ ":hover": { backgroundColor: tokens["--dowel-bg-hover"] },
+ ":focus-visible": {
+ outlineColor: tokens["--dowel-focus-ring"],
+ outlineOffset: "-2px",
+ outlineStyle: "solid",
+ outlineWidth: "2px",
+ },
+ },
+ close: {
+ alignItems: "center",
+ backgroundColor: "transparent",
+ borderWidth: 0,
+ borderRadius: tokens["--dowel-radius-sm"],
+ color: tokens["--dowel-text-tertiary"],
+ display: "inline-flex",
+ height: "1.75rem",
+ justifyContent: "center",
+ padding: 0,
+ width: "1.75rem",
+ ":hover": {
+ backgroundColor: tokens["--dowel-bg-hover"],
+ color: tokens["--dowel-text-primary"],
+ },
+ ":focus-visible": {
+ outlineColor: tokens["--dowel-focus-ring"],
+ outlineOffset: "-2px",
+ outlineStyle: "solid",
+ outlineWidth: "2px",
+ },
+ },
+});
diff --git a/packages/dowel/src/components/toast/toast.test.tsx b/packages/dowel/src/components/toast/toast.test.tsx
new file mode 100644
index 0000000..3a923e7
--- /dev/null
+++ b/packages/dowel/src/components/toast/toast.test.tsx
@@ -0,0 +1,115 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { expectNoA11yViolations } from "../../../test/setup";
+import { ToastProvider, toast } from "./index";
+
+afterEach(() => toast.dismiss());
+
+describe("Toast", () => {
+ it("creates and dismisses a global toast", async () => {
+ render(
+
+
+ toast.success("Repository created", {
+ description: "dowel is ready",
+ })
+ }
+ >
+ Create
+
+ ,
+ );
+ await userEvent.click(screen.getByRole("button", { name: "Create" }));
+ expect(await screen.findByText("Repository created")).toBeDefined();
+ await userEvent.hover(
+ screen
+ .getByText("Repository created")
+ .closest('[data-dowel-component="toast"]')!,
+ );
+ await userEvent.click(
+ screen.getByRole("button", { name: "Dismiss notification" }),
+ );
+ await waitFor(() =>
+ expect(screen.queryByText("Repository created")).toBeNull(),
+ );
+ });
+
+ it("runs toast actions", async () => {
+ const undo = vi.fn();
+ render(
+
+
+ toast("Archived", {
+ actionProps: { children: "Undo", onClick: undo },
+ })
+ }
+ >
+ Archive
+
+ ,
+ );
+ await userEvent.click(screen.getByRole("button", { name: "Archive" }));
+ await userEvent.click(await screen.findByRole("button", { name: "Undo" }));
+ expect(undo).toHaveBeenCalledOnce();
+ });
+
+ it("stacks toasts and expands them on hover", async () => {
+ render(
+
+ {
+ toast("First");
+ toast("Second");
+ toast("Third");
+ }}
+ >
+ Create stack
+
+ ,
+ );
+ await userEvent.click(screen.getByRole("button", { name: "Create stack" }));
+ await screen.findByText("Third");
+
+ const viewport = document.querySelector(
+ '[data-dowel-component="toast-viewport"]',
+ )!;
+ const roots = [
+ ...document.querySelectorAll(
+ '[data-dowel-component="toast"]',
+ ),
+ ];
+ expect(roots).toHaveLength(3);
+ expect(
+ roots.map((root) => root.style.getPropertyValue("--toast-index")).sort(),
+ ).toEqual(["0", "1", "2"]);
+ expect(document.querySelectorAll("[data-behind]")).toHaveLength(2);
+
+ await userEvent.hover(viewport);
+ await waitFor(() => {
+ expect(viewport.hasAttribute("data-expanded")).toBe(true);
+ expect(roots.every((root) => root.hasAttribute("data-expanded"))).toBe(
+ true,
+ );
+ });
+ });
+
+ it("has no accessibility violations", async () => {
+ const { container } = render(
+
+ toast.info("Synced")}>
+ Sync
+
+ ,
+ );
+ await userEvent.click(screen.getByRole("button", { name: "Sync" }));
+ await screen.findByText("Synced");
+ await expectNoA11yViolations(container.ownerDocument.body);
+ });
+});
diff --git a/packages/dowel/src/components/toggle-group/index.tsx b/packages/dowel/src/components/toggle-group/index.tsx
new file mode 100644
index 0000000..0e3c4e5
--- /dev/null
+++ b/packages/dowel/src/components/toggle-group/index.tsx
@@ -0,0 +1,50 @@
+import { Toggle as BaseToggle } from "@base-ui/react/toggle";
+import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group";
+import * as stylex from "@stylexjs/stylex";
+import { forwardRef } from "react";
+import type { ComponentPropsWithoutRef } from "react";
+
+import * as styles from "./toggle-group.stylex";
+
+type RootProps = Omit<
+ ComponentPropsWithoutRef,
+ "className" | "style"
+>;
+type ItemProps = Omit<
+ ComponentPropsWithoutRef,
+ "className" | "style"
+>;
+function sx(...v: stylex.StyleXStyles[]) {
+ const r = stylex.props(...v);
+ return { className: r.className, style: r.style };
+}
+
+const Root = forwardRef(
+ function ToggleGroupRoot(props, ref) {
+ return (
+
+ );
+ },
+);
+const Item = forwardRef(
+ function ToggleGroupItem(props, ref) {
+ return (
+
+ );
+ },
+);
+export const ToggleGroup = { Root, Item };
+export type {
+ RootProps as ToggleGroupRootProps,
+ ItemProps as ToggleGroupItemProps,
+};
diff --git a/packages/dowel/src/components/toggle-group/toggle-group.stylex.ts b/packages/dowel/src/components/toggle-group/toggle-group.stylex.ts
new file mode 100644
index 0000000..f73ab36
--- /dev/null
+++ b/packages/dowel/src/components/toggle-group/toggle-group.stylex.ts
@@ -0,0 +1,57 @@
+import * as stylex from "@stylexjs/stylex";
+import { tokens } from "../../theme/tokens.stylex";
+const COARSE = "@media (pointer: coarse)";
+export const toggle = stylex.create({
+ group: {
+ alignItems: "center",
+ backgroundColor: tokens["--dowel-bg-surface-2"],
+ borderColor: tokens["--dowel-border-default"],
+ borderRadius: tokens["--dowel-radius-md"],
+ borderStyle: "solid",
+ borderWidth: tokens["--dowel-hairline"],
+ display: "inline-flex",
+ gap: "0.125rem",
+ padding: "0.125rem",
+ "[data-orientation=vertical]": {
+ alignItems: "stretch",
+ flexDirection: "column",
+ },
+ },
+ item: {
+ alignItems: "center",
+ backgroundColor: "transparent",
+ borderRadius: tokens["--dowel-radius-sm"],
+ borderWidth: 0,
+ color: tokens["--dowel-text-secondary"],
+ display: "inline-flex",
+ fontFamily: tokens["--dowel-font-sans"],
+ fontSize: "0.75rem",
+ fontWeight: 500,
+ gap: "0.375rem",
+ height: { default: tokens["--dowel-control-md"], [COARSE]: "2.75rem" },
+ justifyContent: "center",
+ minWidth: tokens["--dowel-control-md"],
+ outline: "none",
+ paddingInline: "0.5rem",
+ userSelect: "none",
+ ":hover": {
+ backgroundColor: tokens["--dowel-bg-hover"],
+ color: tokens["--dowel-text-primary"],
+ },
+ ":focus-visible": {
+ outlineColor: tokens["--dowel-focus-ring"],
+ outlineOffset: "1px",
+ outlineStyle: "solid",
+ outlineWidth: "1px",
+ },
+ "[data-pressed]": {
+ backgroundColor: tokens["--dowel-bg-surface-1"],
+ boxShadow: tokens["--dowel-shadow-control"],
+ color: tokens["--dowel-text-primary"],
+ },
+ "[data-disabled]": {
+ color: tokens["--dowel-text-disabled"],
+ opacity: 0.55,
+ },
+ },
+});
diff --git a/packages/dowel/src/components/toggle-group/toggle-group.test.tsx b/packages/dowel/src/components/toggle-group/toggle-group.test.tsx
new file mode 100644
index 0000000..4eb7f98
--- /dev/null
+++ b/packages/dowel/src/components/toggle-group/toggle-group.test.tsx
@@ -0,0 +1,20 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it } from "vitest";
+import { ToggleGroup } from "./index";
+describe("ToggleGroup", () => {
+ it("selects one item by default", async () => {
+ render(
+
+ Left
+ Right
+ ,
+ );
+ await userEvent.click(screen.getByRole("button", { name: "Right" }));
+ expect(
+ screen
+ .getByRole("button", { name: "Right" })
+ .getAttribute("aria-pressed"),
+ ).toBe("true");
+ });
+});
diff --git a/packages/dowel/src/components/tree-view/index.tsx b/packages/dowel/src/components/tree-view/index.tsx
new file mode 100644
index 0000000..dbacd5e
--- /dev/null
+++ b/packages/dowel/src/components/tree-view/index.tsx
@@ -0,0 +1,182 @@
+import { ChevronRightIcon } from "@heroicons/react/16/solid";
+import * as stylex from "@stylexjs/stylex";
+import { useMemo, useRef, useState } from "react";
+import type { KeyboardEvent, ReactNode } from "react";
+import * as styles from "./tree-view.stylex";
+
+export interface TreeViewItem {
+ id: string;
+ label: ReactNode;
+ icon?: ReactNode;
+ disabled?: boolean;
+ children?: TreeViewItem[];
+}
+export interface TreeViewProps {
+ items: TreeViewItem[];
+ selectedId?: string;
+ defaultSelectedId?: string;
+ onSelectionChange?: (id: string) => void;
+ expandedIds?: string[];
+ defaultExpandedIds?: string[];
+ onExpandedChange?: (ids: string[]) => void;
+ ariaLabel?: string;
+}
+function sx(...v: stylex.StyleXStyles[]) {
+ const r = stylex.props(...v);
+ return { className: r.className, style: r.style };
+}
+export function TreeView({
+ items,
+ selectedId,
+ defaultSelectedId,
+ onSelectionChange,
+ expandedIds,
+ defaultExpandedIds = [],
+ onExpandedChange,
+ ariaLabel = "Tree",
+}: TreeViewProps) {
+ const [internalSelected, setInternalSelected] = useState(defaultSelectedId);
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpandedIds);
+ const rootRef = useRef(null);
+ const selected = selectedId ?? internalSelected;
+ const expanded = expandedIds ?? internalExpanded;
+ const expandedSet = useMemo(() => new Set(expanded), [expanded]);
+ function setExpanded(next: string[]) {
+ if (expandedIds === undefined) setInternalExpanded(next);
+ onExpandedChange?.(next);
+ }
+ function choose(id: string) {
+ if (selectedId === undefined) setInternalSelected(id);
+ onSelectionChange?.(id);
+ }
+ function toggle(id: string) {
+ setExpanded(
+ expandedSet.has(id)
+ ? expanded.filter((value) => value !== id)
+ : [...expanded, id],
+ );
+ }
+ function keyDown(event: KeyboardEvent) {
+ const nodes = [
+ ...(rootRef.current?.querySelectorAll("[data-tree-node]") ??
+ []),
+ ].filter((node) => node.offsetParent !== null);
+ const index = nodes.indexOf(event.target as HTMLElement);
+ if (index < 0) return;
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
+ event.preventDefault();
+ nodes[index + (event.key === "ArrowDown" ? 1 : -1)]?.focus();
+ } else if (event.key === "Home" || event.key === "End") {
+ event.preventDefault();
+ nodes[event.key === "Home" ? 0 : nodes.length - 1]?.focus();
+ } else if (event.key === "ArrowRight") {
+ const node = nodes[index]!;
+ const id = node.dataset.treeNode!;
+ if (node.getAttribute("aria-expanded") === "false") {
+ event.preventDefault();
+ toggle(id);
+ } else {
+ const next = nodes[index + 1];
+ if (next && Number(next.dataset.level) > Number(node.dataset.level)) {
+ event.preventDefault();
+ next.focus();
+ }
+ }
+ } else if (event.key === "ArrowLeft") {
+ const node = nodes[index]!;
+ const id = node.dataset.treeNode!;
+ if (node.getAttribute("aria-expanded") === "true") {
+ event.preventDefault();
+ toggle(id);
+ } else {
+ const parentId = node.dataset.parent;
+ const parent = parentId
+ ? rootRef.current?.querySelector(
+ `[data-tree-node="${CSS.escape(parentId)}"]`,
+ )
+ : null;
+ if (parent) {
+ event.preventDefault();
+ parent.focus();
+ }
+ }
+ }
+ }
+ function renderItems(
+ list: TreeViewItem[],
+ level: number,
+ parent?: string,
+ ): ReactNode {
+ return list.map((item) => {
+ const branch = Boolean(item.children?.length);
+ const open = branch && expandedSet.has(item.id);
+ return (
+
+ {
+ choose(item.id);
+ if (branch) toggle(item.id);
+ }}
+ {...sx(
+ styles.tree.node,
+ selected === item.id && styles.tree.selected,
+ )}
+ >
+
+ {branch ? (
+
+ ) : (
+
+ )}
+ {item.icon ? (
+
+ {item.icon}
+
+ ) : null}
+ {item.label}
+
+ {branch && open ? (
+
+ {renderItems(item.children!, level + 1, item.id)}
+
+ ) : null}
+
+ );
+ });
+ }
+ return (
+
+ {renderItems(items, 1)}
+
+ );
+}
diff --git a/packages/dowel/src/components/tree-view/tree-view.stylex.ts b/packages/dowel/src/components/tree-view/tree-view.stylex.ts
new file mode 100644
index 0000000..90fd19f
--- /dev/null
+++ b/packages/dowel/src/components/tree-view/tree-view.stylex.ts
@@ -0,0 +1,70 @@
+import * as stylex from "@stylexjs/stylex";
+import { tokens } from "../../theme/tokens.stylex";
+const REDUCED = "@media (prefers-reduced-motion: reduce)";
+export const tree = stylex.create({
+ root: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "1px",
+ listStyle: "none",
+ margin: 0,
+ padding: 0,
+ width: "100%",
+ },
+ group: { listStyle: "none", margin: 0, padding: 0 },
+ node: {
+ alignItems: "center",
+ backgroundColor: "transparent",
+ borderRadius: tokens["--dowel-radius-sm"],
+ borderWidth: 0,
+ color: tokens["--dowel-text-secondary"],
+ display: "flex",
+ fontFamily: tokens["--dowel-font-sans"],
+ fontSize: "0.8125rem",
+ gap: "0.375rem",
+ minHeight: tokens["--dowel-control-md"],
+ outline: "none",
+ padding: "0 0.5rem",
+ textAlign: "left",
+ width: "100%",
+ ":hover": {
+ backgroundColor: tokens["--dowel-bg-hover"],
+ color: tokens["--dowel-text-primary"],
+ },
+ ":focus-visible": {
+ outlineColor: tokens["--dowel-focus-ring"],
+ outlineOffset: "-1px",
+ outlineStyle: "solid",
+ outlineWidth: "1px",
+ },
+ ":disabled": { color: tokens["--dowel-text-disabled"], opacity: 0.55 },
+ },
+ selected: {
+ backgroundColor: tokens["--dowel-bg-surface-3"],
+ color: tokens["--dowel-text-primary"],
+ },
+ indent: { flexShrink: 0 },
+ chevron: {
+ color: tokens["--dowel-text-tertiary"],
+ flexShrink: 0,
+ transitionDuration: {
+ default: tokens["--dowel-duration-fast"],
+ [REDUCED]: "0.01ms",
+ },
+ transitionProperty: "transform",
+ },
+ open: { transform: "rotate(90deg)" },
+ spacer: { display: "inline-block", width: "0.75rem" },
+ icon: {
+ alignItems: "center",
+ color: tokens["--dowel-text-tertiary"],
+ display: "inline-flex",
+ flexShrink: 0,
+ },
+ label: {
+ minWidth: 0,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ },
+});
diff --git a/packages/dowel/src/components/tree-view/tree-view.test.tsx b/packages/dowel/src/components/tree-view/tree-view.test.tsx
new file mode 100644
index 0000000..a058bc0
--- /dev/null
+++ b/packages/dowel/src/components/tree-view/tree-view.test.tsx
@@ -0,0 +1,26 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it } from "vitest";
+import { TreeView } from "./index";
+describe("TreeView", () => {
+ it("expands branches and selects nodes", async () => {
+ render(
+ ,
+ );
+ await userEvent.click(screen.getByRole("treeitem", { name: "Docs" }));
+ expect(screen.getByRole("treeitem", { name: "API" })).toBeDefined();
+ expect(
+ screen
+ .getByRole("treeitem", { name: "Docs" })
+ .getAttribute("aria-selected"),
+ ).toBe("true");
+ });
+});
diff --git a/packages/dowel/src/index.ts b/packages/dowel/src/index.ts
index d11b52f..b210199 100644
--- a/packages/dowel/src/index.ts
+++ b/packages/dowel/src/index.ts
@@ -98,3 +98,66 @@ export type {
} from "./components/skeleton";
export { EmptyState } from "./components/empty-state";
export type { EmptyStateProps } from "./components/empty-state";
+export { Combobox } from "./components/combobox";
+export type { ComboboxOption, ComboboxProps } from "./components/combobox";
+export { GroupHeader, List, ListCell, ListRow } from "./components/list";
+export type {
+ GroupHeaderProps,
+ ListCellProps,
+ ListProps,
+ ListRowProps,
+} from "./components/list";
+export { Drawer } from "./components/drawer";
+export type {
+ DrawerPopupProps,
+ DrawerRootProps,
+ DrawerSide,
+} from "./components/drawer";
+export { Progress } from "./components/progress";
+export type { ProgressProps } from "./components/progress";
+export { ToastProvider, toast, toastManager } from "./components/toast";
+export type {
+ ToastOptions,
+ ToastProviderProps,
+ ToastTone,
+ ToastUpdateOptions,
+} from "./components/toast";
+export { CommandMenu } from "./components/command-menu";
+export type {
+ CommandMenuItem,
+ CommandMenuProps,
+} from "./components/command-menu";
+export { Accordion } from "./components/accordion";
+export { Collapsible } from "./components/collapsible";
+export { Breadcrumbs } from "./components/breadcrumbs";
+export type {
+ BreadcrumbItem,
+ BreadcrumbsProps,
+} from "./components/breadcrumbs";
+export { Pagination } from "./components/pagination";
+export type { PaginationProps } from "./components/pagination";
+export { ToggleGroup } from "./components/toggle-group";
+export type {
+ ToggleGroupItemProps,
+ ToggleGroupRootProps,
+} from "./components/toggle-group";
+export { Slider } from "./components/slider";
+export { TreeView } from "./components/tree-view";
+export type { TreeViewItem, TreeViewProps } from "./components/tree-view";
+export { Calendar, DatePicker } from "./components/calendar";
+export type { CalendarProps, DatePickerProps } from "./components/calendar";
+export {
+ CalendarDate,
+ getLocalTimeZone,
+ parseDate,
+ today,
+} from "@internationalized/date";
+export type { DateValue } from "@internationalized/date";
+export { ContextMenu } from "./components/context-menu";
+export type { ContextMenuTriggerProps } from "./components/context-menu";
+export { Dropzone, FileUpload } from "./components/file-upload";
+export type {
+ FileRejectReason,
+ FileUploadProps,
+ RejectedFile,
+} from "./components/file-upload";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6d219d5..d61514e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -81,12 +81,18 @@ importers:
'@heroicons/react':
specifier: 2.2.0
version: 2.2.0(react@19.2.8)
+ '@internationalized/date':
+ specifier: ^3.12.3
+ version: 3.12.3
'@stylexjs/stylex':
specifier: 0.19.0
version: 0.19.0
'@tanstack/react-table':
specifier: ^9.1.2
version: 9.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ react-aria-components:
+ specifier: ^1.20.0
+ version: 1.20.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
devDependencies:
'@stylexjs/eslint-plugin':
specifier: 0.19.0
@@ -809,6 +815,15 @@ packages:
cpu: [x64]
os: [win32]
+ '@internationalized/date@3.12.3':
+ resolution: {integrity: sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q==}
+
+ '@internationalized/number@3.6.7':
+ resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==}
+
+ '@internationalized/string@3.2.10':
+ resolution: {integrity: sha512-PDx6//vHSpRnHfxqMqto11zQvhsaU74O3mKv2F/0eicGZcl9NLjQmGlbHz/LsJh5tLKp4A4L7ZVTzN1/MmMTvA==}
+
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -880,6 +895,11 @@ packages:
'@quansync/fs@1.0.0':
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
+ '@react-types/shared@3.36.1':
+ resolution: {integrity: sha512-AzsuD9OfxTOZMMvTRhlN3oHBwOmFN7tDh27LzqmHt4+uOgPhJT7ZM7/kVs/8/o0WxayMUIk3hBmCFRHv1FUoag==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
+
'@rolldown/binding-android-arm64@1.2.3':
resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1000,6 +1020,9 @@ packages:
peerDependencies:
unplugin: ^2.3.11
+ '@swc/helpers@0.5.23':
+ resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
+
'@tanstack/history@1.162.1':
resolution: {integrity: sha512-DR9t6lfLVdrjgCwpglrR9DR7Ok8/HlXjcOE+goWXF3zyuLUO/ug7vMbSFxTqrQTtbRghJfyhmIZ0S6LhPIy44w==}
engines: {node: '>=20.19'}
@@ -1434,6 +1457,10 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ aria-hidden@1.2.6:
+ resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
+ engines: {node: '>=10'}
+
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
@@ -1483,6 +1510,13 @@ packages:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'}
+ client-only@0.0.1:
+ resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
@@ -1922,6 +1956,18 @@ packages:
quansync@1.0.0:
resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==}
+ react-aria-components@1.20.0:
+ resolution: {integrity: sha512-BMbpIgoV9aELeBrB0Y120NgoigHb5OdcJwc+4e7uSnbTbamea6lo+gqcc4LAxzMaK3Jf+7LI1oCDE6yANsmxIQ==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
+ react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
+
+ react-aria@3.51.0:
+ resolution: {integrity: sha512-AyWLw0XR38cFPwBu/ErgGaVrc5dupLEKmRlMXTGvFKOtbaGRQ2+yQJkjVhpdHhoRhU4+G+tJDFeHDTS8tK3bfQ==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
+ react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
+
react-dom@19.2.8:
resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
peerDependencies:
@@ -1930,6 +1976,11 @@ packages:
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+ react-stately@3.49.0:
+ resolution: {integrity: sha512-13iNq2KzBrRAzxRc+n53hgROfIistiYY/sPtIhCw1qUB7/kmo+X1xEU2uiS5zcCIrc55AUPwoHqOIIpKWSwB9A==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
+
react@19.2.8:
resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
engines: {node: '>=0.10.0'}
@@ -2957,6 +3008,18 @@ snapshots:
'@img/sharp-win32-x64@0.35.2':
optional: true
+ '@internationalized/date@3.12.3':
+ dependencies:
+ '@swc/helpers': 0.5.23
+
+ '@internationalized/number@3.6.7':
+ dependencies:
+ '@swc/helpers': 0.5.23
+
+ '@internationalized/string@3.2.10':
+ dependencies:
+ '@swc/helpers': 0.5.23
+
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -3037,6 +3100,10 @@ snapshots:
dependencies:
quansync: 1.0.0
+ '@react-types/shared@3.36.1(react@19.2.8)':
+ dependencies:
+ react: 19.2.8
+
'@rolldown/binding-android-arm64@1.2.3':
optional: true
@@ -3128,6 +3195,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@swc/helpers@0.5.23':
+ dependencies:
+ tslib: 2.8.1
+
'@tanstack/history@1.162.1': {}
'@tanstack/hotkeys@0.8.0':
@@ -3568,6 +3639,10 @@ snapshots:
argparse@2.0.1: {}
+ aria-hidden@1.2.6:
+ dependencies:
+ tslib: 2.8.1
+
aria-query@5.3.0:
dependencies:
dequal: 2.0.3
@@ -3615,6 +3690,10 @@ snapshots:
dependencies:
readdirp: 5.1.1
+ client-only@0.0.1: {}
+
+ clsx@2.1.1: {}
+
convert-source-map@2.0.0: {}
cookie-es@3.1.1: {}
@@ -3983,6 +4062,32 @@ snapshots:
quansync@1.0.0: {}
+ react-aria-components@1.20.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ dependencies:
+ '@internationalized/date': 3.12.3
+ '@internationalized/string': 3.2.10
+ '@react-types/shared': 3.36.1(react@19.2.8)
+ '@swc/helpers': 0.5.23
+ client-only: 0.0.1
+ react: 19.2.8
+ react-aria: 3.51.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ react-dom: 19.2.8(react@19.2.8)
+ react-stately: 3.49.0(react@19.2.8)
+
+ react-aria@3.51.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ dependencies:
+ '@internationalized/date': 3.12.3
+ '@internationalized/number': 3.6.7
+ '@internationalized/string': 3.2.10
+ '@react-types/shared': 3.36.1(react@19.2.8)
+ '@swc/helpers': 0.5.23
+ aria-hidden: 1.2.6
+ clsx: 2.1.1
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ react-stately: 3.49.0(react@19.2.8)
+ use-sync-external-store: 1.6.0(react@19.2.8)
+
react-dom@19.2.8(react@19.2.8):
dependencies:
react: 19.2.8
@@ -3990,6 +4095,16 @@ snapshots:
react-is@17.0.2: {}
+ react-stately@3.49.0(react@19.2.8):
+ dependencies:
+ '@internationalized/date': 3.12.3
+ '@internationalized/number': 3.6.7
+ '@internationalized/string': 3.2.10
+ '@react-types/shared': 3.36.1(react@19.2.8)
+ '@swc/helpers': 0.5.23
+ react: 19.2.8
+ use-sync-external-store: 1.6.0(react@19.2.8)
+
react@19.2.8: {}
readdirp@5.1.1: {}
@@ -4163,8 +4278,7 @@ snapshots:
- oxc-resolver
- vue-tsc
- tslib@2.8.1:
- optional: true
+ tslib@2.8.1: {}
typescript@5.9.3: {}