@aoctech/ui 0.1.3 → 0.1.5

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.
@@ -5,7 +5,6 @@ interface CalendarProps {
5
5
  disabled?: Matcher | Matcher[];
6
6
  className?: string;
7
7
  }
8
- /** An inline, keyboard-operable calendar. Use DatePicker for a field trigger. */
9
8
  declare function Calendar({ selected, onSelect, disabled, className }: CalendarProps): import("react").JSX.Element;
10
9
  interface DatePickerProps {
11
10
  value?: Date;
@@ -1,14 +1,26 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Popover } from "@base-ui/react/popover";
4
+ import { format } from "date-fns";
4
5
  import { ptBR } from "date-fns/locale/pt-BR";
5
6
  import { useState } from "react";
6
7
  import { DayPicker } from "react-day-picker";
7
8
  import { cn } from "../lib/cn";
8
9
  import { useThemeScope } from "./theme-provider";
9
10
  /** An inline, keyboard-operable calendar. Use DatePicker for a field trigger. */
11
+ // react-day-picker's default labels are English ("Go to the Previous Month",
12
+ // "Today, …"): a screen reader in a pt-BR product read half its calendar in
13
+ // another language.
14
+ const LABELS = {
15
+ labelPrevious: () => "Mês anterior",
16
+ labelNext: () => "Próximo mês",
17
+ labelDayButton: (date, modifiers) => `${modifiers.today ? "Hoje, " : ""}${format(date, "PPPP", { locale: ptBR })}${modifiers.selected ? ", selecionado" : ""}`,
18
+ };
10
19
  function Calendar({ selected, onSelect, disabled, className }) {
11
- return (_jsx(DayPicker, { mode: "single", selected: selected, onSelect: onSelect, disabled: disabled, locale: ptBR, classNames: {
20
+ return (_jsx(DayPicker, { mode: "single",
21
+ // Open on the selected date's month, not today's: editing a date months
22
+ // away otherwise starts by paging back to it.
23
+ defaultMonth: selected, labels: LABELS, selected: selected, onSelect: onSelect, disabled: disabled, locale: ptBR, classNames: {
12
24
  month_caption: "calendar-month-caption",
13
25
  caption_label: "calendar-caption-label",
14
26
  nav: "calendar-nav",
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * A panel for a task that needs a form but not a new page (shadcn's Drawer, on
4
+ * Base UI's Drawer primitive): from the right on a wide screen, where the page
5
+ * behind stays readable, and from the bottom on a phone, where a side panel
6
+ * would be the whole screen anyway. Swipe it away in the direction it came
7
+ * from; focus trap, Escape, scroll lock and restore-focus come from Base UI.
8
+ *
9
+ * The body is the caller's: a form brings its own buttons, so there is no fixed
10
+ * footer unless `footer` is given (it stays visible while the body scrolls).
11
+ */
12
+ declare const WIDTH: {
13
+ readonly md: "sm:max-w-md";
14
+ readonly lg: "sm:max-w-xl";
15
+ };
16
+ interface DrawerProps {
17
+ open: boolean;
18
+ onClose: () => void;
19
+ title: string;
20
+ description?: string;
21
+ children?: ReactNode;
22
+ footer?: ReactNode;
23
+ size?: keyof typeof WIDTH;
24
+ }
25
+ declare function Drawer({ open, onClose, title, description, children, footer, size }: DrawerProps): import("react").JSX.Element;
26
+ export { Drawer, type DrawerProps };
@@ -0,0 +1,47 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Drawer as Primitive } from "@base-ui/react/drawer";
4
+ import { useEffect, useState } from "react";
5
+ import { cn } from "../lib/cn";
6
+ import { Button } from "./button";
7
+ import { useThemeScope } from "./theme-provider";
8
+ /**
9
+ * A panel for a task that needs a form but not a new page (shadcn's Drawer, on
10
+ * Base UI's Drawer primitive): from the right on a wide screen, where the page
11
+ * behind stays readable, and from the bottom on a phone, where a side panel
12
+ * would be the whole screen anyway. Swipe it away in the direction it came
13
+ * from; focus trap, Escape, scroll lock and restore-focus come from Base UI.
14
+ *
15
+ * The body is the caller's: a form brings its own buttons, so there is no fixed
16
+ * footer unless `footer` is given (it stays visible while the body scrolls).
17
+ */
18
+ const WIDTH = {
19
+ md: "sm:max-w-md",
20
+ lg: "sm:max-w-xl",
21
+ };
22
+ /** Right on a wide screen, down on a phone; down until the first effect runs (SSR). */
23
+ function useSide() {
24
+ const [wide, setWide] = useState(false);
25
+ useEffect(() => {
26
+ // Test environments (jsdom) and very old browsers have no matchMedia.
27
+ if (typeof window.matchMedia !== "function")
28
+ return;
29
+ const q = window.matchMedia("(min-width: 640px)");
30
+ const update = () => setWide(q.matches);
31
+ update();
32
+ q.addEventListener("change", update);
33
+ return () => q.removeEventListener("change", update);
34
+ }, []);
35
+ return wide ? "right" : "down";
36
+ }
37
+ function Drawer({ open, onClose, title, description, children, footer, size = "md" }) {
38
+ const { theme, density } = useThemeScope();
39
+ const side = useSide();
40
+ return (_jsx(Primitive.Root, { open: open, onOpenChange: next => !next && onClose(), swipeDirection: side, children: _jsxs(Primitive.Portal, { "data-ctech-theme": theme, "data-density": density, children: [_jsx(Primitive.Backdrop, { className: cn("fixed inset-0 z-40 bg-black/40 transition-opacity duration-200 ease-out", "data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none") }), _jsx(Primitive.Viewport, { className: "fixed inset-0 z-50", children: _jsxs(Primitive.Popup, { className: cn("fixed z-50 flex flex-col bg-background text-foreground shadow-modal outline-none", "transition-transform duration-200 ease-out data-[swiping]:transition-none motion-reduce:transition-none", side === "right"
41
+ ? cn("inset-y-0 right-0 w-full border-l border-border", WIDTH[size], "[transform:translateX(var(--drawer-swipe-movement-x,0px))]", "data-[starting-style]:[transform:translateX(100%)] data-[ending-style]:[transform:translateX(100%)]")
42
+ : cn("inset-x-0 bottom-0 max-h-[90dvh] rounded-t-xl border-t border-border", "[transform:translateY(var(--drawer-swipe-movement-y,0px))]", "data-[starting-style]:[transform:translateY(100%)] data-[ending-style]:[transform:translateY(100%)]")), children: [side === "down" && _jsx("div", { "aria-hidden": true, className: "mx-auto mt-2 h-1.5 w-10 shrink-0 rounded-full bg-border" }), _jsxs("header", { className: "flex items-start justify-between gap-4 border-b border-border px-5 py-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Primitive.Title, { className: "text-base font-semibold text-foreground", children: title }), description && _jsx(Primitive.Description, { className: "text-sm text-muted-foreground", children: description })] }), _jsx(Primitive.Close, { render: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Fechar", className: "-mr-2 shrink-0 text-muted-foreground" }), children: _jsx(CloseGlyph, {}) })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: children }), footer && _jsx("footer", { className: "border-t border-border bg-surface px-5 py-3", children: footer })] }) })] }) }));
43
+ }
44
+ function CloseGlyph() {
45
+ return (_jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": true, className: "size-4", children: _jsx("path", { d: "m4 4 8 8M12 4l-8 8", strokeLinecap: "round" }) }));
46
+ }
47
+ export { Drawer };
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export { EmptyState, type EmptyStateProps } from "./components/empty-state";
8
8
  export { Field, type FieldProps } from "./components/field";
9
9
  export { Input } from "./components/input";
10
10
  export { Modal, type ModalProps } from "./components/modal";
11
+ export { Drawer, type DrawerProps } from "./components/drawer";
11
12
  export { PageHeader, type PageHeaderProps } from "./components/page-header";
12
13
  export { Separator } from "./components/separator";
13
14
  export { Skeleton } from "./components/skeleton";
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export { EmptyState } from "./components/empty-state";
8
8
  export { Field } from "./components/field";
9
9
  export { Input } from "./components/input";
10
10
  export { Modal } from "./components/modal";
11
+ export { Drawer } from "./components/drawer";
11
12
  export { PageHeader } from "./components/page-header";
12
13
  export { Separator } from "./components/separator";
13
14
  export { Skeleton } from "./components/skeleton";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aoctech/ui",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Shared CTech UI primitives: Base UI behaviour, Tailwind classes, no colours of its own. Themed entirely by the consuming app's CSS variables.",
5
5
  "repository": {
6
6
  "type": "git",