@krizaka/ui 2.10.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/audio-player.js +1 -1
- package/dist/{chunk-EUPTXKH7.js → chunk-BRKMQ6JV.js} +2 -1
- package/dist/chunk-FDCMBWUU.js +168 -0
- package/dist/chunk-FDCMBWUU.js.map +1 -0
- package/dist/chunk-HCNOOPZE.js +210 -0
- package/dist/chunk-HCNOOPZE.js.map +1 -0
- package/dist/{chunk-X6TW3X6Y.js → chunk-LCZXS6AK.js} +2 -2
- package/dist/chunk-OGJI5X4W.js +157 -0
- package/dist/chunk-OGJI5X4W.js.map +1 -0
- package/dist/combobox.js +2 -2
- package/dist/data-table.d.ts +284 -24
- package/dist/data-table.js +914 -121
- package/dist/data-table.js.map +1 -1
- package/dist/date-picker.js +1 -1
- package/dist/filter-bar.d.ts +12 -12
- package/dist/filter-bar.js +9 -145
- package/dist/filter-bar.js.map +1 -1
- package/dist/form/server.d.ts +96 -0
- package/dist/form/server.js +82 -0
- package/dist/form/server.js.map +1 -0
- package/dist/form.d.ts +503 -0
- package/dist/form.js +760 -0
- package/dist/form.js.map +1 -0
- package/dist/image-cropper.js +1 -1
- package/dist/media-capture.js +1 -1
- package/dist/native/form.d.ts +376 -0
- package/dist/native/form.js +650 -0
- package/dist/native/form.js.map +1 -0
- package/dist/native/list.d.ts +95 -0
- package/dist/native/list.js +135 -0
- package/dist/native/list.js.map +1 -0
- package/dist/native.d.ts +113 -2
- package/dist/native.js +618 -388
- package/dist/native.js.map +1 -1
- package/dist/search-field.js +6 -160
- package/dist/search-field.js.map +1 -1
- package/dist/tag-input.js +2 -2
- package/dist/video-player.js +1 -1
- package/dist/video-trimmer.js +1 -1
- package/dist/virtual.d.ts +140 -0
- package/dist/virtual.js +19 -0
- package/dist/virtual.js.map +1 -0
- package/package.json +111 -8
- package/registry/achievement.json +1 -1
- package/registry/action-bar.json +1 -1
- package/registry/alert.json +1 -1
- package/registry/app-shell.json +1 -1
- package/registry/argument-card.json +1 -1
- package/registry/audio-player.json +1 -1
- package/registry/auth-layout.json +1 -1
- package/registry/avatar.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/button.json +1 -1
- package/registry/card-grid.json +1 -1
- package/registry/card.json +1 -1
- package/registry/catalogue-page.json +1 -1
- package/registry/changelog-page.json +1 -1
- package/registry/checkbox.json +89 -4
- package/registry/chip-link.json +1 -1
- package/registry/chip.json +1 -1
- package/registry/choice-grid.json +1 -1
- package/registry/code-block.json +1 -1
- package/registry/combobox.json +1 -1
- package/registry/command.json +1 -1
- package/registry/component-page.json +1 -1
- package/registry/confirm-button.json +1 -1
- package/registry/contact-page.json +1 -1
- package/registry/continue-prompt.json +1 -1
- package/registry/countdown.json +1 -1
- package/registry/cta-panel.json +1 -1
- package/registry/dashboard-layout.json +1 -1
- package/registry/data-table.json +643 -49
- package/registry/date-picker.json +142 -4
- package/registry/decision-list.json +1 -1
- package/registry/detail-layout.json +1 -1
- package/registry/dialog.json +1 -1
- package/registry/docs-article-page.json +1 -1
- package/registry/docs-hub-page.json +1 -1
- package/registry/docs-layout.json +1 -1
- package/registry/dropdown-menu.json +1 -1
- package/registry/empty-state.json +1 -1
- package/registry/error-page.json +1 -1
- package/registry/examples/checkbox/native/terms.d.ts +4 -0
- package/registry/examples/checkbox/native/terms.dark.png +0 -0
- package/registry/examples/checkbox/native/terms.js +26 -0
- package/registry/examples/checkbox/native/terms.light.png +0 -0
- package/registry/examples/checkbox/native/terms.tsx +28 -0
- package/registry/examples/data-table/details.d.ts +4 -0
- package/registry/examples/data-table/details.js +33 -0
- package/registry/examples/data-table/details.tsx +31 -0
- package/registry/examples/data-table/filters.d.ts +4 -0
- package/registry/examples/data-table/filters.js +40 -0
- package/registry/examples/data-table/filters.tsx +40 -0
- package/registry/examples/data-table/grouped.d.ts +4 -0
- package/registry/examples/data-table/grouped.js +24 -0
- package/registry/examples/data-table/grouped.tsx +23 -0
- package/registry/examples/data-table/paginated.d.ts +4 -0
- package/registry/examples/data-table/paginated.js +23 -0
- package/registry/examples/data-table/paginated.tsx +22 -0
- package/registry/examples/data-table/pinned.d.ts +4 -0
- package/registry/examples/data-table/pinned.js +26 -0
- package/registry/examples/data-table/pinned.tsx +25 -0
- package/registry/examples/data-table/server.d.ts +4 -0
- package/registry/examples/data-table/server.js +40 -0
- package/registry/examples/data-table/server.tsx +40 -0
- package/registry/examples/data-table/virtualized.js +2 -2
- package/registry/examples/data-table/virtualized.tsx +3 -3
- package/registry/examples/date-picker/native/birth-date.d.ts +4 -0
- package/registry/examples/date-picker/native/birth-date.dark.png +0 -0
- package/registry/examples/date-picker/native/birth-date.js +30 -0
- package/registry/examples/date-picker/native/birth-date.light.png +0 -0
- package/registry/examples/date-picker/native/birth-date.tsx +27 -0
- package/registry/examples/field/native/default.d.ts +4 -0
- package/registry/examples/field/native/default.dark.png +0 -0
- package/registry/examples/field/native/default.js +29 -0
- package/registry/examples/field/native/default.light.png +0 -0
- package/registry/examples/field/native/default.tsx +23 -0
- package/registry/examples/form/amount.d.ts +4 -0
- package/registry/examples/form/amount.js +30 -0
- package/registry/examples/form/amount.tsx +36 -0
- package/registry/examples/form/audience.d.ts +4 -0
- package/registry/examples/form/audience.js +29 -0
- package/registry/examples/form/audience.tsx +31 -0
- package/registry/examples/form/native/amount.d.ts +4 -0
- package/registry/examples/form/native/amount.dark.png +0 -0
- package/registry/examples/form/native/amount.js +26 -0
- package/registry/examples/form/native/amount.light.png +0 -0
- package/registry/examples/form/native/amount.tsx +30 -0
- package/registry/examples/form/native/audience.d.ts +4 -0
- package/registry/examples/form/native/audience.dark.png +0 -0
- package/registry/examples/form/native/audience.js +24 -0
- package/registry/examples/form/native/audience.light.png +0 -0
- package/registry/examples/form/native/audience.tsx +24 -0
- package/registry/examples/form/native/profile.d.ts +4 -0
- package/registry/examples/form/native/profile.dark.png +0 -0
- package/registry/examples/form/native/profile.js +33 -0
- package/registry/examples/form/native/profile.light.png +0 -0
- package/registry/examples/form/native/profile.tsx +36 -0
- package/registry/examples/form/native/settings.d.ts +4 -0
- package/registry/examples/form/native/settings.dark.png +0 -0
- package/registry/examples/form/native/settings.js +25 -0
- package/registry/examples/form/native/settings.light.png +0 -0
- package/registry/examples/form/native/settings.tsx +23 -0
- package/registry/examples/form/native/sign-up.d.ts +4 -0
- package/registry/examples/form/native/sign-up.dark.png +0 -0
- package/registry/examples/form/native/sign-up.js +38 -0
- package/registry/examples/form/native/sign-up.light.png +0 -0
- package/registry/examples/form/native/sign-up.tsx +34 -0
- package/registry/examples/form/native/upload.d.ts +4 -0
- package/registry/examples/form/native/upload.dark.png +0 -0
- package/registry/examples/form/native/upload.js +23 -0
- package/registry/examples/form/native/upload.light.png +0 -0
- package/registry/examples/form/native/upload.tsx +22 -0
- package/registry/examples/form/profile.d.ts +4 -0
- package/registry/examples/form/profile.js +35 -0
- package/registry/examples/form/profile.tsx +40 -0
- package/registry/examples/form/report.d.ts +4 -0
- package/registry/examples/form/report.js +30 -0
- package/registry/examples/form/report.tsx +30 -0
- package/registry/examples/form/settings.d.ts +4 -0
- package/registry/examples/form/settings.js +30 -0
- package/registry/examples/form/settings.tsx +31 -0
- package/registry/examples/form/sign-up.d.ts +4 -0
- package/registry/examples/form/sign-up.js +38 -0
- package/registry/examples/form/sign-up.tsx +37 -0
- package/registry/examples/form/upload.d.ts +4 -0
- package/registry/examples/form/upload.js +32 -0
- package/registry/examples/form/upload.tsx +32 -0
- package/registry/examples/list/native/default.d.ts +4 -0
- package/registry/examples/list/native/default.dark.png +0 -0
- package/registry/examples/list/native/default.js +33 -0
- package/registry/examples/list/native/default.light.png +0 -0
- package/registry/examples/list/native/default.tsx +31 -0
- package/registry/examples/list/native/infinite.d.ts +4 -0
- package/registry/examples/list/native/infinite.dark.png +0 -0
- package/registry/examples/list/native/infinite.js +30 -0
- package/registry/examples/list/native/infinite.light.png +0 -0
- package/registry/examples/list/native/infinite.tsx +35 -0
- package/registry/examples/list/native/states.d.ts +4 -0
- package/registry/examples/list/native/states.dark.png +0 -0
- package/registry/examples/list/native/states.js +26 -0
- package/registry/examples/list/native/states.light.png +0 -0
- package/registry/examples/list/native/states.tsx +21 -0
- package/registry/examples/virtual/feed.d.ts +4 -0
- package/registry/examples/virtual/feed.js +26 -0
- package/registry/examples/virtual/feed.tsx +24 -0
- package/registry/examples/virtual/gallery.d.ts +4 -0
- package/registry/examples/virtual/gallery.js +31 -0
- package/registry/examples/virtual/gallery.tsx +28 -0
- package/registry/examples/virtual/messages.d.ts +4 -0
- package/registry/examples/virtual/messages.js +28 -0
- package/registry/examples/virtual/messages.tsx +28 -0
- package/registry/field.json +104 -4
- package/registry/file-upload.json +1 -1
- package/registry/filter-bar.json +1 -1
- package/registry/form.json +2940 -0
- package/registry/gallery-page.json +1 -1
- package/registry/glow-card.json +1 -1
- package/registry/goal-gauge.json +1 -1
- package/registry/how-it-works-page.json +1 -1
- package/registry/image-cropper.json +1 -1
- package/registry/index.json +86 -11
- package/registry/iso.json +1 -1
- package/registry/kbd.json +1 -1
- package/registry/landing-page.json +1 -1
- package/registry/landscape.json +1 -1
- package/registry/legal-page.json +1 -1
- package/registry/link-card.json +1 -1
- package/registry/list.json +249 -0
- package/registry/loader.json +1 -1
- package/registry/longform-page.json +1 -1
- package/registry/marketing-layout.json +1 -1
- package/registry/marquee.json +1 -1
- package/registry/mascot-quote.json +1 -1
- package/registry/mascot-state.json +2 -2
- package/registry/mascots.json +2 -2
- package/registry/media-capture.json +1 -1
- package/registry/meter.json +1 -1
- package/registry/nav-bar.json +1 -1
- package/registry/number-ticker.json +1 -1
- package/registry/page-header.json +1 -1
- package/registry/page-hero.json +1 -1
- package/registry/page.json +1 -1
- package/registry/pagination.json +1 -1
- package/registry/pointer-light.json +1 -1
- package/registry/popover.json +1 -1
- package/registry/pricing-page.json +1 -1
- package/registry/product-page.json +1 -1
- package/registry/profile-card.json +1 -1
- package/registry/progress.json +1 -1
- package/registry/promise-card.json +1 -1
- package/registry/radio-group.json +1 -1
- package/registry/scroll-stage.json +1 -1
- package/registry/search-field.json +1 -1
- package/registry/section-backdrop.json +1 -1
- package/registry/separator.json +1 -1
- package/registry/settings-layout.json +1 -1
- package/registry/skeleton.json +1 -1
- package/registry/slider.json +1 -1
- package/registry/slot.json +1 -1
- package/registry/spinner.json +1 -1
- package/registry/split-view.json +1 -1
- package/registry/stage-banner.json +1 -1
- package/registry/stat.json +1 -1
- package/registry/status-page.json +1 -1
- package/registry/stepper.json +1 -1
- package/registry/switch.json +1 -1
- package/registry/tabs.json +1 -1
- package/registry/tag-input.json +1 -1
- package/registry/terminal-replay.json +1 -1
- package/registry/theme.json +1 -1
- package/registry/toast.json +1 -1
- package/registry/toc.json +1 -1
- package/registry/tooltip.json +1 -1
- package/registry/video-player.json +1 -1
- package/registry/video-trimmer.json +1 -1
- package/registry/virtual.json +569 -0
- package/skills/data-table/SKILL.md +56 -0
- package/skills/form/SKILL.md +44 -0
- package/skills/native-list/SKILL.md +39 -0
- package/skills/virtual/SKILL.md +35 -0
- /package/dist/{chunk-EUPTXKH7.js.map → chunk-BRKMQ6JV.js.map} +0 -0
- /package/dist/{chunk-X6TW3X6Y.js.map → chunk-LCZXS6AK.js.map} +0 -0
package/dist/filter-bar.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/filter-bar/filter-bar.tsx","../src/dropdown-menu/dropdown-menu.tsx"],"sourcesContent":["// Client (menus in a portal). The bar above a list: the controls that narrow it (a SearchField, FilterMenus, a\n// DateRangePicker — any child), the filters in force as removable chips, \"Clear all\", and the count of results in a\n// polite live region. It holds no state: the app keeps the filters — in the address, ideally — and the\n// `readFilterParams` / `writeFilterParams` helpers move them between a `URLSearchParams` and an object.\nimport { DropdownMenu as MenuPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { Chip } from \"../chip\";\nimport { DropdownMenu, menu } from \"../dropdown-menu\";\nimport { ChevronGlyph } from \"../glyphs\";\n\nexport const filterBar = tv({\n slots: {\n root: \"flex w-full flex-col gap-3\",\n controls: \"flex w-full flex-wrap items-center gap-2 [&>*]:min-w-0\",\n active: \"flex flex-wrap items-center gap-2\",\n clear:\n \"rounded-md px-2 py-1 text-xs font-semibold text-fg-accent hover:bg-surface-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n results: \"ml-auto text-xs text-fg-secondary tabular-nums\",\n trigger:\n \"inline-flex h-11 shrink-0 items-center gap-2 rounded-lg border border-border-default bg-surface-1 px-3.5 text-sm font-semibold text-fg-secondary \" +\n \"transition-colors hover:border-border-strong hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring \" +\n \"data-[active]:border-accent data-[active]:bg-accent-soft data-[active]:text-fg data-[state=open]:border-border-strong\",\n count: \"inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-accent px-1.5 text-[11px] font-bold tabular-nums text-on-accent\",\n optionCount: \"ml-auto pl-4 text-xs tabular-nums text-fg-secondary\",\n },\n});\n\nexport type ActiveFilter = {\n /** A stable key (`\"status:failed\"`). */\n id: string;\n /** What the chip says, passed translated (\"Status: Failed\"). */\n label: string;\n /** Removes this filter. */\n onRemove: () => void;\n};\n\nexport type FilterBarProps = Omit<React.ComponentProps<\"div\">, \"children\" | \"results\"> & {\n /** The accessible name of the bar (\"Filter jobs\"), passed translated. */\n label: string;\n /** The controls: `SearchField`, `FilterMenu`, `DateRangePicker`, a `Chip.Group`… */\n children?: React.ReactNode;\n /** The filters in force, each a removable chip. */\n active?: readonly ActiveFilter[];\n /** Clears every filter; the \"Clear all\" button shows when there is at least one. */\n onClearAll?: () => void;\n /** The \"Clear all\" button (\"Clear filters\"), passed translated. */\n clearLabel?: string;\n /** A chip's remove button: `(label) => \"Remove \" + label`, passed translated. */\n removeLabel?: (label: string) => string;\n /** The count of results (\"42 jobs\"), announced politely when it changes. */\n results?: string;\n};\n\n/** The bar of filters above a list: controls, active filters as chips, clear all, the count of results. */\nexport function FilterBar({\n label,\n children,\n active = [],\n onClearAll,\n clearLabel = \"Clear filters\",\n removeLabel = (text) => `Remove ${text}`,\n results,\n className,\n ...props\n}: FilterBarProps) {\n const s = filterBar();\n return (\n <div role=\"group\" aria-label={label} className={s.root({ className })} {...props}>\n {children ? <div className={s.controls()}>{children}</div> : null}\n {active.length > 0 || results !== undefined ? (\n <div className={s.active()}>\n {active.map((filter) => (\n <Chip key={filter.id} size=\"sm\" removable removeLabel={removeLabel(filter.label)} onRemove={filter.onRemove}>\n {filter.label}\n </Chip>\n ))}\n {active.length > 0 && onClearAll ? (\n <button type=\"button\" className={s.clear()} onClick={onClearAll}>\n {clearLabel}\n </button>\n ) : null}\n <p className={s.results()} aria-live=\"polite\" aria-atomic>\n {results}\n </p>\n </div>\n ) : null}\n </div>\n );\n}\n\nexport type FilterOption = {\n /** What the app stores (and puts in the address). */\n value: string;\n /** What the person reads, passed translated. */\n label: string;\n /** How many results it would give: shown on the right. */\n count?: number;\n /** Shown but not choosable. */\n disabled?: boolean;\n};\n\ntype FilterMenuBase = {\n /** The name of the filter on its button (\"Status\"), passed translated. */\n label: string;\n /** The choices. */\n options: readonly FilterOption[];\n /** Opens the menu at first (a story, a test). */\n defaultOpen?: boolean;\n /** Classes merged on the button. */\n className?: string;\n};\n\nexport type FilterMenuProps =\n | (FilterMenuBase & {\n /** Several values (checkboxes, default) or one (radios). */\n multiple?: true;\n /** The values chosen. */\n value: readonly string[];\n /** Called with the values chosen. */\n onValueChange: (value: string[]) => void;\n })\n | (FilterMenuBase & {\n /** One value: the options are radios. */\n multiple: false;\n /** The value chosen, `\"\"` for none. */\n value: string;\n /** Called with the value chosen. */\n onValueChange: (value: string) => void;\n });\n\n/** A filter's button and its menu of options — checkboxes (several values) or radios (one) — with a count of chosen ones. */\nexport function FilterMenu(props: FilterMenuProps) {\n const { label, options, defaultOpen, className } = props;\n const s = filterBar();\n const m = menu();\n const chosen = props.multiple === false ? (props.value ? [props.value] : []) : props.value;\n const single = props.multiple === false ? options.find((o) => o.value === props.value) : undefined;\n const content = (option: FilterOption) => (\n <>\n {option.label}\n {option.count !== undefined ? <span className={s.optionCount()}>{option.count}</span> : null}\n </>\n );\n return (\n <DropdownMenu.Root defaultOpen={defaultOpen} modal={false}>\n <DropdownMenu.Trigger className={s.trigger({ className })} data-active={chosen.length > 0 ? \"\" : undefined}>\n {single ? `${label}: ${single.label}` : label}\n {props.multiple !== false && chosen.length > 0 ? <span className={s.count()}>{chosen.length}</span> : null}\n <ChevronGlyph className=\"h-4 w-4\" />\n </DropdownMenu.Trigger>\n <DropdownMenu.Content align=\"start\" aria-label={label}>\n {props.multiple === false ? (\n <MenuPrimitive.RadioGroup value={props.value} onValueChange={props.onValueChange}>\n {options.map((option) => (\n <MenuPrimitive.RadioItem key={option.value} value={option.value} disabled={option.disabled} className={m.item()}>\n <span className=\"flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border-strong\">\n <MenuPrimitive.ItemIndicator className=\"h-2 w-2 rounded-full bg-accent\" />\n </span>\n {content(option)}\n </MenuPrimitive.RadioItem>\n ))}\n </MenuPrimitive.RadioGroup>\n ) : (\n options.map((option) => (\n <DropdownMenu.CheckboxItem\n key={option.value}\n disabled={option.disabled}\n checked={props.value.includes(option.value)}\n onSelect={(event) => event.preventDefault()}\n onCheckedChange={(checked) =>\n props.onValueChange(checked ? [...props.value, option.value] : props.value.filter((v) => v !== option.value))\n }\n >\n {content(option)}\n </DropdownMenu.CheckboxItem>\n ))\n )}\n </DropdownMenu.Content>\n </DropdownMenu.Root>\n );\n}\n\n/** Filters read from an address: `{ status: [\"failed\", \"running\"], q: [\"cat\"] }` for the keys asked (repeated or comma-separated values). */\nexport function readFilterParams(params: URLSearchParams, keys: readonly string[]): Record<string, string[]> {\n return Object.fromEntries(\n keys.map((key) => [key, params.getAll(key).flatMap((v) => v.split(\",\")).map((v) => v.trim()).filter(Boolean)]),\n );\n}\n\n/** Filters written into an address (a copy of `base`): empty filters removed, several values comma-separated, the page reset. */\nexport function writeFilterParams(base: URLSearchParams, filters: Record<string, string | readonly string[] | null | undefined>, resetKey = \"page\") {\n const next = new URLSearchParams(base);\n for (const [key, value] of Object.entries(filters)) {\n const list = value == null ? [] : typeof value === \"string\" ? [value] : [...value];\n if (list.filter(Boolean).length === 0) next.delete(key);\n else next.set(key, list.filter(Boolean).join(\",\"));\n }\n if (resetKey) next.delete(resetKey);\n return next;\n}\n","// Client (a portal). Radix does the roving focus, the arrows, Home/End, typeahead, Escape and the focus return.\nimport { DropdownMenu as MenuPrimitive } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\nimport { floating } from \"../floating\";\n\nexport const menu = tv({\n slots: {\n content: cn(floating, \"min-w-48 max-h-(--radix-dropdown-menu-content-available-height) overflow-y-auto p-1 origin-(--radix-dropdown-menu-content-transform-origin)\"),\n item:\n \"relative flex cursor-pointer select-none items-center gap-2 rounded-md px-2.5 py-2 text-sm text-fg outline-hidden transition-colors \" +\n \"data-highlighted:bg-surface-3 data-disabled:pointer-events-none data-disabled:opacity-40 [&>svg]:h-4 [&>svg]:w-4 [&>svg]:shrink-0 \" +\n \"[&>svg]:text-fg-secondary\",\n indicator: \"ml-auto flex h-4 w-4 items-center justify-center text-fg-accent\",\n label: \"px-2.5 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n separator: \"-mx-1 my-1 h-px bg-border-subtle\",\n },\n variants: {\n // The label reads in the danger text role (≥ 4.5:1 on the menu surface, both themes); the icon and the highlight\n // carry the danger fill.\n tone: { default: {}, danger: { item: \"text-fg-danger data-highlighted:bg-danger/15 [&>svg]:text-danger\" } },\n },\n defaultVariants: { tone: \"default\" },\n});\nconst s = menu();\n\nexport type DropdownMenuContentProps = React.ComponentProps<typeof MenuPrimitive.Content> & {\n /** The element the portal renders into (default: document.body). */\n container?: HTMLElement | null;\n};\n\nexport function DropdownMenuContent({ sideOffset = 6, align = \"end\", collisionPadding = 8, container, className, ...props }: DropdownMenuContentProps) {\n return (\n <MenuPrimitive.Portal container={container}>\n <MenuPrimitive.Content sideOffset={sideOffset} align={align} collisionPadding={collisionPadding} className={s.content({ className })} {...props} />\n </MenuPrimitive.Portal>\n );\n}\n\nexport type DropdownMenuItemProps = React.ComponentProps<typeof MenuPrimitive.Item> & {\n /** `danger` for a destructive action: the icon and the highlight carry the danger, the label stays legible. */\n tone?: \"default\" | \"danger\";\n};\n\n/** An action. `tone=\"danger\"` for a destructive one; `onSelect` runs it (the menu closes). */\nexport function DropdownMenuItem({ tone, className, ...props }: DropdownMenuItemProps) {\n return <MenuPrimitive.Item data-tone={tone ?? \"default\"} className={menu({ tone }).item({ className })} {...props} />;\n}\n\n/** A toggled option: `checked` + `onCheckedChange`. */\nexport function DropdownMenuCheckboxItem({ className, children, ...props }: React.ComponentProps<typeof MenuPrimitive.CheckboxItem>) {\n return (\n <MenuPrimitive.CheckboxItem className={s.item({ className })} {...props}>\n {children}\n <MenuPrimitive.ItemIndicator className={s.indicator()}>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M20 6 9 17l-5-5\" />\n </svg>\n </MenuPrimitive.ItemIndicator>\n </MenuPrimitive.CheckboxItem>\n );\n}\n\nexport function DropdownMenuLabel({ className, ...props }: React.ComponentProps<typeof MenuPrimitive.Label>) {\n return <MenuPrimitive.Label className={s.label({ className })} {...props} />;\n}\n\nexport function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof MenuPrimitive.Separator>) {\n return <MenuPrimitive.Separator className={s.separator({ className })} {...props} />;\n}\n\n/** `DropdownMenu.Root/Trigger/Content/Item/CheckboxItem/Label/Separator/Group`. `open`/`defaultOpen` + `onOpenChange`. */\nexport const DropdownMenu = {\n Root: MenuPrimitive.Root,\n Trigger: MenuPrimitive.Trigger,\n Content: DropdownMenuContent,\n Item: DropdownMenuItem,\n CheckboxItem: DropdownMenuCheckboxItem,\n Label: DropdownMenuLabel,\n Separator: DropdownMenuSeparator,\n Group: MenuPrimitive.Group,\n};\n"],"mappings":"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","names":["MenuPrimitive","tv","jsx","jsxs","tv","s","MenuPrimitive"]}
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** A value a message placeholder takes (`{minimum}`, `{suggestion}`). */
|
|
2
|
+
type FormMessageValue = string | number;
|
|
3
|
+
/** An error of a field or of the whole form: the key of its words in the app's catalogue, and what fills them in. */
|
|
4
|
+
interface FormFieldError {
|
|
5
|
+
/** The i18n key: `form.errors.required`, or the key a schema names (`z.string().min(3, "signUp.username.short")`). */
|
|
6
|
+
key: string;
|
|
7
|
+
/** The placeholders of the message (`{ minimum: 3 }`). */
|
|
8
|
+
values?: Readonly<Record<string, FormMessageValue>>;
|
|
9
|
+
/** The server's own words, in English: shown only when the catalogue has no message for the key. */
|
|
10
|
+
message?: string;
|
|
11
|
+
/** A value to offer instead (a free username): a button under the error sets it. Its label is passed translated. */
|
|
12
|
+
suggestion?: {
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly value: unknown;
|
|
15
|
+
};
|
|
16
|
+
/** Shown at once, while the person types (an availability check), not only once they leave the field. */
|
|
17
|
+
immediate?: boolean;
|
|
18
|
+
/** Where it comes from: the schema (default), a field's check, or the server's answer (kept until the field changes). */
|
|
19
|
+
source?: "schema" | "check" | "server";
|
|
20
|
+
}
|
|
21
|
+
/** One issue of a Standard Schema (zod 4, valibot, arktype): zod adds its `code` and limits, read when present. */
|
|
22
|
+
interface SchemaIssue {
|
|
23
|
+
readonly message: string;
|
|
24
|
+
readonly path?: ReadonlyArray<PropertyKey | {
|
|
25
|
+
readonly key: PropertyKey;
|
|
26
|
+
}>;
|
|
27
|
+
readonly code?: string;
|
|
28
|
+
readonly minimum?: number | bigint;
|
|
29
|
+
readonly maximum?: number | bigint;
|
|
30
|
+
readonly origin?: string;
|
|
31
|
+
readonly format?: string;
|
|
32
|
+
readonly input?: unknown;
|
|
33
|
+
readonly expected?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A refusal the form shows, serialisable (a Server Action returns it): the shape of `KrizakaApiError` of @krizaka/query —
|
|
37
|
+
* a `KrizakaApiError` *is* a `FormProblem`. `fields` go under their fields, the rest above the submit button.
|
|
38
|
+
*/
|
|
39
|
+
interface FormProblem {
|
|
40
|
+
/** The HTTP status (0 when no response arrived). */
|
|
41
|
+
status?: number;
|
|
42
|
+
/** The stable code of the refusal (`rate-limited`, `conflict`, `ACCOUNT_EXISTS`): the key of its words. */
|
|
43
|
+
code?: string;
|
|
44
|
+
/** The server's sentence, in English: shown only when the catalogue has no message for the code. */
|
|
45
|
+
detail?: string;
|
|
46
|
+
/** The refused fields, by name: a server's `{ code, message }`, or an error the form already knows (`key`). */
|
|
47
|
+
fields?: Readonly<Record<string, {
|
|
48
|
+
code?: string;
|
|
49
|
+
message: string;
|
|
50
|
+
} | FormFieldError | string>>;
|
|
51
|
+
/** Epoch ms before which the same call is refused (a 429). */
|
|
52
|
+
retryAt?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @krizaka/ui/form/server — the server side of a Krizaka form, for a Server Action or a route handler (no "use client",
|
|
57
|
+
* no React): `validateForm` parses with the form's own schema and answers the errors with the same keys the form shows,
|
|
58
|
+
* `formProblem` turns a thrown error (a `KrizakaApiError` of @krizaka/query) into the serialisable refusal a form reads.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/** What a Server Action answers a form: done (with what it made), or a refusal (`fields` under their fields). */
|
|
62
|
+
type FormActionResult<T = unknown> = {
|
|
63
|
+
ok: true;
|
|
64
|
+
data?: T;
|
|
65
|
+
} | {
|
|
66
|
+
ok: false;
|
|
67
|
+
problem: FormProblem;
|
|
68
|
+
};
|
|
69
|
+
type Schema<O> = {
|
|
70
|
+
readonly "~standard": {
|
|
71
|
+
validate: (value: unknown) => {
|
|
72
|
+
value?: O;
|
|
73
|
+
issues?: readonly SchemaIssue[];
|
|
74
|
+
} | Promise<{
|
|
75
|
+
value?: O;
|
|
76
|
+
issues?: readonly SchemaIssue[];
|
|
77
|
+
}>;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Parses what a form sent with its schema (never trust the client): the value, or a `validation-failed` problem whose
|
|
82
|
+
* fields carry the keys the form would show (`form.errors.tooShort`, the schema's own keys).
|
|
83
|
+
*/
|
|
84
|
+
declare function validateForm<O>(schema: Schema<O>, input: unknown, options?: {
|
|
85
|
+
prefix?: string;
|
|
86
|
+
}): Promise<{
|
|
87
|
+
ok: true;
|
|
88
|
+
value: O;
|
|
89
|
+
} | {
|
|
90
|
+
ok: false;
|
|
91
|
+
problem: FormProblem;
|
|
92
|
+
}>;
|
|
93
|
+
/** A thrown error as a refusal a form reads — a `KrizakaApiError` keeps its status, code, fields and retry time. */
|
|
94
|
+
declare function formProblem(error: unknown): FormProblem;
|
|
95
|
+
|
|
96
|
+
export { type FormActionResult, type FormFieldError, type FormProblem, formProblem, validateForm };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// src/form/errors.ts
|
|
2
|
+
var KEY = /^[\w-]+(\.[\w-]+)+$/;
|
|
3
|
+
function pathName(path) {
|
|
4
|
+
let name = "";
|
|
5
|
+
for (const part of path ?? []) {
|
|
6
|
+
const key = typeof part === "object" && part !== null ? part.key : part;
|
|
7
|
+
if (typeof key === "number") name += `[${key}]`;
|
|
8
|
+
else name += name ? `.${String(key)}` : String(key);
|
|
9
|
+
}
|
|
10
|
+
return name;
|
|
11
|
+
}
|
|
12
|
+
var num = (value) => value === void 0 ? void 0 : Number(value);
|
|
13
|
+
function issueError(issue, prefix = "form.errors") {
|
|
14
|
+
if (KEY.test(issue.message)) return { key: issue.message };
|
|
15
|
+
const key = (code, values) => ({ key: `${prefix}.${code}`, ...values ? { values } : {} });
|
|
16
|
+
const collection = issue.origin === "array" || issue.origin === "set";
|
|
17
|
+
switch (issue.code) {
|
|
18
|
+
case "invalid_type":
|
|
19
|
+
return issue.input === void 0 || issue.input === null || issue.input === "" ? key("required") : issue.expected === "number" ? key("invalidNumber") : issue.expected === "date" ? key("invalidDate") : key("invalid");
|
|
20
|
+
case "too_small": {
|
|
21
|
+
const minimum = num(issue.minimum);
|
|
22
|
+
if (issue.origin === "string") return minimum <= 1 ? key("required") : key("tooShort", { minimum });
|
|
23
|
+
if (collection) return minimum <= 1 ? key("required") : key("tooFew", { minimum });
|
|
24
|
+
return key("tooSmall", { minimum });
|
|
25
|
+
}
|
|
26
|
+
case "too_big": {
|
|
27
|
+
const maximum = num(issue.maximum);
|
|
28
|
+
return issue.origin === "string" ? key("tooLong", { maximum }) : collection ? key("tooMany", { maximum }) : key("tooBig", { maximum });
|
|
29
|
+
}
|
|
30
|
+
case "invalid_format":
|
|
31
|
+
if (issue.format === "email") return key("invalidEmail");
|
|
32
|
+
if (issue.format === "url") return key("invalidUrl");
|
|
33
|
+
if (issue.format === "date" || issue.format === "datetime") return key("invalidDate");
|
|
34
|
+
return key("invalidFormat");
|
|
35
|
+
case "invalid_value":
|
|
36
|
+
return issue.input === false || issue.input === void 0 ? key("required") : key("invalidChoice");
|
|
37
|
+
case "invalid_union":
|
|
38
|
+
case "invalid_key":
|
|
39
|
+
case "invalid_element":
|
|
40
|
+
return key("invalidChoice");
|
|
41
|
+
default:
|
|
42
|
+
return key("invalid");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function issuesToFields(issues, prefix) {
|
|
46
|
+
const fields = {};
|
|
47
|
+
for (const issue of issues) {
|
|
48
|
+
const name = pathName(issue.path);
|
|
49
|
+
if (!(name in fields)) fields[name] = { ...issueError(issue, prefix), source: "schema" };
|
|
50
|
+
}
|
|
51
|
+
return fields;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/form/server.ts
|
|
55
|
+
async function validateForm(schema, input, options = {}) {
|
|
56
|
+
const result = await schema["~standard"].validate(input);
|
|
57
|
+
if (!result.issues?.length) return { ok: true, value: result.value };
|
|
58
|
+
const fields = issuesToFields(result.issues, options.prefix);
|
|
59
|
+
const general = fields[""];
|
|
60
|
+
delete fields[""];
|
|
61
|
+
return { ok: false, problem: { status: 422, code: general ? general.key : "validation-failed", fields } };
|
|
62
|
+
}
|
|
63
|
+
function formProblem(error) {
|
|
64
|
+
if (error && typeof error === "object") {
|
|
65
|
+
const e = error;
|
|
66
|
+
if (typeof e.status === "number" || typeof e.code === "string") {
|
|
67
|
+
return {
|
|
68
|
+
...typeof e.status === "number" ? { status: e.status } : {},
|
|
69
|
+
...typeof e.code === "string" ? { code: e.code } : {},
|
|
70
|
+
...typeof e.detail === "string" ? { detail: e.detail } : {},
|
|
71
|
+
...e.fields && typeof e.fields === "object" ? { fields: JSON.parse(JSON.stringify(e.fields)) } : {},
|
|
72
|
+
...typeof e.retryAt === "number" ? { retryAt: e.retryAt } : {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { status: 500, code: "server" };
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
formProblem,
|
|
80
|
+
validateForm
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/form/errors.ts","../../src/form/server.ts"],"sourcesContent":["// Platform-neutral and server-safe: no React, no DOM, no React Native. The errors of a Krizaka form — a schema's issues,\n// a server's Problem Details, an availability check — as one shape, `FormFieldError`, whose words come from a key of the\n// app's catalogue (`form.errors.tooShort`), never from the validation engine's English.\n\n/** A value a message placeholder takes (`{minimum}`, `{suggestion}`). */\nexport type FormMessageValue = string | number;\n\n/** An error of a field or of the whole form: the key of its words in the app's catalogue, and what fills them in. */\nexport interface FormFieldError {\n /** The i18n key: `form.errors.required`, or the key a schema names (`z.string().min(3, \"signUp.username.short\")`). */\n key: string;\n /** The placeholders of the message (`{ minimum: 3 }`). */\n values?: Readonly<Record<string, FormMessageValue>>;\n /** The server's own words, in English: shown only when the catalogue has no message for the key. */\n message?: string;\n /** A value to offer instead (a free username): a button under the error sets it. Its label is passed translated. */\n suggestion?: { readonly label: string; readonly value: unknown };\n /** Shown at once, while the person types (an availability check), not only once they leave the field. */\n immediate?: boolean;\n /** Where it comes from: the schema (default), a field's check, or the server's answer (kept until the field changes). */\n source?: \"schema\" | \"check\" | \"server\";\n}\n\n/** The English defaults of the generic errors, by code — the words a form shows when the app's catalogue has none. */\nexport const formMessages: Readonly<Record<string, string>> = {\n required: \"This field is required.\",\n tooShort: \"Use at least {minimum} characters.\",\n tooLong: \"Use at most {maximum} characters.\",\n tooSmall: \"Enter {minimum} or more.\",\n tooBig: \"Enter {maximum} or less.\",\n tooFew: \"Choose at least {minimum}.\",\n tooMany: \"Choose at most {maximum}.\",\n invalidEmail: \"Enter an e-mail address, like name@example.com.\",\n invalidUrl: \"Enter a web address, like https://example.com.\",\n invalidDate: \"Enter a real date.\",\n invalidNumber: \"Enter a number.\",\n invalidFormat: \"Check the format of this field.\",\n invalidChoice: \"Choose one of the options.\",\n invalid: \"Check this field.\",\n // The whole form (a server's answer without fields): the codes of @krizaka/query's KrizakaApiError.\n network: \"No connection. Check your network and try again.\",\n \"rate-limited\": \"Too many tries. Wait a moment and try again.\",\n unauthorized: \"Your session has ended. Sign in again.\",\n forbidden: \"You are not allowed to do this.\",\n conflict: \"This was changed meanwhile. Reload and try again.\",\n \"validation-failed\": \"Some fields need your attention.\",\n unavailable: \"The service is unavailable for a moment. Try again shortly.\",\n server: \"Something went wrong on our side. Try again.\",\n};\n\n/** How a form finds its words: the app's translate function, and the prefixes of its keys. */\nexport interface FormI18n {\n /** The app's translate function (`(key, values) => t(key, values)`): a string, or `undefined` when the key is missing. */\n t?: (key: string, values?: Readonly<Record<string, FormMessageValue>>) => string | undefined;\n /** The prefix of the generic field errors. Default `form.errors` (→ `form.errors.tooShort`). */\n prefix?: string;\n /** The prefix of the server's codes for the whole form, as `KrizakaApiError.messageKey()`. Default `apiErrors`. */\n apiPrefix?: string;\n /** The locale of numbers, money and dates. Default `en`. */\n locale?: string;\n}\n\nconst KEY = /^[\\w-]+(\\.[\\w-]+)+$/;\n\nconst fill = (text: string, values?: Readonly<Record<string, FormMessageValue>>) =>\n values ? text.replace(/\\{(\\w+)\\}/g, (all, name: string) => (values[name] === undefined ? all : String(values[name]))) : text;\n\n/** The words of an error: the app's catalogue, else the English default of its code, else the server's words, else the key. */\nexport function formMessage(error: FormFieldError | string, i18n: FormI18n = {}): string {\n const e = typeof error === \"string\" ? { key: error } : error;\n const translated = i18n.t?.(e.key, e.values);\n if (translated && translated !== e.key) return translated;\n const code = e.key.slice(e.key.lastIndexOf(\".\") + 1);\n const fallback = formMessages[code];\n // The server's words beat a generic default (a refusal without a code), never a default for its own code.\n if (e.message && (code === \"invalid\" || !fallback)) return e.message;\n if (fallback && (e.key.startsWith(`${i18n.prefix ?? \"form.errors\"}.`) || e.key.startsWith(`${i18n.apiPrefix ?? \"apiErrors\"}.`))) return fill(fallback, e.values);\n return e.message ?? (KEY.test(e.key) ? fill(fallback ?? formMessages.invalid!, e.values) : e.key);\n}\n\n/** One issue of a Standard Schema (zod 4, valibot, arktype): zod adds its `code` and limits, read when present. */\nexport interface SchemaIssue {\n readonly message: string;\n readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>;\n readonly code?: string;\n readonly minimum?: number | bigint;\n readonly maximum?: number | bigint;\n readonly origin?: string;\n readonly format?: string;\n readonly input?: unknown;\n readonly expected?: string;\n}\n\n/** A field's name from an issue's path, as the form writes it: `links[0].url`. */\nexport function pathName(path: SchemaIssue[\"path\"]): string {\n let name = \"\";\n for (const part of path ?? []) {\n const key = typeof part === \"object\" && part !== null ? part.key : part;\n if (typeof key === \"number\") name += `[${key}]`;\n else name += name ? `.${String(key)}` : String(key);\n }\n return name;\n}\n\nconst num = (value: number | bigint | undefined) => (value === undefined ? undefined : Number(value));\n\n/** The key and values of an issue: the schema's own key when it names one, else the generic code of what failed. */\nexport function issueError(issue: SchemaIssue, prefix = \"form.errors\"): FormFieldError {\n if (KEY.test(issue.message)) return { key: issue.message };\n const key = (code: string, values?: Record<string, FormMessageValue>) => ({ key: `${prefix}.${code}`, ...(values ? { values } : {}) });\n const collection = issue.origin === \"array\" || issue.origin === \"set\";\n switch (issue.code) {\n case \"invalid_type\":\n return issue.input === undefined || issue.input === null || issue.input === \"\" ? key(\"required\") : issue.expected === \"number\" ? key(\"invalidNumber\") : issue.expected === \"date\" ? key(\"invalidDate\") : key(\"invalid\");\n case \"too_small\": {\n const minimum = num(issue.minimum)!;\n if (issue.origin === \"string\") return minimum <= 1 ? key(\"required\") : key(\"tooShort\", { minimum });\n if (collection) return minimum <= 1 ? key(\"required\") : key(\"tooFew\", { minimum });\n return key(\"tooSmall\", { minimum });\n }\n case \"too_big\": {\n const maximum = num(issue.maximum)!;\n return issue.origin === \"string\" ? key(\"tooLong\", { maximum }) : collection ? key(\"tooMany\", { maximum }) : key(\"tooBig\", { maximum });\n }\n case \"invalid_format\":\n if (issue.format === \"email\") return key(\"invalidEmail\");\n if (issue.format === \"url\") return key(\"invalidUrl\");\n if (issue.format === \"date\" || issue.format === \"datetime\") return key(\"invalidDate\");\n return key(\"invalidFormat\");\n case \"invalid_value\":\n // `z.literal(true)`: a box that must be ticked.\n return issue.input === false || issue.input === undefined ? key(\"required\") : key(\"invalidChoice\");\n case \"invalid_union\":\n case \"invalid_key\":\n case \"invalid_element\":\n return key(\"invalidChoice\");\n default:\n return key(\"invalid\");\n }\n}\n\n/** The first error of each field of a schema's issues (an issue without a path is the whole form's: `\"\"`). */\nexport function issuesToFields(issues: readonly SchemaIssue[], prefix?: string): Record<string, FormFieldError> {\n const fields: Record<string, FormFieldError> = {};\n for (const issue of issues) {\n const name = pathName(issue.path);\n if (!(name in fields)) fields[name] = { ...issueError(issue, prefix), source: \"schema\" };\n }\n return fields;\n}\n\n/**\n * A refusal the form shows, serialisable (a Server Action returns it): the shape of `KrizakaApiError` of @krizaka/query —\n * a `KrizakaApiError` *is* a `FormProblem`. `fields` go under their fields, the rest above the submit button.\n */\nexport interface FormProblem {\n /** The HTTP status (0 when no response arrived). */\n status?: number;\n /** The stable code of the refusal (`rate-limited`, `conflict`, `ACCOUNT_EXISTS`): the key of its words. */\n code?: string;\n /** The server's sentence, in English: shown only when the catalogue has no message for the code. */\n detail?: string;\n /** The refused fields, by name: a server's `{ code, message }`, or an error the form already knows (`key`). */\n fields?: Readonly<Record<string, { code?: string; message: string } | FormFieldError | string>>;\n /** Epoch ms before which the same call is refused (a 429). */\n retryAt?: number;\n}\n\n/** True for what a form can read as a problem: a `KrizakaApiError`, a `FormProblem`, an `{ ok: false, problem }` result. */\nexport function isFormProblem(value: unknown): value is FormProblem {\n return typeof value === \"object\" && value !== null && (\"fields\" in value || \"code\" in value || \"status\" in value);\n}\n\n/** A problem as field errors (by name) and the whole form's error (or `null` when every refusal has its field). */\nexport function problemToErrors(\n problem: FormProblem,\n i18n: FormI18n = {},\n known?: (name: string) => boolean,\n codeFields: Readonly<Record<string, string>> = {},\n): { fields: Record<string, FormFieldError>; form: FormFieldError | null } {\n const prefix = i18n.prefix ?? \"form.errors\";\n const fields: Record<string, FormFieldError> = {};\n let unplaced = false;\n // A refusal named by a code that belongs to a field (`UNDERAGE` → `dateOfBirth`): under that field, in its words.\n const owner = problem.code ? codeFields[problem.code] : undefined;\n if (owner && (!known || known(owner)) && !problem.fields?.[owner]) {\n fields[owner] = { key: `${prefix}.${problem.code}`, ...(problem.detail ? { message: problem.detail } : {}), source: \"server\" };\n if (!problem.fields || Object.keys(problem.fields).length === 0) return { fields, form: null };\n }\n for (const [name, refusal] of Object.entries(problem.fields ?? {})) {\n if (known && !known(name)) {\n unplaced = true;\n continue;\n }\n const error: FormFieldError =\n typeof refusal === \"string\"\n ? { key: refusal }\n : \"key\" in refusal\n ? refusal\n : { key: refusal.code ? (KEY.test(refusal.code) ? refusal.code : `${prefix}.${refusal.code}`) : `${prefix}.invalid`, message: refusal.message };\n fields[name] = { ...error, source: \"server\" };\n }\n const placed = Object.keys(fields).length > 0;\n if (placed && !unplaced) return { fields, form: null };\n const code = problem.code ?? (problem.status === 0 ? \"network\" : problem.status && problem.status >= 500 ? \"server\" : \"invalid\");\n const retryIn = problem.retryAt ? Math.max(1, Math.ceil((problem.retryAt - Date.now()) / 1000)) : undefined;\n return {\n fields,\n form: { key: KEY.test(code) ? code : `${i18n.apiPrefix ?? \"apiErrors\"}.${code}`, ...(problem.detail ? { message: problem.detail } : {}), ...(retryIn ? { values: { seconds: retryIn } } : {}), source: \"server\" },\n };\n}\n","/**\n * @krizaka/ui/form/server — the server side of a Krizaka form, for a Server Action or a route handler (no \"use client\",\n * no React): `validateForm` parses with the form's own schema and answers the errors with the same keys the form shows,\n * `formProblem` turns a thrown error (a `KrizakaApiError` of @krizaka/query) into the serialisable refusal a form reads.\n */\nimport { type FormFieldError, type FormProblem, issuesToFields, type SchemaIssue } from \"./errors\";\n\nexport type { FormFieldError, FormProblem } from \"./errors\";\n\n/** What a Server Action answers a form: done (with what it made), or a refusal (`fields` under their fields). */\nexport type FormActionResult<T = unknown> = { ok: true; data?: T } | { ok: false; problem: FormProblem };\n\ntype Schema<O> = { readonly \"~standard\": { validate: (value: unknown) => { value?: O; issues?: readonly SchemaIssue[] } | Promise<{ value?: O; issues?: readonly SchemaIssue[] }> } };\n\n/**\n * Parses what a form sent with its schema (never trust the client): the value, or a `validation-failed` problem whose\n * fields carry the keys the form would show (`form.errors.tooShort`, the schema's own keys).\n */\nexport async function validateForm<O>(schema: Schema<O>, input: unknown, options: { prefix?: string } = {}): Promise<{ ok: true; value: O } | { ok: false; problem: FormProblem }> {\n const result = await schema[\"~standard\"].validate(input);\n if (!result.issues?.length) return { ok: true, value: result.value as O };\n const fields: Record<string, FormFieldError> = issuesToFields(result.issues, options.prefix);\n const general = fields[\"\"];\n delete fields[\"\"];\n return { ok: false, problem: { status: 422, code: general ? general.key : \"validation-failed\", fields } };\n}\n\n/** A thrown error as a refusal a form reads — a `KrizakaApiError` keeps its status, code, fields and retry time. */\nexport function formProblem(error: unknown): FormProblem {\n if (error && typeof error === \"object\") {\n const e = error as { status?: unknown; code?: unknown; detail?: unknown; fields?: unknown; retryAt?: unknown };\n if (typeof e.status === \"number\" || typeof e.code === \"string\") {\n return {\n ...(typeof e.status === \"number\" ? { status: e.status } : {}),\n ...(typeof e.code === \"string\" ? { code: e.code } : {}),\n ...(typeof e.detail === \"string\" ? { detail: e.detail } : {}),\n ...(e.fields && typeof e.fields === \"object\" ? { fields: JSON.parse(JSON.stringify(e.fields)) as FormProblem[\"fields\"] } : {}),\n ...(typeof e.retryAt === \"number\" ? { retryAt: e.retryAt } : {}),\n };\n }\n }\n return { status: 500, code: \"server\" };\n}\n"],"mappings":"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","names":[]}
|