tinywidgets 1.0.15 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # TinyWidgets
2
+
3
+ A collection of tiny, reusable, UI components
4
+ — wrapped in a helpful app layout with header, side bar, dark mode, and more.
5
+
6
+ See the [website](https://tinywidgets.org/) for more details. Get started [here](https://tinywidgets.org/#installation).
7
+
8
+ <img width="1213" alt="image" src="https://github.com/user-attachments/assets/22b69545-52d5-424d-ab80-084bb87237f4">
9
+
10
+ ---
11
+
12
+ ## Dependencies
13
+
14
+ TinyWidgets uses [React](https://react.dev/) for DOM manipulation, [Vanilla-Extract](https://vanilla-extract.style/) at build-time for styling, [Lucide](https://lucide.dev/) for icons, and [TinyBase](https://tinybase.org/) for state management. Its philosophy is all about simplicity, decent defaults, a streamlined DOM, and concise styling.
15
+
16
+ But, just to be clear - that doesn't mean you need to use Vanilla-Extract, Lucide, or Tinybase in the apps you build with these widgets. You can set arbitrary class names (from Tailwind, for example!) on all components if you like.
17
+
18
+ ## Help out!
19
+
20
+ This project was created because [I](https://github.com/jamesgpearce) want to be able to build lots of local-first apps quickly and without the overhead of all the app boilerplate each time.
21
+
22
+ Making it open source seemed like the right thing to do, so please try and it out and get involved. I'll always be interested in issues, more style variants, new components altogether, or even some professional-grade design assistance. I'm not a designer...
23
+
24
+ See you on [GitHub](https://github.com/tinyplex/tinywidgets)!
25
+
26
+ ## Installation
27
+
28
+ The easiest way to get started with TinyWidgets is to use its [Vite template](https://github.com/tinyplex/vite-tinywidgets/). This comes with the (simple) build configuration you need to work with TinyWidgets.
29
+
30
+ To create a new TinyWidgets application using this template, do the following:
31
+
32
+ 1. Make a copy of this template into a new directory:
33
+
34
+ ```sh
35
+ npx tiged tinyplex/vite-tinywidgets my-tinywidgets-app
36
+ ```
37
+
38
+ 2. Go into the directory:
39
+
40
+ ```sh
41
+ cd my-tinywidgets-app
42
+ ```
43
+
44
+ 3. Install the dependencies:
45
+
46
+ ```sh
47
+ npm install
48
+ ```
49
+
50
+ 4. Run the application:
51
+
52
+ ```sh
53
+ npm run dev
54
+ ```
55
+
56
+ 5. The Vite server should start up. Go the URL shown and enjoy!
57
+
58
+ <img width="1160" alt="image" src="https://github.com/user-attachments/assets/073a26dc-4212-4ab0-b5a0-d968ac47342c">
59
+
60
+ Note that you can also create a production build with `npm run build`.
package/bun.lockb CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tinywidgets",
3
- "version": "1.0.15",
3
+ "version": "1.1.0",
4
4
  "author": "jamesgpearce",
5
5
  "repository": "github:tinyplex/tinywidgets",
6
6
  "license": "MIT",
@@ -16,31 +16,31 @@
16
16
  ],
17
17
  "type": "module",
18
18
  "scripts": {
19
- "prePublishPackage": "eslint . && cspell --quiet . && tsc"
19
+ "prePublishPackage": "eslint . && cspell --quiet . && tsc && cp ../README.md ./README.md"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/react": "^18.3.11",
23
23
  "@types/react-dom": "^18.3.0",
24
- "@typescript-eslint/eslint-plugin": "^8.8.0",
25
- "@typescript-eslint/parser": "^8.8.0",
26
- "cspell": "^8.14.4",
24
+ "@typescript-eslint/eslint-plugin": "^8.17.0",
25
+ "@typescript-eslint/parser": "^8.17.0",
26
+ "cspell": "^8.16.1",
27
27
  "eslint": "^8.57.0",
28
28
  "eslint-config-prettier": "^9.1.0",
29
- "eslint-plugin-react": "^7.37.1",
30
- "eslint-plugin-react-hooks": "^4.6.2",
31
- "eslint-plugin-react-refresh": "^0.4.12",
32
- "prettier": "^3.3.3",
33
- "typescript": "^5.6.2"
29
+ "eslint-plugin-react": "^7.37.2",
30
+ "eslint-plugin-react-hooks": "^5.1.0",
31
+ "eslint-plugin-react-refresh": "^0.4.16",
32
+ "prettier": "^3.4.2",
33
+ "typescript": "^5.7.2"
34
34
  },
35
35
  "exports": {
36
36
  ".": "./src/index.ts",
37
37
  "./css": "./src/index.css.ts"
38
38
  },
39
39
  "dependencies": {
40
- "@vanilla-extract/css": "^1.16.0",
41
- "lucide-react": "^0.447.0",
40
+ "@vanilla-extract/css": "^1.16.1",
41
+ "lucide-react": "^0.468.0",
42
42
  "react": "^18.3.1",
43
43
  "react-dom": "^18.3.1",
44
- "tinybase": "^5.3.2"
44
+ "tinybase": "^5.4.2"
45
45
  }
46
46
  }
@@ -0,0 +1,287 @@
1
+ import * as UiReact from 'tinybase/ui-react/with-schemas';
2
+ import {
3
+ Id,
4
+ Store as StoreWithSchemas,
5
+ createStore,
6
+ } from 'tinybase/with-schemas';
7
+ import React, {
8
+ ReactNode,
9
+ createContext,
10
+ useCallback,
11
+ useContext,
12
+ useEffect,
13
+ useRef,
14
+ } from 'react';
15
+ import {
16
+ Provider as UiReactProvider,
17
+ useStore,
18
+ useStores,
19
+ } from 'tinybase/ui-react';
20
+ import {Persister} from 'tinybase/persisters/with-schemas';
21
+ import {Store} from 'tinybase';
22
+ import {createLocalPersister} from 'tinybase/persisters/persister-browser/with-schemas';
23
+
24
+ export type Task =
25
+ | ((
26
+ arg: string,
27
+ store: Store | undefined,
28
+ scheduleTask: ScheduleTask,
29
+ ) => void)
30
+ | ((
31
+ arg: string,
32
+ store: StoreWithSchemas<any> | undefined,
33
+ scheduleTask: ScheduleTask,
34
+ ) => void)
35
+ | ((
36
+ arg: string,
37
+ store: Store | undefined,
38
+ scheduleTask: ScheduleTask,
39
+ ) => Promise<void>)
40
+ | ((
41
+ arg: string,
42
+ store: StoreWithSchemas<any> | undefined,
43
+ scheduleTask: ScheduleTask,
44
+ ) => Promise<void>);
45
+ export type Tasks = {
46
+ [taskId: string]: Task;
47
+ };
48
+ export type ScheduleTask = (taskId: Id, arg?: string, storeId?: string) => void;
49
+
50
+ const STORE_ID = 'tinywidgets/Tasks';
51
+ const TABLE_ID = 'tasks';
52
+
53
+ const TABLES_SCHEMA = {
54
+ tasks: {
55
+ taskId: {type: 'string', default: ''},
56
+ arg: {type: 'string'},
57
+ storeId: {type: 'string'},
58
+ running: {type: 'boolean'},
59
+ expires: {type: 'number', default: 0},
60
+ },
61
+ } as const;
62
+ const VALUES_SCHEMA = {} as const;
63
+ type Schemas = [typeof TABLES_SCHEMA, typeof VALUES_SCHEMA];
64
+ const {useCreateStore, useCreatePersister} =
65
+ UiReact as UiReact.WithSchemas<Schemas>;
66
+
67
+ const Context = createContext<ScheduleTask>(() => {});
68
+
69
+ const TaskRunner = ({
70
+ tasks,
71
+ tasksStore,
72
+ interval,
73
+ }: {
74
+ readonly tasks: Tasks;
75
+ readonly tasksStore: StoreWithSchemas<Schemas>;
76
+ readonly interval: number;
77
+ }) => {
78
+ const store = useStore();
79
+ const stores = useStores();
80
+ const scheduleTask = useContext(Context);
81
+ useEffect(() => {
82
+ const nextJob = async () => {
83
+ const jobId = tasksStore.getRowIds(TABLE_ID)[0];
84
+ if (jobId != null) {
85
+ const {taskId, arg, storeId, running, expires} = tasksStore.getRow(
86
+ TABLE_ID,
87
+ jobId,
88
+ );
89
+ if (expires > Date.now()) {
90
+ const task = tasks[taskId];
91
+ if (task && !running) {
92
+ tasksStore.setCell(TABLE_ID, jobId, 'running', true);
93
+ try {
94
+ await task(
95
+ arg ?? '',
96
+ storeId == null ? store : (stores[storeId] as any),
97
+ scheduleTask,
98
+ );
99
+ } catch {}
100
+ tasksStore.delRow(TABLE_ID, jobId);
101
+ }
102
+ } else {
103
+ tasksStore.delRow(TABLE_ID, jobId);
104
+ }
105
+ }
106
+ };
107
+ const intervalHandle = setInterval(nextJob, interval * 1000);
108
+ return () => clearInterval(intervalHandle);
109
+ // eslint-disable-next-line react-hooks/exhaustive-deps
110
+ }, [tasksStore, JSON.stringify(Object.keys(stores))]);
111
+
112
+ return null;
113
+ };
114
+
115
+ /**
116
+ * The useScheduleTask hook returns a function that can be used to schedule a
117
+ * new task.
118
+ *
119
+ * The returned function takes the following arguments:
120
+ *
121
+ * - `taskId`: a required string to identify the task by Id.
122
+ * - `arg`: an optional string that will be passed to the task when run.
123
+ * - `storeId`: an optional string that will be used to look up a Store by Id
124
+ * in the current TinyBase Provider context and be passed to the task.
125
+ *
126
+ * @example
127
+ * ```tsx
128
+ * const TaskButton = () => {
129
+ * const scheduleTask = useScheduleTask();
130
+ * return (<Button
131
+ * title="Hello"
132
+ * onClick={() => scheduleTask('greet', 'Hello')}
133
+ * />);
134
+ * };
135
+ * // ...
136
+ * <TasksProvider tasks={{greet: (arg) => alert(arg)}}>
137
+ * <TaskButton />
138
+ * </TasksProvider>
139
+ * ```
140
+ * This example shows the hook returning a function that will schedule a task to
141
+ * be run by the provider. Note that it may take as long as a second (the
142
+ * default interval between task executions) for the alert to appear.
143
+ * @example
144
+ * ```tsx
145
+ * const TaskButton2 = () => {
146
+ * const scheduleTask = useScheduleTask();
147
+ * return (<Button
148
+ * iconRight={Lucide.Globe}
149
+ * title="Hello"
150
+ * onClick={() => scheduleTask('greet1', 'Hello')}
151
+ * />);
152
+ * };
153
+ * // ...
154
+ * <TasksProvider tasks={{
155
+ * greet1: async (arg, store, scheduleTask) => {
156
+ * scheduleTask('greet2', 'World');
157
+ * alert(arg);
158
+ * },
159
+ * greet2: (arg) => alert(arg)
160
+ * }}>
161
+ * <TaskButton2 />
162
+ * </TasksProvider>
163
+ * ```
164
+ * This example shows the hook returning a function that will schedule a task to
165
+ * be run by the provider. That task in turn will call another.
166
+ */
167
+ export const useScheduleTask = () => useContext(Context);
168
+
169
+ /**
170
+ * The `TasksProvider` component is a non-visual component that makes it easy to
171
+ * manage sequential tasks in your TinyWidget application.
172
+ *
173
+ * @param props The props for the component.
174
+ * @returns The TasksProvider component.
175
+ * @icon Lucide.FileClock
176
+ * @example
177
+ * ```tsx
178
+ * const TaskButton1 = () => {
179
+ * const scheduleTask = useScheduleTask();
180
+ * return (<Button
181
+ * iconRight={Lucide.Hand}
182
+ * title="Hello"
183
+ * onClick={() => scheduleTask('greet1')}
184
+ * />);
185
+ * };
186
+ * const TaskButton2 = () => {
187
+ * const scheduleTask = useScheduleTask();
188
+ * return (<Button
189
+ * iconRight={Lucide.Globe}
190
+ * title="World"
191
+ * onClick={() => scheduleTask('greet2')}
192
+ * />);
193
+ * };
194
+ * // ...
195
+ * <TasksProvider tasks={{
196
+ * greet1: () => alert('Hello'),
197
+ * greet2: () => alert('World'),
198
+ * }}>
199
+ * <TaskButton1 />
200
+ * <TaskButton2 />
201
+ * </TasksProvider>
202
+ * ```
203
+ * This example shows the hook returning a function that will schedule a task to
204
+ * be run by the provider. That task in turn will call another.
205
+ */
206
+ export const TasksProvider = ({
207
+ tasks,
208
+ interval = 1,
209
+ expiry = 5,
210
+ children,
211
+ }: {
212
+ /**
213
+ * An object listing all the tasks that can be executed, keyed by Id.
214
+ *
215
+ * Each task is a function that receives a string argument, a reference to a
216
+ * Store (each as specified when `scheduleTask` was called), and a reference
217
+ * to the `scheduleTask` function again so that tasks can be chained.
218
+ *
219
+ * A task can be asynchronous.
220
+ */
221
+ readonly tasks: Tasks;
222
+ /**
223
+ * The interval in seconds between each task execution, defaulting to 1.
224
+ */
225
+ readonly interval?: number;
226
+ /**
227
+ * The time in seconds from scheduling until a task expires and will not be
228
+ * started, defaulting to 5.
229
+ */
230
+ readonly expiry?: number;
231
+ /**
232
+ * The children of the component to be rendered.
233
+ */
234
+ readonly children: ReactNode;
235
+ }) => {
236
+ const pendingAddedJobs = useRef<
237
+ [taskId: string, arg?: string, storeId?: string][]
238
+ >([]);
239
+ const activePersister = useRef<Persister<Schemas> | undefined>();
240
+
241
+ const tasksStore = useCreateStore(() =>
242
+ createStore().setSchema(TABLES_SCHEMA, VALUES_SCHEMA),
243
+ );
244
+
245
+ useCreatePersister(
246
+ tasksStore,
247
+ (tasksStore) => createLocalPersister(tasksStore, STORE_ID),
248
+ [],
249
+ async (persister) => {
250
+ await persister.startAutoLoad();
251
+ tasksStore.forEachRow('tasks', (taskId, _) =>
252
+ tasksStore.delCell('tasks', taskId, 'running'),
253
+ );
254
+ await persister.startAutoSave();
255
+ activePersister.current = persister;
256
+ pendingAddedJobs.current.forEach((pendingAddedJob) =>
257
+ scheduleTask(...pendingAddedJob),
258
+ );
259
+ pendingAddedJobs.current.splice(0);
260
+ },
261
+ );
262
+
263
+ const scheduleTask: ScheduleTask = useCallback(
264
+ (taskId: string, arg?: string, storeId?: string) => {
265
+ if (activePersister.current) {
266
+ tasksStore.addRow(TABLE_ID, {
267
+ taskId,
268
+ expires: Date.now() + expiry * 1000,
269
+ arg,
270
+ storeId,
271
+ });
272
+ } else {
273
+ pendingAddedJobs.current.push([taskId, arg, storeId]);
274
+ }
275
+ },
276
+ [tasksStore, expiry],
277
+ );
278
+
279
+ return (
280
+ <Context.Provider value={scheduleTask}>
281
+ <UiReactProvider>
282
+ {children}
283
+ <TaskRunner tasks={tasks} tasksStore={tasksStore} interval={interval} />
284
+ </UiReactProvider>
285
+ </Context.Provider>
286
+ );
287
+ };
package/src/index.ts CHANGED
@@ -10,6 +10,10 @@ export {Metric} from './components/Metric/index.tsx';
10
10
  export {Row} from './components/Row/index.tsx';
11
11
  export {Summary} from './components/Summary/index.tsx';
12
12
  export {Tag} from './components/Tag/index.tsx';
13
+ export {
14
+ TasksProvider,
15
+ useScheduleTask,
16
+ } from './components/TasksProvider/index.tsx';
13
17
 
14
18
  export {useRoute, useSetRouteCallback} from './stores/RouteStore.tsx';
15
19
  export {useDark} from './stores/LocalStore.tsx';