@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.
- package/dist/cjs/components/datatable/datatable.js +65 -5
- package/dist/cjs/components/datatable/datatable.recipe.js +2 -0
- package/dist/cjs/components/filldoc/filldoc.js +102 -0
- package/dist/cjs/components/filldoc/filldoc.recipe.js +211 -0
- package/dist/cjs/components/filldoc/index.js +10 -0
- package/dist/cjs/components/filldoc/serialize.js +39 -0
- package/dist/cjs/components/fillsheet/fillsheet.js +719 -0
- package/dist/cjs/components/fillsheet/fillsheet.recipe.js +325 -0
- package/dist/cjs/components/fillsheet/index.js +12 -0
- package/dist/cjs/components/fillsheet/serialize.js +82 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/product/recordpage/recordpage.js +1 -1
- package/dist/datatable.css +13 -5
- package/dist/datatable.layered.css +13 -5
- package/dist/esm/components/datatable/datatable.js +65 -5
- package/dist/esm/components/datatable/datatable.recipe.js +2 -0
- package/dist/esm/components/filldoc/filldoc.js +66 -0
- package/dist/esm/components/filldoc/filldoc.recipe.js +208 -0
- package/dist/esm/components/filldoc/index.js +3 -0
- package/dist/esm/components/filldoc/serialize.js +35 -0
- package/dist/esm/components/fillsheet/fillsheet.js +683 -0
- package/dist/esm/components/fillsheet/fillsheet.recipe.js +322 -0
- package/dist/esm/components/fillsheet/index.js +3 -0
- package/dist/esm/components/fillsheet/serialize.js +76 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/product/recordpage/recordpage.js +1 -1
- package/dist/filldoc.css +31 -0
- package/dist/filldoc.d.ts +7 -0
- package/dist/filldoc.layered.css +33 -0
- package/dist/fillsheet.css +29 -0
- package/dist/fillsheet.d.ts +7 -0
- package/dist/fillsheet.layered.css +31 -0
- package/dist/styles.css +75 -89
- package/dist/styles.layered.css +75 -89
- package/dist/tokens.css +0 -84
- package/dist/types/components/datatable/datatable.d.ts +7 -1
- package/dist/types/components/filldoc/filldoc.d.ts +79 -0
- package/dist/types/components/filldoc/filldoc.recipe.d.ts +201 -0
- package/dist/types/components/filldoc/index.d.ts +3 -0
- package/dist/types/components/filldoc/serialize.d.ts +5 -0
- package/dist/types/components/fillsheet/fillsheet.d.ts +165 -0
- package/dist/types/components/fillsheet/fillsheet.recipe.d.ts +202 -0
- package/dist/types/components/fillsheet/index.d.ts +3 -0
- package/dist/types/components/fillsheet/serialize.d.ts +45 -0
- package/dist/types/index.d.ts +2 -0
- 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,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, "<")
|
|
35
|
+
.replace(/>/g, ">")
|
|
36
|
+
.replace(/"/g, """);
|
|
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";
|
|
@@ -355,7 +355,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
355
355
|
return (_jsx("div", { className: "crisp-recordpage-panel", children: _jsx(EmptyCard, { icon: _jsx(Folder, { size: 22, strokeWidth: 1.5 }), children: t.nothingHereYet }) }));
|
|
356
356
|
}
|
|
357
357
|
};
|
|
358
|
-
return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _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: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: _jsx(Copy, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: _jsx(FileText, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: _jsx(Share2, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: _jsx(SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsxs("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? (_jsx("input", {
|
|
358
|
+
return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _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: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: _jsx(Copy, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: _jsx(FileText, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: _jsx(Share2, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: _jsx(SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsxs("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? (_jsx("input", {
|
|
359
359
|
// biome-ignore lint/a11y/noAutofocus: focusing the just-opened field search is expected
|
|
360
360
|
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) => {
|
|
361
361
|
if (e.key === "Escape") {
|
package/dist/filldoc.css
ADDED
|
@@ -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,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,31 @@
|
|
|
1
|
+
@layer crisp-components {
|
|
2
|
+
.crisp-fillsheet {
|
|
3
|
+
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;
|
|
4
|
+
&:focus, &:focus-visible { outline: none; }
|
|
5
|
+
& .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; }
|
|
6
|
+
& .crisp-fillsheet-cell[aria-readonly] { background: var(--crisp-bg-raised); color: var(--crisp-fg-muted); }
|
|
7
|
+
& .crisp-fillsheet-cell:last-child { border-inline-end: none; }
|
|
8
|
+
& > div:last-child .crisp-fillsheet-cell { border-block-end: none; }
|
|
9
|
+
& .crisp-fillsheet-cell[data-align="end"] { justify-content: flex-end; font-variant-numeric: tabular-nums; }
|
|
10
|
+
&[data-multi] .crisp-fillsheet-cell[data-range] { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
|
|
11
|
+
&[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); }
|
|
12
|
+
& .crisp-fillsheet-cell[data-editing] { background: var(--crisp-bg-surface); }
|
|
13
|
+
& .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; }
|
|
14
|
+
& .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; }
|
|
15
|
+
& .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)); }
|
|
16
|
+
& .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)); }
|
|
17
|
+
& .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; }
|
|
18
|
+
& .crisp-fillsheet-select .crisp-select-trigger { width: 100%; height: 100%; padding: 0; border: none; background: transparent; box-shadow: none; font: inherit; color: inherit; }
|
|
19
|
+
& .crisp-fillsheet-select .crisp-select-trigger:hover, & .crisp-fillsheet-select .crisp-select-trigger[data-open] { background: transparent; box-shadow: none; }
|
|
20
|
+
& .crisp-fillsheet-select .crisp-select-chevron { display: none; }
|
|
21
|
+
}
|
|
22
|
+
.crisp-fillsheet--surface_panel {
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
.crisp-fillsheet--surface_paper {
|
|
26
|
+
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);
|
|
27
|
+
}
|
|
28
|
+
.crisp-fillsheet--surface_bare {
|
|
29
|
+
border: none;
|
|
30
|
+
}
|
|
31
|
+
}
|