@dxos/plugin-sheet 0.6.11 → 0.6.12-main.5cc132e

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 (106) hide show
  1. package/dist/lib/browser/{SheetContainer-U4H5D34A.mjs → SheetContainer-Y7ZMFBAP.mjs} +568 -109
  2. package/dist/lib/browser/SheetContainer-Y7ZMFBAP.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-D5AGLXJP.mjs → chunk-GNNVBNCX.mjs} +55 -47
  4. package/dist/lib/browser/chunk-GNNVBNCX.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-APHOLYUB.mjs → chunk-PGKZPKUD.mjs} +2 -2
  6. package/dist/lib/browser/chunk-VBF7YENS.mjs +8 -0
  7. package/dist/lib/browser/{chunk-FUAGSXA4.mjs → chunk-WUPTZUTX.mjs} +6 -3
  8. package/dist/lib/browser/chunk-WUPTZUTX.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +15 -6
  10. package/dist/lib/browser/index.mjs.map +3 -3
  11. package/dist/lib/browser/meta.json +1 -1
  12. package/dist/lib/browser/testing.mjs +3 -3
  13. package/dist/lib/browser/types.mjs +1 -1
  14. package/dist/lib/node/{SheetContainer-AXQV3ZT5.cjs → SheetContainer-KEOKUKAQ.cjs} +509 -62
  15. package/dist/lib/node/SheetContainer-KEOKUKAQ.cjs.map +7 -0
  16. package/dist/lib/node/{chunk-PYXHNAAK.cjs → chunk-57PB2HPY.cjs} +5 -5
  17. package/dist/lib/node/{chunk-CN3RPESU.cjs → chunk-6LWBQAQZ.cjs} +9 -9
  18. package/dist/lib/node/{chunk-DSYKOI4E.cjs → chunk-VJU3NPUJ.cjs} +8 -5
  19. package/dist/lib/node/chunk-VJU3NPUJ.cjs.map +7 -0
  20. package/dist/lib/node/{chunk-5KKJ4NPP.cjs → chunk-ZRQZFV5T.cjs} +70 -57
  21. package/dist/lib/node/chunk-ZRQZFV5T.cjs.map +7 -0
  22. package/dist/lib/node/index.cjs +31 -23
  23. package/dist/lib/node/index.cjs.map +3 -3
  24. package/dist/lib/node/meta.json +1 -1
  25. package/dist/lib/node/testing.cjs +6 -6
  26. package/dist/lib/node/types.cjs +9 -9
  27. package/dist/lib/node/types.cjs.map +1 -1
  28. package/dist/lib/node-esm/SheetContainer-Y7ZMFBAP.mjs +2231 -0
  29. package/dist/lib/node-esm/SheetContainer-Y7ZMFBAP.mjs.map +7 -0
  30. package/dist/lib/node-esm/chunk-GNNVBNCX.mjs +3243 -0
  31. package/dist/lib/node-esm/chunk-GNNVBNCX.mjs.map +7 -0
  32. package/dist/lib/node-esm/chunk-JRL5LGCE.mjs +18 -0
  33. package/dist/lib/node-esm/chunk-JRL5LGCE.mjs.map +7 -0
  34. package/dist/lib/node-esm/chunk-PGKZPKUD.mjs +175 -0
  35. package/dist/lib/node-esm/chunk-PGKZPKUD.mjs.map +7 -0
  36. package/dist/lib/node-esm/chunk-VBF7YENS.mjs +8 -0
  37. package/dist/lib/node-esm/chunk-VBF7YENS.mjs.map +7 -0
  38. package/dist/lib/node-esm/chunk-WUPTZUTX.mjs +85 -0
  39. package/dist/lib/node-esm/chunk-WUPTZUTX.mjs.map +7 -0
  40. package/dist/lib/node-esm/index.mjs +257 -0
  41. package/dist/lib/node-esm/index.mjs.map +7 -0
  42. package/dist/lib/node-esm/meta.json +1 -0
  43. package/dist/lib/node-esm/meta.mjs +9 -0
  44. package/dist/lib/node-esm/meta.mjs.map +7 -0
  45. package/dist/lib/node-esm/testing.mjs +92 -0
  46. package/dist/lib/node-esm/testing.mjs.map +7 -0
  47. package/dist/lib/node-esm/types.mjs +22 -0
  48. package/dist/lib/node-esm/types.mjs.map +7 -0
  49. package/dist/types/src/SheetPlugin.d.ts.map +1 -1
  50. package/dist/types/src/components/Sheet/Sheet.d.ts.map +1 -1
  51. package/dist/types/src/components/Sheet/Sheet.stories.d.ts.map +1 -1
  52. package/dist/types/src/components/Sheet/decorations.d.ts +24 -0
  53. package/dist/types/src/components/Sheet/decorations.d.ts.map +1 -0
  54. package/dist/types/src/components/Sheet/formatting.d.ts.map +1 -1
  55. package/dist/types/src/components/Sheet/sheet-context.d.ts +2 -0
  56. package/dist/types/src/components/Sheet/sheet-context.d.ts.map +1 -1
  57. package/dist/types/src/components/Sheet/threads.d.ts +2 -0
  58. package/dist/types/src/components/Sheet/threads.d.ts.map +1 -0
  59. package/dist/types/src/components/SheetContainer.d.ts +2 -3
  60. package/dist/types/src/components/SheetContainer.d.ts.map +1 -1
  61. package/dist/types/src/components/Toolbar/Toolbar.d.ts +19 -3
  62. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  63. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +17 -12
  64. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  65. package/dist/types/src/components/index.d.ts +1 -2
  66. package/dist/types/src/components/index.d.ts.map +1 -1
  67. package/dist/types/src/model/index.d.ts +1 -0
  68. package/dist/types/src/model/index.d.ts.map +1 -1
  69. package/dist/types/src/model/model.d.ts +0 -16
  70. package/dist/types/src/model/model.d.ts.map +1 -1
  71. package/dist/types/src/model/util.d.ts +24 -0
  72. package/dist/types/src/model/util.d.ts.map +1 -1
  73. package/dist/types/src/translations.d.ts +17 -12
  74. package/dist/types/src/translations.d.ts.map +1 -1
  75. package/dist/types/src/types.d.ts +72 -2
  76. package/dist/types/src/types.d.ts.map +1 -1
  77. package/package.json +36 -32
  78. package/src/SheetPlugin.tsx +8 -15
  79. package/src/components/CellEditor/extension.test.ts +1 -2
  80. package/src/components/ComputeGraph/graph.browser.test.ts +1 -2
  81. package/src/components/Sheet/Sheet.stories.tsx +5 -1
  82. package/src/components/Sheet/Sheet.tsx +45 -8
  83. package/src/components/Sheet/decorations.ts +62 -0
  84. package/src/components/Sheet/formatting.ts +3 -3
  85. package/src/components/Sheet/sheet-context.tsx +9 -1
  86. package/src/components/Sheet/threads.tsx +201 -0
  87. package/src/components/SheetContainer.tsx +72 -18
  88. package/src/components/Toolbar/Toolbar.tsx +54 -12
  89. package/src/model/index.ts +1 -0
  90. package/src/model/model.browser.test.ts +1 -2
  91. package/src/model/model.ts +9 -43
  92. package/src/model/types.test.ts +1 -2
  93. package/src/model/util.ts +67 -0
  94. package/src/translations.ts +6 -1
  95. package/src/types.ts +26 -3
  96. package/dist/lib/browser/SheetContainer-U4H5D34A.mjs.map +0 -7
  97. package/dist/lib/browser/chunk-D5AGLXJP.mjs.map +0 -7
  98. package/dist/lib/browser/chunk-FUAGSXA4.mjs.map +0 -7
  99. package/dist/lib/browser/chunk-NU4PBN33.mjs +0 -8
  100. package/dist/lib/node/SheetContainer-AXQV3ZT5.cjs.map +0 -7
  101. package/dist/lib/node/chunk-5KKJ4NPP.cjs.map +0 -7
  102. package/dist/lib/node/chunk-DSYKOI4E.cjs.map +0 -7
  103. /package/dist/lib/browser/{chunk-APHOLYUB.mjs.map → chunk-PGKZPKUD.mjs.map} +0 -0
  104. /package/dist/lib/browser/{chunk-NU4PBN33.mjs.map → chunk-VBF7YENS.mjs.map} +0 -0
  105. /package/dist/lib/node/{chunk-PYXHNAAK.cjs.map → chunk-57PB2HPY.cjs.map} +0 -0
  106. /package/dist/lib/node/{chunk-CN3RPESU.cjs.map → chunk-6LWBQAQZ.cjs.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/plugin-sheet",
3
- "version": "0.6.11",
3
+ "version": "0.6.12-main.5cc132e",
4
4
  "description": "Braneframe sketch plugin",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -10,14 +10,16 @@
10
10
  ".": {
11
11
  "browser": "./dist/lib/browser/index.mjs",
12
12
  "node": {
13
- "default": "./dist/lib/node/index.cjs"
13
+ "require": "./dist/lib/node/index.cjs",
14
+ "default": "./dist/lib/node-esm/index.mjs"
14
15
  },
15
16
  "types": "./dist/types/src/index.d.ts"
16
17
  },
17
18
  "./meta": {
18
19
  "browser": "./dist/lib/browser/meta.mjs",
19
20
  "node": {
20
- "default": "./dist/lib/node/meta.cjs"
21
+ "require": "./dist/lib/node/meta.cjs",
22
+ "default": "./dist/lib/node-esm/meta.mjs"
21
23
  },
22
24
  "types": "./dist/types/src/meta.d.ts"
23
25
  },
@@ -31,7 +33,8 @@
31
33
  "./types": {
32
34
  "browser": "./dist/lib/browser/types.mjs",
33
35
  "node": {
34
- "default": "./dist/lib/node/types.cjs"
36
+ "require": "./dist/lib/node/types.cjs",
37
+ "default": "./dist/lib/node-esm/types.mjs"
35
38
  },
36
39
  "types": "./dist/types/src/types.d.ts"
37
40
  }
@@ -76,25 +79,26 @@
76
79
  "re-resizable": "^6.9.17",
77
80
  "react-markdown": "^8.0.5",
78
81
  "react-resize-detector": "^11.0.1",
79
- "@dxos/app-framework": "0.6.11",
80
- "@dxos/async": "0.6.11",
81
- "@dxos/context": "0.6.11",
82
- "@dxos/client": "0.6.11",
83
- "@dxos/crypto": "0.6.11",
84
- "@dxos/debug": "0.6.11",
85
- "@dxos/echo-schema": "0.6.11",
86
- "@dxos/invariant": "0.6.11",
87
- "@dxos/keys": "0.6.11",
88
- "@dxos/plugin-client": "0.6.11",
89
- "@dxos/log": "0.6.11",
90
- "@dxos/plugin-graph": "0.6.11",
91
- "@dxos/plugin-script": "0.6.11",
92
- "@dxos/plugin-space": "0.6.11",
93
- "@dxos/plugin-stack": "0.6.11",
94
- "@dxos/react-ui-attention": "0.6.11",
95
- "@dxos/react-client": "0.6.11",
96
- "@dxos/react-ui-editor": "0.6.11",
97
- "@dxos/util": "0.6.11"
82
+ "@dxos/app-framework": "0.6.12-main.5cc132e",
83
+ "@dxos/async": "0.6.12-main.5cc132e",
84
+ "@dxos/client": "0.6.12-main.5cc132e",
85
+ "@dxos/crypto": "0.6.12-main.5cc132e",
86
+ "@dxos/context": "0.6.12-main.5cc132e",
87
+ "@dxos/echo-schema": "0.6.12-main.5cc132e",
88
+ "@dxos/debug": "0.6.12-main.5cc132e",
89
+ "@dxos/invariant": "0.6.12-main.5cc132e",
90
+ "@dxos/keys": "0.6.12-main.5cc132e",
91
+ "@dxos/log": "0.6.12-main.5cc132e",
92
+ "@dxos/plugin-attention": "0.6.12-main.5cc132e",
93
+ "@dxos/plugin-client": "0.6.12-main.5cc132e",
94
+ "@dxos/plugin-graph": "0.6.12-main.5cc132e",
95
+ "@dxos/plugin-script": "0.6.12-main.5cc132e",
96
+ "@dxos/plugin-space": "0.6.12-main.5cc132e",
97
+ "@dxos/react-client": "0.6.12-main.5cc132e",
98
+ "@dxos/react-ui-attention": "0.6.12-main.5cc132e",
99
+ "@dxos/plugin-stack": "0.6.12-main.5cc132e",
100
+ "@dxos/react-ui-editor": "0.6.12-main.5cc132e",
101
+ "@dxos/util": "0.6.12-main.5cc132e"
98
102
  },
99
103
  "devDependencies": {
100
104
  "@lezer/generator": "^1.7.1",
@@ -106,20 +110,20 @@
106
110
  "@types/react-window": "^1.8.8",
107
111
  "react": "~18.2.0",
108
112
  "react-dom": "~18.2.0",
109
- "vite": "^5.3.4",
110
- "@dxos/echo-generator": "0.6.11",
111
- "@dxos/random": "0.6.11",
112
- "@dxos/react-ui": "0.6.11",
113
- "@dxos/react-ui-types": "0.6.11",
114
- "@dxos/storybook-utils": "0.6.11",
115
- "@dxos/react-ui-theme": "0.6.11"
113
+ "vite": "5.4.7",
114
+ "@dxos/echo-generator": "0.6.12-main.5cc132e",
115
+ "@dxos/random": "0.6.12-main.5cc132e",
116
+ "@dxos/react-ui": "0.6.12-main.5cc132e",
117
+ "@dxos/react-ui-types": "0.6.12-main.5cc132e",
118
+ "@dxos/react-ui-theme": "0.6.12-main.5cc132e",
119
+ "@dxos/storybook-utils": "0.6.12-main.5cc132e"
116
120
  },
117
121
  "optionalDependencies": {
118
122
  "@phosphor-icons/react": "^2.1.5",
119
123
  "react": "^18.0.0",
120
124
  "react-dom": "^18.0.0",
121
- "@dxos/react-ui-theme": "0.6.11",
122
- "@dxos/react-ui": "0.6.11"
125
+ "@dxos/react-ui": "0.6.12-main.5cc132e",
126
+ "@dxos/react-ui-theme": "0.6.12-main.5cc132e"
123
127
  },
124
128
  "publishConfig": {
125
129
  "access": "public"
@@ -5,13 +5,7 @@
5
5
  import { type IconProps, GridNine } from '@phosphor-icons/react';
6
6
  import React from 'react';
7
7
 
8
- import {
9
- NavigationAction,
10
- parseIntentPlugin,
11
- resolvePlugin,
12
- type PluginDefinition,
13
- type LayoutCoordinate,
14
- } from '@dxos/app-framework';
8
+ import { NavigationAction, parseIntentPlugin, resolvePlugin, type PluginDefinition } from '@dxos/app-framework';
15
9
  import { create } from '@dxos/echo-schema';
16
10
  import { parseClientPlugin } from '@dxos/plugin-client';
17
11
  import { type ActionGroup, createExtension, isActionGroup } from '@dxos/plugin-graph';
@@ -30,7 +24,7 @@ import {
30
24
  import { EdgeFunctionPlugin, EdgeFunctionPluginTranslations } from './components/ComputeGraph/edge-function';
31
25
  import { ComputeGraphContextProvider } from './components/ComputeGraph/graph-context';
32
26
  import meta, { SHEET_PLUGIN } from './meta';
33
- import { SheetModel } from './model';
27
+ import { compareIndexPositions, SheetModel } from './model';
34
28
  import translations from './translations';
35
29
  import { createSheet, SheetAction, type SheetPluginProvides, SheetType } from './types';
36
30
 
@@ -138,6 +132,11 @@ export const SheetPlugin = (): PluginDefinition<SheetPluginProvides> => {
138
132
  },
139
133
  ],
140
134
  },
135
+ thread: {
136
+ predicate: (data) => data instanceof SheetType,
137
+ createSort: (sheet) => (anchorA, anchorB) =>
138
+ !anchorA || !anchorB ? 0 : compareIndexPositions(sheet, anchorA, anchorB),
139
+ },
141
140
  surface: {
142
141
  component: ({ data, role = 'never' }) => {
143
142
  // TODO(burdon): Standardize wrapper (with room for toolbar).
@@ -147,13 +146,7 @@ export const SheetPlugin = (): PluginDefinition<SheetPluginProvides> => {
147
146
  case 'article':
148
147
  case 'section': {
149
148
  return (
150
- <SheetContainer
151
- sheet={data.object}
152
- space={space}
153
- role={role}
154
- coordinate={data.coordinate as LayoutCoordinate}
155
- remoteFunctionUrl={remoteFunctionUrl}
156
- />
149
+ <SheetContainer sheet={data.object} space={space} role={role} remoteFunctionUrl={remoteFunctionUrl} />
157
150
  );
158
151
  }
159
152
  }
@@ -6,9 +6,8 @@ import { CompletionContext, type CompletionSource } from '@codemirror/autocomple
6
6
  import { EditorState } from '@codemirror/state';
7
7
  // @ts-ignore
8
8
  import { testTree } from '@lezer/generator/test';
9
- import { expect } from 'chai';
10
9
  import { spreadsheet } from 'codemirror-lang-spreadsheet';
11
- import { describe, test } from 'vitest';
10
+ import { describe, expect, test } from 'vitest';
12
11
 
13
12
  import { sheetExtension } from './extension';
14
13
  import { defaultFunctions } from '../../model/functions';
@@ -2,8 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { expect } from 'chai';
6
- import { describe, test } from 'vitest';
5
+ import { describe, expect, test } from 'vitest';
7
6
 
8
7
  import { Trigger } from '@dxos/async';
9
8
 
@@ -18,6 +18,7 @@ import { withTheme, withLayout } from '@dxos/storybook-utils';
18
18
  import { Sheet } from './Sheet';
19
19
  import { type SizeMap } from './grid';
20
20
  import { useSheetContext } from './sheet-context';
21
+ import { addressToIndex, rangeToIndex } from '../../model';
21
22
  import { createTestSheet, testSheetName } from '../../testing';
22
23
  import { ValueTypeEnum, SheetType } from '../../types';
23
24
  import { type ComputeGraph, createComputeGraph } from '../ComputeGraph';
@@ -36,7 +37,7 @@ const SheetWithToolbar = ({ debug, space }: { debug?: boolean; space: Space }) =
36
37
  return;
37
38
  }
38
39
 
39
- const idx = range ? model.rangeToIndex(range) : model.addressToIndex(cursor);
40
+ const idx = range ? rangeToIndex(model.sheet, range) : addressToIndex(model.sheet, cursor);
40
41
  model.sheet.formatting[idx] ??= {};
41
42
  const format = model.sheet.formatting[idx];
42
43
 
@@ -75,6 +76,9 @@ const SheetWithToolbar = ({ debug, space }: { debug?: boolean; space: Space }) =
75
76
  format.precision = 2;
76
77
  break;
77
78
  }
79
+ case 'comment': {
80
+ break;
81
+ }
78
82
  }
79
83
  };
80
84
 
@@ -60,6 +60,7 @@ import {
60
60
  } from './grid';
61
61
  import { type GridSize, handleArrowNav, handleNav, useRangeSelect } from './nav';
62
62
  import { type SheetContextProps, SheetContextProvider, useSheetContext } from './sheet-context';
63
+ import { useThreads } from './threads';
63
64
  import { getRectUnion, getRelativeClientRect, scrollIntoView } from './util';
64
65
  import {
65
66
  type CellIndex,
@@ -68,6 +69,8 @@ import {
68
69
  columnLetter,
69
70
  posEquals,
70
71
  rangeToA1Notation,
72
+ addressToIndex,
73
+ addressFromIndex,
71
74
  } from '../../model';
72
75
  import {
73
76
  CellEditor,
@@ -141,6 +144,10 @@ const SheetMain = forwardRef<HTMLDivElement, SheetMainProps>(({ classNames, numR
141
144
  // Scrolling.
142
145
  const { rowsRef, columnsRef, contentRef } = useScrollHandlers();
143
146
 
147
+ // Threads.
148
+ // TODO(Zan): Move this to an extension once we have an extension model.
149
+ useThreads();
150
+
144
151
  //
145
152
  // Order of Row/columns.
146
153
  //
@@ -170,21 +177,21 @@ const SheetMain = forwardRef<HTMLDivElement, SheetMainProps>(({ classNames, numR
170
177
  }, [rows, columns]);
171
178
 
172
179
  const handleMoveRows: SheetRowsProps['onMove'] = (from, to, num = 1) => {
173
- const cursorIdx = cursor ? model.addressToIndex(cursor) : undefined;
180
+ const cursorIdx = cursor ? addressToIndex(model.sheet, cursor) : undefined;
174
181
  const [rows] = model.sheet.rows.splice(from, num);
175
182
  model.sheet.rows.splice(to, 0, rows);
176
183
  if (cursorIdx) {
177
- setCursor(model.addressFromIndex(cursorIdx));
184
+ setCursor(addressFromIndex(model.sheet, cursorIdx));
178
185
  }
179
186
  setRows([...model.sheet.rows]);
180
187
  };
181
188
 
182
189
  const handleMoveColumns: SheetColumnsProps['onMove'] = (from, to, num = 1) => {
183
- const cursorIdx = cursor ? model.addressToIndex(cursor) : undefined;
190
+ const cursorIdx = cursor ? addressToIndex(model.sheet, cursor) : undefined;
184
191
  const columns = model.sheet.columns.splice(from, num);
185
192
  model.sheet.columns.splice(to, 0, ...columns);
186
193
  if (cursorIdx) {
187
- setCursor(model.addressFromIndex(cursorIdx));
194
+ setCursor(addressFromIndex(model.sheet, cursorIdx));
188
195
  }
189
196
  setColumns([...model.sheet.columns]);
190
197
  };
@@ -882,7 +889,7 @@ const SheetGrid = forwardRef<HTMLDivElement, SheetGridProps>(
882
889
  const style: CSSProperties = { position: 'absolute', top, left, width, height };
883
890
  const cell = { row, column };
884
891
  const id = addressToA1Notation(cell);
885
- const idx = model.addressToIndex(cell);
892
+ const idx = addressToIndex(model.sheet, cell);
886
893
  const active = posEquals(cursor, cell);
887
894
  if (active && editing) {
888
895
  const value = initialText.current ?? model.getCellText(cell) ?? '';
@@ -1003,16 +1010,46 @@ type SheetCellProps = {
1003
1010
  };
1004
1011
 
1005
1012
  const SheetCell = ({ id, cell, style, active, onSelect }: SheetCellProps) => {
1006
- const { formatting, editing, setRange } = useSheetContext();
1013
+ const {
1014
+ formatting,
1015
+ editing,
1016
+ setRange,
1017
+ decorations,
1018
+ model: { sheet },
1019
+ } = useSheetContext();
1007
1020
  const { value, classNames } = formatting.getFormatting(cell);
1008
1021
 
1022
+ const decorationsForCell = decorations.getDecorationsForCell(addressToIndex(sheet, cell)) ?? [];
1023
+ const decorationAddedClasses = useMemo(
1024
+ () => decorationsForCell.flatMap((d) => d.classNames ?? []),
1025
+ [decorationsForCell],
1026
+ );
1027
+ const decoratedContent = decorationsForCell.reduce(
1028
+ (children, { decorate }) => {
1029
+ if (!decorate) {
1030
+ return children;
1031
+ }
1032
+ const DecoratorComponent = decorate;
1033
+ return <DecoratorComponent>{children}</DecoratorComponent>;
1034
+ },
1035
+ <div
1036
+ role='none'
1037
+ className={mx(
1038
+ 'flex flex-grow bs-full is-full px-2 items-center truncate cursor-pointer',
1039
+ ...decorationAddedClasses,
1040
+ )}
1041
+ >
1042
+ {value}
1043
+ </div>,
1044
+ );
1045
+
1009
1046
  return (
1010
1047
  <div
1011
1048
  {...{ [`data-${CELL_DATA_KEY}`]: id }}
1012
1049
  role='cell'
1013
1050
  style={style}
1014
1051
  className={mx(
1015
- 'flex w-full h-full px-2 py-1 truncate items-center border border-gridLine cursor-pointer',
1052
+ 'border border-gridLine cursor-pointer',
1016
1053
  fragments.cell,
1017
1054
  active && ['z-20', fragments.cellSelected],
1018
1055
  classNames,
@@ -1026,7 +1063,7 @@ const SheetCell = ({ id, cell, style, active, onSelect }: SheetCellProps) => {
1026
1063
  }}
1027
1064
  onDoubleClick={() => onSelect?.(cell, true)}
1028
1065
  >
1029
- {value}
1066
+ {decoratedContent}
1030
1067
  </div>
1031
1068
  );
1032
1069
  };
@@ -0,0 +1,62 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { create } from '@dxos/echo-schema';
6
+
7
+ export type Decoration = {
8
+ type: string;
9
+ /**
10
+ * A wrapping render function to encapsulate cell content. This function is applied between
11
+ * the cell's border and its padding/layout/content, allowing for custom rendering or
12
+ * additional elements to be inserted around the cell's main content.
13
+ */
14
+ decorate?: (props: { children: React.ReactNode }) => React.ReactNode;
15
+ /**
16
+ * An array of CSS class names to be applied to the content of the SheetCell.
17
+ * These classes can be used to style the cell's content independently of its structure.
18
+ */
19
+ classNames?: string[];
20
+ cellIndex: string;
21
+ };
22
+
23
+ export const createDecorations = () => {
24
+ // Reactive object to hold decorations
25
+ // TODO(Zan): Use CELL ID's to key the decoration map.
26
+ // TODO(Zan): Consider maintaining an index of decorations by type.
27
+ const { decorations } = create<{ decorations: Record<string, Decoration[]> }>({ decorations: {} });
28
+
29
+ const addDecoration = (cellIndex: string, decorator: Decoration) => {
30
+ decorations[cellIndex] = [...(decorations[cellIndex] || []), decorator];
31
+ };
32
+
33
+ const removeDecoration = (cellIndex: string, type?: string) => {
34
+ if (type) {
35
+ decorations[cellIndex] = (decorations[cellIndex] || []).filter((d) => d.type !== type);
36
+ } else {
37
+ delete decorations[cellIndex];
38
+ }
39
+ };
40
+
41
+ // TODO(Zan): I should check if returning the a value from a map in a deep signal is a reactive slice.
42
+ const getDecorationsForCell = (cellIndex: string): Decoration[] | undefined => {
43
+ return decorations[cellIndex];
44
+ };
45
+
46
+ const getAllDecorations = (): Decoration[] => {
47
+ const result: Decoration[] = [];
48
+ for (const decoratorArray of Object.values(decorations)) {
49
+ for (const decorator of decoratorArray) {
50
+ result.push(decorator);
51
+ }
52
+ }
53
+ return result;
54
+ };
55
+
56
+ return {
57
+ addDecoration,
58
+ removeDecoration,
59
+ getDecorationsForCell,
60
+ getAllDecorations,
61
+ } as const;
62
+ };
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { type ClassNameValue } from '@dxos/react-ui-types';
6
6
 
7
- import { type SheetModel, type CellAddress, inRange } from '../../model';
7
+ import { type SheetModel, type CellAddress, inRange, addressToIndex, rangeFromIndex } from '../../model';
8
8
  import { ValueTypeEnum } from '../../types';
9
9
 
10
10
  export class FormattingModel {
@@ -23,7 +23,7 @@ export class FormattingModel {
23
23
  const locales = undefined;
24
24
 
25
25
  // Cell-specific formatting.
26
- const idx = this.model.addressToIndex(cell);
26
+ const idx = addressToIndex(this.model.sheet, cell);
27
27
  let formatting = this.model.sheet.formatting?.[idx] ?? {};
28
28
  const classNames = [...(formatting?.classNames ?? [])];
29
29
 
@@ -31,7 +31,7 @@ export class FormattingModel {
31
31
  // TODO(burdon): NOTE: D0 means the D column.
32
32
  // TODO(burdon): Cache model formatting (e.g., for ranges). Create class out of this function.
33
33
  for (const [idx, _formatting] of Object.entries(this.model.sheet.formatting)) {
34
- const range = this.model.rangeFromIndex(idx);
34
+ const range = rangeFromIndex(this.model.sheet, idx);
35
35
  if (inRange(range, cell)) {
36
36
  if (_formatting.classNames) {
37
37
  classNames.push(..._formatting.classNames);
@@ -2,12 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { type PropsWithChildren, createContext, useContext, useState, useEffect } from 'react';
5
+ import React, { type PropsWithChildren, createContext, useContext, useState, useEffect, useMemo } from 'react';
6
6
 
7
7
  import { invariant } from '@dxos/invariant';
8
8
  import { type FunctionType } from '@dxos/plugin-script';
9
9
  import { fullyQualifiedId, type Space } from '@dxos/react-client/echo';
10
10
 
11
+ import { createDecorations } from './decorations';
11
12
  import { FormattingModel } from './formatting';
12
13
  import { type CellAddress, type CellRange, defaultFunctions, SheetModel } from '../../model';
13
14
  import { type SheetType } from '../../types';
@@ -36,6 +37,9 @@ export type SheetContextType = {
36
37
  // Events.
37
38
  // TODO(burdon): Generalize.
38
39
  onInfo?: () => void;
40
+
41
+ // Decorations.
42
+ decorations: ReturnType<typeof createDecorations>;
39
43
  };
40
44
 
41
45
  const SheetContext = createContext<SheetContextType | null>(null);
@@ -104,9 +108,12 @@ export const SheetContextProvider = ({
104
108
  }: PropsWithChildren<SheetContextProps>) => {
105
109
  const graph = useComputeGraph(space, options);
106
110
 
111
+ // TODO(Zan): We should offer a version of set range and set cursor that scrolls to
112
+ // that cell or range if it is not visible.
107
113
  const [cursor, setCursor] = useState<CellAddress>();
108
114
  const [range, setRange] = useState<CellRange>();
109
115
  const [editing, setEditing] = useState<boolean>(false);
116
+ const decorations = useMemo(() => createDecorations(), []);
110
117
 
111
118
  const [[model, formatting] = [], setModels] = useState<[SheetModel, FormattingModel] | undefined>(undefined);
112
119
  useEffect(() => {
@@ -142,6 +149,7 @@ export const SheetContextProvider = ({
142
149
  setEditing,
143
150
  // TODO(burdon): Change to event.
144
151
  onInfo,
152
+ decorations,
145
153
  }}
146
154
  >
147
155
  {children}
@@ -0,0 +1,201 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { effect } from '@preact/signals-core';
6
+ import React, { useCallback, useEffect, useMemo } from 'react';
7
+
8
+ import { type IntentResolver, LayoutAction, useIntentDispatcher, useIntentResolver } from '@dxos/app-framework';
9
+ import { debounce } from '@dxos/async';
10
+ import { fullyQualifiedId } from '@dxos/react-client/echo';
11
+ import { Icon, useTranslation } from '@dxos/react-ui';
12
+ import { mx } from '@dxos/react-ui-theme';
13
+
14
+ import { type Decoration } from './decorations';
15
+ import { useSheetContext } from './sheet-context';
16
+ import { SHEET_PLUGIN } from '../../meta';
17
+ import { addressFromIndex, addressToIndex, type CellAddress, closest } from '../../model';
18
+
19
+ const CommentIndicator = () => {
20
+ return (
21
+ <div
22
+ role='none'
23
+ className='absolute top-0 right-0 w-0 h-0 border-t-8 border-l-8 border-t-cmCommentSurface border-l-transparent'
24
+ />
25
+ );
26
+ };
27
+
28
+ const ThreadedCellWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
29
+ const dispatch = useIntentDispatcher();
30
+ const [isHovered, setIsHovered] = React.useState(true);
31
+ const { t } = useTranslation(SHEET_PLUGIN);
32
+
33
+ const handleClick = React.useCallback(
34
+ (_event: React.MouseEvent) => {
35
+ void dispatch({ action: LayoutAction.SET_LAYOUT, data: { element: 'complementary', state: true } });
36
+ },
37
+ [dispatch],
38
+ );
39
+
40
+ return (
41
+ <div
42
+ role='none'
43
+ className={mx('relative h-full is-full')}
44
+ onMouseEnter={() => {
45
+ setIsHovered(true);
46
+ }}
47
+ onMouseLeave={() => {
48
+ setIsHovered(false);
49
+ }}
50
+ >
51
+ <CommentIndicator />
52
+ {isHovered && (
53
+ <div className='absolute inset-0 flex items-center justify-end pr-1'>
54
+ <button
55
+ className='ch-button text-xs min-bs-0 p-1'
56
+ onClick={handleClick}
57
+ aria-label={t('open comment for sheet cell')}
58
+ >
59
+ <Icon icon='ph--chat--regular' aria-hidden={true} />
60
+ </button>
61
+ </div>
62
+ )}
63
+ {children}
64
+ </div>
65
+ );
66
+ };
67
+
68
+ const createThreadDecoration = (cellIndex: string, threadId: string, sheetId: string): Decoration => {
69
+ return {
70
+ type: 'comment',
71
+ cellIndex,
72
+ decorate: (props) => <ThreadedCellWrapper {...props} />,
73
+ };
74
+ };
75
+
76
+ const useUpdateCursorOnThreadSelection = () => {
77
+ const { setCursor, model } = useSheetContext();
78
+
79
+ const handleScrollIntoView: IntentResolver = useCallback(
80
+ ({ action, data }) => {
81
+ switch (action) {
82
+ case LayoutAction.SCROLL_INTO_VIEW: {
83
+ if (!data?.id || data?.cursor === undefined || data?.id !== fullyQualifiedId(model.sheet)) {
84
+ return;
85
+ }
86
+
87
+ // TODO(Zan): Everywhere we refer to the cursor in a thread context should change to `anchor`.
88
+ const cellAddress = addressFromIndex(model.sheet, data.cursor);
89
+ setCursor(cellAddress);
90
+ }
91
+ }
92
+ },
93
+ [model.sheet, setCursor],
94
+ );
95
+
96
+ useIntentResolver(SHEET_PLUGIN, handleScrollIntoView);
97
+ };
98
+
99
+ const useSelectThreadOnCursorChange = () => {
100
+ const { cursor, model } = useSheetContext();
101
+ const dispatch = useIntentDispatcher();
102
+
103
+ const activeThreads = useMemo(
104
+ () =>
105
+ model.sheet.threads?.filter(
106
+ (thread): thread is NonNullable<typeof thread> => !!thread && thread.status === 'active',
107
+ ) ?? [],
108
+ [JSON.stringify(model.sheet.threads)],
109
+ );
110
+
111
+ const activeThreadAddresses = useMemo(
112
+ () =>
113
+ activeThreads
114
+ .map((thread) => thread.anchor)
115
+ .filter((anchor): anchor is NonNullable<typeof anchor> => anchor !== undefined)
116
+ .map((anchor) => addressFromIndex(model.sheet, anchor)),
117
+ [activeThreads, model.sheet],
118
+ );
119
+
120
+ const selectClosestThread = useCallback(
121
+ (cellAddress: CellAddress) => {
122
+ if (!cellAddress || !activeThreads) {
123
+ return;
124
+ }
125
+
126
+ const closestThreadAnchor = closest(cellAddress, activeThreadAddresses);
127
+ if (closestThreadAnchor) {
128
+ const closestThread = activeThreads.find(
129
+ (thread) => thread && thread.anchor === addressToIndex(model.sheet, closestThreadAnchor),
130
+ );
131
+
132
+ if (closestThread) {
133
+ void dispatch([
134
+ { action: 'dxos.org/plugin/thread/action/select', data: { current: fullyQualifiedId(closestThread) } },
135
+ ]);
136
+ }
137
+ }
138
+ },
139
+ [dispatch, activeThreads, activeThreadAddresses, model.sheet],
140
+ );
141
+
142
+ const debounced = useMemo(() => {
143
+ return debounce((cursor: CellAddress) => requestAnimationFrame(() => selectClosestThread(cursor)), 50);
144
+ }, [selectClosestThread]);
145
+
146
+ useEffect(() => {
147
+ if (!cursor) {
148
+ return;
149
+ }
150
+ debounced(cursor);
151
+ }, [cursor, selectClosestThread]);
152
+ };
153
+
154
+ const useThreadDecorations = () => {
155
+ const { decorations, model } = useSheetContext();
156
+ const sheet = useMemo(() => model.sheet, [model.sheet]);
157
+ const sheetId = useMemo(() => fullyQualifiedId(sheet), [sheet]);
158
+
159
+ useEffect(() => {
160
+ const unsubscribe = effect(() => {
161
+ const activeThreadAnchors = new Set<string>();
162
+ if (!sheet.threads) {
163
+ return;
164
+ }
165
+
166
+ // Process active threads
167
+ for (const thread of sheet.threads) {
168
+ if (!thread || thread.anchor === undefined || thread.status === 'resolved') {
169
+ continue;
170
+ }
171
+
172
+ activeThreadAnchors.add(thread.anchor);
173
+ const index = thread.anchor;
174
+
175
+ // Add decoration only if it doesn't already exist
176
+ const existingDecorations = decorations.getDecorationsForCell(index);
177
+ if (!existingDecorations || !existingDecorations.some((d) => d.type === 'comment')) {
178
+ decorations.addDecoration(index, createThreadDecoration(index, thread.id, sheetId));
179
+ }
180
+ }
181
+
182
+ // Remove decorations for resolved or deleted threads
183
+ for (const decoration of decorations.getAllDecorations()) {
184
+ if (decoration.type !== 'comment') {
185
+ continue;
186
+ }
187
+
188
+ if (!activeThreadAnchors.has(decoration.cellIndex)) {
189
+ decorations.removeDecoration(decoration.cellIndex, 'comment');
190
+ }
191
+ }
192
+ });
193
+ return () => unsubscribe();
194
+ });
195
+ };
196
+
197
+ export const useThreads = () => {
198
+ useUpdateCursorOnThreadSelection();
199
+ useSelectThreadOnCursorChange();
200
+ useThreadDecorations();
201
+ };