create-tinybase 0.2.5 → 0.3.1

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 (81) hide show
  1. package/README.md +62 -18
  2. package/cli.js +2 -2
  3. package/package.json +7 -4
  4. package/screenshots/chat.png +0 -0
  5. package/screenshots/drawing.png +0 -0
  6. package/screenshots/todos.png +0 -0
  7. package/templates/README.md.hbs +130 -12
  8. package/templates/client/eslint.config.js.hbs +7 -0
  9. package/templates/client/package.json.hbs +28 -8
  10. package/templates/client/src/chat/App.tsx.hbs +28 -22
  11. package/templates/client/src/chat/ChatStore.tsx.hbs +108 -27
  12. package/templates/client/src/chat/Message.tsx.hbs +8 -9
  13. package/templates/client/src/chat/MessageInput.tsx.hbs +7 -7
  14. package/templates/client/src/chat/Messages.tsx.hbs +5 -15
  15. package/templates/client/src/chat/SettingsStore.tsx.hbs +86 -9
  16. package/templates/client/src/chat/UsernameInput.tsx.hbs +6 -11
  17. package/templates/client/src/chat/app.ts.hbs +18 -18
  18. package/templates/client/src/chat/chatStore.ts.hbs +97 -35
  19. package/templates/client/src/chat/message.ts.hbs +4 -3
  20. package/templates/client/src/chat/messageInput.css.hbs +1 -1
  21. package/templates/client/src/chat/messageInput.ts.hbs +7 -8
  22. package/templates/client/src/chat/messages.ts.hbs +7 -12
  23. package/templates/client/src/chat/settingsStore.ts.hbs +65 -6
  24. package/templates/client/src/chat/usernameInput.css.hbs +1 -1
  25. package/templates/client/src/chat/usernameInput.ts.hbs +6 -6
  26. package/templates/client/src/chat/utils.ts.hbs +26 -0
  27. package/templates/client/src/drawing/App.tsx.hbs +26 -20
  28. package/templates/client/src/drawing/BrushSize.tsx.hbs +8 -11
  29. package/templates/client/src/drawing/Canvas.tsx.hbs +65 -73
  30. package/templates/client/src/drawing/CanvasStore.tsx.hbs +104 -18
  31. package/templates/client/src/drawing/ColorPicker.tsx.hbs +4 -11
  32. package/templates/client/src/drawing/DrawingControls.tsx.hbs +7 -7
  33. package/templates/client/src/drawing/SettingsStore.tsx.hbs +81 -8
  34. package/templates/client/src/drawing/app.ts.hbs +18 -8
  35. package/templates/client/src/drawing/brushSize.ts.hbs +12 -5
  36. package/templates/client/src/drawing/canvas.ts.hbs +84 -86
  37. package/templates/client/src/drawing/canvasStore.ts.hbs +93 -26
  38. package/templates/client/src/drawing/colorPicker.ts.hbs +3 -3
  39. package/templates/client/src/drawing/drawingControls.ts.hbs +7 -7
  40. package/templates/client/src/drawing/settingsStore.ts.hbs +63 -8
  41. package/templates/client/src/game/App.tsx.hbs +20 -16
  42. package/templates/client/src/game/Board.tsx.hbs +8 -8
  43. package/templates/client/src/game/Game.tsx.hbs +14 -21
  44. package/templates/client/src/game/GameStatus.tsx.hbs +5 -5
  45. package/templates/client/src/game/Square.tsx.hbs +6 -11
  46. package/templates/client/src/game/Store.tsx.hbs +106 -16
  47. package/templates/client/src/game/app.ts.hbs +17 -6
  48. package/templates/client/src/game/board.ts.hbs +7 -7
  49. package/templates/client/src/game/game.ts.hbs +12 -18
  50. package/templates/client/src/game/gameStatus.ts.hbs +3 -3
  51. package/templates/client/src/game/square.ts.hbs +4 -4
  52. package/templates/client/src/game/store.ts.hbs +95 -23
  53. package/templates/client/src/index.tsx.hbs +5 -7
  54. package/templates/client/src/shared/Button.tsx.hbs +3 -3
  55. package/templates/client/src/shared/Input.tsx.hbs +2 -2
  56. package/templates/client/src/shared/Loading.tsx.hbs +5 -0
  57. package/templates/client/src/shared/button.ts.hbs +2 -2
  58. package/templates/client/src/shared/config.ts.hbs +4 -6
  59. package/templates/client/src/shared/input.ts.hbs +2 -2
  60. package/templates/client/src/shared/loading.css.hbs +21 -0
  61. package/templates/client/src/shared/loading.ts.hbs +13 -0
  62. package/templates/client/src/shared/pglite.ts.hbs +10 -0
  63. package/templates/client/src/shared/sqlite.ts.hbs +17 -0
  64. package/templates/client/src/todos/App.tsx.hbs +22 -22
  65. package/templates/client/src/todos/Store.tsx.hbs +106 -23
  66. package/templates/client/src/todos/TodoInput.tsx.hbs +6 -8
  67. package/templates/client/src/todos/TodoItem.tsx.hbs +5 -6
  68. package/templates/client/src/todos/TodoList.tsx.hbs +5 -6
  69. package/templates/client/src/todos/app.ts.hbs +16 -10
  70. package/templates/client/src/todos/store.ts.hbs +94 -30
  71. package/templates/client/src/todos/todoInput.ts.hbs +5 -8
  72. package/templates/client/src/todos/todoItem.ts.hbs +3 -4
  73. package/templates/client/src/todos/todoList.ts.hbs +6 -8
  74. package/templates/client/vite-env.d.ts.hbs +4 -0
  75. package/templates/client/vite.config.js.hbs +53 -3
  76. package/templates/server/index.ts.hbs +43 -0
  77. package/templates/server/package.json.hbs +6 -7
  78. package/templates/server/wrangler.toml.hbs +1 -1
  79. package/templates/server/index-do.ts.hbs +0 -22
  80. package/templates/server/index-node.ts.hbs +0 -8
  81. /package/templates/client/{.prettierrc.hbs → .prettierrc.json.hbs} +0 -0
@@ -1,13 +1,13 @@
1
1
  {{includeFile template="client/src/chat/messageInput.css.hbs" output="client/src/messageInput.css"}}
2
- import './messageInput.css';
3
- import {type SettingsStore} from './settingsStore';
4
- import {type ChatStore} from './chatStore';
2
+ {{addImport "import './messageInput.css';"}}
3
+ {{addImport "import {type SettingsStore} from './settingsStore';"}}
4
+ {{addImport "import {type ChatStore} from './chatStore';"}}
5
5
 
6
6
  {{includeFile template="client/src/shared/button.ts.hbs" output="client/src/button.{{ext}}"}}
7
- import {createButton} from './button';
7
+ {{addImport "import {createButton} from './button';"}}
8
8
 
9
9
  {{includeFile template="client/src/shared/input.ts.hbs" output="client/src/input.{{ext}}"}}
10
- import {createInput} from './input';
10
+ {{addImport "import {createInput} from './input';"}}
11
11
 
12
12
  export const createMessageInput = (settingsStore: SettingsStore, chatStore: ChatStore): HTMLDivElement => {
13
13
  const messageInputContainer = document.createElement('div');
@@ -20,12 +20,11 @@ const sendMessage = () => {
20
20
  const text = messageInput.value.trim();
21
21
  if (text) {
22
22
  chatStore.addRow('messages', {
23
- username: settingsStore.getValue('username'),
23
+ username: settingsStore.getValue('username') || 'Anonymous',
24
24
  text,
25
25
  timestamp: Date.now(),
26
26
  });
27
27
  messageInput.value = '';
28
- messageInput.focus();
29
28
  }
30
29
  };
31
30
 
@@ -39,4 +38,4 @@ const sendButton = createButton('Send', sendMessage, 'primary');
39
38
  messageInputContainer.appendChild(sendButton);
40
39
 
41
40
  return messageInputContainer;
42
- };
41
+ };
@@ -1,33 +1,28 @@
1
1
  {{includeFile template="client/src/chat/messages.css.hbs" output="client/src/messages.css"}}
2
- import './messages.css';
3
- import {type ChatStore} from './chatStore';
2
+ {{addImport "import './messages.css';"}}
3
+ {{addImport "import {type ChatStore, type MessageRow} from './chatStore';"}}
4
4
 
5
5
  {{includeFile template="client/src/chat/message.ts.hbs" output="client/src/message.{{ext}}"}}
6
- import {createMessage} from './message';
6
+ {{addImport "import {createMessage} from './message';"}}
7
7
 
8
8
  export const createMessages = (store: ChatStore): HTMLDivElement => {
9
9
  const messagesContainer = document.createElement('div');
10
10
  messagesContainer.id = 'messages';
11
11
 
12
12
  const updateMessages = () => {
13
- const messageRows = store.getTable('messages');
14
- const sortedIds = Object.keys(messageRows).sort(
15
- (a, b) => messageRows[a].timestamp - messageRows[b].timestamp,
16
- );
13
+ const sortedIds = store.getSortedRowIds('messages', 'timestamp');
17
14
 
18
15
  messagesContainer.innerHTML = '';
19
16
  sortedIds.forEach((id) => {
20
- const msg = messageRows[id];
21
- const messageElement = createMessage(msg.username, msg.text, msg.timestamp);
17
+ const messageRow = store.getRow('messages', id) as MessageRow;
18
+ const messageElement = createMessage(messageRow);
22
19
  messagesContainer.appendChild(messageElement);
23
20
  });
24
21
  };
25
22
 
26
- // Initial render
27
23
  updateMessages();
28
24
 
29
- // Listen for changes
30
25
  store.addTablesListener(updateMessages);
31
26
 
32
27
  return messagesContainer;
33
- };
28
+ };
@@ -1,10 +1,11 @@
1
1
  {{#if schemas}}
2
- import {createStore} from 'tinybase/with-schemas';
2
+ {{addImport "import {createStore} from 'tinybase/with-schemas';"}}
3
3
  {{else}}
4
- import {createStore} from 'tinybase';
4
+ {{addImport "import {createStore} from 'tinybase';"}}
5
5
  {{/if}}
6
6
 
7
- const STORE_ID = 'settings';
7
+ {{includeFile template="client/src/chat/utils.ts.hbs" output="client/src/utils.ts"}}
8
+ {{addImport "import {getRandomName} from './utils';"}}
8
9
 
9
10
  {{#if schemas}}
10
11
  const VALUES_SCHEMA = {
@@ -13,7 +14,65 @@ const STORE_ID = 'settings';
13
14
 
14
15
  {{/if}}
15
16
  export const settingsStore = createStore(){{#if schemas}}
16
- .setValuesSchema(VALUES_SCHEMA){{/if}}
17
- .setValue('username', 'Carol');
17
+ .setValuesSchema(VALUES_SCHEMA){{/if}};
18
18
 
19
- export type SettingsStore = typeof settingsStore;
19
+ let resolveSettingsReady: () => void;
20
+ export const settingsStoreReady = new Promise<void>((resolve) => {
21
+ resolveSettingsReady = resolve;
22
+ });
23
+
24
+ {{#if persist}}
25
+ {{#if persistLocalStorage}}
26
+ {{#if schemas}}
27
+ {{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser/with-schemas';"}}
28
+ {{else}}
29
+ {{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser';"}}
30
+ {{/if}}
31
+
32
+ const settingsPersister = createLocalPersister(settingsStore, 'settings');
33
+ settingsPersister.load([{}, {username: getRandomName()}]).then(() => {
34
+ settingsPersister.startAutoSave();
35
+ resolveSettingsReady();
36
+ });
37
+ {{/if}}
38
+ {{#if persistSqlite}}
39
+ {{#if schemas}}
40
+ {{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm/with-schemas';"}}
41
+ {{else}}
42
+ {{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';"}}
43
+ {{/if}}
44
+ {{includeFile template="client/src/shared/sqlite.ts.hbs" output="client/src/sqlite.ts"}}
45
+ {{addImport "import {getDb} from './sqlite';"}}
46
+
47
+ getDb().then(({sqlite3, db}) => {
48
+ const settingsPersister = createSqliteWasmPersister(settingsStore, sqlite3, db, 'settings');
49
+ settingsPersister.load([{}, {username: getRandomName()}]).then(() => {
50
+ settingsPersister.startAutoSave();
51
+ resolveSettingsReady();
52
+ });
53
+ });
54
+ {{/if}}
55
+ {{#if persistPglite}}
56
+ {{#if schemas}}
57
+ {{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite/with-schemas';"}}
58
+ {{else}}
59
+ {{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite';"}}
60
+ {{/if}}
61
+ {{includeFile template="client/src/shared/pglite.ts.hbs" output="client/src/pglite.{{ext}}"}}
62
+ {{addImport "import {getPgLite} from './pglite';"}}
63
+
64
+ getPgLite().then((pgLite) => {
65
+ createPglitePersister(settingsStore, pgLite, 'settings').then((settingsPersister) => {
66
+ settingsPersister.load([{}, {username: getRandomName()}]).then(() => {
67
+ settingsPersister.startAutoSave();
68
+ resolveSettingsReady();
69
+ });
70
+ });
71
+ });
72
+ {{/if}}
73
+ {{else}}
74
+ settingsStore.setValue('username', getRandomName());
75
+ resolveSettingsReady();
76
+ {{/if}}
77
+
78
+ export type SettingsStore = typeof settingsStore;
@@ -11,4 +11,4 @@ white-space: nowrap;
11
11
 
12
12
  #usernameInput input {
13
13
  flex: 1;
14
- }
14
+ }
@@ -1,12 +1,12 @@
1
1
  {{includeFile template="client/src/chat/usernameInput.css.hbs" output="client/src/usernameInput.css"}}
2
- import './usernameInput.css';
3
- import {type SettingsStore} from './settingsStore';
4
- import {type ChatStore} from './chatStore';
2
+ {{addImport "import './usernameInput.css';"}}
3
+ {{addImport "import {type SettingsStore} from './settingsStore';"}}
4
+ {{addImport "import {type ChatStore} from './chatStore';"}}
5
5
 
6
6
  {{includeFile template="client/src/shared/input.ts.hbs" output="client/src/input.{{ext}}"}}
7
- import {createInput} from './input';
7
+ {{addImport "import {createInput} from './input';"}}
8
8
 
9
- export const createUsernameInput = (settingsStore: SettingsStore, chatStore: ChatStore): HTMLDivElement => {
9
+ export const createUsernameInput = (settingsStore: SettingsStore, _chatStore: ChatStore): HTMLDivElement => {
10
10
  const container = document.createElement('div');
11
11
  container.id = 'usernameInput';
12
12
 
@@ -27,4 +27,4 @@ usernameInput.value = settingsStore.getValue('username') as string;
27
27
  });
28
28
 
29
29
  return container;
30
- };
30
+ };
@@ -0,0 +1,26 @@
1
+ const NAMES = [
2
+ 'Carol',
3
+ 'Charlie',
4
+ 'Casey',
5
+ 'Cameron',
6
+ 'Chris',
7
+ 'Dana',
8
+ 'Dakota',
9
+ 'Drew',
10
+ 'Devon',
11
+ 'Dylan',
12
+ ];
13
+
14
+ export const getRandomName = (): string =>
15
+ NAMES[Math.floor(Math.random() * NAMES.length)];
16
+
17
+ export const getDefaultContent = (): [any, any] =>
18
+ [
19
+ {
20
+ messages: {
21
+ '1': {username: 'Alice', text: 'Hello!', timestamp: Date.now() - 60000},
22
+ '2': {username: 'Bob', text: 'Hi there!', timestamp: Date.now() - 30000},
23
+ },
24
+ },
25
+ {},
26
+ ];
@@ -1,36 +1,42 @@
1
- import {StrictMode} from 'react';
1
+ {{addImport "import {StrictMode, useState} from 'react';"}}
2
+ {{includeFile template="client/src/shared/Loading.tsx.hbs" output="client/src/Loading.{{ext}}"}}
3
+ {{addImport "import {Loading} from './Loading';"}}
2
4
 
3
- {{#if schemas}}
4
- import {Provider} from 'tinybase/ui-react/with-schemas';
5
- {{else}}
6
- import {Provider} from 'tinybase/ui-react';
7
- {{/if}}
8
- import {Inspector} from 'tinybase/ui-react-inspector';
5
+ {{addImport "import {Provider} from 'tinybase/ui-react';"}}
6
+ {{addImport "import {Inspector} from 'tinybase/ui-react-inspector';"}}
9
7
 
10
8
  {{includeFile template="client/src/drawing/SettingsStore.tsx.hbs" output="client/src/SettingsStore.{{ext}}"}}
11
- import {SettingsStore} from './SettingsStore';
9
+ {{addImport "import {SettingsStore} from './SettingsStore';"}}
12
10
 
13
11
  {{includeFile template="client/src/drawing/CanvasStore.tsx.hbs" output="client/src/CanvasStore.{{ext}}"}}
14
- import {CanvasStore} from './CanvasStore';
12
+ {{addImport "import {CanvasStore} from './CanvasStore';"}}
15
13
 
16
14
  {{includeFile template="client/src/drawing/DrawingControls.tsx.hbs" output="client/src/DrawingControls.{{ext}}"}}
17
- import {DrawingControls} from './DrawingControls';
15
+ {{addImport "import {DrawingControls} from './DrawingControls';"}}
18
16
 
19
17
  {{includeFile template="client/src/drawing/Canvas.tsx.hbs" output="client/src/Canvas.{{ext}}"}}
20
- import {Canvas} from './Canvas';
18
+ {{addImport "import {Canvas} from './Canvas';"}}
19
+
20
+ export const App = () => {
21
+ const [settingsReady, setSettingsReady] = useState(false);
22
+ const [canvasReady, setCanvasReady] = useState(false);
23
+ const loading = !settingsReady || !canvasReady;
21
24
 
22
- const App = () => {
23
25
  return (
24
26
  <StrictMode>
25
27
  <Provider>
26
- <SettingsStore />
27
- <CanvasStore />
28
- <DrawingControls />
29
- <Canvas />
30
- <Inspector />
28
+ <SettingsStore onReady={()=> setSettingsReady(true)} />
29
+ <CanvasStore onReady={()=> setCanvasReady(true)} />
30
+ {loading ? (
31
+ <Loading />
32
+ ) : (
33
+ <>
34
+ <DrawingControls />
35
+ <Canvas />
36
+ <Inspector />
37
+ </>
38
+ )}
31
39
  </Provider>
32
40
  </StrictMode>
33
41
  );
34
- };
35
-
36
- export {App};
42
+ };
@@ -1,22 +1,19 @@
1
1
  {{includeFile template="client/src/drawing/brushSize.css.hbs" output="client/src/brushSize.css"}}
2
- import {useValue, useSetValueCallback, STORE_ID} from './SettingsStore';
3
- import './brushSize.css';
2
+ {{addImport "import {useValueState, STORE_ID} from './SettingsStore';"}}
3
+ {{addImport "import './brushSize.css';"}}
4
4
 
5
5
  export const BrushSize = () => {
6
- const size = useValue('brushSize', STORE_ID) ?? 5;
6
+ const [size, setSize] = useValueState('brushSize', STORE_ID);
7
7
 
8
- const setSize = useSetValueCallback(
9
- 'brushSize',
10
- (e: React.ChangeEvent<HTMLInputElement>) => parseInt(e.target.value),
11
- [],
12
- STORE_ID,
13
- );
8
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
9
+ setSize(parseInt(e.target.value));
10
+ };
14
11
 
15
12
  return (
16
13
  <div id="brushSize">
17
14
  <label>Size:</label>
18
- <input type="range" min="1" max="50" value={size} onChange={setSize} />
15
+ <input type="range" min="1" max="50" value={(size as number) ?? 5} onChange={handleChange} />
19
16
  <span id="brushSizeValue">{size}</span>
20
17
  </div>
21
18
  );
22
- };
19
+ };
@@ -1,16 +1,18 @@
1
1
  {{includeFile template="client/src/drawing/canvas.css.hbs" output="client/src/canvas.css"}}
2
- import {useCallback, useEffect, useRef} from 'react';
3
- import {useTable, useStore, STORE_ID as CANVAS_STORE_ID} from './CanvasStore';
4
- import {useValues, STORE_ID as SETTINGS_STORE_ID} from './SettingsStore';
5
- import {getUniqueId} from 'tinybase';
6
- import './canvas.css';
2
+ {{addImport "import {useCallback, useEffect, useRef} from 'react';"}}
3
+ {{addImport "import {useSortedRowIds, useTable, useStore, type StrokeRow, STORE_ID as CANVAS_STORE_ID} from './CanvasStore';"}}
4
+ {{addImport "import {useValues, STORE_ID as SETTINGS_STORE_ID} from './SettingsStore';"}}
5
+ {{addImport "import {getHlcFunctions} from 'tinybase';"}}
6
+ {{addImport "import './canvas.css';"}}
7
+
8
+ const [getNextHlc] = getHlcFunctions();
7
9
 
8
10
  export const Canvas = () => {
9
11
  const canvasRef = useRef<HTMLCanvasElement>(null);
10
12
  const isDrawing = useRef(false);
11
13
  const currentStrokeId = useRef<string | null>(null);
12
- const pointIndex = useRef(0);
13
- const brush = useValues(SETTINGS_STORE_ID);
14
+ const {brushColor, brushSize} = useValues(SETTINGS_STORE_ID);
15
+ const sortedIds = useSortedRowIds('strokes', undefined, false, 0, undefined, CANVAS_STORE_ID);
14
16
  const strokes = useTable('strokes', CANVAS_STORE_ID);
15
17
  const store = useStore(CANVAS_STORE_ID);
16
18
 
@@ -24,77 +26,67 @@ const canvasRef = useRef<HTMLCanvasElement>(null);
24
26
  ctx.fillStyle = '#111';
25
27
  ctx.fillRect(0, 0, canvas.width, canvas.height);
26
28
 
27
- Object.entries(strokes).forEach(([id, stroke]) => {
28
- if (stroke) {
29
- const points: Array<{x: number; y: number}> = [];
30
- let i = 0;
31
- while (stroke[`x${i}`] !== undefined && stroke[`y${i}`] !== undefined) {
32
- points.push({x: stroke[`x${i}`] as number, y: stroke[`y${i}`] as number});
33
- i++;
34
- }
35
- if (points.length > 0) {
36
- ctx.strokeStyle = stroke.color as string;
37
- ctx.lineWidth = (stroke.size as number) * 2;
38
- ctx.lineCap = 'round';
39
- ctx.lineJoin = 'round';
40
- ctx.beginPath();
41
- ctx.moveTo(points[0].x, points[0].y);
42
- for (let i = 1; i < points.length; i++) { ctx.lineTo(points[i].x, points[i].y); } ctx.stroke(); } } }); }, [strokes]); useEffect(()=> {
43
- draw();
44
- }, [draw]);
45
-
46
- const getPoint = (e: MouseEvent | TouchEvent) => {
47
- const canvas = canvasRef.current;
48
- if (!canvas) return null;
29
+ sortedIds.forEach((id) => {
30
+ const stroke = store?.getRow('strokes', id) as StrokeRow;
31
+ if (stroke?.points) {
32
+ const pointsArray = JSON.parse(stroke.points) as number[];
33
+ if (pointsArray.length >= 2) {
34
+ ctx.strokeStyle = stroke.color;
35
+ ctx.lineWidth = stroke.size * 2;
36
+ ctx.lineCap = 'round';
37
+ ctx.lineJoin = 'round';
38
+ ctx.beginPath();
39
+ ctx.moveTo(pointsArray[0], pointsArray[1]);
40
+ for (let i = 2; i < pointsArray.length; i +=2) { ctx.lineTo(pointsArray[i], pointsArray[i + 1]); } ctx.stroke(); } } }); }, [sortedIds, strokes, store]); useEffect(()=> {
41
+ draw();
42
+ }, [draw]);
49
43
 
50
- const rect = canvas.getBoundingClientRect();
51
- const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
52
- const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
44
+ const getPoint = (e: MouseEvent | TouchEvent) => {
45
+ const canvas = canvasRef.current;
46
+ if (!canvas) return null;
53
47
 
54
- return {
55
- x: clientX - rect.left,
56
- y: clientY - rect.top,
57
- };
58
- };
48
+ const rect = canvas.getBoundingClientRect();
49
+ const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
50
+ const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
59
51
 
60
- const handleStart = (e: React.MouseEvent | React.TouchEvent) => {
61
- isDrawing.current = true;
62
- pointIndex.current = 0;
63
- currentStrokeId.current = getUniqueId();
52
+ return {
53
+ x: clientX - rect.left,
54
+ y: clientY - rect.top,
55
+ };
56
+ };
64
57
 
65
- store.setRow('strokes', currentStrokeId.current, {
66
- color: brush.brushColor,
67
- size: brush.brushSize,
68
- });
58
+ const handleStart = (e: React.MouseEvent | React.TouchEvent) => {
59
+ isDrawing.current = true;
60
+ currentStrokeId.current = getNextHlc();
69
61
 
70
- const point = getPoint(e.nativeEvent);
71
- if (point) {
72
- store.setCell('strokes', currentStrokeId.current, 'x0', point.x);
73
- store.setCell('strokes', currentStrokeId.current, 'y0', point.y);
74
- pointIndex.current = 1;
75
- }
76
- };
62
+ const point = getPoint(e.nativeEvent);
63
+ if (point && store) {
64
+ store.setRow('strokes', currentStrokeId.current, {
65
+ color: brushColor,
66
+ size: brushSize,
67
+ points: JSON.stringify([point.x, point.y]),
68
+ });
69
+ }
70
+ };
77
71
 
78
- const handleMove = (e: React.MouseEvent | React.TouchEvent) => {
79
- if (isDrawing.current && currentStrokeId.current) {
80
- const point = getPoint(e.nativeEvent);
81
- if (point) {
82
- const idx = pointIndex.current;
83
- store.setCell('strokes', currentStrokeId.current, `x${idx}`, point.x);
84
- store.setCell('strokes', currentStrokeId.current, `y${idx}`, point.y);
85
- pointIndex.current++;
86
- }
87
- }
88
- };
72
+ const handleMove = (e: React.MouseEvent | React.TouchEvent) => {
73
+ if (isDrawing.current && currentStrokeId.current && store) {
74
+ const point = getPoint(e.nativeEvent);
75
+ if (point) {
76
+ const pointsArray = JSON.parse(store.getCell('strokes', currentStrokeId.current, 'points') as string ?? '[]');
77
+ pointsArray.push(point.x, point.y);
78
+ store.setCell('strokes', currentStrokeId.current, 'points', JSON.stringify(pointsArray));
79
+ }
80
+ }
81
+ };
89
82
 
90
- const handleEnd = () => {
91
- isDrawing.current = false;
92
- currentStrokeId.current = null;
93
- pointIndex.current = 0;
94
- };
83
+ const handleEnd = () => {
84
+ isDrawing.current = false;
85
+ currentStrokeId.current = null;
86
+ };
95
87
 
96
- return (
97
- <canvas ref={canvasRef} width={600} height={400} id="drawingCanvas" onMouseDown={handleStart} onMouseMove={handleMove} onMouseUp={handleEnd} onMouseLeave={handleEnd} onTouchStart={handleStart} onTouchMove={handleMove}
98
- onTouchEnd={handleEnd} />
99
- );
100
- };
88
+ return (
89
+ <canvas ref={canvasRef} width={600} height={400} id="drawingCanvas" onMouseDown={handleStart} onMouseMove={handleMove} onMouseUp={handleEnd} onMouseLeave={handleEnd} onTouchStart={handleStart} onTouchMove={handleMove}
90
+ onTouchEnd={handleEnd} />
91
+ );
92
+ };
@@ -1,48 +1,127 @@
1
1
  {{#if schemas}}
2
- import {createMergeableStore} from 'tinybase/with-schemas';
3
- import * as UiReact from 'tinybase/ui-react/with-schemas';
4
- import {type NoValuesSchema} from 'tinybase/with-schemas';
2
+ {{addImport "import {createMergeableStore, type Row} from 'tinybase/with-schemas';"}}
3
+ {{addImport "import * as UiReact from 'tinybase/ui-react/with-schemas';"}}
4
+ {{addImport "import {type NoValuesSchema} from 'tinybase/with-schemas';"}}
5
5
  {{else}}
6
- import {createMergeableStore} from 'tinybase';
7
- import {useCreateStore, useProvideStore, useAddRowCallback, useDelTableCallback, useRow, useRowIds, useStore, useTable} from 'tinybase/ui-react';
6
+ {{addImport "import {createMergeableStore} from 'tinybase';"}}
7
+ {{addImport "import {useCreateMergeableStore, useProvideStore, useDelTableCallback, useRow, useSortedRowIds, useStore, useTable} from 'tinybase/ui-react';"}}
8
8
  {{/if}}
9
- import {getUniqueId} from 'tinybase';
10
9
 
11
10
  export const STORE_ID = 'canvas';
12
11
 
13
12
  {{#if schemas}}
14
13
  const TABLES_SCHEMA = {
15
14
  strokes: {
16
- color: {type: 'string'},
17
- size: {type: 'number'},
15
+ color: {type: 'string', default: ''},
16
+ size: {type: 'number', default: 1},
17
+ points: {type: 'string', default: '[]'},
18
18
  },
19
19
  } as const;
20
20
 
21
21
  type Schemas = [typeof TABLES_SCHEMA, NoValuesSchema];
22
22
 
23
- const {useCreateStore, useProvideStore, useAddRowCallback, useDelTableCallback, useRow, useRowIds, useStore, useTable} = UiReact as UiReact.WithSchemas<Schemas>;
23
+ const {useCreateMergeableStore, {{#if persist}}useCreatePersister, {{/if}}useDelTableCallback, useProvideStore, useRow, useSortedRowIds, useStore, useTable} = UiReact as UiReact.WithSchemas<Schemas>;
24
24
  {{/if}}
25
25
 
26
- export {useAddRowCallback, useDelTableCallback, useRow, useRowIds, useStore, useTable};
26
+ export type StrokeRow = {{#if schemas}}Row<typeof TABLES_SCHEMA, 'strokes'>{{else}}{color: string; size: number; points: string}{{/if}};
27
27
 
28
- export const CanvasStore = () => {
29
- const store = useCreateStore(() =>
30
- createMergeableStore(STORE_ID){{#if schemas}}
28
+ export {useDelTableCallback, useRow, useSortedRowIds, useStore, useTable};
29
+
30
+ export const CanvasStore = ({onReady}: {onReady?: () => void}) => {
31
+ const store = useCreateMergeableStore(() =>
32
+ createMergeableStore(){{#if schemas}}
31
33
  .setTablesSchema(TABLES_SCHEMA){{/if}}
32
34
  .setDefaultContent([{strokes: {}}, {}]),
33
35
  );
34
36
 
35
37
  useProvideStore(STORE_ID, store);
36
38
 
39
+ {{#if persist}}
40
+ {{#if persistLocalStorage}}
41
+ {{#if schemas}}
42
+ {{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser/with-schemas';"}}
43
+ {{else}}
44
+ {{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser';"}}
45
+ {{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
46
+ {{/if}}
47
+
48
+ useCreatePersister(
49
+ store,
50
+ (store) => createLocalPersister(store, STORE_ID),
51
+ [],
52
+ async (persister) => {
53
+ await persister.load();
54
+ await persister.startAutoSave();
55
+ onReady?.();
56
+ },
57
+ );
58
+ {{/if}}
59
+ {{#if persistSqlite}}
60
+ {{#if schemas}}
61
+ {{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm/with-schemas';"}}
62
+ {{else}}
63
+ {{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';"}}
64
+ {{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
65
+ {{/if}}
66
+ {{includeFile template="client/src/shared/sqlite.ts.hbs" output="client/src/sqlite.{{ext}}"}}
67
+ {{addImport "import {getDb} from './sqlite';"}}
68
+
69
+ useCreatePersister(
70
+ store,
71
+ async (store) => {
72
+ const {sqlite3, db} = await getDb();
73
+ return createSqliteWasmPersister(store, sqlite3, db, STORE_ID);
74
+ },
75
+ [],
76
+ async (persister) => {
77
+ await persister.load();
78
+ await persister.startAutoSave();
79
+ onReady?.();
80
+ },
81
+ );
82
+ {{/if}}
83
+ {{#if persistPglite}}
84
+ {{#if schemas}}
85
+ {{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite/with-schemas';"}}
86
+ {{else}}
87
+ {{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite';"}}
88
+ {{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
89
+ {{/if}}
90
+ {{includeFile template="client/src/shared/pglite.ts.hbs" output="client/src/pglite.{{ext}}"}}
91
+ {{addImport "import {getPgLite} from './pglite';"}}
92
+
93
+ useCreatePersister(
94
+ store,
95
+ async (store) => {
96
+ const pgLite = await getPgLite();
97
+ return await createPglitePersister(store, pgLite, 'canvas');
98
+ },
99
+ [],
100
+ async (persister) => {
101
+ await persister.load();
102
+ await persister.startAutoSave();
103
+ onReady?.();
104
+ },
105
+ );
106
+ {{/if}}
107
+ {{/if}}
108
+
37
109
  {{#if sync}}
38
110
  {{includeFile template="client/src/shared/config.ts.hbs" output="client/src/config.ts"}}
39
111
  {{addImport "import {SERVER} from './config';"}}
40
112
  {{addImport "import ReconnectingWebSocket from 'reconnecting-websocket';"}}
41
- {{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
42
- {{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react';"}}
43
- {{addImport "import type {MergeableStore} from 'tinybase';"}}
113
+ {{#if schemas}}
114
+ {{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client/with-schemas';"}}
115
+ {{else}}
116
+ {{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
117
+ {{/if}}
118
+ {{#if schemas}}
119
+ {{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react/with-schemas';"}}
120
+ {{else}}
121
+ {{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react';"}}
122
+ {{/if}}
44
123
 
45
- useCreateSynchronizer(store, async (store: MergeableStore) => {
124
+ useCreateSynchronizer(store, async (store) => {
46
125
  const serverPathId = location.pathname;
47
126
  const synchronizer = await createWsSynchronizer(
48
127
  store,
@@ -51,12 +130,19 @@ useProvideStore(STORE_ID, store);
51
130
  await synchronizer.startSync();
52
131
 
53
132
  synchronizer.getWebSocket().addEventListener('open', () => {
54
- synchronizer.load().then(() => synchronizer.save());
133
+ synchronizer.load().then(() => {
134
+ synchronizer.save();
135
+ {{#unless persist}}onReady?.();{{/unless}}
136
+ });
55
137
  });
56
138
 
57
139
  return synchronizer;
58
140
  });
59
141
  {{/if}}
60
142
 
143
+ {{#unless persist}}{{#unless sync}}
144
+ onReady?.();
145
+ {{/unless}}{{/unless}}
146
+
61
147
  return null;
62
148
  };