@crisp-ui-kit/crisp 0.34.0 → 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 (43) 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/datatable.css +13 -5
  13. package/dist/datatable.layered.css +13 -5
  14. package/dist/esm/components/datatable/datatable.js +65 -5
  15. package/dist/esm/components/datatable/datatable.recipe.js +2 -0
  16. package/dist/esm/components/filldoc/filldoc.js +66 -0
  17. package/dist/esm/components/filldoc/filldoc.recipe.js +208 -0
  18. package/dist/esm/components/filldoc/index.js +3 -0
  19. package/dist/esm/components/filldoc/serialize.js +35 -0
  20. package/dist/esm/components/fillsheet/fillsheet.js +683 -0
  21. package/dist/esm/components/fillsheet/fillsheet.recipe.js +322 -0
  22. package/dist/esm/components/fillsheet/index.js +3 -0
  23. package/dist/esm/components/fillsheet/serialize.js +76 -0
  24. package/dist/esm/index.js +2 -0
  25. package/dist/filldoc.css +31 -0
  26. package/dist/filldoc.d.ts +7 -0
  27. package/dist/filldoc.layered.css +33 -0
  28. package/dist/fillsheet.css +29 -0
  29. package/dist/fillsheet.d.ts +7 -0
  30. package/dist/fillsheet.layered.css +31 -0
  31. package/dist/styles.css +75 -5
  32. package/dist/styles.layered.css +75 -5
  33. package/dist/types/components/datatable/datatable.d.ts +7 -1
  34. package/dist/types/components/filldoc/filldoc.d.ts +79 -0
  35. package/dist/types/components/filldoc/filldoc.recipe.d.ts +201 -0
  36. package/dist/types/components/filldoc/index.d.ts +3 -0
  37. package/dist/types/components/filldoc/serialize.d.ts +5 -0
  38. package/dist/types/components/fillsheet/fillsheet.d.ts +165 -0
  39. package/dist/types/components/fillsheet/fillsheet.recipe.d.ts +202 -0
  40. package/dist/types/components/fillsheet/index.d.ts +3 -0
  41. package/dist/types/components/fillsheet/serialize.d.ts +45 -0
  42. package/dist/types/index.d.ts +2 -0
  43. package/package.json +11 -1
@@ -171,7 +171,7 @@ function renderTyped(col, value, avatarTone, display, locale = "en-US") {
171
171
  }
172
172
  }
173
173
  const CHECK_W = 40;
174
- function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel = "Select all rows", selectRowLabel = (row) => `Select row ${row.id}`, selectionAnnouncement = (count) => count === 0 ? "Selection cleared" : `${count} selected`, columnMenu = false, onColumnsChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable = false, searchPlaceholder = "Search…", searchLabel = "Search table", onEdit, stickyFirst = true, locale: localeProp, avatarTone = "brand", surface, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", empty = "No results.", showCount = true, countLabel = (shown, total) => shown === total
174
+ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel = "Select all rows", selectRowLabel = (row) => `Select row ${row.id}`, selectionAnnouncement = (count) => count === 0 ? "Selection cleared" : `${count} selected`, columnMenu = false, onColumnsChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable = false, searchPlaceholder = "Search…", searchLabel = "Search table", onEdit, stickyFirst = true, stickyLast = false, locale: localeProp, avatarTone = "brand", surface, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", empty = "No results.", showCount = true, countLabel = (shown, total) => shown === total
175
175
  ? `${total} ${total === 1 ? "row" : "rows"}`
176
176
  : `${shown} of ${total}`, toolbar, className, reorderRowLabel = "Reorder row", ...rest }) {
177
177
  // Two different defaults on purpose. FORMATTING falls back to "en-US", which
@@ -271,6 +271,21 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
271
271
  }
272
272
  const [editing, setEditing] = React.useState(null);
273
273
  const [scrolledX, setScrolledX] = React.useState(false);
274
+ // Mirrors `scrolledX` for the trailing edge: true while there is still
275
+ // unrevealed content sliding under the sticky trailing column, i.e. while
276
+ // NOT scrolled all the way to the end. Recomputed after every render (see
277
+ // the effect below), not just on scroll, because a column resize or a data
278
+ // change can move `scrollWidth` without firing a scroll event.
279
+ const [scrolledXEnd, setScrolledXEnd] = React.useState(false);
280
+ const scrollRef = React.useRef(null);
281
+ React.useEffect(() => {
282
+ if (!stickyLast)
283
+ return;
284
+ const el = scrollRef.current;
285
+ if (!el)
286
+ return;
287
+ setScrolledXEnd(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
288
+ });
274
289
  // column widths (drag-to-resize) + fixed-layout sizing
275
290
  const [widths, setWidths] = React.useState({});
276
291
  const [resizingKey, setResizingKey] = React.useState(null);
@@ -469,6 +484,43 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
469
484
  };
470
485
  return {};
471
486
  };
487
+ // Mirror of the group above, for a single trailing column (#327): the last
488
+ // VISIBLE data column (`columns`, not `allColumns` — a hidden column can't
489
+ // be the one that pins). `insetInlineEnd`, never `right`, for the same
490
+ // direction-safety reason `stickyCol` uses `insetInlineStart`: a physical
491
+ // `right` pins to the trailing edge only in LTR and swaps to the LEADING
492
+ // edge under `dir="rtl"`, which is backwards for a column meant to stay
493
+ // pinned to the reading direction's end.
494
+ const stickyLastCol = (index) => {
495
+ if (!stickyLast || index !== columns.length - 1)
496
+ return {};
497
+ return {
498
+ "data-sticky": "",
499
+ "data-sticky-edge-end": "",
500
+ style: { insetInlineEnd: 0 },
501
+ };
502
+ };
503
+ // Merges the two groups for a data-column cell. A column can be pinned by
504
+ // both (a one-column table with both flags on) — the header sticky outranks
505
+ // either via its own z-index rule (#285's ordering still holds), and here
506
+ // the two style objects merge rather than one overwriting the other.
507
+ const dataStickyCol = (index) => {
508
+ const lead = stickyCol(index);
509
+ const trail = stickyLastCol(index);
510
+ // Not `!lead["data-sticky"]`: the attribute's value is `""`, which is
511
+ // falsy, so that check silently treated every pinned column as unpinned.
512
+ if (!("data-sticky" in lead) && !("data-sticky" in trail))
513
+ return {};
514
+ return {
515
+ ...lead,
516
+ ...trail,
517
+ "data-sticky": "",
518
+ style: {
519
+ ...lead.style,
520
+ ...trail.style,
521
+ },
522
+ };
523
+ };
472
524
  const alignFor = (col) => col.align ??
473
525
  (col.type === "number" || col.type === "currency" ? "right" : undefined);
474
526
  function startResize(col, e) {
@@ -506,8 +558,16 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
506
558
  return ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 }));
507
559
  }
508
560
  return ((0, jsx_runtime_1.jsxs)("div", { ref: gridRef, className: (0, datatable_recipe_1.datatableRecipe)({ className }), tabIndex: -1, onKeyDown: onGridKeyDown, ...rest, children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-live", role: "status", "aria-live": "polite", children: selectionText }), (filterable || showCount || toolbar || columnMenu) && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-datatable-toolbar", children: [columnMenu && ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-viewsettings", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14, strokeWidth: 1.75 }), "View settings"] }), children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-group-label", children: "Columns" }), allColumns.map((c) => ((0, jsx_runtime_1.jsxs)(menu_1.MenuItem, { onSelect: () => c.hidden ? showColumn(c.key) : hideColumn(c.key), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 14, strokeWidth: 2, style: { opacity: c.hidden ? 0 : 1 } }), typeof c.header === "string" ? c.header : c.key] }, c.key)))] })), filterable && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchLabel })] })), showCount && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-count", children: countLabel(rows.length, data.length) })), toolbar] })), (0, jsx_runtime_1.jsxs)(table_1.Table, { interactive: interactive, surface: surface, style: { minWidth }, scrollProps: {
509
- onScroll: (e) => setScrolledX(e.currentTarget.scrollLeft > 0),
561
+ ref: scrollRef,
562
+ onScroll: (e) => {
563
+ const el = e.currentTarget;
564
+ setScrolledX(el.scrollLeft > 0);
565
+ if (stickyLast) {
566
+ setScrolledXEnd(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
567
+ }
568
+ },
510
569
  ...(scrolledX ? { "data-scrolled-x": "" } : {}),
570
+ ...(scrolledXEnd ? { "data-scrolled-x-end": "" } : {}),
511
571
  }, children: [(0, jsx_runtime_1.jsxs)("colgroup", { children: [showGrip && (0, jsx_runtime_1.jsx)("col", { style: { width: GRIP_W } }), selectable && (0, jsx_runtime_1.jsx)("col", { style: { width: CHECK_W } }), columns.map((col) => ((0, jsx_runtime_1.jsx)("col", { style: { width: colWidth(col) } }, col.key)))] }), (0, jsx_runtime_1.jsx)(table_1.TableHeader, { children: (0, jsx_runtime_1.jsxs)(table_1.TableRow, { children: [showGrip && ((0, jsx_runtime_1.jsx)(table_1.TableHead, { className: "crisp-datatable-col-grip", ...stickyCol(-2) })), selectable && ((0, jsx_runtime_1.jsx)(table_1.TableHead, { className: "crisp-datatable-col-check", ...stickyCol(-1), children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-check", children: (0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { "aria-label": selectAllLabel, checked: allSelected, indeterminate: someSelected, onChange: toggleAll }) }) })), columns.map((col, i) => {
512
572
  const Icon = col.icon;
513
573
  // The sort direction was painted (`data-sorted`) and never
@@ -525,7 +585,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
525
585
  : "descending"
526
586
  : "none";
527
587
  const inner = ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-th-inner", children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 14, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-th-label", children: col.header }), col.sortable ? (0, jsx_runtime_1.jsx)(ChevronFor, { colKey: col.key }) : null] }));
528
- return ((0, jsx_runtime_1.jsxs)(table_1.TableHead, { align: alignFor(col), "aria-sort": ariaSort, ...stickyCol(i), children: [renaming === col.key ? ((0, jsx_runtime_1.jsx)("input", {
588
+ return ((0, jsx_runtime_1.jsxs)(table_1.TableHead, { align: alignFor(col), "aria-sort": ariaSort, ...dataStickyCol(i), children: [renaming === col.key ? ((0, jsx_runtime_1.jsx)("input", {
529
589
  // biome-ignore lint/a11y/noAutofocus: the rename item opens this field
530
590
  autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label": `Rename column`, onBlur: (e) => {
531
591
  renameColumn(col.key, e.currentTarget.value);
@@ -565,7 +625,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
565
625
  }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })), selectable && ((0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-col-check", ...stickyCol(-1), onClick: (e) => e.stopPropagation(), children: (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-check", children: (0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { "aria-label": selectRowLabel(row), checked: selected.has(row.id), onChange: () => toggleRow(row.id) }) }) })), columns.map((col, i) => {
566
626
  const isEditing = editing?.id === row.id && editing?.key === col.key;
567
627
  const value = cellValue(col, row);
568
- const sticky = stickyCol(i);
628
+ const sticky = dataStickyCol(i);
569
629
  if (isEditing) {
570
630
  return ((0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), ...sticky, onClick: (e) => e.stopPropagation(), children: (0, jsx_runtime_1.jsx)("input", { className: "crisp-datatable-edit",
571
631
  // biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
@@ -598,6 +658,6 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
598
658
  }, children: col.render
599
659
  ? col.render(value, row)
600
660
  : renderTyped(col, value, avatarTone, col.format?.(value, row), locale) }, col.key));
601
- })] }, row.id)))] }, group ?? "__ungrouped")))) }), (hasSummary || onAddRow) && ((0, jsx_runtime_1.jsxs)(table_1.TableFooter, { children: [hasSummary && ((0, jsx_runtime_1.jsxs)(table_1.TableRow, { children: [showGrip && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-2) }), selectable && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-1) }), columns.map((col, i) => ((0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), ...stickyCol(i), children: col.summary ? col.summary(rows) : null }, col.key)))] })), onAddRow && ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-addcell", colSpan: totalCols, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-addrow", onClick: onAddRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), addRowLabel] }) }) }))] }))] })] }));
661
+ })] }, row.id)))] }, group ?? "__ungrouped")))) }), (hasSummary || onAddRow) && ((0, jsx_runtime_1.jsxs)(table_1.TableFooter, { children: [hasSummary && ((0, jsx_runtime_1.jsxs)(table_1.TableRow, { children: [showGrip && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-2) }), selectable && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-1) }), columns.map((col, i) => ((0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), ...dataStickyCol(i), children: col.summary ? col.summary(rows) : null }, col.key)))] })), onAddRow && ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-addcell", colSpan: totalCols, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-addrow", onClick: onAddRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), addRowLabel] }) }) }))] }))] })] }));
602
662
  }
603
663
  DataTable.displayName = "DataTable";
@@ -62,6 +62,8 @@ const config = (0, core_1.defineRecipe)({
62
62
  & tbody tr[data-selected] td[data-sticky] { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 10%, ${tokens_1.contract.bgSurface}); }
63
63
  & tbody tr:hover td[data-sticky] { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, ${tokens_1.contract.bgSurface}); }
64
64
  & .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); }
65
+ /* mirror of the rule above, for stickyLast's trailing column (#327) */
66
+ & .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); }
65
67
  /* add-row + summary footer */
66
68
  & .crisp-datatable-addcell { padding: 0; border-top: none; }
67
69
  & .crisp-datatable-addrow { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; width: 100%; height: ${tokens_1.contract.size.tableRow}; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-top: 1px solid ${tokens_1.contract.borderSubtle}; background: ${tokens_1.contract.bgSurface}; color: ${tokens_1.contract.fgMuted}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; cursor: pointer; transition: background-color .12s; }
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ "use client";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.FillDoc = void 0;
38
+ const jsx_runtime_1 = require("react/jsx-runtime");
39
+ const React = __importStar(require("react"));
40
+ const core_1 = require("../../core");
41
+ const fillsheet_1 = require("../fillsheet");
42
+ const filldoc_recipe_1 = require("./filldoc.recipe");
43
+ /** Size a textarea to its content; see FillSheet's autoGrow for the reset. */
44
+ function autoGrow(el) {
45
+ if (!el)
46
+ return;
47
+ el.style.height = "auto";
48
+ el.style.height = `${el.scrollHeight}px`;
49
+ }
50
+ let seq = 0;
51
+ const nextId = () => `b${++seq}`;
52
+ const emptyTable = () => ({
53
+ id: nextId(),
54
+ type: "table",
55
+ columns: [{ width: 160 }, { width: 320 }],
56
+ value: [
57
+ ["", ""],
58
+ ["", ""],
59
+ ],
60
+ });
61
+ /**
62
+ * A page you can write on, of which a sheet is one block.
63
+ *
64
+ * FillSheet is the table; this is the document it sits in. The two share a
65
+ * look on purpose -- a real page, real margins, square corners, and nothing
66
+ * drawn that is not content -- and the same split between the person who
67
+ * MAKES a form and the person who fills it in: in `edit` you write the prose
68
+ * and add or remove blocks, in `fill` the prose is fixed and only the sheets
69
+ * take input.
70
+ *
71
+ * Every block's controls are hidden until the pointer is in that block. A
72
+ * page covered in handles and grips stops looking like a page, which is the
73
+ * whole thing this is for.
74
+ */
75
+ exports.FillDoc = React.forwardRef(function FillDoc({ blocks, onBlocksChange, mode = "edit", page = "a4", label, addTextLabel = "Text", addHeadingLabel = "Heading", addTableLabel = "Table", removeLabel = "Remove", className, style, ...rest }, forwardedRef) {
76
+ const editable = mode === "edit";
77
+ const replace = (i, block) => {
78
+ const next = blocks.slice();
79
+ next[i] = block;
80
+ onBlocksChange?.(next);
81
+ };
82
+ const insertAfter = (i, block) => {
83
+ const next = blocks.slice();
84
+ next.splice(i + 1, 0, block);
85
+ onBlocksChange?.(next);
86
+ };
87
+ const removeAt = (i) => {
88
+ const next = blocks.slice();
89
+ next.splice(i, 1);
90
+ onBlocksChange?.(next);
91
+ };
92
+ return ((0, jsx_runtime_1.jsx)("div", { ...rest, ref: forwardedRef,
93
+ // The role rides WITH the name: `aria-label` on a role-less div is
94
+ // not exposed at all, and a bare `role="group"` on an unnamed page
95
+ // just adds an empty group to the tree. Either both or neither.
96
+ role: label ? "group" : undefined, "aria-label": label, className: (0, core_1.cx)((0, filldoc_recipe_1.filldocRecipe)({ page }), className), style: style, children: blocks.map((block, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-filldoc-block", children: [editable && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-filldoc-tools", children: (0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": removeLabel, title: removeLabel, onClick: () => removeAt(i), children: "\u00D7" }) })), block.type === "table" ? ((0, jsx_runtime_1.jsx)(fillsheet_1.FillSheet, { label: block.label, mode: mode, surface: "panel", columns: block.columns, value: block.value, onValueChange: (value) => replace(i, { ...block, value }) })) : editable ? ((0, jsx_runtime_1.jsx)("textarea", { ref: autoGrow, className: (0, core_1.cx)("crisp-filldoc-writer", block.type === "heading"
97
+ ? "crisp-filldoc-heading"
98
+ : "crisp-filldoc-text"), rows: 1, value: block.text, "aria-label": block.type === "heading" ? addHeadingLabel : addTextLabel, onChange: (e) => {
99
+ autoGrow(e.currentTarget);
100
+ replace(i, { ...block, text: e.target.value });
101
+ } })) : block.type === "heading" ? ((0, jsx_runtime_1.jsx)("h2", { className: "crisp-filldoc-heading", children: block.text })) : ((0, jsx_runtime_1.jsx)("p", { className: "crisp-filldoc-text", children: block.text })), editable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-filldoc-insert", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-filldoc-insert-rule" }), (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => insertAfter(i, { id: nextId(), type: "text", text: "" }), children: ["+ ", addTextLabel] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => insertAfter(i, { id: nextId(), type: "heading", text: "" }), children: ["+ ", addHeadingLabel] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => insertAfter(i, emptyTable()), children: ["+ ", addTableLabel] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-filldoc-insert-rule" })] }))] }, block.id))) }));
102
+ });
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.spec = exports.filldocRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ /**
7
+ * FillDoc — a page of blocks, of which a sheet is one.
8
+ *
9
+ * The measurement rule is waived here for the same reason it is waived for
10
+ * FillSheet, and by the same decision: Attio has no document editor. It spends
11
+ * the measured scale everywhere a value exists on it.
12
+ *
13
+ * The look is FillSheet's, continued: a real page with real margins, square
14
+ * corners, and nothing on it that is not content. A block shows no chrome at
15
+ * rest — no handle, no outline, no drag grip — because a page covered in
16
+ * controls stops looking like a page. The controls appear where the pointer
17
+ * is, and only in `edit`.
18
+ */
19
+ const line = tokens_1.contract.borderDefault;
20
+ // A4 at 96dpi, in the unit that actually means A4. `mm` is a real CSS unit,
21
+ // so this prints at the size it says.
22
+ const PAGE_WIDTH = "210mm";
23
+ const PAGE_MIN_HEIGHT = "297mm";
24
+ const PAGE_MARGIN = "20mm";
25
+ const spec = (0, core_1.defineSpec)({
26
+ name: "filldoc",
27
+ base: {
28
+ display: "flex",
29
+ flexDirection: "column",
30
+ gap: tokens_1.contract.space["4"],
31
+ boxSizing: "border-box",
32
+ width: PAGE_WIDTH,
33
+ minHeight: PAGE_MIN_HEIGHT,
34
+ padding: PAGE_MARGIN,
35
+ margin: "0 auto",
36
+ background: tokens_1.contract.bgSurface,
37
+ fontFamily: tokens_1.contract.font.sans,
38
+ fontSize: tokens_1.contract.text.lg,
39
+ lineHeight: "22px",
40
+ color: tokens_1.contract.fgPrimary,
41
+ // Paper is not rounded, and the lift is what says it is a sheet rather
42
+ // than a panel in a layout.
43
+ borderRadius: "0",
44
+ boxShadow: tokens_1.elevation.floating,
45
+ },
46
+ parts: [
47
+ { selector: "&:focus, &:focus-visible", decls: { outline: "none" } },
48
+ {
49
+ slot: "block",
50
+ selector: "& .crisp-filldoc-block",
51
+ decls: { position: "relative" },
52
+ },
53
+ // --- text ------------------------------------------------------------
54
+ {
55
+ slot: "heading",
56
+ selector: "& .crisp-filldoc-heading",
57
+ decls: {
58
+ margin: "0",
59
+ fontSize: tokens_1.contract.text.xl,
60
+ fontWeight: tokens_1.contract.weight.semibold,
61
+ lineHeight: "26px",
62
+ color: tokens_1.contract.fgPrimary,
63
+ whiteSpace: "pre-wrap",
64
+ overflowWrap: "break-word",
65
+ },
66
+ },
67
+ {
68
+ slot: "paragraph",
69
+ selector: "& .crisp-filldoc-text",
70
+ decls: {
71
+ margin: "0",
72
+ fontSize: tokens_1.contract.text.lg,
73
+ lineHeight: "22px",
74
+ color: tokens_1.contract.fgPrimary,
75
+ whiteSpace: "pre-wrap",
76
+ overflowWrap: "break-word",
77
+ },
78
+ },
79
+ // The editor for a text block is the text: same font, same line box, no
80
+ // box of its own. Anything else and the words move when you click them.
81
+ {
82
+ slot: "writer",
83
+ selector: "& .crisp-filldoc-writer",
84
+ decls: {
85
+ display: "block",
86
+ width: "100%",
87
+ margin: "0",
88
+ padding: "0",
89
+ border: "none",
90
+ outline: "none",
91
+ background: "transparent",
92
+ fontFamily: "inherit",
93
+ fontSize: "inherit",
94
+ fontWeight: "inherit",
95
+ lineHeight: "inherit",
96
+ color: "inherit",
97
+ resize: "none",
98
+ overflow: "hidden",
99
+ // the page turns text selection off so a block drag does not smear
100
+ // one across it; the writer has to opt back in
101
+ userSelect: "text",
102
+ WebkitUserSelect: "text",
103
+ },
104
+ },
105
+ // --- the between-blocks control --------------------------------------
106
+ // A page covered in buttons stops reading as a page, so the only thing
107
+ // that ever appears is a hairline with a + on it, in the gap the pointer
108
+ // is actually in.
109
+ {
110
+ slot: "inserter",
111
+ selector: "& .crisp-filldoc-insert",
112
+ decls: {
113
+ position: "absolute",
114
+ insetInlineStart: "0",
115
+ insetInlineEnd: "0",
116
+ insetBlockEnd: `calc(${tokens_1.contract.space["4"]} / -2)`,
117
+ height: tokens_1.contract.space["4"],
118
+ display: "flex",
119
+ alignItems: "center",
120
+ gap: tokens_1.contract.space["2"],
121
+ opacity: "0",
122
+ transition: "opacity .1s",
123
+ cursor: "pointer",
124
+ },
125
+ },
126
+ {
127
+ selector: "& .crisp-filldoc-block:hover .crisp-filldoc-insert, & .crisp-filldoc-insert:focus-within",
128
+ decls: { opacity: "1" },
129
+ },
130
+ {
131
+ selector: "& .crisp-filldoc-insert-rule",
132
+ decls: { flex: "1", height: "1px", background: line },
133
+ },
134
+ {
135
+ slot: "insertButton",
136
+ selector: "& .crisp-filldoc-insert button",
137
+ decls: {
138
+ display: "inline-flex",
139
+ alignItems: "center",
140
+ height: "20px",
141
+ padding: `0 ${tokens_1.contract.space["1.5"]}`,
142
+ border: "none",
143
+ borderRadius: tokens_1.contract.radius.sm,
144
+ background: tokens_1.contract.bgRaised,
145
+ fontFamily: "inherit",
146
+ fontSize: tokens_1.contract.text.sm,
147
+ color: tokens_1.contract.fgMuted,
148
+ cursor: "pointer",
149
+ },
150
+ },
151
+ {
152
+ selector: "& .crisp-filldoc-insert button:hover",
153
+ decls: { background: tokens_1.contract.bgNeutralWeak, color: tokens_1.contract.fgPrimary },
154
+ },
155
+ // --- the block's own controls ----------------------------------------
156
+ {
157
+ slot: "blockTools",
158
+ selector: "& .crisp-filldoc-tools",
159
+ decls: {
160
+ position: "absolute",
161
+ insetInlineEnd: "100%",
162
+ insetBlockStart: "0",
163
+ paddingInlineEnd: tokens_1.contract.space["2"],
164
+ display: "flex",
165
+ gap: tokens_1.contract.space["1"],
166
+ opacity: "0",
167
+ transition: "opacity .1s",
168
+ },
169
+ },
170
+ {
171
+ selector: "& .crisp-filldoc-block:hover .crisp-filldoc-tools, & .crisp-filldoc-tools:focus-within",
172
+ decls: { opacity: "1" },
173
+ },
174
+ {
175
+ selector: "& .crisp-filldoc-tools button",
176
+ decls: {
177
+ width: "20px",
178
+ height: "20px",
179
+ display: "inline-flex",
180
+ alignItems: "center",
181
+ justifyContent: "center",
182
+ padding: "0",
183
+ border: "none",
184
+ borderRadius: tokens_1.contract.radius.sm,
185
+ background: "transparent",
186
+ fontFamily: "inherit",
187
+ fontSize: tokens_1.contract.text.sm,
188
+ color: tokens_1.contract.fgMuted,
189
+ cursor: "pointer",
190
+ },
191
+ },
192
+ {
193
+ selector: "& .crisp-filldoc-tools button:hover",
194
+ decls: { background: tokens_1.contract.bgRaised, color: tokens_1.contract.fgPrimary },
195
+ },
196
+ ],
197
+ variants: {
198
+ page: {
199
+ // A real sheet of A4. The default, because the point is a document.
200
+ a4: {},
201
+ // Grows to its content and fills its container instead, for a document
202
+ // embedded in a screen rather than printed.
203
+ auto: { width: "100%", minHeight: "0", maxWidth: PAGE_WIDTH },
204
+ },
205
+ },
206
+ defaultVariants: { page: "a4" },
207
+ });
208
+ exports.spec = spec;
209
+ const config = (0, core_1.specToRecipe)(spec);
210
+ exports.filldocRecipe = Object.assign((0, core_1.resolveRecipe)(config), { config });
211
+ exports.default = config;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.docToMarkdown = exports.docToHTML = exports.filldocRecipe = exports.FillDoc = void 0;
4
+ var filldoc_1 = require("./filldoc");
5
+ Object.defineProperty(exports, "FillDoc", { enumerable: true, get: function () { return filldoc_1.FillDoc; } });
6
+ var filldoc_recipe_1 = require("./filldoc.recipe");
7
+ Object.defineProperty(exports, "filldocRecipe", { enumerable: true, get: function () { return filldoc_recipe_1.filldocRecipe; } });
8
+ var serialize_1 = require("./serialize");
9
+ Object.defineProperty(exports, "docToHTML", { enumerable: true, get: function () { return serialize_1.docToHTML; } });
10
+ Object.defineProperty(exports, "docToMarkdown", { enumerable: true, get: function () { return serialize_1.docToMarkdown; } });
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.docToHTML = docToHTML;
4
+ exports.docToMarkdown = docToMarkdown;
5
+ const serialize_1 = require("../fillsheet/serialize");
6
+ /**
7
+ * The whole document, in the formats a document reads.
8
+ *
9
+ * The same trick as the sheet's serialisers: plain functions over plain data,
10
+ * no React and no DOM, so a server route can render an export. A table block
11
+ * hands straight off to the sheet's own writer, so there is one implementation
12
+ * of table escaping rather than two that drift.
13
+ */
14
+ const escapeHtml = (s) => s
15
+ .replace(/&/g, "&amp;")
16
+ .replace(/</g, "&lt;")
17
+ .replace(/>/g, "&gt;")
18
+ .replace(/"/g, "&quot;");
19
+ /** An HTML document body: what Word, Google Docs and Pages paste as a document. */
20
+ function docToHTML(blocks) {
21
+ return blocks
22
+ .map((b) => {
23
+ if (b.type === "table")
24
+ return (0, serialize_1.toHTML)(b.value, { columns: b.columns });
25
+ const text = escapeHtml(b.text).replace(/\r?\n/g, "<br>");
26
+ return b.type === "heading" ? `<h2>${text}</h2>` : `<p>${text}</p>`;
27
+ })
28
+ .join("\n");
29
+ }
30
+ /** The same document as Markdown. */
31
+ function docToMarkdown(blocks) {
32
+ return blocks
33
+ .map((b) => {
34
+ if (b.type === "table")
35
+ return (0, serialize_1.toMarkdown)(b.value, { columns: b.columns });
36
+ return b.type === "heading" ? `## ${b.text}` : b.text;
37
+ })
38
+ .join("\n\n");
39
+ }