@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.
- package/dist/cjs/components/fileupload/fileupload.js +118 -0
- package/dist/cjs/components/fileupload/fileupload.recipe.js +104 -0
- package/dist/cjs/components/fileupload/index.js +18 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/product/recordstable/recordstable.js +28 -3
- package/dist/esm/components/fileupload/fileupload.js +81 -0
- package/dist/esm/components/fileupload/fileupload.recipe.js +101 -0
- package/dist/esm/components/fileupload/index.js +2 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/product/recordstable/recordstable.js +28 -3
- package/dist/fileupload.css +85 -0
- package/dist/fileupload.d.ts +7 -0
- package/dist/styles.css +86 -0
- package/dist/types/components/fileupload/fileupload.d.ts +24 -0
- package/dist/types/components/fileupload/fileupload.recipe.d.ts +5 -0
- package/dist/types/components/fileupload/index.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +7 -1
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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.
|
|
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",
|