@kbve/droid 0.0.6 → 0.1.2

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 (190) hide show
  1. package/droid.mjs +2312 -74
  2. package/index.d.ts +32 -0
  3. package/lib/agents/api-types.d.ts +121 -0
  4. package/lib/agents/api-types.d.ts.map +1 -0
  5. package/lib/agents/auth/init.d.ts +8 -0
  6. package/lib/agents/auth/init.d.ts.map +1 -0
  7. package/lib/agents/cache.d.ts +9 -0
  8. package/lib/agents/cache.d.ts.map +1 -0
  9. package/lib/agents/client/callGuildVault.d.ts +3 -0
  10. package/lib/agents/client/callGuildVault.d.ts.map +1 -0
  11. package/lib/agents/client/callJson.d.ts +3 -0
  12. package/lib/agents/client/callJson.d.ts.map +1 -0
  13. package/lib/agents/config.d.ts +36 -0
  14. package/lib/agents/config.d.ts.map +1 -0
  15. package/lib/agents/constants.d.ts +11 -0
  16. package/lib/agents/constants.d.ts.map +1 -0
  17. package/lib/agents/createAgents.d.ts +4 -0
  18. package/lib/agents/createAgents.d.ts.map +1 -0
  19. package/lib/agents/ctx.d.ts +46 -0
  20. package/lib/agents/ctx.d.ts.map +1 -0
  21. package/lib/agents/discord/bot.d.ts +21 -0
  22. package/lib/agents/discord/bot.d.ts.map +1 -0
  23. package/lib/agents/discord/guilds.d.ts +8 -0
  24. package/lib/agents/discord/guilds.d.ts.map +1 -0
  25. package/lib/agents/formDrafts.d.ts +5 -0
  26. package/lib/agents/formDrafts.d.ts.map +1 -0
  27. package/lib/agents/generated/agents-schema.d.ts +73 -0
  28. package/lib/agents/generated/agents-schema.d.ts.map +1 -0
  29. package/lib/agents/generated/discordsh-agents-schema.d.ts +71 -0
  30. package/lib/agents/generated/discordsh-agents-schema.d.ts.map +1 -0
  31. package/lib/agents/github/backfill.d.ts +23 -0
  32. package/lib/agents/github/backfill.d.ts.map +1 -0
  33. package/lib/agents/github/events.d.ts +10 -0
  34. package/lib/agents/github/events.d.ts.map +1 -0
  35. package/lib/agents/github/pat.d.ts +13 -0
  36. package/lib/agents/github/pat.d.ts.map +1 -0
  37. package/lib/agents/github/repoConfig.d.ts +25 -0
  38. package/lib/agents/github/repoConfig.d.ts.map +1 -0
  39. package/lib/agents/github/tokens.d.ts +22 -0
  40. package/lib/agents/github/tokens.d.ts.map +1 -0
  41. package/lib/agents/github/webhook.d.ts +37 -0
  42. package/lib/agents/github/webhook.d.ts.map +1 -0
  43. package/lib/agents/index.d.ts +8 -0
  44. package/lib/agents/index.d.ts.map +1 -0
  45. package/lib/agents/state/atoms.d.ts +122 -0
  46. package/lib/agents/state/atoms.d.ts.map +1 -0
  47. package/lib/agents/types.d.ts +65 -0
  48. package/lib/agents/types.d.ts.map +1 -0
  49. package/lib/gateway/SupabaseGateway.d.ts +38 -0
  50. package/lib/gateway/SupabaseGateway.d.ts.map +1 -0
  51. package/lib/gateway/WorkerCommunication.d.ts +39 -0
  52. package/lib/gateway/WorkerCommunication.d.ts.map +1 -0
  53. package/lib/gateway/WorkerPool.d.ts +55 -0
  54. package/lib/gateway/WorkerPool.d.ts.map +1 -0
  55. package/lib/gateway/capabilities.d.ts +23 -0
  56. package/lib/gateway/capabilities.d.ts.map +1 -0
  57. package/lib/gateway/index.d.ts +9 -0
  58. package/lib/gateway/index.d.ts.map +1 -0
  59. package/lib/gateway/strategies/DirectStrategy.d.ts +52 -0
  60. package/lib/gateway/strategies/DirectStrategy.d.ts.map +1 -0
  61. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts +46 -0
  62. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts.map +1 -0
  63. package/lib/gateway/strategies/WebWorkerStrategy.d.ts +45 -0
  64. package/lib/gateway/strategies/WebWorkerStrategy.d.ts.map +1 -0
  65. package/lib/gateway/types.d.ts +81 -0
  66. package/lib/gateway/types.d.ts.map +1 -0
  67. package/lib/workers/canvas-ui-renderer.d.ts +25 -0
  68. package/lib/workers/canvas-ui-renderer.d.ts.map +1 -0
  69. package/lib/workers/canvas-worker.d.ts +14 -0
  70. package/lib/workers/canvas-worker.d.ts.map +1 -0
  71. package/lib/workers/canvas-worker.js +5 -302
  72. package/lib/workers/data.d.ts +40 -0
  73. package/lib/workers/data.d.ts.map +1 -0
  74. package/lib/workers/db-worker.d.ts +69 -0
  75. package/lib/workers/db-worker.d.ts.map +1 -0
  76. package/lib/workers/db-worker.js +13 -3636
  77. package/lib/workers/events.d.ts +11 -0
  78. package/lib/workers/events.d.ts.map +1 -0
  79. package/lib/workers/flexbuilder.d.ts +4 -0
  80. package/lib/workers/flexbuilder.d.ts.map +1 -0
  81. package/lib/workers/init.d.ts +11 -0
  82. package/lib/workers/init.d.ts.map +1 -0
  83. package/lib/workers/main.d.ts +105 -0
  84. package/lib/workers/main.d.ts.map +1 -0
  85. package/lib/workers/supabase-db-worker.d.ts +2 -0
  86. package/lib/workers/supabase-db-worker.d.ts.map +1 -0
  87. package/lib/workers/supabase-db-worker.js +23 -0
  88. package/lib/workers/supabase-shared-worker.d.ts +2 -0
  89. package/lib/workers/supabase-shared-worker.d.ts.map +1 -0
  90. package/lib/workers/supabase-shared-worker.js +25 -0
  91. package/lib/workers/tools.d.ts +4 -0
  92. package/lib/workers/tools.d.ts.map +1 -0
  93. package/lib/workers/validate.d.ts +15 -0
  94. package/lib/workers/validate.d.ts.map +1 -0
  95. package/lib/workers/ws-worker.d.ts +10 -0
  96. package/lib/workers/ws-worker.d.ts.map +1 -0
  97. package/lib/workers/ws-worker.js +5 -273
  98. package/main.js +5711 -0
  99. package/package.json +18 -6
  100. package/src/index.ts +41 -13
  101. package/src/lib/agents/api-types.ts +170 -0
  102. package/src/lib/agents/auth/init.ts +200 -0
  103. package/src/lib/agents/cache.ts +127 -0
  104. package/src/lib/agents/client/callGuildVault.ts +71 -0
  105. package/src/lib/agents/client/callJson.ts +64 -0
  106. package/src/lib/agents/config.ts +83 -0
  107. package/src/lib/agents/constants.ts +11 -0
  108. package/src/lib/agents/createAgents.ts +62 -0
  109. package/src/lib/agents/ctx.ts +63 -0
  110. package/src/lib/agents/discord/bot.ts +169 -0
  111. package/src/lib/agents/discord/guilds.ts +106 -0
  112. package/src/lib/agents/formDrafts.ts +55 -0
  113. package/src/lib/agents/generated/agents-schema.ts +155 -0
  114. package/src/lib/agents/generated/discordsh-agents-schema.ts +96 -0
  115. package/src/lib/agents/github/backfill.ts +126 -0
  116. package/src/lib/agents/github/events.ts +131 -0
  117. package/src/lib/agents/github/pat.ts +150 -0
  118. package/src/lib/agents/github/repoConfig.ts +240 -0
  119. package/src/lib/agents/github/tokens.ts +222 -0
  120. package/src/lib/agents/github/webhook.ts +349 -0
  121. package/src/lib/agents/index.ts +35 -0
  122. package/src/lib/agents/state/atoms.ts +122 -0
  123. package/src/lib/agents/types.ts +83 -0
  124. package/src/lib/api.ts +71 -0
  125. package/src/lib/droid.spec.ts +14 -19
  126. package/src/lib/droid.ts +48 -4
  127. package/src/lib/gateway/SupabaseGateway.ts +236 -0
  128. package/src/lib/gateway/WorkerCommunication.ts +132 -0
  129. package/src/lib/gateway/WorkerPool.ts +304 -0
  130. package/src/lib/gateway/capabilities.spec.ts +144 -0
  131. package/src/lib/gateway/capabilities.ts +80 -0
  132. package/src/lib/gateway/index.ts +15 -0
  133. package/src/lib/gateway/strategies/DirectStrategy.ts +443 -0
  134. package/src/lib/gateway/strategies/SharedWorkerStrategy.ts +278 -0
  135. package/src/lib/gateway/strategies/WebWorkerStrategy.ts +278 -0
  136. package/src/lib/gateway/types.ts +139 -0
  137. package/src/lib/mod/mod-manager.ts +13 -8
  138. package/src/lib/mod/module/bento/mod-bento.worker.ts +1 -1
  139. package/src/lib/mod/module/supabase/mod-supabase.spec.ts +21 -25
  140. package/src/lib/mod/module/supabase/mod-supabase.ts +73 -0
  141. package/src/lib/mod/module/supabase/mod-supabase.worker.ts +1 -56
  142. package/src/lib/state/auth.spec.ts +58 -0
  143. package/src/lib/state/auth.ts +148 -0
  144. package/src/lib/state/bento.spec.ts +76 -0
  145. package/src/lib/state/bento.ts +51 -0
  146. package/src/lib/state/index.ts +98 -0
  147. package/src/lib/state/overlay-manager.ts +125 -0
  148. package/src/lib/state/profile-sync.ts +214 -0
  149. package/src/lib/state/profile.ts +155 -0
  150. package/src/lib/state/router.ts +24 -0
  151. package/src/lib/state/staff.ts +128 -0
  152. package/src/lib/state/sync-bus.ts +109 -0
  153. package/src/lib/state/theme-sync.ts +75 -0
  154. package/src/lib/state/toasts.spec.ts +106 -0
  155. package/src/lib/state/toasts.ts +34 -0
  156. package/src/lib/state/ui.spec.ts +148 -0
  157. package/src/lib/state/ui.ts +45 -0
  158. package/src/lib/state/welcome-toast.spec.ts +101 -0
  159. package/src/lib/state/welcome-toast.ts +76 -0
  160. package/src/lib/sw-recovery.spec.ts +165 -0
  161. package/src/lib/sw-recovery.ts +141 -0
  162. package/src/lib/types/bento.spec.ts +116 -0
  163. package/src/lib/types/bento.ts +94 -55
  164. package/src/lib/types/event-schemas.spec.ts +120 -0
  165. package/src/lib/types/event-types.spec.ts +96 -0
  166. package/src/lib/types/event-types.ts +113 -3
  167. package/src/lib/types/modules.ts +12 -5
  168. package/src/lib/types/ui-event-types.ts +66 -0
  169. package/src/lib/workers/canvas-ui-renderer.ts +266 -0
  170. package/src/lib/workers/canvas-worker.ts +94 -9
  171. package/src/lib/workers/data.ts +64 -37
  172. package/src/lib/workers/db-worker.ts +105 -32
  173. package/src/lib/workers/events.spec.ts +208 -0
  174. package/src/lib/workers/events.ts +27 -14
  175. package/src/lib/workers/flexbuilder.ts +2 -2
  176. package/src/lib/workers/init.ts +25 -17
  177. package/src/lib/workers/main.ts +506 -327
  178. package/src/lib/workers/supabase-db-worker.ts +243 -0
  179. package/src/lib/workers/supabase-shared-worker.ts +607 -0
  180. package/src/lib/workers/validate.ts +64 -0
  181. package/src/lib/workers/ws-worker.spec.ts +41 -0
  182. package/src/lib/workers/ws-worker.ts +255 -31
  183. package/src/setup-vitest.ts +4 -2
  184. package/src/types.ts +30 -0
  185. package/src/vite-env.d.ts +1 -0
  186. package/workers/main.js +2 -1125
  187. package/comlink.js +0 -253
  188. package/index.js +0 -2972
  189. package/reference.js +0 -259
  190. package/src/types.d.ts +0 -31
@@ -0,0 +1,165 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { swRecovery } from './sw-recovery';
3
+
4
+ type Reg = {
5
+ scriptURL: string;
6
+ unregister: ReturnType<typeof vi.fn>;
7
+ };
8
+
9
+ function makeReg(scriptURL: string, unregisterReturn = true): Reg {
10
+ return {
11
+ scriptURL,
12
+ unregister: vi.fn().mockResolvedValue(unregisterReturn),
13
+ };
14
+ }
15
+
16
+ function installNavigator(regs: Reg[]) {
17
+ const registrations = regs.map((r) => ({
18
+ active: { scriptURL: r.scriptURL },
19
+ installing: null,
20
+ waiting: null,
21
+ unregister: r.unregister,
22
+ })) as unknown as ServiceWorkerRegistration[];
23
+
24
+ Object.defineProperty(globalThis, 'navigator', {
25
+ value: {
26
+ serviceWorker: {
27
+ getRegistrations: vi.fn().mockResolvedValue(registrations),
28
+ },
29
+ },
30
+ configurable: true,
31
+ writable: true,
32
+ });
33
+ }
34
+
35
+ function installCaches(keys: string[]) {
36
+ const cacheStore: Record<string, boolean> = Object.fromEntries(
37
+ keys.map((k) => [k, true]),
38
+ );
39
+ (globalThis as unknown as { caches: CacheStorage }).caches = {
40
+ keys: vi.fn().mockResolvedValue([...keys]),
41
+ delete: vi.fn(async (key: string) => {
42
+ const had = !!cacheStore[key];
43
+ delete cacheStore[key];
44
+ return had;
45
+ }),
46
+ match: vi.fn(),
47
+ has: vi.fn(),
48
+ open: vi.fn(),
49
+ } as unknown as CacheStorage;
50
+ }
51
+
52
+ function clearGlobals() {
53
+ delete (globalThis as unknown as { navigator?: unknown }).navigator;
54
+ delete (globalThis as unknown as { caches?: unknown }).caches;
55
+ }
56
+
57
+ beforeEach(() => {
58
+ clearGlobals();
59
+ vi.spyOn(console, 'info').mockImplementation(vi.fn());
60
+ vi.spyOn(console, 'warn').mockImplementation(vi.fn());
61
+ });
62
+
63
+ afterEach(() => {
64
+ clearGlobals();
65
+ vi.restoreAllMocks();
66
+ });
67
+
68
+ describe('swRecovery', () => {
69
+ it('returns unsupported when navigator has no serviceWorker', async () => {
70
+ Object.defineProperty(globalThis, 'navigator', {
71
+ value: {},
72
+ configurable: true,
73
+ writable: true,
74
+ });
75
+ const result = await swRecovery();
76
+ expect(result.supported).toBe(false);
77
+ expect(result.unregistered).toEqual([]);
78
+ });
79
+
80
+ it('unregisters all SWs when allowedScripts is empty', async () => {
81
+ const stale = makeReg('https://chat.kbve.com/sw.js');
82
+ const other = makeReg('https://chat.kbve.com/old-sw.js');
83
+ installNavigator([stale, other]);
84
+ installCaches(['a', 'b']);
85
+
86
+ const result = await swRecovery();
87
+ expect(result.supported).toBe(true);
88
+ expect(result.unregistered).toEqual([
89
+ 'https://chat.kbve.com/sw.js',
90
+ 'https://chat.kbve.com/old-sw.js',
91
+ ]);
92
+ expect(result.keptScripts).toEqual([]);
93
+ expect(result.cachesCleared).toEqual(['a', 'b']);
94
+ expect(stale.unregister).toHaveBeenCalledTimes(1);
95
+ expect(other.unregister).toHaveBeenCalledTimes(1);
96
+ });
97
+
98
+ it('keeps SWs whose absolute scriptURL is in allowedScripts', async () => {
99
+ const keep = makeReg('https://chat.kbve.com/sw.js');
100
+ const drop = makeReg('https://chat.kbve.com/legacy.js');
101
+ installNavigator([keep, drop]);
102
+ installCaches([]);
103
+
104
+ const result = await swRecovery({
105
+ allowedScripts: ['https://chat.kbve.com/sw.js'],
106
+ });
107
+ expect(result.keptScripts).toEqual(['https://chat.kbve.com/sw.js']);
108
+ expect(result.unregistered).toEqual([
109
+ 'https://chat.kbve.com/legacy.js',
110
+ ]);
111
+ expect(keep.unregister).not.toHaveBeenCalled();
112
+ expect(drop.unregister).toHaveBeenCalledTimes(1);
113
+ });
114
+
115
+ it('keeps SWs whose pathname matches an allowed pathname rule', async () => {
116
+ const keep = makeReg('https://chat.kbve.com/sw.js');
117
+ installNavigator([keep]);
118
+ installCaches([]);
119
+
120
+ const result = await swRecovery({
121
+ allowedScripts: ['/sw.js'],
122
+ });
123
+ expect(result.keptScripts).toEqual(['https://chat.kbve.com/sw.js']);
124
+ expect(keep.unregister).not.toHaveBeenCalled();
125
+ });
126
+
127
+ it('skips cache clearing when clearCaches is false', async () => {
128
+ installNavigator([makeReg('https://chat.kbve.com/old.js')]);
129
+ installCaches(['x']);
130
+
131
+ const result = await swRecovery({ clearCaches: false });
132
+ expect(result.cachesCleared).toEqual([]);
133
+ });
134
+
135
+ it('does not reload when nothing was removed', async () => {
136
+ installNavigator([]);
137
+ installCaches([]);
138
+
139
+ const reload = vi.fn();
140
+ Object.defineProperty(globalThis, 'window', {
141
+ value: { location: { reload } },
142
+ configurable: true,
143
+ writable: true,
144
+ });
145
+
146
+ const result = await swRecovery({ reloadOnCleanup: true });
147
+ expect(result.reloaded).toBe(false);
148
+ expect(reload).not.toHaveBeenCalled();
149
+ });
150
+
151
+ it('continues when an individual unregister rejects', async () => {
152
+ const ok = makeReg('https://chat.kbve.com/a.js');
153
+ const bad: Reg = {
154
+ scriptURL: 'https://chat.kbve.com/b.js',
155
+ unregister: vi.fn().mockRejectedValue(new Error('boom')),
156
+ };
157
+ installNavigator([ok, bad]);
158
+ installCaches([]);
159
+
160
+ const result = await swRecovery();
161
+ expect(result.unregistered).toEqual(['https://chat.kbve.com/a.js']);
162
+ expect(ok.unregister).toHaveBeenCalled();
163
+ expect(bad.unregister).toHaveBeenCalled();
164
+ });
165
+ });
@@ -0,0 +1,141 @@
1
+ export interface SwRecoveryOptions {
2
+ allowedScripts?: readonly string[];
3
+ clearCaches?: boolean;
4
+ reloadOnCleanup?: boolean;
5
+ logPrefix?: string;
6
+ }
7
+
8
+ export interface SwRecoveryResult {
9
+ supported: boolean;
10
+ unregistered: string[];
11
+ keptScripts: string[];
12
+ cachesCleared: string[];
13
+ reloaded: boolean;
14
+ }
15
+
16
+ const DEFAULT_OPTS: Required<Omit<SwRecoveryOptions, 'allowedScripts'>> & {
17
+ allowedScripts: readonly string[];
18
+ } = {
19
+ allowedScripts: [],
20
+ clearCaches: true,
21
+ reloadOnCleanup: false,
22
+ logPrefix: '[sw-recovery]',
23
+ };
24
+
25
+ function getScriptURL(reg: ServiceWorkerRegistration): string | null {
26
+ return (
27
+ reg.active?.scriptURL ??
28
+ reg.installing?.scriptURL ??
29
+ reg.waiting?.scriptURL ??
30
+ null
31
+ );
32
+ }
33
+
34
+ function scriptIsAllowed(
35
+ scriptURL: string | null,
36
+ allowed: readonly string[],
37
+ ): boolean {
38
+ if (!scriptURL) return false;
39
+ if (allowed.length === 0) return false;
40
+ return allowed.some((rule) => {
41
+ if (rule === scriptURL) return true;
42
+ try {
43
+ return new URL(scriptURL).pathname === rule;
44
+ } catch {
45
+ return false;
46
+ }
47
+ });
48
+ }
49
+
50
+ export async function swRecovery(
51
+ opts: SwRecoveryOptions = {},
52
+ ): Promise<SwRecoveryResult> {
53
+ const cfg = { ...DEFAULT_OPTS, ...opts };
54
+ const result: SwRecoveryResult = {
55
+ supported: false,
56
+ unregistered: [],
57
+ keptScripts: [],
58
+ cachesCleared: [],
59
+ reloaded: false,
60
+ };
61
+
62
+ if (
63
+ typeof navigator === 'undefined' ||
64
+ !('serviceWorker' in navigator) ||
65
+ typeof navigator.serviceWorker?.getRegistrations !== 'function'
66
+ ) {
67
+ return result;
68
+ }
69
+
70
+ result.supported = true;
71
+
72
+ let registrations: readonly ServiceWorkerRegistration[] = [];
73
+ try {
74
+ registrations = await navigator.serviceWorker.getRegistrations();
75
+ } catch (err) {
76
+ console.warn(`${cfg.logPrefix} getRegistrations failed:`, err);
77
+ return result;
78
+ }
79
+
80
+ for (const reg of registrations) {
81
+ const scriptURL = getScriptURL(reg);
82
+ if (scriptIsAllowed(scriptURL, cfg.allowedScripts)) {
83
+ result.keptScripts.push(scriptURL as string);
84
+ continue;
85
+ }
86
+
87
+ try {
88
+ const ok = await reg.unregister();
89
+ if (ok) {
90
+ result.unregistered.push(scriptURL ?? '<unknown>');
91
+ }
92
+ } catch (err) {
93
+ console.warn(
94
+ `${cfg.logPrefix} unregister failed for ${scriptURL}:`,
95
+ err,
96
+ );
97
+ }
98
+ }
99
+
100
+ if (cfg.clearCaches && typeof caches !== 'undefined') {
101
+ try {
102
+ const keys = await caches.keys();
103
+ for (const key of keys) {
104
+ try {
105
+ const ok = await caches.delete(key);
106
+ if (ok) result.cachesCleared.push(key);
107
+ } catch (err) {
108
+ console.warn(
109
+ `${cfg.logPrefix} cache delete failed for ${key}:`,
110
+ err,
111
+ );
112
+ }
113
+ }
114
+ } catch (err) {
115
+ console.warn(`${cfg.logPrefix} caches.keys failed:`, err);
116
+ }
117
+ }
118
+
119
+ const removedAnything =
120
+ result.unregistered.length > 0 || result.cachesCleared.length > 0;
121
+
122
+ if (removedAnything) {
123
+ console.info(`${cfg.logPrefix} cleaned`, {
124
+ unregistered: result.unregistered,
125
+ kept: result.keptScripts,
126
+ caches: result.cachesCleared,
127
+ });
128
+ }
129
+
130
+ if (
131
+ cfg.reloadOnCleanup &&
132
+ removedAnything &&
133
+ typeof window !== 'undefined' &&
134
+ typeof window.location?.reload === 'function'
135
+ ) {
136
+ result.reloaded = true;
137
+ window.location.reload();
138
+ }
139
+
140
+ return result;
141
+ }
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ BentoTileSchema,
4
+ BENTO_VARIANT_CLASS_MAP,
5
+ BENTO_ANIMATION_CLASS_MAP,
6
+ BENTO_BADGE_CLASS_MAP,
7
+ } from './bento';
8
+
9
+ const validTile = {
10
+ title: 'Test Tile',
11
+ primaryColor: 'blue-500',
12
+ secondaryColor: 'red-300',
13
+ };
14
+
15
+ describe('BentoTileSchema', () => {
16
+ it('accepts a minimal valid tile', () => {
17
+ const result = BentoTileSchema.safeParse(validTile);
18
+ expect(result.success).toBe(true);
19
+ });
20
+
21
+ it('rejects empty title', () => {
22
+ const result = BentoTileSchema.safeParse({
23
+ ...validTile,
24
+ title: '',
25
+ });
26
+ expect(result.success).toBe(false);
27
+ });
28
+
29
+ it('rejects invalid primary color format', () => {
30
+ const result = BentoTileSchema.safeParse({
31
+ ...validTile,
32
+ primaryColor: 'invalid',
33
+ });
34
+ expect(result.success).toBe(false);
35
+ });
36
+
37
+ it('accepts valid ULID id', () => {
38
+ const result = BentoTileSchema.safeParse({
39
+ ...validTile,
40
+ id: '01ARZ3NDEKTSV4RRFFQ69G5FAV',
41
+ });
42
+ expect(result.success).toBe(true);
43
+ });
44
+
45
+ it('rejects invalid ULID id', () => {
46
+ const result = BentoTileSchema.safeParse({
47
+ ...validTile,
48
+ id: 'not-a-ulid',
49
+ });
50
+ expect(result.success).toBe(false);
51
+ });
52
+
53
+ it('accepts valid span format', () => {
54
+ const result = BentoTileSchema.safeParse({
55
+ ...validTile,
56
+ span: 'col-span-2',
57
+ });
58
+ expect(result.success).toBe(true);
59
+ });
60
+
61
+ it('requires target when href is provided', () => {
62
+ const result = BentoTileSchema.safeParse({
63
+ ...validTile,
64
+ href: 'https://example.com',
65
+ });
66
+ expect(result.success).toBe(false);
67
+ });
68
+
69
+ it('accepts href with target', () => {
70
+ const result = BentoTileSchema.safeParse({
71
+ ...validTile,
72
+ href: 'https://example.com',
73
+ target: '_blank',
74
+ });
75
+ expect(result.success).toBe(true);
76
+ });
77
+
78
+ it('requires badge text when badgeType is set', () => {
79
+ const result = BentoTileSchema.safeParse({
80
+ ...validTile,
81
+ badgeType: 'success',
82
+ });
83
+ expect(result.success).toBe(false);
84
+ });
85
+
86
+ it('accepts badge with badgeType', () => {
87
+ const result = BentoTileSchema.safeParse({
88
+ ...validTile,
89
+ badge: 'New',
90
+ badgeType: 'success',
91
+ });
92
+ expect(result.success).toBe(true);
93
+ });
94
+ });
95
+
96
+ describe('Bento class maps', () => {
97
+ it('BENTO_VARIANT_CLASS_MAP has all variants', () => {
98
+ expect(BENTO_VARIANT_CLASS_MAP).toHaveProperty('default');
99
+ expect(BENTO_VARIANT_CLASS_MAP).toHaveProperty('minimal');
100
+ expect(BENTO_VARIANT_CLASS_MAP).toHaveProperty('image-heavy');
101
+ });
102
+
103
+ it('BENTO_ANIMATION_CLASS_MAP has all animations', () => {
104
+ expect(BENTO_ANIMATION_CLASS_MAP).toHaveProperty('fade-in');
105
+ expect(BENTO_ANIMATION_CLASS_MAP).toHaveProperty('slide-up');
106
+ expect(BENTO_ANIMATION_CLASS_MAP).toHaveProperty('none');
107
+ expect(BENTO_ANIMATION_CLASS_MAP['none']).toBe('');
108
+ });
109
+
110
+ it('BENTO_BADGE_CLASS_MAP has all badge types', () => {
111
+ expect(BENTO_BADGE_CLASS_MAP).toHaveProperty('default');
112
+ expect(BENTO_BADGE_CLASS_MAP).toHaveProperty('success');
113
+ expect(BENTO_BADGE_CLASS_MAP).toHaveProperty('warning');
114
+ expect(BENTO_BADGE_CLASS_MAP).toHaveProperty('error');
115
+ });
116
+ });
@@ -5,7 +5,12 @@ export const BENTO_VARIANTS = ['default', 'minimal', 'image-heavy'] as const;
5
5
  export const BENTO_ANIMATIONS = ['fade-in', 'slide-up', 'none'] as const;
6
6
  export const BENTO_TARGETS = ['_self', '_blank', '_parent', '_top'] as const;
7
7
  export const BENTO_ROLES = ['button', 'link', 'region', 'article'] as const;
8
- export const BENTO_BADGE_TYPES = ['default', 'success', 'warning', 'error'] as const;
8
+ export const BENTO_BADGE_TYPES = [
9
+ 'default',
10
+ 'success',
11
+ 'warning',
12
+ 'error',
13
+ ] as const;
9
14
 
10
15
  // Regex validators
11
16
  const tailwindColorRegex = /^[a-z]+-(100|200|300|400|500|600|700|800|900)$/;
@@ -13,68 +18,102 @@ const spanRegex = /^(?:(?:[a-z]+:)?(col|row)-span-\d+\s*)+$/;
13
18
  const ulidRegex = /^01[0-9A-HJKMNP-TV-Z]{24}$/;
14
19
 
15
20
  // Schema
16
- export const BentoTileSchema = z.object({
17
- id: z.string().regex(ulidRegex, 'Invalid ULID format').optional(),
18
-
19
- title: z.string().min(1, 'Title is required'),
20
- subtitle: z.string().optional(),
21
- description: z.string().optional(),
22
- span: z.string().regex(spanRegex, 'Invalid span format (e.g., col-span-2, row-span-1)').optional(),
23
-
24
- primaryColor: z.string().regex(tailwindColorRegex, 'Invalid Tailwind color format (e.g., blue-500)'),
25
- secondaryColor: z.string().regex(tailwindColorRegex, 'Invalid Tailwind color format (e.g., blue-500)'),
26
-
27
- icon: z.string().optional(),
28
- backgroundImage: z.string().url().optional(),
29
-
30
- onclick: z.string().optional(),
31
- href: z.string().url().optional(),
32
- target: z.enum(BENTO_TARGETS).optional(),
33
-
34
- ariaLabel: z.string().optional(),
35
- role: z.enum(BENTO_ROLES).optional(),
36
-
37
- variant: z.enum(BENTO_VARIANTS).optional(),
38
- animation: z.enum(BENTO_ANIMATIONS).optional(),
39
-
40
- badge: z.string().optional(),
41
- badgeType: z.enum(BENTO_BADGE_TYPES).optional(),
42
-
43
- priority: z.number().int().min(0).optional(),
44
- tooltip: z.string().optional(),
45
- disabled: z.boolean().optional(),
46
-
47
- meta: z.record(z.any()).optional(),
48
- dataset: z.record(z.string()).optional(),
49
- className: z.string().optional(),
50
- }).refine(
51
- (data) => !data.href || (data.href && data.target),
52
- { message: 'target is required when href is provided', path: ['target'] }
53
- ).refine(
54
- (data) => !data.badgeType || (data.badge && data.badge.length > 0),
55
- { message: 'badge must be provided if badgeType is set', path: ['badge'] }
56
- );
21
+ export const BentoTileSchema = z
22
+ .object({
23
+ id: z.string().regex(ulidRegex, 'Invalid ULID format').optional(),
24
+
25
+ title: z.string().min(1, 'Title is required'),
26
+ subtitle: z.string().optional(),
27
+ description: z.string().optional(),
28
+ span: z
29
+ .string()
30
+ .regex(
31
+ spanRegex,
32
+ 'Invalid span format (e.g., col-span-2, row-span-1)',
33
+ )
34
+ .optional(),
35
+
36
+ primaryColor: z
37
+ .string()
38
+ .regex(
39
+ tailwindColorRegex,
40
+ 'Invalid Tailwind color format (e.g., blue-500)',
41
+ ),
42
+ secondaryColor: z
43
+ .string()
44
+ .regex(
45
+ tailwindColorRegex,
46
+ 'Invalid Tailwind color format (e.g., blue-500)',
47
+ ),
48
+
49
+ icon: z.string().optional(),
50
+ backgroundImage: z.string().url().optional(),
51
+
52
+ onclick: z.string().optional(),
53
+ href: z.string().url().optional(),
54
+ target: z.enum(BENTO_TARGETS).optional(),
55
+
56
+ ariaLabel: z.string().optional(),
57
+ role: z.enum(BENTO_ROLES).optional(),
58
+
59
+ variant: z.enum(BENTO_VARIANTS).optional(),
60
+ animation: z.enum(BENTO_ANIMATIONS).optional(),
61
+
62
+ badge: z.string().optional(),
63
+ badgeType: z.enum(BENTO_BADGE_TYPES).optional(),
64
+
65
+ priority: z.number().int().min(0).optional(),
66
+ tooltip: z.string().optional(),
67
+ disabled: z.boolean().optional(),
68
+
69
+ meta: z.record(z.string(), z.any()).optional(),
70
+ dataset: z.record(z.string(), z.string()).optional(),
71
+ className: z.string().optional(),
72
+ })
73
+ .refine((data) => !data.href || (data.href && data.target), {
74
+ message: 'target is required when href is provided',
75
+ path: ['target'],
76
+ })
77
+ .refine(
78
+ (data) => !data.badgeType || (data.badge && data.badge.length > 0),
79
+ {
80
+ message: 'badge must be provided if badgeType is set',
81
+ path: ['badge'],
82
+ },
83
+ );
57
84
 
58
85
  // Types
59
86
  export type BentoTile = z.infer<typeof BentoTileSchema>;
60
87
  export type BentoVariantClass = { base: string; hover?: string };
61
88
 
62
89
  // Mappings
63
- export const BENTO_VARIANT_CLASS_MAP: Record<(typeof BENTO_VARIANTS)[number], BentoVariantClass> = {
64
- default: { base: 'p-4 flex flex-col justify-between' },
65
- minimal: { base: 'p-2 bg-opacity-30 backdrop-blur-sm', hover: 'bg-opacity-50' },
66
- 'image-heavy': { base: 'p-0 overflow-hidden', hover: 'scale-105' },
90
+ export const BENTO_VARIANT_CLASS_MAP: Record<
91
+ (typeof BENTO_VARIANTS)[number],
92
+ BentoVariantClass
93
+ > = {
94
+ default: { base: 'p-4 flex flex-col justify-between' },
95
+ minimal: {
96
+ base: 'p-2 bg-opacity-30 backdrop-blur-sm',
97
+ hover: 'bg-opacity-50',
98
+ },
99
+ 'image-heavy': { base: 'p-0 overflow-hidden', hover: 'scale-105' },
67
100
  };
68
101
 
69
- export const BENTO_ANIMATION_CLASS_MAP: Record<(typeof BENTO_ANIMATIONS)[number], string> = {
70
- 'fade-in': 'animate-fade-in',
71
- 'slide-up': 'animate-slide-up',
72
- none: '',
102
+ export const BENTO_ANIMATION_CLASS_MAP: Record<
103
+ (typeof BENTO_ANIMATIONS)[number],
104
+ string
105
+ > = {
106
+ 'fade-in': 'animate-fade-in',
107
+ 'slide-up': 'animate-slide-up',
108
+ none: '',
73
109
  };
74
110
 
75
- export const BENTO_BADGE_CLASS_MAP: Record<(typeof BENTO_BADGE_TYPES)[number], string> = {
76
- default: 'bg-white/30 text-white',
77
- success: 'bg-green-500/80 text-white',
78
- warning: 'bg-yellow-500/80 text-black',
79
- error: 'bg-red-500/80 text-white',
111
+ export const BENTO_BADGE_CLASS_MAP: Record<
112
+ (typeof BENTO_BADGE_TYPES)[number],
113
+ string
114
+ > = {
115
+ default: 'bg-white/30 text-white',
116
+ success: 'bg-green-500/80 text-white',
117
+ warning: 'bg-yellow-500/80 text-black',
118
+ error: 'bg-red-500/80 text-white',
80
119
  };