@crisp-ui-kit/crisp 0.33.5 → 0.35.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 (46) hide show
  1. package/dist/cjs/components/datatable/datatable.js +65 -5
  2. package/dist/cjs/components/datatable/datatable.recipe.js +2 -0
  3. package/dist/cjs/components/filldoc/filldoc.js +102 -0
  4. package/dist/cjs/components/filldoc/filldoc.recipe.js +211 -0
  5. package/dist/cjs/components/filldoc/index.js +10 -0
  6. package/dist/cjs/components/filldoc/serialize.js +39 -0
  7. package/dist/cjs/components/fillsheet/fillsheet.js +719 -0
  8. package/dist/cjs/components/fillsheet/fillsheet.recipe.js +325 -0
  9. package/dist/cjs/components/fillsheet/index.js +12 -0
  10. package/dist/cjs/components/fillsheet/serialize.js +82 -0
  11. package/dist/cjs/index.js +2 -0
  12. package/dist/cjs/product/recordpage/recordpage.js +1 -1
  13. package/dist/datatable.css +13 -5
  14. package/dist/datatable.layered.css +13 -5
  15. package/dist/esm/components/datatable/datatable.js +65 -5
  16. package/dist/esm/components/datatable/datatable.recipe.js +2 -0
  17. package/dist/esm/components/filldoc/filldoc.js +66 -0
  18. package/dist/esm/components/filldoc/filldoc.recipe.js +208 -0
  19. package/dist/esm/components/filldoc/index.js +3 -0
  20. package/dist/esm/components/filldoc/serialize.js +35 -0
  21. package/dist/esm/components/fillsheet/fillsheet.js +683 -0
  22. package/dist/esm/components/fillsheet/fillsheet.recipe.js +322 -0
  23. package/dist/esm/components/fillsheet/index.js +3 -0
  24. package/dist/esm/components/fillsheet/serialize.js +76 -0
  25. package/dist/esm/index.js +2 -0
  26. package/dist/esm/product/recordpage/recordpage.js +1 -1
  27. package/dist/filldoc.css +31 -0
  28. package/dist/filldoc.d.ts +7 -0
  29. package/dist/filldoc.layered.css +33 -0
  30. package/dist/fillsheet.css +29 -0
  31. package/dist/fillsheet.d.ts +7 -0
  32. package/dist/fillsheet.layered.css +31 -0
  33. package/dist/styles.css +75 -89
  34. package/dist/styles.layered.css +75 -89
  35. package/dist/tokens.css +0 -84
  36. package/dist/types/components/datatable/datatable.d.ts +7 -1
  37. package/dist/types/components/filldoc/filldoc.d.ts +79 -0
  38. package/dist/types/components/filldoc/filldoc.recipe.d.ts +201 -0
  39. package/dist/types/components/filldoc/index.d.ts +3 -0
  40. package/dist/types/components/filldoc/serialize.d.ts +5 -0
  41. package/dist/types/components/fillsheet/fillsheet.d.ts +165 -0
  42. package/dist/types/components/fillsheet/fillsheet.recipe.d.ts +202 -0
  43. package/dist/types/components/fillsheet/index.d.ts +3 -0
  44. package/dist/types/components/fillsheet/serialize.d.ts +45 -0
  45. package/dist/types/index.d.ts +2 -0
  46. package/package.json +11 -1
@@ -0,0 +1,325 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.spec = exports.fillsheetRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ /**
7
+ * FillSheet — a cell canvas, not a table.
8
+ *
9
+ * ON THE MEASUREMENT RULE: every other recipe in this repo takes its numbers
10
+ * from app.attio.com, and CLAUDE.md says not to build what cannot be measured.
11
+ * That rule is WAIVED here, deliberately and with the owner's sign-off: Attio
12
+ * has no spreadsheet, so there is nothing to measure against. This is the one
13
+ * component whose composition is designed rather than reproduced. Do not
14
+ * "restore" it to a measured source — there isn't one.
15
+ *
16
+ * It still spends the measured SCALE wherever a value exists on it (space,
17
+ * text, the semantic colours), because the scale is what keeps this looking
18
+ * like the rest of crisp. The few raw px below each say what decides them.
19
+ *
20
+ * ON THE LOOK: bare. A cell is NOT a box. Boxing every cell on four sides is
21
+ * the single thing that makes a grid read as spreadsheet software, and it was
22
+ * rejected on sight. What is left is a faint rule under each row and between
23
+ * each column, with none on the outer edge — the cells are separated, the
24
+ * sheet is not fenced. There is no lettering, no numbered gutter and no header
25
+ * row anywhere in this file.
26
+ */
27
+ // border-DEFAULT (#e4e4e7), not border-subtle (#eeeff1). Subtle is for a rule
28
+ // on a tinted surface; on white paper it vanishes, and a form whose grid you
29
+ // cannot see is not a form.
30
+ const line = tokens_1.contract.borderDefault;
31
+ // Selection is NEUTRAL, not brand. A blue block laid over the cells is the
32
+ // Excel selection and it was rejected; this is the same ink wash the record
33
+ // tables use for a selected row, so a selected range here and a selected row
34
+ // there read as the same gesture.
35
+ const tint = (pct) => `color-mix(in srgb, ${tokens_1.contract.fgPrimary} ${pct}%, transparent)`;
36
+ // The grab strip for a resize edge. 5px is the smallest target that is
37
+ // reliably hittable with a mouse without swallowing clicks meant for the cell.
38
+ const GRAB = "5px";
39
+ const spec = (0, core_1.defineSpec)({
40
+ name: "fillsheet",
41
+ base: {
42
+ // `grid` (not `table`) is the whole point: per-column widths and per-row
43
+ // heights are template tracks, so either axis can be dragged independently
44
+ // and a cell never has to agree with its column about its height.
45
+ display: "grid",
46
+ gridTemplateColumns: "var(--crisp-fillsheet-cols)",
47
+ gridTemplateRows: "var(--crisp-fillsheet-rows)",
48
+ position: "relative",
49
+ overflow: "auto",
50
+ maxWidth: "100%",
51
+ // The frame is a real border, so without this a consumer who sets a width
52
+ // (or leans on `maxWidth: 100%`, as the A4 example does) gets a content
53
+ // box 2px narrower than they sized for, the column tracks overflow it by
54
+ // those 2px, and the sheet grows a scrollbar for one pixel of nothing.
55
+ boxSizing: "border-box",
56
+ fontFamily: tokens_1.contract.font.sans,
57
+ fontSize: tokens_1.contract.text.md,
58
+ color: tokens_1.contract.fgPrimary,
59
+ background: tokens_1.contract.bgSurface,
60
+ // SQUARE. The web attribute panel this borrows its structure from is a
61
+ // radius-xl card, but a card is a UI surface and this is a form printed
62
+ // on a page -- ruled paper has corners.
63
+ borderRadius: "0",
64
+ // A real BORDER, not `inset` box-shadow. An inset shadow is painted under
65
+ // the element's own children, and every cell here has an opaque
66
+ // background covering the grid edge to edge -- so the frame was drawn and
67
+ // then painted straight over, and the sheet read as having no outline at
68
+ // all. `outline` is the other option and is barred: the root sets
69
+ // `outline: none` on focus, and the frame would blink out when you click
70
+ // into the sheet.
71
+ border: `1px solid ${line}`,
72
+ // Dragging a selection across cells otherwise paints the browser's own
73
+ // text selection over the range tint.
74
+ userSelect: "none",
75
+ WebkitUserSelect: "none",
76
+ },
77
+ parts: [
78
+ // The grid root takes focus so it can receive keys, but it must NOT draw
79
+ // the browser's focus ring: a rectangle around the whole sheet reads as
80
+ // "everything is selected" and drowns the one ring that means it. The
81
+ // focused cell is the focus indicator, and focusing the sheet always
82
+ // selects a cell, so a keyboard user is never left with no visible
83
+ // position.
84
+ { selector: "&:focus, &:focus-visible", decls: { outline: "none" } },
85
+ {
86
+ slot: "cell",
87
+ selector: "& .crisp-fillsheet-cell",
88
+ decls: {
89
+ position: "relative",
90
+ display: "flex",
91
+ // Top, not centre: a cell that wrapped to four lines should start
92
+ // where every other cell in its row starts.
93
+ alignItems: "flex-start",
94
+ minWidth: "0",
95
+ paddingInline: tokens_1.contract.space["3"],
96
+ paddingBlock: tokens_1.contract.space["3"],
97
+ // A divider on both axes: the point is to read it cell by cell, not
98
+ // as ruled rows. The last column and the last row drop theirs (below)
99
+ // so nothing doubles up against the panel's own frame -- the same
100
+ // rule fms uses, where `.attr-row + .attr-row` draws the divider and
101
+ // the outer edge is left to the panel.
102
+ borderInlineEnd: `1px solid ${line}`,
103
+ borderBlockEnd: `1px solid ${line}`,
104
+ background: tokens_1.contract.bgSurface,
105
+ // Narrowing a column WRAPS the text; it does not clip it or hide it
106
+ // behind an ellipsis. `break-word` rather than `anywhere` so ordinary
107
+ // prose still breaks at spaces and only an unbreakable run is split.
108
+ // `pre-wrap` so an explicit newline survives too.
109
+ whiteSpace: "pre-wrap",
110
+ overflowWrap: "break-word",
111
+ // Pinned, not left to `normal`: the row height below is 8 + 20 + 8, so
112
+ // a single-line cell sits with EQUAL space above and below it. Leave
113
+ // this to the font and the text rides high in the row, which is what
114
+ // "too near the top" was.
115
+ lineHeight: "20px",
116
+ cursor: "cell",
117
+ },
118
+ },
119
+ // A read-only column is a KEY cell, and it is filled rather than blank.
120
+ // This is the move the fms iOS detail view makes: its label cell sits on
121
+ // gray200 against a white value cell, and that contrast -- not an outline
122
+ // -- is what makes you read the form cell by cell. It also says which
123
+ // cells are yours to fill before you touch one.
124
+ {
125
+ selector: "& .crisp-fillsheet-cell[aria-readonly]",
126
+ decls: { background: tokens_1.contract.bgRaised, color: tokens_1.contract.fgMuted },
127
+ },
128
+ // The outer edges belong to the frame, not to a cell.
129
+ {
130
+ selector: "& .crisp-fillsheet-cell:last-child",
131
+ decls: { borderInlineEnd: "none" },
132
+ },
133
+ {
134
+ // each row is a `display: contents` wrapper, so the LAST row wrapper is
135
+ // the grid's last child and its cells are the bottom row
136
+ selector: "& > div:last-child .crisp-fillsheet-cell",
137
+ decls: { borderBlockEnd: "none" },
138
+ },
139
+ {
140
+ selector: '& .crisp-fillsheet-cell[data-align="end"]',
141
+ decls: {
142
+ justifyContent: "flex-end",
143
+ fontVariantNumeric: "tabular-nums",
144
+ },
145
+ },
146
+ // --- the state stack ------------------------------------------------
147
+ // One flat tint, and nothing else. There is no hover treatment and no
148
+ // ring: a click opens the cell immediately, so anything layered on the
149
+ // moment before that is decoration you see only in passing. Every cell in
150
+ // a range shares one fill, so a selection reads as a single block rather
151
+ // than a block with its lead cell picked out of it.
152
+ // Only a MULTI-cell selection is painted. Pressing a single cell paints
153
+ // nothing at all: the press opens that cell, so a tint under the pointer
154
+ // is a state that exists for the length of a mouse click and is then
155
+ // replaced -- which reads as a flicker, not as feedback. A range is
156
+ // different. It persists, and it is the thing the fill handle acts on.
157
+ {
158
+ selector: "&[data-multi] .crisp-fillsheet-cell[data-range]",
159
+ decls: { background: tint(6) },
160
+ },
161
+ // A KEYBOARD user has to be able to see where they are. Pressing a cell
162
+ // paints nothing, which is right for the mouse -- the press is on its way
163
+ // to opening that cell -- but it left arrow-key navigation completely
164
+ // invisible: the root drew no focus ring (deliberately, a rectangle round
165
+ // the whole sheet reads as "everything is selected") and a single-cell
166
+ // selection drew nothing either, so nothing on screen said where you
167
+ // were.
168
+ //
169
+ // Hung off `data-keynav`, which the component sets from the last thing
170
+ // that moved the selection, NOT off `:focus-visible`. That was tried and
171
+ // is wrong here: the cell press calls `focus()` on the root, and Chrome
172
+ // counts a programmatic focus as keyboard-ish, so `:focus-visible`
173
+ // matched on a plain mouse press and put the flicker straight back.
174
+ {
175
+ selector: "&[data-keynav] .crisp-fillsheet-cell[data-selected]",
176
+ decls: {
177
+ background: tint(6),
178
+ boxShadow: `inset 0 -2px 0 ${tokens_1.contract.fgPrimary}`,
179
+ },
180
+ },
181
+ {
182
+ // Last, so it beats both. The cell you are typing in shows nothing at
183
+ // all -- the tint is there to say "this is selected", and once you are
184
+ // writing in it that is already obvious.
185
+ selector: "& .crisp-fillsheet-cell[data-editing]",
186
+ decls: { background: tokens_1.contract.bgSurface },
187
+ },
188
+ {
189
+ slot: "columnResize",
190
+ selector: "& .crisp-fillsheet-colresize",
191
+ decls: {
192
+ position: "absolute",
193
+ insetBlockStart: "0",
194
+ insetBlockEnd: "0",
195
+ insetInlineEnd: "0",
196
+ zIndex: "3",
197
+ width: GRAB,
198
+ cursor: "col-resize",
199
+ // a drag on the grab is a RESIZE, never a scroll — without this a
200
+ // finger on the boundary pans the page instead of moving the column
201
+ touchAction: "none",
202
+ },
203
+ },
204
+ {
205
+ slot: "rowResize",
206
+ selector: "& .crisp-fillsheet-rowresize",
207
+ decls: {
208
+ position: "absolute",
209
+ insetInlineStart: "0",
210
+ insetInlineEnd: "0",
211
+ insetBlockEnd: "0",
212
+ zIndex: "3",
213
+ height: GRAB,
214
+ cursor: "row-resize",
215
+ touchAction: "none",
216
+ },
217
+ },
218
+ // Hovering the boundary draws it. This is the ONLY affordance for
219
+ // resizing -- there is no header row to grab -- so it is a solid 2px
220
+ // line in the primary ink, not a tint: at 5px wide and invisible until
221
+ // touched, a fainter one is a control nobody finds.
222
+ {
223
+ selector: "& .crisp-fillsheet-colresize:hover",
224
+ decls: {
225
+ background: `linear-gradient(to right, transparent 0, transparent calc(50% - 1px), ${tokens_1.contract.fgPrimary} calc(50% - 1px), ${tokens_1.contract.fgPrimary} calc(50% + 1px), transparent calc(50% + 1px))`,
226
+ },
227
+ },
228
+ {
229
+ selector: "& .crisp-fillsheet-rowresize:hover",
230
+ decls: {
231
+ background: `linear-gradient(to bottom, transparent 0, transparent calc(50% - 1px), ${tokens_1.contract.fgPrimary} calc(50% - 1px), ${tokens_1.contract.fgPrimary} calc(50% + 1px), transparent calc(50% + 1px))`,
232
+ },
233
+ },
234
+ {
235
+ slot: "editor",
236
+ selector: "& .crisp-fillsheet-editor",
237
+ decls: {
238
+ // IN FLOW, not absolutely positioned over the cell. Pinned to the
239
+ // cell's box with `inset: 0` it could never be taller than the cell,
240
+ // so a second line scrolled out of sight under `overflow: hidden`
241
+ // instead of growing the row. In flow, the textarea's height drives
242
+ // the cell, the cell drives the row's `minmax(h, auto)` track, and a
243
+ // cell grows as you type the way a textarea does.
244
+ display: "block",
245
+ flexGrow: "1",
246
+ width: "100%",
247
+ minWidth: "0",
248
+ margin: "0",
249
+ // No inset of its own: it sits INSIDE the cell's padding now, so
250
+ // repeating it here would double it and shift the text on open.
251
+ padding: "0",
252
+ border: "none",
253
+ outline: "none",
254
+ background: "transparent",
255
+ fontFamily: "inherit",
256
+ fontSize: "inherit",
257
+ // must match the cell's, or a wrapped cell re-flows as you type
258
+ lineHeight: "20px",
259
+ color: "inherit",
260
+ resize: "none",
261
+ // The root sets `user-select: none` so dragging a range does not paint
262
+ // the browser's text selection across the cells -- and that INHERITS,
263
+ // so without putting it back here you cannot select the text inside a
264
+ // cell you have opened. Drag-select, double-click a word, ⌘A: none of
265
+ // it worked in the editor.
266
+ userSelect: "text",
267
+ WebkitUserSelect: "text",
268
+ // the height is set from scrollHeight on every keystroke; a scrollbar
269
+ // would mean that measurement had failed
270
+ overflow: "hidden",
271
+ },
272
+ },
273
+ // A select inside a cell. `Select`'s trigger is a field -- it has a ring,
274
+ // a hover fill and its own inset -- and all of that belongs to a form
275
+ // control sitting in a layout, not to a cell in a sheet that is meant to
276
+ // look like paper. Stripped back so the open cell reads exactly like the
277
+ // closed one, with the list doing the work.
278
+ {
279
+ slot: "selectCell",
280
+ selector: "& .crisp-fillsheet-select .crisp-select-trigger",
281
+ decls: {
282
+ width: "100%",
283
+ height: "100%",
284
+ padding: "0",
285
+ border: "none",
286
+ background: "transparent",
287
+ boxShadow: "none",
288
+ font: "inherit",
289
+ color: "inherit",
290
+ },
291
+ },
292
+ {
293
+ selector: "& .crisp-fillsheet-select .crisp-select-trigger:hover, & .crisp-fillsheet-select .crisp-select-trigger[data-open]",
294
+ decls: { background: "transparent", boxShadow: "none" },
295
+ },
296
+ {
297
+ // the chevron is the one bit of chrome a closed cell must never show,
298
+ // and it has no job while the list is already open either
299
+ selector: "& .crisp-fillsheet-select .crisp-select-chevron",
300
+ decls: { display: "none" },
301
+ },
302
+ ],
303
+ variants: {
304
+ surface: {
305
+ // The frame on the root is the whole edge. The default.
306
+ panel: {},
307
+ // The same frame, lifted off the page, for a sheet that IS the document
308
+ // rather than one section of a screen.
309
+ //
310
+ // `elevation.floating`, not a hand-written stack. The first version was
311
+ // two raw `rgba(0,0,0,...)` layers, which is Lesson 30 exactly: a black
312
+ // shadow does nothing on a dark background, so the lift simply vanished
313
+ // when the theme flipped. The spec gate caught it. The elevation module
314
+ // exists so this value is written once and is theme-aware.
315
+ paper: { boxShadow: tokens_1.elevation.floating },
316
+ // No frame at all.
317
+ bare: { border: "none" },
318
+ },
319
+ },
320
+ defaultVariants: { surface: "panel" },
321
+ });
322
+ exports.spec = spec;
323
+ const config = (0, core_1.specToRecipe)(spec);
324
+ exports.fillsheetRecipe = Object.assign((0, core_1.resolveRecipe)(config), { config });
325
+ exports.default = config;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toTSV = exports.toMarkdown = exports.toHTML = exports.toCSV = exports.fillsheetRecipe = exports.FillSheet = void 0;
4
+ var fillsheet_1 = require("./fillsheet");
5
+ Object.defineProperty(exports, "FillSheet", { enumerable: true, get: function () { return fillsheet_1.FillSheet; } });
6
+ var fillsheet_recipe_1 = require("./fillsheet.recipe");
7
+ Object.defineProperty(exports, "fillsheetRecipe", { enumerable: true, get: function () { return fillsheet_recipe_1.fillsheetRecipe; } });
8
+ var serialize_1 = require("./serialize");
9
+ Object.defineProperty(exports, "toCSV", { enumerable: true, get: function () { return serialize_1.toCSV; } });
10
+ Object.defineProperty(exports, "toHTML", { enumerable: true, get: function () { return serialize_1.toHTML; } });
11
+ Object.defineProperty(exports, "toMarkdown", { enumerable: true, get: function () { return serialize_1.toMarkdown; } });
12
+ Object.defineProperty(exports, "toTSV", { enumerable: true, get: function () { return serialize_1.toTSV; } });
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ /**
3
+ * Getting a sheet OUT, in the formats documents actually read.
4
+ *
5
+ * These are plain functions over `string[][]` — no React, no DOM — so they run
6
+ * in a server route building an export just as well as in a click handler.
7
+ * That is the whole reason the grid is `string[][]`: the value IS the
8
+ * document, and nothing here has to know what a cell meant.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.toTSV = toTSV;
12
+ exports.toCSV = toCSV;
13
+ exports.toHTML = toHTML;
14
+ exports.toMarkdown = toMarkdown;
15
+ const headings = (columns) => columns?.some((c) => c.label !== undefined)
16
+ ? columns.map((c) => c.label ?? "")
17
+ : null;
18
+ /**
19
+ * Tab-separated, the clipboard's lingua franca for a grid: Excel, Sheets,
20
+ * Numbers and Word's table paste all read it as CELLS.
21
+ */
22
+ function toTSV(value) {
23
+ // A tab or a newline INSIDE a cell would invent columns and rows in
24
+ // whatever reads this, so they are flattened to spaces. TSV has no quoting
25
+ // to escape them with — that is what CSV is for.
26
+ return value
27
+ .map((row) => row.map((c) => c.replace(/[\t\n\r]+/g, " ")).join("\t"))
28
+ .join("\n");
29
+ }
30
+ /** RFC 4180: quote a field that contains a comma, a quote or a newline. */
31
+ function toCSV(value, o = {}) {
32
+ const esc = (cell) => /[",\n\r]/.test(cell) ? `"${cell.replace(/"/g, '""')}"` : cell;
33
+ const head = headings(o.columns);
34
+ const rows = head ? [head, ...value] : value;
35
+ // CRLF, which is what RFC 4180 says and what Excel expects on Windows.
36
+ return rows.map((row) => row.map(esc).join(",")).join("\r\n");
37
+ }
38
+ const escapeHtml = (s) => s
39
+ .replace(/&/g, "&")
40
+ .replace(/</g, "&lt;")
41
+ .replace(/>/g, "&gt;")
42
+ .replace(/"/g, "&quot;");
43
+ /**
44
+ * A real `<table>`.
45
+ *
46
+ * This is the one that makes "paste into a document" work: Word, Google Docs,
47
+ * Pages and every rich-text editor read `text/html` from the clipboard and
48
+ * build a formatted table from it. Given only tab-separated text they paste a
49
+ * paragraph of tabs instead.
50
+ *
51
+ * Newlines inside a cell become `<br>`, so a wrapped note survives as a
52
+ * wrapped note rather than collapsing into one line.
53
+ */
54
+ function toHTML(value, o = {}) {
55
+ const cell = (text) => escapeHtml(text).replace(/\r?\n/g, "<br>");
56
+ const head = headings(o.columns);
57
+ const thead = head
58
+ ? `<thead><tr>${head.map((h) => `<th>${cell(h)}</th>`).join("")}</tr></thead>`
59
+ : "";
60
+ const tbody = `<tbody>${value
61
+ .map((row) => `<tr>${row.map((c) => `<td>${cell(c)}</td>`).join("")}</tr>`)
62
+ .join("")}</tbody>`;
63
+ const caption = o.title ? `<caption>${escapeHtml(o.title)}</caption>` : "";
64
+ return `<table>${caption}${thead}${tbody}</table>`;
65
+ }
66
+ /**
67
+ * A GitHub-flavoured Markdown table.
68
+ *
69
+ * A pipe inside a cell is escaped and a newline becomes `<br>`, because a
70
+ * Markdown table row is a single line — an unescaped one silently splits the
71
+ * row into columns that were never there.
72
+ */
73
+ function toMarkdown(value, o = {}) {
74
+ const cell = (text) => text.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
75
+ const width = Math.max(0, ...value.map((r) => r.length));
76
+ const head = headings(o.columns) ?? Array.from({ length: width }, () => "");
77
+ const line = (cells) => `| ${Array.from({ length: head.length }, (_, i) => cell(cells[i] ?? "")).join(" | ")} |`;
78
+ const rule = `| ${head.map(() => "---").join(" | ")} |`;
79
+ const body = value.map(line).join("\n");
80
+ const title = o.title ? `### ${o.title}\n\n` : "";
81
+ return `${title}${line(head)}\n${rule}\n${body}`;
82
+ }
package/dist/cjs/index.js CHANGED
@@ -40,6 +40,8 @@ __exportStar(require("./components/donutchart"), exports);
40
40
  __exportStar(require("./components/drawer"), exports);
41
41
  __exportStar(require("./components/emptystate"), exports);
42
42
  __exportStar(require("./components/fileupload"), exports);
43
+ __exportStar(require("./components/filldoc"), exports);
44
+ __exportStar(require("./components/fillsheet"), exports);
43
45
  __exportStar(require("./components/formfield"), exports);
44
46
  __exportStar(require("./components/input"), exports);
45
47
  __exportStar(require("./components/kbd"), exports);
@@ -392,7 +392,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
392
392
  return ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel", children: (0, jsx_runtime_1.jsx)(EmptyCard, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Folder, { size: 22, strokeWidth: 1.5 }), children: t.nothingHereYet }) }));
393
393
  }
394
394
  };
395
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpage_recipe_1.recordpageRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-topbar", children: [(0, jsx_runtime_1.jsxs)("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-name", children: name })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--bg-brand-solid)" }), t.askAttio] }), onToggleFull && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Minimize2, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Maximize2, { size: 15, strokeWidth: 1.75 })) })), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.recordActions, trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), (0, jsx_runtime_1.jsx)(menu_1.MenuSeparator, {}), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-body", children: [(0, jsx_runtime_1.jsxs)("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-entity-name", children: name }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Pencil, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-quickactions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-compose", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: (0, jsx_runtime_1.jsx)(lucide_react_1.FileText, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Share2, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: (0, jsx_runtime_1.jsx)(lucide_react_1.SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-section", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? ((0, jsx_runtime_1.jsx)("input", {
395
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpage_recipe_1.recordpageRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-topbar", children: [(0, jsx_runtime_1.jsxs)("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-name", children: name })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Minimize2, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Maximize2, { size: 15, strokeWidth: 1.75 })) })), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.recordActions, trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), (0, jsx_runtime_1.jsx)(menu_1.MenuSeparator, {}), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-body", children: [(0, jsx_runtime_1.jsxs)("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-entity-name", children: name }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Pencil, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-quickactions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-compose", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: (0, jsx_runtime_1.jsx)(lucide_react_1.FileText, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Share2, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: (0, jsx_runtime_1.jsx)(lucide_react_1.SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-section", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? ((0, jsx_runtime_1.jsx)("input", {
396
396
  // biome-ignore lint/a11y/noAutofocus: focusing the just-opened field search is expected
397
397
  autoFocus: true, type: "text", className: "crisp-recordpage-fieldsearch", placeholder: t.searchPlaceholder, "aria-label": t.searchFields, value: fieldQuery, onChange: (e) => setFieldQuery(e.target.value), onKeyDown: (e) => {
398
398
  if (e.key === "Escape") {
@@ -43,6 +43,8 @@
43
43
  & tbody tr[data-selected] td[data-sticky] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
44
44
  & tbody tr:hover td[data-sticky] { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); }
45
45
  & .crisp-table-scroll[data-scrolled-x] [data-sticky-edge] { clip-path: inset(0 -38px 0 0); box-shadow: 6px 0 16px 4px rgba(0,0,0,0.12); }
46
+ /* mirror of the rule above, for stickyLast's trailing column (#327) */
47
+ & .crisp-table-scroll[data-scrolled-x-end] [data-sticky-edge-end] { clip-path: inset(0 0 0 -38px); box-shadow: -6px 0 16px 4px rgba(0,0,0,0.12); }
46
48
  /* add-row + summary footer */
47
49
  & .crisp-datatable-addcell { padding: 0; border-top: none; }
48
50
  & .crisp-datatable-addrow { display: flex; align-items: center; gap: var(--crisp-space-2); width: 100%; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border: none; border-top: 1px solid var(--crisp-border-subtle); background: var(--crisp-bg-surface); color: var(--crisp-fg-muted); font-family: inherit; font-size: var(--crisp-text-lg); cursor: pointer; transition: background-color .12s; }
@@ -66,7 +68,12 @@
66
68
  & .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
67
69
  & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
68
70
  & .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
69
- & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); } & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); } & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); } & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); } & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); } & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
71
+ & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
72
+ & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
73
+ & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
74
+ & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
75
+ & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
76
+ & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
70
77
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
71
78
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
72
79
  & .crisp-datatable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
@@ -79,10 +86,11 @@
79
86
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
80
87
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
81
88
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
82
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
83
- a leading 20px grip column, a 16px grip icon, and the row's own hairline doubling as the drop indicator (it tints toward the brand colour as the active drop target). The affordance + geometry are measured; the drag MECHANISM is crisp's existing house idiom -- HTML5
84
- draggable + dataTransfer, the same one column reorder already uses
85
- (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
89
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
90
+ hairline doubling as the drop indicator (it tints toward the brand
91
+ colour as the active drop target). The affordance + geometry are
92
+ measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
93
+ pointer-events implementation. */
86
94
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
87
95
  & .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
88
96
  & .crisp-datatable-grip:active { cursor: grabbing; }
@@ -44,6 +44,8 @@
44
44
  & tbody tr[data-selected] td[data-sticky] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 10%, var(--crisp-bg-surface)); }
45
45
  & tbody tr:hover td[data-sticky] { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); }
46
46
  & .crisp-table-scroll[data-scrolled-x] [data-sticky-edge] { clip-path: inset(0 -38px 0 0); box-shadow: 6px 0 16px 4px rgba(0,0,0,0.12); }
47
+ /* mirror of the rule above, for stickyLast's trailing column (#327) */
48
+ & .crisp-table-scroll[data-scrolled-x-end] [data-sticky-edge-end] { clip-path: inset(0 0 0 -38px); box-shadow: -6px 0 16px 4px rgba(0,0,0,0.12); }
47
49
  /* add-row + summary footer */
48
50
  & .crisp-datatable-addcell { padding: 0; border-top: none; }
49
51
  & .crisp-datatable-addrow { display: flex; align-items: center; gap: var(--crisp-space-2); width: 100%; height: var(--crisp-size-tableRow); padding: 0 var(--crisp-space-2); border: none; border-top: 1px solid var(--crisp-border-subtle); background: var(--crisp-bg-surface); color: var(--crisp-fg-muted); font-family: inherit; font-size: var(--crisp-text-lg); cursor: pointer; transition: background-color .12s; }
@@ -67,7 +69,12 @@
67
69
  & .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
68
70
  & .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
69
71
  & .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
70
- & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); } & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); } & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); } & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); } & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); } & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
72
+ & .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
73
+ & .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
74
+ & .crisp-datatable-avatar[data-tone="danger"] { background: var(--crisp-bg-danger); }
75
+ & .crisp-datatable-avatar[data-tone="purple"] { background: var(--crisp-bg-purple); }
76
+ & .crisp-datatable-avatar[data-tone="cyan"] { background: var(--crisp-bg-cyan); }
77
+ & .crisp-datatable-avatar[data-tone="orange"] { background: var(--crisp-bg-orange); }
71
78
  & .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
72
79
  & .crisp-datatable-tags { display: flex; align-items: center; gap: var(--crisp-space-1); overflow: hidden; }
73
80
  & .crisp-datatable-tag { display: inline-flex; align-items: center; height: var(--crisp-size-chip); padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-chip); font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); white-space: nowrap; background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: color-mix(in srgb, var(--crisp-fg-primary) 82%, transparent); }
@@ -80,10 +87,11 @@
80
87
  & .crisp-datatable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); }
81
88
  & .crisp-datatable-num { font-variant-numeric: tabular-nums; }
82
89
  & .crisp-datatable-muted { color: var(--crisp-fg-muted); }
83
- /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures
84
- a leading 20px grip column, a 16px grip icon, and the row's own hairline doubling as the drop indicator (it tints toward the brand colour as the active drop target). The affordance + geometry are measured; the drag MECHANISM is crisp's existing house idiom -- HTML5
85
- draggable + dataTransfer, the same one column reorder already uses
86
- (RecordsTable's reorderColumn) -- not a proprietary pointer-events implementation. */
90
+ /* row drag-to-reorder -- a Settings > Objects > Attributes screen measures a leading 20px grip column, a 16px grip icon, and the row's own
91
+ hairline doubling as the drop indicator (it tints toward the brand
92
+ colour as the active drop target). The affordance + geometry are
93
+ measured; the drag MECHANISM is crisp's existing house idiom -- HTML5 draggable + dataTransfer, the same one column reorder already uses (RecordsTable's reorderColumn) -- not a proprietary
94
+ pointer-events implementation. */
87
95
  & .crisp-datatable-col-grip { width: 20px; padding: 0; text-align: center; }
88
96
  & .crisp-datatable-grip { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; border: none; border-radius: 0; background: none; font-family: inherit; color: var(--crisp-fg-muted); cursor: grab; }
89
97
  & .crisp-datatable-grip:active { cursor: grabbing; }