# منتقي التاريخ المحوّلات

> استخدام منتقي التاريخ من slotsmith مع shadcn/ui وMUI وChakra وAnt Design وRadix Themes.

## shadcn/ui

يعرض هذا المحوّل `DatePicker` بأجزاء shadcn/ui. انسخ الملف إلى تطبيقك ومرّر الخريطة إلى `components`، وكل فتحة لا تمرّرها تبقى على بديلها الافتراضي.

```tsx
/**
 * DatePicker for shadcn/ui
 *
 * A `components` map that renders the slotsmith date picker with shadcn/ui
 * primitives, written for shadcn/ui on Tailwind CSS v4. Copy the file, keep
 * the parts you want, and pass the map as `components={shadcnComponents}`;
 * every slot left out keeps its fallback.
 *
 * The `@/components/ui/*` and `@/lib/utils` imports are the app's own
 * shadcn/ui files.
 */
import { useMemo, type RefObject } from "react";
import {
  useDatePickerContext,
  type DatePickerComponents,
} from "slotsmith/date-picker";
import { Button } from "@/components/ui/button";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
} from "@/components/ui/input-group";
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
import { Popover, PopoverAnchor, PopoverContent } from "@/components/ui/popover";
import { cn } from "@/lib/utils";

/**
 * shadcn/ui date picker parts
 *
 * What the `components` map looks like against shadcn/ui. Element parts are
 * plain Tailwind on the DOM node the component already renders; only the
 * widget parts need a body.
 *
 * The range band is built from stacked `data-*` variants rather than from
 * class order: `[data-in-range][data-range-start]` outranks `[data-in-range]`
 * on specificity, so which corners are round never depends on how Tailwind
 * happens to sort its output.
 */

const Chevron = ({ flip }: { flip?: boolean }) => (
  <svg
    className={cn("size-4", flip && "rotate-180")}
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    strokeWidth="2"
    strokeLinecap="round"
    strokeLinejoin="round"
    aria-hidden="true"
  >
    <path d="m15 18-6-6 6-6" />
  </svg>
);

/**
 * The caption's two dropdowns: shadcn's `NativeSelect`, as shadcn's own
 * calendar uses native selects for its month and year. The browser draws the
 * open list itself, and `color-scheme` makes it dark in dark mode.
 */
const captionSelect =
  "h-7 border-transparent py-0 ps-2 pe-7 text-sm font-medium shadow-none scheme-light dark:scheme-dark";
/** The open list takes the popover's colours: on Windows the browser draws it from the options' own. */
const captionOption = "bg-popover text-popover-foreground";

/**
 * Calendar layout
 *
 * The calendar's own structure, which no slot renders: the header row, the
 * seven columns of the weekday and week rows, and the centred cells. Laid
 * out here too, so the picker holds together without slotsmith's stylesheet.
 * Tailwind reads `_` in a selector as a space, so the class names' own
 * underscores are escaped, in `String.raw` so the backslash stays.
 */
const calendarLayout = cn(
  String.raw`[&_.sdp\_\_header]:flex [&_.sdp\_\_header]:items-center [&_.sdp\_\_header]:justify-between [&_.sdp\_\_header]:gap-1 [&_.sdp\_\_header]:pb-2`,
  String.raw`[&_.sdp\_\_weekdays]:grid [&_.sdp\_\_weekdays]:grid-cols-7 [&_.sdp\_\_week]:grid [&_.sdp\_\_week]:grid-cols-7`,
  String.raw`[&_.sdp\_\_columnheader]:grid [&_.sdp\_\_columnheader]:min-w-0 [&_.sdp\_\_columnheader]:place-items-center`,
  String.raw`[&_.sdp\_\_cell]:grid [&_.sdp\_\_cell]:min-w-0 [&_.sdp\_\_cell]:place-items-center`,
);

export const shadcnComponents: Partial<DatePickerComponents> = {
  Root: ({ className, ...props }) => (
    <div
      data-slot="date-picker"
      className={cn("relative w-full max-w-72", className)}
      {...props}
    />
  ),

  /**
   * shadcn's `InputGroup`: the field frame, with its border, ring and
   * destructive state. With the text field it holds the `InputGroupInput`
   * and the buttons; as the single button trigger it is the combobox itself,
   * a frame rather than a `Button` because it holds the clear button and a
   * button cannot hold another.
   */
  Trigger: ({ className, ...props }) => (
    <InputGroup
      className={cn(
        "min-h-9 cursor-pointer gap-2 bg-background px-3 text-sm",
        "data-open:border-ring data-open:ring-[3px] data-open:ring-ring/50",
        "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
        "aria-disabled:cursor-not-allowed aria-disabled:opacity-50",
        /** The text-field trigger: the field and its buttons sit right against the frame. */
        "data-[trigger=input]:cursor-text data-[trigger=input]:gap-0 data-[trigger=input]:px-0",
        "data-[trigger=input]:data-disabled:cursor-not-allowed data-[trigger=input]:data-disabled:opacity-50",
        "data-invalid:border-destructive data-invalid:ring-destructive/20 dark:data-invalid:ring-destructive/40",
        "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
        className,
      )}
      {...props}
    />
  ),

  Value: ({ text, placeholder, empty }) => (
    <span className={cn("flex-1 truncate", empty && "text-muted-foreground")}>
      {empty ? placeholder : text}
    </span>
  ),

  /** The group's `InputGroupInput`: the frame draws the field around it. */
  Input: ({ className, ...props }) => (
    <InputGroupInput
      className={cn("h-auto min-w-0 px-3", className)}
      {...props}
    />
  ),

  /** An icon `InputGroupButton` around the calendar glyph, in the inline-end addon. */
  Toggle: ({ className, type = "button", ...props }) => (
    <InputGroupAddon
      align="inline-end"
      className="p-0 pe-1.5 has-[>button]:me-0"
    >
      <InputGroupButton
        type={type}
        size="icon-xs"
        className={cn("size-7", className)}
        {...props}
      />
    </InputGroupAddon>
  ),

  Icon: ({ open }) => (
    <svg
      className={cn(
        "size-4 shrink-0 opacity-60 transition-opacity",
        open && "text-primary opacity-100",
      )}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <rect x="3" y="5" width="18" height="16" rx="2" />
      <path d="M8 3v4M16 3v4M3 11h18" />
    </svg>
  ),

  /** An icon `InputGroupButton`; the click stops here so it does not reach the trigger. */
  Clear: ({ onClick, ...aria }) => (
    <InputGroupAddon align="inline-end" className="p-0 has-[>button]:me-0">
      <InputGroupButton
        size="icon-xs"
        className="size-6 text-muted-foreground hover:text-foreground"
        onClick={(event) => {
          event.stopPropagation();
          onClick();
        }}
        {...aria}
      >
        <svg
          viewBox="0 0 24 24"
          className="size-3.5"
          fill="none"
          stroke="currentColor"
          strokeWidth="2.5"
          aria-hidden="true"
        >
          <path d="M18 6 6 18M6 6l12 12" />
        </svg>
      </InputGroupButton>
    </InputGroupAddon>
  ),

  /**
   * shadcn's `Popover`, anchored to the trigger: it portals the calendar,
   * places and animates it as every shadcn popover does. Focus stays with
   * the picker, which moves it into the grid itself. The content keeps every
   * prop the part is given, its `id` included, which is how the picker knows
   * the portaled calendar is still part of it; the built-in position style is
   * left out.
   */
  Popup: function ShadcnPopup({ className, style: _style, ...props }) {
    const { position } = useDatePickerContext();
    const anchor = useMemo(
      () => ({ current: position.anchor }),
      [position.anchor],
    );
    if (!position.anchor) return null;
    return (
      <Popover open>
        <PopoverAnchor virtualRef={anchor as RefObject<HTMLElement>} />
        <PopoverContent
          align="start"
          side={position.placement === "top-start" ? "top" : "bottom"}
          onOpenAutoFocus={(event) => event.preventDefault()}
          onCloseAutoFocus={(event) => event.preventDefault()}
          // Its own stacking context, so the range band behind each day stays above the surface.
          className={cn("isolate w-72 p-2", calendarLayout, className)}
          {...props}
        />
      </Popover>
    );
  },

  Calendar: ({ className, ...props }) => (
    <div className={cn("grid gap-0.5", className)} {...props} />
  ),

  Caption: ({
    monthIndex,
    year,
    months,
    years,
    onMonthChange,
    onYearChange,
    labels,
  }) => (
    <span className="flex items-center gap-1">
      <NativeSelect
        size="sm"
        className={captionSelect}
        aria-label={labels.month}
        value={monthIndex}
        onChange={(event) => onMonthChange(Number(event.target.value))}
      >
        {months.map((name, index) => (
          <NativeSelectOption
            key={name}
            value={index}
            className={captionOption}
          >
            {name}
          </NativeSelectOption>
        ))}
      </NativeSelect>
      <NativeSelect
        size="sm"
        className={captionSelect}
        aria-label={labels.year}
        value={year}
        onChange={(event) => onYearChange(Number(event.target.value))}
      >
        {years.map((value) => (
          <NativeSelectOption
            key={value}
            value={value}
            className={captionOption}
          >
            {value}
          </NativeSelectOption>
        ))}
      </NativeSelect>
    </span>
  ),

  /** A ghost icon `Button` with the chevron, mirrored for the next month. */
  Nav: ({ direction, onClick, disabled, ...aria }) => (
    <Button
      type="button"
      variant="ghost"
      size="icon"
      onClick={onClick}
      disabled={disabled}
      className="size-7 disabled:opacity-35"
      {...aria}
    >
      <Chevron flip={direction === "next"} />
    </Button>
  ),

  /**
   * One column header. Clipped rather than wrapped: the column is one cell
   * wide, and locales whose short weekday names are full words would
   * otherwise make the row two lines tall.
   */
  Weekday: ({ label }) => (
    <span className="block truncate py-1 text-center text-[0.7rem] font-medium text-muted-foreground">
      {label}
    </span>
  ),

  /**
   * One cell of the grid.
   *
   * A fixed `size-8` square, centred in its column, so the button is exactly
   * as wide as it is tall rather than stretching to the column. Blocked days
   * are dimmed through `data-[disabled]`: they stay focusable, so the
   * `disabled:` variant never applies.
   *
   * The band is a `before:` layer at a negative z-index: it reaches half the
   * gutter past each inline edge so neighbouring days meet across the gap,
   * and it sits behind both the picked chip and the label.
   */
  Day: ({ className, type = "button", ...props }) => (
    <Button
      type={type}
      variant="ghost"
      size="icon"
      className={cn(
        "relative size-8 justify-self-center rounded-md text-[0.8125rem] font-normal",
        "data-disabled:cursor-not-allowed data-disabled:opacity-30",
        "data-outside:text-muted-foreground",
        "data-today:inset-ring-1 data-today:inset-ring-primary/60",
        "data-in-range:before:absolute data-in-range:before:inset-y-0 data-in-range:before:content-['']",
        "data-in-range:before:inset-x-[calc(var(--sdp-gap,0.25rem)/-2-0.5px)]",
        "data-in-range:before:z-[-1] data-in-range:before:bg-primary/15",
        "data-in-preview:bg-primary/10",
        "data-in-range:data-range-start:before:start-[0]",
        "data-in-range:data-range-start:before:rounded-s-md",
        "data-in-range:data-range-end:before:end-[0]",
        "data-in-range:data-range-end:before:rounded-e-md",
        "data-selected:bg-primary data-selected:font-semibold data-selected:text-primary-foreground",
        "data-selected:hover:bg-primary data-selected:hover:text-primary-foreground",
        className,
      )}
      {...props}
    />
  ),

  DayContent: ({ day }) => <span>{day.day}</span>,

  /** Outline `Button` chips for the presets, and a ghost `Button` for today. */
  Footer: ({ presets, onPreset, onToday, todayLabel }) => (
    <div className="mt-2 flex flex-wrap items-center gap-1 border-t border-border pt-2">
      {presets.map((preset) => (
        <Button
          key={preset.label}
          type="button"
          variant="outline"
          size="sm"
          onClick={() => onPreset(preset.value)}
          className="h-6 rounded-full px-2 text-xs font-normal"
        >
          {preset.label}
        </Button>
      ))}
      <Button
        type="button"
        variant="ghost"
        size="sm"
        onClick={onToday}
        className="ms-auto h-6 rounded-full px-2 text-xs font-normal text-muted-foreground"
      >
        {todayLabel}
      </Button>
    </div>
  ),
};
```

## MUI

يعرض هذا المحوّل `DatePicker` بأجزاء MUI. انسخ الملف إلى تطبيقك ومرّر الخريطة إلى `components`، وكل فتحة لا تمرّرها تبقى على بديلها الافتراضي.

```tsx
/**
 * DatePicker for MUI
 *
 * A `components` map that renders the slotsmith date picker with MUI
 * primitives, tested against MUI v7. Copy the file, keep the parts you want,
 * and pass the map as `components={muiComponents}`; every slot left out keeps
 * its fallback.
 */
import Box from "@mui/material/Box";
import Chip from "@mui/material/Chip";
import IconButton from "@mui/material/IconButton";
import InputBase from "@mui/material/InputBase";
import Paper from "@mui/material/Paper";
import Popper from "@mui/material/Popper";
import Select from "@mui/material/Select";
import type { SxProps, Theme } from "@mui/material/styles";
import Typography from "@mui/material/Typography";
import { useDatePickerContext, type DatePickerComponents } from "slotsmith/date-picker";

/**
 * MUI date picker parts
 *
 * Built from MUI's primitives — `Paper`, `IconButton`, `Select`, `Typography`,
 * `Box`, `Chip` — not from MUI's own date picker, which is a competing engine
 * rather than a set of parts and ships in a separate package. The result
 * wears Material and follows the theme, dark mode included.
 *
 * This is the reason the element parts `Omit` `color` from their DOM props:
 * MUI reuses that attribute name for its own semantic palette prop, so a slot
 * that forwarded the DOM `color` would collide with `<IconButton color>`.
 */

/** The day cell, in pixels. Matches `--sdp-cell`, so the square fills its column. */
const DAY_SIZE = 32;
/** The chip's corner, shared with the two ends of the band so they line up. */
const DAY_RADIUS = 8;

const ChevronIcon = ({ flip }: { flip: boolean }) => (
  <Box
    component="svg"
    viewBox="0 0 24 24"
    aria-hidden="true"
    sx={{
      width: 18,
      height: 18,
      fill: "none",
      stroke: "currentColor",
      strokeWidth: 2,
      transform: flip ? "rotate(180deg)" : undefined,
    }}
  >
    <path d="m15 18-6-6 6-6" strokeLinecap="round" strokeLinejoin="round" />
  </Box>
);

/**
 * Caption select
 *
 * The month and year dropdowns share their whole look.
 *
 * These are `native`, not `MenuItem` lists, for two reasons that both come
 * from MUI rendering its menu as a `Modal`: a portalled menu is outside the
 * picker's root, so choosing a month would count as an outside click and
 * close the calendar; and with the portal disabled the modal marks the page
 * behind it `aria-hidden` while focus is still inside it, which the browser
 * reports.
 */
const captionSx: SxProps<Theme> = {
  "& .MuiNativeSelect-select": { py: 0.25, typography: "body2", fontWeight: 600 },
  "&::before, &::after": { display: "none" },
};

/**
 * Calendar style
 *
 * The surface, in its own stacking context so the range band behind each
 * day stays above it, and the calendar's own structure, which no slot
 * renders: the header row, the seven columns of the weekday and week rows,
 * and the centred cells. Laid out here, so the picker holds together
 * without slotsmith's stylesheet.
 */
const calendarSx: SxProps<Theme> = {
  width: 288,
  p: 1,
  isolation: "isolate",
  "& .sdp__header": { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 0.5, pb: 1 },
  "& :is(.sdp__weekdays, .sdp__week)": { display: "grid", gridTemplateColumns: "repeat(7, minmax(0, 1fr))" },
  "& :is(.sdp__columnheader, .sdp__cell)": { display: "grid", placeItems: "center", minWidth: 0 },
};

export const muiComponents: Partial<DatePickerComponents> = {
  Root: ({ className, ...props }) => (
    <Box className={className} sx={{ position: "relative", width: 1, maxWidth: 288 }} {...props} />
  ),

  /**
   * An outlined-input shell, so it sits beside MUI's TextFields without
   * looking pasted in. An invalid value takes the theme's `error` colour and
   * MUI's `Mui-error` state class, as a `TextField` with `error` does.
   */
  Trigger: ({ className, ...props }) => (
    <Box
      className={[className, props["aria-invalid"] || (props as Record<string, unknown>)["data-invalid"] ? "Mui-error" : undefined].filter(Boolean).join(" ") || undefined}
      sx={{
        cursor: "pointer",
        outline: 0,
        width: 1,
        minHeight: 40,
        display: "flex",
        alignItems: "center",
        gap: 1,
        px: 1.5,
        py: 0.75,
        borderRadius: 1,
        border: 1,
        borderColor: "divider",
        typography: "body2",
        color: "text.primary",
        textAlign: "start",
        "&:hover": { borderColor: "text.primary" },
        "&[data-open], &:focus-visible": {
          borderColor: "primary.main",
          boxShadow: (theme) => `0 0 0 1px ${theme.palette.primary.main}`,
        },
        "&[data-trigger='input']": { cursor: "text", pe: 0.5 },
        "&[data-trigger='input']:focus-within": {
          borderColor: "primary.main",
          boxShadow: (theme) => `0 0 0 1px ${theme.palette.primary.main}`,
        },
        "&[aria-disabled='true'], &[data-trigger='input'][data-disabled]": { opacity: 0.5, cursor: "not-allowed" },
        "&.Mui-error": { borderColor: "error.main" },
        "&.Mui-error[data-open], &.Mui-error:focus-visible, &.Mui-error[data-trigger='input']:focus-within": {
          boxShadow: (theme) => `0 0 0 1px ${theme.palette.error.main}`,
        },
      }}
      {...props}
    />
  ),

  /** MUI's bare `InputBase`: the trigger frame holds the border, the field only the text. */
  Input: ({ ref, value, onChange, onKeyDown, onBlur, onFocus, onClick, disabled, placeholder, className, ...inputProps }) => (
    <InputBase
      inputRef={ref}
      className={className}
      value={value}
      onChange={onChange}
      onKeyDown={onKeyDown}
      onBlur={onBlur}
      onFocus={onFocus}
      onClick={onClick}
      disabled={disabled}
      placeholder={placeholder}
      inputProps={inputProps}
      sx={{ flex: 1, minWidth: 0, typography: "body2", "& input": { p: 0 } }}
    />
  ),

  /** A small `IconButton` around the calendar glyph. */
  Toggle: ({ children, ...props }) => (
    <IconButton size="small" {...props}>
      {children}
    </IconButton>
  ),

  Value: ({ text, placeholder, empty }) => (
    <Typography
      variant="body2"
      noWrap
      sx={{ flex: 1, minWidth: 0 }}
      color={empty ? "text.secondary" : "text.primary"}
    >
      {empty ? placeholder : text}
    </Typography>
  ),

  Icon: ({ open }) => (
    <Box
      component="svg"
      viewBox="0 0 24 24"
      aria-hidden="true"
      sx={{
        width: 20,
        height: 20,
        flex: "none",
        fill: "none",
        stroke: "currentColor",
        strokeWidth: 2,
        color: open ? "primary.main" : "action.active",
      }}
    >
      <rect x="3" y="5" width="18" height="16" rx="2" />
      <path d="M8 3v4M16 3v4M3 11h18" strokeLinecap="round" />
    </Box>
  ),

  Clear: ({ onClick, ...aria }) => (
    <IconButton
      size="small"
      onClick={(event) => {
        event.stopPropagation();
        onClick();
      }}
      {...aria}
    >
      <Box
        component="svg"
        viewBox="0 0 24 24"
        aria-hidden="true"
        sx={{ width: 16, height: 16, fill: "none", stroke: "currentColor", strokeWidth: 2.5 }}
      >
        <path d="M18 6 6 18M6 6l12 12" strokeLinecap="round" />
      </Box>
    </IconButton>
  ),

  /**
   * MUI's `Popper`: it portals the calendar, places it against the trigger
   * (flipping when there is no room) and stacks it with the theme's z-index.
   * The `Paper` keeps every prop the part is given, its `id` included, which
   * is how the picker knows the portaled calendar is still part of it; the
   * built-in position style is left out.
   */
  Popup: function MuiPopup({ className, style: _style, ...props }) {
    const { position } = useDatePickerContext();
    if (!position.anchor) return null;
    return (
      <Popper
        open
        anchorEl={position.anchor}
        placement={position.placement === "top-start" ? "top-start" : "bottom-start"}
        modifiers={[{ name: "offset", options: { offset: [0, 4] } }]}
        sx={{ zIndex: "modal" }}
      >
        {/* Its own stacking context, so the range band behind each day stays above the surface. */}
        <Paper className={className} elevation={8} sx={calendarSx} {...props} />
      </Popper>
    );
  },

  Calendar: ({ className, ...props }) => (
    <Box className={className} sx={{ display: "grid", rowGap: "2px" }} {...props} />
  ),

  Caption: ({ monthIndex, year, months, years, onMonthChange, onYearChange, labels }) => (
    <Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
      <Select
        native
        variant="standard"
        inputProps={{ "aria-label": labels.month }}
        value={monthIndex}
        onChange={(event) => onMonthChange(Number(event.target.value))}
        sx={captionSx}
      >
        {months.map((name, index) => (
          <option key={name} value={index}>
            {name}
          </option>
        ))}
      </Select>
      <Select
        native
        variant="standard"
        inputProps={{ "aria-label": labels.year }}
        value={year}
        onChange={(event) => onYearChange(Number(event.target.value))}
        sx={captionSx}
      >
        {years.map((value) => (
          <option key={value} value={value}>
            {value}
          </option>
        ))}
      </Select>
    </Box>
  ),

  Nav: ({ direction, onClick, disabled, ...aria }) => (
    <IconButton size="small" onClick={onClick} disabled={disabled} {...aria}>
      <ChevronIcon flip={direction === "next"} />
    </IconButton>
  ),

  /**
   * One column header.
   *
   * `noWrap`, because the column is one cell wide and locales whose short
   * weekday names are full words would otherwise make the row two lines tall.
   */
  Weekday: ({ label }) => (
    <Typography
      variant="caption"
      align="center"
      noWrap
      color="text.secondary"
      sx={{ display: "block", py: 0.5, fontWeight: 600 }}
    >
      {label}
    </Typography>
  ),

  /**
   * One cell of the grid.
   *
   * An `IconButton`, which is square by construction, pinned to the cell size
   * so it neither stretches to the column nor carries the icon padding.
   *
   * Blocked days are styled from `data-disabled`, not `.Mui-disabled`: the
   * component keeps them focusable, so it never sets `disabled` on them.
   *
   * The band is a `::before` at a negative z-index: it reaches half the gutter
   * past each inline edge so neighbouring days meet across the gap, and it
   * sits behind both the picked chip and the label. `overflow: visible` is
   * asserted because that bleed is outside the button's own box.
   */
  Day: ({ className, ...props }) => (
    <IconButton
      className={className}
      sx={{
        position: "relative",
        justifySelf: "center",
        width: DAY_SIZE,
        height: DAY_SIZE,
        p: 0,
        overflow: "visible",
        borderRadius: `${DAY_RADIUS}px`,
        typography: "body2",
        fontSize: "0.8125rem",
        color: "text.primary",
        "&:hover": { bgcolor: "action.hover" },
        "&[data-disabled]": { opacity: 0.3, cursor: "not-allowed" },
        "&[data-outside]": { color: "text.disabled" },
        "&[data-today]": { boxShadow: (theme) => `inset 0 0 0 1px ${theme.palette.primary.main}` },
        "&[data-in-range]::before, &[data-in-preview]::before": {
          content: '""',
          position: "absolute",
          zIndex: -1,
          insetBlock: 0,
          insetInline: "calc(var(--sdp-gap, 0.25rem) / -2 - 0.5px)",
          bgcolor: (theme) => `color-mix(in oklab, ${theme.palette.primary.main}, transparent 85%)`,
        },
        "&[data-in-range][data-range-start]::before": {
          insetInlineStart: 0,
          borderStartStartRadius: DAY_RADIUS,
          borderEndStartRadius: DAY_RADIUS,
        },
        "&[data-in-range][data-range-end]::before": {
          insetInlineEnd: 0,
          borderStartEndRadius: DAY_RADIUS,
          borderEndEndRadius: DAY_RADIUS,
        },
        "&[data-selected]": { bgcolor: "primary.main", color: "primary.contrastText", fontWeight: 600 },
        "&[data-selected]:hover": { bgcolor: "primary.dark" },
      }}
      {...props}
    />
  ),

  DayContent: ({ day }) => <span>{day.day}</span>,

  Footer: ({ presets, onPreset, onToday, todayLabel }) => (
    <Box
      sx={{
        display: "flex",
        flexWrap: "wrap",
        alignItems: "center",
        gap: 0.5,
        mt: 1,
        pt: 1,
        borderTop: 1,
        borderColor: "divider",
      }}
    >
      {presets.map((preset) => (
        <Chip
          key={preset.label}
          size="small"
          variant="outlined"
          label={preset.label}
          onClick={() => onPreset(preset.value)}
        />
      ))}
      <Chip
        size="small"
        variant="outlined"
        label={todayLabel}
        onClick={onToday}
        sx={{ marginInlineStart: "auto", borderColor: "transparent", color: "text.secondary" }}
      />
    </Box>
  ),
};
```

## Chakra UI

يعرض هذا المحوّل `DatePicker` بأجزاء Chakra UI. انسخ الملف إلى تطبيقك ومرّر الخريطة إلى `components`، وكل فتحة لا تمرّرها تبقى على بديلها الافتراضي.

```tsx
/**
 * DatePicker for Chakra UI
 *
 * A `components` map that renders the slotsmith date picker with Chakra UI
 * primitives, tested against Chakra UI v3. Copy the file, keep the parts you
 * want, and pass the map as `components={chakraComponents}`; every slot left
 * out keeps its fallback.
 */
import { Box, Button, chakra, IconButton, NativeSelect, Popover, Portal, Text } from "@chakra-ui/react";
import type { ReactNode } from "react";
import { useDatePickerContext, type DatePickerComponents } from "slotsmith/date-picker";

/**
 * Chakra date picker parts
 *
 * The `components` map against Chakra UI v3 primitives — `Box`, `Button`,
 * `IconButton`, `NativeSelect`, `Text` and the `chakra` factory — written
 * against Chakra's semantic tokens, so it follows the theme the way the
 * table's Chakra parts do.
 *
 * `NativeSelect` rather than Chakra's `Select` collection on purpose: the
 * collection renders its list in a portal, and the picker closes on any
 * pointer-down outside its own root.
 */

/** The day cell, on Chakra's spacing scale. `8` is 2rem, which matches `--sdp-cell`. */
const DAY_SIZE = "8";

/**
 * Glyph
 *
 * A styled `<svg>` from Chakra's factory rather than `Icon`, which treats its
 * child as the element to render and so swallows a multi-path drawing.
 */
const Glyph = chakra("svg");

const Chevron = ({ flip }: { flip: boolean }) => (
  <Glyph
    width="4"
    height="4"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    strokeWidth="2"
    strokeLinecap="round"
    strokeLinejoin="round"
    transform={flip ? "rotate(180deg)" : undefined}
    aria-hidden="true"
  >
    <path d="m15 18-6-6 6-6" />
  </Glyph>
);

/**
 * Caption select
 *
 * The month and year dropdowns are the same control with different options.
 *
 * @param label - Accessible name for the field.
 * @param value - The option currently shown.
 * @param onValueChange - Receives the new option as a number.
 * @param children - The `<option>` list.
 * @returns A plain Chakra `NativeSelect`.
 */
const CaptionSelect = ({
  label,
  value,
  onValueChange,
  children,
}: {
  label: string;
  value: number;
  onValueChange: (value: number) => void;
  children: ReactNode;
}) => (
  <NativeSelect.Root size="xs" width="auto" variant="plain">
    <NativeSelect.Field
      aria-label={label}
      value={value}
      fontWeight="semibold"
      onChange={(event) => onValueChange(Number(event.currentTarget.value))}
    >
      {children}
    </NativeSelect.Field>
    <NativeSelect.Indicator />
  </NativeSelect.Root>
);

/**
 * Calendar layout
 *
 * The calendar's own structure, which no slot renders: the header row, the
 * seven columns of the weekday and week rows, and the centred cells. Laid
 * out here too, so the picker holds together without slotsmith's stylesheet.
 */
const calendarCss = {
  "& .sdp__header": { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "1", pb: "2" },
  "& :is(.sdp__weekdays, .sdp__week)": { display: "grid", gridTemplateColumns: "repeat(7, minmax(0, 1fr))" },
  "& :is(.sdp__columnheader, .sdp__cell)": { display: "grid", placeItems: "center", minWidth: "0" },
} as const;

export const chakraComponents: Partial<DatePickerComponents> = {
  /** `colorPalette` is set here for the trigger, and again on the popover, which renders in a portal outside it. */
  Root: ({ className, ...props }) => (
    <Box className={className} colorPalette="blue" position="relative" width="full" maxWidth="18rem" {...props} />
  ),

  Trigger: ({ className, ...props }) => (
    <Box
      className={className}
      display="flex"
      alignItems="center"
      gap="2"
      width="full"
      minHeight="10"
      px="3"
      py="1.5"
      rounded="md"
      borderWidth="1px"
      bg="bg.panel"
      color="fg"
      textStyle="sm"
      cursor="pointer"
      outline="none"
      css={{
        "&:hover": { borderColor: "border.emphasized" },
        "&[data-open], &:focus-visible": { borderColor: "colorPalette.solid" },
        "&[aria-disabled='true'], &[data-trigger='input'][data-disabled]": { opacity: 0.5, cursor: "not-allowed" },
        "&[data-trigger='input']": { cursor: "text", paddingInlineEnd: "1.5" },
        "&[data-trigger='input']:focus-within": { borderColor: "colorPalette.solid" },
        /** Chakra's own invalid condition and token, as its `Input` uses them. */
        _invalid: { borderColor: "border.error" },
        "&[data-invalid]": { borderColor: "border.error" },
      }}
      {...props}
    />
  ),

  /** A bare `chakra.input`: the trigger frame holds the border. */
  Input: (props) => (
    <chakra.input
      flex="1"
      minWidth="0"
      bg="transparent"
      border="0"
      outline="none"
      textStyle="sm"
      color="fg"
      css={{ "&::placeholder": { color: "fg.muted" }, "&:disabled": { cursor: "not-allowed" } }}
      {...props}
    />
  ),

  /** A ghost `IconButton` around the calendar glyph. */
  Toggle: ({ children, ...props }) => (
    <IconButton size="2xs" variant="ghost" {...props}>
      {children}
    </IconButton>
  ),

  Value: ({ text, placeholder, empty }) => (
    <Text as="span" flex="1" minWidth="0" truncate textStyle="sm" color={empty ? "fg.muted" : "fg"}>
      {empty ? placeholder : text}
    </Text>
  ),

  Icon: ({ open }) => (
    <Glyph
      width="4"
      height="4"
      flex="none"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      color={open ? "colorPalette.solid" : "fg.muted"}
      aria-hidden="true"
    >
      <rect x="3" y="5" width="18" height="16" rx="2" />
      <path d="M8 3v4M16 3v4M3 11h18" />
    </Glyph>
  ),

  Clear: ({ onClick, ...aria }) => (
    <IconButton
      size="2xs"
      variant="ghost"
      onClick={(event) => {
        event.stopPropagation();
        onClick();
      }}
      {...aria}
    >
      <Glyph
        width="3"
        height="3"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        strokeWidth="2.5"
        strokeLinecap="round"
        aria-hidden="true"
      >
        <path d="M18 6 6 18M6 6l12 12" />
      </Glyph>
    </IconButton>
  ),

  /**
   * Chakra's `Popover`: it portals the calendar, places it against the trigger
   * and draws Chakra's popover surface. The picker owns the open state, the
   * focus and closing, so the popover's own are turned off. The content keeps
   * every prop the part is given, its `id` included, which is how the picker
   * knows the portaled calendar is still part of it; the built-in position
   * style is left out.
   */
  Popup: function ChakraPopup({ className, style: _style, ...props }) {
    const { position } = useDatePickerContext();
    const anchor = position.anchor;
    if (!anchor) return null;
    return (
      <Popover.Root
        open
        // The popover finds its content by this id, and so does the picker.
        ids={{ content: props.id }}
        modal={false}
        autoFocus={false}
        closeOnEscape={false}
        closeOnInteractOutside={false}
        positioning={{
          placement: position.placement === "top-start" ? "top-start" : "bottom-start",
          gutter: 4,
          getAnchorRect: () => anchor.getBoundingClientRect(),
        }}
      >
        <Portal>
          <Popover.Positioner>
            {/* Its own stacking context, so the range band behind each day stays above the surface. */}
            <Popover.Content className={className} colorPalette="blue" width="18rem" p="2" isolation="isolate" css={calendarCss} {...props} />
          </Popover.Positioner>
        </Portal>
      </Popover.Root>
    );
  },

  Calendar: ({ className, ...props }) => <Box className={className} display="grid" rowGap="0.5" {...props} />,

  Caption: ({ monthIndex, year, months, years, onMonthChange, onYearChange, labels }) => (
    <Box display="flex" alignItems="center" gap="1">
      <CaptionSelect label={labels.month} value={monthIndex} onValueChange={onMonthChange}>
        {months.map((name, index) => (
          <option key={name} value={index}>
            {name}
          </option>
        ))}
      </CaptionSelect>
      <CaptionSelect label={labels.year} value={year} onValueChange={onYearChange}>
        {years.map((value) => (
          <option key={value} value={value}>
            {value}
          </option>
        ))}
      </CaptionSelect>
    </Box>
  ),

  Nav: ({ direction, onClick, disabled, ...aria }) => (
    <IconButton size="xs" variant="ghost" disabled={disabled} onClick={onClick} {...aria}>
      <Chevron flip={direction === "next"} />
    </IconButton>
  ),

  /**
   * One column header.
   *
   * Clipped rather than wrapped: the column is one cell wide, and locales
   * whose short weekday names are full words would make the row two lines
   * tall.
   */
  Weekday: ({ label }) => (
    <Text
      as="span"
      display="block"
      truncate
      py="1"
      textAlign="center"
      textStyle="xs"
      fontWeight="semibold"
      color="fg.muted"
    >
      {label}
    </Text>
  ),

  /**
   * One cell of the grid.
   *
   * An `IconButton` at a fixed `boxSize`, so the button is exactly as wide as
   * it is tall rather than stretching to the column. Blocked days are dimmed
   * from `data-disabled`: they stay focusable, so `:disabled` never matches.
   *
   * The band is a `_before` layer at a negative z-index: it reaches half the
   * gutter past each inline edge so neighbouring days meet across the gap,
   * and it sits behind both the picked chip and the label.
   */
  Day: ({ className, ...props }) => (
    <IconButton
      className={className}
      variant="ghost"
      boxSize={DAY_SIZE}
      minWidth={DAY_SIZE}
      justifySelf="center"
      position="relative"
      overflow="visible"
      p="0"
      rounded="md"
      bg="transparent"
      color="fg"
      textStyle="sm"
      fontWeight="normal"
      css={{
        "&[data-disabled]": { opacity: 0.3, cursor: "not-allowed" },
        "&[data-outside]": { color: "fg.muted" },
        "&[data-today]": { boxShadow: "inset 0 0 0 1px var(--chakra-colors-color-palette-solid)" },
        "&[data-in-range], &[data-in-preview]": {
          _before: {
            content: '""',
            position: "absolute",
            zIndex: -1,
            insetBlock: "0",
            insetInline: "calc(var(--sdp-gap, 0.25rem) / -2 - 0.5px)",
            bg: "colorPalette.subtle",
          },
        },
        "&[data-in-range][data-range-start]": {
          _before: { insetInlineStart: "0", borderStartStartRadius: "md", borderEndStartRadius: "md" },
        },
        "&[data-in-range][data-range-end]": {
          _before: { insetInlineEnd: "0", borderStartEndRadius: "md", borderEndEndRadius: "md" },
        },
        "&[data-selected], &[data-selected]:hover": {
          bg: "colorPalette.solid",
          color: "colorPalette.contrast",
          fontWeight: "semibold",
        },
      }}
      {...props}
    />
  ),

  DayContent: ({ day }) => <span>{day.day}</span>,

  Footer: ({ presets, onPreset, onToday, todayLabel }) => (
    <Box
      display="flex"
      flexWrap="wrap"
      alignItems="center"
      gap="1"
      mt="2"
      pt="2"
      borderTopWidth="1px"
      borderColor="border"
    >
      {presets.map((preset) => (
        <Button
          key={preset.label}
          size="xs"
          variant="outline"
          rounded="full"
          onClick={() => onPreset(preset.value)}
        >
          {preset.label}
        </Button>
      ))}
      <Button size="xs" variant="ghost" color="fg.muted" marginInlineStart="auto" onClick={onToday}>
        {todayLabel}
      </Button>
    </Box>
  ),
};
```

## Ant Design

يعرض هذا المحوّل `DatePicker` بأجزاء Ant Design. انسخ الملف إلى تطبيقك ومرّر الخريطة إلى `components`، وكل فتحة لا تمرّرها تبقى على بديلها الافتراضي.

```tsx
/**
 * DatePicker for Ant Design
 *
 * A `components` map that renders the slotsmith date picker with Ant Design
 * primitives, tested against Ant Design v6. Copy the file, keep the parts you
 * want, and pass the map as `components={antdComponents}`; every slot left out
 * keeps its fallback.
 */
import { CalendarOutlined, CloseCircleFilled, LeftOutlined, RightOutlined } from "@ant-design/icons";
import { Button, ConfigProvider, Flex, Input, Popover, Select, Typography, theme } from "antd";
import { useContext, useId, useState } from "react";
import { createPortal } from "react-dom";
import type { GlobalToken } from "antd";
import { useDatePickerContext, type DatePickerComponents } from "slotsmith/date-picker";

/**
 * Ant Design date picker parts
 *
 * Built from Ant Design v6 primitives — `Button`, `Flex`, `Typography` and
 * Ant's icons — not from Ant's own `DatePicker`, which is a competing engine
 * rather than a set of parts. The surfaces take the look of Ant's pickers
 * from the theme's tokens; their state arrives as `data-*` props, so each part
 * reads its own and picks the matching token.
 *
 * This is also why the element parts `Omit` `color` from their DOM props:
 * Ant's `Button` has a `color` prop of its own, a preset palette, which a
 * forwarded DOM `color` would collide with.
 */

/** The day cell, in pixels. Matches `--sdp-cell`, so the square fills its column. */
const DAY_SIZE = 32;

/**
 * Has flag
 *
 * @param props - A part's props.
 * @param name - A `data-*` attribute.
 * @returns Whether the engine set it.
 */
const has = (props: object, name: `data-${string}`) => (props as Record<string, unknown>)[name] !== undefined;

/**
 * Ant Design components
 *
 * The slot map an Ant Design v6 project would pass as `components`. Every
 * part reads the theme's tokens, so a `ConfigProvider` theme, dark included,
 * reaches the calendar without extra wiring.
 */
/**
 * Calendar sheet
 *
 * The calendar's own structure, which no slot renders: the header row, the
 * seven columns of the weekday and week rows, and the centred cells. Ant's
 * parts take inline styles, which cannot reach those, so a sheet scoped to
 * the popup lays them out, and the picker holds together without
 * slotsmith's stylesheet. It carries the nonce from `ConfigProvider`'s
 * `csp`, as Ant's own styles do.
 *
 * @param scope - The popup's scope class.
 * @param token - The theme's tokens.
 * @returns The sheet.
 */
function calendarSheet(scope: string, token: GlobalToken): string {
  const s = `.${scope}`;
  return `${s} .sdp__header { display: flex; align-items: center; justify-content: space-between; gap: ${token.paddingXXS}px; }
${s} :is(.sdp__weekdays, .sdp__week) { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
${s} :is(.sdp__columnheader, .sdp__cell) { display: grid; place-items: center; min-width: 0; }`;
}

export const antdComponents: Partial<DatePickerComponents> = {
  /** The box the popup is placed against, at the width of Ant's pickers. */
  Root: ({ style, ...props }) => <div style={{ position: "relative", width: "100%", maxWidth: 288, ...style }} {...props} />,

  /**
   * The outlined field of Ant's pickers: the primary border and focus ring
   * while open, and the error border and ring of `status="error"` when the
   * value is invalid.
   */
  Trigger: function AntTrigger({ style, onFocus, onBlur, ...props }) {
    const { token } = theme.useToken();
    /** Focus anywhere in the frame, so the text field gets the ring Ant Design's `Input` shows. */
    const [focused, setFocused] = useState(false);
    const typing = (props as Record<string, unknown>)["data-trigger"] === "input";
    const open = has(props, "data-open") || (typing && focused);
    const disabled = has(props, "data-disabled");
    const invalid = has(props, "data-invalid");
    const edge = invalid ? token.colorError : open ? token.colorPrimary : token.colorBorder;
    const ring = invalid ? token.colorErrorOutline : token.controlOutline;
    return (
      <div
        style={{
          position: "relative",
          display: "flex",
          alignItems: "center",
          gap: token.paddingXS,
          width: "100%",
          minHeight: token.controlHeight,
          paddingBlock: token.paddingXXS,
          paddingInline: token.paddingSM - 1,
          color: token.colorText,
          fontSize: token.fontSize,
          textAlign: "start",
          outline: 0,
          background: disabled ? token.colorBgContainerDisabled : token.colorBgContainer,
          border: `${token.lineWidth}px ${token.lineType} ${edge}`,
          borderRadius: token.borderRadius,
          boxShadow: open ? `0 0 0 ${token.controlOutlineWidth}px ${ring}` : undefined,
          cursor: disabled ? "not-allowed" : typing ? "text" : "pointer",
          transition: `all ${token.motionDurationMid}`,
          ...style,
        }}
        onFocus={(event) => {
          setFocused(true);
          onFocus?.(event);
        }}
        onBlur={(event) => {
          setFocused(false);
          onBlur?.(event);
        }}
        {...props}
      />
    );
  },

  /** A borderless `Input`: the trigger frame holds the border and the ring. */
  Input: ({ ref, style, ...props }) => (
    <Input
      variant="borderless"
      ref={(input) => {
        const node = input?.input ?? null;
        if (typeof ref === "function") ref(node);
        else if (ref) ref.current = node;
      }}
      style={{ flex: 1, minWidth: 0, padding: 0, ...style }}
      {...props}
    />
  ),

  /** A text `Button` with the calendar glyph. Its `type` is the button's look, so the HTML type goes in `htmlType`. */
  Toggle: ({ type: _type, children, ...props }) => <Button type="text" htmlType="button" size="small" icon={children} {...props} />,

  /** The trigger text on one ellipsised line, in the placeholder colour while empty. */
  Value: function AntValue({ text, placeholder, empty }) {
    const { token } = theme.useToken();
    return (
      <Typography.Text
        style={{
          flex: 1,
          minWidth: 0,
          overflow: "hidden",
          whiteSpace: "nowrap",
          textOverflow: "ellipsis",
          color: empty ? token.colorTextPlaceholder : undefined,
        }}
      >
        {empty ? placeholder : text}
      </Typography.Text>
    );
  },

  /** The calendar glyph, in the primary colour while the popup is open. */
  Icon: function AntIcon({ open }) {
    const { token } = theme.useToken();
    return (
      <span aria-hidden="true" style={{ display: "inline-flex", color: open ? token.colorPrimary : token.colorTextQuaternary }}>
        <CalendarOutlined />
      </span>
    );
  },

  /** A text `Button` with the filled close glyph; the click stops here so it does not also open the popup. */
  Clear: ({ onClick, ...aria }) => (
    <Button
      type="text"
      size="small"
      icon={<CloseCircleFilled />}
      onClick={(event) => {
        event.stopPropagation();
        onClick();
      }}
      {...aria}
    />
  ),

  /**
   * Ant Design's `Popover`: it portals the calendar, places it against the
   * trigger (flipping when there is no room) and draws Ant's popover surface.
   * An Ant popover is placed against its child, so the child is an empty box
   * laid over the trigger, rendered into it. The calendar keeps every prop
   * the part is given, its `id` included, which is how the picker knows the
   * portaled calendar is still part of it; the built-in position style is
   * left out.
   */
  Popup: function AntPopup({ style: _style, children, ...props }) {
    const { position } = useDatePickerContext();
    const { token } = theme.useToken();
    const { csp } = useContext(ConfigProvider.ConfigContext);
    const scope = `ss-antd-calendar-${useId().replace(/[^\w-]/g, "")}`;
    if (!position.anchor) return null;
    return createPortal(
      <Popover
        open
        arrow={false}
        trigger={[]}
        placement={position.placement === "top-start" ? "topLeft" : "bottomLeft"}
        content={
          // Its own stacking context, so the range band behind each day stays above the surface.
          <div className={scope} style={{ width: 288, isolation: "isolate" }} {...props}>
            <style nonce={csp?.nonce}>{calendarSheet(scope, token)}</style>
            {children}
          </div>
        }
      >
        <span aria-hidden="true" style={{ position: "absolute", inset: 0, pointerEvents: "none" }} />
      </Popover>,
      position.anchor,
    );
  },

  /** The grid of weeks, with a small gap between rows. */
  Calendar: ({ style, ...props }) => <div style={{ display: "grid", rowGap: 2, ...style }} {...props} />,

  /**
   * The month and year as Ant's borderless `Select`, like the header of
   * Ant's own picker. Its menu renders beside it, inside the calendar's popup,
   * so choosing a month is a press inside the picker and does not close it.
   */
  Caption: function AntCaption({ monthIndex, year, months, years, onMonthChange, onYearChange, labels }) {
    const { token } = theme.useToken();
    const inside = (node: HTMLElement) => node.parentElement ?? document.body;
    return (
      <Flex align="center" gap={token.paddingXXS}>
        <Select
          size="small"
          variant="borderless"
          aria-label={labels.month}
          value={monthIndex}
          onChange={onMonthChange}
          options={months.map((name, index) => ({ label: name, value: index }))}
          popupMatchSelectWidth={false}
          virtual={false}
          getPopupContainer={inside}
          style={{ fontWeight: token.fontWeightStrong }}
        />
        <Select
          size="small"
          variant="borderless"
          aria-label={labels.year}
          value={year}
          onChange={onYearChange}
          options={years.map((value) => ({ label: String(value), value }))}
          popupMatchSelectWidth={false}
          virtual={false}
          getPopupContainer={inside}
          style={{ fontWeight: token.fontWeightStrong }}
        />
      </Flex>
    );
  },

  /** `direction` already accounts for right to left, so the arrow follows it as is. */
  Nav: ({ direction, onClick, disabled, ...aria }) => (
    <Button
      type="text"
      size="small"
      icon={direction === "next" ? <RightOutlined /> : <LeftOutlined />}
      onClick={onClick}
      disabled={disabled}
      {...aria}
    />
  ),

  /**
   * One column header.
   *
   * No wrapping, because the column is one cell wide and locales whose short
   * weekday names are full words would otherwise make the row two lines tall.
   */
  Weekday: function AntWeekday({ label }) {
    const { token } = theme.useToken();
    return (
      <Typography.Text
        type="secondary"
        style={{ display: "block", paddingBlock: token.paddingXXS, fontSize: token.fontSizeSM, textAlign: "center", whiteSpace: "nowrap" }}
      >
        {label}
      </Typography.Text>
    );
  },

  /**
   * One cell of the grid.
   *
   * A `text` `Button`, pinned to the cell size, so the hover is Ant's own;
   * the picked fill is painted from the theme's primary tokens. The variant
   * never changes: Ant wraps every other variant in an extra element, so
   * switching to `primary` as the day is picked would replace the button and
   * drop focus to `<body>`. The HTML `type` moves to `htmlType`, since Ant's
   * `type` is the variant.
   *
   * Blocked days are styled from `data-disabled`, not Ant's `disabled`: the
   * component keeps them focusable, so it never sets `disabled` on them.
   *
   * The range band is a span behind the button, at a negative z-index: it
   * reaches half the gutter past each inline edge so neighbouring days meet
   * across the gap, and stops square at the button's edge on the two ends,
   * behind the picked day's rounded corners.
   */
  Day: function AntDay({ type, style, children, ...props }) {
    const { token } = theme.useToken();
    const selected = has(props, "data-selected");
    const band = has(props, "data-in-range") || has(props, "data-in-preview");
    const start = has(props, "data-in-range") && has(props, "data-range-start");
    const end = has(props, "data-in-range") && has(props, "data-range-end");
    const bleed = "calc(var(--sdp-gap, 0.25rem) / -2 - 0.5px)";
    return (
      <Button
        type="text"
        htmlType={type}
        style={{
          position: "relative",
          justifySelf: "center",
          width: DAY_SIZE,
          minWidth: DAY_SIZE,
          height: DAY_SIZE,
          padding: 0,
          overflow: "visible",
          borderRadius: token.borderRadius,
          fontSize: token.fontSizeSM + 1,
          color: selected ? token.colorTextLightSolid : has(props, "data-outside") ? token.colorTextDisabled : token.colorText,
          background: selected ? token.colorPrimary : undefined,
          boxShadow: has(props, "data-today") && !selected ? `inset 0 0 0 ${token.lineWidth}px ${token.colorPrimary}` : "none",
          ...(has(props, "data-disabled") && { opacity: 0.3, cursor: "not-allowed" }),
          ...style,
        }}
        {...props}
      >
        {band && (
          <span
            data-band=""
            aria-hidden="true"
            style={{
              position: "absolute",
              zIndex: -1,
              insetBlock: 0,
              insetInlineStart: start ? 0 : bleed,
              insetInlineEnd: end ? 0 : bleed,
              background: has(props, "data-in-range") ? token.controlItemBgActive : token.controlItemBgHover,
              borderStartStartRadius: start ? token.borderRadius : 0,
              borderEndStartRadius: start ? token.borderRadius : 0,
              borderStartEndRadius: end ? token.borderRadius : 0,
              borderEndEndRadius: end ? token.borderRadius : 0,
            }}
          />
        )}
        {children}
      </Button>
    );
  },

  /** The day number alone; the `Day` button around it carries every state. */
  DayContent: ({ day }) => <span>{day.day}</span>,

  /** The preset `Button`s under a hairline, and a link `Button` for today at the far end. */
  Footer: function AntFooter({ presets, onPreset, onToday, todayLabel }) {
    const { token } = theme.useToken();
    return (
      <Flex
        wrap
        align="center"
        gap={token.paddingXXS}
        style={{ marginTop: token.paddingXS, paddingTop: token.paddingXS, borderTop: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}` }}
      >
        {presets.map((preset) => (
          <Button key={preset.label} size="small" onClick={() => onPreset(preset.value)}>
            {preset.label}
          </Button>
        ))}
        <Button type="link" size="small" onClick={onToday} style={{ marginInlineStart: "auto" }}>
          {todayLabel}
        </Button>
      </Flex>
    );
  },
};
```

## Radix Themes

يعرض هذا المحوّل `DatePicker` بأجزاء Radix Themes. انسخ الملف إلى تطبيقك ومرّر الخريطة إلى `components`، وكل فتحة لا تمرّرها تبقى على بديلها الافتراضي.

```tsx
/**
 * DatePicker for Radix Themes
 *
 * A `components` map that renders the slotsmith date picker with Radix Themes
 * primitives, tested against Radix Themes v3. Copy the file, keep the parts
 * you want, and pass the map as `components={radixComponents}`; every slot
 * left out keeps its fallback.
 *
 * This is Radix Themes (`@radix-ui/themes`), the styled library; an app on the
 * bare Radix primitives wants the shadcn/ui adapter, which is built on them.
 */
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon, CrossCircledIcon } from "@radix-ui/react-icons";
import { Button, Flex, IconButton, Popover, Select, Text, TextField } from "@radix-ui/themes";
import { useEffect, useId, useMemo, useState, type RefObject } from "react";
import { useDatePickerContext, type DatePickerComponents } from "slotsmith/date-picker";

/**
 * Radix Themes date picker parts
 *
 * Built from Radix Themes v3 — `Button`, `IconButton`, `Flex`, `Text` and
 * Radix icons — not from its `Popover`, which would position the calendar a
 * second time. The surfaces take the look of Radix's fields and menus from
 * its variables (`--accent-*`, `--gray-*`, `--color-panel-solid`), so they
 * follow the app's `<Theme accentColor appearance>`; their state arrives as
 * `data-*` props, so each part reads its own. The app must import
 * `@radix-ui/themes/styles.css`, as every Radix Themes app already does.
 *
 * Radix's `Button` and `IconButton` render a `<button>` with no `type`, which
 * would submit a surrounding form, so each one here is given
 * `type="button"` ahead of the slot's props.
 *
 * The element parts `Omit` `color` from their DOM props because Radix's
 * `color` prop is an accent colour, which a forwarded DOM `color` would
 * collide with.
 */

/** The day cell, in pixels. Matches `--sdp-cell`, so the square fills its column. */
const DAY_SIZE = 32;

/**
 * Has flag
 *
 * @param props - A part's props.
 * @param name - A `data-*` attribute.
 * @returns Whether the engine set it.
 */
const has = (props: object, name: `data-${string}`) => (props as Record<string, unknown>)[name] !== undefined;

/**
 * Calendar sheet
 *
 * The calendar's own structure, which no slot renders: the header row, the
 * seven columns of the weekday and week rows, and the centred cells. Radix's
 * parts take inline styles, which cannot reach those, so a sheet scoped to
 * the popup lays them out, and the picker holds together without
 * slotsmith's stylesheet. Radix Themes offers no nonce setting: an app with
 * a strict `style-src` policy adds its nonce to the `<style>` here.
 *
 * @param scope - The popup's scope class.
 * @returns The sheet.
 */
function calendarSheet(scope: string): string {
  const s = `.${scope}`;
  return `${s} .sdp__header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-1); }
${s} :is(.sdp__weekdays, .sdp__week) { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
${s} :is(.sdp__columnheader, .sdp__cell) { display: grid; place-items: center; min-width: 0; }`;
}

export const radixComponents: Partial<DatePickerComponents> = {
  Root: ({ style, ...props }) => <div style={{ position: "relative", width: "100%", maxWidth: 288, ...style }} {...props} />,

  /**
   * The surface field of Radix's `TextField`, with its focus outline while
   * open. An invalid value switches it to the red scale with
   * `data-accent-color="red"`, which is what Radix's own `color="red"` sets.
   */
  Trigger: function RadixTrigger({ style, onFocus, onBlur, ...props }) {
    /** Focus anywhere in the frame, so the text field gets the focus ring Radix's `TextField` shows. */
    const [focused, setFocused] = useState(false);
    const typing = (props as Record<string, unknown>)["data-trigger"] === "input";
    const open = has(props, "data-open") || (typing && focused);
    const disabled = has(props, "data-disabled");
    const invalid = has(props, "data-invalid");
    return (
      <div
        data-accent-color={invalid ? "red" : undefined}
        style={{
          display: "flex",
          alignItems: "center",
          gap: "var(--space-2)",
          width: "100%",
          minHeight: "var(--space-6)",
          paddingInline: "var(--space-2)",
          color: disabled ? "var(--gray-a11)" : "var(--gray-12)",
          fontFamily: "var(--default-font-family)",
          fontSize: "var(--font-size-2)",
          textAlign: "start",
          background: disabled ? "var(--gray-a2)" : "var(--color-surface)",
          boxShadow: `inset 0 0 0 1px ${invalid ? "var(--red-a8)" : "var(--gray-a7)"}`,
          borderRadius: "var(--radius-2)",
          outline: open ? `2px solid ${invalid ? "var(--red-8)" : "var(--focus-8)"}` : 0,
          outlineOffset: -1,
          cursor: disabled ? "not-allowed" : typing ? "text" : "pointer",
          ...style,
        }}
        onFocus={(event) => {
          setFocused(true);
          onFocus?.(event);
        }}
        onBlur={(event) => {
          setFocused(false);
          onBlur?.(event);
        }}
        {...props}
      />
    );
  },

  /**
   * Radix's `TextField`, its own frame turned off: the trigger frame holds
   * the border and the ring. `TextField.Root` hands the ref, the `role`, the
   * aria wiring and the handlers to its `<input>`. Its `value` must be a
   * string, which the field's text always is, and its `type` a text-like one,
   * which it keeps by default.
   */
  Input: ({ style, value, type: _type, defaultValue: _defaultValue, ...props }) => (
    <TextField.Root
      size="2"
      variant="surface"
      value={value === undefined ? undefined : String(value)}
      style={{ flex: 1, minWidth: 0, boxShadow: "none", outline: "none", background: "transparent", ...style }}
      {...props}
    />
  ),

  /** A ghost `IconButton` around the calendar glyph. */
  Toggle: ({ children, ...props }) => (
    <IconButton size="1" variant="ghost" color="gray" {...props}>
      {children}
    </IconButton>
  ),

  Value: ({ text, placeholder, empty }) => (
    <Text size="2" truncate style={{ flex: 1, minWidth: 0, color: empty ? "var(--gray-a10)" : undefined }}>
      {empty ? placeholder : text}
    </Text>
  ),

  Icon: ({ open }) => (
    <span aria-hidden="true" style={{ display: "inline-flex", color: open ? "var(--accent-11)" : "var(--gray-a11)" }}>
      <CalendarIcon />
    </span>
  ),

  Clear: ({ onClick, ...aria }) => (
    <IconButton
      type="button"
      size="1"
      variant="ghost"
      color="gray"
      onClick={(event) => {
        event.stopPropagation();
        onClick();
      }}
      {...aria}
    >
      <CrossCircledIcon />
    </IconButton>
  ),

  /**
   * Radix Themes' `Popover`, anchored to the trigger: it portals the
   * calendar inside the theme, places it and draws Radix's panel. Focus stays
   * with the picker, which moves it into the grid itself. The content keeps
   * every prop the part is given, its `id` included, which is how the picker
   * knows the portaled calendar is still part of it; the built-in position
   * style is left out.
   */
  Popup: function RadixPopup({ style: _style, className, children, ...props }) {
    const { position } = useDatePickerContext();
    const anchor = useMemo(() => ({ current: position.anchor }), [position.anchor]);
    const scope = `ss-radix-calendar-${useId().replace(/[^\w-]/g, "")}`;
    if (!position.anchor) return null;
    return (
      <Popover.Root open>
        <Popover.Anchor virtualRef={anchor as RefObject<HTMLElement>} />
        <Popover.Content
          size="1"
          width="288px"
          align="start"
          side={position.placement === "top-start" ? "top" : "bottom"}
          sideOffset={4}
          onOpenAutoFocus={(event) => event.preventDefault()}
          onCloseAutoFocus={(event) => event.preventDefault()}
          // Its own stacking context, so the range band behind each day stays above the surface.
          style={{ isolation: "isolate" }}
          className={className ? `${scope} ${className}` : scope}
          {...props}
        >
          <style>{calendarSheet(scope)}</style>
          {children}
        </Popover.Content>
      </Popover.Root>
    );
  },

  Calendar: ({ style, ...props }) => <div style={{ display: "grid", rowGap: 2, ...style }} {...props} />,

  /**
   * The month and year as Radix's `Select`, ghost triggers in the caption.
   * Its menu renders into the calendar's own popup, so choosing a month is a
   * press inside the picker and does not close it.
   */
  Caption: function RadixCaption({ monthIndex, year, months, years, onMonthChange, onYearChange, labels }) {
    const { ids } = useDatePickerContext();
    const [popup, setPopup] = useState<HTMLElement | null>(null);
    useEffect(() => setPopup(document.getElementById(ids.popup)), [ids.popup]);
    return (
      <Flex align="center" gap="1">
        <Select.Root size="1" value={String(monthIndex)} onValueChange={(value) => onMonthChange(Number(value))}>
          <Select.Trigger variant="ghost" color="gray" aria-label={labels.month} />
          <Select.Content position="popper" container={popup}>
            {months.map((name, index) => (
              <Select.Item key={name} value={String(index)}>
                {name}
              </Select.Item>
            ))}
          </Select.Content>
        </Select.Root>
        <Select.Root size="1" value={String(year)} onValueChange={(value) => onYearChange(Number(value))}>
          <Select.Trigger variant="ghost" color="gray" aria-label={labels.year} />
          <Select.Content position="popper" container={popup}>
            {years.map((value) => (
              <Select.Item key={value} value={String(value)}>
                {value}
              </Select.Item>
            ))}
          </Select.Content>
        </Select.Root>
      </Flex>
    );
  },

  Nav: ({ direction, onClick, disabled, ...aria }) => (
    <IconButton type="button" size="1" variant="ghost" color="gray" onClick={onClick} disabled={disabled} style={{ margin: 0 }} {...aria}>
      <span data-icon={direction === "next" ? "chevron-right" : "chevron-left"} style={{ display: "inline-flex" }}>
        {direction === "next" ? <ChevronRightIcon /> : <ChevronLeftIcon />}
      </span>
    </IconButton>
  ),

  /**
   * One column header.
   *
   * No wrapping, because the column is one cell wide and locales whose short
   * weekday names are full words would otherwise make the row two lines tall.
   */
  Weekday: ({ label }) => (
    <Text as="div" size="1" color="gray" align="center" style={{ paddingBlock: "var(--space-1)", whiteSpace: "nowrap" }}>
      {label}
    </Text>
  ),

  /**
   * One cell of the grid.
   *
   * A `Button`, pinned to the cell size: `solid` when picked, `ghost`
   * otherwise, so the hover and the picked fill are Radix's own. Radix pulls
   * ghost buttons out by their padding; the cell is a fixed square, so the
   * margin is put back.
   *
   * Blocked days are styled from `data-disabled`, not Radix's `disabled`: the
   * component keeps them focusable, so it never sets `disabled` on them.
   *
   * The range band is a span behind the button, at a negative z-index: it
   * reaches half the gutter past each inline edge so neighbouring days meet
   * across the gap, and stops square at the button's edge on the two ends,
   * behind the picked day's rounded corners.
   */
  Day: ({ style, children, ...props }) => {
    const selected = has(props, "data-selected");
    const band = has(props, "data-in-range") || has(props, "data-in-preview");
    const start = has(props, "data-in-range") && has(props, "data-range-start");
    const end = has(props, "data-in-range") && has(props, "data-range-end");
    const bleed = "calc(var(--sdp-gap, 0.25rem) / -2 - 0.5px)";
    return (
      <Button
        type="button"
        variant={selected ? "solid" : "ghost"}
        color={selected ? undefined : "gray"}
        highContrast={!selected}
        style={{
          position: "relative",
          justifySelf: "center",
          width: DAY_SIZE,
          height: DAY_SIZE,
          margin: 0,
          padding: 0,
          overflow: "visible",
          borderRadius: "var(--radius-2)",
          fontWeight: "var(--font-weight-regular)",
          color: selected ? undefined : has(props, "data-outside") ? "var(--gray-a9)" : undefined,
          boxShadow: has(props, "data-today") && !selected ? "inset 0 0 0 1px var(--accent-8)" : "none",
          ...(has(props, "data-disabled") && { opacity: 0.3, cursor: "not-allowed" }),
          ...style,
        }}
        {...props}
      >
        {band && (
          <span
            data-band=""
            aria-hidden="true"
            style={{
              position: "absolute",
              zIndex: -1,
              insetBlock: 0,
              insetInlineStart: start ? 0 : bleed,
              insetInlineEnd: end ? 0 : bleed,
              backgroundColor: has(props, "data-in-range") ? "var(--accent-a4)" : "var(--accent-a3)",
              borderStartStartRadius: start ? "var(--radius-2)" : 0,
              borderEndStartRadius: start ? "var(--radius-2)" : 0,
              borderStartEndRadius: end ? "var(--radius-2)" : 0,
              borderEndEndRadius: end ? "var(--radius-2)" : 0,
            }}
          />
        )}
        {children}
      </Button>
    );
  },

  DayContent: ({ day }) => <span>{day.day}</span>,

  Footer: ({ presets, onPreset, onToday, todayLabel }) => (
    <Flex wrap="wrap" align="center" gap="1" mt="2" pt="2" style={{ boxShadow: "inset 0 1px var(--gray-a5)" }}>
      {presets.map((preset) => (
        <Button type="button" key={preset.label} size="1" variant="soft" color="gray" onClick={() => onPreset(preset.value)}>
          {preset.label}
        </Button>
      ))}
      <Button type="button" size="1" variant="ghost" onClick={onToday} style={{ marginBlock: 0, marginInlineStart: "auto", marginInlineEnd: 0 }}>
        {todayLabel}
      </Button>
    </Flex>
  ),
};
```
