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 +60 -0
- package/bun.lockb +0 -0
- package/package.json +13 -13
- package/src/components/TasksProvider/index.tsx +287 -0
- package/src/index.ts +4 -0
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
|
|
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.
|
|
25
|
-
"@typescript-eslint/parser": "^8.
|
|
26
|
-
"cspell": "^8.
|
|
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.
|
|
30
|
-
"eslint-plugin-react-hooks": "^
|
|
31
|
-
"eslint-plugin-react-refresh": "^0.4.
|
|
32
|
-
"prettier": "^3.
|
|
33
|
-
"typescript": "^5.
|
|
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.
|
|
41
|
-
"lucide-react": "^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.
|
|
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';
|