lytx 0.3.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.
Files changed (213) hide show
  1. package/.env.example +37 -0
  2. package/README.md +486 -0
  3. package/alchemy.run.ts +155 -0
  4. package/cli/bootstrap-admin.ts +284 -0
  5. package/cli/deploy-staging.ts +692 -0
  6. package/cli/import-events.ts +628 -0
  7. package/cli/import-sites.ts +518 -0
  8. package/cli/index.ts +609 -0
  9. package/cli/init-db.ts +269 -0
  10. package/cli/migrate-to-durable-objects.ts +564 -0
  11. package/cli/migration-worker.ts +300 -0
  12. package/cli/performance-test.ts +588 -0
  13. package/cli/pg/client.ts +4 -0
  14. package/cli/pg/new-site.ts +153 -0
  15. package/cli/rollback-durable-objects.ts +622 -0
  16. package/cli/seed-data.ts +459 -0
  17. package/cli/setup.js +18 -0
  18. package/cli/setup.ts +463 -0
  19. package/cli/validate-migration.ts +200 -0
  20. package/cli/wrangler-migration.jsonc +28 -0
  21. package/db/adapter.ts +166 -0
  22. package/db/analytics_engine/client.ts +0 -0
  23. package/db/analytics_engine/sites.ts +0 -0
  24. package/db/client.ts +16 -0
  25. package/db/d1/client.ts +8 -0
  26. package/db/d1/drizzle.config.ts +35 -0
  27. package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
  28. package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
  29. package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
  30. package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
  31. package/db/d1/migrations/0004_mute_stardust.sql +1 -0
  32. package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
  33. package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
  34. package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
  35. package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
  36. package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
  37. package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
  38. package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
  39. package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
  40. package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
  41. package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
  42. package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
  43. package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
  44. package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
  45. package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
  46. package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
  47. package/db/d1/migrations/meta/_journal.json +76 -0
  48. package/db/d1/schema.ts +407 -0
  49. package/db/d1/sites.ts +374 -0
  50. package/db/d1/teamAiUsage.ts +101 -0
  51. package/db/d1/teams.ts +127 -0
  52. package/db/durable/drizzle.config.ts +8 -0
  53. package/db/durable/durableObjectClient.ts +480 -0
  54. package/db/durable/events.ts +100 -0
  55. package/db/durable/migrations/0000_fair_bucky.sql +38 -0
  56. package/db/durable/migrations/meta/0000_snapshot.json +278 -0
  57. package/db/durable/migrations/meta/_journal.json +13 -0
  58. package/db/durable/migrations/migrations.js +10 -0
  59. package/db/durable/schema.ts +5 -0
  60. package/db/durable/siteDurableObject.ts +1352 -0
  61. package/db/durable/types.ts +53 -0
  62. package/db/postgres/client.ts +13 -0
  63. package/db/postgres/drizzle.config.ts +12 -0
  64. package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
  65. package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
  66. package/db/postgres/migrations/meta/_journal.json +13 -0
  67. package/db/postgres/schema.ts +145 -0
  68. package/db/postgres/sites.ts +118 -0
  69. package/db/tranformReports.ts +595 -0
  70. package/db/types.ts +55 -0
  71. package/endpoints/api_worker.tsx +1854 -0
  72. package/endpoints/site_do_worker.ts +11 -0
  73. package/index.d.ts +63 -0
  74. package/index.ts +83 -0
  75. package/lib/auth.ts +279 -0
  76. package/lib/geojson/world_countries.json +45307 -0
  77. package/lib/random_name.ts +41 -0
  78. package/lib/sendMail.ts +252 -0
  79. package/package.json +142 -0
  80. package/public/favicon.ico +0 -0
  81. package/public/images/android-chrome-192x192.png +0 -0
  82. package/public/images/android-chrome-512x512.png +0 -0
  83. package/public/images/apple-touch-icon.png +0 -0
  84. package/public/images/favicon-16x16.png +0 -0
  85. package/public/images/favicon-32x32.png +0 -0
  86. package/public/images/lytx_dark_dashboard.png +0 -0
  87. package/public/images/lytx_light_dashboard.png +0 -0
  88. package/public/images/safari-pinned-tab.svg +4 -0
  89. package/public/logo.png +0 -0
  90. package/public/site.webmanifest +26 -0
  91. package/public/sw.js +107 -0
  92. package/src/Document.tsx +86 -0
  93. package/src/api/ai_api.ts +1156 -0
  94. package/src/api/authMiddleware.ts +45 -0
  95. package/src/api/auth_api.ts +465 -0
  96. package/src/api/event_labels_api.ts +193 -0
  97. package/src/api/events_api.ts +210 -0
  98. package/src/api/queueWorker.ts +303 -0
  99. package/src/api/reports_api.ts +278 -0
  100. package/src/api/seed_api.ts +288 -0
  101. package/src/api/sites_api.ts +904 -0
  102. package/src/api/tag_api.ts +458 -0
  103. package/src/api/tag_api_v2.ts +289 -0
  104. package/src/api/team_api.ts +456 -0
  105. package/src/app/Dashboard.tsx +1339 -0
  106. package/src/app/Events.tsx +974 -0
  107. package/src/app/Explore.tsx +312 -0
  108. package/src/app/Layout.tsx +58 -0
  109. package/src/app/Settings.tsx +1302 -0
  110. package/src/app/components/DashboardCard.tsx +118 -0
  111. package/src/app/components/EditableCell.tsx +123 -0
  112. package/src/app/components/EventForm.tsx +93 -0
  113. package/src/app/components/MarketingFooter.tsx +49 -0
  114. package/src/app/components/MarketingNav.tsx +150 -0
  115. package/src/app/components/Nav.tsx +755 -0
  116. package/src/app/components/NewSiteSetup.tsx +298 -0
  117. package/src/app/components/SQLEditor.tsx +740 -0
  118. package/src/app/components/SiteSelector.tsx +126 -0
  119. package/src/app/components/SiteTag.tsx +42 -0
  120. package/src/app/components/SiteTagInstallCard.tsx +241 -0
  121. package/src/app/components/WorldMapCard.tsx +337 -0
  122. package/src/app/components/charts/ChartComponents.tsx +1481 -0
  123. package/src/app/components/charts/EventFunnel.tsx +45 -0
  124. package/src/app/components/charts/EventSummary.tsx +194 -0
  125. package/src/app/components/charts/SankeyFlows.tsx +72 -0
  126. package/src/app/components/marketing/CheckIcon.tsx +16 -0
  127. package/src/app/components/marketing/MarketingLayout.tsx +23 -0
  128. package/src/app/components/marketing/SectionHeading.tsx +35 -0
  129. package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
  130. package/src/app/components/reports/CreateReportStarter.tsx +74 -0
  131. package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
  132. package/src/app/components/reports/DashboardToolbar.tsx +154 -0
  133. package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
  134. package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
  135. package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
  136. package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
  137. package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
  138. package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
  139. package/src/app/components/reports/custom/chartPalettes.ts +18 -0
  140. package/src/app/components/reports/custom/types.ts +50 -0
  141. package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
  142. package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
  143. package/src/app/components/ui/AlertBanner.tsx +101 -0
  144. package/src/app/components/ui/Button.tsx +55 -0
  145. package/src/app/components/ui/Card.tsx +80 -0
  146. package/src/app/components/ui/Input.tsx +72 -0
  147. package/src/app/components/ui/Link.tsx +23 -0
  148. package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
  149. package/src/app/components/ui/ThemeToggle.tsx +54 -0
  150. package/src/app/constants.ts +6 -0
  151. package/src/app/headers.ts +33 -0
  152. package/src/app/providers/AuthProvider.tsx +189 -0
  153. package/src/app/providers/ClientProviders.tsx +18 -0
  154. package/src/app/providers/QueryProvider.tsx +23 -0
  155. package/src/app/providers/ThemeProvider.tsx +88 -0
  156. package/src/app/utils/chartThemes.ts +146 -0
  157. package/src/app/utils/keybinds.ts +96 -0
  158. package/src/app/utils/media.tsx +24 -0
  159. package/src/client.tsx +114 -0
  160. package/src/config/createLytxAppConfig.ts +252 -0
  161. package/src/config/resourceNames.ts +88 -0
  162. package/src/db/index.ts +67 -0
  163. package/src/index.css +285 -0
  164. package/src/lib/featureFlags.ts +69 -0
  165. package/src/pages/GetStarted.tsx +290 -0
  166. package/src/pages/Home.tsx +268 -0
  167. package/src/pages/Login.tsx +283 -0
  168. package/src/pages/PrivacyPolicy.tsx +120 -0
  169. package/src/pages/Signup.tsx +267 -0
  170. package/src/pages/TermsOfService.tsx +126 -0
  171. package/src/pages/VerifyEmail.tsx +56 -0
  172. package/src/session/durableObject.ts +7 -0
  173. package/src/session/siteSchema.ts +86 -0
  174. package/src/session/types.ts +36 -0
  175. package/src/templates/README.md +80 -0
  176. package/src/templates/cleanFunctions.js +44 -0
  177. package/src/templates/embedFunctions.js +52 -0
  178. package/src/templates/lytx-shared.ts +662 -0
  179. package/src/templates/lytxpixel-core.ts +144 -0
  180. package/src/templates/lytxpixel.ts +267 -0
  181. package/src/templates/lytxpixelBrowser.js +634 -0
  182. package/src/templates/lytxpixelBrowser.mjs +634 -0
  183. package/src/templates/parseData.js +12 -0
  184. package/src/templates/script.ts +31 -0
  185. package/src/templates/template.tsx +50 -0
  186. package/src/templates/test.js +3 -0
  187. package/src/templates/trackWebEvents.ts +177 -0
  188. package/src/templates/vendors/clickcease.ts +8 -0
  189. package/src/templates/vendors/google.ts +174 -0
  190. package/src/templates/vendors/linkedin.ts +23 -0
  191. package/src/templates/vendors/meta.ts +56 -0
  192. package/src/templates/vendors/quantcast.ts +22 -0
  193. package/src/templates/vendors/simplfi.ts +7 -0
  194. package/src/types/app-context.ts +16 -0
  195. package/src/utilities/dashboardParams.ts +188 -0
  196. package/src/utilities/dashboardQueries.ts +537 -0
  197. package/src/utilities/dashboardTransforms.ts +167 -0
  198. package/src/utilities/dataValidation.ts +414 -0
  199. package/src/utilities/detector.ts +73 -0
  200. package/src/utilities/encrypt.ts +103 -0
  201. package/src/utilities/index.ts +13 -0
  202. package/src/utilities/parser.ts +117 -0
  203. package/src/utilities/performanceMonitoring.ts +570 -0
  204. package/src/utilities/route_interuptors.ts +24 -0
  205. package/src/worker.tsx +675 -0
  206. package/tsconfig.json +78 -0
  207. package/types/env.d.ts +16 -0
  208. package/types/rw.d.ts +7 -0
  209. package/types/shims.d.ts +53 -0
  210. package/types/vite.d.ts +19 -0
  211. package/vite/vite-plugin-pixel-bundle.ts +126 -0
  212. package/vite.config.ts +53 -0
  213. package/worker-configuration.d.ts +8401 -0
@@ -0,0 +1,235 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo, useState, type ReactNode } from "react";
4
+ import {
5
+ DatePicker,
6
+ FilterModal,
7
+ HelpTooltip,
8
+ SpinnerIcon,
9
+ type DashboardFilters,
10
+ type DashboardNotice,
11
+ type DateRange,
12
+ } from "@/app/components/charts/ChartComponents";
13
+
14
+ type UseDashboardToolbarControlsParams = {
15
+ filters: DashboardFilters;
16
+ setFilters: React.Dispatch<React.SetStateAction<DashboardFilters>>;
17
+ timezone: string;
18
+ onNotify?: (notice: DashboardNotice) => void;
19
+ isUpdating?: boolean;
20
+ deviceTypeOptions?: string[];
21
+ countryOptions?: string[];
22
+ cityOptions?: string[];
23
+ regionOptions?: string[];
24
+ sourceOptions?: string[];
25
+ pageUrlOptions?: string[];
26
+ eventNameOptions?: string[];
27
+ };
28
+
29
+ type DashboardToolbarControlsResult = {
30
+ controls: ReactNode;
31
+ footer: ReactNode;
32
+ modal: ReactNode;
33
+ };
34
+
35
+ const getDateRangeDisplay = (dateRange: DateRange) => {
36
+ if (!dateRange.start || !dateRange.end) {
37
+ return "Loading dates...";
38
+ }
39
+ if (dateRange.preset) {
40
+ return dateRange.preset;
41
+ }
42
+ return `${dateRange.start} to ${dateRange.end}`;
43
+ };
44
+
45
+ export function useDashboardToolbarControls({
46
+ filters,
47
+ setFilters,
48
+ timezone,
49
+ onNotify,
50
+ isUpdating = false,
51
+ deviceTypeOptions = [],
52
+ countryOptions = [],
53
+ cityOptions = [],
54
+ regionOptions = [],
55
+ sourceOptions = [],
56
+ pageUrlOptions = [],
57
+ eventNameOptions = [],
58
+ }: UseDashboardToolbarControlsParams): DashboardToolbarControlsResult {
59
+ const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
60
+ const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
61
+
62
+ const handleFiltersChange = useCallback(
63
+ (nextFilters: DashboardFilters) => {
64
+ setFilters(nextFilters);
65
+
66
+ const hasAnyFilter =
67
+ !!nextFilters.deviceType ||
68
+ !!nextFilters.country ||
69
+ !!nextFilters.city ||
70
+ !!nextFilters.region ||
71
+ !!nextFilters.source ||
72
+ !!nextFilters.pageUrl ||
73
+ !!nextFilters.eventName;
74
+
75
+ onNotify?.({
76
+ type: "info",
77
+ message: hasAnyFilter ? "Filters updated." : "All filters cleared.",
78
+ });
79
+ },
80
+ [onNotify, setFilters],
81
+ );
82
+
83
+ const handleDateRangeChange = useCallback(
84
+ (nextDateRange: DateRange) => {
85
+ setFilters((prev) => ({ ...prev, dateRange: nextDateRange }));
86
+ onNotify?.({ type: "info", message: "Date range updated." });
87
+ },
88
+ [onNotify, setFilters],
89
+ );
90
+
91
+ const controls = useMemo<ReactNode>(
92
+ () => (
93
+ <>
94
+ <button
95
+ type="button"
96
+ onClick={() => setIsFilterModalOpen(true)}
97
+ aria-haspopup="dialog"
98
+ aria-expanded={isFilterModalOpen}
99
+ className="bg-(--theme-bg-secondary) hover:bg-(--theme-bg-tertiary) text-(--theme-text-primary) font-medium h-9 sm:h-auto px-2.5 sm:py-2 sm:px-4 text-sm rounded-md border border-(--theme-border-primary) transition-colors whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-(--theme-border-secondary)"
100
+ >
101
+ Filter
102
+ </button>
103
+
104
+ <div className="relative flex items-center gap-2">
105
+ <button
106
+ type="button"
107
+ onClick={() => setIsDatePickerOpen((prev) => !prev)}
108
+ aria-haspopup="dialog"
109
+ aria-expanded={isDatePickerOpen}
110
+ className="bg-(--theme-bg-secondary) hover:bg-(--theme-bg-tertiary) text-(--theme-text-primary) font-medium h-9 sm:h-auto px-2.5 sm:py-2 sm:px-4 text-sm rounded-md border border-(--theme-border-primary) cursor-pointer transition-colors flex items-center gap-1.5 sm:gap-2 whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-(--theme-border-secondary)"
111
+ >
112
+ <span className="truncate max-w-32 sm:max-w-56">{getDateRangeDisplay(filters.dateRange)}</span>
113
+ <svg
114
+ aria-hidden="true"
115
+ focusable="false"
116
+ className="w-4 h-4 shrink-0"
117
+ fill="none"
118
+ stroke="currentColor"
119
+ viewBox="0 0 24 24"
120
+ >
121
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
122
+ </svg>
123
+ </button>
124
+
125
+ <div className="hidden sm:flex">
126
+ <HelpTooltip text="Change the reporting period. Presets update immediately; custom dates apply when the picker closes." />
127
+ </div>
128
+
129
+ <DatePicker
130
+ dateRange={filters.dateRange}
131
+ onDateRangeChange={handleDateRangeChange}
132
+ isOpen={isDatePickerOpen}
133
+ onToggle={() => setIsDatePickerOpen((prev) => !prev)}
134
+ timezone={timezone}
135
+ />
136
+ </div>
137
+ </>
138
+ ),
139
+ [filters.dateRange, handleDateRangeChange, isDatePickerOpen, isFilterModalOpen, timezone],
140
+ );
141
+
142
+ const footer = useMemo<ReactNode>(
143
+ () => {
144
+ const chips: Array<{ label: string; key: keyof DashboardFilters }> = [];
145
+ if (filters.deviceType) chips.push({ label: `Device: ${filters.deviceType}`, key: "deviceType" });
146
+ if (filters.country) chips.push({ label: `Country: ${filters.country}`, key: "country" });
147
+ if (filters.region) chips.push({ label: `Region: ${filters.region}`, key: "region" });
148
+ if (filters.city) chips.push({ label: `City: ${filters.city}`, key: "city" });
149
+ if (filters.source) chips.push({ label: `Source: ${filters.source}`, key: "source" });
150
+ if (filters.pageUrl) chips.push({ label: `Page: ${filters.pageUrl}`, key: "pageUrl" });
151
+ if (filters.eventName) chips.push({ label: `Event: ${filters.eventName}`, key: "eventName" });
152
+
153
+ return (
154
+ <>
155
+ {chips.length > 0 ? (
156
+ <div className="flex flex-wrap items-center gap-1.5 mt-2">
157
+ {chips.map((chip) => (
158
+ <span
159
+ key={chip.key}
160
+ className="inline-flex items-center gap-1 rounded-full bg-(--theme-bg-tertiary) border border-(--theme-border-primary) px-2.5 py-0.5 text-xs text-(--theme-text-primary)"
161
+ >
162
+ {chip.label}
163
+ <button
164
+ type="button"
165
+ onClick={() => setFilters((prev) => ({ ...prev, [chip.key]: undefined }))}
166
+ className="ml-0.5 rounded-full p-0.5 hover:bg-(--theme-bg-secondary) transition-colors focus:outline-none focus:ring-1 focus:ring-(--theme-border-secondary)"
167
+ aria-label={`Remove ${chip.label} filter`}
168
+ >
169
+ <svg aria-hidden="true" className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
170
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
171
+ </svg>
172
+ </button>
173
+ </span>
174
+ ))}
175
+ <button
176
+ type="button"
177
+ onClick={() =>
178
+ setFilters((prev) => ({
179
+ dateRange: prev.dateRange,
180
+ siteId: prev.siteId,
181
+ }))
182
+ }
183
+ className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs text-(--theme-text-secondary) hover:text-(--theme-text-primary) transition-colors focus:outline-none focus:ring-1 focus:ring-(--theme-border-secondary)"
184
+ >
185
+ Clear all
186
+ </button>
187
+ </div>
188
+ ) : null}
189
+
190
+ {isUpdating ? (
191
+ <div className="flex items-center gap-2 mt-2 text-xs text-(--theme-text-secondary)">
192
+ <SpinnerIcon className="w-3 h-3 animate-spin" />
193
+ <span>Updating dashboard...</span>
194
+ </div>
195
+ ) : null}
196
+ </>
197
+ );
198
+ },
199
+ [filters, isUpdating, setFilters],
200
+ );
201
+
202
+ const modal = useMemo<ReactNode>(
203
+ () => (
204
+ <FilterModal
205
+ filters={filters}
206
+ onFiltersChange={handleFiltersChange}
207
+ isOpen={isFilterModalOpen}
208
+ onClose={() => setIsFilterModalOpen(false)}
209
+ onNotify={(notice) => onNotify?.(notice)}
210
+ deviceTypeOptions={deviceTypeOptions}
211
+ countryOptions={countryOptions}
212
+ cityOptions={cityOptions}
213
+ regionOptions={regionOptions}
214
+ sourceOptions={sourceOptions}
215
+ pageUrlOptions={pageUrlOptions}
216
+ eventNameOptions={eventNameOptions}
217
+ />
218
+ ),
219
+ [
220
+ cityOptions,
221
+ countryOptions,
222
+ deviceTypeOptions,
223
+ eventNameOptions,
224
+ filters,
225
+ handleFiltersChange,
226
+ isFilterModalOpen,
227
+ onNotify,
228
+ pageUrlOptions,
229
+ regionOptions,
230
+ sourceOptions,
231
+ ],
232
+ );
233
+
234
+ return { controls, footer, modal };
235
+ }
@@ -0,0 +1,101 @@
1
+ type AlertTone = "success" | "error" | "info" | "warning";
2
+
3
+ type AlertToneConfig = {
4
+ label: string;
5
+ borderClass: string;
6
+ bgClass: string;
7
+ textClass: string;
8
+ };
9
+
10
+ type AlertOverrides = Partial<AlertToneConfig>;
11
+
12
+ type AlertBannerProps = {
13
+ tone?: AlertTone;
14
+ title?: string;
15
+ message: string;
16
+ onDismiss?: () => void;
17
+ className?: string;
18
+ accent?: AlertOverrides;
19
+ };
20
+
21
+ const toneStyles: Record<AlertTone, AlertToneConfig> = {
22
+ success: {
23
+ label: "Success",
24
+ borderClass: "border-emerald-500/60",
25
+ bgClass: "bg-emerald-500/10",
26
+ textClass: "text-emerald-400",
27
+ },
28
+ error: {
29
+ label: "Error",
30
+ borderClass: "border-red-500/70",
31
+ bgClass: "bg-red-500/10",
32
+ textClass: "text-red-400",
33
+ },
34
+ warning: {
35
+ label: "Warning",
36
+ borderClass: "border-amber-500/60",
37
+ bgClass: "bg-amber-500/10",
38
+ textClass: "text-amber-400",
39
+ },
40
+ info: {
41
+ label: "Info",
42
+ borderClass: "border-[var(--theme-border-primary)]",
43
+ bgClass: "bg-[var(--theme-bg-secondary)]",
44
+ textClass: "text-[var(--theme-text-primary)]",
45
+ },
46
+ };
47
+
48
+ export function AlertBanner({
49
+ tone = "info",
50
+ title,
51
+ message,
52
+ onDismiss,
53
+ className,
54
+ accent,
55
+ }: AlertBannerProps) {
56
+ const base = toneStyles[tone];
57
+ const label = title ?? accent?.label ?? base.label;
58
+ const borderClass = accent?.borderClass ?? base.borderClass;
59
+ const bgClass = accent?.bgClass ?? base.bgClass;
60
+ const textClass = accent?.textClass ?? base.textClass;
61
+
62
+ return (
63
+ <div
64
+ role="status"
65
+ className={`flex items-start justify-between gap-4 p-4 rounded-md border ${borderClass} ${bgClass} ${className ?? ""}`}
66
+ >
67
+ <div className="flex-1">
68
+ <p className={`text-sm font-semibold ${textClass}`}>{label}</p>
69
+ <p className="text-sm text-[var(--theme-text-secondary)]">
70
+ {message}
71
+ </p>
72
+ </div>
73
+ {onDismiss ? (
74
+ <button
75
+ type="button"
76
+ onClick={onDismiss}
77
+ className="text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)] transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--theme-border-secondary)] rounded"
78
+ aria-label="Dismiss notification"
79
+ >
80
+ <svg
81
+ aria-hidden="true"
82
+ focusable="false"
83
+ className="w-5 h-5"
84
+ fill="none"
85
+ stroke="currentColor"
86
+ viewBox="0 0 24 24"
87
+ >
88
+ <path
89
+ strokeLinecap="round"
90
+ strokeLinejoin="round"
91
+ strokeWidth={2}
92
+ d="M6 18L18 6M6 6l12 12"
93
+ />
94
+ </svg>
95
+ </button>
96
+ ) : null}
97
+ </div>
98
+ );
99
+ }
100
+
101
+ export type { AlertTone, AlertBannerProps };
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+
3
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
4
+ variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
5
+ size?: 'sm' | 'md' | 'lg';
6
+ isLoading?: boolean;
7
+ children: React.ReactNode;
8
+ }
9
+
10
+ const buttonVariants = {
11
+ primary: 'bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-white border-transparent',
12
+ secondary: 'bg-[var(--color-secondary)] hover:bg-[var(--color-secondary-hover)] text-white border-transparent',
13
+ outline: 'bg-transparent hover:bg-[var(--theme-bg-secondary)] text-[var(--theme-text-primary)] border-[var(--theme-border-primary)]',
14
+ ghost: 'bg-transparent hover:bg-[var(--theme-bg-secondary)] text-[var(--theme-text-primary)] border-transparent',
15
+ danger: 'bg-[var(--color-danger)] hover:bg-[var(--color-danger-hover)] text-white border-transparent'
16
+ };
17
+
18
+ const buttonSizes = {
19
+ sm: 'px-3 py-1.5 text-sm',
20
+ md: 'px-4 py-2 text-base',
21
+ lg: 'px-6 py-3 text-lg'
22
+ };
23
+
24
+ export const Button: React.FC<ButtonProps> = ({
25
+ variant = 'primary',
26
+ size = 'md',
27
+ isLoading = false,
28
+ disabled,
29
+ className = '',
30
+ children,
31
+ ...props
32
+ }) => {
33
+ const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg border cursor-pointer transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--color-primary)] disabled:opacity-50 disabled:cursor-not-allowed';
34
+
35
+ const variantClasses = buttonVariants[variant];
36
+ const sizeClasses = buttonSizes[size];
37
+
38
+ const combinedClasses = `${baseClasses} ${variantClasses} ${sizeClasses} ${className}`.trim();
39
+
40
+ return (
41
+ <button
42
+ className={combinedClasses}
43
+ disabled={disabled || isLoading}
44
+ {...props}
45
+ >
46
+ {isLoading && (
47
+ <svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
48
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
49
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
50
+ </svg>
51
+ )}
52
+ {children}
53
+ </button>
54
+ );
55
+ };
@@ -0,0 +1,80 @@
1
+ import React from 'react';
2
+
3
+ export interface CardProps {
4
+ children: React.ReactNode;
5
+ className?: string;
6
+ padding?: 'none' | 'sm' | 'md' | 'lg';
7
+ shadow?: 'none' | 'sm' | 'md' | 'lg';
8
+ }
9
+
10
+ const paddingVariants = {
11
+ none: '',
12
+ sm: 'p-3',
13
+ md: 'p-4',
14
+ lg: 'p-6'
15
+ };
16
+
17
+ const shadowVariants = {
18
+ none: '',
19
+ sm: 'shadow-[var(--shadow-sm)]',
20
+ md: 'shadow-[var(--shadow-md)]',
21
+ lg: 'shadow-[var(--shadow-lg)]'
22
+ };
23
+
24
+ export const Card: React.FC<CardProps> = ({
25
+ children,
26
+ className = '',
27
+ padding = 'md',
28
+ shadow = 'sm'
29
+ }) => {
30
+ const baseClasses = 'bg-[var(--theme-card-bg)] border border-[var(--theme-card-border)] rounded-lg';
31
+ const paddingClasses = paddingVariants[padding];
32
+ const shadowClasses = shadowVariants[shadow];
33
+
34
+ const combinedClasses = `${baseClasses} ${paddingClasses} ${shadowClasses} ${className}`.trim();
35
+
36
+ return (
37
+ <div className={combinedClasses}>
38
+ {children}
39
+ </div>
40
+ );
41
+ };
42
+
43
+ export interface CardHeaderProps {
44
+ children: React.ReactNode;
45
+ className?: string;
46
+ }
47
+
48
+ export const CardHeader: React.FC<CardHeaderProps> = ({ children, className = '' }) => {
49
+ return (
50
+ <div className={`border-b border-[var(--theme-border-primary)] pb-3 mb-4 ${className}`}>
51
+ {children}
52
+ </div>
53
+ );
54
+ };
55
+
56
+ export interface CardTitleProps {
57
+ children: React.ReactNode;
58
+ className?: string;
59
+ }
60
+
61
+ export const CardTitle: React.FC<CardTitleProps> = ({ children, className = '' }) => {
62
+ return (
63
+ <h3 className={`text-lg font-semibold text-[var(--theme-text-primary)] ${className}`}>
64
+ {children}
65
+ </h3>
66
+ );
67
+ };
68
+
69
+ export interface CardContentProps {
70
+ children: React.ReactNode;
71
+ className?: string;
72
+ }
73
+
74
+ export const CardContent: React.FC<CardContentProps> = ({ children, className = '' }) => {
75
+ return (
76
+ <div className={className}>
77
+ {children}
78
+ </div>
79
+ );
80
+ };
@@ -0,0 +1,72 @@
1
+ import React, { useId, forwardRef } from "react";
2
+
3
+ export interface InputProps
4
+ extends React.InputHTMLAttributes<HTMLInputElement> {
5
+ label?: string;
6
+ error?: string;
7
+ helperText?: string;
8
+ variant?: "default" | "filled";
9
+ inputSize?: "sm" | "md" | "lg";
10
+ }
11
+
12
+ const inputSizes = {
13
+ sm: "px-3 py-1.5 text-sm",
14
+ md: "px-4 py-2 text-base",
15
+ lg: "px-4 py-3 text-lg",
16
+ };
17
+
18
+ export const Input = forwardRef<HTMLInputElement, InputProps>((
19
+ {
20
+ label,
21
+ error,
22
+ helperText,
23
+ variant = "default",
24
+ inputSize = "md",
25
+ className = "",
26
+ id,
27
+ ...props
28
+ },
29
+ ref) => {
30
+ const generatedId = useId();
31
+ const inputId = id || generatedId;
32
+ const baseClasses =
33
+ "w-full rounded-lg border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] disabled:opacity-50 disabled:cursor-not-allowed";
34
+
35
+ const variantClasses =
36
+ variant === "filled"
37
+ ? "bg-[var(--theme-bg-secondary)] border-transparent"
38
+ : "bg-[var(--theme-input-bg)] border-[var(--theme-input-border)]";
39
+
40
+ const errorClasses = error
41
+ ? "border-[var(--color-danger)] focus:ring-[var(--color-danger)] focus:border-[var(--color-danger)]"
42
+ : "";
43
+
44
+ const sizeClasses = inputSizes[inputSize];
45
+
46
+ const combinedClasses =
47
+ `${baseClasses} ${variantClasses} ${errorClasses} ${sizeClasses} ${className}`.trim();
48
+
49
+ return (
50
+ <div className="w-full">
51
+ {label && (
52
+ <label
53
+ htmlFor={inputId}
54
+ className="block text-sm font-medium text-[var(--theme-text-primary)] mb-2"
55
+ >
56
+ {label}
57
+ </label>
58
+ )}
59
+ <input ref={ref} id={inputId} className={combinedClasses} {...props} />
60
+ {error && (
61
+ <p className="mt-1 text-sm text-[var(--color-danger)]">{error}</p>
62
+ )}
63
+ {helperText && !error && (
64
+ <p className="mt-1 text-sm text-[var(--theme-text-secondary)]">
65
+ {helperText}
66
+ </p>
67
+ )}
68
+ </div>
69
+ );
70
+ });
71
+ //For development only
72
+ Input.displayName = "Input";
@@ -0,0 +1,23 @@
1
+ "use client";
2
+ import type { AnchorHTMLAttributes, ReactNode } from "react";
3
+
4
+ interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
5
+ href: string;
6
+ children: ReactNode;
7
+ }
8
+
9
+ /**
10
+ * A semantic Link component for internal navigation.
11
+ * View transitions are handled globally in client.tsx for all navigation.
12
+ */
13
+ export function Link({
14
+ href,
15
+ children,
16
+ ...props
17
+ }: LinkProps) {
18
+ return (
19
+ <a href={href} {...props}>
20
+ {children}
21
+ </a>
22
+ );
23
+ }