@crisp-ui-kit/crisp 0.34.0 → 0.36.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.
Files changed (59) hide show
  1. package/dist/bulkbar.css +3 -0
  2. package/dist/bulkbar.layered.css +3 -0
  3. package/dist/cjs/components/bulkbar/bulkbar.js +1 -1
  4. package/dist/cjs/components/bulkbar/bulkbar.recipe.js +6 -0
  5. package/dist/cjs/components/datatable/datatable.js +65 -5
  6. package/dist/cjs/components/datatable/datatable.recipe.js +2 -0
  7. package/dist/cjs/components/filldoc/filldoc.js +102 -0
  8. package/dist/cjs/components/filldoc/filldoc.recipe.js +211 -0
  9. package/dist/cjs/components/filldoc/index.js +10 -0
  10. package/dist/cjs/components/filldoc/serialize.js +39 -0
  11. package/dist/cjs/components/fillsheet/fillsheet.js +719 -0
  12. package/dist/cjs/components/fillsheet/fillsheet.recipe.js +325 -0
  13. package/dist/cjs/components/fillsheet/index.js +12 -0
  14. package/dist/cjs/components/fillsheet/serialize.js +82 -0
  15. package/dist/cjs/index.js +2 -0
  16. package/dist/cjs/product/recordpanel/recordpanel.js +4 -2
  17. package/dist/cjs/product/recordpanel/recordpanel.recipe.js +8 -0
  18. package/dist/cjs/product/sidebar/sidebar.js +16 -3
  19. package/dist/datatable.css +13 -5
  20. package/dist/datatable.layered.css +13 -5
  21. package/dist/esm/components/bulkbar/bulkbar.js +1 -1
  22. package/dist/esm/components/bulkbar/bulkbar.recipe.js +6 -0
  23. package/dist/esm/components/datatable/datatable.js +65 -5
  24. package/dist/esm/components/datatable/datatable.recipe.js +2 -0
  25. package/dist/esm/components/filldoc/filldoc.js +66 -0
  26. package/dist/esm/components/filldoc/filldoc.recipe.js +208 -0
  27. package/dist/esm/components/filldoc/index.js +3 -0
  28. package/dist/esm/components/filldoc/serialize.js +35 -0
  29. package/dist/esm/components/fillsheet/fillsheet.js +683 -0
  30. package/dist/esm/components/fillsheet/fillsheet.recipe.js +322 -0
  31. package/dist/esm/components/fillsheet/index.js +3 -0
  32. package/dist/esm/components/fillsheet/serialize.js +76 -0
  33. package/dist/esm/index.js +2 -0
  34. package/dist/esm/product/recordpanel/recordpanel.js +4 -2
  35. package/dist/esm/product/recordpanel/recordpanel.recipe.js +8 -0
  36. package/dist/esm/product/sidebar/sidebar.js +17 -4
  37. package/dist/filldoc.css +31 -0
  38. package/dist/filldoc.d.ts +7 -0
  39. package/dist/filldoc.layered.css +33 -0
  40. package/dist/fillsheet.css +29 -0
  41. package/dist/fillsheet.d.ts +7 -0
  42. package/dist/fillsheet.layered.css +31 -0
  43. package/dist/recordpanel.css +6 -0
  44. package/dist/recordpanel.layered.css +6 -0
  45. package/dist/styles.css +84 -5
  46. package/dist/styles.layered.css +84 -5
  47. package/dist/types/components/datatable/datatable.d.ts +7 -1
  48. package/dist/types/components/filldoc/filldoc.d.ts +79 -0
  49. package/dist/types/components/filldoc/filldoc.recipe.d.ts +201 -0
  50. package/dist/types/components/filldoc/index.d.ts +3 -0
  51. package/dist/types/components/filldoc/serialize.d.ts +5 -0
  52. package/dist/types/components/fillsheet/fillsheet.d.ts +165 -0
  53. package/dist/types/components/fillsheet/fillsheet.recipe.d.ts +202 -0
  54. package/dist/types/components/fillsheet/index.d.ts +3 -0
  55. package/dist/types/components/fillsheet/serialize.d.ts +45 -0
  56. package/dist/types/index.d.ts +2 -0
  57. package/dist/types/product/recordpanel/recordpanel.d.ts +11 -1
  58. package/dist/types/product/sidebar/sidebar.d.ts +56 -0
  59. package/package.json +11 -1
@@ -0,0 +1,322 @@
1
+ import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
+ import { contract as c, elevation as e } from "../../tokens/index.js";
3
+ /**
4
+ * FillSheet — a cell canvas, not a table.
5
+ *
6
+ * ON THE MEASUREMENT RULE: every other recipe in this repo takes its numbers
7
+ * from app.attio.com, and CLAUDE.md says not to build what cannot be measured.
8
+ * That rule is WAIVED here, deliberately and with the owner's sign-off: Attio
9
+ * has no spreadsheet, so there is nothing to measure against. This is the one
10
+ * component whose composition is designed rather than reproduced. Do not
11
+ * "restore" it to a measured source — there isn't one.
12
+ *
13
+ * It still spends the measured SCALE wherever a value exists on it (space,
14
+ * text, the semantic colours), because the scale is what keeps this looking
15
+ * like the rest of crisp. The few raw px below each say what decides them.
16
+ *
17
+ * ON THE LOOK: bare. A cell is NOT a box. Boxing every cell on four sides is
18
+ * the single thing that makes a grid read as spreadsheet software, and it was
19
+ * rejected on sight. What is left is a faint rule under each row and between
20
+ * each column, with none on the outer edge — the cells are separated, the
21
+ * sheet is not fenced. There is no lettering, no numbered gutter and no header
22
+ * row anywhere in this file.
23
+ */
24
+ // border-DEFAULT (#e4e4e7), not border-subtle (#eeeff1). Subtle is for a rule
25
+ // on a tinted surface; on white paper it vanishes, and a form whose grid you
26
+ // cannot see is not a form.
27
+ const line = c.borderDefault;
28
+ // Selection is NEUTRAL, not brand. A blue block laid over the cells is the
29
+ // Excel selection and it was rejected; this is the same ink wash the record
30
+ // tables use for a selected row, so a selected range here and a selected row
31
+ // there read as the same gesture.
32
+ const tint = (pct) => `color-mix(in srgb, ${c.fgPrimary} ${pct}%, transparent)`;
33
+ // The grab strip for a resize edge. 5px is the smallest target that is
34
+ // reliably hittable with a mouse without swallowing clicks meant for the cell.
35
+ const GRAB = "5px";
36
+ const spec = defineSpec({
37
+ name: "fillsheet",
38
+ base: {
39
+ // `grid` (not `table`) is the whole point: per-column widths and per-row
40
+ // heights are template tracks, so either axis can be dragged independently
41
+ // and a cell never has to agree with its column about its height.
42
+ display: "grid",
43
+ gridTemplateColumns: "var(--crisp-fillsheet-cols)",
44
+ gridTemplateRows: "var(--crisp-fillsheet-rows)",
45
+ position: "relative",
46
+ overflow: "auto",
47
+ maxWidth: "100%",
48
+ // The frame is a real border, so without this a consumer who sets a width
49
+ // (or leans on `maxWidth: 100%`, as the A4 example does) gets a content
50
+ // box 2px narrower than they sized for, the column tracks overflow it by
51
+ // those 2px, and the sheet grows a scrollbar for one pixel of nothing.
52
+ boxSizing: "border-box",
53
+ fontFamily: c.font.sans,
54
+ fontSize: c.text.md,
55
+ color: c.fgPrimary,
56
+ background: c.bgSurface,
57
+ // SQUARE. The web attribute panel this borrows its structure from is a
58
+ // radius-xl card, but a card is a UI surface and this is a form printed
59
+ // on a page -- ruled paper has corners.
60
+ borderRadius: "0",
61
+ // A real BORDER, not `inset` box-shadow. An inset shadow is painted under
62
+ // the element's own children, and every cell here has an opaque
63
+ // background covering the grid edge to edge -- so the frame was drawn and
64
+ // then painted straight over, and the sheet read as having no outline at
65
+ // all. `outline` is the other option and is barred: the root sets
66
+ // `outline: none` on focus, and the frame would blink out when you click
67
+ // into the sheet.
68
+ border: `1px solid ${line}`,
69
+ // Dragging a selection across cells otherwise paints the browser's own
70
+ // text selection over the range tint.
71
+ userSelect: "none",
72
+ WebkitUserSelect: "none",
73
+ },
74
+ parts: [
75
+ // The grid root takes focus so it can receive keys, but it must NOT draw
76
+ // the browser's focus ring: a rectangle around the whole sheet reads as
77
+ // "everything is selected" and drowns the one ring that means it. The
78
+ // focused cell is the focus indicator, and focusing the sheet always
79
+ // selects a cell, so a keyboard user is never left with no visible
80
+ // position.
81
+ { selector: "&:focus, &:focus-visible", decls: { outline: "none" } },
82
+ {
83
+ slot: "cell",
84
+ selector: "& .crisp-fillsheet-cell",
85
+ decls: {
86
+ position: "relative",
87
+ display: "flex",
88
+ // Top, not centre: a cell that wrapped to four lines should start
89
+ // where every other cell in its row starts.
90
+ alignItems: "flex-start",
91
+ minWidth: "0",
92
+ paddingInline: c.space["3"],
93
+ paddingBlock: c.space["3"],
94
+ // A divider on both axes: the point is to read it cell by cell, not
95
+ // as ruled rows. The last column and the last row drop theirs (below)
96
+ // so nothing doubles up against the panel's own frame -- the same
97
+ // rule fms uses, where `.attr-row + .attr-row` draws the divider and
98
+ // the outer edge is left to the panel.
99
+ borderInlineEnd: `1px solid ${line}`,
100
+ borderBlockEnd: `1px solid ${line}`,
101
+ background: c.bgSurface,
102
+ // Narrowing a column WRAPS the text; it does not clip it or hide it
103
+ // behind an ellipsis. `break-word` rather than `anywhere` so ordinary
104
+ // prose still breaks at spaces and only an unbreakable run is split.
105
+ // `pre-wrap` so an explicit newline survives too.
106
+ whiteSpace: "pre-wrap",
107
+ overflowWrap: "break-word",
108
+ // Pinned, not left to `normal`: the row height below is 8 + 20 + 8, so
109
+ // a single-line cell sits with EQUAL space above and below it. Leave
110
+ // this to the font and the text rides high in the row, which is what
111
+ // "too near the top" was.
112
+ lineHeight: "20px",
113
+ cursor: "cell",
114
+ },
115
+ },
116
+ // A read-only column is a KEY cell, and it is filled rather than blank.
117
+ // This is the move the fms iOS detail view makes: its label cell sits on
118
+ // gray200 against a white value cell, and that contrast -- not an outline
119
+ // -- is what makes you read the form cell by cell. It also says which
120
+ // cells are yours to fill before you touch one.
121
+ {
122
+ selector: "& .crisp-fillsheet-cell[aria-readonly]",
123
+ decls: { background: c.bgRaised, color: c.fgMuted },
124
+ },
125
+ // The outer edges belong to the frame, not to a cell.
126
+ {
127
+ selector: "& .crisp-fillsheet-cell:last-child",
128
+ decls: { borderInlineEnd: "none" },
129
+ },
130
+ {
131
+ // each row is a `display: contents` wrapper, so the LAST row wrapper is
132
+ // the grid's last child and its cells are the bottom row
133
+ selector: "& > div:last-child .crisp-fillsheet-cell",
134
+ decls: { borderBlockEnd: "none" },
135
+ },
136
+ {
137
+ selector: '& .crisp-fillsheet-cell[data-align="end"]',
138
+ decls: {
139
+ justifyContent: "flex-end",
140
+ fontVariantNumeric: "tabular-nums",
141
+ },
142
+ },
143
+ // --- the state stack ------------------------------------------------
144
+ // One flat tint, and nothing else. There is no hover treatment and no
145
+ // ring: a click opens the cell immediately, so anything layered on the
146
+ // moment before that is decoration you see only in passing. Every cell in
147
+ // a range shares one fill, so a selection reads as a single block rather
148
+ // than a block with its lead cell picked out of it.
149
+ // Only a MULTI-cell selection is painted. Pressing a single cell paints
150
+ // nothing at all: the press opens that cell, so a tint under the pointer
151
+ // is a state that exists for the length of a mouse click and is then
152
+ // replaced -- which reads as a flicker, not as feedback. A range is
153
+ // different. It persists, and it is the thing the fill handle acts on.
154
+ {
155
+ selector: "&[data-multi] .crisp-fillsheet-cell[data-range]",
156
+ decls: { background: tint(6) },
157
+ },
158
+ // A KEYBOARD user has to be able to see where they are. Pressing a cell
159
+ // paints nothing, which is right for the mouse -- the press is on its way
160
+ // to opening that cell -- but it left arrow-key navigation completely
161
+ // invisible: the root drew no focus ring (deliberately, a rectangle round
162
+ // the whole sheet reads as "everything is selected") and a single-cell
163
+ // selection drew nothing either, so nothing on screen said where you
164
+ // were.
165
+ //
166
+ // Hung off `data-keynav`, which the component sets from the last thing
167
+ // that moved the selection, NOT off `:focus-visible`. That was tried and
168
+ // is wrong here: the cell press calls `focus()` on the root, and Chrome
169
+ // counts a programmatic focus as keyboard-ish, so `:focus-visible`
170
+ // matched on a plain mouse press and put the flicker straight back.
171
+ {
172
+ selector: "&[data-keynav] .crisp-fillsheet-cell[data-selected]",
173
+ decls: {
174
+ background: tint(6),
175
+ boxShadow: `inset 0 -2px 0 ${c.fgPrimary}`,
176
+ },
177
+ },
178
+ {
179
+ // Last, so it beats both. The cell you are typing in shows nothing at
180
+ // all -- the tint is there to say "this is selected", and once you are
181
+ // writing in it that is already obvious.
182
+ selector: "& .crisp-fillsheet-cell[data-editing]",
183
+ decls: { background: c.bgSurface },
184
+ },
185
+ {
186
+ slot: "columnResize",
187
+ selector: "& .crisp-fillsheet-colresize",
188
+ decls: {
189
+ position: "absolute",
190
+ insetBlockStart: "0",
191
+ insetBlockEnd: "0",
192
+ insetInlineEnd: "0",
193
+ zIndex: "3",
194
+ width: GRAB,
195
+ cursor: "col-resize",
196
+ // a drag on the grab is a RESIZE, never a scroll — without this a
197
+ // finger on the boundary pans the page instead of moving the column
198
+ touchAction: "none",
199
+ },
200
+ },
201
+ {
202
+ slot: "rowResize",
203
+ selector: "& .crisp-fillsheet-rowresize",
204
+ decls: {
205
+ position: "absolute",
206
+ insetInlineStart: "0",
207
+ insetInlineEnd: "0",
208
+ insetBlockEnd: "0",
209
+ zIndex: "3",
210
+ height: GRAB,
211
+ cursor: "row-resize",
212
+ touchAction: "none",
213
+ },
214
+ },
215
+ // Hovering the boundary draws it. This is the ONLY affordance for
216
+ // resizing -- there is no header row to grab -- so it is a solid 2px
217
+ // line in the primary ink, not a tint: at 5px wide and invisible until
218
+ // touched, a fainter one is a control nobody finds.
219
+ {
220
+ selector: "& .crisp-fillsheet-colresize:hover",
221
+ decls: {
222
+ background: `linear-gradient(to right, transparent 0, transparent calc(50% - 1px), ${c.fgPrimary} calc(50% - 1px), ${c.fgPrimary} calc(50% + 1px), transparent calc(50% + 1px))`,
223
+ },
224
+ },
225
+ {
226
+ selector: "& .crisp-fillsheet-rowresize:hover",
227
+ decls: {
228
+ background: `linear-gradient(to bottom, transparent 0, transparent calc(50% - 1px), ${c.fgPrimary} calc(50% - 1px), ${c.fgPrimary} calc(50% + 1px), transparent calc(50% + 1px))`,
229
+ },
230
+ },
231
+ {
232
+ slot: "editor",
233
+ selector: "& .crisp-fillsheet-editor",
234
+ decls: {
235
+ // IN FLOW, not absolutely positioned over the cell. Pinned to the
236
+ // cell's box with `inset: 0` it could never be taller than the cell,
237
+ // so a second line scrolled out of sight under `overflow: hidden`
238
+ // instead of growing the row. In flow, the textarea's height drives
239
+ // the cell, the cell drives the row's `minmax(h, auto)` track, and a
240
+ // cell grows as you type the way a textarea does.
241
+ display: "block",
242
+ flexGrow: "1",
243
+ width: "100%",
244
+ minWidth: "0",
245
+ margin: "0",
246
+ // No inset of its own: it sits INSIDE the cell's padding now, so
247
+ // repeating it here would double it and shift the text on open.
248
+ padding: "0",
249
+ border: "none",
250
+ outline: "none",
251
+ background: "transparent",
252
+ fontFamily: "inherit",
253
+ fontSize: "inherit",
254
+ // must match the cell's, or a wrapped cell re-flows as you type
255
+ lineHeight: "20px",
256
+ color: "inherit",
257
+ resize: "none",
258
+ // The root sets `user-select: none` so dragging a range does not paint
259
+ // the browser's text selection across the cells -- and that INHERITS,
260
+ // so without putting it back here you cannot select the text inside a
261
+ // cell you have opened. Drag-select, double-click a word, ⌘A: none of
262
+ // it worked in the editor.
263
+ userSelect: "text",
264
+ WebkitUserSelect: "text",
265
+ // the height is set from scrollHeight on every keystroke; a scrollbar
266
+ // would mean that measurement had failed
267
+ overflow: "hidden",
268
+ },
269
+ },
270
+ // A select inside a cell. `Select`'s trigger is a field -- it has a ring,
271
+ // a hover fill and its own inset -- and all of that belongs to a form
272
+ // control sitting in a layout, not to a cell in a sheet that is meant to
273
+ // look like paper. Stripped back so the open cell reads exactly like the
274
+ // closed one, with the list doing the work.
275
+ {
276
+ slot: "selectCell",
277
+ selector: "& .crisp-fillsheet-select .crisp-select-trigger",
278
+ decls: {
279
+ width: "100%",
280
+ height: "100%",
281
+ padding: "0",
282
+ border: "none",
283
+ background: "transparent",
284
+ boxShadow: "none",
285
+ font: "inherit",
286
+ color: "inherit",
287
+ },
288
+ },
289
+ {
290
+ selector: "& .crisp-fillsheet-select .crisp-select-trigger:hover, & .crisp-fillsheet-select .crisp-select-trigger[data-open]",
291
+ decls: { background: "transparent", boxShadow: "none" },
292
+ },
293
+ {
294
+ // the chevron is the one bit of chrome a closed cell must never show,
295
+ // and it has no job while the list is already open either
296
+ selector: "& .crisp-fillsheet-select .crisp-select-chevron",
297
+ decls: { display: "none" },
298
+ },
299
+ ],
300
+ variants: {
301
+ surface: {
302
+ // The frame on the root is the whole edge. The default.
303
+ panel: {},
304
+ // The same frame, lifted off the page, for a sheet that IS the document
305
+ // rather than one section of a screen.
306
+ //
307
+ // `elevation.floating`, not a hand-written stack. The first version was
308
+ // two raw `rgba(0,0,0,...)` layers, which is Lesson 30 exactly: a black
309
+ // shadow does nothing on a dark background, so the lift simply vanished
310
+ // when the theme flipped. The spec gate caught it. The elevation module
311
+ // exists so this value is written once and is theme-aware.
312
+ paper: { boxShadow: e.floating },
313
+ // No frame at all.
314
+ bare: { border: "none" },
315
+ },
316
+ },
317
+ defaultVariants: { surface: "panel" },
318
+ });
319
+ const config = specToRecipe(spec);
320
+ export const fillsheetRecipe = Object.assign(resolveRecipe(config), { config });
321
+ export { spec };
322
+ export default config;
@@ -0,0 +1,3 @@
1
+ export { FillSheet, } from "./fillsheet.js";
2
+ export { fillsheetRecipe } from "./fillsheet.recipe.js";
3
+ export { toCSV, toHTML, toMarkdown, toTSV, } from "./serialize.js";
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Getting a sheet OUT, in the formats documents actually read.
3
+ *
4
+ * These are plain functions over `string[][]` — no React, no DOM — so they run
5
+ * in a server route building an export just as well as in a click handler.
6
+ * That is the whole reason the grid is `string[][]`: the value IS the
7
+ * document, and nothing here has to know what a cell meant.
8
+ */
9
+ const headings = (columns) => columns?.some((c) => c.label !== undefined)
10
+ ? columns.map((c) => c.label ?? "")
11
+ : null;
12
+ /**
13
+ * Tab-separated, the clipboard's lingua franca for a grid: Excel, Sheets,
14
+ * Numbers and Word's table paste all read it as CELLS.
15
+ */
16
+ export function toTSV(value) {
17
+ // A tab or a newline INSIDE a cell would invent columns and rows in
18
+ // whatever reads this, so they are flattened to spaces. TSV has no quoting
19
+ // to escape them with — that is what CSV is for.
20
+ return value
21
+ .map((row) => row.map((c) => c.replace(/[\t\n\r]+/g, " ")).join("\t"))
22
+ .join("\n");
23
+ }
24
+ /** RFC 4180: quote a field that contains a comma, a quote or a newline. */
25
+ export function toCSV(value, o = {}) {
26
+ const esc = (cell) => /[",\n\r]/.test(cell) ? `"${cell.replace(/"/g, '""')}"` : cell;
27
+ const head = headings(o.columns);
28
+ const rows = head ? [head, ...value] : value;
29
+ // CRLF, which is what RFC 4180 says and what Excel expects on Windows.
30
+ return rows.map((row) => row.map(esc).join(",")).join("\r\n");
31
+ }
32
+ const escapeHtml = (s) => s
33
+ .replace(/&/g, "&")
34
+ .replace(/</g, "&lt;")
35
+ .replace(/>/g, "&gt;")
36
+ .replace(/"/g, "&quot;");
37
+ /**
38
+ * A real `<table>`.
39
+ *
40
+ * This is the one that makes "paste into a document" work: Word, Google Docs,
41
+ * Pages and every rich-text editor read `text/html` from the clipboard and
42
+ * build a formatted table from it. Given only tab-separated text they paste a
43
+ * paragraph of tabs instead.
44
+ *
45
+ * Newlines inside a cell become `<br>`, so a wrapped note survives as a
46
+ * wrapped note rather than collapsing into one line.
47
+ */
48
+ export function toHTML(value, o = {}) {
49
+ const cell = (text) => escapeHtml(text).replace(/\r?\n/g, "<br>");
50
+ const head = headings(o.columns);
51
+ const thead = head
52
+ ? `<thead><tr>${head.map((h) => `<th>${cell(h)}</th>`).join("")}</tr></thead>`
53
+ : "";
54
+ const tbody = `<tbody>${value
55
+ .map((row) => `<tr>${row.map((c) => `<td>${cell(c)}</td>`).join("")}</tr>`)
56
+ .join("")}</tbody>`;
57
+ const caption = o.title ? `<caption>${escapeHtml(o.title)}</caption>` : "";
58
+ return `<table>${caption}${thead}${tbody}</table>`;
59
+ }
60
+ /**
61
+ * A GitHub-flavoured Markdown table.
62
+ *
63
+ * A pipe inside a cell is escaped and a newline becomes `<br>`, because a
64
+ * Markdown table row is a single line — an unescaped one silently splits the
65
+ * row into columns that were never there.
66
+ */
67
+ export function toMarkdown(value, o = {}) {
68
+ const cell = (text) => text.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
69
+ const width = Math.max(0, ...value.map((r) => r.length));
70
+ const head = headings(o.columns) ?? Array.from({ length: width }, () => "");
71
+ const line = (cells) => `| ${Array.from({ length: head.length }, (_, i) => cell(cells[i] ?? "")).join(" | ")} |`;
72
+ const rule = `| ${head.map(() => "---").join(" | ")} |`;
73
+ const body = value.map(line).join("\n");
74
+ const title = o.title ? `### ${o.title}\n\n` : "";
75
+ return `${title}${line(head)}\n${rule}\n${body}`;
76
+ }
package/dist/esm/index.js CHANGED
@@ -24,6 +24,8 @@ export * from "./components/donutchart/index.js";
24
24
  export * from "./components/drawer/index.js";
25
25
  export * from "./components/emptystate/index.js";
26
26
  export * from "./components/fileupload/index.js";
27
+ export * from "./components/filldoc/index.js";
28
+ export * from "./components/fillsheet/index.js";
27
29
  export * from "./components/formfield/index.js";
28
30
  export * from "./components/input/index.js";
29
31
  export * from "./components/kbd/index.js";
@@ -26,7 +26,8 @@ function FieldValue({ col, row, emptyLabel, }) {
26
26
  * relative` container (e.g. the app main area); it slides in from the right and
27
27
  * overlays the content.
28
28
  */
29
- export function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", detailsLabel = "Details", emptyLabel = "Empty", ...rest }) {
29
+ export function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", title, detailsLabel = "Details", emptyLabel = "Empty", footer, ...rest }) {
30
+ const titleId = React.useId();
30
31
  // close on Escape while open
31
32
  React.useEffect(() => {
32
33
  if (!record)
@@ -42,6 +43,7 @@ export function RecordPanel({ record, columns, onClose, className, closeLabel =
42
43
  ? record[entityCol.key]
43
44
  : undefined;
44
45
  const fields = columns.filter((c) => c.type !== "entity");
45
- return (_jsxs("div", { className: recordpanelRecipe({ className }), ...rest, role: "dialog", "aria-label": entity?.name ?? "Record", children: [_jsxs("div", { className: "crisp-recordpanel-header", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpanel-fav", src: entity.avatar, alt: "" })) : entity ? (_jsx(Avatar, { size: "md", shape: "rounded", tone: "brand", children: entity.name.charAt(0) })) : null, _jsx("span", { className: "crisp-recordpanel-title", children: entity?.name ?? "Record" }), _jsx("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 18, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpanel-body", children: [_jsx("div", { className: "crisp-recordpanel-sectionlabel", children: detailsLabel }), fields.map((col) => (_jsxs("div", { className: "crisp-recordpanel-field", children: [_jsxs("span", { className: "crisp-recordpanel-field-label", children: [col.icon ? _jsx(col.icon, { size: 15, strokeWidth: 1.75 }) : null, col.label] }), _jsx("span", { className: "crisp-recordpanel-field-value", children: _jsx(FieldValue, { col: col, row: record, emptyLabel: emptyLabel }) })] }, col.key)))] })] }));
46
+ const heading = title ?? entity?.name ?? "Record";
47
+ return (_jsxs("div", { className: recordpanelRecipe({ className }), ...rest, role: "dialog", "aria-labelledby": titleId, children: [_jsxs("div", { className: "crisp-recordpanel-header", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpanel-fav", src: entity.avatar, alt: "" })) : entity ? (_jsx(Avatar, { size: "md", shape: "rounded", tone: "brand", children: entity.name.charAt(0) })) : null, _jsx("span", { id: titleId, className: "crisp-recordpanel-title", children: heading }), _jsx("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 18, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpanel-body", children: [_jsx("div", { className: "crisp-recordpanel-sectionlabel", children: detailsLabel }), fields.map((col) => (_jsxs("div", { className: "crisp-recordpanel-field", children: [_jsxs("span", { className: "crisp-recordpanel-field-label", children: [col.icon ? _jsx(col.icon, { size: 15, strokeWidth: 1.75 }) : null, col.label] }), _jsx("span", { className: "crisp-recordpanel-field-value", children: _jsx(FieldValue, { col: col, row: record, emptyLabel: emptyLabel }) })] }, col.key)))] }), footer != null && (_jsx("div", { className: "crisp-recordpanel-footer", children: footer }))] }));
46
48
  }
47
49
  RecordPanel.displayName = "RecordPanel";
@@ -36,6 +36,14 @@ const config = defineRecipe({
36
36
  ${recordTagCss}
37
37
  & .crisp-recordpanel-link { color: ${c.bgBrandSolid}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
38
38
  & .crisp-recordpanel-link:hover { text-decoration: underline; }
39
+
40
+ /* #329: an optional slot for record actions (e.g. Edit). Reuses the
41
+ header's own measured values rather than inventing new ones -- the
42
+ same horizontal inset (space.3), the same hairline border token
43
+ (borderSubtle, flipped to border-top since this sits at the opposite
44
+ edge), and the header/body gap (space.2) for its own vertical padding
45
+ and for spacing between whatever it's given. */
46
+ & .crisp-recordpanel-footer { flex-shrink: 0; display: flex; align-items: center; gap: ${c.space["2"]}; padding: ${c.space["2"]} ${c.space["3"]}; border-top: 1px solid ${c.borderSubtle}; }
39
47
  `,
40
48
  variants: {},
41
49
  defaultVariants: {},
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Building2, ChartNoAxesColumn, ChevronDown, CircleDollarSign, FileText, House, Inbox, LayoutGrid, Mail, MessageSquare, PanelLeft, Phone, Search, Send, SquareCheck, User, UserPlus, Users, Waypoints, } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { Avatar } from "../../components/avatar/index.js";
@@ -81,8 +81,14 @@ export const ATTIO_SIDEBAR_FOOTER = {
81
81
  function SectionHeader({ label, open, onToggle, }) {
82
82
  return (_jsxs("button", { type: "button", className: "crisp-sidebar-section-header", "data-collapsed": open ? undefined : "", "aria-expanded": open, onClick: onToggle, children: [_jsx(IconChevronMini, {}), _jsx("span", { children: label })] }));
83
83
  }
84
- export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = ATTIO_SIDEBAR_WORKSPACE, items = ATTIO_SIDEBAR_ITEMS, recordsSection = ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = ATTIO_SIDEBAR_CHATS_SECTION, footer = ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
85
- const [collapsedSections, setCollapsedSections] = React.useState(() => new Set());
84
+ export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse, collapseLabel = "Collapse sidebar", inboxLabel = "Inbox", searchLabel = "Search", workspace = ATTIO_SIDEBAR_WORKSPACE, items = ATTIO_SIDEBAR_ITEMS, recordsSection = ATTIO_SIDEBAR_RECORDS_SECTION, listsSection = ATTIO_SIDEBAR_LISTS_SECTION, chatsSection = ATTIO_SIDEBAR_CHATS_SECTION, groups, footer = ATTIO_SIDEBAR_FOOTER, ...props }, ref) => {
85
+ // Seeded once, on mount, from whatever groups start collapsed — not
86
+ // reactive to `groups` changing later, matching every other piece of
87
+ // this component's state (recordsSection/listsSection/chatsSection have
88
+ // never supported a starting-collapsed prop at all).
89
+ const [collapsedSections, setCollapsedSections] = React.useState(() => new Set(groups
90
+ ?.filter((g) => g.defaultCollapsed)
91
+ .map((g) => g.key ?? g.label)));
86
92
  const [commandOpen, setCommandOpen] = React.useState(false);
87
93
  React.useEffect(() => {
88
94
  function handleKeyDown(event) {
@@ -107,6 +113,13 @@ export const Sidebar = React.forwardRef(({ className, asChild, onToggleCollapse,
107
113
  return next;
108
114
  });
109
115
  }, []);
110
- return (_jsxs(Primitive.div, { ref: ref, asChild: asChild, className: sidebarRecipe({ className }), ...props, children: [_jsxs("div", { className: "crisp-sidebar-ws", children: [_jsx(Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), _jsx("span", { className: "crisp-sidebar-ws-name", children: workspace.name }), _jsx(IconChevronDown, {}), _jsx(Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: _jsx(IconCollapsePanel, {}) })] }), _jsxs("div", { className: "crisp-sidebar-searchrow", children: [_jsxs("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [_jsx(IconSearch, {}), _jsx("span", { className: "crisp-sidebar-search-label", children: searchLabel }), _jsx("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), _jsx(Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: _jsx(IconInbox, {}) })] }), _jsx("div", { className: "crisp-sidebar-divider" }), _jsx("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing }) => (_jsx(NavItem, { href: "#", active: active, icon: _jsx(ItemIcon, {}), trailing: trailing, children: label }, label))) }), _jsx(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records") }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph }) => (_jsx(NavItem, { href: "#", icon: _jsx(Avatar, { size: "sm", shape: "rounded", tone: tone, children: _jsx(Glyph, {}) }), children: label }, label))) }), _jsx(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists") }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: _jsx("button", { type: "button", className: "crisp-sidebar-newlist", children: listsSection.newListLabel }) }), _jsx(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats") }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon }) => (_jsx(NavItem, { href: "#", icon: _jsx(ItemIcon, {}), children: label }, label))) }), _jsxs("div", { className: "crisp-sidebar-footer", children: [_jsx(Button, { intent: "neutral", size: "sm", className: "crisp-sidebar-pill", children: footer.gettingStartedLabel }), _jsx(NavItem, { href: "#", icon: _jsx(IconInvite, {}), children: footer.inviteLabel }), _jsx("div", { className: "crisp-sidebar-divider" }), _jsxs("div", { className: "crisp-sidebar-trial", children: [_jsx("span", { children: footer.trialText }), _jsx(Button, { intent: "neutral", size: "sm", children: footer.trialActionLabel })] })] }), _jsx(Command, { open: commandOpen, onOpenChange: setCommandOpen })] }));
116
+ return (_jsxs(Primitive.div, { ref: ref, asChild: asChild, className: sidebarRecipe({ className }), ...props, children: [_jsxs("div", { className: "crisp-sidebar-ws", children: [_jsx(Avatar, { size: "lg", shape: "rounded", tone: workspace.avatarTone, children: workspace.avatarLabel }), _jsx("span", { className: "crisp-sidebar-ws-name", children: workspace.name }), _jsx(IconChevronDown, {}), _jsx(Button, { intent: "ghost", size: "sm", shape: "icon", className: "crisp-sidebar-ws-collapse", "aria-label": collapseLabel, onClick: onToggleCollapse, children: _jsx(IconCollapsePanel, {}) })] }), _jsxs("div", { className: "crisp-sidebar-searchrow", children: [_jsxs("button", { type: "button", className: "crisp-sidebar-search", onClick: () => setCommandOpen(true), children: [_jsx(IconSearch, {}), _jsx("span", { className: "crisp-sidebar-search-label", children: searchLabel }), _jsx("span", { className: "crisp-sidebar-search-kbd", children: "\u2318K" })] }), _jsx(Button, { intent: "neutral", size: "sm", shape: "icon", "aria-label": inboxLabel, children: _jsx(IconInbox, {}) })] }), _jsx("div", { className: "crisp-sidebar-divider" }), _jsx("div", { className: "crisp-sidebar-section", children: items.map(({ label, icon: ItemIcon, active, trailing }) => (_jsx(NavItem, { href: "#", active: active, icon: _jsx(ItemIcon, {}), trailing: trailing, children: label }, label))) }), groups ? (
117
+ // #325: navigation as data, reusing the exact section markup/
118
+ // recipe above — SectionHeader + .crisp-sidebar-section — instead
119
+ // of the three fixed Attio sections.
120
+ groups.map((group) => {
121
+ const groupKey = group.key ?? group.label;
122
+ return (_jsxs(React.Fragment, { children: [_jsx(SectionHeader, { label: group.label, open: !collapsedSections.has(groupKey), onToggle: () => toggleSection(groupKey) }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has(groupKey) ? "" : undefined, children: group.items.map((item) => (_jsx(NavItem, { href: item.href ?? "#", active: item.active, icon: item.icon ? _jsx(item.icon, {}) : undefined, trailing: item.count, onClick: item.onSelect, children: item.label }, item.href ?? item.label))) })] }, groupKey));
123
+ })) : (_jsxs(_Fragment, { children: [_jsx(SectionHeader, { label: recordsSection.label, open: !collapsedSections.has("records"), onToggle: () => toggleSection("records") }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("records") ? "" : undefined, children: recordsSection.items.map(({ label, tone, glyph: Glyph }) => (_jsx(NavItem, { href: "#", icon: _jsx(Avatar, { size: "sm", shape: "rounded", tone: tone, children: _jsx(Glyph, {}) }), children: label }, label))) }), _jsx(SectionHeader, { label: listsSection.label, open: !collapsedSections.has("lists"), onToggle: () => toggleSection("lists") }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("lists") ? "" : undefined, children: _jsx("button", { type: "button", className: "crisp-sidebar-newlist", children: listsSection.newListLabel }) }), _jsx(SectionHeader, { label: chatsSection.label, open: !collapsedSections.has("chats"), onToggle: () => toggleSection("chats") }), _jsx("div", { className: "crisp-sidebar-section", "data-collapsed": collapsedSections.has("chats") ? "" : undefined, children: chatsSection.items.map(({ label, icon: ItemIcon }) => (_jsx(NavItem, { href: "#", icon: _jsx(ItemIcon, {}), children: label }, label))) })] })), _jsxs("div", { className: "crisp-sidebar-footer", children: [_jsx(Button, { intent: "neutral", size: "sm", className: "crisp-sidebar-pill", children: footer.gettingStartedLabel }), _jsx(NavItem, { href: "#", icon: _jsx(IconInvite, {}), children: footer.inviteLabel }), _jsx("div", { className: "crisp-sidebar-divider" }), _jsxs("div", { className: "crisp-sidebar-trial", children: [_jsx("span", { children: footer.trialText }), _jsx(Button, { intent: "neutral", size: "sm", children: footer.trialActionLabel })] })] }), _jsx(Command, { open: commandOpen, onOpenChange: setCommandOpen })] }));
111
124
  });
112
125
  Sidebar.displayName = "Sidebar";
@@ -0,0 +1,31 @@
1
+ .crisp-filldoc {
2
+ display: flex; flex-direction: column; gap: var(--crisp-space-4); box-sizing: border-box; width: 210mm; min-height: 297mm; padding: 20mm; margin: 0 auto; background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 22px; color: var(--crisp-fg-primary); border-radius: 0; box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3);
3
+ &:focus, &:focus-visible { outline: none; }
4
+ & .crisp-filldoc-block { position: relative; }
5
+ & .crisp-filldoc-heading { margin: 0; font-size: var(--crisp-text-xl); font-weight: var(--crisp-weight-semibold); line-height: 26px; color: var(--crisp-fg-primary); white-space: pre-wrap; overflow-wrap: break-word; }
6
+ & .crisp-filldoc-text { margin: 0; font-size: var(--crisp-text-lg); line-height: 22px; color: var(--crisp-fg-primary); white-space: pre-wrap; overflow-wrap: break-word; }
7
+ & .crisp-filldoc-writer { display: block; width: 100%; margin: 0; padding: 0; border: none; outline: none; background: transparent; font-family: inherit; font-size: inherit; font-weight: inherit; line-height: inherit; color: inherit; resize: none; overflow: hidden; user-select: text; -webkit-user-select: text; }
8
+ & .crisp-filldoc-insert { position: absolute; inset-inline-start: 0; inset-inline-end: 0; inset-block-end: calc(var(--crisp-space-4) / -2); height: var(--crisp-space-4); display: flex; align-items: center; gap: var(--crisp-space-2); opacity: 0; transition: opacity .1s; cursor: pointer; }
9
+ & .crisp-filldoc-block:hover .crisp-filldoc-insert, & .crisp-filldoc-insert:focus-within { opacity: 1; }
10
+ & .crisp-filldoc-insert-rule { flex: 1; height: 1px; background: var(--crisp-border-default); }
11
+ & .crisp-filldoc-insert button { display: inline-flex; align-items: center; height: 20px; padding: 0 var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-sm); background: var(--crisp-bg-raised); font-family: inherit; font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); cursor: pointer; }
12
+ & .crisp-filldoc-insert button:hover { background: var(--crisp-bg-neutral-weak); color: var(--crisp-fg-primary); }
13
+ & .crisp-filldoc-tools { position: absolute; inset-inline-end: 100%; inset-block-start: 0; padding-inline-end: var(--crisp-space-2); display: flex; gap: var(--crisp-space-1); opacity: 0; transition: opacity .1s; }
14
+ & .crisp-filldoc-block:hover .crisp-filldoc-tools, & .crisp-filldoc-tools:focus-within { opacity: 1; }
15
+ & .crisp-filldoc-tools button { width: 20px; height: 20px; display: inline-flex; align-items: center; justify-content: center; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: transparent; font-family: inherit; font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); cursor: pointer; }
16
+ & .crisp-filldoc-tools button:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
17
+ }
18
+ .crisp-filldoc--page_a4 {
19
+
20
+ }
21
+ .crisp-filldoc--page_auto {
22
+ width: 100%; min-height: 0; max-width: 210mm;
23
+ }
24
+
25
+ @media (prefers-reduced-motion: reduce) {
26
+ .crisp-filldoc, .crisp-filldoc * {
27
+ animation-duration: 0.01ms !important;
28
+ animation-iteration-count: 1 !important;
29
+ transition-duration: 0.01ms !important;
30
+ }
31
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface FilldocVariants {
4
+ page?: "a4" | "auto";
5
+ }
6
+
7
+ export declare const filldocRecipe: RecipeFn<{ page: { a4: string; auto: string } }>;
@@ -0,0 +1,33 @@
1
+ @layer crisp-components {
2
+ .crisp-filldoc {
3
+ display: flex; flex-direction: column; gap: var(--crisp-space-4); box-sizing: border-box; width: 210mm; min-height: 297mm; padding: 20mm; margin: 0 auto; background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); font-size: var(--crisp-text-lg); line-height: 22px; color: var(--crisp-fg-primary); border-radius: 0; box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3);
4
+ &:focus, &:focus-visible { outline: none; }
5
+ & .crisp-filldoc-block { position: relative; }
6
+ & .crisp-filldoc-heading { margin: 0; font-size: var(--crisp-text-xl); font-weight: var(--crisp-weight-semibold); line-height: 26px; color: var(--crisp-fg-primary); white-space: pre-wrap; overflow-wrap: break-word; }
7
+ & .crisp-filldoc-text { margin: 0; font-size: var(--crisp-text-lg); line-height: 22px; color: var(--crisp-fg-primary); white-space: pre-wrap; overflow-wrap: break-word; }
8
+ & .crisp-filldoc-writer { display: block; width: 100%; margin: 0; padding: 0; border: none; outline: none; background: transparent; font-family: inherit; font-size: inherit; font-weight: inherit; line-height: inherit; color: inherit; resize: none; overflow: hidden; user-select: text; -webkit-user-select: text; }
9
+ & .crisp-filldoc-insert { position: absolute; inset-inline-start: 0; inset-inline-end: 0; inset-block-end: calc(var(--crisp-space-4) / -2); height: var(--crisp-space-4); display: flex; align-items: center; gap: var(--crisp-space-2); opacity: 0; transition: opacity .1s; cursor: pointer; }
10
+ & .crisp-filldoc-block:hover .crisp-filldoc-insert, & .crisp-filldoc-insert:focus-within { opacity: 1; }
11
+ & .crisp-filldoc-insert-rule { flex: 1; height: 1px; background: var(--crisp-border-default); }
12
+ & .crisp-filldoc-insert button { display: inline-flex; align-items: center; height: 20px; padding: 0 var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-sm); background: var(--crisp-bg-raised); font-family: inherit; font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); cursor: pointer; }
13
+ & .crisp-filldoc-insert button:hover { background: var(--crisp-bg-neutral-weak); color: var(--crisp-fg-primary); }
14
+ & .crisp-filldoc-tools { position: absolute; inset-inline-end: 100%; inset-block-start: 0; padding-inline-end: var(--crisp-space-2); display: flex; gap: var(--crisp-space-1); opacity: 0; transition: opacity .1s; }
15
+ & .crisp-filldoc-block:hover .crisp-filldoc-tools, & .crisp-filldoc-tools:focus-within { opacity: 1; }
16
+ & .crisp-filldoc-tools button { width: 20px; height: 20px; display: inline-flex; align-items: center; justify-content: center; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: transparent; font-family: inherit; font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); cursor: pointer; }
17
+ & .crisp-filldoc-tools button:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
18
+ }
19
+ .crisp-filldoc--page_a4 {
20
+
21
+ }
22
+ .crisp-filldoc--page_auto {
23
+ width: 100%; min-height: 0; max-width: 210mm;
24
+ }
25
+
26
+ @media (prefers-reduced-motion: reduce) {
27
+ .crisp-filldoc, .crisp-filldoc * {
28
+ animation-duration: 0.01ms !important;
29
+ animation-iteration-count: 1 !important;
30
+ transition-duration: 0.01ms !important;
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,29 @@
1
+ .crisp-fillsheet {
2
+ display: grid; grid-template-columns: var(--crisp-fillsheet-cols); grid-template-rows: var(--crisp-fillsheet-rows); position: relative; overflow: auto; max-width: 100%; box-sizing: border-box; font-family: var(--crisp-font-sans); font-size: var(--crisp-text-md); color: var(--crisp-fg-primary); background: var(--crisp-bg-surface); border-radius: 0; border: 1px solid var(--crisp-border-default); user-select: none; -webkit-user-select: none;
3
+ &:focus, &:focus-visible { outline: none; }
4
+ & .crisp-fillsheet-cell { position: relative; display: flex; align-items: flex-start; min-width: 0; padding-inline: var(--crisp-space-3); padding-block: var(--crisp-space-3); border-inline-end: 1px solid var(--crisp-border-default); border-block-end: 1px solid var(--crisp-border-default); background: var(--crisp-bg-surface); white-space: pre-wrap; overflow-wrap: break-word; line-height: 20px; cursor: cell; }
5
+ & .crisp-fillsheet-cell[aria-readonly] { background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); }
6
+ & .crisp-fillsheet-cell:last-child { border-inline-end: none; }
7
+ & > div:last-child .crisp-fillsheet-cell { border-block-end: none; }
8
+ & .crisp-fillsheet-cell[data-align="end"] { justify-content: flex-end; font-variant-numeric: tabular-nums; }
9
+ &[data-multi] .crisp-fillsheet-cell[data-range] { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
10
+ &[data-keynav] .crisp-fillsheet-cell[data-selected] { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); box-shadow: inset 0 -2px 0 var(--crisp-fg-primary); }
11
+ & .crisp-fillsheet-cell[data-editing] { background: var(--crisp-bg-surface); }
12
+ & .crisp-fillsheet-colresize { position: absolute; inset-block-start: 0; inset-block-end: 0; inset-inline-end: 0; z-index: 3; width: 5px; cursor: col-resize; touch-action: none; }
13
+ & .crisp-fillsheet-rowresize { position: absolute; inset-inline-start: 0; inset-inline-end: 0; inset-block-end: 0; z-index: 3; height: 5px; cursor: row-resize; touch-action: none; }
14
+ & .crisp-fillsheet-colresize:hover { background: linear-gradient(to right, transparent 0, transparent calc(50% - 1px), var(--crisp-fg-primary) calc(50% - 1px), var(--crisp-fg-primary) calc(50% + 1px), transparent calc(50% + 1px)); }
15
+ & .crisp-fillsheet-rowresize:hover { background: linear-gradient(to bottom, transparent 0, transparent calc(50% - 1px), var(--crisp-fg-primary) calc(50% - 1px), var(--crisp-fg-primary) calc(50% + 1px), transparent calc(50% + 1px)); }
16
+ & .crisp-fillsheet-editor { display: block; flex-grow: 1; width: 100%; min-width: 0; margin: 0; padding: 0; border: none; outline: none; background: transparent; font-family: inherit; font-size: inherit; line-height: 20px; color: inherit; resize: none; user-select: text; -webkit-user-select: text; overflow: hidden; }
17
+ & .crisp-fillsheet-select .crisp-select-trigger { width: 100%; height: 100%; padding: 0; border: none; background: transparent; box-shadow: none; font: inherit; color: inherit; }
18
+ & .crisp-fillsheet-select .crisp-select-trigger:hover, & .crisp-fillsheet-select .crisp-select-trigger[data-open] { background: transparent; box-shadow: none; }
19
+ & .crisp-fillsheet-select .crisp-select-chevron { display: none; }
20
+ }
21
+ .crisp-fillsheet--surface_panel {
22
+
23
+ }
24
+ .crisp-fillsheet--surface_paper {
25
+ box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3);
26
+ }
27
+ .crisp-fillsheet--surface_bare {
28
+ border: none;
29
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface FillsheetVariants {
4
+ surface?: "panel" | "paper" | "bare";
5
+ }
6
+
7
+ export declare const fillsheetRecipe: RecipeFn<{ surface: { panel: string; paper: string; bare: string } }>;