@seekalfred/primitives 0.7.3 → 0.8.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 +315 -4
- package/dist/alert/alert.d.ts +2 -2
- package/dist/alert/alert.js +1 -1
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert-dialog/alert-dialog.d.ts +14 -0
- package/dist/alert-dialog/alert-dialog.js +52 -27
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/announcement-bar/announcement-bar.d.ts +15 -4
- package/dist/announcement-bar/announcement-bar.js +31 -6
- package/dist/announcement-bar/announcement-bar.js.map +1 -1
- package/dist/avatar/avatar.d.ts +7 -0
- package/dist/avatar/avatar.js +33 -12
- package/dist/avatar/avatar.js.map +1 -1
- package/dist/badge/badge.d.ts +16 -1
- package/dist/badge/badge.js +12 -5
- package/dist/badge/badge.js.map +1 -1
- package/dist/bar-chart/bar-chart-series.d.ts +75 -0
- package/dist/bar-chart/bar-chart-series.js +401 -0
- package/dist/bar-chart/bar-chart-series.js.map +1 -0
- package/dist/bar-chart/bar-chart.d.ts +25 -1
- package/dist/bar-chart/bar-chart.js +26 -1
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/index.d.ts +2 -1
- package/dist/button/button.d.ts +24 -2
- package/dist/button/button.js +62 -13
- package/dist/button/button.js.map +1 -1
- package/dist/card/card.js +1 -1
- package/dist/card/card.js.map +1 -1
- package/dist/checkbox/checkbox.d.ts +25 -1
- package/dist/checkbox/checkbox.js +103 -28
- package/dist/checkbox/checkbox.js.map +1 -1
- package/dist/checkbox/index.d.ts +2 -2
- package/dist/checkbox/index.js +2 -2
- package/dist/chip-group/chip-group.d.ts +11 -0
- package/dist/chip-group/chip-group.js +50 -34
- package/dist/chip-group/chip-group.js.map +1 -1
- package/dist/chip-group/index.d.ts +2 -2
- package/dist/code-block/code-block.d.ts +45 -0
- package/dist/code-block/code-block.js +74 -0
- package/dist/code-block/code-block.js.map +1 -0
- package/dist/code-block/index.d.ts +2 -0
- package/dist/code-block/index.js +2 -0
- package/dist/command-palette/command-palette.d.ts +128 -0
- package/dist/command-palette/command-palette.js +371 -0
- package/dist/command-palette/command-palette.js.map +1 -0
- package/dist/command-palette/index.d.ts +2 -0
- package/dist/command-palette/index.js +2 -0
- package/dist/copy-button/copy-button.d.ts +19 -3
- package/dist/copy-button/copy-button.js +7 -4
- package/dist/copy-button/copy-button.js.map +1 -1
- package/dist/dialog/dialog.js +1 -1
- package/dist/dialog/dialog.js.map +1 -1
- package/dist/empty-state/empty-state.d.ts +17 -0
- package/dist/empty-state/empty-state.js +9 -7
- package/dist/empty-state/empty-state.js.map +1 -1
- package/dist/file-drop-zone/file-drop-zone.d.ts +29 -3
- package/dist/file-drop-zone/file-drop-zone.js +26 -8
- package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
- package/dist/gauge/gauge.d.ts +18 -1
- package/dist/gauge/gauge.js +35 -21
- package/dist/gauge/gauge.js.map +1 -1
- package/dist/gauge/index.d.ts +2 -2
- package/dist/index.d.ts +14 -9
- package/dist/index.js +14 -10
- package/dist/info-tooltip/index.d.ts +2 -0
- package/dist/info-tooltip/index.js +2 -0
- package/dist/info-tooltip/info-tooltip.d.ts +35 -0
- package/dist/info-tooltip/info-tooltip.js +49 -0
- package/dist/info-tooltip/info-tooltip.js.map +1 -0
- package/dist/input/input.d.ts +7 -0
- package/dist/input/input.js +4 -2
- package/dist/input/input.js.map +1 -1
- package/dist/lib/field-shell.js +2 -6
- package/dist/lib/field-shell.js.map +1 -1
- package/dist/lib/required-marker.js +20 -0
- package/dist/lib/required-marker.js.map +1 -0
- package/dist/lib/selection-control.js +32 -1
- package/dist/lib/selection-control.js.map +1 -1
- package/dist/line-chart/index.d.ts +2 -2
- package/dist/line-chart/line-chart.d.ts +106 -3
- package/dist/line-chart/line-chart.js +185 -46
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/mention-input/mention-input.js +1 -1
- package/dist/money-input/index.d.ts +2 -2
- package/dist/money-input/money-input.d.ts +14 -0
- package/dist/money-input/money-input.js +17 -7
- package/dist/money-input/money-input.js.map +1 -1
- package/dist/morph-select/morph-select.d.ts +8 -1
- package/dist/morph-select/morph-select.js +8 -5
- package/dist/morph-select/morph-select.js.map +1 -1
- package/dist/multi-select/multi-select.d.ts +31 -0
- package/dist/multi-select/multi-select.js +123 -6
- package/dist/multi-select/multi-select.js.map +1 -1
- package/dist/number-field/number-field.js +1 -1
- package/dist/password-field/password-field.js +1 -1
- package/dist/password-strength/password-strength.js +1 -1
- package/dist/phone-input/phone-input.js +1 -1
- package/dist/popover/popover.js +1 -1
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/index.js +2 -2
- package/dist/progress/progress.d.ts +53 -2
- package/dist/progress/progress.js +146 -36
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/radio-group.d.ts +12 -3
- package/dist/radio-group/radio-group.js +62 -52
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/rich-text-editor/rich-text-editor.js +1 -1
- package/dist/scroll-area/scroll-area.d.ts +1 -1
- package/dist/scrub-input/scrub-input.d.ts +11 -1
- package/dist/scrub-input/scrub-input.js +22 -10
- package/dist/scrub-input/scrub-input.js.map +1 -1
- package/dist/search-field/search-field.js +1 -1
- package/dist/select/select.d.ts +10 -1
- package/dist/select/select.js +4 -3
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +1 -1
- package/dist/shortcut-recorder/shortcut-recorder.js +1 -1
- package/dist/sparkline/sparkline.d.ts +27 -2
- package/dist/sparkline/sparkline.js +115 -18
- package/dist/sparkline/sparkline.js.map +1 -1
- package/dist/stat-card/index.d.ts +2 -2
- package/dist/stat-card/index.js +2 -2
- package/dist/stat-card/stat-card.d.ts +32 -1
- package/dist/stat-card/stat-card.js +52 -9
- package/dist/stat-card/stat-card.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +2 -2
- package/dist/table/table.d.ts +211 -3
- package/dist/table/table.js +473 -93
- package/dist/table/table.js.map +1 -1
- package/dist/tabs/tabs.d.ts +4 -2
- package/dist/tabs/tabs.js +7 -5
- package/dist/tabs/tabs.js.map +1 -1
- package/dist/tag-input/tag-input.js +1 -1
- package/dist/text-shimmer/text-shimmer.d.ts +18 -2
- package/dist/text-shimmer/text-shimmer.js +5 -2
- package/dist/text-shimmer/text-shimmer.js.map +1 -1
- package/dist/textarea/textarea.d.ts +7 -0
- package/dist/textarea/textarea.js +4 -2
- package/dist/textarea/textarea.js.map +1 -1
- package/dist/toast/toast.d.ts +2 -2
- package/dist/toggle-button/index.d.ts +2 -0
- package/dist/toggle-button/index.js +2 -0
- package/dist/toggle-button/toggle-button.d.ts +93 -0
- package/dist/toggle-button/toggle-button.js +199 -0
- package/dist/toggle-button/toggle-button.js.map +1 -0
- package/package.json +19 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialog.js","names":["Dialog","AriaModalOverlay","AriaModal","AriaDialog","AriaHeading","AriaText","AriaButton"],"sources":["../../src/dialog/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Dialog as AriaDialog,\n Heading as AriaHeading,\n Modal as AriaModal,\n ModalOverlay as AriaModalOverlay,\n Text as AriaText,\n type ModalOverlayProps as AriaModalOverlayProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Dialog, ported from the legacy shadcn/Radix component onto React Aria's\n * Modal primitives.\n *\n * FOUR LEGACY PARTS COLLAPSE INTO TWO PROPS. The census (9 importing files,\n * but only **7** that render anything -- header.tsx and PollQuizCreator.tsx\n * import the parts and use none) reads: DialogContent 9, DialogTitle 8,\n * DialogHeader 7, DialogDescription 2. Those four always appear in the same\n * shape, so `title` and `description` are props of the dialog itself.\n *\n * That is not tidying. `title` is REQUIRED, which makes an unnamed dialog\n * impossible to write -- and the legacy shipped one: LiveQAOverlay.tsx\n * renders a DialogContent with no DialogTitle, so its `aria-labelledby`\n * points at an id that matches no element and the dialog is announced with\n * no name.\n *\n * It also fixes the dangling-description defect for free. Radix set\n * `aria-describedby` unconditionally from a `useId`, so seven of the nine\n * legacy dialogs referenced an id that did not exist. React Aria wires it\n * only when a description slot is actually rendered.\n *\n * `DialogPortal` and `DialogOverlay` are not ported: zero external call\n * sites, and React Aria's ModalOverlay does both jobs.\n *\n * NOT PORTED, and deliberately: the enter/exit animation. The legacy leans\n * on tailwindcss-animate's `animate-in` / `zoom-in-95` utilities, which this\n * repo does not ship, and the motion tokens are a separate task. React Aria\n * exposes `data-entering` / `data-exiting` for whoever adds them.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const dialogOverlayVariants = cva([\n // Centring by flex, not by `top-1/2 left-1/2 -translate-1/2`. The legacy's\n // translate pair is a bracket value that check:hardcoding rejects, and\n // flex centring needs no transform to undo.\n \"fixed inset-0 z-50 flex items-center justify-center\",\n // The inset that keeps a full-width panel off the viewport edge. The\n // legacy did this with max-w-[calc(100%-2rem)] -- also a bracket value.\n \"p-4\",\n \"bg-scrim\",\n]);\n\nexport const dialogPanelVariants = cva([\n // max-w-128 (32rem), NOT max-w-lg. `--container-lg` does not exist in this\n // repo -- the container scale holds only page-shell widths (max 75rem,\n // wide 90rem) -- so `max-w-lg` compiles to NOTHING and the panel spans the\n // viewport. It emitted no rule at all, tsc and ESLint were both blind to\n // it, and a screenshot is what caught it. 128 spacing units is 32rem,\n // exactly the legacy sm:max-w-lg.\n \"relative w-full max-w-128\",\n \"rounded-panel border-(length:--border-width) border-border p-6\",\n // bg-card, not bg-background: a dialog is a raised surface, and card is\n // the role that already steps up the ink ramp on dark, where a shadow made\n // of ink is invisible. Its contrast pairs are in the contract already.\n \"bg-card text-card-foreground shadow-modal\",\n \"flex flex-col gap-4\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none:\n // content-box would add the 24px padding to max-w-lg, and an unstyled\n // document renders text in a serif. The Accordion shipped its panel that\n // way and the Alert overflowed by 46px.\n \"box-border font-sans\",\n // React Aria gives the dialog tabindex=-1 and focuses it when nothing\n // inside is autofocused, so it can receive a visible focus ring it does\n // not want.\n \"outline-none\",\n // A dialog taller than the viewport has to scroll, or its footer is\n // unreachable. max-h is on the panel rather than the overlay so the\n // padding above stays visible.\n \"max-h-full overflow-y-auto\",\n]);\n\nexport const dialogTitleVariants = cva([\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"text-lg leading-(--leading-tight) font-semibold\",\n // The user-agent <h2> ships a 1em block margin. The legacy was inside a\n // Preflight'd app and never met it.\n \"m-0\",\n]);\n\nexport const dialogDescriptionVariants = cva([\n \"text-sm leading-(--leading-normal) text-muted-foreground\",\n // Same: the user-agent <p> block margin.\n \"m-0\",\n]);\n\nexport const dialogFooterVariants = cva([\n // NOT flex-col-reverse. The legacy reversed the footer below `sm`, so a\n // DOM order of Cancel-then-Confirm rendered Confirm-then-Cancel on a\n // phone -- WCAG 1.3.2 Meaningful Sequence, and both alert-dialog call\n // sites hit it. Wrapping keeps narrow viewports working without lying\n // about the order.\n \"flex flex-wrap items-center justify-end gap-2\",\n]);\n\nconst dialogCloseVariants = cva([\n \"absolute end-4 top-4 inline-flex size-6 items-center justify-center\",\n \"rounded-2xs box-border cursor-pointer\",\n \"border-(length:--border-width) border-transparent bg-transparent\",\n \"text-muted-foreground\",\n // Fill on hover, a quick press to .96 and a spring release (see calendar-nav-motion).\n \"calendar-nav-motion\",\n // The legacy sat at opacity-70 until hover, dimming the only close\n // affordance below its already-marginal contrast. A token that is legible\n // at rest does the same job without that.\n \"data-[hovered]:bg-muted data-[hovered]:text-foreground\",\n // outline-solid is load-bearing: outline-none sets --tw-outline-style:none,\n // and the width utility resolves its style from that variable, so a focus\n // rule can apply a width and a colour and paint nothing. Button shipped\n // exactly that.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4\",\n]);\n\n/**\n * A modal dialog.\n *\n * @example\n * <Dialog isOpen={open} onOpenChange={setOpen} title=\"Delete user\"\n * description=\"This cannot be undone.\">\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n * </Dialog>\n */\n/** A string title or description swaps as a whole when its text changes while the dialog is open. */\nfunction Swappable({ value, children }: { value: ReactNode; children: ReactNode }) {\n if (typeof value !== \"string\") return <>{children}</>;\n return (\n <SwapText swapKey={value} className=\"block\">\n {children}\n </SwapText>\n );\n}\n\nexport interface DialogProps\n extends Omit<AriaModalOverlayProps, \"className\" | \"style\" | \"children\"> {\n /**\n * The dialog's heading, and its accessible name.\n *\n * **Required.** The legacy allowed a dialog with no title and one call\n * site shipped that, producing an `aria-labelledby` pointing at nothing.\n * Making it a required prop turns that into something you cannot express.\n */\n title: ReactNode;\n /**\n * Rendered ABOVE the heading, inside the panel.\n *\n * Exists for Sheet's back control. A panel that replaces its own contents\n * rather than stacking a second modal needs a way back, and that control\n * cannot live in `title`: React Aria wires aria-labelledby to the heading,\n * so a button inside it becomes part of the dialog's accessible name --\n * \"Back Campaign details\" instead of \"Campaign details\".\n */\n beforeTitle?: ReactNode;\n /** Optional supporting text, wired to `aria-describedby` when present. */\n description?: ReactNode;\n /** The dialog body. */\n children?: ReactNode;\n /** Extra classes for the panel. Merged last, so they win conflicts. */\n className?: string;\n /**\n * Extra classes for the scrim, merged last.\n *\n * A centred modal never needs this. A Sheet does: it has to replace the\n * overlay's centring with an edge alignment, and it is the only way to\n * do that without a second copy of the whole overlay.\n */\n overlayClassName?: string;\n /**\n * Whether to render the corner close button.\n *\n * `true` by default. An `alertdialog` sets it `false`: a confirmation must\n * be answered, not dismissed silently.\n */\n closeButton?: boolean;\n /**\n * Accessible name for the close button.\n *\n * A prop because the legacy hard-coded the English string `Close` in an\n * `sr-only` span with no way to override it, which makes the component\n * unlocalisable.\n *\n * @default \"Close\"\n */\n closeLabel?: string;\n /** `alertdialog` for a destructive confirmation. @default \"dialog\" */\n role?: \"dialog\" | \"alertdialog\";\n /**\n * Whether clicking outside closes the dialog.\n *\n * `true`, which is NOT React Aria's default. React Aria ships\n * `isDismissable = false` (useOverlay.mjs:26); Radix, which the legacy\n * component wraps, closes on outside click unless told otherwise. Taking\n * the headless default here would have silently changed the behaviour of\n * all seven legacy call sites, and nothing in the suite would have\n * noticed.\n *\n * An `alertdialog` should set this `false`: a confirmation must be\n * answered, not clicked away.\n *\n * @default true\n */\n isDismissable?: boolean;\n /**\n * How the dialog comes in: `rise` fades the overlay (blurring the page) and lifts\n * the panel in on a spring; `none` leaves the entrance to the caller, as an\n * edge-anchored preset (Sheet) does.\n *\n * @default \"rise\"\n */\n entrance?: \"rise\" | \"none\";\n}\n\nexport const Dialog = forwardRef<HTMLElement, DialogProps>(function Dialog(\n {\n title,\n beforeTitle,\n description,\n children,\n className,\n closeButton = true,\n closeLabel = \"Close\",\n role = \"dialog\",\n isDismissable = true,\n overlayClassName,\n entrance = \"rise\",\n ...props\n },\n ref,\n) {\n return (\n // ModalOverlay explicitly, not a bare <Modal>. A bare Modal still renders\n // an overlay div, but className lands on the INNER element -- so the\n // scrim would be unstyleable and would paint nothing at all.\n <AriaModalOverlay\n {...props}\n isDismissable={isDismissable}\n data-slot=\"dialog-overlay\"\n className={cnAll(\n dialogOverlayVariants(),\n entrance === \"rise\" && \"dialog-overlay-motion\",\n overlayClassName,\n )}\n >\n <AriaModal className=\"contents\">\n <AriaDialog\n // Modal is a transparent pass-through (className=\"contents\", no\n // box of its own) and ModalOverlay is the scrim -- AriaDialog is\n // the only one of the three that is the visible panel, so the ref\n // goes here and not on either ancestor.\n ref={ref}\n role={role}\n data-slot=\"dialog\"\n className={cnAll(dialogPanelVariants(), entrance === \"rise\" && \"dialog-panel-motion\", className)}\n >\n {/*\n Above the heading, not inside it. Sheet's back control lives here:\n folding it into `title` would put a button inside the <h2> React\n Aria wires aria-labelledby to, so the dialog's accessible name\n would become \"Back Campaign details\".\n */}\n {beforeTitle}\n {/*\n slot=\"title\" is what React Aria wires aria-labelledby to. A plain\n <h2> here would render identically and leave the dialog unnamed.\n */}\n <AriaHeading slot=\"title\" data-slot=\"dialog-title\" className={dialogTitleVariants()}>\n <Swappable value={title}>{title}</Swappable>\n </AriaHeading>\n {description == null ? null : (\n <AriaText\n slot=\"description\"\n data-slot=\"dialog-description\"\n className={dialogDescriptionVariants()}\n >\n <Swappable value={description}>{description}</Swappable>\n </AriaText>\n )}\n {children}\n {closeButton ? (\n // slot=\"close\" is React Aria's own dismiss wiring; an onPress\n // that called onOpenChange by hand would not work inside an\n // uncontrolled DialogTrigger.\n <AriaButton slot=\"close\" aria-label={closeLabel} className={dialogCloseVariants()}>\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n ) : null}\n </AriaDialog>\n </AriaModal>\n </AriaModalOverlay>\n );\n});\n\n/**\n * Actions at the foot of a dialog.\n *\n * @example\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n */\nexport interface DialogFooterProps {\n children?: ReactNode;\n className?: string;\n}\n\nexport const DialogFooter = forwardRef<HTMLDivElement, DialogFooterProps>(function DialogFooter(\n { children, className },\n ref,\n) {\n return (\n <div ref={ref} data-slot=\"dialog-footer\" className={cn(dialogFooterVariants(), className)}>\n {children}\n </div>\n );\n});\n\n/**\n * Pairs a trigger with a dialog and owns the open state.\n *\n * Re-exported from React Aria unchanged. Only 3 of the 9 legacy call sites\n * use a trigger at all -- the rest drive `isOpen` themselves -- so wrapping\n * it would add a layer with nothing to do.\n */\nexport { DialogTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,MAAa,wBAAwB,IAAI;CAIvC;CAGA;CACA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI;CAOrC;CACA;CAIA;CACA;CAKA;CAIA;CAIA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI,CAGrC,mDAGA,KACF,CAAC;AAED,MAAa,4BAA4B,IAAI,CAC3C,4DAEA,KACF,CAAC;AAED,MAAa,uBAAuB,IAAI,CAMtC,+CACF,CAAC;AAED,MAAM,sBAAsB,IAAI;CAC9B;CACA;CACA;CACA;CAEA;CAIA;CAKA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;;AAeD,SAAS,UAAU,EAAE,OAAO,YAAuD;CACjF,IAAI,OAAO,UAAU,UAAU,OAAO,oBAAA,YAAA,EAAG,SAAW,CAAA;CACpD,OACE,oBAAC,UAAD;EAAU,SAAS;EAAO,WAAU;EACjC;CACO,CAAA;AAEd;AAiFA,MAAaA,WAAS,WAAqC,SAASA,SAClE,EACE,OACA,aACA,aACA,UACA,WACA,cAAc,MACd,aAAa,SACb,OAAO,UACP,gBAAgB,MAChB,kBACA,WAAW,QACX,GAAG,SAEL,KACA;CACA,OAIE,oBAACC,cAAD;EACE,GAAI;EACW;EACf,aAAU;EACV,WAAW,MACT,sBAAsB,GACtB,aAAa,UAAU,yBACvB,gBACF;EAEA,UAAA,oBAACC,OAAD;GAAW,WAAU;GACnB,UAAA,qBAACC,QAAD;IAKO;IACC;IACN,aAAU;IACV,WAAW,MAAM,oBAAoB,GAAG,aAAa,UAAU,uBAAuB,SAAS;IARjG,UAAA;KAgBG;KAKD,oBAACC,SAAD;MAAa,MAAK;MAAQ,aAAU;MAAe,WAAW,oBAAoB;MAChF,UAAA,oBAAC,WAAD;OAAW,OAAO;OAAQ,UAAA;MAAiB,CAAA;KAChC,CAAA;KACZ,eAAe,OAAO,OACrB,oBAACC,MAAD;MACE,MAAK;MACL,aAAU;MACV,WAAW,0BAA0B;MAErC,UAAA,oBAAC,WAAD;OAAW,OAAO;OAAc,UAAA;MAAuB,CAAA;KAC/C,CAAA;KAEX;KACA,cAIC,oBAACC,QAAD;MAAY,MAAK;MAAQ,cAAY;MAAY,WAAW,oBAAoB;MAC9E,UAAA,oBAAC,OAAD;OACE,eAAY;OACZ,SAAQ;OACR,MAAK;OACL,QAAO;OACP,aAAY;OACZ,eAAc;OAEd,UAAA,oBAAC,QAAD,EAAM,GAAE,qBAAsB,CAAA;MAC3B,CAAA;KACK,CAAA,IACV;IACM;;EACH,CAAA;CACK,CAAA;AAEtB,CAAC;AAgBD,MAAa,eAAe,WAA8C,SAAS,aACjF,EAAE,UAAU,aACZ,KACA;CACA,OACE,oBAAC,OAAD;EAAU;EAAK,aAAU;EAAgB,WAAW,GAAG,qBAAqB,GAAG,SAAS;EACrF;CACE,CAAA;AAET,CAAC"}
|
|
1
|
+
{"version":3,"file":"dialog.js","names":["Dialog","AriaModalOverlay","AriaModal","AriaDialog","AriaHeading","AriaText","AriaButton"],"sources":["../../src/dialog/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Dialog as AriaDialog,\n Heading as AriaHeading,\n Modal as AriaModal,\n ModalOverlay as AriaModalOverlay,\n Text as AriaText,\n type ModalOverlayProps as AriaModalOverlayProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Dialog, ported from the legacy shadcn/Radix component onto React Aria's\n * Modal primitives.\n *\n * FOUR LEGACY PARTS COLLAPSE INTO TWO PROPS. The census (9 importing files,\n * but only **7** that render anything -- header.tsx and PollQuizCreator.tsx\n * import the parts and use none) reads: DialogContent 9, DialogTitle 8,\n * DialogHeader 7, DialogDescription 2. Those four always appear in the same\n * shape, so `title` and `description` are props of the dialog itself.\n *\n * That is not tidying. `title` is REQUIRED, which makes an unnamed dialog\n * impossible to write -- and the legacy shipped one: LiveQAOverlay.tsx\n * renders a DialogContent with no DialogTitle, so its `aria-labelledby`\n * points at an id that matches no element and the dialog is announced with\n * no name.\n *\n * It also fixes the dangling-description defect for free. Radix set\n * `aria-describedby` unconditionally from a `useId`, so seven of the nine\n * legacy dialogs referenced an id that did not exist. React Aria wires it\n * only when a description slot is actually rendered.\n *\n * `DialogPortal` and `DialogOverlay` are not ported: zero external call\n * sites, and React Aria's ModalOverlay does both jobs.\n *\n * NOT PORTED, and deliberately: the enter/exit animation. The legacy leans\n * on tailwindcss-animate's `animate-in` / `zoom-in-95` utilities, which this\n * repo does not ship, and the motion tokens are a separate task. React Aria\n * exposes `data-entering` / `data-exiting` for whoever adds them.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const dialogOverlayVariants = cva([\n // Centring by flex, not by `top-1/2 left-1/2 -translate-1/2`. The legacy's\n // translate pair is a bracket value that check:hardcoding rejects, and\n // flex centring needs no transform to undo.\n \"fixed inset-0 z-50 flex items-center justify-center\",\n // The inset that keeps a full-width panel off the viewport edge. The\n // legacy did this with max-w-[calc(100%-2rem)] -- also a bracket value.\n \"p-4\",\n \"bg-scrim\",\n]);\n\nexport const dialogPanelVariants = cva([\n // max-w-128 (32rem), NOT max-w-lg. `--container-lg` does not exist in this\n // repo -- the container scale holds only page-shell widths (max 75rem,\n // wide 90rem) -- so `max-w-lg` compiles to NOTHING and the panel spans the\n // viewport. It emitted no rule at all, tsc and ESLint were both blind to\n // it, and a screenshot is what caught it. 128 spacing units is 32rem,\n // exactly the legacy sm:max-w-lg.\n \"relative w-full max-w-128\",\n \"rounded-panel border-(length:--border-width) border-border p-6\",\n // bg-card, not bg-background: a dialog is a raised surface, and card is\n // the role that already steps up the ink ramp on dark, where a shadow made\n // of ink is invisible. Its contrast pairs are in the contract already.\n \"bg-card text-card-foreground shadow-modal\",\n \"flex flex-col gap-4\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none:\n // content-box would add the 24px padding to max-w-lg, and an unstyled\n // document renders text in a serif. The Accordion shipped its panel that\n // way and the Alert overflowed by 46px.\n \"box-border font-sans\",\n // React Aria gives the dialog tabindex=-1 and focuses it when nothing\n // inside is autofocused, so it can receive a visible focus ring it does\n // not want.\n \"outline-none\",\n // A dialog taller than the viewport has to scroll, or its footer is\n // unreachable. max-h is on the panel rather than the overlay so the\n // padding above stays visible.\n \"max-h-full overflow-y-auto\",\n]);\n\nexport const dialogTitleVariants = cva([\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"font-display text-lg leading-(--leading-tight) font-semibold\",\n // The user-agent <h2> ships a 1em block margin. The legacy was inside a\n // Preflight'd app and never met it.\n \"m-0\",\n]);\n\nexport const dialogDescriptionVariants = cva([\n \"text-sm leading-(--leading-normal) text-muted-foreground\",\n // Same: the user-agent <p> block margin.\n \"m-0\",\n]);\n\nexport const dialogFooterVariants = cva([\n // NOT flex-col-reverse. The legacy reversed the footer below `sm`, so a\n // DOM order of Cancel-then-Confirm rendered Confirm-then-Cancel on a\n // phone -- WCAG 1.3.2 Meaningful Sequence, and both alert-dialog call\n // sites hit it. Wrapping keeps narrow viewports working without lying\n // about the order.\n \"flex flex-wrap items-center justify-end gap-2\",\n]);\n\nconst dialogCloseVariants = cva([\n \"absolute end-4 top-4 inline-flex size-6 items-center justify-center\",\n \"rounded-2xs box-border cursor-pointer\",\n \"border-(length:--border-width) border-transparent bg-transparent\",\n \"text-muted-foreground\",\n // Fill on hover, a quick press to .96 and a spring release (see calendar-nav-motion).\n \"calendar-nav-motion\",\n // The legacy sat at opacity-70 until hover, dimming the only close\n // affordance below its already-marginal contrast. A token that is legible\n // at rest does the same job without that.\n \"data-[hovered]:bg-muted data-[hovered]:text-foreground\",\n // outline-solid is load-bearing: outline-none sets --tw-outline-style:none,\n // and the width utility resolves its style from that variable, so a focus\n // rule can apply a width and a colour and paint nothing. Button shipped\n // exactly that.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4\",\n]);\n\n/**\n * A modal dialog.\n *\n * @example\n * <Dialog isOpen={open} onOpenChange={setOpen} title=\"Delete user\"\n * description=\"This cannot be undone.\">\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n * </Dialog>\n */\n/** A string title or description swaps as a whole when its text changes while the dialog is open. */\nfunction Swappable({ value, children }: { value: ReactNode; children: ReactNode }) {\n if (typeof value !== \"string\") return <>{children}</>;\n return (\n <SwapText swapKey={value} className=\"block\">\n {children}\n </SwapText>\n );\n}\n\nexport interface DialogProps\n extends Omit<AriaModalOverlayProps, \"className\" | \"style\" | \"children\"> {\n /**\n * The dialog's heading, and its accessible name.\n *\n * **Required.** The legacy allowed a dialog with no title and one call\n * site shipped that, producing an `aria-labelledby` pointing at nothing.\n * Making it a required prop turns that into something you cannot express.\n */\n title: ReactNode;\n /**\n * Rendered ABOVE the heading, inside the panel.\n *\n * Exists for Sheet's back control. A panel that replaces its own contents\n * rather than stacking a second modal needs a way back, and that control\n * cannot live in `title`: React Aria wires aria-labelledby to the heading,\n * so a button inside it becomes part of the dialog's accessible name --\n * \"Back Campaign details\" instead of \"Campaign details\".\n */\n beforeTitle?: ReactNode;\n /** Optional supporting text, wired to `aria-describedby` when present. */\n description?: ReactNode;\n /** The dialog body. */\n children?: ReactNode;\n /** Extra classes for the panel. Merged last, so they win conflicts. */\n className?: string;\n /**\n * Extra classes for the scrim, merged last.\n *\n * A centred modal never needs this. A Sheet does: it has to replace the\n * overlay's centring with an edge alignment, and it is the only way to\n * do that without a second copy of the whole overlay.\n */\n overlayClassName?: string;\n /**\n * Whether to render the corner close button.\n *\n * `true` by default. An `alertdialog` sets it `false`: a confirmation must\n * be answered, not dismissed silently.\n */\n closeButton?: boolean;\n /**\n * Accessible name for the close button.\n *\n * A prop because the legacy hard-coded the English string `Close` in an\n * `sr-only` span with no way to override it, which makes the component\n * unlocalisable.\n *\n * @default \"Close\"\n */\n closeLabel?: string;\n /** `alertdialog` for a destructive confirmation. @default \"dialog\" */\n role?: \"dialog\" | \"alertdialog\";\n /**\n * Whether clicking outside closes the dialog.\n *\n * `true`, which is NOT React Aria's default. React Aria ships\n * `isDismissable = false` (useOverlay.mjs:26); Radix, which the legacy\n * component wraps, closes on outside click unless told otherwise. Taking\n * the headless default here would have silently changed the behaviour of\n * all seven legacy call sites, and nothing in the suite would have\n * noticed.\n *\n * An `alertdialog` should set this `false`: a confirmation must be\n * answered, not clicked away.\n *\n * @default true\n */\n isDismissable?: boolean;\n /**\n * How the dialog comes in: `rise` fades the overlay (blurring the page) and lifts\n * the panel in on a spring; `none` leaves the entrance to the caller, as an\n * edge-anchored preset (Sheet) does.\n *\n * @default \"rise\"\n */\n entrance?: \"rise\" | \"none\";\n}\n\nexport const Dialog = forwardRef<HTMLElement, DialogProps>(function Dialog(\n {\n title,\n beforeTitle,\n description,\n children,\n className,\n closeButton = true,\n closeLabel = \"Close\",\n role = \"dialog\",\n isDismissable = true,\n overlayClassName,\n entrance = \"rise\",\n ...props\n },\n ref,\n) {\n return (\n // ModalOverlay explicitly, not a bare <Modal>. A bare Modal still renders\n // an overlay div, but className lands on the INNER element -- so the\n // scrim would be unstyleable and would paint nothing at all.\n <AriaModalOverlay\n {...props}\n isDismissable={isDismissable}\n data-slot=\"dialog-overlay\"\n className={cnAll(\n dialogOverlayVariants(),\n entrance === \"rise\" && \"dialog-overlay-motion\",\n overlayClassName,\n )}\n >\n <AriaModal className=\"contents\">\n <AriaDialog\n // Modal is a transparent pass-through (className=\"contents\", no\n // box of its own) and ModalOverlay is the scrim -- AriaDialog is\n // the only one of the three that is the visible panel, so the ref\n // goes here and not on either ancestor.\n ref={ref}\n role={role}\n data-slot=\"dialog\"\n className={cnAll(dialogPanelVariants(), entrance === \"rise\" && \"dialog-panel-motion\", className)}\n >\n {/*\n Above the heading, not inside it. Sheet's back control lives here:\n folding it into `title` would put a button inside the <h2> React\n Aria wires aria-labelledby to, so the dialog's accessible name\n would become \"Back Campaign details\".\n */}\n {beforeTitle}\n {/*\n slot=\"title\" is what React Aria wires aria-labelledby to. A plain\n <h2> here would render identically and leave the dialog unnamed.\n */}\n <AriaHeading slot=\"title\" data-slot=\"dialog-title\" className={dialogTitleVariants()}>\n <Swappable value={title}>{title}</Swappable>\n </AriaHeading>\n {description == null ? null : (\n <AriaText\n slot=\"description\"\n data-slot=\"dialog-description\"\n className={dialogDescriptionVariants()}\n >\n <Swappable value={description}>{description}</Swappable>\n </AriaText>\n )}\n {children}\n {closeButton ? (\n // slot=\"close\" is React Aria's own dismiss wiring; an onPress\n // that called onOpenChange by hand would not work inside an\n // uncontrolled DialogTrigger.\n <AriaButton slot=\"close\" aria-label={closeLabel} className={dialogCloseVariants()}>\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n ) : null}\n </AriaDialog>\n </AriaModal>\n </AriaModalOverlay>\n );\n});\n\n/**\n * Actions at the foot of a dialog.\n *\n * @example\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n */\nexport interface DialogFooterProps {\n children?: ReactNode;\n className?: string;\n}\n\nexport const DialogFooter = forwardRef<HTMLDivElement, DialogFooterProps>(function DialogFooter(\n { children, className },\n ref,\n) {\n return (\n <div ref={ref} data-slot=\"dialog-footer\" className={cn(dialogFooterVariants(), className)}>\n {children}\n </div>\n );\n});\n\n/**\n * Pairs a trigger with a dialog and owns the open state.\n *\n * Re-exported from React Aria unchanged. Only 3 of the 9 legacy call sites\n * use a trigger at all -- the rest drive `isOpen` themselves -- so wrapping\n * it would add a layer with nothing to do.\n */\nexport { DialogTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,MAAa,wBAAwB,IAAI;CAIvC;CAGA;CACA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI;CAOrC;CACA;CAIA;CACA;CAKA;CAIA;CAIA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI,CAGrC,gEAGA,KACF,CAAC;AAED,MAAa,4BAA4B,IAAI,CAC3C,4DAEA,KACF,CAAC;AAED,MAAa,uBAAuB,IAAI,CAMtC,+CACF,CAAC;AAED,MAAM,sBAAsB,IAAI;CAC9B;CACA;CACA;CACA;CAEA;CAIA;CAKA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;;AAeD,SAAS,UAAU,EAAE,OAAO,YAAuD;CACjF,IAAI,OAAO,UAAU,UAAU,OAAO,oBAAA,YAAA,EAAG,SAAW,CAAA;CACpD,OACE,oBAAC,UAAD;EAAU,SAAS;EAAO,WAAU;EACjC;CACO,CAAA;AAEd;AAiFA,MAAaA,WAAS,WAAqC,SAASA,SAClE,EACE,OACA,aACA,aACA,UACA,WACA,cAAc,MACd,aAAa,SACb,OAAO,UACP,gBAAgB,MAChB,kBACA,WAAW,QACX,GAAG,SAEL,KACA;CACA,OAIE,oBAACC,cAAD;EACE,GAAI;EACW;EACf,aAAU;EACV,WAAW,MACT,sBAAsB,GACtB,aAAa,UAAU,yBACvB,gBACF;EAEA,UAAA,oBAACC,OAAD;GAAW,WAAU;GACnB,UAAA,qBAACC,QAAD;IAKO;IACC;IACN,aAAU;IACV,WAAW,MAAM,oBAAoB,GAAG,aAAa,UAAU,uBAAuB,SAAS;IARjG,UAAA;KAgBG;KAKD,oBAACC,SAAD;MAAa,MAAK;MAAQ,aAAU;MAAe,WAAW,oBAAoB;MAChF,UAAA,oBAAC,WAAD;OAAW,OAAO;OAAQ,UAAA;MAAiB,CAAA;KAChC,CAAA;KACZ,eAAe,OAAO,OACrB,oBAACC,MAAD;MACE,MAAK;MACL,aAAU;MACV,WAAW,0BAA0B;MAErC,UAAA,oBAAC,WAAD;OAAW,OAAO;OAAc,UAAA;MAAuB,CAAA;KAC/C,CAAA;KAEX;KACA,cAIC,oBAACC,QAAD;MAAY,MAAK;MAAQ,cAAY;MAAY,WAAW,oBAAoB;MAC9E,UAAA,oBAAC,OAAD;OACE,eAAY;OACZ,SAAQ;OACR,MAAK;OACL,QAAO;OACP,aAAY;OACZ,eAAc;OAEd,UAAA,oBAAC,QAAD,EAAM,GAAE,qBAAsB,CAAA;MAC3B,CAAA;KACK,CAAA,IACV;IACM;;EACH,CAAA;CACK,CAAA;AAEtB,CAAC;AAgBD,MAAa,eAAe,WAA8C,SAAS,aACjF,EAAE,UAAU,aACZ,KACA;CACA,OACE,oBAAC,OAAD;EAAU;EAAK,aAAU;EAAgB,WAAW,GAAG,qBAAqB,GAAG,SAAS;EACrF;CACE,CAAA;AAET,CAAC"}
|
|
@@ -68,6 +68,23 @@ export interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
68
68
|
image?: ReactNode;
|
|
69
69
|
/** A next step under the text: a button or a link. Omitted from the DOM when absent. */
|
|
70
70
|
action?: ReactNode;
|
|
71
|
+
/**
|
|
72
|
+
* Draws the empty state as a block of its own. `dashed` gives it a dashed
|
|
73
|
+
* border, a rounded corner and the muted surface, for a placeholder where a
|
|
74
|
+
* chart or a panel will go. `none` is the bare box, as before.
|
|
75
|
+
*
|
|
76
|
+
* @default "none"
|
|
77
|
+
*/
|
|
78
|
+
frame?: "none" | "dashed";
|
|
79
|
+
/**
|
|
80
|
+
* Where the content sits. `center` is the page-level empty state, as before.
|
|
81
|
+
* `start` aligns icon, text and action to the leading edge, for a block in a
|
|
82
|
+
* layout rather than a whole page; with it, and no icon or image, the title
|
|
83
|
+
* also drops its top margin.
|
|
84
|
+
*
|
|
85
|
+
* @default "center"
|
|
86
|
+
*/
|
|
87
|
+
align?: "center" | "start";
|
|
71
88
|
/** Extra classes for the box. Merged last, so they win conflicts with ours. */
|
|
72
89
|
className?: string;
|
|
73
90
|
}
|
|
@@ -21,7 +21,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
21
21
|
* simply never be emitted.
|
|
22
22
|
*/
|
|
23
23
|
const emptyStateVariants = cva(["flex flex-col items-center px-5 py-12 text-center", "box-border font-sans text-foreground"]);
|
|
24
|
-
const emptyStateTitleVariants = cva("m-0 mt-5 text-base leading-(--leading-normal) font-medium text-foreground");
|
|
24
|
+
const emptyStateTitleVariants = cva("m-0 mt-5 font-display text-base leading-(--leading-normal) font-medium text-foreground");
|
|
25
25
|
const emptyStateTextVariants = cva("mx-auto mt-0 mb-0 max-w-72 text-sm leading-(--leading-normal) text-pretty text-text-secondary");
|
|
26
26
|
const emptyStateActionVariants = cva("mt-5 flex flex-wrap justify-center gap-3");
|
|
27
27
|
/**
|
|
@@ -36,7 +36,7 @@ function iconKey(icon) {
|
|
|
36
36
|
const type = icon.type;
|
|
37
37
|
return typeof type === "string" ? type : type.displayName ?? type.name ?? "icon";
|
|
38
38
|
}
|
|
39
|
-
const EmptyState = forwardRef(function EmptyState({ title, description, children, icon, image, action, className, ...props }, ref) {
|
|
39
|
+
const EmptyState = forwardRef(function EmptyState({ title, description, children, icon, image, action, frame: frameStyle = "none", align = "center", className, ...props }, ref) {
|
|
40
40
|
const frame = useRef(null);
|
|
41
41
|
const copy = useRef(null);
|
|
42
42
|
const message = description ?? children;
|
|
@@ -51,11 +51,13 @@ const EmptyState = forwardRef(function EmptyState({ title, description, children
|
|
|
51
51
|
ref,
|
|
52
52
|
...props,
|
|
53
53
|
"data-slot": "empty-state",
|
|
54
|
-
|
|
54
|
+
"data-frame": frameStyle === "none" ? void 0 : frameStyle,
|
|
55
|
+
"data-align": align === "center" ? void 0 : align,
|
|
56
|
+
className: cn(cnAll(emptyStateVariants(), frameStyle === "dashed" && "rounded-panel border-(length:--border-width) border-dashed border-border bg-muted", align === "start" && "items-start text-start"), className),
|
|
55
57
|
children: [
|
|
56
58
|
!hasImage ? null : /* @__PURE__ */ jsx("div", {
|
|
57
59
|
"data-slot": "empty-state-image",
|
|
58
|
-
className: emptyStateImageVariants(),
|
|
60
|
+
className: cnAll(emptyStateImageVariants(), align === "start" && "justify-start"),
|
|
59
61
|
children: image
|
|
60
62
|
}),
|
|
61
63
|
icon === void 0 || icon === null ? null : /* @__PURE__ */ jsx("div", {
|
|
@@ -78,18 +80,18 @@ const EmptyState = forwardRef(function EmptyState({ title, description, children
|
|
|
78
80
|
className: "flow-root",
|
|
79
81
|
children: [title == null ? null : /* @__PURE__ */ jsx("h3", {
|
|
80
82
|
"data-slot": "empty-state-title",
|
|
81
|
-
className: emptyStateTitleVariants(),
|
|
83
|
+
className: cnAll(emptyStateTitleVariants(), align === "start" && icon == null && !hasImage && "mt-0"),
|
|
82
84
|
children: text(title)
|
|
83
85
|
}), message == null ? null : /* @__PURE__ */ jsx("p", {
|
|
84
86
|
"data-slot": "empty-state-text",
|
|
85
|
-
className: cnAll(emptyStateTextVariants(), title != null ? "mt-2" : (icon != null || hasImage) && "mt-5"),
|
|
87
|
+
className: cnAll(emptyStateTextVariants(), align === "start" && "mx-0", title != null ? "mt-2" : (icon != null || hasImage) && "mt-5"),
|
|
86
88
|
children: text(message)
|
|
87
89
|
})]
|
|
88
90
|
})
|
|
89
91
|
}),
|
|
90
92
|
action == null ? null : /* @__PURE__ */ jsx("div", {
|
|
91
93
|
"data-slot": "empty-state-action",
|
|
92
|
-
className: emptyStateActionVariants(),
|
|
94
|
+
className: cnAll(emptyStateActionVariants(), align === "start" && "justify-start"),
|
|
93
95
|
children: action
|
|
94
96
|
})
|
|
95
97
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"empty-state.js","names":[],"sources":["../../src/empty-state/empty-state.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n forwardRef,\n isValidElement,\n useRef,\n type ComponentPropsWithoutRef,\n type ReactNode,\n} from \"react\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { useHeightFollow } from \"../lib/height-follow.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * EmptyState: what a list shows when it has nothing to show. An icon in a\n * circle, a short title, a one- or two-line description and an optional next\n * step, centred on the page surface with room to breathe.\n *\n * Presentational, so there is no React Aria base. It exists because every\n * list has an empty case, and without a shared component each screen picks its\n * own padding, border and alignment, so \"nothing here\" looks different on\n * every page.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const emptyStateVariants = cva([\n \"flex flex-col items-center px-5 py-12 text-center\",\n // The reset in @seekalfred/styles is opt-in, so a consumer may load none.\n // Its own text colour too, so it reads the same on any page background.\n \"box-border font-sans text-foreground\",\n]);\n\nexport const emptyStateTitleVariants = cva(\n \"m-0 mt-5 text-base leading-(--leading-normal) font-medium text-foreground\",\n);\n\nexport const emptyStateTextVariants = cva(\n \"mx-auto mt-0 mb-0 max-w-72 text-sm leading-(--leading-normal) text-pretty text-text-secondary\",\n);\n\nexport const emptyStateActionVariants = cva(\"mt-5 flex flex-wrap justify-center gap-3\");\n\n/**\n * The illustration's frame. It centres what it is given and keeps an image or\n * SVG inside the box at its own aspect ratio; the illustration's SIZE is the\n * caller's, because the artwork sets it, not the empty state.\n */\nexport const emptyStateImageVariants = cva(\n \"flex max-w-full justify-center [&>img]:h-auto [&>img]:max-w-full [&>svg]:h-auto [&>svg]:max-w-full\",\n);\n\n/**\n * An empty list's message.\n *\n * @example\n * <EmptyState\n * icon={<Folder aria-hidden=\"true\" />}\n * title=\"No saved views\"\n * description=\"Save a set of filters as a view to come back to it later.\"\n * action={<Button>Create view</Button>}\n * />\n */\nexport interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\"> {\n /**\n * A short heading above the description. When it is set, `description` (or\n * `children`) is the supporting text under it.\n */\n title?: ReactNode;\n /** One or two lines of supporting text. `children` is the same slot and works the same way. */\n description?: ReactNode;\n /** Why the list is empty, in one short statement. The message when there is no `description`. */\n children?: ReactNode;\n /**\n * A glyph above the message, inside a circle. It settles in once when the state first appears\n * (small and transparent to rest over 480ms); a different icon component later\n * swaps in place, and the same one re-rendering stays still. Mark it `aria-hidden`.\n */\n icon?: ReactNode;\n /**\n * An illustration above the icon and the title: an `<img>`, an inline SVG or any\n * element. It is the caller's artwork, so the empty state does not hide it from\n * assistive technology, and the caller names it:\n * - an `<img>` takes an `alt`, or `alt=\"\"` when it only decorates what the title\n * already says;\n * - an inline `<svg>` has no name of its own, so give it `role=\"img\"` and an\n * `aria-label`, or `aria-hidden=\"true\"` when it is decorative.\n *\n * The slot reserves no space, so give the artwork its intrinsic size (`width` and\n * `height` on an `<img>` or `<svg>`): without it the layout jumps when an image\n * loads. Omitted from the DOM when absent; `false` (as in `cond && <img />`)\n * counts as absent.\n */\n image?: ReactNode;\n /** A next step under the text: a button or a link. Omitted from the DOM when absent. */\n action?: ReactNode;\n /** Extra classes for the box. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\n/** The name an icon swaps under: a different component swaps; the same one re-rendering does not. */\nfunction iconKey(icon: ReactNode) {\n if (!isValidElement(icon)) return \"icon\";\n const type = icon.type as string | { displayName?: string; name?: string };\n return typeof type === \"string\" ? type : (type.displayName ?? type.name ?? \"icon\");\n}\n\nexport const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(function EmptyState(\n { title, description, children, icon, image, action, className, ...props },\n ref,\n) {\n const frame = useRef<HTMLDivElement>(null);\n const copy = useRef<HTMLDivElement>(null);\n const message = description ?? children;\n // `cond && <img />` hands over `false` when the condition fails: absent, not\n // an empty slot with its spacing.\n const hasImage = image != null && typeof image !== \"boolean\";\n const text = (value: ReactNode) =>\n typeof value === \"string\" ? (\n <SwapText swapKey={value} className=\"block\">\n {value}\n </SwapText>\n ) : (\n value\n );\n // The copy column follows its height when the title or the message changes.\n useHeightFollow(\n frame,\n copy,\n `${typeof title === \"string\" ? title : \"\"}\\n${typeof message === \"string\" ? message : \"\"}`,\n );\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"empty-state\"\n className={cn(emptyStateVariants(), className)}\n >\n {!hasImage ? null : (\n <div data-slot=\"empty-state-image\" className={emptyStateImageVariants()}>\n {image}\n </div>\n )}\n {icon === undefined || icon === null ? null : (\n <div\n data-slot=\"empty-state-icon\"\n aria-hidden=\"true\"\n className={cnAll(\n \"grid size-12 place-items-center rounded-circle border-(length:--border-width) border-border bg-muted text-text-secondary motion-safe:animate-empty-state-icon [&_svg]:size-6\",\n // Under an illustration it clears it by the same step as the title.\n hasImage && \"mt-5\",\n )}\n >\n <SwapText swapKey={iconKey(icon)} motion=\"glyph\" className=\"grid place-items-center\">\n {icon}\n </SwapText>\n </div>\n )}\n <div ref={frame} data-slot=\"empty-state-copy\" className=\"height-follow w-full\">\n <div ref={copy} className=\"flow-root\">\n {title == null ? null : (\n <h3 data-slot=\"empty-state-title\" className={emptyStateTitleVariants()}>\n {text(title)}\n </h3>\n )}\n {message == null ? null : (\n <p\n data-slot=\"empty-state-text\"\n className={cnAll(\n emptyStateTextVariants(),\n // Under the title it sits close; straight under the icon or the\n // illustration it clears it.\n title != null ? \"mt-2\" : (icon != null || hasImage) && \"mt-5\",\n )}\n >\n {text(message)}\n </p>\n )}\n </div>\n </div>\n {action == null ? null : (\n <div data-slot=\"empty-state-action\" className={emptyStateActionVariants()}>\n {action}\n </div>\n )}\n </div>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,qBAAqB,IAAI,CACpC,qDAGA,sCACF,CAAC;AAED,MAAa,0BAA0B,IACrC,2EACF;AAEA,MAAa,yBAAyB,IACpC,+FACF;AAEA,MAAa,2BAA2B,IAAI,0CAA0C;;;;;;AAOtF,MAAa,0BAA0B,IACrC,oGACF;;AAmDA,SAAS,QAAQ,MAAiB;CAChC,IAAI,CAAC,eAAe,IAAI,GAAG,OAAO;CAClC,MAAM,OAAO,KAAK;CAClB,OAAO,OAAO,SAAS,WAAW,OAAQ,KAAK,eAAe,KAAK,QAAQ;AAC7E;AAEA,MAAa,aAAa,WAA4C,SAAS,WAC7E,EAAE,OAAO,aAAa,UAAU,MAAM,OAAO,QAAQ,WAAW,GAAG,SACnE,KACA;CACA,MAAM,QAAQ,OAAuB,IAAI;CACzC,MAAM,OAAO,OAAuB,IAAI;CACxC,MAAM,UAAU,eAAe;CAG/B,MAAM,WAAW,SAAS,QAAQ,OAAO,UAAU;CACnD,MAAM,QAAQ,UACZ,OAAO,UAAU,WACf,oBAAC,UAAD;EAAU,SAAS;EAAO,WAAU;EACjC,UAAA;CACO,CAAA,IAEV;CAGJ,gBACE,OACA,MACA,GAAG,OAAO,UAAU,WAAW,QAAQ,GAAG,IAAI,OAAO,YAAY,WAAW,UAAU,IACxF;CACA,OACE,qBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;EAJ/C,UAAA;GAMG,CAAC,WAAW,OACX,oBAAC,OAAD;IAAK,aAAU;IAAoB,WAAW,wBAAwB;IACnE,UAAA;GACE,CAAA;GAEN,SAAS,KAAA,KAAa,SAAS,OAAO,OACrC,oBAAC,OAAD;IACE,aAAU;IACV,eAAY;IACZ,WAAW,MACT,gLAEA,YAAY,MACd;IAEA,UAAA,oBAAC,UAAD;KAAU,SAAS,QAAQ,IAAI;KAAG,QAAO;KAAQ,WAAU;KACxD,UAAA;IACO,CAAA;GACP,CAAA;GAEP,oBAAC,OAAD;IAAK,KAAK;IAAO,aAAU;IAAmB,WAAU;IACtD,UAAA,qBAAC,OAAD;KAAK,KAAK;KAAM,WAAU;KAA1B,UAAA,CACG,SAAS,OAAO,OACf,oBAAC,MAAD;MAAI,aAAU;MAAoB,WAAW,wBAAwB;MAClE,UAAA,KAAK,KAAK;KACT,CAAA,GAEL,WAAW,OAAO,OACjB,oBAAC,KAAD;MACE,aAAU;MACV,WAAW,MACT,uBAAuB,GAGvB,SAAS,OAAO,UAAU,QAAQ,QAAQ,aAAa,MACzD;MAEC,UAAA,KAAK,OAAO;KACZ,CAAA,CAEF;;GACF,CAAA;GACJ,UAAU,OAAO,OAChB,oBAAC,OAAD;IAAK,aAAU;IAAqB,WAAW,yBAAyB;IACrE,UAAA;GACE,CAAA;EAEJ;;AAET,CAAC"}
|
|
1
|
+
{"version":3,"file":"empty-state.js","names":[],"sources":["../../src/empty-state/empty-state.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n forwardRef,\n isValidElement,\n useRef,\n type ComponentPropsWithoutRef,\n type ReactNode,\n} from \"react\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { useHeightFollow } from \"../lib/height-follow.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * EmptyState: what a list shows when it has nothing to show. An icon in a\n * circle, a short title, a one- or two-line description and an optional next\n * step, centred on the page surface with room to breathe.\n *\n * Presentational, so there is no React Aria base. It exists because every\n * list has an empty case, and without a shared component each screen picks its\n * own padding, border and alignment, so \"nothing here\" looks different on\n * every page.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const emptyStateVariants = cva([\n \"flex flex-col items-center px-5 py-12 text-center\",\n // The reset in @seekalfred/styles is opt-in, so a consumer may load none.\n // Its own text colour too, so it reads the same on any page background.\n \"box-border font-sans text-foreground\",\n]);\n\nexport const emptyStateTitleVariants = cva(\n \"m-0 mt-5 font-display text-base leading-(--leading-normal) font-medium text-foreground\",\n);\n\nexport const emptyStateTextVariants = cva(\n \"mx-auto mt-0 mb-0 max-w-72 text-sm leading-(--leading-normal) text-pretty text-text-secondary\",\n);\n\nexport const emptyStateActionVariants = cva(\"mt-5 flex flex-wrap justify-center gap-3\");\n\n/**\n * The illustration's frame. It centres what it is given and keeps an image or\n * SVG inside the box at its own aspect ratio; the illustration's SIZE is the\n * caller's, because the artwork sets it, not the empty state.\n */\nexport const emptyStateImageVariants = cva(\n \"flex max-w-full justify-center [&>img]:h-auto [&>img]:max-w-full [&>svg]:h-auto [&>svg]:max-w-full\",\n);\n\n/**\n * An empty list's message.\n *\n * @example\n * <EmptyState\n * icon={<Folder aria-hidden=\"true\" />}\n * title=\"No saved views\"\n * description=\"Save a set of filters as a view to come back to it later.\"\n * action={<Button>Create view</Button>}\n * />\n */\nexport interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\"> {\n /**\n * A short heading above the description. When it is set, `description` (or\n * `children`) is the supporting text under it.\n */\n title?: ReactNode;\n /** One or two lines of supporting text. `children` is the same slot and works the same way. */\n description?: ReactNode;\n /** Why the list is empty, in one short statement. The message when there is no `description`. */\n children?: ReactNode;\n /**\n * A glyph above the message, inside a circle. It settles in once when the state first appears\n * (small and transparent to rest over 480ms); a different icon component later\n * swaps in place, and the same one re-rendering stays still. Mark it `aria-hidden`.\n */\n icon?: ReactNode;\n /**\n * An illustration above the icon and the title: an `<img>`, an inline SVG or any\n * element. It is the caller's artwork, so the empty state does not hide it from\n * assistive technology, and the caller names it:\n * - an `<img>` takes an `alt`, or `alt=\"\"` when it only decorates what the title\n * already says;\n * - an inline `<svg>` has no name of its own, so give it `role=\"img\"` and an\n * `aria-label`, or `aria-hidden=\"true\"` when it is decorative.\n *\n * The slot reserves no space, so give the artwork its intrinsic size (`width` and\n * `height` on an `<img>` or `<svg>`): without it the layout jumps when an image\n * loads. Omitted from the DOM when absent; `false` (as in `cond && <img />`)\n * counts as absent.\n */\n image?: ReactNode;\n /** A next step under the text: a button or a link. Omitted from the DOM when absent. */\n action?: ReactNode;\n /**\n * Draws the empty state as a block of its own. `dashed` gives it a dashed\n * border, a rounded corner and the muted surface, for a placeholder where a\n * chart or a panel will go. `none` is the bare box, as before.\n *\n * @default \"none\"\n */\n frame?: \"none\" | \"dashed\";\n /**\n * Where the content sits. `center` is the page-level empty state, as before.\n * `start` aligns icon, text and action to the leading edge, for a block in a\n * layout rather than a whole page; with it, and no icon or image, the title\n * also drops its top margin.\n *\n * @default \"center\"\n */\n align?: \"center\" | \"start\";\n /** Extra classes for the box. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\n/** The name an icon swaps under: a different component swaps; the same one re-rendering does not. */\nfunction iconKey(icon: ReactNode) {\n if (!isValidElement(icon)) return \"icon\";\n const type = icon.type as string | { displayName?: string; name?: string };\n return typeof type === \"string\" ? type : (type.displayName ?? type.name ?? \"icon\");\n}\n\nexport const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(function EmptyState(\n { title, description, children, icon, image, action, frame: frameStyle = \"none\", align = \"center\", className, ...props },\n ref,\n) {\n const frame = useRef<HTMLDivElement>(null);\n const copy = useRef<HTMLDivElement>(null);\n const message = description ?? children;\n // `cond && <img />` hands over `false` when the condition fails: absent, not\n // an empty slot with its spacing.\n const hasImage = image != null && typeof image !== \"boolean\";\n const text = (value: ReactNode) =>\n typeof value === \"string\" ? (\n <SwapText swapKey={value} className=\"block\">\n {value}\n </SwapText>\n ) : (\n value\n );\n // The copy column follows its height when the title or the message changes.\n useHeightFollow(\n frame,\n copy,\n `${typeof title === \"string\" ? title : \"\"}\\n${typeof message === \"string\" ? message : \"\"}`,\n );\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"empty-state\"\n data-frame={frameStyle === \"none\" ? undefined : frameStyle}\n data-align={align === \"center\" ? undefined : align}\n className={cn(\n cnAll(\n emptyStateVariants(),\n frameStyle === \"dashed\" &&\n \"rounded-panel border-(length:--border-width) border-dashed border-border bg-muted\",\n align === \"start\" && \"items-start text-start\",\n ),\n className,\n )}\n >\n {!hasImage ? null : (\n <div\n data-slot=\"empty-state-image\"\n className={cnAll(emptyStateImageVariants(), align === \"start\" && \"justify-start\")}\n >\n {image}\n </div>\n )}\n {icon === undefined || icon === null ? null : (\n <div\n data-slot=\"empty-state-icon\"\n aria-hidden=\"true\"\n className={cnAll(\n \"grid size-12 place-items-center rounded-circle border-(length:--border-width) border-border bg-muted text-text-secondary motion-safe:animate-empty-state-icon [&_svg]:size-6\",\n // Under an illustration it clears it by the same step as the title.\n hasImage && \"mt-5\",\n )}\n >\n <SwapText swapKey={iconKey(icon)} motion=\"glyph\" className=\"grid place-items-center\">\n {icon}\n </SwapText>\n </div>\n )}\n <div ref={frame} data-slot=\"empty-state-copy\" className=\"height-follow w-full\">\n <div ref={copy} className=\"flow-root\">\n {title == null ? null : (\n <h3\n data-slot=\"empty-state-title\"\n className={cnAll(\n emptyStateTitleVariants(),\n // First thing in the box: nothing above it to clear.\n align === \"start\" && icon == null && !hasImage && \"mt-0\",\n )}\n >\n {text(title)}\n </h3>\n )}\n {message == null ? null : (\n <p\n data-slot=\"empty-state-text\"\n className={cnAll(\n emptyStateTextVariants(),\n align === \"start\" && \"mx-0\",\n // Under the title it sits close; straight under the icon or the\n // illustration it clears it.\n title != null ? \"mt-2\" : (icon != null || hasImage) && \"mt-5\",\n )}\n >\n {text(message)}\n </p>\n )}\n </div>\n </div>\n {action == null ? null : (\n <div\n data-slot=\"empty-state-action\"\n className={cnAll(emptyStateActionVariants(), align === \"start\" && \"justify-start\")}\n >\n {action}\n </div>\n )}\n </div>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,qBAAqB,IAAI,CACpC,qDAGA,sCACF,CAAC;AAED,MAAa,0BAA0B,IACrC,wFACF;AAEA,MAAa,yBAAyB,IACpC,+FACF;AAEA,MAAa,2BAA2B,IAAI,0CAA0C;;;;;;AAOtF,MAAa,0BAA0B,IACrC,oGACF;;AAoEA,SAAS,QAAQ,MAAiB;CAChC,IAAI,CAAC,eAAe,IAAI,GAAG,OAAO;CAClC,MAAM,OAAO,KAAK;CAClB,OAAO,OAAO,SAAS,WAAW,OAAQ,KAAK,eAAe,KAAK,QAAQ;AAC7E;AAEA,MAAa,aAAa,WAA4C,SAAS,WAC7E,EAAE,OAAO,aAAa,UAAU,MAAM,OAAO,QAAQ,OAAO,aAAa,QAAQ,QAAQ,UAAU,WAAW,GAAG,SACjH,KACA;CACA,MAAM,QAAQ,OAAuB,IAAI;CACzC,MAAM,OAAO,OAAuB,IAAI;CACxC,MAAM,UAAU,eAAe;CAG/B,MAAM,WAAW,SAAS,QAAQ,OAAO,UAAU;CACnD,MAAM,QAAQ,UACZ,OAAO,UAAU,WACf,oBAAC,UAAD;EAAU,SAAS;EAAO,WAAU;EACjC,UAAA;CACO,CAAA,IAEV;CAGJ,gBACE,OACA,MACA,GAAG,OAAO,UAAU,WAAW,QAAQ,GAAG,IAAI,OAAO,YAAY,WAAW,UAAU,IACxF;CACA,OACE,qBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,cAAY,eAAe,SAAS,KAAA,IAAY;EAChD,cAAY,UAAU,WAAW,KAAA,IAAY;EAC7C,WAAW,GACT,MACE,mBAAmB,GACnB,eAAe,YACb,qFACF,UAAU,WAAW,wBACvB,GACA,SACF;EAdF,UAAA;GAgBG,CAAC,WAAW,OACX,oBAAC,OAAD;IACE,aAAU;IACV,WAAW,MAAM,wBAAwB,GAAG,UAAU,WAAW,eAAe;IAE/E,UAAA;GACE,CAAA;GAEN,SAAS,KAAA,KAAa,SAAS,OAAO,OACrC,oBAAC,OAAD;IACE,aAAU;IACV,eAAY;IACZ,WAAW,MACT,gLAEA,YAAY,MACd;IAEA,UAAA,oBAAC,UAAD;KAAU,SAAS,QAAQ,IAAI;KAAG,QAAO;KAAQ,WAAU;KACxD,UAAA;IACO,CAAA;GACP,CAAA;GAEP,oBAAC,OAAD;IAAK,KAAK;IAAO,aAAU;IAAmB,WAAU;IACtD,UAAA,qBAAC,OAAD;KAAK,KAAK;KAAM,WAAU;KAA1B,UAAA,CACG,SAAS,OAAO,OACf,oBAAC,MAAD;MACE,aAAU;MACV,WAAW,MACT,wBAAwB,GAExB,UAAU,WAAW,QAAQ,QAAQ,CAAC,YAAY,MACpD;MAEC,UAAA,KAAK,KAAK;KACT,CAAA,GAEL,WAAW,OAAO,OACjB,oBAAC,KAAD;MACE,aAAU;MACV,WAAW,MACT,uBAAuB,GACvB,UAAU,WAAW,QAGrB,SAAS,OAAO,UAAU,QAAQ,QAAQ,aAAa,MACzD;MAEC,UAAA,KAAK,OAAO;KACZ,CAAA,CAEF;;GACF,CAAA;GACJ,UAAU,OAAO,OAChB,oBAAC,OAAD;IACE,aAAU;IACV,WAAW,MAAM,yBAAyB,GAAG,UAAU,WAAW,eAAe;IAEhF,UAAA;GACE,CAAA;EAEJ;;AAET,CAAC"}
|
|
@@ -18,14 +18,30 @@ import { ReactNode } from "react";
|
|
|
18
18
|
* source text, so a class assembled at runtime is invisible to it and would
|
|
19
19
|
* simply never be emitted.
|
|
20
20
|
*/
|
|
21
|
-
export declare const fileDropZoneVariants: (props?:
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
export declare const fileDropZoneVariants: (props?: ({
|
|
22
|
+
size?: "sm" | "md" | null | undefined;
|
|
23
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
24
|
+
export declare const fileDropZonePromptVariants: (props?: ({
|
|
25
|
+
size?: "sm" | "md" | null | undefined;
|
|
26
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
27
|
+
export declare const fileDropZoneHintVariants: (props?: ({
|
|
28
|
+
size?: "sm" | "md" | null | undefined;
|
|
29
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
24
30
|
/**
|
|
25
31
|
* A place to drop or choose files.
|
|
26
32
|
*
|
|
27
33
|
* @example
|
|
28
34
|
* <FileDropZone
|
|
35
|
+
* size="sm"
|
|
36
|
+
* label="Company logo"
|
|
37
|
+
* prompt="Drop a logo here"
|
|
38
|
+
* hint="PNG, JPG or SVG, up to 5 MB."
|
|
39
|
+
* acceptedFileTypes={["image/*"]}
|
|
40
|
+
* onFiles={(files) => upload(files[0])}
|
|
41
|
+
* />
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* <FileDropZone
|
|
29
45
|
* label="Contract"
|
|
30
46
|
* prompt="Drop a contract here"
|
|
31
47
|
* hint="PDF only, up to 20 MB."
|
|
@@ -63,6 +79,16 @@ export interface FileDropZoneProps {
|
|
|
63
79
|
buttonLabel?: string;
|
|
64
80
|
/** Take no file: the button is disabled and a drop is refused. */
|
|
65
81
|
isDisabled?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* How much room the zone takes. `md` is the stacked zone (prompt, button and
|
|
84
|
+
* hint centred in a tall dashed area). `sm` is one row as tall as a medium
|
|
85
|
+
* field, for a form row such as a logo upload: the prompt, the button and the
|
|
86
|
+
* hint sit side by side, and when the row is narrow the hint truncates first.
|
|
87
|
+
* Same parts, same order, same keyboard path at both sizes.
|
|
88
|
+
*
|
|
89
|
+
* @default "md"
|
|
90
|
+
*/
|
|
91
|
+
size?: "sm" | "md";
|
|
66
92
|
/** Extra classes for the zone. Merged last, so they win conflicts with ours. */
|
|
67
93
|
className?: string;
|
|
68
94
|
}
|
|
@@ -27,7 +27,7 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
|
27
27
|
* simply never be emitted.
|
|
28
28
|
*/
|
|
29
29
|
const fileDropZoneVariants = cva([
|
|
30
|
-
"flex
|
|
30
|
+
"flex items-center",
|
|
31
31
|
"rounded-panel border-(length:--border-width) border-dashed border-border",
|
|
32
32
|
"box-border font-sans text-foreground",
|
|
33
33
|
"outline-none",
|
|
@@ -39,14 +39,32 @@ const fileDropZoneVariants = cva([
|
|
|
39
39
|
"data-[focus-visible]:outline-offset-(--focus-ring-offset)",
|
|
40
40
|
"data-[focus-visible]:outline-(--color-ring)",
|
|
41
41
|
"data-[disabled]:cursor-not-allowed"
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
], {
|
|
43
|
+
variants: { size: {
|
|
44
|
+
md: "flex-col gap-2 px-4 py-8 text-center",
|
|
45
|
+
sm: "min-h-(--density-control-h-md) flex-row justify-center gap-2 px-3 py-0.5 text-start"
|
|
46
|
+
} },
|
|
47
|
+
defaultVariants: { size: "md" }
|
|
48
|
+
});
|
|
49
|
+
const fileDropZonePromptVariants = cva("m-0 text-sm leading-(--leading-normal)", {
|
|
50
|
+
variants: { size: {
|
|
51
|
+
md: "",
|
|
52
|
+
sm: "min-w-0 truncate"
|
|
53
|
+
} },
|
|
54
|
+
defaultVariants: { size: "md" }
|
|
55
|
+
});
|
|
56
|
+
const fileDropZoneHintVariants = cva("m-0 text-xs leading-(--leading-normal) text-muted-foreground", {
|
|
57
|
+
variants: { size: {
|
|
58
|
+
md: "",
|
|
59
|
+
sm: "min-w-0 flex-1 basis-0 truncate"
|
|
60
|
+
} },
|
|
61
|
+
defaultVariants: { size: "md" }
|
|
62
|
+
});
|
|
45
63
|
async function filesOf(event) {
|
|
46
64
|
const items = event.items.filter((item) => item.kind === "file");
|
|
47
65
|
return Promise.all(items.map((item) => item.getFile()));
|
|
48
66
|
}
|
|
49
|
-
const FileDropZone = forwardRef(function FileDropZone({ label, prompt, dropPrompt, hint, acceptedFileTypes, allowsMultiple = false, onFiles, buttonLabel, isDisabled = false, className }, ref) {
|
|
67
|
+
const FileDropZone = forwardRef(function FileDropZone({ label, prompt, dropPrompt, hint, acceptedFileTypes, allowsMultiple = false, onFiles, buttonLabel, isDisabled = false, size = "md", className }, ref) {
|
|
50
68
|
const hand = (files) => {
|
|
51
69
|
if (isDisabled || files.length === 0) return;
|
|
52
70
|
onFiles(allowsMultiple ? files : files.slice(0, 1));
|
|
@@ -64,7 +82,7 @@ const FileDropZone = forwardRef(function FileDropZone({ label, prompt, dropPromp
|
|
|
64
82
|
"aria-label": label,
|
|
65
83
|
"data-slot": "file-drop-zone",
|
|
66
84
|
isDisabled,
|
|
67
|
-
className: cn(fileDropZoneVariants(), className),
|
|
85
|
+
className: cn(fileDropZoneVariants({ size }), className),
|
|
68
86
|
onDropEnter: (event) => lean(event.x, event.y),
|
|
69
87
|
onDropMove: (event) => lean(event.x, event.y),
|
|
70
88
|
onDrop: (event) => {
|
|
@@ -83,7 +101,7 @@ const FileDropZone = forwardRef(function FileDropZone({ label, prompt, dropPromp
|
|
|
83
101
|
}),
|
|
84
102
|
/* @__PURE__ */ jsx("p", {
|
|
85
103
|
"data-slot": "file-drop-zone-prompt",
|
|
86
|
-
className: cn(fileDropZonePromptVariants(), "relative"),
|
|
104
|
+
className: cn(fileDropZonePromptVariants({ size }), "relative"),
|
|
87
105
|
children: dropPrompt == null ? prompt : /* @__PURE__ */ jsx(SwapText, {
|
|
88
106
|
swapKey: isDropTarget ? "drop" : "prompt",
|
|
89
107
|
className: "block",
|
|
@@ -104,7 +122,7 @@ const FileDropZone = forwardRef(function FileDropZone({ label, prompt, dropPromp
|
|
|
104
122
|
}),
|
|
105
123
|
hint == null ? null : /* @__PURE__ */ jsx("p", {
|
|
106
124
|
"data-slot": "file-drop-zone-hint",
|
|
107
|
-
className: cn(fileDropZoneHintVariants(), "relative"),
|
|
125
|
+
className: cn(fileDropZoneHintVariants({ size }), "relative"),
|
|
108
126
|
children: hint
|
|
109
127
|
})
|
|
110
128
|
] })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-drop-zone.js","names":["AriaDropZone","AriaFileTrigger","Button"],"sources":["../../src/file-drop-zone/file-drop-zone.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useMemo, useRef, type ReactNode } from \"react\";\nimport {\n DropZone as AriaDropZone,\n FileTrigger as AriaFileTrigger,\n type DropZoneProps as AriaDropZoneProps,\n type FileDropItem,\n} from \"react-aria-components\";\n\nimport { Button } from \"../button/button.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * FileDropZone: a dashed area a file can be dropped on, with a \"Choose file\"\n * button that opens the system's file dialog.\n *\n * Built on React Aria's `DropZone` and `FileTrigger`. The browser's own\n * `<input type=\"file\">` (\"Choose Files / No file chosen\") is never shown: its\n * look differs on every browser and it cannot be styled. FileTrigger keeps a\n * real file input, visually hidden and out of the Tab order, and opens it when\n * the button is pressed, so the keyboard path is Tab to the button, then Enter\n * or Space.\n *\n * The zone states what it takes before a file is chosen: `hint` is where the\n * accepted types and the size limit go.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const fileDropZoneVariants = cva([\n \"flex flex-col items-center gap-2 px-4 py-8 text-center\",\n \"rounded-panel border-(length:--border-width) border-dashed border-border\",\n // The reset in @seekalfred/styles is opt-in, so a consumer may load none.\n \"box-border font-sans text-foreground\",\n \"outline-none\",\n // Fill and edge ease, a file over it breathes and light follows the pointer (see drop-zone-motion).\n \"drop-zone-motion\",\n \"data-[hovered]:bg-muted\",\n \"data-[drop-target]:border-primary data-[drop-target]:bg-muted\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n // Disabled dims only the button (its own disabled look). Dimming the whole\n // zone took the prompt and hint below the 4.5:1 contrast floor.\n \"data-[disabled]:cursor-not-allowed\",\n]);\n\nexport const fileDropZonePromptVariants = cva(\"m-0 text-sm leading-(--leading-normal)\");\n\nexport const fileDropZoneHintVariants = cva(\n \"m-0 text-xs leading-(--leading-normal) text-muted-foreground\",\n);\n\n/**\n * A place to drop or choose files.\n *\n * @example\n * <FileDropZone\n * label=\"Contract\"\n * prompt=\"Drop a contract here\"\n * hint=\"PDF only, up to 20 MB.\"\n * acceptedFileTypes={[\"application/pdf\"]}\n * onFiles={(files) => upload(files[0])}\n * />\n */\nexport interface FileDropZoneProps {\n /** What the file is. The drop zone's accessible name. */\n label: string;\n /** The zone's main line: what to drop here, or the chosen file's name. */\n prompt: ReactNode;\n /**\n * What the prompt says while a file is over the zone. When it is given the\n * prompt swaps to it (the new text rises in, the old lifts away) and back again.\n */\n dropPrompt?: ReactNode;\n /** The types and size accepted, and anything else to know before choosing. */\n hint?: ReactNode;\n /**\n * MIME types or extensions the file dialog offers, as for a native\n * `accept`. A drop is not filtered: what was dropped is handed over and the\n * consumer decides what it can read.\n */\n acceptedFileTypes?: string[];\n /** Take several files at once. Otherwise only the first is handed over. */\n allowsMultiple?: boolean;\n /** Called with the chosen or dropped files. */\n onFiles: (files: File[]) => void;\n /**\n * The button's text.\n *\n * @default \"Choose file\", or \"Choose files\" with `allowsMultiple`\n */\n buttonLabel?: string;\n /** Take no file: the button is disabled and a drop is refused. */\n isDisabled?: boolean;\n /** Extra classes for the zone. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\ntype DropEvent = Parameters<NonNullable<AriaDropZoneProps[\"onDrop\"]>>[0];\n\nasync function filesOf(event: DropEvent): Promise<File[]> {\n const items = event.items.filter((item): item is FileDropItem => item.kind === \"file\");\n return Promise.all(items.map((item) => item.getFile()));\n}\n\nexport const FileDropZone = forwardRef<HTMLDivElement, FileDropZoneProps>(function FileDropZone(\n {\n label,\n prompt,\n dropPrompt,\n hint,\n acceptedFileTypes,\n allowsMultiple = false,\n onFiles,\n buttonLabel,\n isDisabled = false,\n className,\n },\n ref,\n) {\n const hand = (files: File[]) => {\n if (isDisabled || files.length === 0) return;\n onFiles(allowsMultiple ? files : files.slice(0, 1));\n };\n const zone = useRef<HTMLDivElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, zone), [ref]);\n // Where the file is over the zone, as percentages, for the light that follows it.\n const lean = (x: number, y: number) => {\n const el = zone.current;\n if (!el) return;\n el.style.setProperty(\"--drop-x\", `${(x / Math.max(1, el.offsetWidth)) * 100}%`);\n el.style.setProperty(\"--drop-y\", `${(y / Math.max(1, el.offsetHeight)) * 100}%`);\n };\n return (\n <AriaDropZone\n ref={refs}\n aria-label={label}\n data-slot=\"file-drop-zone\"\n isDisabled={isDisabled}\n className={cn(fileDropZoneVariants(), className)}\n onDropEnter={(event) => lean(event.x, event.y)}\n onDropMove={(event) => lean(event.x, event.y)}\n onDrop={(event) => {\n void filesOf(event).then(hand);\n }}\n >\n {({ isDropTarget }) => (\n <>\n {/* The wash and the edge light sit under the content and follow the file. */}\n <span aria-hidden=\"true\" data-slot=\"file-drop-zone-wash\" className=\"drop-wash\" />\n <span aria-hidden=\"true\" data-slot=\"file-drop-zone-glow\" className=\"drop-glow\" />\n <p data-slot=\"file-drop-zone-prompt\" className={cn(fileDropZonePromptVariants(), \"relative\")}>\n {dropPrompt == null ? (\n prompt\n ) : (\n <SwapText swapKey={isDropTarget ? \"drop\" : \"prompt\"} className=\"block\">\n {isDropTarget ? dropPrompt : prompt}\n </SwapText>\n )}\n </p>\n <AriaFileTrigger\n acceptedFileTypes={acceptedFileTypes}\n allowsMultiple={allowsMultiple}\n onSelect={(list) => hand(Array.from(list ?? []))}\n >\n <Button variant=\"outlineNeutral\" size=\"sm\" isDisabled={isDisabled} className=\"relative\">\n {buttonLabel ?? (allowsMultiple ? \"Choose files\" : \"Choose file\")}\n </Button>\n </AriaFileTrigger>\n {hint == null ? null : (\n <p\n data-slot=\"file-drop-zone-hint\"\n className={cn(fileDropZoneHintVariants(), \"relative\")}\n >\n {hint}\n </p>\n )}\n </>\n )}\n </AriaDropZone>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,uBAAuB,IAAI;CACtC;CACA;CAEA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;AACF,CAAC;AAED,MAAa,6BAA6B,IAAI,wCAAwC;AAEtF,MAAa,2BAA2B,IACtC,8DACF;AAkDA,eAAe,QAAQ,OAAmC;CACxD,MAAM,QAAQ,MAAM,MAAM,QAAQ,SAA+B,KAAK,SAAS,MAAM;CACrF,OAAO,QAAQ,IAAI,MAAM,KAAK,SAAS,KAAK,QAAQ,CAAC,CAAC;AACxD;AAEA,MAAa,eAAe,WAA8C,SAAS,aACjF,EACE,OACA,QACA,YACA,MACA,mBACA,iBAAiB,OACjB,SACA,aACA,aAAa,OACb,aAEF,KACA;CACA,MAAM,QAAQ,UAAkB;EAC9B,IAAI,cAAc,MAAM,WAAW,GAAG;EACtC,QAAQ,iBAAiB,QAAQ,MAAM,MAAM,GAAG,CAAC,CAAC;CACpD;CACA,MAAM,OAAO,OAA8B,IAAI;CAC/C,MAAM,OAAO,cAAc,UAAU,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC;CAEtD,MAAM,QAAQ,GAAW,MAAc;EACrC,MAAM,KAAK,KAAK;EAChB,IAAI,CAAC,IAAI;EACT,GAAG,MAAM,YAAY,YAAY,GAAI,IAAI,KAAK,IAAI,GAAG,GAAG,WAAW,IAAK,IAAI,EAAE;EAC9E,GAAG,MAAM,YAAY,YAAY,GAAI,IAAI,KAAK,IAAI,GAAG,GAAG,YAAY,IAAK,IAAI,EAAE;CACjF;CACA,OACE,oBAACA,UAAD;EACE,KAAK;EACL,cAAY;EACZ,aAAU;EACE;EACZ,WAAW,GAAG,qBAAqB,GAAG,SAAS;EAC/C,cAAc,UAAU,KAAK,MAAM,GAAG,MAAM,CAAC;EAC7C,aAAa,UAAU,KAAK,MAAM,GAAG,MAAM,CAAC;EAC5C,SAAS,UAAU;GACjB,QAAa,KAAK,CAAC,CAAC,KAAK,IAAI;EAC/B;EAEE,WAAA,EAAE,mBACF,qBAAA,YAAA,EAAA,UAAA;GAEE,oBAAC,QAAD;IAAM,eAAY;IAAO,aAAU;IAAsB,WAAU;GAAa,CAAA;GAChF,oBAAC,QAAD;IAAM,eAAY;IAAO,aAAU;IAAsB,WAAU;GAAa,CAAA;GAChF,oBAAC,KAAD;IAAG,aAAU;IAAwB,WAAW,GAAG,2BAA2B,GAAG,UAAU;IACxF,UAAA,cAAc,OACb,SAEA,oBAAC,UAAD;KAAU,SAAS,eAAe,SAAS;KAAU,WAAU;KAC5D,UAAA,eAAe,aAAa;IACrB,CAAA;GAEX,CAAA;GACH,oBAACC,aAAD;IACqB;IACH;IAChB,WAAW,SAAS,KAAK,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC;IAE/C,UAAA,oBAACC,UAAD;KAAQ,SAAQ;KAAiB,MAAK;KAAiB;KAAY,WAAU;KAC1E,UAAA,gBAAgB,iBAAiB,iBAAiB;IAC7C,CAAA;GACO,CAAA;GAChB,QAAQ,OAAO,OACd,oBAAC,KAAD;IACE,aAAU;IACV,WAAW,GAAG,yBAAyB,GAAG,UAAU;IAEnD,UAAA;GACA,CAAA;EAEL,EAAA,CAAA;CAEQ,CAAA;AAElB,CAAC"}
|
|
1
|
+
{"version":3,"file":"file-drop-zone.js","names":["AriaDropZone","AriaFileTrigger","Button"],"sources":["../../src/file-drop-zone/file-drop-zone.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useMemo, useRef, type ReactNode } from \"react\";\nimport {\n DropZone as AriaDropZone,\n FileTrigger as AriaFileTrigger,\n type DropZoneProps as AriaDropZoneProps,\n type FileDropItem,\n} from \"react-aria-components\";\n\nimport { Button } from \"../button/button.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * FileDropZone: a dashed area a file can be dropped on, with a \"Choose file\"\n * button that opens the system's file dialog.\n *\n * Built on React Aria's `DropZone` and `FileTrigger`. The browser's own\n * `<input type=\"file\">` (\"Choose Files / No file chosen\") is never shown: its\n * look differs on every browser and it cannot be styled. FileTrigger keeps a\n * real file input, visually hidden and out of the Tab order, and opens it when\n * the button is pressed, so the keyboard path is Tab to the button, then Enter\n * or Space.\n *\n * The zone states what it takes before a file is chosen: `hint` is where the\n * accepted types and the size limit go.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const fileDropZoneVariants = cva(\n [\n \"flex items-center\",\n \"rounded-panel border-(length:--border-width) border-dashed border-border\",\n // The reset in @seekalfred/styles is opt-in, so a consumer may load none.\n \"box-border font-sans text-foreground\",\n \"outline-none\",\n // Fill and edge ease, a file over it breathes and light follows the pointer (see drop-zone-motion).\n \"drop-zone-motion\",\n \"data-[hovered]:bg-muted\",\n \"data-[drop-target]:border-primary data-[drop-target]:bg-muted\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n // Disabled dims only the button (its own disabled look). Dimming the whole\n // zone took the prompt and hint below the 4.5:1 contrast floor.\n \"data-[disabled]:cursor-not-allowed\",\n ],\n {\n variants: {\n size: {\n // A stacked zone: prompt, button, hint, centred.\n md: \"flex-col gap-2 px-4 py-8 text-center\",\n // One row as tall as a medium field (the sm button plus a hairline of\n // padding), for a form row such as a logo upload. Nothing wraps: the\n // hint gives way first, then the prompt; the button (shrink-0) stays whole.\n sm: \"min-h-(--density-control-h-md) flex-row justify-center gap-2 px-3 py-0.5 text-start\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\nexport const fileDropZonePromptVariants = cva(\"m-0 text-sm leading-(--leading-normal)\", {\n variants: {\n size: {\n md: \"\",\n sm: \"min-w-0 truncate\",\n },\n },\n defaultVariants: { size: \"md\" },\n});\n\nexport const fileDropZoneHintVariants = cva(\n \"m-0 text-xs leading-(--leading-normal) text-muted-foreground\",\n {\n variants: {\n size: {\n md: \"\",\n // Takes only the room left over, so it is the first to truncate.\n sm: \"min-w-0 flex-1 basis-0 truncate\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n/**\n * A place to drop or choose files.\n *\n * @example\n * <FileDropZone\n * size=\"sm\"\n * label=\"Company logo\"\n * prompt=\"Drop a logo here\"\n * hint=\"PNG, JPG or SVG, up to 5 MB.\"\n * acceptedFileTypes={[\"image/*\"]}\n * onFiles={(files) => upload(files[0])}\n * />\n *\n * @example\n * <FileDropZone\n * label=\"Contract\"\n * prompt=\"Drop a contract here\"\n * hint=\"PDF only, up to 20 MB.\"\n * acceptedFileTypes={[\"application/pdf\"]}\n * onFiles={(files) => upload(files[0])}\n * />\n */\nexport interface FileDropZoneProps {\n /** What the file is. The drop zone's accessible name. */\n label: string;\n /** The zone's main line: what to drop here, or the chosen file's name. */\n prompt: ReactNode;\n /**\n * What the prompt says while a file is over the zone. When it is given the\n * prompt swaps to it (the new text rises in, the old lifts away) and back again.\n */\n dropPrompt?: ReactNode;\n /** The types and size accepted, and anything else to know before choosing. */\n hint?: ReactNode;\n /**\n * MIME types or extensions the file dialog offers, as for a native\n * `accept`. A drop is not filtered: what was dropped is handed over and the\n * consumer decides what it can read.\n */\n acceptedFileTypes?: string[];\n /** Take several files at once. Otherwise only the first is handed over. */\n allowsMultiple?: boolean;\n /** Called with the chosen or dropped files. */\n onFiles: (files: File[]) => void;\n /**\n * The button's text.\n *\n * @default \"Choose file\", or \"Choose files\" with `allowsMultiple`\n */\n buttonLabel?: string;\n /** Take no file: the button is disabled and a drop is refused. */\n isDisabled?: boolean;\n /**\n * How much room the zone takes. `md` is the stacked zone (prompt, button and\n * hint centred in a tall dashed area). `sm` is one row as tall as a medium\n * field, for a form row such as a logo upload: the prompt, the button and the\n * hint sit side by side, and when the row is narrow the hint truncates first.\n * Same parts, same order, same keyboard path at both sizes.\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\";\n /** Extra classes for the zone. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\ntype DropEvent = Parameters<NonNullable<AriaDropZoneProps[\"onDrop\"]>>[0];\n\nasync function filesOf(event: DropEvent): Promise<File[]> {\n const items = event.items.filter((item): item is FileDropItem => item.kind === \"file\");\n return Promise.all(items.map((item) => item.getFile()));\n}\n\nexport const FileDropZone = forwardRef<HTMLDivElement, FileDropZoneProps>(function FileDropZone(\n {\n label,\n prompt,\n dropPrompt,\n hint,\n acceptedFileTypes,\n allowsMultiple = false,\n onFiles,\n buttonLabel,\n isDisabled = false,\n size = \"md\",\n className,\n },\n ref,\n) {\n const hand = (files: File[]) => {\n if (isDisabled || files.length === 0) return;\n onFiles(allowsMultiple ? files : files.slice(0, 1));\n };\n const zone = useRef<HTMLDivElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, zone), [ref]);\n // Where the file is over the zone, as percentages, for the light that follows it.\n const lean = (x: number, y: number) => {\n const el = zone.current;\n if (!el) return;\n el.style.setProperty(\"--drop-x\", `${(x / Math.max(1, el.offsetWidth)) * 100}%`);\n el.style.setProperty(\"--drop-y\", `${(y / Math.max(1, el.offsetHeight)) * 100}%`);\n };\n return (\n <AriaDropZone\n ref={refs}\n aria-label={label}\n data-slot=\"file-drop-zone\"\n isDisabled={isDisabled}\n className={cn(fileDropZoneVariants({ size }), className)}\n onDropEnter={(event) => lean(event.x, event.y)}\n onDropMove={(event) => lean(event.x, event.y)}\n onDrop={(event) => {\n void filesOf(event).then(hand);\n }}\n >\n {({ isDropTarget }) => (\n <>\n {/* The wash and the edge light sit under the content and follow the file. */}\n <span aria-hidden=\"true\" data-slot=\"file-drop-zone-wash\" className=\"drop-wash\" />\n <span aria-hidden=\"true\" data-slot=\"file-drop-zone-glow\" className=\"drop-glow\" />\n <p\n data-slot=\"file-drop-zone-prompt\"\n className={cn(fileDropZonePromptVariants({ size }), \"relative\")}\n >\n {dropPrompt == null ? (\n prompt\n ) : (\n <SwapText swapKey={isDropTarget ? \"drop\" : \"prompt\"} className=\"block\">\n {isDropTarget ? dropPrompt : prompt}\n </SwapText>\n )}\n </p>\n <AriaFileTrigger\n acceptedFileTypes={acceptedFileTypes}\n allowsMultiple={allowsMultiple}\n onSelect={(list) => hand(Array.from(list ?? []))}\n >\n <Button variant=\"outlineNeutral\" size=\"sm\" isDisabled={isDisabled} className=\"relative\">\n {buttonLabel ?? (allowsMultiple ? \"Choose files\" : \"Choose file\")}\n </Button>\n </AriaFileTrigger>\n {hint == null ? null : (\n <p\n data-slot=\"file-drop-zone-hint\"\n className={cn(fileDropZoneHintVariants({ size }), \"relative\")}\n >\n {hint}\n </p>\n )}\n </>\n )}\n </AriaDropZone>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,uBAAuB,IAClC;CACE;CACA;CAEA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;AACF,GACA;CACE,UAAU,EACR,MAAM;EAEJ,IAAI;EAIJ,IAAI;CACN,EACF;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;AAEA,MAAa,6BAA6B,IAAI,0CAA0C;CACtF,UAAU,EACR,MAAM;EACJ,IAAI;EACJ,IAAI;CACN,EACF;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CAAC;AAED,MAAa,2BAA2B,IACtC,gEACA;CACE,UAAU,EACR,MAAM;EACJ,IAAI;EAEJ,IAAI;CACN,EACF;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;AAsEA,eAAe,QAAQ,OAAmC;CACxD,MAAM,QAAQ,MAAM,MAAM,QAAQ,SAA+B,KAAK,SAAS,MAAM;CACrF,OAAO,QAAQ,IAAI,MAAM,KAAK,SAAS,KAAK,QAAQ,CAAC,CAAC;AACxD;AAEA,MAAa,eAAe,WAA8C,SAAS,aACjF,EACE,OACA,QACA,YACA,MACA,mBACA,iBAAiB,OACjB,SACA,aACA,aAAa,OACb,OAAO,MACP,aAEF,KACA;CACA,MAAM,QAAQ,UAAkB;EAC9B,IAAI,cAAc,MAAM,WAAW,GAAG;EACtC,QAAQ,iBAAiB,QAAQ,MAAM,MAAM,GAAG,CAAC,CAAC;CACpD;CACA,MAAM,OAAO,OAA8B,IAAI;CAC/C,MAAM,OAAO,cAAc,UAAU,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC;CAEtD,MAAM,QAAQ,GAAW,MAAc;EACrC,MAAM,KAAK,KAAK;EAChB,IAAI,CAAC,IAAI;EACT,GAAG,MAAM,YAAY,YAAY,GAAI,IAAI,KAAK,IAAI,GAAG,GAAG,WAAW,IAAK,IAAI,EAAE;EAC9E,GAAG,MAAM,YAAY,YAAY,GAAI,IAAI,KAAK,IAAI,GAAG,GAAG,YAAY,IAAK,IAAI,EAAE;CACjF;CACA,OACE,oBAACA,UAAD;EACE,KAAK;EACL,cAAY;EACZ,aAAU;EACE;EACZ,WAAW,GAAG,qBAAqB,EAAE,KAAK,CAAC,GAAG,SAAS;EACvD,cAAc,UAAU,KAAK,MAAM,GAAG,MAAM,CAAC;EAC7C,aAAa,UAAU,KAAK,MAAM,GAAG,MAAM,CAAC;EAC5C,SAAS,UAAU;GACjB,QAAa,KAAK,CAAC,CAAC,KAAK,IAAI;EAC/B;EAEE,WAAA,EAAE,mBACF,qBAAA,YAAA,EAAA,UAAA;GAEE,oBAAC,QAAD;IAAM,eAAY;IAAO,aAAU;IAAsB,WAAU;GAAa,CAAA;GAChF,oBAAC,QAAD;IAAM,eAAY;IAAO,aAAU;IAAsB,WAAU;GAAa,CAAA;GAChF,oBAAC,KAAD;IACE,aAAU;IACV,WAAW,GAAG,2BAA2B,EAAE,KAAK,CAAC,GAAG,UAAU;IAE7D,UAAA,cAAc,OACb,SAEA,oBAAC,UAAD;KAAU,SAAS,eAAe,SAAS;KAAU,WAAU;KAC5D,UAAA,eAAe,aAAa;IACrB,CAAA;GAEX,CAAA;GACH,oBAACC,aAAD;IACqB;IACH;IAChB,WAAW,SAAS,KAAK,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC;IAE/C,UAAA,oBAACC,UAAD;KAAQ,SAAQ;KAAiB,MAAK;KAAiB;KAAY,WAAU;KAC1E,UAAA,gBAAgB,iBAAiB,iBAAiB;IAC7C,CAAA;GACO,CAAA;GAChB,QAAQ,OAAO,OACd,oBAAC,KAAD;IACE,aAAU;IACV,WAAW,GAAG,yBAAyB,EAAE,KAAK,CAAC,GAAG,UAAU;IAE3D,UAAA;GACA,CAAA;EAEL,EAAA,CAAA;CAEQ,CAAA;AAElB,CAAC"}
|
package/dist/gauge/gauge.d.ts
CHANGED
|
@@ -14,10 +14,17 @@ import { ComponentPropsWithoutRef } from "react";
|
|
|
14
14
|
* status and detail rise in when they change, from the side the value moved
|
|
15
15
|
* toward.
|
|
16
16
|
*
|
|
17
|
+
* A missing value (`null`) shows an empty ring, a dash and "No data", and is
|
|
18
|
+
* read as an image named "<label>: No data" rather than a meter, since there
|
|
19
|
+
* is nothing to measure. Size `sm` is the number alone in a small ring, for a
|
|
20
|
+
* table cell or a card corner; the label and detail stay for assistive
|
|
21
|
+
* technology.
|
|
22
|
+
*
|
|
17
23
|
* Plain SVG and CSS. One small spring tween writes the ring and the count
|
|
18
24
|
* straight to the DOM. The ring is a `meter` for assistive technology.
|
|
19
25
|
*/
|
|
20
26
|
export type GaugeTone = "primary" | "success" | "warning" | "danger";
|
|
27
|
+
export type GaugeSize = "sm" | "md";
|
|
21
28
|
export interface GaugeThreshold {
|
|
22
29
|
/** The band starts at this value (in the same unit as `value`). */
|
|
23
30
|
from: number;
|
|
@@ -26,7 +33,8 @@ export interface GaugeThreshold {
|
|
|
26
33
|
label: string;
|
|
27
34
|
}
|
|
28
35
|
export interface GaugeProps extends Omit<ComponentPropsWithoutRef<"figure">, "children"> {
|
|
29
|
-
value:
|
|
36
|
+
/** The value. `null` (or a value that is not a finite number) is no data: an empty ring that reads `missingLabel`. */
|
|
37
|
+
value: number | null;
|
|
30
38
|
/** @default 0 */
|
|
31
39
|
min?: number;
|
|
32
40
|
/** @default 100 */
|
|
@@ -39,6 +47,15 @@ export interface GaugeProps extends Omit<ComponentPropsWithoutRef<"figure">, "ch
|
|
|
39
47
|
tone?: GaugeTone;
|
|
40
48
|
/** Bands that set the tone and name the state. */
|
|
41
49
|
thresholds?: GaugeThreshold[];
|
|
50
|
+
/**
|
|
51
|
+
* `"sm"` is the number alone in a small ring (48px; set `--gauge-size` to
|
|
52
|
+
* change it), coloured by the tone. The percent sign, the status line and
|
|
53
|
+
* the caption are not drawn; the label, band and detail are still read.
|
|
54
|
+
* @default "md"
|
|
55
|
+
*/
|
|
56
|
+
size?: GaugeSize;
|
|
57
|
+
/** Shown and read in place of the value when it is missing. @default "No data" */
|
|
58
|
+
missingLabel?: string;
|
|
42
59
|
}
|
|
43
60
|
export declare const Gauge: import("react").ForwardRefExoticComponent<GaugeProps & import("react").RefAttributes<HTMLElement>>;
|
|
44
61
|
//#endregion
|
package/dist/gauge/gauge.js
CHANGED
|
@@ -18,13 +18,16 @@ function ringFor(sweep) {
|
|
|
18
18
|
return `M ${at(START)} A ${RADIUS} ${RADIUS} 0 ${end - START > 180 ? 1 : 0} 1 ${at(end)}`;
|
|
19
19
|
}
|
|
20
20
|
const TRACK = ringFor(1);
|
|
21
|
-
const Gauge = forwardRef(function Gauge({ value, min = 0, max = 100, label, detail, tone = "primary", thresholds, className, ...props }, ref) {
|
|
21
|
+
const Gauge = forwardRef(function Gauge({ value: given, min = 0, max = 100, label, detail, tone = "primary", thresholds, size = "md", missingLabel = "No data", className, ...props }, ref) {
|
|
22
|
+
const missing = typeof given !== "number" || !Number.isFinite(given);
|
|
23
|
+
const value = missing ? min : given;
|
|
24
|
+
const small = size === "sm";
|
|
22
25
|
const safeMax = max > min ? max : min + 1;
|
|
23
26
|
const percentage = clamp((value - min) / (safeMax - min), 0, 1);
|
|
24
27
|
const displayValue = Math.round(percentage * 100);
|
|
25
28
|
const bands = [...thresholds ?? []].sort((a, b) => a.from - b.from);
|
|
26
29
|
const bandAt = (at) => bands.filter((band) => at >= band.from - 1e-6).pop();
|
|
27
|
-
const band = bandAt(value);
|
|
30
|
+
const band = missing ? void 0 : bandAt(value);
|
|
28
31
|
const figure = useRef(null);
|
|
29
32
|
const refs = useMemo(() => mergeRefs(ref, figure), [ref]);
|
|
30
33
|
const ring = useRef(null);
|
|
@@ -47,14 +50,16 @@ const Gauge = forwardRef(function Gauge({ value, min = 0, max = 100, label, deta
|
|
|
47
50
|
bandAt,
|
|
48
51
|
min,
|
|
49
52
|
span: safeMax - min,
|
|
50
|
-
filled
|
|
53
|
+
filled,
|
|
54
|
+
missing
|
|
51
55
|
});
|
|
52
56
|
useIsomorphicLayoutEffect(() => {
|
|
53
57
|
latest.current = {
|
|
54
58
|
bandAt,
|
|
55
59
|
min,
|
|
56
60
|
span: safeMax - min,
|
|
57
|
-
filled
|
|
61
|
+
filled,
|
|
62
|
+
missing
|
|
58
63
|
};
|
|
59
64
|
});
|
|
60
65
|
const shownRef = useRef(shownBand);
|
|
@@ -67,9 +72,9 @@ const Gauge = forwardRef(function Gauge({ value, min = 0, max = 100, label, deta
|
|
|
67
72
|
ring.current?.setAttribute("d", ringFor(to));
|
|
68
73
|
if (ring.current) ring.current.style.opacity = String(clamp(to / .02, 0, 1));
|
|
69
74
|
const shown = clamp(count.current.value, 0, 100);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (done) {
|
|
75
|
+
const { bandAt: pick, min: lo, span, filled: done, missing: empty } = latest.current;
|
|
76
|
+
if (digits.current) digits.current.textContent = empty ? "–" : String(Math.round(shown));
|
|
77
|
+
if (done && !empty) {
|
|
73
78
|
const next = pick(lo + shown / 100 * span);
|
|
74
79
|
const current = shownRef.current;
|
|
75
80
|
if (next?.from !== current?.from || next?.label !== current?.label) {
|
|
@@ -111,6 +116,7 @@ const Gauge = forwardRef(function Gauge({ value, min = 0, max = 100, label, deta
|
|
|
111
116
|
seen,
|
|
112
117
|
paint
|
|
113
118
|
]);
|
|
119
|
+
useIsomorphicLayoutEffect(paint, [missing, paint]);
|
|
114
120
|
useEffect(() => {
|
|
115
121
|
const sweeping = sweep.current;
|
|
116
122
|
const number = count.current;
|
|
@@ -131,23 +137,31 @@ const Gauge = forwardRef(function Gauge({ value, min = 0, max = 100, label, deta
|
|
|
131
137
|
observer.observe(node);
|
|
132
138
|
return () => observer.disconnect();
|
|
133
139
|
}, []);
|
|
134
|
-
const shown = filled ? shownBand : band;
|
|
135
|
-
const activeTone =
|
|
140
|
+
const shown = missing ? void 0 : filled ? shownBand : band;
|
|
141
|
+
const activeTone = shown?.tone ?? tone;
|
|
136
142
|
const clampedValue = clamp(value, min, safeMax);
|
|
143
|
+
const reading = missing ? {
|
|
144
|
+
role: "img",
|
|
145
|
+
"aria-label": `${label}: ${missingLabel}`
|
|
146
|
+
} : {
|
|
147
|
+
role: "meter",
|
|
148
|
+
"aria-label": label,
|
|
149
|
+
"aria-valuemin": min,
|
|
150
|
+
"aria-valuemax": safeMax,
|
|
151
|
+
"aria-valuenow": clampedValue,
|
|
152
|
+
"aria-valuetext": `${displayValue}%${band ? `, ${band.label}` : ""}`
|
|
153
|
+
};
|
|
137
154
|
return /* @__PURE__ */ jsxs("figure", {
|
|
138
155
|
ref: refs,
|
|
139
156
|
...props,
|
|
140
157
|
"data-slot": "gauge",
|
|
141
158
|
"data-tone": activeTone,
|
|
159
|
+
"data-size": small ? size : void 0,
|
|
160
|
+
"data-missing": missing || void 0,
|
|
142
161
|
className: cn("gauge", className),
|
|
143
162
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
144
163
|
"data-slot": "gauge-visual",
|
|
145
|
-
|
|
146
|
-
"aria-label": label,
|
|
147
|
-
"aria-valuemin": min,
|
|
148
|
-
"aria-valuemax": safeMax,
|
|
149
|
-
"aria-valuenow": clampedValue,
|
|
150
|
-
"aria-valuetext": `${displayValue}%${band ? `, ${band.label}` : ""}`,
|
|
164
|
+
...reading,
|
|
151
165
|
className: "gauge-visual",
|
|
152
166
|
children: [/* @__PURE__ */ jsxs("svg", {
|
|
153
167
|
viewBox: "0 0 100 90",
|
|
@@ -172,28 +186,28 @@ const Gauge = forwardRef(function Gauge({ value, min = 0, max = 100, label, deta
|
|
|
172
186
|
ref: digits,
|
|
173
187
|
className: "gauge-digits"
|
|
174
188
|
}),
|
|
175
|
-
/* @__PURE__ */ jsx("span", {
|
|
189
|
+
small || missing ? null : /* @__PURE__ */ jsx("span", {
|
|
176
190
|
className: "gauge-unit",
|
|
177
191
|
children: "%"
|
|
178
192
|
}),
|
|
179
193
|
/* @__PURE__ */ jsx("span", {
|
|
180
194
|
ref: sizer,
|
|
181
195
|
className: "gauge-sizer",
|
|
182
|
-
children: displayValue
|
|
196
|
+
children: missing ? "–" : displayValue
|
|
183
197
|
})
|
|
184
198
|
]
|
|
185
|
-
}), thresholds ? /* @__PURE__ */ jsx("span", {
|
|
199
|
+
}), !small && (thresholds || missing) ? /* @__PURE__ */ jsx("span", {
|
|
186
200
|
className: "gauge-status",
|
|
187
201
|
children: /* @__PURE__ */ jsx(SwapText, {
|
|
188
|
-
swapKey: filled ? shown?.label ?? "" : "",
|
|
202
|
+
swapKey: missing ? missingLabel : filled ? shown?.label ?? "" : "",
|
|
189
203
|
dir: moved.direction,
|
|
190
204
|
className: "block",
|
|
191
|
-
children: shown?.label
|
|
205
|
+
children: missing ? missingLabel : shown?.label
|
|
192
206
|
})
|
|
193
207
|
}) : null]
|
|
194
208
|
})]
|
|
195
209
|
}), /* @__PURE__ */ jsxs("figcaption", {
|
|
196
|
-
className: "grid min-w-0 gap-0.5",
|
|
210
|
+
className: small ? "sr-only" : "grid min-w-0 gap-0.5",
|
|
197
211
|
children: [/* @__PURE__ */ jsx("strong", {
|
|
198
212
|
className: "text-sm font-medium text-foreground",
|
|
199
213
|
children: /* @__PURE__ */ jsx(SwapText, {
|