@crewlethq/ui 0.17.1 → 0.19.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 (226) hide show
  1. package/README.md +55 -3
  2. package/dist/AccessDeniedCard/AccessDeniedCard.d.ts +39 -0
  3. package/dist/AccessDeniedCard/index.d.ts +3 -0
  4. package/dist/AccessDeniedCard/index.js +8 -0
  5. package/dist/AccessDeniedCard-WXM5DBDZ.css +8 -0
  6. package/dist/AppShell/index.js +2 -2
  7. package/dist/ArrangeBoard/ArrangeBoard.d.ts +32 -0
  8. package/dist/ArrangeBoard/index.d.ts +3 -0
  9. package/dist/ArrangeBoard/index.js +2 -0
  10. package/dist/ArrangeBoard-MQE35P6Z.css +80 -0
  11. package/dist/ArrangeFrame/ArrangeFrame.d.ts +82 -0
  12. package/dist/ArrangeFrame/index.d.ts +5 -0
  13. package/dist/ArrangeFrame/index.js +5 -0
  14. package/dist/ArrangeFrame/useGridResize.d.ts +66 -0
  15. package/dist/ArrangeFrame-3PLZAG22.css +329 -0
  16. package/dist/ArrangeGroup/ArrangeGroup.d.ts +42 -0
  17. package/dist/ArrangeGroup/index.d.ts +3 -0
  18. package/dist/ArrangeGroup/index.js +4 -0
  19. package/dist/ArrangeGroup-SHFHA7BE.css +170 -0
  20. package/dist/ArrangeList/ArrangeList.d.ts +64 -0
  21. package/dist/ArrangeList/index.d.ts +3 -0
  22. package/dist/ArrangeList/index.js +10 -0
  23. package/dist/ArrangeList-NNRTDFI5.css +94 -0
  24. package/dist/ArtifactCard/ArtifactCard.d.ts +61 -0
  25. package/dist/ArtifactCard/index.d.ts +3 -0
  26. package/dist/ArtifactCard/index.js +12 -0
  27. package/dist/ArtifactCard-EXVAEBV6.css +109 -0
  28. package/dist/AuthLayout/index.js +2 -2
  29. package/dist/AvatarPicker/index.js +5 -5
  30. package/dist/Canvas/index.js +2 -2
  31. package/dist/Card/index.js +1 -1
  32. package/dist/ChangeMark/ChangeMark.d.ts +26 -0
  33. package/dist/ChangeMark/index.d.ts +3 -0
  34. package/dist/ChangeMark/index.js +8 -0
  35. package/dist/ChangeMark-3VMTW7MT.css +22 -0
  36. package/dist/Checkbox/index.js +3 -3
  37. package/dist/Combobox/index.js +3 -3
  38. package/dist/CommandPalette/index.js +5 -5
  39. package/dist/Composer/index.js +3 -3
  40. package/dist/ConfirmField/index.js +3 -3
  41. package/dist/CursorFlag/CursorFlag.d.ts +34 -0
  42. package/dist/CursorFlag/index.d.ts +3 -0
  43. package/dist/CursorFlag/index.js +3 -0
  44. package/dist/CursorFlag-AJQJBUGY.css +61 -0
  45. package/dist/DataTable/index.js +7 -7
  46. package/dist/DataView/index.js +13 -13
  47. package/dist/DateTimePicker/index.js +2 -2
  48. package/dist/Divider/Divider.d.ts +15 -0
  49. package/dist/Divider/index.d.ts +3 -0
  50. package/dist/Divider/index.js +2 -0
  51. package/dist/Divider-3Z77ZAJH.css +37 -0
  52. package/dist/FollowBanner/FollowBanner.d.ts +35 -0
  53. package/dist/FollowBanner/index.d.ts +2 -0
  54. package/dist/FollowBanner/index.js +7 -0
  55. package/dist/FrameProtocol/index.d.ts +2 -0
  56. package/dist/FrameProtocol/index.js +2 -0
  57. package/dist/FrameProtocol/protocol.d.ts +164 -0
  58. package/dist/HuePicker/HuePicker.d.ts +16 -4
  59. package/dist/HuePicker/index.js +3 -3
  60. package/dist/{HuePicker-5QIUR6L7.css → HuePicker-PAIGH6LR.css} +6 -0
  61. package/dist/IconTile/IconTile.d.ts +23 -0
  62. package/dist/IconTile/index.d.ts +3 -0
  63. package/dist/IconTile/index.js +2 -0
  64. package/dist/IconTile-4GNWTLAW.css +30 -0
  65. package/dist/Input/index.js +2 -2
  66. package/dist/LinkInterstitial/LinkInterstitial.d.ts +41 -0
  67. package/dist/LinkInterstitial/index.d.ts +3 -0
  68. package/dist/LinkInterstitial/index.js +11 -0
  69. package/dist/LinkInterstitial-ET4MSVSV.css +34 -0
  70. package/dist/ListInput/index.js +4 -4
  71. package/dist/LiveCursor/LiveCursor.d.ts +34 -0
  72. package/dist/LiveCursor/index.d.ts +3 -0
  73. package/dist/LiveCursor/index.js +4 -0
  74. package/dist/LiveCursor-DZHGIYWE.css +47 -0
  75. package/dist/MarkdownEditor/index.js +3 -3
  76. package/dist/Menu/index.js +2 -2
  77. package/dist/Modal/Modal.d.ts +20 -0
  78. package/dist/Modal/index.js +2 -2
  79. package/dist/{Modal-RQAYGG4L.css → Modal-XOSV6FAH.css} +14 -0
  80. package/dist/Note/Note.d.ts +23 -0
  81. package/dist/Note/index.d.ts +3 -0
  82. package/dist/Note/index.js +2 -0
  83. package/dist/Note-RT4L6VUT.css +58 -0
  84. package/dist/OrgTable/index.js +5 -5
  85. package/dist/PinTile/PinTile.d.ts +37 -0
  86. package/dist/PinTile/index.d.ts +3 -0
  87. package/dist/PinTile/index.js +4 -0
  88. package/dist/PinTile-IIA25MNZ.css +141 -0
  89. package/dist/PresenceModel/index.d.ts +2 -0
  90. package/dist/PresenceModel/index.js +3 -0
  91. package/dist/PresenceModel/presence.d.ts +80 -0
  92. package/dist/PresenceRing/PresenceRing.d.ts +31 -0
  93. package/dist/PresenceRing/index.d.ts +3 -0
  94. package/dist/PresenceRing/index.js +2 -0
  95. package/dist/PresenceRing-M4C22Q6U.css +68 -0
  96. package/dist/PresenceStack/PresenceStack.d.ts +36 -0
  97. package/dist/PresenceStack/index.d.ts +3 -0
  98. package/dist/PresenceStack/index.js +9 -0
  99. package/dist/PresenceStack-QSXQJND5.css +59 -0
  100. package/dist/PricingCard/index.js +2 -2
  101. package/dist/Principal/Principal.d.ts +35 -0
  102. package/dist/Principal/index.d.ts +2 -0
  103. package/dist/Principal/index.js +4 -0
  104. package/dist/PrincipalPicker/PrincipalPicker.d.ts +45 -0
  105. package/dist/PrincipalPicker/index.d.ts +3 -0
  106. package/dist/PrincipalPicker/index.js +12 -0
  107. package/dist/PrincipalPicker-7NNZTGUP.css +41 -0
  108. package/dist/Reorder/index.d.ts +2 -0
  109. package/dist/Reorder/index.js +4 -0
  110. package/dist/Reorder/useReorder.d.ts +138 -0
  111. package/dist/SandboxedFrame/SandboxedFrame.d.ts +142 -0
  112. package/dist/SandboxedFrame/index.d.ts +3 -0
  113. package/dist/SandboxedFrame/index.js +8 -0
  114. package/dist/SandboxedFrame-L6HBQZCD.css +50 -0
  115. package/dist/ShareDialog/ShareDialog.d.ts +101 -0
  116. package/dist/ShareDialog/index.d.ts +5 -0
  117. package/dist/ShareDialog/index.js +19 -0
  118. package/dist/ShareDialog/roles.d.ts +10 -0
  119. package/dist/ShareDialog-YZ4F37MW.css +146 -0
  120. package/dist/SheetGrid/SheetGrid.d.ts +78 -0
  121. package/dist/SheetGrid/cells.d.ts +117 -0
  122. package/dist/SheetGrid/index.d.ts +5 -0
  123. package/dist/SheetGrid/index.js +18 -0
  124. package/dist/SheetGrid-4HOXDVIB.css +595 -0
  125. package/dist/SheetModel/clipboard.d.ts +37 -0
  126. package/dist/SheetModel/edits.d.ts +68 -0
  127. package/dist/SheetModel/index.d.ts +14 -0
  128. package/dist/SheetModel/index.js +3 -0
  129. package/dist/SheetModel/labels.d.ts +71 -0
  130. package/dist/SheetModel/navigation.d.ts +79 -0
  131. package/dist/SheetModel/options.d.ts +28 -0
  132. package/dist/SheetModel/types.d.ts +126 -0
  133. package/dist/SheetModel/values.d.ts +76 -0
  134. package/dist/SheetModel/view.d.ts +32 -0
  135. package/dist/StatGroup/StatGroup.d.ts +7 -1
  136. package/dist/StatGroup/index.js +1 -1
  137. package/dist/{StatGroup-HVG6O62W.css → StatGroup-PADTJCUQ.css} +7 -0
  138. package/dist/Switcher/index.js +8 -8
  139. package/dist/Tag/Tag.d.ts +13 -0
  140. package/dist/Tag/index.js +1 -1
  141. package/dist/{Tag-4BRQS6GN.css → Tag-SRGTK5KI.css} +42 -0
  142. package/dist/TagsInput/index.js +4 -4
  143. package/dist/ThemeSwitcher/apply.d.ts +43 -0
  144. package/dist/ThemeSwitcher/index.d.ts +4 -2
  145. package/dist/ThemeSwitcher/index.js +3 -2
  146. package/dist/ThemeSwitcher/preferences.d.ts +2 -19
  147. package/dist/ThemeToggle/index.js +3 -2
  148. package/dist/TimeWindowPicker/index.js +4 -4
  149. package/dist/Timeline/Timeline.d.ts +7 -2
  150. package/dist/Timeline/index.js +1 -1
  151. package/dist/TreeCanvas/index.js +3 -3
  152. package/dist/TreeGrid/index.js +2 -2
  153. package/dist/VersionTimeline/VersionTimeline.d.ts +76 -0
  154. package/dist/VersionTimeline/index.d.ts +3 -0
  155. package/dist/VersionTimeline/index.js +10 -0
  156. package/dist/VersionTimeline-EBJ6MIPJ.css +108 -0
  157. package/dist/{chunk-SBRE7IML.js → chunk-2ONWEDQT.js} +1 -1
  158. package/dist/chunk-35XVR5JQ.js +81 -0
  159. package/dist/chunk-3BYIWOTV.js +29 -0
  160. package/dist/chunk-4KP4RC3O.js +25 -0
  161. package/dist/chunk-5XACKNYR.js +53 -0
  162. package/dist/chunk-63WJLRB2.js +24 -0
  163. package/dist/{chunk-L4ZLXYPO.js → chunk-6A7P6II6.js} +1 -1
  164. package/dist/chunk-6KWNXIDE.js +18 -0
  165. package/dist/chunk-6MIXD6IW.js +49 -0
  166. package/dist/{chunk-FA2V2VT6.js → chunk-7FQEUNHX.js} +4 -4
  167. package/dist/{chunk-DEEVA6TV.js → chunk-7IJARUIF.js} +17 -4
  168. package/dist/{chunk-JCO3SIST.js → chunk-7ZNKEPLN.js} +2 -2
  169. package/dist/chunk-AIVZVKYZ.js +126 -0
  170. package/dist/{chunk-FSKTOIYC.js → chunk-AJESMWJS.js} +5 -5
  171. package/dist/{chunk-O67VF5MS.js → chunk-AUQPBL4O.js} +2 -2
  172. package/dist/chunk-CENOOTAV.js +1 -0
  173. package/dist/chunk-DEJHAOC5.js +279 -0
  174. package/dist/{chunk-UCYIRDND.js → chunk-DQSXWQMJ.js} +1 -1
  175. package/dist/{chunk-JF7X673Z.js → chunk-EIGEIT4O.js} +2 -2
  176. package/dist/chunk-F6V2PTN6.js +142 -0
  177. package/dist/chunk-FFMCMSJB.js +1 -0
  178. package/dist/chunk-FW7HN6AP.js +40 -0
  179. package/dist/chunk-G3EEUNZQ.js +46 -0
  180. package/dist/{chunk-LOECRMHA.js → chunk-GFV4LJHH.js} +4 -3
  181. package/dist/chunk-HTCJSUIZ.js +216 -0
  182. package/dist/{chunk-75CLJNDQ.js → chunk-I6NZKJGK.js} +1 -1
  183. package/dist/chunk-I6POAB4V.js +20 -0
  184. package/dist/chunk-IXJHTFXG.js +17 -0
  185. package/dist/chunk-JEDWC3GI.js +98 -0
  186. package/dist/{chunk-QVAFL2LE.js → chunk-JXRI5YD2.js} +3 -3
  187. package/dist/{chunk-ZFH7H7GS.js → chunk-LOLELDBW.js} +1 -1
  188. package/dist/{chunk-6TSZCBID.js → chunk-NP2C4Y4E.js} +1 -1
  189. package/dist/{chunk-6NN2F7GM.js → chunk-OIMAK5AE.js} +6 -6
  190. package/dist/chunk-PPAQFGS2.js +12 -0
  191. package/dist/chunk-PUQY4WXR.js +1235 -0
  192. package/dist/chunk-QO7QEQKV.js +24 -0
  193. package/dist/chunk-RT323I4I.js +49 -0
  194. package/dist/chunk-S35G6PNC.js +80 -0
  195. package/dist/{chunk-HENOTGUV.js → chunk-T7ICFXEC.js} +2 -2
  196. package/dist/{chunk-2OLCUNG7.js → chunk-TBIZSQHB.js} +1 -1
  197. package/dist/chunk-TODND5AD.js +99 -0
  198. package/dist/{chunk-PQ63NLFC.js → chunk-UXELEORU.js} +3 -3
  199. package/dist/{chunk-6NIMZ5XC.js → chunk-VJLYGPPE.js} +1 -1
  200. package/dist/chunk-VYDFB6PJ.js +243 -0
  201. package/dist/{chunk-7QFAORCY.js → chunk-WHZFDS62.js} +1 -1
  202. package/dist/chunk-WXAULPBH.js +135 -0
  203. package/dist/chunk-XJ2QEBMM.js +277 -0
  204. package/dist/{chunk-NXDEPUVG.js → chunk-XU4ZUGLE.js} +20 -5
  205. package/dist/chunk-YM3GJHGW.js +69 -0
  206. package/dist/{chunk-T3ZWBZ65.js → chunk-YSJYWQSP.js} +2 -2
  207. package/dist/{chunk-CXBAIVYZ.js → chunk-YSY6UJ4F.js} +1 -1
  208. package/dist/chunk-YYZRLLLY.js +822 -0
  209. package/dist/{chunk-EG6NYWVK.js → chunk-YZCJ63JF.js} +1 -1
  210. package/dist/{chunk-K3OWD7BJ.js → chunk-Z24ZUSTX.js} +4 -19
  211. package/dist/chunk-ZXXDZJQN.js +36 -0
  212. package/dist/generated/index.d.ts +27 -0
  213. package/dist/generated/index.js +98 -69
  214. package/dist/index.js +98 -69
  215. package/dist/styles.css +13625 -11206
  216. package/dist/utils/index.js +1 -1
  217. package/package.json +3 -3
  218. package/dist/chunk-EXHQ2QVW.js +0 -1
  219. package/dist/{chunk-CRVETI5L.js → chunk-4Z7PW3L6.js} +0 -0
  220. package/dist/{chunk-OYOQU7O5.js → chunk-EDGCPVVD.js} +1 -1
  221. package/dist/{chunk-NS7BVLL3.js → chunk-JULMZNGT.js} +1 -1
  222. package/dist/{chunk-2UU5IGZI.js → chunk-L4H7SSWF.js} +1 -1
  223. package/dist/{chunk-4GB57WIT.js → chunk-Q55TXPA3.js} +1 -1
  224. package/dist/{chunk-77IEHFY4.js → chunk-QQGWES46.js} +1 -1
  225. package/dist/{chunk-B2Z2CDM5.js → chunk-T5TFZ2JB.js} +1 -1
  226. package/dist/{chunk-AMH67MLM.js → chunk-TXAFKH2A.js} +1 -1
@@ -0,0 +1,822 @@
1
+ import { PRESENCE_LABELS } from './chunk-S35G6PNC.js';
2
+ import { isNodeHue } from './chunk-AYKXMIOR.js';
3
+
4
+ // src/SheetModel/types.ts
5
+ var SHEET_COLUMN_TYPES = [
6
+ "text",
7
+ "longtext",
8
+ "number",
9
+ "checkbox",
10
+ "select",
11
+ "multiselect",
12
+ "status",
13
+ "person",
14
+ "date",
15
+ "url",
16
+ "rating"
17
+ ];
18
+ var isSheetColumnType = (value) => typeof value === "string" && SHEET_COLUMN_TYPES.includes(value);
19
+
20
+ // src/SheetModel/labels.ts
21
+ var TYPE_NAMES = {
22
+ text: "Text",
23
+ longtext: "Long text",
24
+ number: "Number",
25
+ checkbox: "Checkbox",
26
+ select: "Select",
27
+ multiselect: "Multi-select",
28
+ status: "Status",
29
+ person: "Person",
30
+ date: "Date",
31
+ url: "URL",
32
+ rating: "Rating"
33
+ };
34
+ var changes = (count) => count === 1 ? "1 change" : `${count} changes`;
35
+ var SHEET_LABELS = {
36
+ rowHeader: "Row",
37
+ addRow: "Add row",
38
+ empty: "Empty",
39
+ checked: "Checked",
40
+ unchecked: "Not checked",
41
+ rating: (value, max) => `${value} of ${max} stars`,
42
+ archived: (label) => `Archived: ${label}`,
43
+ unknownPerson: "Unknown person",
44
+ types: TYPE_NAMES,
45
+ saving: "Saving",
46
+ forbidden: "You can view this sheet but not edit it.",
47
+ couldNotSave: "Could not save this change.",
48
+ notANumber: (column) => `${column} must be a number.`,
49
+ belowMin: (column, min) => `${column} must be at least ${min}.`,
50
+ aboveMax: (column, max) => `${column} must be at most ${max}.`,
51
+ tooLong: (column, max) => `${column} must be at most ${max} characters.`,
52
+ required: (column) => `${column} is required.`,
53
+ notAnOption: (column) => `${column} must be one of its options.`,
54
+ tooManyItems: (column, max) => `${column} holds at most ${max} options.`,
55
+ notAPerson: (column) => `${column} must be a person or an agent in this company.`,
56
+ notADate: (column) => `${column} must be a date.`,
57
+ notAUrl: (column) => `${column} must be a web address starting with https:// or http://.`,
58
+ notARating: (column, max) => `${column} must be a whole number from 1 to ${max}.`,
59
+ notABoolean: (column) => `${column} must be checked or not checked.`,
60
+ readOnly: (column) => `${column} is read only.`,
61
+ conflict: (writer, value) => `${writer} changed this to ${value} while you were editing.`,
62
+ keepMine: "Keep yours",
63
+ useTheirs: (writer) => `Use ${writer}'s`,
64
+ someone: "Someone",
65
+ changed: (by, at, previous) => `Changed by ${by}, ${at}.${previous === null ? "" : ` Was ${previous}.`}`,
66
+ history: "Cell history",
67
+ sync: (state) => {
68
+ if (state.offline) {
69
+ return state.saving === 0 ? "Offline." : `Offline. ${changes(state.saving)} will sync when you reconnect.`;
70
+ }
71
+ if (state.failed !== void 0 && state.failed > 0) return "Could not save.";
72
+ if (state.saving > 0) return `Saving ${changes(state.saving)}`;
73
+ return "Saved";
74
+ },
75
+ retry: "Retry",
76
+ filter: "Filter options",
77
+ noOptions: "No matching options",
78
+ clear: "Clear",
79
+ keyboardHelp: "Arrow keys move between cells. Enter or F2 edits a cell, Escape cancels, and typing replaces its value. Space toggles a checkbox. Control or Command with C and V copies and pastes. Control or Command with Alt and H opens the history of a cell.",
80
+ sortBy: (column) => `Sort by ${column}`
81
+ };
82
+ function sheetLabels(overrides) {
83
+ return { ...SHEET_LABELS, ...overrides, types: { ...SHEET_LABELS.types, ...overrides?.types } };
84
+ }
85
+
86
+ // src/SheetModel/options.ts
87
+ var HUE_ALIASES = {
88
+ violet: "purple",
89
+ indigo: "purple",
90
+ teal: "cyan",
91
+ sky: "cyan",
92
+ emerald: "green",
93
+ lime: "green",
94
+ yellow: "amber",
95
+ orange: "amber",
96
+ red: "rose",
97
+ pink: "rose"
98
+ };
99
+ function optionHue(color) {
100
+ if (color === void 0) return "neutral";
101
+ const name = color.trim().toLowerCase();
102
+ if (isNodeHue(name)) return name;
103
+ return HUE_ALIASES[name] ?? "neutral";
104
+ }
105
+ var TONES = ["neutral", "info", "success", "warning", "danger"];
106
+ var TONE_OF_HUE = {
107
+ green: "success",
108
+ amber: "warning",
109
+ rose: "danger",
110
+ blue: "info",
111
+ cyan: "info",
112
+ purple: "neutral"
113
+ };
114
+ var TONE_OF_CATEGORY = {
115
+ todo: "neutral",
116
+ in_progress: "info",
117
+ done: "success",
118
+ blocked: "danger"
119
+ };
120
+ function statusTone(option) {
121
+ const name = option.color?.trim().toLowerCase();
122
+ if (name !== void 0 && TONES.includes(name)) return name;
123
+ if (name !== void 0) {
124
+ const hue = optionHue(name);
125
+ if (hue !== "neutral") return TONE_OF_HUE[hue];
126
+ }
127
+ return option.category ? TONE_OF_CATEGORY[option.category] : "neutral";
128
+ }
129
+ function findOption(column, id) {
130
+ return column.options?.find((option) => option.id === id);
131
+ }
132
+ function liveOptions(column) {
133
+ return (column.options ?? []).filter((option) => !option.archived);
134
+ }
135
+ function parseOptionList(text) {
136
+ const seen = /* @__PURE__ */ new Set();
137
+ const out = [];
138
+ for (const part of text.split(",")) {
139
+ const [rawLabel = "", rawColor] = part.split(":");
140
+ const label = rawLabel.trim();
141
+ if (label === "") continue;
142
+ let id = label.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
143
+ if (id === "") id = `option_${out.length + 1}`;
144
+ while (seen.has(id)) id = `${id}_`;
145
+ seen.add(id);
146
+ const color = rawColor?.trim();
147
+ out.push(color ? { id, label, color } : { id, label });
148
+ }
149
+ return out;
150
+ }
151
+
152
+ // src/SheetModel/values.ts
153
+ var TEXT_MAX_DEFAULT = 1e3;
154
+ var LONGTEXT_MAX_DEFAULT = 2e4;
155
+ var URL_MAX = 2048;
156
+ var RATING_MAX_DEFAULT = 5;
157
+ var RATING_MAX_CEILING = 10;
158
+ var YEAR_MIN = 1900;
159
+ var YEAR_MAX = 2200;
160
+ function ratingMax(column) {
161
+ const max = column.max ?? RATING_MAX_DEFAULT;
162
+ return Number.isInteger(max) && max >= 1 ? Math.min(max, RATING_MAX_CEILING) : RATING_MAX_DEFAULT;
163
+ }
164
+ function textMax(column) {
165
+ return column.maxLength ?? (column.type === "longtext" ? LONGTEXT_MAX_DEFAULT : TEXT_MAX_DEFAULT);
166
+ }
167
+ var isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
168
+ function normalizeValue(column, raw) {
169
+ if (raw === void 0 || raw === null) return column.type === "checkbox" ? false : column.type === "multiselect" ? [] : null;
170
+ switch (column.type) {
171
+ case "checkbox":
172
+ return raw === true;
173
+ case "number": {
174
+ const value = typeof raw === "number" ? raw : typeof raw === "string" && raw.trim() !== "" ? Number(raw) : Number.NaN;
175
+ return Number.isFinite(value) ? value : null;
176
+ }
177
+ case "rating":
178
+ return typeof raw === "number" && Number.isInteger(raw) && raw >= 1 ? raw : null;
179
+ case "multiselect":
180
+ return isStringArray(raw) ? raw : [];
181
+ case "text":
182
+ case "longtext":
183
+ case "url":
184
+ case "date":
185
+ return typeof raw === "string" ? raw : typeof raw === "number" || typeof raw === "boolean" ? String(raw) : null;
186
+ case "select":
187
+ case "status":
188
+ case "person":
189
+ return typeof raw === "string" ? raw : null;
190
+ }
191
+ }
192
+ function isEmptyValue(value) {
193
+ return value === null || value === "" || Array.isArray(value) && value.length === 0;
194
+ }
195
+ function sameValue(a, b) {
196
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => item === b[index]);
197
+ return a === b;
198
+ }
199
+ function findPerson(people, id) {
200
+ return people?.find((person) => person.id === id);
201
+ }
202
+ function numberFormat(column, locale) {
203
+ const digits = column.precision;
204
+ const fraction = digits === void 0 ? { maximumFractionDigits: 10 } : { minimumFractionDigits: digits, maximumFractionDigits: digits };
205
+ if (column.numberStyle === "percent") return new Intl.NumberFormat(locale, { style: "percent", ...fraction });
206
+ if (column.numberStyle === "currency" && column.currency) {
207
+ return new Intl.NumberFormat(locale, { style: "currency", currency: column.currency, ...fraction });
208
+ }
209
+ return new Intl.NumberFormat(locale, fraction);
210
+ }
211
+ var DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
212
+ function calendarDate(text) {
213
+ const match = DATE_ONLY.exec(text);
214
+ if (!match) return null;
215
+ const year = Number(match[1]);
216
+ const month = Number(match[2]);
217
+ const day = Number(match[3]);
218
+ const at = new Date(Date.UTC(year, month - 1, day));
219
+ if (at.getUTCFullYear() !== year || at.getUTCMonth() !== month - 1 || at.getUTCDate() !== day) return null;
220
+ if (year < YEAR_MIN || year > YEAR_MAX) return null;
221
+ return { year, month, day };
222
+ }
223
+ function displayText(column, value, context) {
224
+ const { labels, locale } = context;
225
+ if (column.type === "checkbox") return value === true ? labels.checked : labels.unchecked;
226
+ if (isEmptyValue(value)) return "";
227
+ switch (column.type) {
228
+ case "number":
229
+ return typeof value === "number" ? numberFormat(column, locale).format(value) : "";
230
+ case "rating":
231
+ return typeof value === "number" ? labels.rating(value, ratingMax(column)) : "";
232
+ case "select":
233
+ case "status": {
234
+ const option = findOption(column, String(value));
235
+ if (!option) return String(value);
236
+ return option.archived ? labels.archived(option.label) : option.label;
237
+ }
238
+ case "multiselect":
239
+ return value.map((id) => {
240
+ const option = findOption(column, id);
241
+ if (!option) return id;
242
+ return option.archived ? labels.archived(option.label) : option.label;
243
+ }).join(", ");
244
+ case "person":
245
+ return findPerson(context.people, String(value))?.name ?? labels.unknownPerson;
246
+ case "date": {
247
+ const text = String(value);
248
+ if (calendarDate(text)) {
249
+ return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone: "UTC" }).format(/* @__PURE__ */ new Date(`${text}T00:00:00Z`));
250
+ }
251
+ const at = Date.parse(text);
252
+ return Number.isNaN(at) ? text : new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(at);
253
+ }
254
+ default:
255
+ return String(value);
256
+ }
257
+ }
258
+ function spokenText(column, value, context) {
259
+ if (column.type !== "checkbox" && isEmptyValue(value)) return context.labels.empty;
260
+ return displayText(column, value, context);
261
+ }
262
+ function editText(column, value, context) {
263
+ if (column.type === "checkbox") return value === true ? "TRUE" : "FALSE";
264
+ if (isEmptyValue(value)) return "";
265
+ switch (column.type) {
266
+ case "number":
267
+ if (typeof value !== "number") return "";
268
+ return column.numberStyle === "percent" ? `${roundTo(value * 100, 10)}%` : String(value);
269
+ case "select":
270
+ case "status":
271
+ return findOption(column, String(value))?.label ?? String(value);
272
+ case "multiselect":
273
+ return value.map((id) => findOption(column, id)?.label ?? id).join(", ");
274
+ case "person":
275
+ return findPerson(context.people, String(value))?.name ?? String(value);
276
+ default:
277
+ return String(value);
278
+ }
279
+ }
280
+ function roundTo(value, digits) {
281
+ const factor = 10 ** digits;
282
+ return Math.round(value * factor) / factor;
283
+ }
284
+ var refuse = (error) => ({ ok: false, error });
285
+ function matchOption(column, text) {
286
+ const wanted = text.trim().toLowerCase();
287
+ const options = liveOptions(column);
288
+ const hit = options.find((option) => option.id === text.trim()) ?? options.find((option) => option.label.toLowerCase() === wanted);
289
+ return hit ? hit.id : null;
290
+ }
291
+ function matchPerson(people, text) {
292
+ const wanted = text.trim().toLowerCase();
293
+ if (!people || wanted === "") return null;
294
+ const exact = people.find((person) => person.id === text.trim() || person.name.toLowerCase() === wanted);
295
+ if (exact) return exact.id;
296
+ const prefixed = people.filter((person) => person.name.toLowerCase().startsWith(wanted));
297
+ return prefixed.length === 1 ? prefixed[0].id : null;
298
+ }
299
+ var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "yes", "y", "1", "x", "checked", "on", "\u2713", "\u2714"]);
300
+ var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "no", "n", "0", "", "unchecked", "off"]);
301
+ function parseNumber(column, text, labels) {
302
+ let source = text.trim().replace(/\s+/g, "");
303
+ const percent = source.endsWith("%");
304
+ if (percent) source = source.slice(0, -1);
305
+ source = source.replace(/^[$€£¥]|[$€£¥]$/g, "");
306
+ if (/^-?\d{1,3}(,\d{3})+(\.\d+)?$/.test(source)) source = source.replaceAll(",", "");
307
+ if (!/^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i.test(source)) return refuse(labels.notANumber(column.label));
308
+ let value = Number(source);
309
+ if (percent || column.numberStyle === "percent") value /= 100;
310
+ if (!Number.isFinite(value)) return refuse(labels.notANumber(column.label));
311
+ if (column.precision !== void 0) value = roundTo(value, column.precision);
312
+ const show = (bound) => column.numberStyle === "percent" ? `${roundTo(bound * 100, 10)}%` : String(bound);
313
+ if (column.min !== void 0 && value < column.min) return refuse(labels.belowMin(column.label, show(column.min)));
314
+ if (column.max !== void 0 && value > column.max) return refuse(labels.aboveMax(column.label, show(column.max)));
315
+ return { ok: true, value };
316
+ }
317
+ function parseDate(column, text, labels) {
318
+ const source = text.trim();
319
+ if (!column.time) {
320
+ const day = calendarDate(source.slice(0, 10));
321
+ if (day && (source.length === 10 || source[10] === "T" || source[10] === " ")) return { ok: true, value: source.slice(0, 10) };
322
+ return refuse(labels.notADate(column.label));
323
+ }
324
+ const at = Date.parse(source);
325
+ if (Number.isNaN(at)) return refuse(labels.notADate(column.label));
326
+ const year = new Date(at).getUTCFullYear();
327
+ if (year < YEAR_MIN || year > YEAR_MAX) return refuse(labels.notADate(column.label));
328
+ return { ok: true, value: new Date(at).toISOString().replace(/\.000Z$/, "Z") };
329
+ }
330
+ function parseUrl(column, text, labels) {
331
+ let source = text.trim();
332
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(source) && /^[^\s/]+\.[^\s/]+/.test(source)) source = `https://${source}`;
333
+ let url;
334
+ try {
335
+ url = new URL(source);
336
+ } catch {
337
+ return refuse(labels.notAUrl(column.label));
338
+ }
339
+ if (url.protocol !== "https:" && url.protocol !== "http:" || source.length > URL_MAX || url.host === "") {
340
+ return refuse(labels.notAUrl(column.label));
341
+ }
342
+ return { ok: true, value: source };
343
+ }
344
+ function parseText(column, text, context) {
345
+ const { labels } = context;
346
+ const blank = text.trim() === "";
347
+ if (blank && column.type !== "checkbox") {
348
+ if (column.required) return refuse(labels.required(column.label));
349
+ return { ok: true, value: column.type === "multiselect" ? [] : null };
350
+ }
351
+ switch (column.type) {
352
+ case "checkbox": {
353
+ const word = text.trim().toLowerCase();
354
+ if (TRUE_WORDS.has(word)) return { ok: true, value: true };
355
+ if (FALSE_WORDS.has(word)) return { ok: true, value: false };
356
+ return refuse(labels.notABoolean(column.label));
357
+ }
358
+ case "number":
359
+ return parseNumber(column, text, labels);
360
+ case "rating": {
361
+ const max = ratingMax(column);
362
+ const source = text.trim();
363
+ const stars = /^★+$/.test(source) ? source.length : Number(source);
364
+ if (stars === 0) return column.required ? refuse(labels.required(column.label)) : { ok: true, value: null };
365
+ if (!Number.isInteger(stars) || stars < 1 || stars > max) return refuse(labels.notARating(column.label, max));
366
+ return { ok: true, value: stars };
367
+ }
368
+ case "text": {
369
+ const value = text.replace(/\s*[\r\n]+\s*/g, " ");
370
+ if (value.length > textMax(column)) return refuse(labels.tooLong(column.label, textMax(column)));
371
+ return { ok: true, value };
372
+ }
373
+ case "longtext":
374
+ if (text.length > textMax(column)) return refuse(labels.tooLong(column.label, textMax(column)));
375
+ return { ok: true, value: text };
376
+ case "select":
377
+ case "status": {
378
+ const id = matchOption(column, text);
379
+ return id === null ? refuse(labels.notAnOption(column.label)) : { ok: true, value: id };
380
+ }
381
+ case "multiselect": {
382
+ const ids = [];
383
+ for (const part of text.split(/[,;\n]/)) {
384
+ if (part.trim() === "") continue;
385
+ const id = matchOption(column, part);
386
+ if (id === null) return refuse(labels.notAnOption(column.label));
387
+ if (!ids.includes(id)) ids.push(id);
388
+ }
389
+ if (column.maxItems !== void 0 && ids.length > column.maxItems) return refuse(labels.tooManyItems(column.label, column.maxItems));
390
+ return { ok: true, value: ids };
391
+ }
392
+ case "person": {
393
+ const id = matchPerson(context.people, text);
394
+ return id === null ? refuse(labels.notAPerson(column.label)) : { ok: true, value: id };
395
+ }
396
+ case "date":
397
+ return parseDate(column, text, labels);
398
+ case "url":
399
+ return parseUrl(column, text, labels);
400
+ }
401
+ }
402
+ function checkValue(column, value, context) {
403
+ const { labels } = context;
404
+ if (column.required && column.type !== "checkbox" && isEmptyValue(value)) return labels.required(column.label);
405
+ if (isEmptyValue(value)) return null;
406
+ switch (column.type) {
407
+ case "checkbox":
408
+ return typeof value === "boolean" ? null : labels.notABoolean(column.label);
409
+ case "select":
410
+ case "status":
411
+ return typeof value === "string" && liveOptions(column).some((option) => option.id === value) ? null : labels.notAnOption(column.label);
412
+ case "multiselect": {
413
+ if (!Array.isArray(value)) return labels.notAnOption(column.label);
414
+ const live = new Set(liveOptions(column).map((option) => option.id));
415
+ if (!value.every((id) => live.has(id) || findOption(column, id)?.archived)) return labels.notAnOption(column.label);
416
+ if (column.maxItems !== void 0 && value.length > column.maxItems) return labels.tooManyItems(column.label, column.maxItems);
417
+ return null;
418
+ }
419
+ case "person":
420
+ return typeof value === "string" && findPerson(context.people, value) ? null : labels.notAPerson(column.label);
421
+ case "rating":
422
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= ratingMax(column) ? null : labels.notARating(column.label, ratingMax(column));
423
+ default: {
424
+ const parsed = parseText(column, editText(column, value, context), context);
425
+ return parsed.ok ? null : parsed.error;
426
+ }
427
+ }
428
+ }
429
+ function compareValues(column, a, b, context) {
430
+ const emptyA = column.type !== "checkbox" && isEmptyValue(a);
431
+ const emptyB = column.type !== "checkbox" && isEmptyValue(b);
432
+ if (emptyA || emptyB) return emptyA === emptyB ? 0 : emptyA ? 1 : -1;
433
+ switch (column.type) {
434
+ case "number":
435
+ case "rating":
436
+ return a - b;
437
+ case "checkbox":
438
+ return Number(a === true) - Number(b === true);
439
+ case "select":
440
+ case "status": {
441
+ const order = (id) => (column.options ?? []).findIndex((option) => option.id === id);
442
+ return order(a) - order(b);
443
+ }
444
+ default:
445
+ return displayText(column, a, context).localeCompare(displayText(column, b, context), context.locale, { numeric: true });
446
+ }
447
+ }
448
+
449
+ // src/SheetModel/navigation.ts
450
+ var isMod = (key2) => Boolean(key2.ctrlKey || key2.metaKey);
451
+ var clamp = (value, low, high) => Math.min(Math.max(value, low), high);
452
+ function moveFrom(at, key2, bounds) {
453
+ const lastRow = bounds.rows - 1;
454
+ const lastCol = bounds.cols - 1;
455
+ if (bounds.cols === 0) return null;
456
+ const mod = isMod(key2);
457
+ switch (key2.key) {
458
+ case "ArrowUp":
459
+ return { row: mod ? at.row < 0 ? -1 : 0 : Math.max(at.row - 1, -1), col: at.col };
460
+ case "ArrowDown":
461
+ if (lastRow < 0) return at;
462
+ return { row: mod ? lastRow : clamp(at.row + 1, -1, lastRow), col: at.col };
463
+ case "ArrowLeft":
464
+ return { row: at.row, col: mod ? 0 : Math.max(at.col - 1, 0) };
465
+ case "ArrowRight":
466
+ return { row: at.row, col: mod ? lastCol : Math.min(at.col + 1, lastCol) };
467
+ case "Home":
468
+ return mod ? { row: lastRow < 0 ? -1 : 0, col: 0 } : { row: at.row, col: 0 };
469
+ case "End":
470
+ return mod ? { row: Math.max(lastRow, -1), col: lastCol } : { row: at.row, col: lastCol };
471
+ case "PageUp":
472
+ return { row: at.row < 0 ? -1 : Math.max(at.row - bounds.page, 0), col: at.col };
473
+ case "PageDown":
474
+ if (lastRow < 0) return at;
475
+ return { row: clamp(at.row + bounds.page, 0, lastRow), col: at.col };
476
+ default:
477
+ return null;
478
+ }
479
+ }
480
+ function rangeOf(anchor, active) {
481
+ const a = Math.max(anchor.row, 0);
482
+ const b = Math.max(active.row, 0);
483
+ return { top: Math.min(a, b), bottom: Math.max(a, b), left: Math.min(anchor.col, active.col), right: Math.max(anchor.col, active.col) };
484
+ }
485
+ var inRange = (range, row, col) => row >= range.top && row <= range.bottom && col >= range.left && col <= range.right;
486
+ var isMultiCell = (range) => range.top !== range.bottom || range.left !== range.right;
487
+ function visibleRows(scrollTop, viewport, rowHeight, rows, overscan = 8) {
488
+ if (rows === 0) return [0, -1];
489
+ const height = viewport > 0 ? viewport : rowHeight * 40;
490
+ const first = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
491
+ const last = Math.min(rows - 1, Math.ceil((scrollTop + height) / rowHeight) + overscan);
492
+ return [first, last];
493
+ }
494
+ function scrollToRow(row, scrollTop, viewport, rowHeight, header) {
495
+ const top = row * rowHeight;
496
+ const bottom = top + rowHeight;
497
+ const visibleTop = scrollTop;
498
+ const visibleBottom = scrollTop + Math.max(viewport - header, rowHeight);
499
+ if (top < visibleTop) return top;
500
+ if (bottom > visibleBottom) return bottom - Math.max(viewport - header, rowHeight);
501
+ return scrollTop;
502
+ }
503
+ function editorSide(cellTop, cellBottom, viewTop, viewBottom, panelHeight) {
504
+ const below = viewBottom - cellBottom;
505
+ const above = cellTop - viewTop;
506
+ return below >= panelHeight || below >= above ? "below" : "above";
507
+ }
508
+ var SHEET_COLUMN_WIDTHS = {
509
+ text: 200,
510
+ longtext: 260,
511
+ number: 112,
512
+ checkbox: 104,
513
+ select: 144,
514
+ multiselect: 208,
515
+ status: 144,
516
+ person: 176,
517
+ date: 136,
518
+ url: 208,
519
+ rating: 128
520
+ };
521
+ var SHEET_ROW_NUMBER_WIDTH = 52;
522
+ var SHEET_ROW_HEIGHT = 32;
523
+ var columnWidth = (column) => column.width !== void 0 && column.width > 0 ? column.width : SHEET_COLUMN_WIDTHS[column.type];
524
+ function columnOffsets(columns) {
525
+ const offsets = [];
526
+ let at = SHEET_ROW_NUMBER_WIDTH;
527
+ for (const column of columns) {
528
+ offsets.push(at);
529
+ at += columnWidth(column);
530
+ }
531
+ return offsets;
532
+ }
533
+ function drawnColumns(columns) {
534
+ return [...columns.filter((column) => column.frozen), ...columns.filter((column) => !column.frozen)];
535
+ }
536
+ var isTypingKey = (key2) => key2.key.length === 1 && !isMod(key2) && !key2.altKey;
537
+
538
+ // src/SheetModel/clipboard.ts
539
+ function quote(field) {
540
+ return /[\t\n\r"]/.test(field) ? `"${field.replaceAll('"', '""')}"` : field;
541
+ }
542
+ function toTSV(columns, rows, range, context) {
543
+ const lines = [];
544
+ for (let row = range.top; row <= range.bottom; row += 1) {
545
+ const record = rows[row];
546
+ if (!record) continue;
547
+ const fields = [];
548
+ for (let col = range.left; col <= range.right; col += 1) {
549
+ const column = columns[col];
550
+ if (!column) continue;
551
+ fields.push(quote(editText(column, normalizeValue(column, record.values[column.field]), context)));
552
+ }
553
+ lines.push(fields.join(" "));
554
+ }
555
+ return lines.join("\n");
556
+ }
557
+ function parseTSV(text) {
558
+ const rows = [];
559
+ let row = [];
560
+ let field = "";
561
+ let quoted = false;
562
+ let at = 0;
563
+ const source = text.replace(/\r\n?/g, "\n");
564
+ while (at < source.length) {
565
+ const char = source[at];
566
+ if (quoted) {
567
+ if (char === '"' && source[at + 1] === '"') {
568
+ field += '"';
569
+ at += 2;
570
+ continue;
571
+ }
572
+ if (char === '"') {
573
+ quoted = false;
574
+ at += 1;
575
+ continue;
576
+ }
577
+ field += char;
578
+ at += 1;
579
+ continue;
580
+ }
581
+ if (char === '"' && field === "") {
582
+ quoted = true;
583
+ } else if (char === " ") {
584
+ row.push(field);
585
+ field = "";
586
+ } else if (char === "\n") {
587
+ row.push(field);
588
+ rows.push(row);
589
+ row = [];
590
+ field = "";
591
+ } else {
592
+ field += char;
593
+ }
594
+ at += 1;
595
+ }
596
+ if (field !== "" || row.length > 0) {
597
+ row.push(field);
598
+ rows.push(row);
599
+ }
600
+ return rows;
601
+ }
602
+ function planPaste(columns, rows, target, block, context) {
603
+ const plan = { edits: [], refused: [] };
604
+ if (block.length === 0) return plan;
605
+ const single = block.length === 1 && block[0].length === 1;
606
+ const height = single ? target.bottom - target.top + 1 : block.length;
607
+ const width = single ? target.right - target.left + 1 : Math.max(...block.map((line) => line.length));
608
+ for (let r = 0; r < height; r += 1) {
609
+ const row = rows[target.top + r];
610
+ if (!row) break;
611
+ for (let c = 0; c < width; c += 1) {
612
+ const column = columns[target.left + c];
613
+ if (!column) break;
614
+ if (column.readOnly) continue;
615
+ const text = single ? block[0][0] : block[r]?.[c];
616
+ if (text === void 0) continue;
617
+ const parsed = parseText(column, text, context);
618
+ if (!parsed.ok) {
619
+ plan.refused.push({ rowId: row.id, field: column.field, error: parsed.error });
620
+ continue;
621
+ }
622
+ if (sameValue(parsed.value, normalizeValue(column, row.values[column.field]))) continue;
623
+ plan.edits.push({ rowId: row.id, field: column.field, value: parsed.value });
624
+ }
625
+ }
626
+ return plan;
627
+ }
628
+ function planClear(columns, rows, range, context) {
629
+ const plan = { edits: [], refused: [] };
630
+ for (let r = range.top; r <= range.bottom; r += 1) {
631
+ const row = rows[r];
632
+ if (!row) continue;
633
+ for (let c = range.left; c <= range.right; c += 1) {
634
+ const column = columns[c];
635
+ if (!column || column.readOnly) continue;
636
+ const empty = column.type === "checkbox" ? false : column.type === "multiselect" ? [] : null;
637
+ if (sameValue(normalizeValue(column, row.values[column.field]), empty)) continue;
638
+ if (column.required && column.type !== "checkbox") {
639
+ plan.refused.push({ rowId: row.id, field: column.field, error: context.labels.required(column.label) });
640
+ continue;
641
+ }
642
+ plan.edits.push({ rowId: row.id, field: column.field, value: empty });
643
+ }
644
+ }
645
+ return plan;
646
+ }
647
+
648
+ // src/SheetModel/edits.ts
649
+ function refusalText(code, detail, labels) {
650
+ if (code === "forbidden" || code === "capability_not_declared") return labels.forbidden;
651
+ const said = detail?.trim();
652
+ return said ? said : labels.couldNotSave;
653
+ }
654
+ var key = (rowId, field) => `${rowId}\0${field}`;
655
+ var SheetPendingEdits = class {
656
+ waiting = /* @__PURE__ */ new Map();
657
+ refusals = /* @__PURE__ */ new Map();
658
+ listeners = /* @__PURE__ */ new Set();
659
+ failed = 0;
660
+ /** Marks a cell pending, and hands back the one function that settles this edit. Calling it twice does nothing. */
661
+ begin(rowId, field) {
662
+ const at = key(rowId, field);
663
+ this.refusals.delete(at);
664
+ this.waiting.set(at, (this.waiting.get(at) ?? 0) + 1);
665
+ this.emit();
666
+ let settled = false;
667
+ return (outcome) => {
668
+ if (settled) return;
669
+ settled = true;
670
+ const left = (this.waiting.get(at) ?? 1) - 1;
671
+ if (left <= 0) this.waiting.delete(at);
672
+ else this.waiting.set(at, left);
673
+ if (!outcome.ok) {
674
+ this.refusals.set(at, outcome.message);
675
+ this.failed += 1;
676
+ }
677
+ this.emit();
678
+ };
679
+ }
680
+ isPending(rowId, field) {
681
+ return this.waiting.has(key(rowId, field));
682
+ }
683
+ refusal(rowId, field) {
684
+ return this.refusals.get(key(rowId, field));
685
+ }
686
+ /** Clears a cell's refusal, once its reason has been read or acted on. */
687
+ dismiss(rowId, field) {
688
+ if (this.refusals.delete(key(rowId, field))) this.emit();
689
+ }
690
+ /** Edits sent and not yet settled, for the sync strip. */
691
+ get saving() {
692
+ let count = 0;
693
+ for (const value of this.waiting.values()) count += value;
694
+ return count;
695
+ }
696
+ /** The sync strip's state. Refusals are said on their cells, so they do not count as failures to retry. */
697
+ sync(offline = false) {
698
+ return { saving: this.saving, offline };
699
+ }
700
+ /** Every refusal since the last `resetFailures`, for a host that reports them. */
701
+ get failures() {
702
+ return this.failed;
703
+ }
704
+ resetFailures() {
705
+ this.failed = 0;
706
+ }
707
+ /** The host's rows with each one's pending and refused cells attached. */
708
+ decorate(rows) {
709
+ if (this.waiting.size === 0 && this.refusals.size === 0) return [...rows];
710
+ return rows.map((row) => {
711
+ const pending = [];
712
+ const refused = {};
713
+ for (const at of this.waiting.keys()) {
714
+ const [rowId, field] = at.split("\0");
715
+ if (rowId === row.id) pending.push(field);
716
+ }
717
+ for (const [at, message] of this.refusals) {
718
+ const [rowId, field] = at.split("\0");
719
+ if (rowId === row.id) refused[field] = message;
720
+ }
721
+ if (pending.length === 0 && Object.keys(refused).length === 0) return row;
722
+ return {
723
+ ...row,
724
+ pending: [...row.pending ?? [], ...pending],
725
+ refused: { ...row.refused, ...refused }
726
+ };
727
+ });
728
+ }
729
+ subscribe(listener) {
730
+ this.listeners.add(listener);
731
+ return () => void this.listeners.delete(listener);
732
+ }
733
+ emit() {
734
+ for (const listener of this.listeners) listener();
735
+ }
736
+ };
737
+ function changeTime(at, locale) {
738
+ const when = at instanceof Date ? at : new Date(at);
739
+ if (Number.isNaN(when.getTime())) return "";
740
+ return new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit" }).format(when);
741
+ }
742
+ function changeText(change, labels, previous, locale) {
743
+ return labels.changed(change.by, changeTime(change.at, locale), previous);
744
+ }
745
+ var hasPrevious = (change) => change.previous !== void 0;
746
+
747
+ // src/SheetModel/view.ts
748
+ function sortRows(columns, rows, sort, context) {
749
+ if (!sort) return [...rows];
750
+ const column = columns.find((candidate) => candidate.field === sort.field);
751
+ if (!column) return [...rows];
752
+ const sign = sort.direction === "descending" ? -1 : 1;
753
+ return rows.map((row, index) => ({ row, index, value: normalizeValue(column, row.values[column.field]) })).sort((a, b) => {
754
+ const empty = (value) => value === null || value === "" || Array.isArray(value) && value.length === 0;
755
+ if (column.type !== "checkbox" && (empty(a.value) || empty(b.value))) {
756
+ return empty(a.value) === empty(b.value) ? a.index - b.index : empty(a.value) ? 1 : -1;
757
+ }
758
+ return sign * compareValues(column, a.value, b.value, context) || a.index - b.index;
759
+ }).map(({ row }) => row);
760
+ }
761
+ function nextSort(current, field) {
762
+ if (!current || current.field !== field) return { field, direction: "ascending" };
763
+ return current.direction === "ascending" ? { field, direction: "descending" } : null;
764
+ }
765
+ var cellKey = (rowId, field) => `${rowId}\0${field}`;
766
+ function presenceByCell(presence) {
767
+ const out = /* @__PURE__ */ new Map();
768
+ for (const entry of presence ?? []) {
769
+ const at = cellKey(entry.rowId, entry.field);
770
+ const list = out.get(at);
771
+ if (list) list.push(entry);
772
+ else out.set(at, [entry]);
773
+ }
774
+ for (const list of out.values()) list.sort((a, b) => Number(Boolean(b.editing)) - Number(Boolean(a.editing)));
775
+ return out;
776
+ }
777
+ function presenceByRow(presence) {
778
+ const out = /* @__PURE__ */ new Map();
779
+ for (const entry of presence ?? []) if (!out.has(entry.rowId)) out.set(entry.rowId, entry);
780
+ return out;
781
+ }
782
+ function presenceByColumn(presence, max = 2) {
783
+ const out = /* @__PURE__ */ new Map();
784
+ for (const entry of presence ?? []) {
785
+ const list = out.get(entry.field) ?? [];
786
+ if (list.length < max && !list.some((other) => other.id === entry.id)) list.push(entry);
787
+ out.set(entry.field, list);
788
+ }
789
+ return out;
790
+ }
791
+ function flagText(entries) {
792
+ return entries.map((entry) => entry.name).join(", ");
793
+ }
794
+ function cellDescription(column, row, others, context, presenceLabels = PRESENCE_LABELS) {
795
+ const { labels } = context;
796
+ const parts = [`${column.label}.`];
797
+ if (column.readOnly) parts.push(labels.readOnly(column.label));
798
+ if (row.pending?.includes(column.field)) parts.push(`${labels.saving}.`);
799
+ const refused = row.refused?.[column.field];
800
+ if (refused) parts.push(refused);
801
+ const names = others.map((entry) => entry.name);
802
+ if (names.length > 0) parts.push(presenceLabels.alsoHere(names));
803
+ const change = row.changed?.[column.field];
804
+ if (change) {
805
+ const previous = hasPrevious(change) ? displayText(column, normalizeValue(column, change.previous), context) || null : null;
806
+ parts.push(changeText(change, labels, previous, context.locale));
807
+ }
808
+ return parts.join(" ");
809
+ }
810
+ function rowName(columns, row, index, context) {
811
+ const primary = columns.find((column) => column.type === "text");
812
+ if (primary) {
813
+ const text = displayText(primary, normalizeValue(primary, row.values[primary.field]), context);
814
+ if (text) return text;
815
+ }
816
+ return String(index + 1);
817
+ }
818
+ function cellSpeech(column, row, context) {
819
+ return spokenText(column, normalizeValue(column, row.values[column.field]), context);
820
+ }
821
+
822
+ export { LONGTEXT_MAX_DEFAULT, RATING_MAX_CEILING, RATING_MAX_DEFAULT, SHEET_COLUMN_TYPES, SHEET_COLUMN_WIDTHS, SHEET_LABELS, SHEET_ROW_HEIGHT, SHEET_ROW_NUMBER_WIDTH, SheetPendingEdits, TEXT_MAX_DEFAULT, URL_MAX, cellDescription, cellKey, cellSpeech, changeText, changeTime, checkValue, columnOffsets, columnWidth, compareValues, displayText, drawnColumns, editText, editorSide, findOption, findPerson, flagText, hasPrevious, inRange, isEmptyValue, isMod, isMultiCell, isSheetColumnType, isTypingKey, liveOptions, moveFrom, nextSort, normalizeValue, optionHue, parseOptionList, parseTSV, parseText, planClear, planPaste, presenceByCell, presenceByColumn, presenceByRow, rangeOf, ratingMax, refusalText, rowName, sameValue, scrollToRow, sheetLabels, sortRows, spokenText, statusTone, textMax, toTSV, visibleRows };