@motir/design-system 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +150 -5
  2. package/dist/components/theme/AppearancePickers.js +30 -2
  3. package/dist/components/theme/AppearancePickers.js.map +1 -1
  4. package/dist/components/ui/DatePicker.d.ts +31 -0
  5. package/dist/components/ui/DatePicker.js +442 -0
  6. package/dist/components/ui/DatePicker.js.map +1 -0
  7. package/dist/components/ui/DatePickerFormatContext.d.ts +26 -0
  8. package/dist/components/ui/DatePickerFormatContext.js +58 -0
  9. package/dist/components/ui/DatePickerFormatContext.js.map +1 -0
  10. package/dist/components/ui/Modal.d.ts +23 -1
  11. package/dist/components/ui/Modal.js +17 -3
  12. package/dist/components/ui/Modal.js.map +1 -1
  13. package/dist/components/ui/datePickerFormat.d.ts +40 -0
  14. package/dist/components/ui/datePickerFormat.js +30 -0
  15. package/dist/components/ui/datePickerFormat.js.map +1 -0
  16. package/dist/contexts/theme-context.js +30 -2
  17. package/dist/contexts/theme-context.js.map +1 -1
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.js +5 -0
  20. package/dist/mock/index.js +30 -2
  21. package/dist/mock/index.js.map +1 -1
  22. package/dist/mock/renderMock.js +30 -2
  23. package/dist/mock/renderMock.js.map +1 -1
  24. package/dist/specimen/TokensSpecimen.js +2 -2
  25. package/dist/specimen/TokensSpecimen.js.map +1 -1
  26. package/dist/theme/appearance-resolution.js +30 -2
  27. package/dist/theme/appearance-resolution.js.map +1 -1
  28. package/dist/theme/init-script.js +30 -2
  29. package/dist/theme/init-script.js.map +1 -1
  30. package/dist/theme/styles.d.ts +1 -1
  31. package/dist/theme/styles.js +2 -2
  32. package/dist/theme/styles.js.map +1 -1
  33. package/dist/theme/types.js +2 -2
  34. package/dist/theme/types.js.map +1 -1
  35. package/dist/theme/typography.d.ts +46 -3
  36. package/dist/theme/typography.js +28 -0
  37. package/dist/theme/typography.js.map +1 -1
  38. package/package.json +1 -1
  39. package/theme.css +119 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/ui/datePickerFormat.ts","../../../src/components/ui/DatePickerFormatContext.tsx"],"names":[],"mappings":";;;;AAuCO,IAAM,0BAAA,GAA+C,IAAA;AAM5D,IAAM,aAAA,GAA4C;AAAA,EAChD,QAAA,EAAU,KAAA;AAAA,EACV,IAAA,EAAM,SAAA;AAAA,EACN,KAAA,EAAO,OAAA;AAAA,EACP,GAAA,EAAK;AACP,CAAA;AAIA,IAAM,iBAAA,GAAoB,OAAA;AAE1B,SAAS,UAAU,MAAA,EAAkC;AACnD,EAAA,OAAO,MAAA,KAAW,6BAA6B,iBAAA,GAAoB,MAAA;AACrE;AAQO,IAAM,0BAAA,GAAkD,CAAC,OAAA,EAAS,MAAA,KAAW;AAClF,EAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,OAAO,CAAA;AAC7B,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,cAAA,CAAe,SAAA,CAAU,MAAM,GAAG,aAAa,CAAA;AAAA,EAC7D,SAAS,KAAA,EAAO;AAGd,IAAA,IAAI,iBAAiB,UAAA,EAAY,OAAO,IAAA,CAAK,cAAA,CAAe,mBAAmB,aAAa,CAAA;AAC5F,IAAA,MAAM,KAAA;AAAA,EACR;AACF,CAAA;AAQO,SAAS,wBACd,KAAA,GAAgC,EAAC,EACjC,QAAA,GAAmC,EAAC,EACV;AAC1B,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,MAAA,IAAU,QAAA,CAAS,MAAA,IAAU,0BAAA;AAClD,EAAA,MAAM,SAAA,GAAY,KAAA,CAAM,UAAA,IAAc,QAAA,CAAS,UAAA,IAAc,0BAAA;AAC7D,EAAA,OAAO,EAAE,QAAQ,UAAA,EAAY,CAAC,YAAY,SAAA,CAAU,OAAA,EAAS,MAAM,CAAA,EAAE;AACvE;AC9EA,IAAM,uBAAA,GAA0B,aAAA,CAAsC,EAAE,CAAA;AAgBjE,SAAS,wBAAA,CAAyB;AAAA,EACvC,MAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA,EAAkC;AAChC,EAAA,MAAM,MAAA,GAAS,WAAW,uBAAuB,CAAA;AACjD,EAAA,MAAM,KAAA,GAAQ,OAAA;AAAA,IACZ,OAAO;AAAA,MACL,MAAA,EAAQ,UAAU,MAAA,CAAO,MAAA;AAAA,MACzB,UAAA,EAAY,cAAc,MAAA,CAAO;AAAA,KACnC,CAAA;AAAA,IACA,CAAC,MAAA,EAAQ,UAAA,EAAY,MAAA,CAAO,MAAA,EAAQ,OAAO,UAAU;AAAA,GACvD;AACA,EAAA,uBACE,GAAA,CAAC,uBAAA,CAAwB,QAAA,EAAxB,EAAiC,OAAe,QAAA,EAAS,CAAA;AAE9D;AAOO,SAAS,mBAAA,CAAoB,KAAA,GAAgC,EAAC,EAA6B;AAChG,EAAA,MAAM,QAAA,GAAW,WAAW,uBAAuB,CAAA;AACnD,EAAA,MAAM,EAAE,MAAA,EAAQ,UAAA,EAAW,GAAI,KAAA;AAC/B,EAAA,OAAO,OAAA;AAAA,IACL,MAAM,uBAAA,CAAwB,EAAE,MAAA,EAAQ,UAAA,IAAc,QAAQ,CAAA;AAAA,IAC9D,CAAC,MAAA,EAAQ,UAAA,EAAY,QAAQ;AAAA,GAC/B;AACF","file":"DatePickerFormatContext.js","sourcesContent":["// Date-format injection for the package `DatePicker` — the PURE half.\n//\n// The picker formats the chosen date for its trigger with whatever locale and\n// formatter the consuming app hands it, so it needs no i18n library of its own\n// (no `next-intl`, no app path). This module holds the parts a Server Component\n// may import too: the two types, the documented default and the pure resolver.\n// It carries no `'use client'` directive and imports nothing; the React\n// provider + hook live beside it in `DatePickerFormatContext.tsx`.\n//\n// Precedence, resolved for EACH input on its own: a prop beats the provider,\n// and the provider beats the default (locale `en` + `defaultDatePickerFormatter`).\n\n/**\n * The locale the picker formats its trigger date in: a plain BCP-47-style\n * string such as `en` or `zh-CN`. Deliberately opaque — not a closed union — so\n * any consumer can pass its own app locale through unchanged.\n */\nexport type DatePickerLocale = string;\n\n/**\n * Formats an ISO `YYYY-MM-DD` date for display in `locale`. The picker calls it\n * with the selected value and the resolved locale.\n */\nexport type DatePickerFormatter = (isoDate: string, locale: DatePickerLocale) => string;\n\n/** The two injectable inputs, each optional — a prop set or a provider value. */\nexport interface DatePickerFormatInputs {\n locale?: DatePickerLocale;\n formatDate?: DatePickerFormatter;\n}\n\n/** The resolved inputs: the locale in force and a formatter already bound to it. */\nexport interface ResolvedDatePickerFormat {\n locale: DatePickerLocale;\n /** The chosen formatter called with the resolved locale: `formatDate(iso)`. */\n formatDate: (isoDate: string) => string;\n}\n\n/** The locale used when neither a prop nor a provider supplies one. */\nexport const DEFAULT_DATE_PICKER_LOCALE: DatePickerLocale = 'en';\n\n// The exact Intl options motir-core's `formatDate` uses, so the default reads\n// `Jun 3, 2026` for `2026-06-03` byte for byte. The zone is pinned to UTC: an\n// ISO date-only string parses as UTC midnight, so formatting in UTC is what keeps\n// a host west of Greenwich from showing the previous day.\nconst DATE_ONLY_UTC: Intl.DateTimeFormatOptions = {\n timeZone: 'UTC',\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n};\n\n// The tag the default falls back to — and the one bare `en` maps to, matching\n// motir-core's `BCP47` `en` row.\nconst FALLBACK_INTL_TAG = 'en-US';\n\nfunction toIntlTag(locale: DatePickerLocale): string {\n return locale === DEFAULT_DATE_PICKER_LOCALE ? FALLBACK_INTL_TAG : locale;\n}\n\n/**\n * The default formatter: `toLocaleString(locale, { timeZone: 'UTC', year:\n * 'numeric', month: 'short', day: 'numeric' })`. Bare `en` maps to `en-US`, any\n * other string reaches Intl unchanged, and a tag Intl rejects as malformed falls\n * back to the `en-US` output instead of throwing.\n */\nexport const defaultDatePickerFormatter: DatePickerFormatter = (isoDate, locale) => {\n const date = new Date(isoDate);\n try {\n return date.toLocaleString(toIntlTag(locale), DATE_ONLY_UTC);\n } catch (error) {\n // Intl signals a malformed language tag with a RangeError; that one case\n // degrades to the default's own locale. Anything else is a real defect.\n if (error instanceof RangeError) return date.toLocaleString(FALLBACK_INTL_TAG, DATE_ONLY_UTC);\n throw error;\n }\n};\n\n/**\n * Resolve the picker's format inputs: for the locale and for the formatter\n * separately, the prop wins, then the provider's value, then the default. An\n * `undefined` prop falls through. The returned `formatDate` is bound to the\n * resolved locale.\n */\nexport function resolveDatePickerFormat(\n props: DatePickerFormatInputs = {},\n provider: DatePickerFormatInputs = {},\n): ResolvedDatePickerFormat {\n const locale = props.locale ?? provider.locale ?? DEFAULT_DATE_PICKER_LOCALE;\n const formatter = props.formatDate ?? provider.formatDate ?? defaultDatePickerFormatter;\n return { locale, formatDate: (isoDate) => formatter(isoDate, locale) };\n}\n","'use client';\n\nimport { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport {\n resolveDatePickerFormat,\n type DatePickerFormatInputs,\n type ResolvedDatePickerFormat,\n} from './datePickerFormat';\n\n// The client half of the DatePicker's format injection: a context an app sets\n// once for a subtree, and the hook the picker reads it through. The pure types,\n// default and resolver live in `./datePickerFormat` (no directive, server-safe).\n\nconst DatePickerFormatContext = createContext<DatePickerFormatInputs>({});\n\nexport interface DatePickerFormatProviderProps extends DatePickerFormatInputs {\n children: ReactNode;\n}\n\n/**\n * Sets the `DatePicker`'s locale and/or formatter for everything below it.\n * Each input is optional and merges over an enclosing provider PER FIELD: an\n * inner provider that sets only `locale` keeps the outer `formatDate`.\n *\n * @example\n * <DatePickerFormatProvider locale={appLocale} formatDate={appFormatDate}>\n * <App />\n * </DatePickerFormatProvider>\n */\nexport function DatePickerFormatProvider({\n locale,\n formatDate,\n children,\n}: DatePickerFormatProviderProps) {\n const parent = useContext(DatePickerFormatContext);\n const value = useMemo<DatePickerFormatInputs>(\n () => ({\n locale: locale ?? parent.locale,\n formatDate: formatDate ?? parent.formatDate,\n }),\n [locale, formatDate, parent.locale, parent.formatDate],\n );\n return (\n <DatePickerFormatContext.Provider value={value}>{children}</DatePickerFormatContext.Provider>\n );\n}\n\n/**\n * The resolved format for a picker: `props` (a component's own `locale` /\n * `formatDate`) beat the nearest provider, which beats the default — per field.\n * Called with no argument it resolves the provider over the default.\n */\nexport function useDatePickerFormat(props: DatePickerFormatInputs = {}): ResolvedDatePickerFormat {\n const provider = useContext(DatePickerFormatContext);\n const { locale, formatDate } = props;\n return useMemo(\n () => resolveDatePickerFormat({ locale, formatDate }, provider),\n [locale, formatDate, provider],\n );\n}\n"]}
@@ -82,8 +82,30 @@ interface ModalProps extends VariantProps<typeof contentVariants> {
82
82
  * `document.body`.
83
83
  */
84
84
  container?: Element | null;
85
+ /**
86
+ * More element ids that DESCRIBE the dialog, added to its `aria-describedby`
87
+ * after the `description`'s own id (MOTIR-10544; design MOTIR-10543's
88
+ * can't-plan alert). For a dialog whose meaning lives in its BODY as well as
89
+ * its one-line description — a list of reasons — so assistive tech reads the
90
+ * body with the title rather than only when the reader tabs into it. A
91
+ * space-separated id list, as `aria-describedby` takes it.
92
+ */
93
+ describedBy?: string;
94
+ /**
95
+ * Where focus goes when the dialog opens — Radix's `onOpenAutoFocus`. Call
96
+ * `event.preventDefault()` and focus an element yourself to land on a control
97
+ * other than the first focusable one (an alert lands on its safe action).
98
+ */
99
+ onOpenAutoFocus?: (event: Event) => void;
100
+ /**
101
+ * Where focus goes when the dialog closes — Radix's `onCloseAutoFocus`. A
102
+ * dialog opened WITHOUT a `Modal.Trigger` has nothing for Radix to return
103
+ * focus to, so a consumer that opened it from a control calls
104
+ * `event.preventDefault()` and focuses that control here.
105
+ */
106
+ onCloseAutoFocus?: (event: Event) => void;
85
107
  }
86
- declare function ModalRoot({ open, onOpenChange, title, description, size, hideClose, srTitle, className, overlayClassName, role, children, closeLabel, dialogLabel, container, }: ModalProps): react_jsx_runtime.JSX.Element;
108
+ declare function ModalRoot({ open, onOpenChange, title, description, size, hideClose, srTitle, className, overlayClassName, role, children, closeLabel, dialogLabel, container, describedBy, onOpenAutoFocus, onCloseAutoFocus, }: ModalProps): react_jsx_runtime.JSX.Element;
87
109
  declare const Modal: typeof ModalRoot & {
88
110
  Body: react.ForwardRefExoticComponent<react.HTMLAttributes<HTMLDivElement> & react.RefAttributes<HTMLDivElement>>;
89
111
  Footer: react.ForwardRefExoticComponent<react.HTMLAttributes<HTMLDivElement> & react.RefAttributes<HTMLDivElement>>;
@@ -92,11 +92,16 @@ function ModalRoot({
92
92
  children,
93
93
  closeLabel = "Close",
94
94
  dialogLabel = "Dialog",
95
- container
95
+ container,
96
+ describedBy,
97
+ onOpenAutoFocus,
98
+ onCloseAutoFocus
96
99
  }) {
97
100
  const contentRef = useRef(null);
98
101
  const fullscreenContainer = usePortalContainer(contentRef);
99
102
  const portalContainer = container === void 0 ? fullscreenContainer ?? void 0 : container;
103
+ const descriptionId = describedBy ? `${describedBy}-description` : void 0;
104
+ const describedByIds = describedBy ? [description ? descriptionId : null, describedBy].filter(Boolean).join(" ") : null;
100
105
  return /* @__PURE__ */ jsx(Dialog.Root, { open, onOpenChange, children: /* @__PURE__ */ jsxs(Dialog.Portal, { container: portalContainer, children: [
101
106
  /* @__PURE__ */ jsx(
102
107
  Dialog.Overlay,
@@ -112,11 +117,20 @@ function ModalRoot({
112
117
  className: cn(contentVariants({ size }), className),
113
118
  "data-surface": "modal",
114
119
  ...role ? { role } : {},
115
- ...description ? {} : { "aria-describedby": void 0 },
120
+ ...describedByIds ? { "aria-describedby": describedByIds } : description ? {} : { "aria-describedby": void 0 },
121
+ ...onOpenAutoFocus ? { onOpenAutoFocus } : {},
122
+ ...onCloseAutoFocus ? { onCloseAutoFocus } : {},
116
123
  children: [
117
124
  title || description ? /* @__PURE__ */ jsxs("div", { className: "mb-(--spacing-md) shrink-0", children: [
118
125
  title ? /* @__PURE__ */ jsx(Dialog.Title, { className: "font-serif text-xl font-semibold text-(--el-text)", children: title }) : null,
119
- description ? /* @__PURE__ */ jsx(Dialog.Description, { className: "text-(--el-text-subtitle) mt-1 font-sans text-sm", children: description }) : null
126
+ description ? /* @__PURE__ */ jsx(
127
+ Dialog.Description,
128
+ {
129
+ ...describedByIds ? { id: descriptionId } : {},
130
+ className: "text-(--el-text-subtitle) mt-1 font-sans text-sm",
131
+ children: description
132
+ }
133
+ ) : null
120
134
  ] }) : (
121
135
  // Radix requires Title for a11y; provide a visually-hidden one if
122
136
  // missing. `srTitle` lets a consumer with its own visible heading
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/utils/cn.ts","../../../src/utils/fullscreen.ts","../../../src/components/ui/Modal.tsx"],"names":["ModalFooter","ModalBody"],"mappings":"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","file":"Modal.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Merge Tailwind class names without conflicts.\n *\n * `clsx` handles conditional inclusion (booleans, undefineds, arrays);\n * `twMerge` resolves conflicting Tailwind classes (later wins, e.g.\n * `px-2 px-4` → `px-4`). Together they are the de facto standard for\n * composing Tailwind classes in React components.\n *\n * @example\n * cn('px-2 py-1', isActive && 'bg-primary', className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","'use client';\n\nimport { useState, useSyncExternalStore, type RefObject } from 'react';\n\n/**\n * The element currently in NATIVE full screen (`document.fullscreenElement`), or\n * `null` — re-read on every `fullscreenchange`.\n *\n * While an element is in native full screen the browser paints ONLY that\n * element's subtree, in the top layer. Anything portalled to `document.body` —\n * a dialog, a popover, a picker's menu — is not shown at all, whatever its\n * z-index (MOTIR-7658: the roadmap canvas's quick view opened invisibly and\n * held focus and scroll lock until Esc).\n *\n * A portalling primitive does not use this directly: it uses\n * `usePortalContainer`, which also refuses to follow full screen into the\n * primitive's own subtree.\n *\n * `null` on the server and in a document with no Fullscreen API.\n */\nexport function useFullscreenElement(): Element | null {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nfunction subscribe(onChange: () => void): () => void {\n document.addEventListener('fullscreenchange', onChange);\n return () => document.removeEventListener('fullscreenchange', onChange);\n}\n\nfunction getSnapshot(): Element | null {\n return document.fullscreenElement ?? null;\n}\n\nfunction getServerSnapshot(): null {\n return null;\n}\n\n/**\n * The element a portalling surface mounts into: the element in native full\n * screen while there is one (MOTIR-7658), else `null` — which the caller turns\n * into `document.body`.\n *\n * `own` is the surface's portalled root (a dialog's content, a menu). When the\n * element that goes full screen is INSIDE it — the native full-screen button of\n * a `<video>` in a dialog — the container stays where it was (MOTIR-8560).\n * Following it would re-portal the surface into its own descendant: React\n * unmounts the subtree to move it, the full-screen element leaves the document,\n * the browser exits full screen, and the surface snaps back to a fresh copy.\n *\n * `null` on the server and in a document with no Fullscreen API.\n */\nexport function usePortalContainer(own: RefObject<Element | null>): Element | null {\n const [store] = useState(() => createPortalContainerStore(own));\n return useSyncExternalStore(store.subscribe, store.getSnapshot, getServerSnapshot);\n}\n\nfunction createPortalContainerStore(own: RefObject<Element | null>) {\n let container: Element | null =\n typeof document === 'undefined' ? null : (document.fullscreenElement ?? null);\n return {\n subscribe(onChange: () => void): () => void {\n const sync = () => {\n const next = document.fullscreenElement ?? null;\n const root = own.current;\n // The surface's own descendant went full screen: stay put.\n if (next !== null && root !== null && root.contains(next)) return;\n if (next === container) return;\n container = next;\n onChange();\n };\n // A change between creating the store and subscribing to it.\n sync();\n document.addEventListener('fullscreenchange', sync);\n return () => document.removeEventListener('fullscreenchange', sync);\n },\n getSnapshot: (): Element | null => container,\n };\n}\n","'use client';\n\nimport { forwardRef, useRef, type ReactNode } from 'react';\nimport * as Dialog from '@radix-ui/react-dialog';\nimport { X } from 'lucide-react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\nimport { usePortalContainer } from '../../utils/fullscreen';\n\n/**\n * Modal — accessible dialog wrapping @radix-ui/react-dialog.\n *\n * Radix handles focus trap, ESC-to-close, click-outside-to-close, and\n * focus-return-on-close out of the box. We style on top.\n *\n * Open/close is controlled by the consumer via `open` + `onOpenChange`.\n *\n * For a scrollable body with a pinned footer, wrap the fields in `Modal.Body`\n * (it owns the `flex-1 overflow-y-auto` scroll recipe and keeps focus rings\n * from clipping against the scroll edge — see `ModalBody`).\n *\n * @example\n * const [open, setOpen] = useState(false);\n * <Modal open={open} onOpenChange={setOpen} title=\"Confirm\" size=\"md\">\n * <Modal.Body className=\"gap-4\">\n * <Input label=\"Name\" autoFocus />\n * </Modal.Body>\n * <Modal.Footer>\n * <Button variant=\"ghost\" onClick={() => setOpen(false)}>Cancel</Button>\n * <Button variant=\"primary\" onClick={confirm}>Confirm</Button>\n * </Modal.Footer>\n * </Modal>\n */\nconst contentVariants = cva(\n cn(\n 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',\n 'w-[90vw] rounded-(--radius-modal) bg-(--el-page-bg)',\n 'shadow-(--shadow-modal) border border-(--el-border)',\n 'p-(--spacing-card-padding)',\n 'focus:outline-none',\n // Never exceed the viewport: cap height and lay out as a column so a\n // consumer can wrap its fields in `Modal.Body` (which owns the\n // `flex-1 overflow-y-auto` scroll recipe) and pin a `Modal.Footer`,\n // instead of the dialog growing off-screen (e.g. the create modal's\n // expandable Explanation pushing the Create button out of view).\n 'flex max-h-[90vh] flex-col overflow-hidden',\n ),\n {\n variants: {\n // Literal widths, NOT the max-w-sm/md/lg utilities: the design\n // system's @theme defines --spacing-sm/md/lg (12/16/20px), and\n // Tailwind v4 resolves `max-w-{key}` against the --spacing-* scale\n // when that key exists — so `max-w-md` would collapse the modal to\n // 16px wide. Pinning the rem values (Tailwind's stock sm/md/lg) keeps\n // the design-system token set untouched and the dialog readable.\n size: {\n sm: 'max-w-[24rem]',\n md: 'max-w-[28rem]',\n lg: 'max-w-[32rem]',\n // The large peek surface (Subtask 2.5.19's quick view) — a generous\n // dialog that takes a big part of the screen; pairs with a height +\n // p-0 className on the consumer side. Stock Tailwind 4xl rem value, so\n // the design-system --spacing-* scale stays untouched (see the comment\n // above on why we pin rems instead of max-w-{key}).\n xl: 'max-w-[58rem]',\n // The whole viewport (Subtask 5.2.6's attachment preview lightbox —\n // the 2.5.19 growth pattern one step further). Like `xl`, the variant\n // only lifts the size caps; the consumer pairs it with bg/border/\n // radius/padding overrides to drop the panel chrome it doesn't want.\n full: 'h-dvh max-h-dvh w-screen max-w-none',\n },\n },\n defaultVariants: { size: 'md' },\n },\n);\n\nexport interface ModalProps extends VariantProps<typeof contentVariants> {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title?: string;\n description?: string;\n children?: ReactNode;\n /** Hide the default close (×) button in the corner. */\n hideClose?: boolean;\n /**\n * Accessible name for a dialog that renders its OWN visible heading (so no\n * `title` is passed). Radix requires a Dialog.Title; without `title`/`srTitle`\n * we fall back to the generic \"Dialog\". Supply this to label the dialog with\n * something meaningful (e.g. the previewed issue's key) while keeping the\n * heading inside `children` (Subtask 2.5.19's quick-view peek).\n */\n srTitle?: string;\n className?: string;\n /**\n * Extra classes merged onto the backdrop — e.g. the attachment preview\n * lightbox (Subtask 5.2.6) deepens the default `--el-overlay-scrim` to\n * `bg-black/80` per `design/work-items/attachments.mock.html` panel 6.\n */\n overlayClassName?: string;\n /**\n * The dialog's ARIA role. Defaults to Radix's `dialog`; a DESTRUCTIVE confirm\n * passes `alertdialog` (Subtask 2.8.4's delete confirm) so assistive tech\n * treats it as an alert that interrupts and announces its description.\n */\n role?: 'dialog' | 'alertdialog';\n /**\n * Accessible label for the corner close (×) button. Defaults to `'Close'`.\n * i18n is the consuming app's concern — the design-system package is\n * framework-agnostic (no `next-intl` dependency), so a localized app passes\n * the translated string here (motir-core wires it from its `common.close`).\n */\n closeLabel?: string;\n /**\n * Fallback accessible name when neither `title` nor `srTitle` is given.\n * Defaults to `'Dialog'`. See `closeLabel` for the i18n rationale.\n */\n dialogLabel?: string;\n /**\n * The element the dialog portals into. Defaults to the element in NATIVE full\n * screen while there is one (`document.fullscreenElement` — the browser paints\n * nothing outside it, so a body-portalled dialog would open invisibly,\n * MOTIR-7658), else `document.body` — unless that element is inside the\n * dialog itself, where the dialog stays put (MOTIR-8560). Pass `null` to force\n * `document.body`.\n */\n container?: Element | null;\n}\n\nfunction ModalRoot({\n open,\n onOpenChange,\n title,\n description,\n size,\n hideClose,\n srTitle,\n className,\n overlayClassName,\n role,\n children,\n closeLabel = 'Close',\n dialogLabel = 'Dialog',\n container,\n}: ModalProps) {\n // The dialog's own content: a full-screen element inside it (a `<video>`'s\n // native full-screen button) must not pull the dialog into itself (MOTIR-8560).\n const contentRef = useRef<HTMLDivElement>(null);\n const fullscreenContainer = usePortalContainer(contentRef);\n // `undefined` lets Radix fall back to `document.body`.\n const portalContainer = container === undefined ? (fullscreenContainer ?? undefined) : container;\n return (\n <Dialog.Root open={open} onOpenChange={onOpenChange}>\n <Dialog.Portal container={portalContainer}>\n {/* `data-surface=\"overlay\"` lets the glassmorphism style turn this dark\n scrim into a LIGHT, page-blurring backdrop, so the frosted modal\n refracts the (blurred) page behind it instead of darkness — the\n point of a glass modal. Inert for every other style. */}\n <Dialog.Overlay\n data-surface=\"overlay\"\n className={cn('fixed inset-0 z-40 bg-(--el-overlay-scrim)', overlayClassName)}\n />\n {/*\n PRODECT_FINDINGS #8: Radix checks for a describing element\n (Dialog.Description → aria-describedby) independently of the Title\n check. When no `description` prop is passed we render no\n Dialog.Description, so Radix logs a \"Missing Description or\n aria-describedby={undefined}\" warning. We pick the explicit opt-out\n (aria-describedby={undefined}) rather than emitting an empty sr-only\n Description — an empty description announces nothing useful and is\n worse for screen-reader users than declaring the dialog has none.\n */}\n <Dialog.Content\n ref={contentRef}\n className={cn(contentVariants({ size }), className)}\n // `data-surface` lets a surface-material style (glassmorphism) frost\n // the dialog panel — see globals.css's material layer.\n data-surface=\"modal\"\n // A destructive confirm opts into `alertdialog` (2.8.4); otherwise\n // Radix's default `dialog` role applies.\n {...(role ? { role } : {})}\n // Only opt out when there's no description. When `description` is\n // set we omit the prop entirely so Radix keeps auto-wiring\n // aria-describedby to the rendered Dialog.Description's id.\n {...(description ? {} : { 'aria-describedby': undefined })}\n >\n {title || description ? (\n <div className=\"mb-(--spacing-md) shrink-0\">\n {title ? (\n <Dialog.Title className=\"font-serif text-xl font-semibold text-(--el-text)\">\n {title}\n </Dialog.Title>\n ) : null}\n {description ? (\n <Dialog.Description className=\"text-(--el-text-subtitle) mt-1 font-sans text-sm\">\n {description}\n </Dialog.Description>\n ) : null}\n </div>\n ) : (\n // Radix requires Title for a11y; provide a visually-hidden one if\n // missing. `srTitle` lets a consumer with its own visible heading\n // still give the dialog a meaningful accessible name.\n <Dialog.Title className=\"sr-only\">{srTitle ?? dialogLabel}</Dialog.Title>\n )}\n {children}\n {!hideClose ? (\n <Dialog.Close\n aria-label={closeLabel}\n className=\"text-(--el-icon-muted) hover:text-(--el-text) absolute right-3 top-3 rounded-(--radius-control) p-(--spacing-icon-btn) transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color)\"\n >\n <X className=\"h-4 w-4\" />\n </Dialog.Close>\n ) : null}\n </Dialog.Content>\n </Dialog.Portal>\n </Dialog.Root>\n );\n}\n\nconst ModalFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function ModalFooter({ className, children, ...rest }, ref) {\n return (\n <div\n ref={ref}\n className={cn(\n 'border-(--el-border) mt-(--spacing-md) flex items-center justify-end gap-2 border-t pt-(--spacing-md)',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n },\n);\n\n/**\n * ModalBody — the scrollable field area of a modal. Owns the scroll recipe\n * once so consumers don't re-derive it (and re-introduce the focus-ring clip):\n * it fills the remaining column height (the panel caps at `max-h-[90vh]`) and\n * scrolls, so a sibling `Modal.Footer` stays pinned.\n *\n * Ring-safe by construction. `Input` draws its focus ring as\n * `focus-within:ring-2 … ring-offset-2` — a box-shadow extending ~4px OUTSIDE\n * the field's border box. Per the CSS spec, `overflow-y: auto` forces the\n * computed `overflow-x` from `visible` to `auto`, so this body is a clip box on\n * BOTH axes; a full-width field's ring overhang would be painted outside it and\n * silently clipped (rings are box-shadows — they never grow the layout box, so\n * nothing scrolls, the paint is just cut). We pad the scroll container by more\n * than the overhang (`p-1.5` = 6px > 4px) and pull an equal negative margin\n * (`-m-1.5`), so the ring gets room while the body's visual gutter — and the\n * fields' alignment with the modal title/footer — is unchanged.\n *\n * Pass `gap-*` (and any extra layout) via `className`.\n */\nconst ModalBody = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function ModalBody({ className, children, ...rest }, ref) {\n return (\n <div\n ref={ref}\n className={cn(\n 'flex min-h-0 flex-1 flex-col overflow-y-auto',\n // Ring-safe inset (see the component doc above): padding ≥ the focus\n // ring's ~4px overhang + an equal negative margin to keep the gutter.\n '-m-1.5 p-1.5',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n },\n);\n\n/** Convenience trigger — wires children to Radix's DialogTrigger. */\nconst ModalTrigger = Dialog.Trigger;\n\nexport const Modal = Object.assign(ModalRoot, {\n Body: ModalBody,\n Footer: ModalFooter,\n Trigger: ModalTrigger,\n});\n"]}
1
+ {"version":3,"sources":["../../../src/utils/cn.ts","../../../src/utils/fullscreen.ts","../../../src/components/ui/Modal.tsx"],"names":["ModalFooter","ModalBody"],"mappings":"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","file":"Modal.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Merge Tailwind class names without conflicts.\n *\n * `clsx` handles conditional inclusion (booleans, undefineds, arrays);\n * `twMerge` resolves conflicting Tailwind classes (later wins, e.g.\n * `px-2 px-4` → `px-4`). Together they are the de facto standard for\n * composing Tailwind classes in React components.\n *\n * @example\n * cn('px-2 py-1', isActive && 'bg-primary', className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","'use client';\n\nimport { useState, useSyncExternalStore, type RefObject } from 'react';\n\n/**\n * The element currently in NATIVE full screen (`document.fullscreenElement`), or\n * `null` — re-read on every `fullscreenchange`.\n *\n * While an element is in native full screen the browser paints ONLY that\n * element's subtree, in the top layer. Anything portalled to `document.body` —\n * a dialog, a popover, a picker's menu — is not shown at all, whatever its\n * z-index (MOTIR-7658: the roadmap canvas's quick view opened invisibly and\n * held focus and scroll lock until Esc).\n *\n * A portalling primitive does not use this directly: it uses\n * `usePortalContainer`, which also refuses to follow full screen into the\n * primitive's own subtree.\n *\n * `null` on the server and in a document with no Fullscreen API.\n */\nexport function useFullscreenElement(): Element | null {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nfunction subscribe(onChange: () => void): () => void {\n document.addEventListener('fullscreenchange', onChange);\n return () => document.removeEventListener('fullscreenchange', onChange);\n}\n\nfunction getSnapshot(): Element | null {\n return document.fullscreenElement ?? null;\n}\n\nfunction getServerSnapshot(): null {\n return null;\n}\n\n/**\n * The element a portalling surface mounts into: the element in native full\n * screen while there is one (MOTIR-7658), else `null` — which the caller turns\n * into `document.body`.\n *\n * `own` is the surface's portalled root (a dialog's content, a menu). When the\n * element that goes full screen is INSIDE it — the native full-screen button of\n * a `<video>` in a dialog — the container stays where it was (MOTIR-8560).\n * Following it would re-portal the surface into its own descendant: React\n * unmounts the subtree to move it, the full-screen element leaves the document,\n * the browser exits full screen, and the surface snaps back to a fresh copy.\n *\n * `null` on the server and in a document with no Fullscreen API.\n */\nexport function usePortalContainer(own: RefObject<Element | null>): Element | null {\n const [store] = useState(() => createPortalContainerStore(own));\n return useSyncExternalStore(store.subscribe, store.getSnapshot, getServerSnapshot);\n}\n\nfunction createPortalContainerStore(own: RefObject<Element | null>) {\n let container: Element | null =\n typeof document === 'undefined' ? null : (document.fullscreenElement ?? null);\n return {\n subscribe(onChange: () => void): () => void {\n const sync = () => {\n const next = document.fullscreenElement ?? null;\n const root = own.current;\n // The surface's own descendant went full screen: stay put.\n if (next !== null && root !== null && root.contains(next)) return;\n if (next === container) return;\n container = next;\n onChange();\n };\n // A change between creating the store and subscribing to it.\n sync();\n document.addEventListener('fullscreenchange', sync);\n return () => document.removeEventListener('fullscreenchange', sync);\n },\n getSnapshot: (): Element | null => container,\n };\n}\n","'use client';\n\nimport { forwardRef, useRef, type ReactNode } from 'react';\nimport * as Dialog from '@radix-ui/react-dialog';\nimport { X } from 'lucide-react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\nimport { usePortalContainer } from '../../utils/fullscreen';\n\n/**\n * Modal — accessible dialog wrapping @radix-ui/react-dialog.\n *\n * Radix handles focus trap, ESC-to-close, click-outside-to-close, and\n * focus-return-on-close out of the box. We style on top.\n *\n * Open/close is controlled by the consumer via `open` + `onOpenChange`.\n *\n * For a scrollable body with a pinned footer, wrap the fields in `Modal.Body`\n * (it owns the `flex-1 overflow-y-auto` scroll recipe and keeps focus rings\n * from clipping against the scroll edge — see `ModalBody`).\n *\n * @example\n * const [open, setOpen] = useState(false);\n * <Modal open={open} onOpenChange={setOpen} title=\"Confirm\" size=\"md\">\n * <Modal.Body className=\"gap-4\">\n * <Input label=\"Name\" autoFocus />\n * </Modal.Body>\n * <Modal.Footer>\n * <Button variant=\"ghost\" onClick={() => setOpen(false)}>Cancel</Button>\n * <Button variant=\"primary\" onClick={confirm}>Confirm</Button>\n * </Modal.Footer>\n * </Modal>\n */\nconst contentVariants = cva(\n cn(\n 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',\n 'w-[90vw] rounded-(--radius-modal) bg-(--el-page-bg)',\n 'shadow-(--shadow-modal) border border-(--el-border)',\n 'p-(--spacing-card-padding)',\n 'focus:outline-none',\n // Never exceed the viewport: cap height and lay out as a column so a\n // consumer can wrap its fields in `Modal.Body` (which owns the\n // `flex-1 overflow-y-auto` scroll recipe) and pin a `Modal.Footer`,\n // instead of the dialog growing off-screen (e.g. the create modal's\n // expandable Explanation pushing the Create button out of view).\n 'flex max-h-[90vh] flex-col overflow-hidden',\n ),\n {\n variants: {\n // Literal widths, NOT the max-w-sm/md/lg utilities: the design\n // system's @theme defines --spacing-sm/md/lg (12/16/20px), and\n // Tailwind v4 resolves `max-w-{key}` against the --spacing-* scale\n // when that key exists — so `max-w-md` would collapse the modal to\n // 16px wide. Pinning the rem values (Tailwind's stock sm/md/lg) keeps\n // the design-system token set untouched and the dialog readable.\n size: {\n sm: 'max-w-[24rem]',\n md: 'max-w-[28rem]',\n lg: 'max-w-[32rem]',\n // The large peek surface (Subtask 2.5.19's quick view) — a generous\n // dialog that takes a big part of the screen; pairs with a height +\n // p-0 className on the consumer side. Stock Tailwind 4xl rem value, so\n // the design-system --spacing-* scale stays untouched (see the comment\n // above on why we pin rems instead of max-w-{key}).\n xl: 'max-w-[58rem]',\n // The whole viewport (Subtask 5.2.6's attachment preview lightbox —\n // the 2.5.19 growth pattern one step further). Like `xl`, the variant\n // only lifts the size caps; the consumer pairs it with bg/border/\n // radius/padding overrides to drop the panel chrome it doesn't want.\n full: 'h-dvh max-h-dvh w-screen max-w-none',\n },\n },\n defaultVariants: { size: 'md' },\n },\n);\n\nexport interface ModalProps extends VariantProps<typeof contentVariants> {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title?: string;\n description?: string;\n children?: ReactNode;\n /** Hide the default close (×) button in the corner. */\n hideClose?: boolean;\n /**\n * Accessible name for a dialog that renders its OWN visible heading (so no\n * `title` is passed). Radix requires a Dialog.Title; without `title`/`srTitle`\n * we fall back to the generic \"Dialog\". Supply this to label the dialog with\n * something meaningful (e.g. the previewed issue's key) while keeping the\n * heading inside `children` (Subtask 2.5.19's quick-view peek).\n */\n srTitle?: string;\n className?: string;\n /**\n * Extra classes merged onto the backdrop — e.g. the attachment preview\n * lightbox (Subtask 5.2.6) deepens the default `--el-overlay-scrim` to\n * `bg-black/80` per `design/work-items/attachments.mock.html` panel 6.\n */\n overlayClassName?: string;\n /**\n * The dialog's ARIA role. Defaults to Radix's `dialog`; a DESTRUCTIVE confirm\n * passes `alertdialog` (Subtask 2.8.4's delete confirm) so assistive tech\n * treats it as an alert that interrupts and announces its description.\n */\n role?: 'dialog' | 'alertdialog';\n /**\n * Accessible label for the corner close (×) button. Defaults to `'Close'`.\n * i18n is the consuming app's concern — the design-system package is\n * framework-agnostic (no `next-intl` dependency), so a localized app passes\n * the translated string here (motir-core wires it from its `common.close`).\n */\n closeLabel?: string;\n /**\n * Fallback accessible name when neither `title` nor `srTitle` is given.\n * Defaults to `'Dialog'`. See `closeLabel` for the i18n rationale.\n */\n dialogLabel?: string;\n /**\n * The element the dialog portals into. Defaults to the element in NATIVE full\n * screen while there is one (`document.fullscreenElement` — the browser paints\n * nothing outside it, so a body-portalled dialog would open invisibly,\n * MOTIR-7658), else `document.body` — unless that element is inside the\n * dialog itself, where the dialog stays put (MOTIR-8560). Pass `null` to force\n * `document.body`.\n */\n container?: Element | null;\n /**\n * More element ids that DESCRIBE the dialog, added to its `aria-describedby`\n * after the `description`'s own id (MOTIR-10544; design MOTIR-10543's\n * can't-plan alert). For a dialog whose meaning lives in its BODY as well as\n * its one-line description — a list of reasons — so assistive tech reads the\n * body with the title rather than only when the reader tabs into it. A\n * space-separated id list, as `aria-describedby` takes it.\n */\n describedBy?: string;\n /**\n * Where focus goes when the dialog opens — Radix's `onOpenAutoFocus`. Call\n * `event.preventDefault()` and focus an element yourself to land on a control\n * other than the first focusable one (an alert lands on its safe action).\n */\n onOpenAutoFocus?: (event: Event) => void;\n /**\n * Where focus goes when the dialog closes — Radix's `onCloseAutoFocus`. A\n * dialog opened WITHOUT a `Modal.Trigger` has nothing for Radix to return\n * focus to, so a consumer that opened it from a control calls\n * `event.preventDefault()` and focuses that control here.\n */\n onCloseAutoFocus?: (event: Event) => void;\n}\n\nfunction ModalRoot({\n open,\n onOpenChange,\n title,\n description,\n size,\n hideClose,\n srTitle,\n className,\n overlayClassName,\n role,\n children,\n closeLabel = 'Close',\n dialogLabel = 'Dialog',\n container,\n describedBy,\n onOpenAutoFocus,\n onCloseAutoFocus,\n}: ModalProps) {\n // The dialog's own content: a full-screen element inside it (a `<video>`'s\n // native full-screen button) must not pull the dialog into itself (MOTIR-8560).\n const contentRef = useRef<HTMLDivElement>(null);\n const fullscreenContainer = usePortalContainer(contentRef);\n // `undefined` lets Radix fall back to `document.body`.\n const portalContainer = container === undefined ? (fullscreenContainer ?? undefined) : container;\n // With `describedBy` the description's id is OURS, so the content can name it\n // beside the extra ids; without it Radix wires its own id (unchanged). Derived\n // from the caller's id rather than a `useId` of our own, so a Modal without\n // `describedBy` draws no extra id and every other `useId` keeps its value.\n const descriptionId = describedBy ? `${describedBy}-description` : undefined;\n const describedByIds = describedBy\n ? [description ? descriptionId : null, describedBy].filter(Boolean).join(' ')\n : null;\n return (\n <Dialog.Root open={open} onOpenChange={onOpenChange}>\n <Dialog.Portal container={portalContainer}>\n {/* `data-surface=\"overlay\"` lets the glassmorphism style turn this dark\n scrim into a LIGHT, page-blurring backdrop, so the frosted modal\n refracts the (blurred) page behind it instead of darkness — the\n point of a glass modal. Inert for every other style. */}\n <Dialog.Overlay\n data-surface=\"overlay\"\n className={cn('fixed inset-0 z-40 bg-(--el-overlay-scrim)', overlayClassName)}\n />\n {/*\n PRODECT_FINDINGS #8: Radix checks for a describing element\n (Dialog.Description → aria-describedby) independently of the Title\n check. When no `description` prop is passed we render no\n Dialog.Description, so Radix logs a \"Missing Description or\n aria-describedby={undefined}\" warning. We pick the explicit opt-out\n (aria-describedby={undefined}) rather than emitting an empty sr-only\n Description — an empty description announces nothing useful and is\n worse for screen-reader users than declaring the dialog has none.\n */}\n <Dialog.Content\n ref={contentRef}\n className={cn(contentVariants({ size }), className)}\n // `data-surface` lets a surface-material style (glassmorphism) frost\n // the dialog panel — see globals.css's material layer.\n data-surface=\"modal\"\n // A destructive confirm opts into `alertdialog` (2.8.4); otherwise\n // Radix's default `dialog` role applies.\n {...(role ? { role } : {})}\n // Only opt out when there's no description. When `description` is\n // set we omit the prop entirely so Radix keeps auto-wiring\n // aria-describedby to the rendered Dialog.Description's id.\n {...(describedByIds\n ? { 'aria-describedby': describedByIds }\n : description\n ? {}\n : { 'aria-describedby': undefined })}\n {...(onOpenAutoFocus ? { onOpenAutoFocus } : {})}\n {...(onCloseAutoFocus ? { onCloseAutoFocus } : {})}\n >\n {title || description ? (\n <div className=\"mb-(--spacing-md) shrink-0\">\n {title ? (\n <Dialog.Title className=\"font-serif text-xl font-semibold text-(--el-text)\">\n {title}\n </Dialog.Title>\n ) : null}\n {description ? (\n <Dialog.Description\n {...(describedByIds ? { id: descriptionId } : {})}\n className=\"text-(--el-text-subtitle) mt-1 font-sans text-sm\"\n >\n {description}\n </Dialog.Description>\n ) : null}\n </div>\n ) : (\n // Radix requires Title for a11y; provide a visually-hidden one if\n // missing. `srTitle` lets a consumer with its own visible heading\n // still give the dialog a meaningful accessible name.\n <Dialog.Title className=\"sr-only\">{srTitle ?? dialogLabel}</Dialog.Title>\n )}\n {children}\n {!hideClose ? (\n <Dialog.Close\n aria-label={closeLabel}\n className=\"text-(--el-icon-muted) hover:text-(--el-text) absolute right-3 top-3 rounded-(--radius-control) p-(--spacing-icon-btn) transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color)\"\n >\n <X className=\"h-4 w-4\" />\n </Dialog.Close>\n ) : null}\n </Dialog.Content>\n </Dialog.Portal>\n </Dialog.Root>\n );\n}\n\nconst ModalFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function ModalFooter({ className, children, ...rest }, ref) {\n return (\n <div\n ref={ref}\n className={cn(\n 'border-(--el-border) mt-(--spacing-md) flex items-center justify-end gap-2 border-t pt-(--spacing-md)',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n },\n);\n\n/**\n * ModalBody — the scrollable field area of a modal. Owns the scroll recipe\n * once so consumers don't re-derive it (and re-introduce the focus-ring clip):\n * it fills the remaining column height (the panel caps at `max-h-[90vh]`) and\n * scrolls, so a sibling `Modal.Footer` stays pinned.\n *\n * Ring-safe by construction. `Input` draws its focus ring as\n * `focus-within:ring-2 … ring-offset-2` — a box-shadow extending ~4px OUTSIDE\n * the field's border box. Per the CSS spec, `overflow-y: auto` forces the\n * computed `overflow-x` from `visible` to `auto`, so this body is a clip box on\n * BOTH axes; a full-width field's ring overhang would be painted outside it and\n * silently clipped (rings are box-shadows — they never grow the layout box, so\n * nothing scrolls, the paint is just cut). We pad the scroll container by more\n * than the overhang (`p-1.5` = 6px > 4px) and pull an equal negative margin\n * (`-m-1.5`), so the ring gets room while the body's visual gutter — and the\n * fields' alignment with the modal title/footer — is unchanged.\n *\n * Pass `gap-*` (and any extra layout) via `className`.\n */\nconst ModalBody = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function ModalBody({ className, children, ...rest }, ref) {\n return (\n <div\n ref={ref}\n className={cn(\n 'flex min-h-0 flex-1 flex-col overflow-y-auto',\n // Ring-safe inset (see the component doc above): padding ≥ the focus\n // ring's ~4px overhang + an equal negative margin to keep the gutter.\n '-m-1.5 p-1.5',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n },\n);\n\n/** Convenience trigger — wires children to Radix's DialogTrigger. */\nconst ModalTrigger = Dialog.Trigger;\n\nexport const Modal = Object.assign(ModalRoot, {\n Body: ModalBody,\n Footer: ModalFooter,\n Trigger: ModalTrigger,\n});\n"]}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The locale the picker formats its trigger date in: a plain BCP-47-style
3
+ * string such as `en` or `zh-CN`. Deliberately opaque — not a closed union — so
4
+ * any consumer can pass its own app locale through unchanged.
5
+ */
6
+ type DatePickerLocale = string;
7
+ /**
8
+ * Formats an ISO `YYYY-MM-DD` date for display in `locale`. The picker calls it
9
+ * with the selected value and the resolved locale.
10
+ */
11
+ type DatePickerFormatter = (isoDate: string, locale: DatePickerLocale) => string;
12
+ /** The two injectable inputs, each optional — a prop set or a provider value. */
13
+ interface DatePickerFormatInputs {
14
+ locale?: DatePickerLocale;
15
+ formatDate?: DatePickerFormatter;
16
+ }
17
+ /** The resolved inputs: the locale in force and a formatter already bound to it. */
18
+ interface ResolvedDatePickerFormat {
19
+ locale: DatePickerLocale;
20
+ /** The chosen formatter called with the resolved locale: `formatDate(iso)`. */
21
+ formatDate: (isoDate: string) => string;
22
+ }
23
+ /** The locale used when neither a prop nor a provider supplies one. */
24
+ declare const DEFAULT_DATE_PICKER_LOCALE: DatePickerLocale;
25
+ /**
26
+ * The default formatter: `toLocaleString(locale, { timeZone: 'UTC', year:
27
+ * 'numeric', month: 'short', day: 'numeric' })`. Bare `en` maps to `en-US`, any
28
+ * other string reaches Intl unchanged, and a tag Intl rejects as malformed falls
29
+ * back to the `en-US` output instead of throwing.
30
+ */
31
+ declare const defaultDatePickerFormatter: DatePickerFormatter;
32
+ /**
33
+ * Resolve the picker's format inputs: for the locale and for the formatter
34
+ * separately, the prop wins, then the provider's value, then the default. An
35
+ * `undefined` prop falls through. The returned `formatDate` is bound to the
36
+ * resolved locale.
37
+ */
38
+ declare function resolveDatePickerFormat(props?: DatePickerFormatInputs, provider?: DatePickerFormatInputs): ResolvedDatePickerFormat;
39
+
40
+ export { DEFAULT_DATE_PICKER_LOCALE, type DatePickerFormatInputs, type DatePickerFormatter, type DatePickerLocale, type ResolvedDatePickerFormat, defaultDatePickerFormatter, resolveDatePickerFormat };
@@ -0,0 +1,30 @@
1
+ // src/components/ui/datePickerFormat.ts
2
+ var DEFAULT_DATE_PICKER_LOCALE = "en";
3
+ var DATE_ONLY_UTC = {
4
+ timeZone: "UTC",
5
+ year: "numeric",
6
+ month: "short",
7
+ day: "numeric"
8
+ };
9
+ var FALLBACK_INTL_TAG = "en-US";
10
+ function toIntlTag(locale) {
11
+ return locale === DEFAULT_DATE_PICKER_LOCALE ? FALLBACK_INTL_TAG : locale;
12
+ }
13
+ var defaultDatePickerFormatter = (isoDate, locale) => {
14
+ const date = new Date(isoDate);
15
+ try {
16
+ return date.toLocaleString(toIntlTag(locale), DATE_ONLY_UTC);
17
+ } catch (error) {
18
+ if (error instanceof RangeError) return date.toLocaleString(FALLBACK_INTL_TAG, DATE_ONLY_UTC);
19
+ throw error;
20
+ }
21
+ };
22
+ function resolveDatePickerFormat(props = {}, provider = {}) {
23
+ const locale = props.locale ?? provider.locale ?? DEFAULT_DATE_PICKER_LOCALE;
24
+ const formatter = props.formatDate ?? provider.formatDate ?? defaultDatePickerFormatter;
25
+ return { locale, formatDate: (isoDate) => formatter(isoDate, locale) };
26
+ }
27
+
28
+ export { DEFAULT_DATE_PICKER_LOCALE, defaultDatePickerFormatter, resolveDatePickerFormat };
29
+ //# sourceMappingURL=datePickerFormat.js.map
30
+ //# sourceMappingURL=datePickerFormat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/ui/datePickerFormat.ts"],"names":[],"mappings":";AAuCO,IAAM,0BAAA,GAA+C;AAM5D,IAAM,aAAA,GAA4C;AAAA,EAChD,QAAA,EAAU,KAAA;AAAA,EACV,IAAA,EAAM,SAAA;AAAA,EACN,KAAA,EAAO,OAAA;AAAA,EACP,GAAA,EAAK;AACP,CAAA;AAIA,IAAM,iBAAA,GAAoB,OAAA;AAE1B,SAAS,UAAU,MAAA,EAAkC;AACnD,EAAA,OAAO,MAAA,KAAW,6BAA6B,iBAAA,GAAoB,MAAA;AACrE;AAQO,IAAM,0BAAA,GAAkD,CAAC,OAAA,EAAS,MAAA,KAAW;AAClF,EAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,OAAO,CAAA;AAC7B,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,cAAA,CAAe,SAAA,CAAU,MAAM,GAAG,aAAa,CAAA;AAAA,EAC7D,SAAS,KAAA,EAAO;AAGd,IAAA,IAAI,iBAAiB,UAAA,EAAY,OAAO,IAAA,CAAK,cAAA,CAAe,mBAAmB,aAAa,CAAA;AAC5F,IAAA,MAAM,KAAA;AAAA,EACR;AACF;AAQO,SAAS,wBACd,KAAA,GAAgC,EAAC,EACjC,QAAA,GAAmC,EAAC,EACV;AAC1B,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,MAAA,IAAU,QAAA,CAAS,MAAA,IAAU,0BAAA;AAClD,EAAA,MAAM,SAAA,GAAY,KAAA,CAAM,UAAA,IAAc,QAAA,CAAS,UAAA,IAAc,0BAAA;AAC7D,EAAA,OAAO,EAAE,QAAQ,UAAA,EAAY,CAAC,YAAY,SAAA,CAAU,OAAA,EAAS,MAAM,CAAA,EAAE;AACvE","file":"datePickerFormat.js","sourcesContent":["// Date-format injection for the package `DatePicker` — the PURE half.\n//\n// The picker formats the chosen date for its trigger with whatever locale and\n// formatter the consuming app hands it, so it needs no i18n library of its own\n// (no `next-intl`, no app path). This module holds the parts a Server Component\n// may import too: the two types, the documented default and the pure resolver.\n// It carries no `'use client'` directive and imports nothing; the React\n// provider + hook live beside it in `DatePickerFormatContext.tsx`.\n//\n// Precedence, resolved for EACH input on its own: a prop beats the provider,\n// and the provider beats the default (locale `en` + `defaultDatePickerFormatter`).\n\n/**\n * The locale the picker formats its trigger date in: a plain BCP-47-style\n * string such as `en` or `zh-CN`. Deliberately opaque — not a closed union — so\n * any consumer can pass its own app locale through unchanged.\n */\nexport type DatePickerLocale = string;\n\n/**\n * Formats an ISO `YYYY-MM-DD` date for display in `locale`. The picker calls it\n * with the selected value and the resolved locale.\n */\nexport type DatePickerFormatter = (isoDate: string, locale: DatePickerLocale) => string;\n\n/** The two injectable inputs, each optional — a prop set or a provider value. */\nexport interface DatePickerFormatInputs {\n locale?: DatePickerLocale;\n formatDate?: DatePickerFormatter;\n}\n\n/** The resolved inputs: the locale in force and a formatter already bound to it. */\nexport interface ResolvedDatePickerFormat {\n locale: DatePickerLocale;\n /** The chosen formatter called with the resolved locale: `formatDate(iso)`. */\n formatDate: (isoDate: string) => string;\n}\n\n/** The locale used when neither a prop nor a provider supplies one. */\nexport const DEFAULT_DATE_PICKER_LOCALE: DatePickerLocale = 'en';\n\n// The exact Intl options motir-core's `formatDate` uses, so the default reads\n// `Jun 3, 2026` for `2026-06-03` byte for byte. The zone is pinned to UTC: an\n// ISO date-only string parses as UTC midnight, so formatting in UTC is what keeps\n// a host west of Greenwich from showing the previous day.\nconst DATE_ONLY_UTC: Intl.DateTimeFormatOptions = {\n timeZone: 'UTC',\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n};\n\n// The tag the default falls back to — and the one bare `en` maps to, matching\n// motir-core's `BCP47` `en` row.\nconst FALLBACK_INTL_TAG = 'en-US';\n\nfunction toIntlTag(locale: DatePickerLocale): string {\n return locale === DEFAULT_DATE_PICKER_LOCALE ? FALLBACK_INTL_TAG : locale;\n}\n\n/**\n * The default formatter: `toLocaleString(locale, { timeZone: 'UTC', year:\n * 'numeric', month: 'short', day: 'numeric' })`. Bare `en` maps to `en-US`, any\n * other string reaches Intl unchanged, and a tag Intl rejects as malformed falls\n * back to the `en-US` output instead of throwing.\n */\nexport const defaultDatePickerFormatter: DatePickerFormatter = (isoDate, locale) => {\n const date = new Date(isoDate);\n try {\n return date.toLocaleString(toIntlTag(locale), DATE_ONLY_UTC);\n } catch (error) {\n // Intl signals a malformed language tag with a RangeError; that one case\n // degrades to the default's own locale. Anything else is a real defect.\n if (error instanceof RangeError) return date.toLocaleString(FALLBACK_INTL_TAG, DATE_ONLY_UTC);\n throw error;\n }\n};\n\n/**\n * Resolve the picker's format inputs: for the locale and for the formatter\n * separately, the prop wins, then the provider's value, then the default. An\n * `undefined` prop falls through. The returned `formatDate` is bound to the\n * resolved locale.\n */\nexport function resolveDatePickerFormat(\n props: DatePickerFormatInputs = {},\n provider: DatePickerFormatInputs = {},\n): ResolvedDatePickerFormat {\n const locale = props.locale ?? provider.locale ?? DEFAULT_DATE_PICKER_LOCALE;\n const formatter = props.formatDate ?? provider.formatDate ?? defaultDatePickerFormatter;\n return { locale, formatDate: (isoDate) => formatter(isoDate, locale) };\n}\n"]}
@@ -45,6 +45,34 @@ var TYPE_REGISTRY = {
45
45
  tagline: "IBM Plex Mono throughout \u2014 a precise, developer-grade UI.",
46
46
  faces: "IBM Plex Mono headlines + body/UI + meta/code.",
47
47
  designDoc: "docs/typography/mono-technical.md"
48
+ },
49
+ marker: {
50
+ id: "marker",
51
+ name: "Marker",
52
+ tagline: "Hand-lettered marker headlines over a clean sans body \u2014 sketchy, friendly, human.",
53
+ faces: "Caveat marker headlines \xB7 Inter body \xB7 JetBrains Mono meta.",
54
+ designDoc: "docs/typography/marker.md"
55
+ },
56
+ rounded: {
57
+ id: "rounded",
58
+ name: "Rounded",
59
+ tagline: "A soft rounded sans across the whole UI \u2014 friendly and approachable.",
60
+ faces: "Nunito headlines + body/UI \xB7 JetBrains Mono meta.",
61
+ designDoc: "docs/typography/rounded.md"
62
+ },
63
+ slab: {
64
+ id: "slab",
65
+ name: "Slab",
66
+ tagline: "Sturdy slab-serif headlines over a clean sans body \u2014 confident, print-like.",
67
+ faces: "Roboto Slab headlines \xB7 Inter body \xB7 JetBrains Mono meta.",
68
+ designDoc: "docs/typography/slab.md"
69
+ },
70
+ humanist: {
71
+ id: "humanist",
72
+ name: "Humanist",
73
+ tagline: "A warm humanist sans across the whole UI \u2014 open and readable.",
74
+ faces: "Fira Sans headlines + body/UI \xB7 JetBrains Mono meta.",
75
+ designDoc: "docs/typography/humanist.md"
48
76
  }
49
77
  };
50
78
  function isTypeId(value) {
@@ -221,7 +249,7 @@ var STYLE_REGISTRY = {
221
249
  tagline: "Sketchy and friendly \u2014 rough hand-inked edges, wonky corners, soft offset shadows, a playful bounce.",
222
250
  inspiration: "Indie / zine + hand-drawn web design \u2014 Excalidraw\u2019s rough hand-sketched strokes and doodled notebook UIs: warm, imperfect, human.",
223
251
  designDoc: "docs/styles/hand-drawn-indie.md",
224
- defaultTypeId: "editorial",
252
+ defaultTypeId: "marker",
225
253
  dimensions: {
226
254
  silhouette: "Wonky hand-drawn corners \u2014 every surface gets a strongly asymmetric multi-value radius (one corner near-sharp, the opposite generously round, no two alike) so cards/buttons/inputs read as DRAWN boxes, never machined rectangles.",
227
255
  stroke: "Genuinely ROUGH, wavy ink edges \u2014 a turbulence-displacement filter (Excalidraw / rough.js style) warps each outline into a hand-inked wobble, drawn on a content-safe pseudo-border so the lines are sketchy but the text stays crisp.",
@@ -229,7 +257,7 @@ var STYLE_REGISTRY = {
229
257
  surface: "Opaque, warm, paper-like panels \u2014 colour stays the palette axis; the personality is in the rough wavy outline + the soft shadow, not a tint or a wash.",
230
258
  density: "Roomy and relaxed \u2014 generous 20\xD712 buttons, 26px card padding, taller 38px controls; the hand-drawn feel wants air, like margins round a sketch.",
231
259
  motion: "Springy and playful \u2014 gentle 200ms transitions with a bouncy hover lift (1.02) and a deeper press squish (0.97); the UI has a friendly wobble.",
232
- typography: "Ships the Editorial pairing (Fraunces display headlines + Inter body) \u2014 the warmest, most characterful registered face, its soft optical wonk echoing the hand-drawn shapes; an actual marker/script face is a future data-type addition.",
260
+ typography: "Ships the Marker pairing (Caveat hand-lettered marker headlines + Inter body) \u2014 the headlines read as if written with the same pen that inked the rough strokes, while the Inter body stays fully legible.",
233
261
  components: "Wonky-cornered buttons/inputs/cards/modals, irregular (non-pill) status chips, rough wavy-ink-bordered surfaces with soft hand-placed shadows."
234
262
  }
235
263
  },