@salemcobalt/ui 0.2.0-rc.7 → 0.2.0-rc.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @salemcobalt/ui
2
2
 
3
- React 19 components for the Salem Cobalt design system: `Button`, `IconButton`, `Icon` and a shipped icon set, `Badge`, `Card`, `Stack`, `Inline`, `Heading`, `Text`, `Kbd`, `Dialog`, `Tooltip`, `Menu`, `Toolbar`, `Banner`, `Spinner`, `Progress`, `Tabs`, `Input`, `Textarea`, `Select`, `Checkbox`, `Switch`, `Table`, `Stat`, `Chip`, `Disclosure`, `EmptyState`, `Skeleton`. Requires `@salemcobalt/tokens`.
3
+ React 19 components for the Salem Cobalt design system: `Button`, `IconButton`, `Icon` and a shipped icon set, `Badge`, `Card`, `Stack`, `Inline`, `Heading`, `Text`, `Kbd`, `Dialog`, `Tooltip`, `Menu`, `Toolbar`, `Banner`, `Spinner`, `Progress`, `Tabs`, `Input`, `Textarea`, `Select`, `FileInput`, `Checkbox`, `Switch`, `Table`, `Stat`, `Chip`, `Disclosure`, `EmptyState`, `Skeleton`. Requires `@salemcobalt/tokens`.
4
4
 
5
5
  ```ts
6
6
  import '@salemcobalt/tokens/css'; // custom properties, light + dark
@@ -23,28 +23,29 @@ import {
23
23
 
24
24
  Styles ship as one stylesheet inside `@layer sc`, so an app's unlayered CSS always wins. Every value in it is a token custom property; theme with `data-theme="light|dark"` on `<html>` and density with `data-density="compact"` on any ancestor. Interactive behaviour (Dialog, Tooltip and Menu focus and keyboard handling, `render` composition) comes from Base UI; see `docs/adr/0001-headless-primitives.md`.
25
25
 
26
- | Component | Notes |
27
- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
- | `Button` | `variant` primary · secondary · ghost · danger; `size` sm · md · lg; `iconStart`/`iconEnd`; `loading` (keeps width, `aria-busy`); `fullWidth`. |
29
- | `IconButton` | Square Button for one icon. Required `label` (the accessible name); `variant` ghost (default) · secondary · primary · danger; `size`; `tooltip` (`true` shows `label`, or a string); `tooltipSide`; `round`; `loading`; `render={<a href />}` or a Next.js `Link` makes it a link. |
30
- | `Icon`, `*Icon` | 49 outline icons on a 16px grid in `currentColor`, one component each (`PlayIcon`, `ChevronDownIcon` …, listed in `iconNames`); `size` xs 12 · sm 14 · md 16 · lg 20 · xl 24 · em; decorative unless `title` is given. `Icon` wraps your own paths. |
31
- | `Badge` | `tone` neutral · accent · success · warning · danger · info; `variant` soft · solid · outline; `size` sm · md; `dot`. Text only. |
32
- | `Card` | `variant` outlined · elevated · filled; `padding` none · sm · md · lg; `interactive`; `selected`; `as` or `render` to make it an `a`/`button`/`li`. |
33
- | `Stack`, `Inline` | `gap` from the spacing scale (`none` … `4xl`); `align`; `justify`; `as`. `Inline` wraps unless `wrap={false}`. |
34
- | `Kbd` | One key per `Kbd`; `Kbd.Group label="Command K"` groups a shortcut with a `+` (or `separator="space"`). |
35
- | `Dialog` | `Root` (`open`, `defaultOpen`, `onOpenChange`, `dismissible`), `Trigger`, `Content` (`size` sm · md · lg, every size clamps to the viewport minus a 16px gutter; `variant="sheet"` becomes a bottom sheet below 640px with a scrolling `Body`), `Title`, `Description`, `Body`, `Actions`, `Close`. Menus, selects and tooltips opened inside render above it. |
36
- | `Toolbar` | `Root` (required `aria-label`, `wrap`, `gap`), `Group` (`aria-label`, `gap`), `Separator`, `Spacer`, `Button` (`render={<IconButton … />}` or `render={<Menu.Trigger render={…} />}` so the control joins the roving focus: Tab enters, arrows move, Tab leaves), `Link` (an anchor in the same navigation; `render={<IconButton label render={<a href />} />}` for an icon link). |
37
- | `Banner` | `tone` info (default) · success · warning · danger · neutral; `icon` (or `false`); `action` slot; `dismissible` + `onDismiss` (`open` to control); warning and danger are `role="alert"`, the rest `role="status"`. |
38
- | `Tabs` | `Root` (`value`, `defaultValue`, `onValueChange`), `List` (required `aria-label`, `fill`), `Tab` (`value`, `icon`, `count`, `disabled`), `Panel` (`value`, `keepMounted`). Arrow keys move, Enter/Space activates. |
39
- | `Input`, `Textarea` | Labelled native controls on Base UI's Field: required `label` (`hideLabel` keeps it for assistive tech), `description`, `error` (sets invalid and announces), `invalid`, `required`, `disabled`, `name`, `size` sm · md · lg (Input), `iconStart`/`iconEnd` (Input), `rows`/`resize`/`variant="code"` (Textarea; monospace, tab-size 2, no spellcheck, add native `wrap="off"` for sideways scroll), `onValueChange(string)` beside native `onChange`. Every native attribute passes through: `type="date" \| "time" \| "number"` keep the same height, `list` + a `<datalist>` gives suggestions. |
40
- | `Select` | Same field props; `options` as `{ value, label, disabled? }` or `{ label, options }` groups; `value`/`defaultValue`/`onValueChange` (`null` for none, shows `placeholder`; no sentinel option needed); `clearable` adds an × and Backspace/Delete on the trigger that call `onValueChange(null)`; `clearLabel`; `fullWidth`. Submits through `name` like a native select. |
41
- | `Checkbox`, `Switch` | Required `label`, `description`, `checked`/`defaultChecked`/`onCheckedChange(boolean)`, `name`/`value`, `disabled`, `required`; Checkbox adds `indeterminate`; Switch adds `labelPosition` start · end. |
42
- | `Tooltip` | `content`; `children` is the trigger (an `IconButton`, `Button` or focusable `Badge`); `side` top · bottom · left · right; `align`; `delay` (300ms); `open`/`onOpenChange`; `disabled`. `Tooltip.Provider` around a row skips the delay between siblings. |
43
- | `Chip` | A pill: `tone` neutral · accent · success · warning · danger · info; `size` sm · md; `icon`; `onClick` + `selected` make it a toggle button (`aria-pressed`); `onRemove` adds a labelled × (Backspace/Delete on the chip removes too); `removeLabel`; `disabled`. |
44
- | `Disclosure` | A native `<details>` collapse: required `summary`, `summaryOpen`, `open`/`defaultOpen`/`onOpenChange`, `previewLines` (fades the first N lines while closed, summary moves below), `summaryPosition`; share `name` for an exclusive group. |
45
- | `EmptyState` | Centred placeholder: required `title`, `description`, `icon` (24px in a muted circle), children are the actions, `size` sm · md, `as` div · section · li. |
46
- | `Skeleton` | Loading placeholder, `aria-hidden`: `variant` text (default, one line) · rect · circle, `width`, `height`, `lines` (text, last line shorter), `still`. Put `aria-busy` on the region or a labelled `Spinner` beside it. |
47
- | `Menu` | `Root` (`open`, `onOpenChange`, `modal`), `Trigger` (secondary Button with a chevron, or `render={<IconButton label="More" />}`), `Content` (`side`, `align`), `Item` (`icon`, `trailing`, `tone="danger"`, `disabled`), `LinkItem` (`href`), `CheckboxItem` (`checked`, `onCheckedChange`), `RadioGroup` (`value`, `onValueChange`, `label`) + `RadioItem`, `Group` (`label`), `Separator`. Arrow keys, Home/End, type-ahead, Esc and focus return come from Base UI. |
26
+ | Component | Notes |
27
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
+ | `Button` | `variant` primary · secondary · ghost · danger; `size` sm · md · lg; `iconStart`/`iconEnd`; `loading` (keeps width, `aria-busy`); `fullWidth`. |
29
+ | `IconButton` | Square Button for one icon. Required `label` (the accessible name); `variant` ghost (default) · secondary · primary · danger; `size`; `tooltip` (`true` shows `label`, or a string); `tooltipSide`; `round`; `loading`; `render={<a href />}` or a Next.js `Link` makes it a link. |
30
+ | `Icon`, `*Icon` | 49 outline icons on a 16px grid in `currentColor`, one component each (`PlayIcon`, `ChevronDownIcon` …, listed in `iconNames`); `size` xs 12 · sm 14 · md 16 · lg 20 · xl 24 · em; decorative unless `title` is given. `Icon` wraps your own paths. |
31
+ | `Badge` | `tone` neutral · accent · success · warning · danger · info; `variant` soft · solid · outline; `size` sm · md; `dot`. Text only. |
32
+ | `Card` | `variant` outlined · elevated · filled; `padding` none · sm · md · lg; `interactive`; `selected`; `as` or `render` to make it an `a`/`button`/`li`. |
33
+ | `Stack`, `Inline` | `gap` from the spacing scale (`none` … `4xl`); `align`; `justify`; `as`. `Inline` wraps unless `wrap={false}`. |
34
+ | `Kbd` | One key per `Kbd`; `Kbd.Group label="Command K"` groups a shortcut with a `+` (or `separator="space"`). |
35
+ | `Dialog` | `Root` (`open`, `defaultOpen`, `onOpenChange`, `dismissible`), `Trigger`, `Content` (`size` sm · md · lg, every size clamps to the viewport minus a 16px gutter; `variant="sheet"` becomes a bottom sheet below 640px with a scrolling `Body`), `Title`, `Description`, `Body`, `Actions`, `Close`. Menus, selects and tooltips opened inside render above it. |
36
+ | `Toolbar` | `Root` (required `aria-label`, `wrap`, `gap`), `Group` (`aria-label`, `gap`), `Separator`, `Spacer`, `Button` (`render={<IconButton … />}` or `render={<Menu.Trigger render={…} />}` so the control joins the roving focus: Tab enters, arrows move, Tab leaves), `Link` (an anchor in the same navigation; `render={<IconButton label render={<a href />} />}` for an icon link). |
37
+ | `Banner` | `tone` info (default) · success · warning · danger · neutral; `icon` (or `false`); `action` slot; `dismissible` + `onDismiss` (`open` to control); warning and danger are `role="alert"`, the rest `role="status"`. |
38
+ | `Tabs` | `Root` (`value`, `defaultValue`, `onValueChange`), `List` (required `aria-label`, `fill`), `Tab` (`value`, `icon`, `count`, `disabled`), `Panel` (`value`, `keepMounted`). Arrow keys move, Enter/Space activates. |
39
+ | `Input`, `Textarea` | Labelled native controls on Base UI's Field: required `label` (`hideLabel` keeps it for assistive tech), `description`, `error` (sets invalid and announces), `invalid`, `required`, `disabled`, `name`, `size` sm · md · lg (Input), `iconStart`/`iconEnd` (Input), `rows`/`resize`/`variant="code"` (Textarea; monospace, tab-size 2, no spellcheck, add native `wrap="off"` for sideways scroll), `onValueChange(string)` beside native `onChange`. Every native attribute passes through: `type="date" \| "time" \| "number"` keep the same height, `list` + a `<datalist>` gives suggestions. |
40
+ | `Select` | Same field props; `options` as `{ value, label, disabled? }` or `{ label, options }` groups; `value`/`defaultValue`/`onValueChange` (`null` for none, shows `placeholder`; no sentinel option needed); `clearable` adds an × and Backspace/Delete on the trigger that call `onValueChange(null)`; `clearLabel`; `fullWidth`. Submits through `name` like a native select. |
41
+ | `FileInput` | Same field props (`label`, `hideLabel`, `description`, `error`, `invalid`, `required`, `disabled`, `name`, `size`) on a drop zone built on FilePond (docs/adr/0004): `accept` (`".pdf,.docx"`, MIME types, `image/*`, as the native attribute), `acceptLabel`, `maxFileSize` (bytes or `"10MB"`), `multiple` + `maxFiles`, `files`/`defaultFiles`/`onFilesChange(File[])`, `idleLabel`, `browseLabel`. No upload server: plain `File`s go to the callback, and with `name` to the form's `FormData` (Server Actions included). Rejected files never join the list; the reason shows in the error slot and is announced. Without `multiple`, a new file replaces the current one only once it passes. FilePond loads on mount, in its own chunk. |
42
+ | `Checkbox`, `Switch` | Required `label`, `description`, `checked`/`defaultChecked`/`onCheckedChange(boolean)`, `name`/`value`, `disabled`, `required`; Checkbox adds `indeterminate`; Switch adds `labelPosition` start · end. |
43
+ | `Tooltip` | `content`; `children` is the trigger (an `IconButton`, `Button` or focusable `Badge`); `side` top · bottom · left · right; `align`; `delay` (300ms); `open`/`onOpenChange`; `disabled`. `Tooltip.Provider` around a row skips the delay between siblings. |
44
+ | `Chip` | A pill: `tone` neutral · accent · success · warning · danger · info; `size` sm · md; `icon`; `onClick` + `selected` make it a toggle button (`aria-pressed`); `onRemove` adds a labelled × (Backspace/Delete on the chip removes too); `removeLabel`; `disabled`. |
45
+ | `Disclosure` | A native `<details>` collapse: required `summary`, `summaryOpen`, `open`/`defaultOpen`/`onOpenChange`, `previewLines` (fades the first N lines while closed, summary moves below), `summaryPosition`; share `name` for an exclusive group. |
46
+ | `EmptyState` | Centred placeholder: required `title`, `description`, `icon` (24px in a muted circle), children are the actions, `size` sm · md, `as` div · section · li. |
47
+ | `Skeleton` | Loading placeholder, `aria-hidden`: `variant` text (default, one line) · rect · circle, `width`, `height`, `lines` (text, last line shorter), `still`. Put `aria-busy` on the region or a labelled `Spinner` beside it. |
48
+ | `Menu` | `Root` (`open`, `onOpenChange`, `modal`), `Trigger` (secondary Button with a chevron, or `render={<IconButton label="More" />}`), `Content` (`side`, `align`), `Item` (`icon`, `trailing`, `tone="danger"`, `disabled`), `LinkItem` (`href`), `CheckboxItem` (`checked`, `onCheckedChange`), `RadioGroup` (`value`, `onValueChange`, `label`) + `RadioItem`, `Group` (`label`), `Separator`. Arrow keys, Home/End, type-ahead, Esc and focus return come from Base UI. |
48
49
 
49
50
  ```tsx
50
51
  <Dialog.Root>
package/dist/index.d.ts CHANGED
@@ -2424,7 +2424,38 @@ interface SkeletonProps extends ComponentPropsWithRef<'span'> {
2424
2424
  */
2425
2425
  declare function Skeleton({ variant, width, height, lines, still, className, style, ...rest }: SkeletonProps): react.JSX.Element;
2426
2426
 
2427
+ interface FileInputProps extends FieldProps {
2428
+ /** Accepted types, as for the native attribute: `".pdf,.docx,text/plain"` or a list. Extensions, exact MIME types and `image/*` wildcards. */
2429
+ accept?: string | readonly string[] | undefined;
2430
+ /** How the accepted types read in the error message. Default derived from `accept` ("PDF, DOCX or plain text"). */
2431
+ acceptLabel?: string | undefined;
2432
+ /** Largest file accepted, in bytes or as `"10MB"` (base 1000, like the size shown in the list). */
2433
+ maxFileSize?: number | string | undefined;
2434
+ /** Allows several files. Without it a new file replaces the current one. */
2435
+ multiple?: boolean | undefined;
2436
+ /** With `multiple`, the most files the list holds. */
2437
+ maxFiles?: number | undefined;
2438
+ /** Controlled list of accepted files. Pass `[]` to clear. */
2439
+ files?: readonly File[] | undefined;
2440
+ /** Initial files when uncontrolled. */
2441
+ defaultFiles?: readonly File[] | undefined;
2442
+ /** Called with the accepted files (plain `File` objects) after every add or remove. Rejected files never appear here. */
2443
+ onFilesChange?: ((files: File[]) => void) | undefined;
2444
+ /** Drop zone text before the browse link. Default "Drop a file here or" (or "Drop files here or" with `multiple`). */
2445
+ idleLabel?: string | undefined;
2446
+ /** The browse link's text. Default "browse". */
2447
+ browseLabel?: string | undefined;
2448
+ }
2449
+ /**
2450
+ * A labelled file picker with a drop zone, on FilePond (docs/adr/0004). Files stay in the browser: the
2451
+ * component hands plain `File` objects to `onFilesChange`, and with `name` it also keeps them in a file
2452
+ * input so a `<form>` (or a Server Action) submits them. Type, size and count are checked before a file
2453
+ * joins the list; a rejected file never joins it, and the reason shows in the field's error slot and is
2454
+ * announced. FilePond loads on mount, so the server renders a static drop zone and nothing of FilePond.
2455
+ */
2456
+ declare function FileInput({ label, hideLabel, description, error, invalid, required, disabled, name, size, className, accept, acceptLabel, maxFileSize, multiple, maxFiles, files, defaultFiles, onFilesChange, idleLabel, browseLabel, }: FileInputProps): react.JSX.Element;
2457
+
2427
2458
  declare const version = "0.1.1";
2428
2459
 
2429
- export { ArchiveIcon, ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, Badge, Banner, BellIcon, BriefcaseIcon, Button, CalendarIcon, Card, CheckIcon, Checkbox, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, Chip, CircleCheckIcon, CircleXIcon, ClockIcon, CopyIcon, Dialog, Disclosure, DocumentIcon, DownloadIcon, EditIcon, EmptyState, ExternalLinkIcon, EyeIcon, FilterIcon, FlagIcon, Heading, Icon, IconButton, InboxIcon, InfoIcon, Inline, Input, Kbd, LinkIcon, MailIcon, MapPinIcon, Menu, MenuIcon, MinusIcon, MoonIcon, MoreHorizontalIcon, MoreVerticalIcon, PauseIcon, PlayIcon, PlusIcon, Progress, RefreshIcon, SearchIcon, Select, SettingsIcon, Skeleton, Spinner, Stack, StarIcon, Stat, SunIcon, Switch, Table, Tabs, Text, Textarea, ThumbsDownIcon, ThumbsUpIcon, Toolbar, Tooltip, TrashIcon, UndoIcon, UserIcon, WarningIcon, XIcon, iconNames, version };
2430
- export type { BadgeProps, BadgeSize, BadgeTone, BadgeVariant, BannerProps, BannerTone, ButtonProps, ButtonSize, ButtonVariant, CardElement, CardPadding, CardProps, CardVariant, CheckboxProps, ChipProps, ChipSize, ChipTone, ControlSize, DialogActionsProps, DialogBodyProps, DialogCloseProps, DialogContentProps, DialogDescriptionProps, DialogRootProps, DialogSize, DialogTitleProps, DialogVariant, DisclosureProps, EmptyStateElement, EmptyStateProps, EmptyStateSize, FieldProps, HeadingElement, HeadingLevel, HeadingProps, HeadingSize, IconButtonProps, IconGlyphProps, IconName, IconProps, IconSize, InlineAlign, InlineElement, InlineJustify, InlineProps, InputProps, KbdGroupProps, KbdProps, MenuAlign, MenuCheckboxItemProps, MenuContentProps, MenuGroupProps, MenuItemProps, MenuItemTone, MenuLinkItemProps, MenuRadioGroupProps, MenuRadioItemProps, MenuRootProps, MenuSeparatorProps, MenuSide, MenuTriggerProps, ProgressProps, ProgressSize, ProgressTone, ProgressVariant, SelectGroup, SelectOption, SelectProps, SkeletonProps, SkeletonVariant, SpacingSize, SpinnerProps, StackAlign, StackElement, StackJustify, StackProps, StatDeltaTone, StatElement, StatProps, StatSize, StatTrend, StatVariant, SwitchProps, TableAlign, TableBodyProps, TableCaptionProps, TableCellProps, TableDensity, TableFootProps, TableHeadProps, TableHeaderProps, TableRootProps, TableRowProps, TableSortDirection, TabsListProps, TabsPanelProps, TabsRootProps, TabsTabProps, TextAlign, TextElement, TextProps, TextSize, TextTone, TextWeight, TextareaProps, TextareaVariant, ToolbarButtonProps, ToolbarGroupProps, ToolbarLinkProps, ToolbarRootProps, ToolbarSeparatorProps, ToolbarSpacerProps, TooltipAlign, TooltipProps, TooltipSide };
2460
+ export { ArchiveIcon, ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, Badge, Banner, BellIcon, BriefcaseIcon, Button, CalendarIcon, Card, CheckIcon, Checkbox, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, Chip, CircleCheckIcon, CircleXIcon, ClockIcon, CopyIcon, Dialog, Disclosure, DocumentIcon, DownloadIcon, EditIcon, EmptyState, ExternalLinkIcon, EyeIcon, FileInput, FilterIcon, FlagIcon, Heading, Icon, IconButton, InboxIcon, InfoIcon, Inline, Input, Kbd, LinkIcon, MailIcon, MapPinIcon, Menu, MenuIcon, MinusIcon, MoonIcon, MoreHorizontalIcon, MoreVerticalIcon, PauseIcon, PlayIcon, PlusIcon, Progress, RefreshIcon, SearchIcon, Select, SettingsIcon, Skeleton, Spinner, Stack, StarIcon, Stat, SunIcon, Switch, Table, Tabs, Text, Textarea, ThumbsDownIcon, ThumbsUpIcon, Toolbar, Tooltip, TrashIcon, UndoIcon, UserIcon, WarningIcon, XIcon, iconNames, version };
2461
+ export type { BadgeProps, BadgeSize, BadgeTone, BadgeVariant, BannerProps, BannerTone, ButtonProps, ButtonSize, ButtonVariant, CardElement, CardPadding, CardProps, CardVariant, CheckboxProps, ChipProps, ChipSize, ChipTone, ControlSize, DialogActionsProps, DialogBodyProps, DialogCloseProps, DialogContentProps, DialogDescriptionProps, DialogRootProps, DialogSize, DialogTitleProps, DialogVariant, DisclosureProps, EmptyStateElement, EmptyStateProps, EmptyStateSize, FieldProps, FileInputProps, HeadingElement, HeadingLevel, HeadingProps, HeadingSize, IconButtonProps, IconGlyphProps, IconName, IconProps, IconSize, InlineAlign, InlineElement, InlineJustify, InlineProps, InputProps, KbdGroupProps, KbdProps, MenuAlign, MenuCheckboxItemProps, MenuContentProps, MenuGroupProps, MenuItemProps, MenuItemTone, MenuLinkItemProps, MenuRadioGroupProps, MenuRadioItemProps, MenuRootProps, MenuSeparatorProps, MenuSide, MenuTriggerProps, ProgressProps, ProgressSize, ProgressTone, ProgressVariant, SelectGroup, SelectOption, SelectProps, SkeletonProps, SkeletonVariant, SpacingSize, SpinnerProps, StackAlign, StackElement, StackJustify, StackProps, StatDeltaTone, StatElement, StatProps, StatSize, StatTrend, StatVariant, SwitchProps, TableAlign, TableBodyProps, TableCaptionProps, TableCellProps, TableDensity, TableFootProps, TableHeadProps, TableHeaderProps, TableRootProps, TableRowProps, TableSortDirection, TabsListProps, TabsPanelProps, TabsRootProps, TabsTabProps, TextAlign, TextElement, TextProps, TextSize, TextTone, TextWeight, TextareaProps, TextareaVariant, ToolbarButtonProps, ToolbarGroupProps, ToolbarLinkProps, ToolbarRootProps, ToolbarSeparatorProps, ToolbarSpacerProps, TooltipAlign, TooltipProps, TooltipSide };
package/dist/index.js CHANGED
@@ -1734,6 +1734,391 @@ function Skeleton({
1734
1734
  );
1735
1735
  }
1736
1736
 
1737
+ // src/components/FileInput/FileInput.tsx
1738
+ import { useEffect as useEffect2, useId as useId3, useRef as useRef2, useState as useState4 } from "react";
1739
+
1740
+ // src/components/FileInput/accept.ts
1741
+ function parseAccept(accept) {
1742
+ if (accept == null) return [];
1743
+ const list = typeof accept === "string" ? accept.split(",") : accept;
1744
+ return list.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
1745
+ }
1746
+ function matchesAccept(file, accept) {
1747
+ if (accept.length === 0) return true;
1748
+ const name = file.name.toLowerCase();
1749
+ const type = file.type.toLowerCase();
1750
+ return accept.some((entry) => {
1751
+ if (entry.startsWith(".")) return name.endsWith(entry);
1752
+ if (entry.endsWith("/*")) return type.startsWith(entry.slice(0, -1));
1753
+ return type === entry;
1754
+ });
1755
+ }
1756
+ var UNITS = { b: 1, kb: 1e3, mb: 1e6, gb: 1e9 };
1757
+ function parseSize(size) {
1758
+ if (size == null) return void 0;
1759
+ if (typeof size === "number") return size;
1760
+ const match = /^\s*(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)\s*$/i.exec(size);
1761
+ if (!match) throw new Error(`FileInput: maxFileSize "${size}" is not a size like "10MB".`);
1762
+ return Math.round(Number(match[1]) * UNITS[match[2].toLowerCase()]);
1763
+ }
1764
+ function formatSize(bytes) {
1765
+ const [unit, base] = bytes >= 1e9 ? ["GB", 1e9] : bytes >= 1e6 ? ["MB", 1e6] : bytes >= 1e3 ? ["KB", 1e3] : ["bytes", 1];
1766
+ const value = bytes / base;
1767
+ return `${Number.isInteger(value) ? value : Number(value.toFixed(1))} ${unit}`;
1768
+ }
1769
+ var MIME_NAMES = {
1770
+ "application/pdf": "PDF",
1771
+ "application/msword": "DOC",
1772
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX",
1773
+ "application/json": "JSON",
1774
+ "text/plain": "plain text",
1775
+ "text/markdown": "Markdown",
1776
+ "text/csv": "CSV",
1777
+ "image/png": "PNG",
1778
+ "image/jpeg": "JPEG"
1779
+ };
1780
+ function describeAccept(accept) {
1781
+ const names = accept.map((entry) => {
1782
+ if (entry.startsWith(".")) return entry.slice(1).toUpperCase();
1783
+ if (entry.endsWith("/*")) return `any ${entry.slice(0, -2)}`;
1784
+ return MIME_NAMES[entry] ?? entry.split("/").pop().toUpperCase();
1785
+ });
1786
+ const unique = [...new Set(names)];
1787
+ if (unique.length <= 1) return unique[0] ?? "";
1788
+ return `${unique.slice(0, -1).join(", ")} or ${unique.at(-1)}`;
1789
+ }
1790
+ function rejectionMessage(reason, detail) {
1791
+ switch (reason) {
1792
+ case "type":
1793
+ return `${detail.name} is not an accepted file type. Use ${detail.acceptLabel}.`;
1794
+ case "size":
1795
+ return `${detail.name} is ${formatSize(detail.size ?? 0)}, over the ${formatSize(detail.maxFileSize ?? 0)} limit.`;
1796
+ case "count":
1797
+ return detail.maxFiles === 1 ? "Choose one file." : `Up to ${detail.maxFiles} files. Remove one to add another.`;
1798
+ }
1799
+ }
1800
+ function escapeHtml(text) {
1801
+ return text.replace(
1802
+ /[&<>"']/g,
1803
+ (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[ch]
1804
+ );
1805
+ }
1806
+
1807
+ // src/components/FileInput/FileInput.tsx
1808
+ import { jsx as jsx27, jsxs as jsxs22 } from "react/jsx-runtime";
1809
+ var REMOVE_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
1810
+ var sameFiles = (a, b) => a.length === b.length && a.every((file, i) => file === b[i]);
1811
+ function FileInput({
1812
+ label,
1813
+ hideLabel = false,
1814
+ description,
1815
+ error,
1816
+ invalid = false,
1817
+ required = false,
1818
+ disabled = false,
1819
+ name,
1820
+ size = "md",
1821
+ className,
1822
+ accept,
1823
+ acceptLabel,
1824
+ maxFileSize,
1825
+ multiple = false,
1826
+ maxFiles,
1827
+ files,
1828
+ defaultFiles,
1829
+ onFilesChange,
1830
+ idleLabel,
1831
+ browseLabel = "browse"
1832
+ }) {
1833
+ const id = useId3();
1834
+ const labelId = `${id}label`;
1835
+ const descriptionId = `${id}description`;
1836
+ const messagesId = `${id}messages`;
1837
+ const controlled = files !== void 0;
1838
+ const [uncontrolledFiles, setUncontrolledFiles] = useState4(defaultFiles ?? []);
1839
+ const current = controlled ? files : uncontrolledFiles;
1840
+ const [rejections, setRejections] = useState4([]);
1841
+ const [inputId, setInputId] = useState4();
1842
+ const [pondVersion, setPondVersion] = useState4(0);
1843
+ const hostRef = useRef2(null);
1844
+ const pondRef = useRef2(null);
1845
+ const moduleRef = useRef2(null);
1846
+ const patchRef = useRef2(() => {
1847
+ });
1848
+ const settledRef = useRef2(current);
1849
+ const targetRef = useRef2(null);
1850
+ const acceptList = parseAccept(accept);
1851
+ const acceptAttr = acceptList.join(",");
1852
+ const maxBytes = parseSize(maxFileSize);
1853
+ const messages = [...rejections, ...error != null ? [error] : []];
1854
+ const isInvalid = invalid || messages.length > 0;
1855
+ const idleText = idleLabel ?? (multiple ? "Drop files here or" : current.length > 0 ? "Drop another file to replace it, or" : "Drop a file here or");
1856
+ const live = {
1857
+ acceptList,
1858
+ acceptLabel: acceptLabel ?? describeAccept(acceptList),
1859
+ maxBytes,
1860
+ maxFiles: multiple ? maxFiles : 1,
1861
+ multiple,
1862
+ current,
1863
+ controlled,
1864
+ onFilesChange,
1865
+ labelId,
1866
+ describedBy: [description != null ? descriptionId : null, messages.length > 0 ? messagesId : null],
1867
+ isInvalid,
1868
+ acceptAttr
1869
+ };
1870
+ const liveRef = useRef2(live);
1871
+ useEffect2(() => {
1872
+ liveRef.current = live;
1873
+ });
1874
+ useEffect2(() => {
1875
+ let cancelled = false;
1876
+ let pond = null;
1877
+ let observer = null;
1878
+ const host = hostRef.current;
1879
+ if (!host) return void 0;
1880
+ const reject = (reason, file) => {
1881
+ const l = liveRef.current;
1882
+ const message = rejectionMessage(reason, {
1883
+ name: file?.name,
1884
+ size: file?.size,
1885
+ acceptLabel: l.acceptLabel,
1886
+ maxFileSize: l.maxBytes,
1887
+ maxFiles: l.maxFiles
1888
+ });
1889
+ setRejections((list) => list.includes(message) ? list : [...list, message]);
1890
+ if (file && targetRef.current) {
1891
+ targetRef.current = targetRef.current.filter((f) => f !== file);
1892
+ queueMicrotask(emit);
1893
+ }
1894
+ };
1895
+ const startAttempt = () => setRejections((list) => list.length ? [] : list);
1896
+ let removeIntent = false;
1897
+ const acceptedIds = /* @__PURE__ */ new Set();
1898
+ const noteRemove = (event) => {
1899
+ if (event.target?.closest?.(".filepond--action-remove-item")) removeIntent = true;
1900
+ };
1901
+ const emit = () => {
1902
+ const mod = moduleRef.current;
1903
+ if (!pond || !mod) return;
1904
+ const idle = pond.getFiles().filter((item) => item.status === mod.FileStatus.IDLE).map((item) => item.file);
1905
+ const accepted = liveRef.current.multiple ? idle : idle.slice(-1);
1906
+ const target = targetRef.current;
1907
+ if (target) {
1908
+ if (!sameFiles(accepted, target)) return;
1909
+ targetRef.current = null;
1910
+ settledRef.current = accepted;
1911
+ if (!sameFiles(accepted, liveRef.current.current)) {
1912
+ if (!liveRef.current.controlled) setUncontrolledFiles(accepted);
1913
+ liveRef.current.onFilesChange?.(accepted);
1914
+ }
1915
+ return;
1916
+ }
1917
+ if (sameFiles(accepted, settledRef.current)) return;
1918
+ settledRef.current = accepted;
1919
+ if (!liveRef.current.controlled) setUncontrolledFiles(accepted);
1920
+ liveRef.current.onFilesChange?.(accepted);
1921
+ };
1922
+ const patch = () => {
1923
+ if (!pond) return;
1924
+ const l = liveRef.current;
1925
+ const root = pond.element;
1926
+ if (!root) return;
1927
+ const set = (el, attr, value) => {
1928
+ if (!el || el.getAttribute(attr) === value) return;
1929
+ if (value == null) el.removeAttribute(attr);
1930
+ else el.setAttribute(attr, value);
1931
+ };
1932
+ const input = root.querySelector("input.filepond--browser");
1933
+ const dropLabel = root.querySelector(".filepond--drop-label label");
1934
+ set(input, "aria-labelledby", l.labelId);
1935
+ set(input, "aria-describedby", [dropLabel?.id, ...l.describedBy].filter(Boolean).join(" ") || null);
1936
+ set(input, "aria-invalid", l.isInvalid ? "true" : null);
1937
+ set(input, "accept", l.acceptAttr || null);
1938
+ set(input, "multiple", l.multiple ? "" : null);
1939
+ set(root.querySelector(".filepond--label-action"), "tabindex", null);
1940
+ for (const item of root.querySelectorAll(".filepond--item")) {
1941
+ const fileName = item.querySelector(".filepond--file-info-main")?.textContent?.trim();
1942
+ if (fileName)
1943
+ set(item.querySelector(".filepond--action-remove-item"), "aria-label", `Remove ${fileName}`);
1944
+ }
1945
+ setInputId(input?.id || void 0);
1946
+ };
1947
+ void import("filepond").then((mod) => {
1948
+ if (cancelled || !mod.supported()) return;
1949
+ moduleRef.current = mod;
1950
+ const options = {
1951
+ credits: false,
1952
+ allowPaste: false,
1953
+ allowReorder: false,
1954
+ allowProcess: false,
1955
+ allowRevert: false,
1956
+ instantUpload: false,
1957
+ dropOnPage: false,
1958
+ // Always multi: FilePond's own single mode replaces the current file *before* validating the new
1959
+ // one, so a rejected file would discard a good one. Replacement happens in `addfile` instead.
1960
+ allowMultiple: true,
1961
+ allowReplace: false,
1962
+ itemInsertLocation: "after",
1963
+ fileSizeBase: 1e3,
1964
+ iconRemove: REMOVE_ICON,
1965
+ labelButtonRemoveItem: "Remove",
1966
+ styleButtonRemoveItemPosition: "right",
1967
+ beforeAddFile: (item) => {
1968
+ const l = liveRef.current;
1969
+ const file = item.file;
1970
+ if (!matchesAccept(file, l.acceptList)) return reject("type", file), false;
1971
+ if (l.maxBytes != null && file.size > l.maxBytes) return reject("size", file), false;
1972
+ return true;
1973
+ }
1974
+ };
1975
+ pond = mod.create(options);
1976
+ const root = pond.element;
1977
+ if (!root) return;
1978
+ host.appendChild(root);
1979
+ pondRef.current = pond;
1980
+ patchRef.current = patch;
1981
+ pond.on("addfile", (error2, added) => {
1982
+ if (!error2) acceptedIds.add(added.id);
1983
+ if (!error2 && !liveRef.current.multiple) {
1984
+ for (const other of pond?.getFiles() ?? []) if (other.id !== added.id) pond?.removeFile(other.id);
1985
+ }
1986
+ emit();
1987
+ });
1988
+ pond.on("removefile", (_error, removed) => {
1989
+ const active = document.activeElement;
1990
+ const inRemovedRow = active?.closest(".filepond--item")?.id === `filepond--item-${removed.id}`;
1991
+ if (inRemovedRow || removeIntent && (active == null || active === document.body))
1992
+ root.querySelector("input.filepond--browser")?.focus();
1993
+ removeIntent = false;
1994
+ if (acceptedIds.delete(removed.id)) startAttempt();
1995
+ emit();
1996
+ });
1997
+ pond.on("updatefiles", emit);
1998
+ pond.on("warning", (warning) => {
1999
+ if (warning?.body === "Max files") reject("count");
2000
+ });
2001
+ observer = new MutationObserver(patch);
2002
+ observer.observe(root, {
2003
+ subtree: true,
2004
+ childList: true,
2005
+ attributes: true,
2006
+ attributeFilter: ["tabindex", "aria-labelledby", "aria-describedby", "accept", "id", "multiple"]
2007
+ });
2008
+ host.addEventListener("drop", startAttempt, true);
2009
+ host.addEventListener("change", startAttempt, true);
2010
+ host.addEventListener("click", noteRemove, true);
2011
+ settledRef.current = [];
2012
+ setPondVersion((v) => v + 1);
2013
+ });
2014
+ return () => {
2015
+ cancelled = true;
2016
+ host.removeEventListener("drop", startAttempt, true);
2017
+ host.removeEventListener("change", startAttempt, true);
2018
+ host.removeEventListener("click", noteRemove, true);
2019
+ observer?.disconnect();
2020
+ if (pond) {
2021
+ pond.element?.remove();
2022
+ pond.destroy();
2023
+ }
2024
+ pondRef.current = null;
2025
+ patchRef.current = () => {
2026
+ };
2027
+ };
2028
+ }, []);
2029
+ useEffect2(() => {
2030
+ const pond = pondRef.current;
2031
+ if (!pond) return;
2032
+ pond.setOptions({
2033
+ name: name ?? "",
2034
+ storeAsFile: name != null,
2035
+ disabled,
2036
+ required,
2037
+ maxFiles: multiple ? maxFiles ?? null : null,
2038
+ allowRemove: !disabled,
2039
+ labelIdle: `${escapeHtml(idleText)} <span class="filepond--label-action">${escapeHtml(browseLabel)}</span>`
2040
+ });
2041
+ patchRef.current();
2042
+ }, [
2043
+ pondVersion,
2044
+ name,
2045
+ disabled,
2046
+ required,
2047
+ multiple,
2048
+ maxFiles,
2049
+ idleText,
2050
+ browseLabel,
2051
+ labelId,
2052
+ acceptAttr,
2053
+ isInvalid,
2054
+ description,
2055
+ messages.length
2056
+ ]);
2057
+ useEffect2(() => {
2058
+ const pond = pondRef.current;
2059
+ const mod = moduleRef.current;
2060
+ if (!pond || !mod) return;
2061
+ const items = pond.getFiles();
2062
+ const idle = items.filter((item) => item.status === mod.FileStatus.IDLE);
2063
+ const inPond = idle.map((item) => item.file);
2064
+ if (sameFiles(inPond, current)) {
2065
+ settledRef.current = current;
2066
+ return;
2067
+ }
2068
+ targetRef.current = current;
2069
+ for (const item of idle) if (!current.includes(item.file)) pond.removeFile(item.id);
2070
+ const toAdd = current.filter((file) => !inPond.includes(file));
2071
+ if (toAdd.length > 0) void pond.addFiles(toAdd);
2072
+ }, [current, pondVersion]);
2073
+ const ready = pondVersion > 0;
2074
+ return /* @__PURE__ */ jsxs22(
2075
+ "div",
2076
+ {
2077
+ className: cx("sc-field", "sc-file-input-field", className),
2078
+ "data-size": size,
2079
+ "data-disabled": disabled ? "" : void 0,
2080
+ "data-invalid": isInvalid ? "" : void 0,
2081
+ children: [
2082
+ /* @__PURE__ */ jsxs22(
2083
+ "label",
2084
+ {
2085
+ id: labelId,
2086
+ htmlFor: inputId,
2087
+ className: cx("sc-field__label", hideLabel && "sc-visually-hidden"),
2088
+ children: [
2089
+ label,
2090
+ required ? /* @__PURE__ */ jsx27("span", { className: "sc-field__required", "aria-hidden": "true", children: "*" }) : null
2091
+ ]
2092
+ }
2093
+ ),
2094
+ /* @__PURE__ */ jsxs22(
2095
+ "div",
2096
+ {
2097
+ className: "sc-file-input",
2098
+ "data-ready": ready ? "" : void 0,
2099
+ "data-disabled": disabled ? "" : void 0,
2100
+ "data-invalid": isInvalid ? "" : void 0,
2101
+ "data-multiple": multiple ? "" : void 0,
2102
+ children: [
2103
+ ready ? null : (
2104
+ // Server render and first paint: the same drop zone, inert until FilePond takes over.
2105
+ /* @__PURE__ */ jsxs22("div", { className: "sc-file-input__placeholder", "aria-hidden": "true", children: [
2106
+ idleText,
2107
+ " ",
2108
+ /* @__PURE__ */ jsx27("span", { className: "sc-file-input__browse", children: browseLabel })
2109
+ ] })
2110
+ ),
2111
+ /* @__PURE__ */ jsx27("div", { ref: hostRef, className: "sc-file-input__pond" })
2112
+ ]
2113
+ }
2114
+ ),
2115
+ description != null ? /* @__PURE__ */ jsx27("p", { id: descriptionId, className: "sc-field__description", children: description }) : null,
2116
+ /* @__PURE__ */ jsx27("div", { id: messagesId, className: "sc-file-input__messages", "aria-live": "polite", children: messages.map((message, i) => /* @__PURE__ */ jsx27("p", { className: "sc-field__error", children: message }, i)) })
2117
+ ]
2118
+ }
2119
+ );
2120
+ }
2121
+
1737
2122
  // src/index.ts
1738
2123
  var version = "0.1.1";
1739
2124
  export {
@@ -1768,6 +2153,7 @@ export {
1768
2153
  EmptyState,
1769
2154
  ExternalLinkIcon,
1770
2155
  EyeIcon,
2156
+ FileInput,
1771
2157
  FilterIcon,
1772
2158
  FlagIcon,
1773
2159
  Heading,