sibujs 1.0.0-beta.4 → 1.0.0-beta.5

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 (68) hide show
  1. package/README.md +18 -20
  2. package/dist/browser.cjs +6 -3
  3. package/dist/browser.js +17 -440
  4. package/dist/build.cjs +51 -10
  5. package/dist/build.js +10 -8
  6. package/dist/cdn.global.js +4 -4
  7. package/dist/chunk-2X5NDXNG.js +877 -0
  8. package/dist/chunk-353KB3DI.js +271 -0
  9. package/dist/chunk-6BCDNGIH.js +1839 -0
  10. package/dist/chunk-6BDUQJ7K.js +949 -0
  11. package/dist/chunk-6DJQF4Z7.js +33 -0
  12. package/dist/chunk-BS5EYKCJ.js +26 -0
  13. package/dist/chunk-CHJ27IGK.js +26 -0
  14. package/dist/chunk-DKXVN442.js +90 -0
  15. package/dist/chunk-DZZMIMGL.js +725 -0
  16. package/dist/chunk-E6BNBVMK.js +457 -0
  17. package/dist/chunk-GVDEIJ3G.js +291 -0
  18. package/dist/chunk-HGKEBAW3.js +63 -0
  19. package/dist/chunk-HJNW3HLI.js +255 -0
  20. package/dist/chunk-JLOASJXU.js +654 -0
  21. package/dist/chunk-K5ZUMYVS.js +89 -0
  22. package/dist/chunk-K7JUDY3C.js +364 -0
  23. package/dist/chunk-MWE3PK5S.js +551 -0
  24. package/dist/chunk-MXR7LXGC.js +1084 -0
  25. package/dist/chunk-QI6ZDDUR.js +35 -0
  26. package/dist/chunk-UKDBQVM3.js +346 -0
  27. package/dist/chunk-USPT25TC.js +365 -0
  28. package/dist/chunk-WGCQNOEQ.js +712 -0
  29. package/dist/chunk-XFW7B23S.js +282 -0
  30. package/dist/data.cjs +11 -3
  31. package/dist/data.js +26 -920
  32. package/dist/devtools.js +36 -1046
  33. package/dist/ecosystem.cjs +6 -3
  34. package/dist/ecosystem.d.cts +1 -14
  35. package/dist/ecosystem.d.ts +1 -14
  36. package/dist/ecosystem.js +16 -356
  37. package/dist/extras.cjs +1102 -998
  38. package/dist/extras.d.cts +15 -2356
  39. package/dist/extras.d.ts +15 -2356
  40. package/dist/extras.js +337 -4925
  41. package/dist/index.cjs +51 -10
  42. package/dist/index.d.cts +1 -1
  43. package/dist/index.d.ts +1 -1
  44. package/dist/index.js +14 -10
  45. package/dist/motion.js +21 -323
  46. package/dist/patterns.cjs +16 -5
  47. package/dist/patterns.d.cts +8 -0
  48. package/dist/patterns.d.ts +8 -0
  49. package/dist/patterns.js +15 -251
  50. package/dist/performance.js +36 -616
  51. package/dist/plugin-Bek4RhJY.d.cts +43 -0
  52. package/dist/plugin-Bek4RhJY.d.ts +43 -0
  53. package/dist/plugins.cjs +914 -36
  54. package/dist/plugins.d.cts +6 -2
  55. package/dist/plugins.d.ts +6 -2
  56. package/dist/plugins.js +106 -36
  57. package/dist/ssr-NFAIHWJJ.js +35 -0
  58. package/dist/ssr.cjs +17 -10
  59. package/dist/ssr.d.cts +209 -209
  60. package/dist/ssr.d.ts +209 -209
  61. package/dist/ssr.js +32 -685
  62. package/dist/startup-0Qv6aosO.d.cts +291 -0
  63. package/dist/startup-0Qv6aosO.d.ts +291 -0
  64. package/dist/ui.cjs +26 -13
  65. package/dist/ui.js +43 -826
  66. package/dist/widgets.cjs +42 -5
  67. package/dist/widgets.js +14 -531
  68. package/package.json +7 -2
package/README.md CHANGED
@@ -671,7 +671,7 @@ function App() {
671
671
  ${RouterLink({ to: "/", nodes: "Home" })}
672
672
  ${RouterLink({ to: "/about", nodes: "About" })}
673
673
  </nav>
674
- ${route()}
674
+ ${Route()}
675
675
  </div>`;
676
676
  }
677
677
 
@@ -751,22 +751,20 @@ can("RETRY"); // false (not in error state)
751
751
  ```ts
752
752
  import { form, required, email, minLength } from "sibujs/ui";
753
753
 
754
- const form = form({
755
- fields: {
756
- username: { initial: "", validators: [required(), minLength(3)] },
757
- email: { initial: "", validators: [required(), email()] },
758
- },
759
- onSubmit: (values) => api.register(values),
754
+ const myForm = form({
755
+ username: { initial: "", validators: [required(), minLength(3)] },
756
+ email: { initial: "", validators: [required(), email()] },
760
757
  });
761
758
 
762
759
  // Reactive getters
763
- form.fields.username.value();
764
- form.errors.username();
765
- form.isValid();
766
- form.isDirty();
760
+ myForm.fields.username.value();
761
+ myForm.fields.username.error();
762
+ myForm.isValid();
763
+ myForm.isDirty();
767
764
 
768
- // Handle submission
769
- form.handleSubmit();
765
+ // Handle submission (pass callback to handleSubmit)
766
+ const onSubmit = myForm.handleSubmit((values) => api.register(values));
767
+ // Attach to form: on: { submit: onSubmit }
770
768
  ```
771
769
 
772
770
  ### Global Store
@@ -780,7 +778,7 @@ const store = globalStore({
780
778
  increment: (state) => ({ ...state, count: state.count + 1 }),
781
779
  setUser: (state, user) => ({ ...state, user }),
782
780
  },
783
- middleware: [(action, state) => console.log(action, state)],
781
+ middleware: [(state, action, payload, next) => { console.log(action, state); next(); }],
784
782
  });
785
783
 
786
784
  store.dispatch("increment");
@@ -1418,7 +1416,7 @@ import {
1418
1416
  startTransition,
1419
1417
  deferredValue,
1420
1418
  transitionState,
1421
- id,
1419
+ uniqueId,
1422
1420
  scheduleUpdate,
1423
1421
  yieldToMain,
1424
1422
  processInChunks,
@@ -1437,7 +1435,7 @@ const deferredQuery = deferredValue(() => query());
1437
1435
  const [isPending, startTransition] = transitionState();
1438
1436
 
1439
1437
  // Unique IDs (SSR-safe)
1440
- const id = id(); // "sibu-0"
1438
+ const myId = uniqueId(); // "sibu-0"
1441
1439
  const labelId = uniqueId("label"); // "sibu-1-label"
1442
1440
 
1443
1441
  // Priority-based scheduling
@@ -1461,7 +1459,7 @@ await processInChunks(bigArray, (item) => processItem(item), 50);
1461
1459
  import {
1462
1460
  enableDebug,
1463
1461
  debugLog,
1464
- performance,
1462
+ perfTracker,
1465
1463
  measureRender,
1466
1464
  getPerformanceReport,
1467
1465
  checkLeaks,
@@ -1474,7 +1472,7 @@ debugLog("Counter", "increment", { value: 5 });
1474
1472
  const MeasuredList = measureRender("ItemList", ItemList);
1475
1473
 
1476
1474
  // Manual performance tracking
1477
- const perf = performance("search");
1475
+ const perf = perfTracker("search");
1478
1476
  perf.startMeasure();
1479
1477
  // ... expensive operation
1480
1478
  perf.endMeasure();
@@ -1557,7 +1555,7 @@ const theme = createTheme({ colors: { primary: "#007bff" } });
1557
1555
 
1558
1556
  ---
1559
1557
 
1560
- ## Build (`sibu/build`)
1558
+ ## Build (`sibujs/build`)
1561
1559
 
1562
1560
  Bundler plugins and deployment utilities.
1563
1561
 
@@ -1575,7 +1573,7 @@ Additional build utilities: CDN deployment, type declaration generation, bundle
1575
1573
 
1576
1574
  ---
1577
1575
 
1578
- ## Testing (`sibu/testing`)
1576
+ ## Testing (`sibujs/testing`)
1579
1577
 
1580
1578
  Component testing utilities, accessibility testing, E2E helpers, snapshot testing, and visual regression support. Works with Vitest, Jest, and Playwright.
1581
1579
 
package/dist/browser.cjs CHANGED
@@ -58,9 +58,12 @@ function track(effectFn, subscriber) {
58
58
  }
59
59
  subscriberStack[stackTop] = subscriber;
60
60
  currentSubscriber = subscriber;
61
- effectFn();
62
- stackTop--;
63
- currentSubscriber = stackTop >= 0 ? subscriberStack[stackTop] : null;
61
+ try {
62
+ effectFn();
63
+ } finally {
64
+ stackTop--;
65
+ currentSubscriber = stackTop >= 0 ? subscriberStack[stackTop] : null;
66
+ }
64
67
  return () => cleanup(subscriber);
65
68
  }
66
69
  function suspendTracking() {
package/dist/browser.js CHANGED
@@ -1,445 +1,22 @@
1
1
  import {
2
- effect
3
- } from "./chunk-G4AX3DQ4.js";
4
- import {
5
- batch,
6
- signal
7
- } from "./chunk-ONOO74UN.js";
2
+ battery,
3
+ clipboard,
4
+ colorScheme,
5
+ draggable,
6
+ dropZone,
7
+ geo,
8
+ idle,
9
+ media,
10
+ online,
11
+ permissions,
12
+ resize,
13
+ scroll,
14
+ title
15
+ } from "./chunk-E6BNBVMK.js";
16
+ import "./chunk-6DJQF4Z7.js";
17
+ import "./chunk-CHJ27IGK.js";
18
+ import "./chunk-K7JUDY3C.js";
8
19
  import "./chunk-MLKGABMK.js";
9
-
10
- // src/browser/media.ts
11
- function media(query) {
12
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
13
- const [matches2] = signal(false);
14
- return { matches: matches2, dispose: () => {
15
- } };
16
- }
17
- const mql = window.matchMedia(query);
18
- const [matches, setMatches] = signal(mql.matches);
19
- const handler = (event) => {
20
- setMatches(event.matches);
21
- };
22
- mql.addEventListener("change", handler);
23
- function dispose() {
24
- mql.removeEventListener("change", handler);
25
- }
26
- return { matches, dispose };
27
- }
28
-
29
- // src/browser/resize.ts
30
- function resize(target) {
31
- const [width, setWidth] = signal(0);
32
- const [height, setHeight] = signal(0);
33
- let observer = null;
34
- if (typeof window === "undefined" || typeof ResizeObserver === "undefined") {
35
- return { width, height, dispose: () => {
36
- } };
37
- }
38
- const cleanup = effect(() => {
39
- const el = target();
40
- if (observer) {
41
- observer.disconnect();
42
- observer = null;
43
- }
44
- if (!el) return;
45
- observer = new ResizeObserver((entries) => {
46
- const entry = entries[0];
47
- if (entry) {
48
- batch(() => {
49
- setWidth(entry.contentRect.width);
50
- setHeight(entry.contentRect.height);
51
- });
52
- }
53
- });
54
- observer.observe(el);
55
- });
56
- function dispose() {
57
- cleanup();
58
- if (observer) {
59
- observer.disconnect();
60
- observer = null;
61
- }
62
- }
63
- return { width, height, dispose };
64
- }
65
-
66
- // src/browser/scroll.ts
67
- function scroll(target) {
68
- const [x, setX] = signal(0);
69
- const [y, setY] = signal(0);
70
- const [isScrolling, setIsScrolling] = signal(false);
71
- let scrollTimer = null;
72
- if (typeof window === "undefined") {
73
- return { x, y, isScrolling, dispose: () => {
74
- } };
75
- }
76
- const handler = () => {
77
- const el = target ? target() : null;
78
- batch(() => {
79
- if (el) {
80
- setX(el.scrollLeft);
81
- setY(el.scrollTop);
82
- } else {
83
- setX(window.scrollX ?? window.pageXOffset ?? 0);
84
- setY(window.scrollY ?? window.pageYOffset ?? 0);
85
- }
86
- setIsScrolling(true);
87
- });
88
- if (scrollTimer !== null) clearTimeout(scrollTimer);
89
- scrollTimer = setTimeout(() => {
90
- setIsScrolling(false);
91
- scrollTimer = null;
92
- }, 150);
93
- };
94
- const scrollTarget = target ? target() : null;
95
- const eventTarget = scrollTarget || window;
96
- eventTarget.addEventListener("scroll", handler, { passive: true });
97
- function dispose() {
98
- eventTarget.removeEventListener("scroll", handler);
99
- if (scrollTimer !== null) {
100
- clearTimeout(scrollTimer);
101
- scrollTimer = null;
102
- }
103
- }
104
- return { x, y, isScrolling, dispose };
105
- }
106
-
107
- // src/browser/online.ts
108
- function online() {
109
- if (typeof window === "undefined" || typeof navigator === "undefined") {
110
- const [online3] = signal(true);
111
- return { online: online3, dispose: () => {
112
- } };
113
- }
114
- const [online2, setOnline] = signal(navigator.onLine);
115
- const onOnline = () => setOnline(true);
116
- const onOffline = () => setOnline(false);
117
- window.addEventListener("online", onOnline);
118
- window.addEventListener("offline", onOffline);
119
- function dispose() {
120
- window.removeEventListener("online", onOnline);
121
- window.removeEventListener("offline", onOffline);
122
- }
123
- return { online: online2, dispose };
124
- }
125
-
126
- // src/browser/geo.ts
127
- function geo(options) {
128
- const [latitude, setLatitude] = signal(null);
129
- const [longitude, setLongitude] = signal(null);
130
- const [accuracy, setAccuracy] = signal(null);
131
- const [error, setError] = signal(null);
132
- let watchId = null;
133
- if (typeof navigator !== "undefined" && navigator.geolocation) {
134
- watchId = navigator.geolocation.watchPosition(
135
- (position) => {
136
- batch(() => {
137
- setLatitude(position.coords.latitude);
138
- setLongitude(position.coords.longitude);
139
- setAccuracy(position.coords.accuracy);
140
- setError(null);
141
- });
142
- },
143
- (err) => {
144
- setError(err);
145
- },
146
- options
147
- );
148
- }
149
- function dispose() {
150
- if (watchId !== null && typeof navigator !== "undefined" && navigator.geolocation) {
151
- navigator.geolocation.clearWatch(watchId);
152
- watchId = null;
153
- }
154
- }
155
- return { latitude, longitude, accuracy, error, dispose };
156
- }
157
-
158
- // src/browser/battery.ts
159
- function battery() {
160
- const [level, setLevel] = signal(null);
161
- const [charging, setCharging] = signal(null);
162
- const [chargingTime, setChargingTime] = signal(null);
163
- const [dischargingTime, setDischargingTime] = signal(null);
164
- const [supported, setSupported] = signal(false);
165
- let battery2 = null;
166
- let onLevelChange = null;
167
- let onChargingChange = null;
168
- let onChargingTimeChange = null;
169
- let onDischargingTimeChange = null;
170
- let disposed = false;
171
- if (typeof navigator !== "undefined" && "getBattery" in navigator) {
172
- setSupported(true);
173
- navigator.getBattery().then((bm) => {
174
- if (disposed) return;
175
- battery2 = bm;
176
- batch(() => {
177
- setLevel(bm.level);
178
- setCharging(bm.charging);
179
- setChargingTime(bm.chargingTime);
180
- setDischargingTime(bm.dischargingTime);
181
- });
182
- onLevelChange = () => setLevel(bm.level);
183
- onChargingChange = () => setCharging(bm.charging);
184
- onChargingTimeChange = () => setChargingTime(bm.chargingTime);
185
- onDischargingTimeChange = () => setDischargingTime(bm.dischargingTime);
186
- bm.addEventListener("levelchange", onLevelChange);
187
- bm.addEventListener("chargingchange", onChargingChange);
188
- bm.addEventListener("chargingtimechange", onChargingTimeChange);
189
- bm.addEventListener("dischargingtimechange", onDischargingTimeChange);
190
- });
191
- }
192
- function dispose() {
193
- disposed = true;
194
- if (battery2) {
195
- if (onLevelChange) battery2.removeEventListener("levelchange", onLevelChange);
196
- if (onChargingChange) battery2.removeEventListener("chargingchange", onChargingChange);
197
- if (onChargingTimeChange) battery2.removeEventListener("chargingtimechange", onChargingTimeChange);
198
- if (onDischargingTimeChange) battery2.removeEventListener("dischargingtimechange", onDischargingTimeChange);
199
- battery2 = null;
200
- }
201
- }
202
- return { level, charging, chargingTime, dischargingTime, supported, dispose };
203
- }
204
-
205
- // src/browser/idle.ts
206
- var ACTIVITY_EVENTS = ["mousemove", "mousedown", "keydown", "touchstart", "scroll"];
207
- function idle(timeout = 6e4) {
208
- const [idle2, setIdle] = signal(false);
209
- if (typeof window === "undefined" || typeof document === "undefined") {
210
- return { idle: idle2, dispose: () => {
211
- } };
212
- }
213
- let timer = null;
214
- function resetTimer() {
215
- setIdle(false);
216
- if (timer !== null) clearTimeout(timer);
217
- timer = setTimeout(() => {
218
- setIdle(true);
219
- }, timeout);
220
- }
221
- for (const event of ACTIVITY_EVENTS) {
222
- document.addEventListener(event, resetTimer, { passive: true });
223
- }
224
- resetTimer();
225
- function dispose() {
226
- if (timer !== null) {
227
- clearTimeout(timer);
228
- timer = null;
229
- }
230
- for (const event of ACTIVITY_EVENTS) {
231
- document.removeEventListener(event, resetTimer);
232
- }
233
- }
234
- return { idle: idle2, dispose };
235
- }
236
-
237
- // src/browser/permissions.ts
238
- function permissions(name) {
239
- const [state, setState] = signal("prompt");
240
- let permissionStatus = null;
241
- let onChange = null;
242
- let disposed = false;
243
- if (typeof navigator === "undefined" || !navigator.permissions) {
244
- setState("unsupported");
245
- return { state, dispose: () => {
246
- } };
247
- }
248
- navigator.permissions.query({ name }).then((status) => {
249
- if (disposed) return;
250
- permissionStatus = status;
251
- setState(status.state);
252
- onChange = () => {
253
- setState(status.state);
254
- };
255
- status.addEventListener("change", onChange);
256
- }).catch(() => {
257
- setState("unsupported");
258
- });
259
- function dispose() {
260
- disposed = true;
261
- if (permissionStatus && onChange) {
262
- permissionStatus.removeEventListener("change", onChange);
263
- permissionStatus = null;
264
- onChange = null;
265
- }
266
- }
267
- return { state, dispose };
268
- }
269
-
270
- // src/browser/clipboard.ts
271
- function clipboard() {
272
- const [text, setText] = signal("");
273
- const [copied, setCopied] = signal(false);
274
- let copiedTimer = null;
275
- async function copy(value) {
276
- if (typeof navigator === "undefined" || !navigator.clipboard) {
277
- return;
278
- }
279
- await navigator.clipboard.writeText(value);
280
- setText(value);
281
- setCopied(true);
282
- if (copiedTimer !== null) clearTimeout(copiedTimer);
283
- copiedTimer = setTimeout(() => {
284
- setCopied(false);
285
- copiedTimer = null;
286
- }, 2e3);
287
- }
288
- function dispose() {
289
- if (copiedTimer !== null) {
290
- clearTimeout(copiedTimer);
291
- copiedTimer = null;
292
- }
293
- }
294
- return { text, copy, copied, dispose };
295
- }
296
-
297
- // src/browser/dragDrop.ts
298
- function draggable(element, data) {
299
- const [isDragging, setIsDragging] = signal(false);
300
- if (typeof window === "undefined") {
301
- return { isDragging, dispose: () => {
302
- } };
303
- }
304
- let currentEl = null;
305
- let onDragStart = null;
306
- let onDragEnd = null;
307
- const cleanup = effect(() => {
308
- if (currentEl && onDragStart && onDragEnd) {
309
- currentEl.removeEventListener("dragstart", onDragStart);
310
- currentEl.removeEventListener("dragend", onDragEnd);
311
- }
312
- const el = element();
313
- currentEl = el;
314
- if (!el) return;
315
- el.draggable = true;
316
- onDragStart = (e) => {
317
- setIsDragging(true);
318
- if (e.dataTransfer && data !== void 0) {
319
- e.dataTransfer.setData("application/json", JSON.stringify(data));
320
- }
321
- };
322
- onDragEnd = () => {
323
- setIsDragging(false);
324
- };
325
- el.addEventListener("dragstart", onDragStart);
326
- el.addEventListener("dragend", onDragEnd);
327
- });
328
- function dispose() {
329
- cleanup();
330
- if (currentEl && onDragStart && onDragEnd) {
331
- currentEl.removeEventListener("dragstart", onDragStart);
332
- currentEl.removeEventListener("dragend", onDragEnd);
333
- currentEl = null;
334
- }
335
- }
336
- return { isDragging, dispose };
337
- }
338
- function dropZone(element, options) {
339
- const [isOver, setIsOver] = signal(false);
340
- if (typeof window === "undefined") {
341
- return { isOver, dispose: () => {
342
- } };
343
- }
344
- let currentEl = null;
345
- let onDragOver = null;
346
- let onDragEnter = null;
347
- let onDragLeave = null;
348
- let onDrop = null;
349
- const cleanup = effect(() => {
350
- if (currentEl && onDragOver && onDragEnter && onDragLeave && onDrop) {
351
- currentEl.removeEventListener("dragover", onDragOver);
352
- currentEl.removeEventListener("dragenter", onDragEnter);
353
- currentEl.removeEventListener("dragleave", onDragLeave);
354
- currentEl.removeEventListener("drop", onDrop);
355
- }
356
- const el = element();
357
- currentEl = el;
358
- if (!el) return;
359
- onDragOver = (e) => {
360
- e.preventDefault();
361
- };
362
- onDragEnter = (e) => {
363
- e.preventDefault();
364
- setIsOver(true);
365
- };
366
- onDragLeave = () => {
367
- setIsOver(false);
368
- };
369
- onDrop = (e) => {
370
- e.preventDefault();
371
- setIsOver(false);
372
- let transferData = null;
373
- if (e.dataTransfer) {
374
- const raw = e.dataTransfer.getData("application/json");
375
- if (raw) {
376
- try {
377
- transferData = JSON.parse(raw);
378
- } catch {
379
- transferData = raw;
380
- }
381
- }
382
- }
383
- options.onDrop(transferData, e);
384
- };
385
- el.addEventListener("dragover", onDragOver);
386
- el.addEventListener("dragenter", onDragEnter);
387
- el.addEventListener("dragleave", onDragLeave);
388
- el.addEventListener("drop", onDrop);
389
- });
390
- function dispose() {
391
- cleanup();
392
- if (currentEl && onDragOver && onDragEnter && onDragLeave && onDrop) {
393
- currentEl.removeEventListener("dragover", onDragOver);
394
- currentEl.removeEventListener("dragenter", onDragEnter);
395
- currentEl.removeEventListener("dragleave", onDragLeave);
396
- currentEl.removeEventListener("drop", onDrop);
397
- currentEl = null;
398
- }
399
- }
400
- return { isOver, dispose };
401
- }
402
-
403
- // src/browser/title.ts
404
- function title(value) {
405
- if (typeof document === "undefined") {
406
- return () => {
407
- };
408
- }
409
- const previousTitle = document.title;
410
- if (typeof value === "function") {
411
- const cleanup = effect(() => {
412
- document.title = value();
413
- });
414
- return () => {
415
- cleanup();
416
- document.title = previousTitle;
417
- };
418
- }
419
- document.title = value;
420
- return () => {
421
- document.title = previousTitle;
422
- };
423
- }
424
-
425
- // src/browser/colorScheme.ts
426
- function colorScheme() {
427
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
428
- const [scheme2] = signal("light");
429
- return { scheme: scheme2, dispose: () => {
430
- } };
431
- }
432
- const mql = window.matchMedia("(prefers-color-scheme: dark)");
433
- const [scheme, setScheme] = signal(mql.matches ? "dark" : "light");
434
- const handler = (event) => {
435
- setScheme(event.matches ? "dark" : "light");
436
- };
437
- mql.addEventListener("change", handler);
438
- function dispose() {
439
- mql.removeEventListener("change", handler);
440
- }
441
- return { scheme, dispose };
442
- }
443
20
  export {
444
21
  battery,
445
22
  clipboard,