@crisp-ui-kit/crisp 0.7.5 → 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.
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ "use client";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.fileUploadLabels = void 0;
38
+ exports.FileUpload = FileUpload;
39
+ const jsx_runtime_1 = require("react/jsx-runtime");
40
+ const react_file_upload_1 = require("@seed-design/react-file-upload");
41
+ const lucide_react_1 = require("lucide-react");
42
+ const React = __importStar(require("react"));
43
+ const fileupload_recipe_1 = require("./fileupload.recipe");
44
+ exports.fileUploadLabels = {
45
+ // Attio's own copy on the record Files pane.
46
+ title: "No files",
47
+ hint: "Drag a file here or choose one from your computer.",
48
+ remove: "Remove file",
49
+ errors: {
50
+ FILE_TOO_LARGE: "That file is too large.",
51
+ FILE_TOO_SMALL: "That file is too small.",
52
+ TOO_MANY_FILES: "Too many files.",
53
+ INVALID_TYPE: "That file type is not accepted.",
54
+ },
55
+ };
56
+ /** 1000-based, matching how every file manager reports size. */
57
+ function formatSize(bytes) {
58
+ if (bytes < 1000)
59
+ return `${bytes} B`;
60
+ const units = ["kB", "MB", "GB", "TB"];
61
+ let n = bytes;
62
+ let i = -1;
63
+ while (n >= 1000 && i < units.length - 1) {
64
+ n /= 1000;
65
+ i++;
66
+ }
67
+ return `${n < 10 ? n.toFixed(1) : Math.round(n)} ${units[i]}`;
68
+ }
69
+ /**
70
+ * A file dropzone with an accepted-file list, driven by seed-design's headless
71
+ * engine so the behaviour — drag state, accept/reject, per-file upload status —
72
+ * is theirs by construction. Controlled or uncontrolled, the same way every
73
+ * other crisp component is.
74
+ */
75
+ function FileUpload({ className, labels, "aria-label": ariaLabel, accept, disabled: disabledProp, required, invalid: invalidProp, readOnly, maxFiles, maxFileSize, minFileSize, name, validate, preventDocumentDrop, onFileReject, onFileAccept, acceptedFileEntries: acceptedFileEntriesProp, defaultAcceptedFileEntries, onAcceptedFileEntriesChange, ...rest }) {
76
+ const props = {
77
+ accept,
78
+ disabled: disabledProp,
79
+ required,
80
+ invalid: invalidProp,
81
+ readOnly,
82
+ maxFiles,
83
+ maxFileSize,
84
+ minFileSize,
85
+ name,
86
+ validate,
87
+ preventDocumentDrop,
88
+ onFileReject,
89
+ onFileAccept,
90
+ acceptedFileEntries: acceptedFileEntriesProp,
91
+ defaultAcceptedFileEntries,
92
+ onAcceptedFileEntriesChange,
93
+ };
94
+ const [rejections, setRejections] = React.useState([]);
95
+ const t = {
96
+ ...exports.fileUploadLabels,
97
+ ...labels,
98
+ errors: { ...exports.fileUploadLabels.errors, ...labels?.errors },
99
+ };
100
+ const api = (0, react_file_upload_1.useFileUpload)({
101
+ ...props,
102
+ onFileReject: (r) => {
103
+ setRejections(r);
104
+ props.onFileReject?.(r);
105
+ },
106
+ onFileAccept: (entries, helpers) => {
107
+ setRejections([]);
108
+ props.onFileAccept?.(entries, helpers);
109
+ },
110
+ });
111
+ const { acceptedFileEntries, dragging, disabled, invalid, openFilePicker, removeFileEntry, dropzoneProps, hiddenInputProps,
112
+ // The prop getters deliberately exclude the refs -- the engine's own
113
+ // FileUploadDropzone/HiddenInput compose them. Rendering crisp's markup
114
+ // instead means attaching them here; without inputRef, `openFilePicker`
115
+ // finds no input and returns silently, so the dropzone opens nothing.
116
+ inputRef, dropzoneRef, } = api;
117
+ return ((0, jsx_runtime_1.jsxs)("div", { ...rest, className: (0, fileupload_recipe_1.fileuploadRecipe)({ className }), children: [(0, jsx_runtime_1.jsxs)("button", { ...dropzoneProps, ref: dropzoneRef, type: "button", className: "fileupload-zone", tabIndex: disabled ? -1 : 0, "aria-label": ariaLabel ?? t.hint, disabled: disabled, "data-dragging": dragging ? "" : undefined, "data-disabled": disabled ? "" : undefined, "data-invalid": invalid ? "" : undefined, onClick: () => openFilePicker(), children: [(0, jsx_runtime_1.jsx)("span", { className: "fileupload-title", children: t.title }), (0, jsx_runtime_1.jsx)("span", { className: "fileupload-hint", children: t.hint })] }), (0, jsx_runtime_1.jsx)("input", { ...hiddenInputProps, ref: inputRef, className: "fileupload-input" }), acceptedFileEntries.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { className: "fileupload-list", children: acceptedFileEntries.map((entry) => ((0, jsx_runtime_1.jsxs)("li", { className: "fileupload-item", "data-status": entry.status, children: [(0, jsx_runtime_1.jsx)("span", { className: "fileupload-item-name", children: entry.file.name }), entry.status === "uploading" && entry.progress != null && ((0, jsx_runtime_1.jsx)("span", { className: "fileupload-progress", role: "progressbar", "aria-valuenow": Math.round(entry.progress), "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": entry.file.name, children: (0, jsx_runtime_1.jsx)("span", { className: "fileupload-progress-fill", style: { width: `${entry.progress}%` } }) })), (0, jsx_runtime_1.jsx)("span", { className: "fileupload-item-size", children: formatSize(entry.file.size) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "fileupload-remove", "aria-label": `${t.remove}: ${entry.file.name}`, onClick: () => removeFileEntry(entry.id), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2 }) })] }, entry.id))) })), rejections.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { className: "fileupload-errors", "aria-live": "polite", children: rejections.map((r) => ((0, jsx_runtime_1.jsx)("li", { className: "fileupload-error", children: `${r.file.name}: ${r.errors.map((e) => t.errors[e] ?? e).join(" ")}` }, r.file.name))) }))] }));
118
+ }
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fileuploadRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // seed-design's file-upload behaviour on Attio's measured scale.
7
+ //
8
+ // Measured from Attio's record Files pane (Deal record → Files, empty state):
9
+ // the title is 16/20 at weight 600 in `rgb(16,17,18)` (= gray900 = fgPrimary),
10
+ // the hint 14/20 at weight 500 in `rgba(0,0,0,0.63)`, and the action button is
11
+ // 28px tall with an 8px corner.
12
+ //
13
+ // Attio draws NO boundary around that pane -- the whole surface is the drop
14
+ // target -- but a reusable dropzone needs an edge, so the dashed affordance is
15
+ // crisp's own measured one from `sidebar-newlist` (1px dashed borderDefault,
16
+ // r8, bgRaised on hover) rather than a new invention. Attio's drag-OVER
17
+ // treatment could not be measured: synthetic DragEvents do not trigger its
18
+ // handlers. The active state therefore reuses the brand emphasis crisp already
19
+ // uses for a live drag affordance (`appshell-resize[data-dragging]`).
20
+ const config = (0, core_1.defineRecipe)({
21
+ name: "fileupload",
22
+ base: `
23
+ display: flex; flex-direction: column; gap: ${tokens_1.contract.space["2"]};
24
+ font-family: ${tokens_1.contract.font.sans}; min-width: 0;
25
+
26
+ & .fileupload-zone {
27
+ box-sizing: border-box; position: relative;
28
+ display: flex; flex-direction: column; align-items: center;
29
+ justify-content: center; gap: ${tokens_1.contract.space["1"]};
30
+ width: 100%; padding: ${tokens_1.contract.space["6"]} ${tokens_1.contract.space["4"]};
31
+ border: 1px dashed ${tokens_1.contract.borderDefault}; border-radius: ${tokens_1.contract.radius.md};
32
+ background: transparent; cursor: pointer; text-align: center;
33
+ font-family: inherit;
34
+ transition: background-color .12s, border-color .12s;
35
+ outline: 2px solid transparent; outline-offset: -2px;
36
+ }
37
+ & .fileupload-zone:hover { background: ${tokens_1.contract.bgRaised}; }
38
+ & .fileupload-zone:focus-visible { outline-color: ${tokens_1.contract.strokeFocusRing}; }
39
+ & .fileupload-zone[data-dragging] {
40
+ border-color: ${tokens_1.contract.bgBrandSolid};
41
+ background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 6%, transparent);
42
+ }
43
+ & .fileupload-zone[data-disabled] { opacity: .5; cursor: not-allowed; }
44
+ & .fileupload-zone[data-invalid] { border-color: ${tokens_1.contract.bgDanger}; }
45
+
46
+ /* Attio: "No files" 16/20 weight 600 */
47
+ & .fileupload-title {
48
+ color: ${tokens_1.contract.fgPrimary}; font-size: ${tokens_1.contract.text.xl}; line-height: 20px;
49
+ font-weight: ${tokens_1.contract.weight.semibold};
50
+ }
51
+ /* Attio: the hint line, 14/20 weight 500 at 63% of the foreground */
52
+ & .fileupload-hint {
53
+ color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent);
54
+ font-size: ${tokens_1.contract.text.lg}; line-height: 20px;
55
+ font-weight: ${tokens_1.contract.weight.medium};
56
+ }
57
+
58
+ & .fileupload-input {
59
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
60
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
61
+ }
62
+
63
+ & .fileupload-list { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["1"]}; margin: 0; padding: 0; list-style: none; }
64
+ /* the row height crisp measured for a data-grid row */
65
+ & .fileupload-item {
66
+ display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]};
67
+ min-height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["2"]};
68
+ border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgRaised};
69
+ font-size: ${tokens_1.contract.text.lg}; line-height: 20px; color: ${tokens_1.contract.fgPrimary};
70
+ }
71
+ & .fileupload-item-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
72
+ & .fileupload-item-size { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; font-size: ${tokens_1.contract.text.sm}; font-variant-numeric: tabular-nums; }
73
+ & .fileupload-item[data-status="error"] { color: ${tokens_1.contract.bgDanger}; }
74
+ & .fileupload-item[data-status="error"] .fileupload-item-size { color: ${tokens_1.contract.bgDanger}; }
75
+
76
+ /* progress track, reusing the measured Progress geometry */
77
+ & .fileupload-progress {
78
+ flex-shrink: 0; width: 64px; height: 4px; border-radius: ${tokens_1.contract.radius.full};
79
+ background: ${tokens_1.contract.bgNeutralWeak}; overflow: hidden;
80
+ }
81
+ & .fileupload-progress-fill { height: 100%; background: ${tokens_1.contract.bgBrandSolid}; transition: width .2s ease; }
82
+
83
+ & .fileupload-remove {
84
+ display: inline-flex; align-items: center; justify-content: center;
85
+ flex-shrink: 0; width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon};
86
+ padding: 0; border: none; border-radius: ${tokens_1.contract.radius.sm};
87
+ background: transparent; color: ${tokens_1.contract.fgMuted}; cursor: pointer;
88
+ font-family: inherit;
89
+ outline: 2px solid transparent; outline-offset: -2px;
90
+ }
91
+ & .fileupload-remove:hover { color: ${tokens_1.contract.fgPrimary}; }
92
+ & .fileupload-remove:focus-visible { outline-color: ${tokens_1.contract.strokeFocusRing}; }
93
+
94
+ & .fileupload-errors { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["0.5"]}; margin: 0; padding: 0; list-style: none; }
95
+ & .fileupload-error { color: ${tokens_1.contract.bgDanger}; font-size: ${tokens_1.contract.text.md}; line-height: 18px; }
96
+ `,
97
+ variants: {},
98
+ defaultVariants: {},
99
+ compoundVariants: [],
100
+ });
101
+ exports.fileuploadRecipe = Object.assign((0, core_1.resolveRecipe)(config), {
102
+ config,
103
+ });
104
+ exports.default = config;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./fileupload"), exports);
18
+ __exportStar(require("./fileupload.recipe"), exports);
package/dist/cjs/index.js CHANGED
@@ -39,6 +39,7 @@ __exportStar(require("./components/divider"), exports);
39
39
  __exportStar(require("./components/donutchart"), exports);
40
40
  __exportStar(require("./components/drawer"), exports);
41
41
  __exportStar(require("./components/emptystate"), exports);
42
+ __exportStar(require("./components/fileupload"), exports);
42
43
  __exportStar(require("./components/formfield"), exports);
43
44
  __exportStar(require("./components/input"), exports);
44
45
  __exportStar(require("./components/kbd"), exports);
@@ -394,6 +394,15 @@ function CreateRecordDialog({ open, onOpenChange, columns, entityLabel, onCreate
394
394
  function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, rows: rowsProp = exports.defaultRows, selectedIds, onSelectionChange, showNew = true, allowAddColumn = true, onOpenRecord, onOpenRecordPage, avatarTone = "brand", className, }) {
395
395
  const [cols, setCols] = React.useState(columnsProp);
396
396
  const [data, setData] = React.useState(rowsProp);
397
+ // The table owns `cols`/`data` so it can rename a column, edit a cell, or
398
+ // append a record without a round trip. That state is seeded from the props,
399
+ // so without these it would be a mount-time SNAPSHOT: an app that renders
400
+ // `rows={[]}` and re-renders with fetched rows would show "No records yet"
401
+ // for ever. Re-seed whenever the caller hands us a new array -- caller wins,
402
+ // exactly as DataTable does for its columns. Local edits survive until then,
403
+ // because a caller passing a stable array never triggers this.
404
+ React.useEffect(() => setCols(columnsProp), [columnsProp]);
405
+ React.useEffect(() => setData(rowsProp), [rowsProp]);
397
406
  // Attio's multi-criteria sort: an ordered list of {field, direction} rows.
398
407
  const [sorts, setSorts] = React.useState([]);
399
408
  const [dragSort, setDragSort] = React.useState(null);
@@ -607,7 +616,20 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
607
616
  };
608
617
  const allSelected = visibleRows.length > 0 && visibleRows.every((r) => selected.has(r.id));
609
618
  const someSelected = selected.size > 0 && !allSelected;
610
- const toggleAll = () => setSelected(allSelected ? new Set() : new Set(visibleRows.map((r) => r.id)));
619
+ // The header checkbox speaks for the rows you can SEE. Rebuilding the whole
620
+ // set from `visibleRows` would silently drop anything an active filter is
621
+ // hiding -- on a surface with a bulk Delete, that loses a selection the user
622
+ // made. Add/remove the visible rows and leave the rest of the set alone.
623
+ const toggleAll = () => {
624
+ const next = new Set(selected);
625
+ for (const r of visibleRows) {
626
+ if (allSelected)
627
+ next.delete(r.id);
628
+ else
629
+ next.add(r.id);
630
+ }
631
+ setSelected(next);
632
+ };
611
633
  const toggleRow = (id) => {
612
634
  const next = new Set(selected);
613
635
  // shift-click → select the contiguous range from the anchor to this row
@@ -804,7 +826,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
804
826
  URL.revokeObjectURL(url);
805
827
  setOpenMenu(null);
806
828
  };
807
- const entityKey = cols.find((c) => c.type === "entity")?.key;
829
+ const entityCol = cols.find((c) => c.type === "entity");
808
830
  // "Add to list" → pin the selected rows (shows a ★ on the entity)
809
831
  const addToList = () => {
810
832
  setPinned((prev) => new Set([...prev, ...selected]));
@@ -814,7 +836,10 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
814
836
  const sendEmail = () => {
815
837
  const names = data
816
838
  .filter((r) => selected.has(r.id))
817
- .map((r) => entityKey ? cellText({ key: entityKey, label: "" }, r) : r.id);
839
+ // Pass the real column: `cellText` -> `rawText` reads `col.type` to pull
840
+ // an entity's `.name`, so a `{key, label}` stand-in with no `type` falls
841
+ // through to String(value) and yields "[object Object]".
842
+ .map((r) => (entityCol ? cellText(entityCol, r) : r.id));
818
843
  window.open(`mailto:?subject=${encodeURIComponent(names.join(", "))}`);
819
844
  };
820
845
  // "Run workflow" → a demo automation: duplicate the selected records
@@ -0,0 +1,81 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useFileUpload, } from "@seed-design/react-file-upload";
4
+ import { X } from "lucide-react";
5
+ import * as React from "react";
6
+ import { fileuploadRecipe } from "./fileupload.recipe.js";
7
+ export const fileUploadLabels = {
8
+ // Attio's own copy on the record Files pane.
9
+ title: "No files",
10
+ hint: "Drag a file here or choose one from your computer.",
11
+ remove: "Remove file",
12
+ errors: {
13
+ FILE_TOO_LARGE: "That file is too large.",
14
+ FILE_TOO_SMALL: "That file is too small.",
15
+ TOO_MANY_FILES: "Too many files.",
16
+ INVALID_TYPE: "That file type is not accepted.",
17
+ },
18
+ };
19
+ /** 1000-based, matching how every file manager reports size. */
20
+ function formatSize(bytes) {
21
+ if (bytes < 1000)
22
+ return `${bytes} B`;
23
+ const units = ["kB", "MB", "GB", "TB"];
24
+ let n = bytes;
25
+ let i = -1;
26
+ while (n >= 1000 && i < units.length - 1) {
27
+ n /= 1000;
28
+ i++;
29
+ }
30
+ return `${n < 10 ? n.toFixed(1) : Math.round(n)} ${units[i]}`;
31
+ }
32
+ /**
33
+ * A file dropzone with an accepted-file list, driven by seed-design's headless
34
+ * engine so the behaviour — drag state, accept/reject, per-file upload status —
35
+ * is theirs by construction. Controlled or uncontrolled, the same way every
36
+ * other crisp component is.
37
+ */
38
+ export function FileUpload({ className, labels, "aria-label": ariaLabel, accept, disabled: disabledProp, required, invalid: invalidProp, readOnly, maxFiles, maxFileSize, minFileSize, name, validate, preventDocumentDrop, onFileReject, onFileAccept, acceptedFileEntries: acceptedFileEntriesProp, defaultAcceptedFileEntries, onAcceptedFileEntriesChange, ...rest }) {
39
+ const props = {
40
+ accept,
41
+ disabled: disabledProp,
42
+ required,
43
+ invalid: invalidProp,
44
+ readOnly,
45
+ maxFiles,
46
+ maxFileSize,
47
+ minFileSize,
48
+ name,
49
+ validate,
50
+ preventDocumentDrop,
51
+ onFileReject,
52
+ onFileAccept,
53
+ acceptedFileEntries: acceptedFileEntriesProp,
54
+ defaultAcceptedFileEntries,
55
+ onAcceptedFileEntriesChange,
56
+ };
57
+ const [rejections, setRejections] = React.useState([]);
58
+ const t = {
59
+ ...fileUploadLabels,
60
+ ...labels,
61
+ errors: { ...fileUploadLabels.errors, ...labels?.errors },
62
+ };
63
+ const api = useFileUpload({
64
+ ...props,
65
+ onFileReject: (r) => {
66
+ setRejections(r);
67
+ props.onFileReject?.(r);
68
+ },
69
+ onFileAccept: (entries, helpers) => {
70
+ setRejections([]);
71
+ props.onFileAccept?.(entries, helpers);
72
+ },
73
+ });
74
+ const { acceptedFileEntries, dragging, disabled, invalid, openFilePicker, removeFileEntry, dropzoneProps, hiddenInputProps,
75
+ // The prop getters deliberately exclude the refs -- the engine's own
76
+ // FileUploadDropzone/HiddenInput compose them. Rendering crisp's markup
77
+ // instead means attaching them here; without inputRef, `openFilePicker`
78
+ // finds no input and returns silently, so the dropzone opens nothing.
79
+ inputRef, dropzoneRef, } = api;
80
+ return (_jsxs("div", { ...rest, className: fileuploadRecipe({ className }), children: [_jsxs("button", { ...dropzoneProps, ref: dropzoneRef, type: "button", className: "fileupload-zone", tabIndex: disabled ? -1 : 0, "aria-label": ariaLabel ?? t.hint, disabled: disabled, "data-dragging": dragging ? "" : undefined, "data-disabled": disabled ? "" : undefined, "data-invalid": invalid ? "" : undefined, onClick: () => openFilePicker(), children: [_jsx("span", { className: "fileupload-title", children: t.title }), _jsx("span", { className: "fileupload-hint", children: t.hint })] }), _jsx("input", { ...hiddenInputProps, ref: inputRef, className: "fileupload-input" }), acceptedFileEntries.length > 0 && (_jsx("ul", { className: "fileupload-list", children: acceptedFileEntries.map((entry) => (_jsxs("li", { className: "fileupload-item", "data-status": entry.status, children: [_jsx("span", { className: "fileupload-item-name", children: entry.file.name }), entry.status === "uploading" && entry.progress != null && (_jsx("span", { className: "fileupload-progress", role: "progressbar", "aria-valuenow": Math.round(entry.progress), "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": entry.file.name, children: _jsx("span", { className: "fileupload-progress-fill", style: { width: `${entry.progress}%` } }) })), _jsx("span", { className: "fileupload-item-size", children: formatSize(entry.file.size) }), _jsx("button", { type: "button", className: "fileupload-remove", "aria-label": `${t.remove}: ${entry.file.name}`, onClick: () => removeFileEntry(entry.id), children: _jsx(X, { size: 13, strokeWidth: 2 }) })] }, entry.id))) })), rejections.length > 0 && (_jsx("ul", { className: "fileupload-errors", "aria-live": "polite", children: rejections.map((r) => (_jsx("li", { className: "fileupload-error", children: `${r.file.name}: ${r.errors.map((e) => t.errors[e] ?? e).join(" ")}` }, r.file.name))) }))] }));
81
+ }
@@ -0,0 +1,101 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // seed-design's file-upload behaviour on Attio's measured scale.
4
+ //
5
+ // Measured from Attio's record Files pane (Deal record → Files, empty state):
6
+ // the title is 16/20 at weight 600 in `rgb(16,17,18)` (= gray900 = fgPrimary),
7
+ // the hint 14/20 at weight 500 in `rgba(0,0,0,0.63)`, and the action button is
8
+ // 28px tall with an 8px corner.
9
+ //
10
+ // Attio draws NO boundary around that pane -- the whole surface is the drop
11
+ // target -- but a reusable dropzone needs an edge, so the dashed affordance is
12
+ // crisp's own measured one from `sidebar-newlist` (1px dashed borderDefault,
13
+ // r8, bgRaised on hover) rather than a new invention. Attio's drag-OVER
14
+ // treatment could not be measured: synthetic DragEvents do not trigger its
15
+ // handlers. The active state therefore reuses the brand emphasis crisp already
16
+ // uses for a live drag affordance (`appshell-resize[data-dragging]`).
17
+ const config = defineRecipe({
18
+ name: "fileupload",
19
+ base: `
20
+ display: flex; flex-direction: column; gap: ${c.space["2"]};
21
+ font-family: ${c.font.sans}; min-width: 0;
22
+
23
+ & .fileupload-zone {
24
+ box-sizing: border-box; position: relative;
25
+ display: flex; flex-direction: column; align-items: center;
26
+ justify-content: center; gap: ${c.space["1"]};
27
+ width: 100%; padding: ${c.space["6"]} ${c.space["4"]};
28
+ border: 1px dashed ${c.borderDefault}; border-radius: ${c.radius.md};
29
+ background: transparent; cursor: pointer; text-align: center;
30
+ font-family: inherit;
31
+ transition: background-color .12s, border-color .12s;
32
+ outline: 2px solid transparent; outline-offset: -2px;
33
+ }
34
+ & .fileupload-zone:hover { background: ${c.bgRaised}; }
35
+ & .fileupload-zone:focus-visible { outline-color: ${c.strokeFocusRing}; }
36
+ & .fileupload-zone[data-dragging] {
37
+ border-color: ${c.bgBrandSolid};
38
+ background: color-mix(in srgb, ${c.bgBrandSolid} 6%, transparent);
39
+ }
40
+ & .fileupload-zone[data-disabled] { opacity: .5; cursor: not-allowed; }
41
+ & .fileupload-zone[data-invalid] { border-color: ${c.bgDanger}; }
42
+
43
+ /* Attio: "No files" 16/20 weight 600 */
44
+ & .fileupload-title {
45
+ color: ${c.fgPrimary}; font-size: ${c.text.xl}; line-height: 20px;
46
+ font-weight: ${c.weight.semibold};
47
+ }
48
+ /* Attio: the hint line, 14/20 weight 500 at 63% of the foreground */
49
+ & .fileupload-hint {
50
+ color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent);
51
+ font-size: ${c.text.lg}; line-height: 20px;
52
+ font-weight: ${c.weight.medium};
53
+ }
54
+
55
+ & .fileupload-input {
56
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
57
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
58
+ }
59
+
60
+ & .fileupload-list { display: flex; flex-direction: column; gap: ${c.space["1"]}; margin: 0; padding: 0; list-style: none; }
61
+ /* the row height crisp measured for a data-grid row */
62
+ & .fileupload-item {
63
+ display: flex; align-items: center; gap: ${c.space["2"]};
64
+ min-height: ${c.size.tableRow}; padding: 0 ${c.space["2"]};
65
+ border-radius: ${c.radius.md}; background: ${c.bgRaised};
66
+ font-size: ${c.text.lg}; line-height: 20px; color: ${c.fgPrimary};
67
+ }
68
+ & .fileupload-item-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
69
+ & .fileupload-item-size { flex-shrink: 0; color: ${c.fgMuted}; font-size: ${c.text.sm}; font-variant-numeric: tabular-nums; }
70
+ & .fileupload-item[data-status="error"] { color: ${c.bgDanger}; }
71
+ & .fileupload-item[data-status="error"] .fileupload-item-size { color: ${c.bgDanger}; }
72
+
73
+ /* progress track, reusing the measured Progress geometry */
74
+ & .fileupload-progress {
75
+ flex-shrink: 0; width: 64px; height: 4px; border-radius: ${c.radius.full};
76
+ background: ${c.bgNeutralWeak}; overflow: hidden;
77
+ }
78
+ & .fileupload-progress-fill { height: 100%; background: ${c.bgBrandSolid}; transition: width .2s ease; }
79
+
80
+ & .fileupload-remove {
81
+ display: inline-flex; align-items: center; justify-content: center;
82
+ flex-shrink: 0; width: ${c.size.icon}; height: ${c.size.icon};
83
+ padding: 0; border: none; border-radius: ${c.radius.sm};
84
+ background: transparent; color: ${c.fgMuted}; cursor: pointer;
85
+ font-family: inherit;
86
+ outline: 2px solid transparent; outline-offset: -2px;
87
+ }
88
+ & .fileupload-remove:hover { color: ${c.fgPrimary}; }
89
+ & .fileupload-remove:focus-visible { outline-color: ${c.strokeFocusRing}; }
90
+
91
+ & .fileupload-errors { display: flex; flex-direction: column; gap: ${c.space["0.5"]}; margin: 0; padding: 0; list-style: none; }
92
+ & .fileupload-error { color: ${c.bgDanger}; font-size: ${c.text.md}; line-height: 18px; }
93
+ `,
94
+ variants: {},
95
+ defaultVariants: {},
96
+ compoundVariants: [],
97
+ });
98
+ export const fileuploadRecipe = Object.assign(resolveRecipe(config), {
99
+ config,
100
+ });
101
+ export default config;
@@ -0,0 +1,2 @@
1
+ export * from "./fileupload.js";
2
+ export * from "./fileupload.recipe.js";
package/dist/esm/index.js CHANGED
@@ -23,6 +23,7 @@ export * from "./components/divider/index.js";
23
23
  export * from "./components/donutchart/index.js";
24
24
  export * from "./components/drawer/index.js";
25
25
  export * from "./components/emptystate/index.js";
26
+ export * from "./components/fileupload/index.js";
26
27
  export * from "./components/formfield/index.js";
27
28
  export * from "./components/input/index.js";
28
29
  export * from "./components/kbd/index.js";
@@ -356,6 +356,15 @@ function CreateRecordDialog({ open, onOpenChange, columns, entityLabel, onCreate
356
356
  export function RecordsTable({ labels, columns: columnsProp = defaultColumns, rows: rowsProp = defaultRows, selectedIds, onSelectionChange, showNew = true, allowAddColumn = true, onOpenRecord, onOpenRecordPage, avatarTone = "brand", className, }) {
357
357
  const [cols, setCols] = React.useState(columnsProp);
358
358
  const [data, setData] = React.useState(rowsProp);
359
+ // The table owns `cols`/`data` so it can rename a column, edit a cell, or
360
+ // append a record without a round trip. That state is seeded from the props,
361
+ // so without these it would be a mount-time SNAPSHOT: an app that renders
362
+ // `rows={[]}` and re-renders with fetched rows would show "No records yet"
363
+ // for ever. Re-seed whenever the caller hands us a new array -- caller wins,
364
+ // exactly as DataTable does for its columns. Local edits survive until then,
365
+ // because a caller passing a stable array never triggers this.
366
+ React.useEffect(() => setCols(columnsProp), [columnsProp]);
367
+ React.useEffect(() => setData(rowsProp), [rowsProp]);
359
368
  // Attio's multi-criteria sort: an ordered list of {field, direction} rows.
360
369
  const [sorts, setSorts] = React.useState([]);
361
370
  const [dragSort, setDragSort] = React.useState(null);
@@ -569,7 +578,20 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
569
578
  };
570
579
  const allSelected = visibleRows.length > 0 && visibleRows.every((r) => selected.has(r.id));
571
580
  const someSelected = selected.size > 0 && !allSelected;
572
- const toggleAll = () => setSelected(allSelected ? new Set() : new Set(visibleRows.map((r) => r.id)));
581
+ // The header checkbox speaks for the rows you can SEE. Rebuilding the whole
582
+ // set from `visibleRows` would silently drop anything an active filter is
583
+ // hiding -- on a surface with a bulk Delete, that loses a selection the user
584
+ // made. Add/remove the visible rows and leave the rest of the set alone.
585
+ const toggleAll = () => {
586
+ const next = new Set(selected);
587
+ for (const r of visibleRows) {
588
+ if (allSelected)
589
+ next.delete(r.id);
590
+ else
591
+ next.add(r.id);
592
+ }
593
+ setSelected(next);
594
+ };
573
595
  const toggleRow = (id) => {
574
596
  const next = new Set(selected);
575
597
  // shift-click → select the contiguous range from the anchor to this row
@@ -766,7 +788,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
766
788
  URL.revokeObjectURL(url);
767
789
  setOpenMenu(null);
768
790
  };
769
- const entityKey = cols.find((c) => c.type === "entity")?.key;
791
+ const entityCol = cols.find((c) => c.type === "entity");
770
792
  // "Add to list" → pin the selected rows (shows a ★ on the entity)
771
793
  const addToList = () => {
772
794
  setPinned((prev) => new Set([...prev, ...selected]));
@@ -776,7 +798,10 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
776
798
  const sendEmail = () => {
777
799
  const names = data
778
800
  .filter((r) => selected.has(r.id))
779
- .map((r) => entityKey ? cellText({ key: entityKey, label: "" }, r) : r.id);
801
+ // Pass the real column: `cellText` -> `rawText` reads `col.type` to pull
802
+ // an entity's `.name`, so a `{key, label}` stand-in with no `type` falls
803
+ // through to String(value) and yields "[object Object]".
804
+ .map((r) => (entityCol ? cellText(entityCol, r) : r.id));
780
805
  window.open(`mailto:?subject=${encodeURIComponent(names.join(", "))}`);
781
806
  };
782
807
  // "Run workflow" → a demo automation: duplicate the selected records
@@ -0,0 +1,85 @@
1
+ .fileupload {
2
+
3
+ display: flex; flex-direction: column; gap: var(--space-2);
4
+ font-family: var(--font-sans); min-width: 0;
5
+
6
+ & .fileupload-zone {
7
+ box-sizing: border-box; position: relative;
8
+ display: flex; flex-direction: column; align-items: center;
9
+ justify-content: center; gap: var(--space-1);
10
+ width: 100%; padding: var(--space-6) var(--space-4);
11
+ border: 1px dashed var(--border-default); border-radius: var(--radius-md);
12
+ background: transparent; cursor: pointer; text-align: center;
13
+ font-family: inherit;
14
+ transition: background-color .12s, border-color .12s;
15
+ outline: 2px solid transparent; outline-offset: -2px;
16
+ }
17
+ & .fileupload-zone:hover { background: var(--bg-raised); }
18
+ & .fileupload-zone:focus-visible { outline-color: var(--stroke-focus-ring); }
19
+ & .fileupload-zone[data-dragging] {
20
+ border-color: var(--bg-brand-solid);
21
+ background: color-mix(in srgb, var(--bg-brand-solid) 6%, transparent);
22
+ }
23
+ & .fileupload-zone[data-disabled] { opacity: .5; cursor: not-allowed; }
24
+ & .fileupload-zone[data-invalid] { border-color: var(--bg-danger); }
25
+
26
+ /* Attio: "No files" 16/20 weight 600 */
27
+ & .fileupload-title {
28
+ color: var(--fg-primary); font-size: var(--text-xl); line-height: 20px;
29
+ font-weight: var(--weight-semibold);
30
+ }
31
+ /* Attio: the hint line, 14/20 weight 500 at 63% of the foreground */
32
+ & .fileupload-hint {
33
+ color: color-mix(in srgb, var(--fg-primary) 63%, transparent);
34
+ font-size: var(--text-lg); line-height: 20px;
35
+ font-weight: var(--weight-medium);
36
+ }
37
+
38
+ & .fileupload-input {
39
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
40
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
41
+ }
42
+
43
+ & .fileupload-list { display: flex; flex-direction: column; gap: var(--space-1); margin: 0; padding: 0; list-style: none; }
44
+ /* the row height crisp measured for a data-grid row */
45
+ & .fileupload-item {
46
+ display: flex; align-items: center; gap: var(--space-2);
47
+ min-height: var(--size-tableRow); padding: 0 var(--space-2);
48
+ border-radius: var(--radius-md); background: var(--bg-raised);
49
+ font-size: var(--text-lg); line-height: 20px; color: var(--fg-primary);
50
+ }
51
+ & .fileupload-item-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
52
+ & .fileupload-item-size { flex-shrink: 0; color: var(--fg-muted); font-size: var(--text-sm); font-variant-numeric: tabular-nums; }
53
+ & .fileupload-item[data-status="error"] { color: var(--bg-danger); }
54
+ & .fileupload-item[data-status="error"] .fileupload-item-size { color: var(--bg-danger); }
55
+
56
+ /* progress track, reusing the measured Progress geometry */
57
+ & .fileupload-progress {
58
+ flex-shrink: 0; width: 64px; height: 4px; border-radius: var(--radius-full);
59
+ background: var(--bg-neutral-weak); overflow: hidden;
60
+ }
61
+ & .fileupload-progress-fill { height: 100%; background: var(--bg-brand-solid); transition: width .2s ease; }
62
+
63
+ & .fileupload-remove {
64
+ display: inline-flex; align-items: center; justify-content: center;
65
+ flex-shrink: 0; width: var(--size-icon); height: var(--size-icon);
66
+ padding: 0; border: none; border-radius: var(--radius-sm);
67
+ background: transparent; color: var(--fg-muted); cursor: pointer;
68
+ font-family: inherit;
69
+ outline: 2px solid transparent; outline-offset: -2px;
70
+ }
71
+ & .fileupload-remove:hover { color: var(--fg-primary); }
72
+ & .fileupload-remove:focus-visible { outline-color: var(--stroke-focus-ring); }
73
+
74
+ & .fileupload-errors { display: flex; flex-direction: column; gap: var(--space-0-5); margin: 0; padding: 0; list-style: none; }
75
+ & .fileupload-error { color: var(--bg-danger); font-size: var(--text-md); line-height: 18px; }
76
+
77
+ }
78
+
79
+ @media (prefers-reduced-motion: reduce) {
80
+ .fileupload, .fileupload * {
81
+ animation-duration: 0.01ms !important;
82
+ animation-iteration-count: 1 !important;
83
+ transition-duration: 0.01ms !important;
84
+ }
85
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface FileuploadVariants {
4
+
5
+ }
6
+
7
+ export declare const fileuploadRecipe: RecipeFn<{ }>;
package/dist/styles.css CHANGED
@@ -874,6 +874,92 @@
874
874
  & .emptystate-action { margin-top: var(--space-2); }
875
875
  }
876
876
 
877
+ .fileupload {
878
+
879
+ display: flex; flex-direction: column; gap: var(--space-2);
880
+ font-family: var(--font-sans); min-width: 0;
881
+
882
+ & .fileupload-zone {
883
+ box-sizing: border-box; position: relative;
884
+ display: flex; flex-direction: column; align-items: center;
885
+ justify-content: center; gap: var(--space-1);
886
+ width: 100%; padding: var(--space-6) var(--space-4);
887
+ border: 1px dashed var(--border-default); border-radius: var(--radius-md);
888
+ background: transparent; cursor: pointer; text-align: center;
889
+ font-family: inherit;
890
+ transition: background-color .12s, border-color .12s;
891
+ outline: 2px solid transparent; outline-offset: -2px;
892
+ }
893
+ & .fileupload-zone:hover { background: var(--bg-raised); }
894
+ & .fileupload-zone:focus-visible { outline-color: var(--stroke-focus-ring); }
895
+ & .fileupload-zone[data-dragging] {
896
+ border-color: var(--bg-brand-solid);
897
+ background: color-mix(in srgb, var(--bg-brand-solid) 6%, transparent);
898
+ }
899
+ & .fileupload-zone[data-disabled] { opacity: .5; cursor: not-allowed; }
900
+ & .fileupload-zone[data-invalid] { border-color: var(--bg-danger); }
901
+
902
+ /* Attio: "No files" 16/20 weight 600 */
903
+ & .fileupload-title {
904
+ color: var(--fg-primary); font-size: var(--text-xl); line-height: 20px;
905
+ font-weight: var(--weight-semibold);
906
+ }
907
+ /* Attio: the hint line, 14/20 weight 500 at 63% of the foreground */
908
+ & .fileupload-hint {
909
+ color: color-mix(in srgb, var(--fg-primary) 63%, transparent);
910
+ font-size: var(--text-lg); line-height: 20px;
911
+ font-weight: var(--weight-medium);
912
+ }
913
+
914
+ & .fileupload-input {
915
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
916
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
917
+ }
918
+
919
+ & .fileupload-list { display: flex; flex-direction: column; gap: var(--space-1); margin: 0; padding: 0; list-style: none; }
920
+ /* the row height crisp measured for a data-grid row */
921
+ & .fileupload-item {
922
+ display: flex; align-items: center; gap: var(--space-2);
923
+ min-height: var(--size-tableRow); padding: 0 var(--space-2);
924
+ border-radius: var(--radius-md); background: var(--bg-raised);
925
+ font-size: var(--text-lg); line-height: 20px; color: var(--fg-primary);
926
+ }
927
+ & .fileupload-item-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
928
+ & .fileupload-item-size { flex-shrink: 0; color: var(--fg-muted); font-size: var(--text-sm); font-variant-numeric: tabular-nums; }
929
+ & .fileupload-item[data-status="error"] { color: var(--bg-danger); }
930
+ & .fileupload-item[data-status="error"] .fileupload-item-size { color: var(--bg-danger); }
931
+
932
+ /* progress track, reusing the measured Progress geometry */
933
+ & .fileupload-progress {
934
+ flex-shrink: 0; width: 64px; height: 4px; border-radius: var(--radius-full);
935
+ background: var(--bg-neutral-weak); overflow: hidden;
936
+ }
937
+ & .fileupload-progress-fill { height: 100%; background: var(--bg-brand-solid); transition: width .2s ease; }
938
+
939
+ & .fileupload-remove {
940
+ display: inline-flex; align-items: center; justify-content: center;
941
+ flex-shrink: 0; width: var(--size-icon); height: var(--size-icon);
942
+ padding: 0; border: none; border-radius: var(--radius-sm);
943
+ background: transparent; color: var(--fg-muted); cursor: pointer;
944
+ font-family: inherit;
945
+ outline: 2px solid transparent; outline-offset: -2px;
946
+ }
947
+ & .fileupload-remove:hover { color: var(--fg-primary); }
948
+ & .fileupload-remove:focus-visible { outline-color: var(--stroke-focus-ring); }
949
+
950
+ & .fileupload-errors { display: flex; flex-direction: column; gap: var(--space-0-5); margin: 0; padding: 0; list-style: none; }
951
+ & .fileupload-error { color: var(--bg-danger); font-size: var(--text-md); line-height: 18px; }
952
+
953
+ }
954
+
955
+ @media (prefers-reduced-motion: reduce) {
956
+ .fileupload, .fileupload * {
957
+ animation-duration: 0.01ms !important;
958
+ animation-iteration-count: 1 !important;
959
+ transition-duration: 0.01ms !important;
960
+ }
961
+ }
962
+
877
963
  .formfield {
878
964
  display: flex; flex-direction: column; gap: var(--space-1-5); box-sizing: border-box; font-family: var(--font-sans);
879
965
  & .formfield-label { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--fg-muted); }
@@ -0,0 +1,24 @@
1
+ import { type FileEntry, type FileError, type FileRejection, type UseFileUploadProps } from "@seed-design/react-file-upload";
2
+ import * as React from "react";
3
+ export type { FileEntry, FileError, FileRejection };
4
+ /** Every user-facing string, so a consumer can translate without restating the rest. */
5
+ export type FileUploadLabels = {
6
+ title: string;
7
+ hint: string;
8
+ remove: string;
9
+ errors: Record<string, string>;
10
+ };
11
+ export declare const fileUploadLabels: FileUploadLabels;
12
+ export type FileUploadProps = UseFileUploadProps & Omit<React.ComponentPropsWithoutRef<"div">, keyof UseFileUploadProps | "children"> & {
13
+ /** PARTIAL override of the chrome strings; anything omitted keeps its default. */
14
+ labels?: Partial<Omit<FileUploadLabels, "errors">> & {
15
+ errors?: Record<string, string>;
16
+ };
17
+ };
18
+ /**
19
+ * A file dropzone with an accepted-file list, driven by seed-design's headless
20
+ * engine so the behaviour — drag state, accept/reject, per-file upload status —
21
+ * is theirs by construction. Controlled or uncontrolled, the same way every
22
+ * other crisp component is.
23
+ */
24
+ export declare function FileUpload({ className, labels, "aria-label": ariaLabel, accept, disabled: disabledProp, required, invalid: invalidProp, readOnly, maxFiles, maxFileSize, minFileSize, name, validate, preventDocumentDrop, onFileReject, onFileAccept, acceptedFileEntries: acceptedFileEntriesProp, defaultAcceptedFileEntries, onAcceptedFileEntriesChange, ...rest }: FileUploadProps): React.JSX.Element;
@@ -0,0 +1,5 @@
1
+ declare const config: import("../..").RecipeConfig<{}>;
2
+ export declare const fileuploadRecipe: import("../..").RecipeFn<{}> & {
3
+ config: import("../..").RecipeConfig<{}>;
4
+ };
5
+ export default config;
@@ -0,0 +1,2 @@
1
+ export * from "./fileupload";
2
+ export * from "./fileupload.recipe";
@@ -19,6 +19,7 @@ export * from "./components/divider";
19
19
  export * from "./components/donutchart";
20
20
  export * from "./components/drawer";
21
21
  export * from "./components/emptystate";
22
+ export * from "./components/fileupload";
22
23
  export * from "./components/formfield";
23
24
  export * from "./components/input";
24
25
  export * from "./components/kbd";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.7.5",
3
+ "version": "0.8.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -137,6 +137,11 @@
137
137
  "import": "./dist/esm/components/emptystate/index.js",
138
138
  "require": "./dist/cjs/components/emptystate/index.js"
139
139
  },
140
+ "./fileupload": {
141
+ "types": "./dist/types/components/fileupload/index.d.ts",
142
+ "import": "./dist/esm/components/fileupload/index.js",
143
+ "require": "./dist/cjs/components/fileupload/index.js"
144
+ },
140
145
  "./formfield": {
141
146
  "types": "./dist/types/components/formfield/index.d.ts",
142
147
  "import": "./dist/esm/components/formfield/index.js",
@@ -407,6 +412,7 @@
407
412
  "@radix-ui/react-slot": "^1.3.3",
408
413
  "@seed-design/dom-utils": "^2.1.0",
409
414
  "@seed-design/react-date-picker": "1.2.0",
415
+ "@seed-design/react-file-upload": "1.0.1",
410
416
  "@zag-js/combobox": "^1.43.3",
411
417
  "@zag-js/dialog": "^1.43.3",
412
418
  "@zag-js/react": "^1.43.3",