@epicbagel/saas-kit 1.1.0 → 1.2.1

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.
@@ -1,5 +1,6 @@
1
+ import * as React from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
1
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ReactNode } from 'react';
3
4
 
4
5
  interface FileDropzoneBaseProps {
5
6
  accept?: string;
@@ -29,8 +30,70 @@ type FileDropzoneProps = SingleFileDropzoneProps | MultiFileDropzoneProps;
29
30
  *
30
31
  * The primitive owns the hidden native input, focus state, drag state and
31
32
  * async busy state. It deliberately does not parse, upload or validate file
32
- * contents; those remain consumer concerns.
33
+ * contents; those remain consumer concerns. It installs `useFileDropGuard`,
34
+ * so a file dropped just beside it never opens in the browser.
33
35
  */
34
36
  declare function FileDropzone({ onFile, onFiles, multiple, accept, disabled, busy: externalBusy, label, description, busyLabel, icon, variant, className, }: FileDropzoneProps): react_jsx_runtime.JSX.Element;
37
+ /**
38
+ * Makes sure a file dropped where nothing takes it never opens or downloads
39
+ * in the browser, which is what a browser does by default with a file
40
+ * dropped on a page. Returns the function that removes it again.
41
+ *
42
+ * Listens on `window`, after every other handler: a drag or a drop that a
43
+ * drop target has already accepted (`preventDefault`) is left alone, and so
44
+ * is a native `<input type="file">`, which takes its own drop. Any other file
45
+ * drag is cancelled, with the "no drop" cursor.
46
+ *
47
+ * Call it once in an app's entry, before rendering: it lives outside React,
48
+ * so a render error, an error boundary's fallback or an unmounted root never
49
+ * takes it away. `useFileDropGuard` is the same guard for a component's
50
+ * lifetime.
51
+ */
52
+ declare function installFileDropGuard(): () => void;
53
+ /**
54
+ * `installFileDropGuard` while the component is mounted (and `enabled`).
55
+ * `FileDropzone` and `FileDropArea` install it themselves. For a whole app,
56
+ * prefer `installFileDropGuard()` in the entry: a hook in the app's root
57
+ * component goes when a render error unmounts it.
58
+ */
59
+ declare function useFileDropGuard(enabled?: boolean): void;
60
+ interface FileDropAreaProps extends Omit<HTMLAttributes<HTMLDivElement>, "onDrop"> {
61
+ /**
62
+ * Every dropped file, unfiltered: the callback the region's file picker
63
+ * calls, so the caller validates types, sizes and counts and reports its
64
+ * own errors. Without it the area is a plain div and installs no guard.
65
+ */
66
+ onFiles?: (files: File[]) => void | Promise<void>;
67
+ /** The overlay stays down and drops are ignored; the page guard stays on. */
68
+ disabled?: boolean;
69
+ /**
70
+ * `area` (the default): a file dragged over this element raises an overlay
71
+ * over it, and a drop on it lands here. `page`: a file dragged anywhere in
72
+ * the document raises an overlay over the whole viewport, and a drop
73
+ * anywhere lands here.
74
+ */
75
+ scope?: "area" | "page";
76
+ /** The overlay's label. Defaults to "Drop to attach". */
77
+ label?: ReactNode;
78
+ /** A muted line under the label, such as the accepted types and limits. */
79
+ description?: ReactNode;
80
+ /** The overlay's icon. Defaults to the upload icon `FileDropzone` shows. */
81
+ icon?: ReactNode;
82
+ }
83
+ /**
84
+ * A region that takes files dragged onto it. While a file is dragged over
85
+ * it, an overlay covers it ("Drop to attach"), and a drop hands every file to
86
+ * `onFiles`, the callback its file picker already calls. A file dropped
87
+ * anywhere else in the page never opens in the browser (`useFileDropGuard`).
88
+ *
89
+ * The label stays on screen however the region sits: an area taller than the
90
+ * viewport centres it in the part that shows, and `scope="page"` covers the
91
+ * viewport itself, wherever in the page the region is.
92
+ *
93
+ * The overlay is decorative (`aria-hidden`): keep a picker in the region as
94
+ * the accessible way to add files. A drop target inside it (a
95
+ * `FileDropzone`) keeps its own drops, so a file is never delivered twice.
96
+ */
97
+ declare const FileDropArea: React.ForwardRefExoticComponent<FileDropAreaProps & React.RefAttributes<HTMLDivElement>>;
35
98
 
36
- export { FileDropzone, type FileDropzoneProps };
99
+ export { FileDropArea, type FileDropAreaProps, FileDropzone, type FileDropzoneProps, installFileDropGuard, useFileDropGuard };
@@ -1,5 +1,6 @@
1
+ import * as React from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
1
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ReactNode } from 'react';
3
4
 
4
5
  interface FileDropzoneBaseProps {
5
6
  accept?: string;
@@ -29,8 +30,70 @@ type FileDropzoneProps = SingleFileDropzoneProps | MultiFileDropzoneProps;
29
30
  *
30
31
  * The primitive owns the hidden native input, focus state, drag state and
31
32
  * async busy state. It deliberately does not parse, upload or validate file
32
- * contents; those remain consumer concerns.
33
+ * contents; those remain consumer concerns. It installs `useFileDropGuard`,
34
+ * so a file dropped just beside it never opens in the browser.
33
35
  */
34
36
  declare function FileDropzone({ onFile, onFiles, multiple, accept, disabled, busy: externalBusy, label, description, busyLabel, icon, variant, className, }: FileDropzoneProps): react_jsx_runtime.JSX.Element;
37
+ /**
38
+ * Makes sure a file dropped where nothing takes it never opens or downloads
39
+ * in the browser, which is what a browser does by default with a file
40
+ * dropped on a page. Returns the function that removes it again.
41
+ *
42
+ * Listens on `window`, after every other handler: a drag or a drop that a
43
+ * drop target has already accepted (`preventDefault`) is left alone, and so
44
+ * is a native `<input type="file">`, which takes its own drop. Any other file
45
+ * drag is cancelled, with the "no drop" cursor.
46
+ *
47
+ * Call it once in an app's entry, before rendering: it lives outside React,
48
+ * so a render error, an error boundary's fallback or an unmounted root never
49
+ * takes it away. `useFileDropGuard` is the same guard for a component's
50
+ * lifetime.
51
+ */
52
+ declare function installFileDropGuard(): () => void;
53
+ /**
54
+ * `installFileDropGuard` while the component is mounted (and `enabled`).
55
+ * `FileDropzone` and `FileDropArea` install it themselves. For a whole app,
56
+ * prefer `installFileDropGuard()` in the entry: a hook in the app's root
57
+ * component goes when a render error unmounts it.
58
+ */
59
+ declare function useFileDropGuard(enabled?: boolean): void;
60
+ interface FileDropAreaProps extends Omit<HTMLAttributes<HTMLDivElement>, "onDrop"> {
61
+ /**
62
+ * Every dropped file, unfiltered: the callback the region's file picker
63
+ * calls, so the caller validates types, sizes and counts and reports its
64
+ * own errors. Without it the area is a plain div and installs no guard.
65
+ */
66
+ onFiles?: (files: File[]) => void | Promise<void>;
67
+ /** The overlay stays down and drops are ignored; the page guard stays on. */
68
+ disabled?: boolean;
69
+ /**
70
+ * `area` (the default): a file dragged over this element raises an overlay
71
+ * over it, and a drop on it lands here. `page`: a file dragged anywhere in
72
+ * the document raises an overlay over the whole viewport, and a drop
73
+ * anywhere lands here.
74
+ */
75
+ scope?: "area" | "page";
76
+ /** The overlay's label. Defaults to "Drop to attach". */
77
+ label?: ReactNode;
78
+ /** A muted line under the label, such as the accepted types and limits. */
79
+ description?: ReactNode;
80
+ /** The overlay's icon. Defaults to the upload icon `FileDropzone` shows. */
81
+ icon?: ReactNode;
82
+ }
83
+ /**
84
+ * A region that takes files dragged onto it. While a file is dragged over
85
+ * it, an overlay covers it ("Drop to attach"), and a drop hands every file to
86
+ * `onFiles`, the callback its file picker already calls. A file dropped
87
+ * anywhere else in the page never opens in the browser (`useFileDropGuard`).
88
+ *
89
+ * The label stays on screen however the region sits: an area taller than the
90
+ * viewport centres it in the part that shows, and `scope="page"` covers the
91
+ * viewport itself, wherever in the page the region is.
92
+ *
93
+ * The overlay is decorative (`aria-hidden`): keep a picker in the region as
94
+ * the accessible way to add files. A drop target inside it (a
95
+ * `FileDropzone`) keeps its own drops, so a file is never delivered twice.
96
+ */
97
+ declare const FileDropArea: React.ForwardRefExoticComponent<FileDropAreaProps & React.RefAttributes<HTMLDivElement>>;
35
98
 
36
- export { FileDropzone, type FileDropzoneProps };
99
+ export { FileDropArea, type FileDropAreaProps, FileDropzone, type FileDropzoneProps, installFileDropGuard, useFileDropGuard };
@@ -1,12 +1,18 @@
1
1
  "use client";
2
2
  "use client";
3
3
  import {
4
- FileDropzone
5
- } from "../chunk-6V4XMQ6D.js";
4
+ FileDropArea,
5
+ FileDropzone,
6
+ installFileDropGuard,
7
+ useFileDropGuard
8
+ } from "../chunk-M7FYGAGO.js";
6
9
  import "../chunk-EIY5LLSP.js";
7
10
  import "../chunk-ZRLUUZHK.js";
8
11
  import "../chunk-HQRWRRZJ.js";
9
12
  import "../chunk-G6SRSWXB.js";
10
13
  export {
11
- FileDropzone
14
+ FileDropArea,
15
+ FileDropzone,
16
+ installFileDropGuard,
17
+ useFileDropGuard
12
18
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicbagel/saas-kit",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Accessible React primitives, layout components and design tokens for SaaS apps, built on Radix UI and Tailwind CSS v4.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -1,118 +0,0 @@
1
- "use client";
2
- import {
3
- Text
4
- } from "./chunk-EIY5LLSP.js";
5
- import {
6
- Tooltip,
7
- TooltipContent,
8
- TooltipTrigger
9
- } from "./chunk-ZRLUUZHK.js";
10
- import {
11
- Button
12
- } from "./chunk-HQRWRRZJ.js";
13
- import {
14
- cn
15
- } from "./chunk-G6SRSWXB.js";
16
-
17
- // src/primitives/file-dropzone.tsx
18
- import { FileUp, Loader2 } from "lucide-react";
19
- import { useRef, useState } from "react";
20
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
21
- function FileDropzone({
22
- onFile,
23
- onFiles,
24
- multiple = false,
25
- accept,
26
- disabled = false,
27
- busy: externalBusy = false,
28
- label = "Drop a file, or choose one",
29
- description,
30
- busyLabel = "Reading file\u2026",
31
- icon,
32
- variant = "dropzone",
33
- className
34
- }) {
35
- const inputRef = useRef(null);
36
- const [dragging, setDragging] = useState(false);
37
- const [internalBusy, setInternalBusy] = useState(false);
38
- const busy = externalBusy || internalBusy;
39
- async function select(files) {
40
- if (files.length === 0 || disabled || busy) return;
41
- setInternalBusy(true);
42
- try {
43
- if (multiple) {
44
- await onFiles?.(files);
45
- } else {
46
- const first = files[0];
47
- if (first) await onFile?.(first);
48
- }
49
- } finally {
50
- setInternalBusy(false);
51
- if (inputRef.current) inputRef.current.value = "";
52
- }
53
- }
54
- function onDrop(event) {
55
- event.preventDefault();
56
- setDragging(false);
57
- void select(Array.from(event.dataTransfer.files ?? []));
58
- }
59
- const trigger = /* @__PURE__ */ jsxs(
60
- Button,
61
- {
62
- type: "button",
63
- "data-file-dropzone": "true",
64
- variant: "outline",
65
- size: variant === "icon" ? "icon" : "default",
66
- "aria-label": variant === "icon" ? String(busy ? busyLabel : label) : void 0,
67
- disabled: disabled || busy,
68
- onClick: () => inputRef.current?.click(),
69
- onDragEnter: (event) => {
70
- event.preventDefault();
71
- setDragging(true);
72
- },
73
- onDragOver: (event) => {
74
- event.preventDefault();
75
- setDragging(true);
76
- },
77
- onDragLeave: () => setDragging(false),
78
- onDrop,
79
- className: cn(
80
- variant === "dropzone" ? "h-auto w-full flex-col whitespace-normal border-2 border-dashed px-6 py-8 text-center" : variant === "button" ? "w-fit" : "",
81
- dragging && "border-primary bg-primary/5",
82
- className
83
- ),
84
- children: [
85
- /* @__PURE__ */ jsx("span", { className: cn(
86
- "flex items-center justify-center text-muted-foreground",
87
- variant === "dropzone" && "mb-2 h-10 w-10 rounded-full bg-muted"
88
- ), children: busy ? /* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 animate-spin" }) : icon ?? /* @__PURE__ */ jsx(FileUp, { className: "h-5 w-5" }) }),
89
- /* @__PURE__ */ jsx("span", { className: variant === "icon" ? "sr-only" : "font-medium", children: busy ? busyLabel : label }),
90
- description && variant === "dropzone" && /* @__PURE__ */ jsx(Text, { variant: "muted", className: "mt-1 max-w-md text-xs", children: description })
91
- ]
92
- }
93
- );
94
- return /* @__PURE__ */ jsxs(Fragment, { children: [
95
- /* @__PURE__ */ jsx(
96
- "input",
97
- {
98
- ref: inputRef,
99
- type: "file",
100
- accept,
101
- multiple,
102
- disabled: disabled || busy,
103
- onChange: (event) => void select(Array.from(event.target.files ?? [])),
104
- className: "sr-only",
105
- tabIndex: -1,
106
- "aria-hidden": "true"
107
- }
108
- ),
109
- variant === "icon" && description ? /* @__PURE__ */ jsxs(Tooltip, { children: [
110
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: trigger }),
111
- /* @__PURE__ */ jsx(TooltipContent, { children: description })
112
- ] }) : trigger
113
- ] });
114
- }
115
-
116
- export {
117
- FileDropzone
118
- };
@@ -1,118 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
2
-
3
-
4
- var _chunk373W6VADcjs = require('./chunk-373W6VAD.cjs');
5
-
6
-
7
-
8
-
9
- var _chunkU2NES7VRcjs = require('./chunk-U2NES7VR.cjs');
10
-
11
-
12
- var _chunkL4DH72UFcjs = require('./chunk-L4DH72UF.cjs');
13
-
14
-
15
- var _chunkKWLSBTB2cjs = require('./chunk-KWLSBTB2.cjs');
16
-
17
- // src/primitives/file-dropzone.tsx
18
- var _lucidereact = require('lucide-react');
19
- var _react = require('react');
20
- var _jsxruntime = require('react/jsx-runtime');
21
- function FileDropzone({
22
- onFile,
23
- onFiles,
24
- multiple = false,
25
- accept,
26
- disabled = false,
27
- busy: externalBusy = false,
28
- label = "Drop a file, or choose one",
29
- description,
30
- busyLabel = "Reading file\u2026",
31
- icon,
32
- variant = "dropzone",
33
- className
34
- }) {
35
- const inputRef = _react.useRef.call(void 0, null);
36
- const [dragging, setDragging] = _react.useState.call(void 0, false);
37
- const [internalBusy, setInternalBusy] = _react.useState.call(void 0, false);
38
- const busy = externalBusy || internalBusy;
39
- async function select(files) {
40
- if (files.length === 0 || disabled || busy) return;
41
- setInternalBusy(true);
42
- try {
43
- if (multiple) {
44
- await _optionalChain([onFiles, 'optionalCall', _ => _(files)]);
45
- } else {
46
- const first = files[0];
47
- if (first) await _optionalChain([onFile, 'optionalCall', _2 => _2(first)]);
48
- }
49
- } finally {
50
- setInternalBusy(false);
51
- if (inputRef.current) inputRef.current.value = "";
52
- }
53
- }
54
- function onDrop(event) {
55
- event.preventDefault();
56
- setDragging(false);
57
- void select(Array.from(_nullishCoalesce(event.dataTransfer.files, () => ( []))));
58
- }
59
- const trigger = /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
60
- _chunkL4DH72UFcjs.Button,
61
- {
62
- type: "button",
63
- "data-file-dropzone": "true",
64
- variant: "outline",
65
- size: variant === "icon" ? "icon" : "default",
66
- "aria-label": variant === "icon" ? String(busy ? busyLabel : label) : void 0,
67
- disabled: disabled || busy,
68
- onClick: () => _optionalChain([inputRef, 'access', _3 => _3.current, 'optionalAccess', _4 => _4.click, 'call', _5 => _5()]),
69
- onDragEnter: (event) => {
70
- event.preventDefault();
71
- setDragging(true);
72
- },
73
- onDragOver: (event) => {
74
- event.preventDefault();
75
- setDragging(true);
76
- },
77
- onDragLeave: () => setDragging(false),
78
- onDrop,
79
- className: _chunkKWLSBTB2cjs.cn.call(void 0,
80
- variant === "dropzone" ? "h-auto w-full flex-col whitespace-normal border-2 border-dashed px-6 py-8 text-center" : variant === "button" ? "w-fit" : "",
81
- dragging && "border-primary bg-primary/5",
82
- className
83
- ),
84
- children: [
85
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: _chunkKWLSBTB2cjs.cn.call(void 0,
86
- "flex items-center justify-center text-muted-foreground",
87
- variant === "dropzone" && "mb-2 h-10 w-10 rounded-full bg-muted"
88
- ), children: busy ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.Loader2, { className: "h-5 w-5 animate-spin" }) : _nullishCoalesce(icon, () => ( /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.FileUp, { className: "h-5 w-5" }))) }),
89
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: variant === "icon" ? "sr-only" : "font-medium", children: busy ? busyLabel : label }),
90
- description && variant === "dropzone" && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk373W6VADcjs.Text, { variant: "muted", className: "mt-1 max-w-md text-xs", children: description })
91
- ]
92
- }
93
- );
94
- return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
95
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
96
- "input",
97
- {
98
- ref: inputRef,
99
- type: "file",
100
- accept,
101
- multiple,
102
- disabled: disabled || busy,
103
- onChange: (event) => void select(Array.from(_nullishCoalesce(event.target.files, () => ( [])))),
104
- className: "sr-only",
105
- tabIndex: -1,
106
- "aria-hidden": "true"
107
- }
108
- ),
109
- variant === "icon" && description ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkU2NES7VRcjs.Tooltip, { children: [
110
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkU2NES7VRcjs.TooltipTrigger, { asChild: true, children: trigger }),
111
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkU2NES7VRcjs.TooltipContent, { children: description })
112
- ] }) : trigger
113
- ] });
114
- }
115
-
116
-
117
-
118
- exports.FileDropzone = FileDropzone;