@usequark/quark-create-app 1.23.11

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 (237) hide show
  1. package/README.md +210 -0
  2. package/package.json +56 -0
  3. package/src/dependabot-config.test.js +128 -0
  4. package/src/deploy/adapters/iac.js +203 -0
  5. package/src/deploy/adapters/index.js +27 -0
  6. package/src/deploy/adapters/railway.js +508 -0
  7. package/src/deploy/adapters/railway.test.js +318 -0
  8. package/src/deploy/contract.js +59 -0
  9. package/src/deploy/deploy.integration.test.js +183 -0
  10. package/src/deploy/deploy.js +430 -0
  11. package/src/deploy/deploy.test.js +105 -0
  12. package/src/deploy/discovery.js +115 -0
  13. package/src/deploy/discovery.test.js +141 -0
  14. package/src/deploy/index.js +38 -0
  15. package/src/deploy/inspect.js +130 -0
  16. package/src/index.js +2920 -0
  17. package/src/mobile-template.test.js +232 -0
  18. package/src/prepare-hooks.test.js +535 -0
  19. package/src/scaffold-guards.test.js +140 -0
  20. package/src/scaffold-output.test.js +228 -0
  21. package/src/template-scripts.test.js +160 -0
  22. package/src/utils.js +77 -0
  23. package/src/utils.test.js +95 -0
  24. package/src/worker-dockerfile.test.js +85 -0
  25. package/templates/base-project/.cursor/rules/quark.mdc +172 -0
  26. package/templates/base-project/.dockerignore +14 -0
  27. package/templates/base-project/.env.railway.example +87 -0
  28. package/templates/base-project/.github/copilot-instructions.md +65 -0
  29. package/templates/base-project/.github/dependabot.yml +31 -0
  30. package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
  31. package/templates/base-project/.github/workflows/ci.yml +116 -0
  32. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  33. package/templates/base-project/.github/workflows/install-windows.yml +27 -0
  34. package/templates/base-project/.github/workflows/release.yml +67 -0
  35. package/templates/base-project/.railway/railway.ts +40 -0
  36. package/templates/base-project/CLAUDE.md +163 -0
  37. package/templates/base-project/MAIN.md +21 -0
  38. package/templates/base-project/README.md +164 -0
  39. package/templates/base-project/apps/web/.env.example +99 -0
  40. package/templates/base-project/apps/web/Dockerfile +38 -0
  41. package/templates/base-project/apps/web/biome.json +8 -0
  42. package/templates/base-project/apps/web/jsconfig.json +9 -0
  43. package/templates/base-project/apps/web/next.config.js +154 -0
  44. package/templates/base-project/apps/web/package.json +42 -0
  45. package/templates/base-project/apps/web/postcss.config.mjs +7 -0
  46. package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
  47. package/templates/base-project/apps/web/public/quark.svg +47 -0
  48. package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
  49. package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
  50. package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
  51. package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
  52. package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
  53. package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
  54. package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
  55. package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
  56. package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
  57. package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
  58. package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
  59. package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
  60. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
  61. package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
  62. package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
  63. package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
  64. package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
  65. package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
  66. package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
  67. package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
  68. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
  69. package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
  70. package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
  71. package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
  72. package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
  73. package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
  74. package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
  75. package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
  76. package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
  77. package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
  78. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  79. package/templates/base-project/apps/web/src/app/global-error.js +58 -0
  80. package/templates/base-project/apps/web/src/app/globals.css +1166 -0
  81. package/templates/base-project/apps/web/src/app/icon.svg +47 -0
  82. package/templates/base-project/apps/web/src/app/layout.js +57 -0
  83. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  84. package/templates/base-project/apps/web/src/app/not-found.js +39 -0
  85. package/templates/base-project/apps/web/src/app/page.js +262 -0
  86. package/templates/base-project/apps/web/src/app/robots.js +23 -0
  87. package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
  88. package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
  89. package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
  90. package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
  91. package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
  92. package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
  93. package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
  94. package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
  95. package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
  96. package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
  97. package/templates/base-project/apps/web/src/lib/auth.js +191 -0
  98. package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
  99. package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
  100. package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
  101. package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
  102. package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
  103. package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
  104. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
  105. package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
  106. package/templates/base-project/apps/web/src/preflight.js +55 -0
  107. package/templates/base-project/apps/web/src/proxy.js +328 -0
  108. package/templates/base-project/biome.json +52 -0
  109. package/templates/base-project/docker-compose.override.yml +17 -0
  110. package/templates/base-project/docker-compose.yml +37 -0
  111. package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
  112. package/templates/base-project/docs/START_HERE.md +70 -0
  113. package/templates/base-project/docs/adr/README.md +42 -0
  114. package/templates/base-project/package.json +58 -0
  115. package/templates/base-project/packages/db/package.json +38 -0
  116. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
  117. package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
  118. package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
  119. package/templates/base-project/packages/db/prisma/seed.js +152 -0
  120. package/templates/base-project/packages/db/prisma.config.js +27 -0
  121. package/templates/base-project/packages/db/src/client.js +75 -0
  122. package/templates/base-project/packages/db/src/connection.js +44 -0
  123. package/templates/base-project/packages/db/src/context.js +275 -0
  124. package/templates/base-project/packages/db/src/index.js +5 -0
  125. package/templates/base-project/packages/db/src/ping.js +66 -0
  126. package/templates/base-project/packages/db/src/queries.js +292 -0
  127. package/templates/base-project/packages/db/src/schemas.js +30 -0
  128. package/templates/base-project/packages/db/src/umami.js +299 -0
  129. package/templates/base-project/pnpm-workspace.yaml +20 -0
  130. package/templates/base-project/scripts/check-loading.mjs +49 -0
  131. package/templates/base-project/scripts/check-standards.mjs +186 -0
  132. package/templates/base-project/scripts/clean-workspace.mjs +479 -0
  133. package/templates/base-project/scripts/dev-preflight.mjs +48 -0
  134. package/templates/base-project/scripts/doctor.js +408 -0
  135. package/templates/base-project/scripts/prepare.js +490 -0
  136. package/templates/base-project/scripts/run-tests.mjs +71 -0
  137. package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
  138. package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
  139. package/templates/base-project/skills/add-model/SKILL.md +28 -0
  140. package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
  141. package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
  142. package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
  143. package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
  144. package/templates/base-project/skills/i18n/SKILL.md +211 -0
  145. package/templates/base-project/skills/payment/SKILL.md +280 -0
  146. package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
  147. package/templates/base-project/turbo.json +143 -0
  148. package/templates/config/package.json +20 -0
  149. package/templates/config/src/app-url.js +134 -0
  150. package/templates/config/src/environment.js +284 -0
  151. package/templates/config/src/index.js +16 -0
  152. package/templates/config/src/load-config.js +157 -0
  153. package/templates/config/src/rate-limit.js +136 -0
  154. package/templates/config/src/redis.js +43 -0
  155. package/templates/config/src/validate-env.js +532 -0
  156. package/templates/jobs/README.md +52 -0
  157. package/templates/jobs/package.json +9 -0
  158. package/templates/jobs/src/definitions.js +13 -0
  159. package/templates/jobs/src/index.js +1 -0
  160. package/templates/mobile/.env.example +16 -0
  161. package/templates/mobile/CHECKLIST.md +40 -0
  162. package/templates/mobile/README.md +130 -0
  163. package/templates/mobile/app/(app)/_layout.tsx +83 -0
  164. package/templates/mobile/app/(app)/index.tsx +147 -0
  165. package/templates/mobile/app/(app)/profile.tsx +204 -0
  166. package/templates/mobile/app/(app)/settings.tsx +80 -0
  167. package/templates/mobile/app/(auth)/_layout.tsx +10 -0
  168. package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
  169. package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
  170. package/templates/mobile/app/+not-found.tsx +32 -0
  171. package/templates/mobile/app/_layout.tsx +18 -0
  172. package/templates/mobile/app.json +47 -0
  173. package/templates/mobile/assets/adaptive-icon.png +0 -0
  174. package/templates/mobile/assets/icon.png +0 -0
  175. package/templates/mobile/assets/notification-icon.png +0 -0
  176. package/templates/mobile/assets/splash.png +0 -0
  177. package/templates/mobile/biome.json +27 -0
  178. package/templates/mobile/components/AuthGate.tsx +19 -0
  179. package/templates/mobile/components/ThemeProvider.tsx +23 -0
  180. package/templates/mobile/eas.json +29 -0
  181. package/templates/mobile/hooks/use-auth.ts +127 -0
  182. package/templates/mobile/lib/api-client.ts +138 -0
  183. package/templates/mobile/lib/api.ts +83 -0
  184. package/templates/mobile/lib/auth-providers-google.ts +74 -0
  185. package/templates/mobile/lib/auth-providers.ts +74 -0
  186. package/templates/mobile/lib/auth.ts +100 -0
  187. package/templates/mobile/lib/config.ts +14 -0
  188. package/templates/mobile/lib/errors.ts +46 -0
  189. package/templates/mobile/lib/notifications.ts +77 -0
  190. package/templates/mobile/lib/storage.ts +25 -0
  191. package/templates/mobile/lib/theme-tokens.ts +16 -0
  192. package/templates/mobile/package.json +37 -0
  193. package/templates/mobile/tsconfig.json +10 -0
  194. package/templates/pwa/app/_components/PWARegister.js +15 -0
  195. package/templates/pwa/app/manifest.js +32 -0
  196. package/templates/pwa/public/sw.js +72 -0
  197. package/templates/ui/README.md +44 -0
  198. package/templates/ui/package.json +21 -0
  199. package/templates/ui/src/badge.js +23 -0
  200. package/templates/ui/src/button.js +111 -0
  201. package/templates/ui/src/card.js +128 -0
  202. package/templates/ui/src/checkbox.js +20 -0
  203. package/templates/ui/src/container.js +13 -0
  204. package/templates/ui/src/dialog.js +137 -0
  205. package/templates/ui/src/error-banner.js +15 -0
  206. package/templates/ui/src/footer.js +239 -0
  207. package/templates/ui/src/form-field.integration-test.js +142 -0
  208. package/templates/ui/src/form-field.js +50 -0
  209. package/templates/ui/src/index.js +24 -0
  210. package/templates/ui/src/input.js +11 -0
  211. package/templates/ui/src/label.js +14 -0
  212. package/templates/ui/src/lightbox.integration-test.js +167 -0
  213. package/templates/ui/src/lightbox.js +243 -0
  214. package/templates/ui/src/logo.js +52 -0
  215. package/templates/ui/src/navbar.js +602 -0
  216. package/templates/ui/src/password-input.js +31 -0
  217. package/templates/ui/src/rich-text.js +514 -0
  218. package/templates/ui/src/select.js +317 -0
  219. package/templates/ui/src/skeleton.js +10 -0
  220. package/templates/ui/src/spinner.js +31 -0
  221. package/templates/ui/src/table.js +169 -0
  222. package/templates/ui/src/textarea.js +11 -0
  223. package/templates/ui/src/theme-constants.js +24 -0
  224. package/templates/ui/src/theme.js +196 -0
  225. package/templates/ui/src/toast.js +206 -0
  226. package/templates/worker/Dockerfile +64 -0
  227. package/templates/worker/README.md +56 -0
  228. package/templates/worker/package.json +36 -0
  229. package/templates/worker/src/config/pricing.js +46 -0
  230. package/templates/worker/src/handlers/email-job-validation.js +36 -0
  231. package/templates/worker/src/handlers/email.js +98 -0
  232. package/templates/worker/src/handlers/files.js +59 -0
  233. package/templates/worker/src/handlers/index.js +20 -0
  234. package/templates/worker/src/handlers/push-notification.js +234 -0
  235. package/templates/worker/src/index.js +505 -0
  236. package/templates/worker/src/lib/railway.js +438 -0
  237. package/templates/worker/src/lib/tokens.js +81 -0
@@ -0,0 +1,239 @@
1
+ import React from "react";
2
+
3
+ const footerCls = "border-t border-[--footer-border] bg-[--footer-bg]";
4
+ const containerCls = "mx-auto";
5
+ const topGridCls =
6
+ "grid gap-8 sm:gap-10 md:grid-cols-2 lg:grid-cols-[minmax(0,1.45fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)]";
7
+ const brandRowCls = "flex items-center gap-3";
8
+ const markCls =
9
+ "flex shrink-0 items-center justify-center rounded-2xl bg-[--footer-mark-bg] font-bold text-[--footer-mark-text]";
10
+ const brandNameCls =
11
+ "text-2xl font-bold tracking-tight text-[--footer-text] sm:text-3xl";
12
+ const brandTextCls = "max-w-md text-sm leading-7 text-[--footer-text-muted]";
13
+ const ctaCls =
14
+ "inline-flex w-full items-center justify-center rounded-full border border-[--footer-cta-border] font-semibold text-[--footer-mark-text] transition-colors hover:bg-[--footer-cta-hover-bg] sm:w-auto";
15
+ const columnTitleCls = "mb-4 font-semibold text-[--footer-text-faint]";
16
+ const columnListCls = "space-y-2.5";
17
+ const linkCls =
18
+ "break-words text-[--footer-text-muted] transition-colors hover:text-[--footer-link-hover]";
19
+ const textItemCls = "break-words text-[--footer-text-muted]";
20
+ const bottomBarCls =
21
+ "mt-8 border-t border-[--footer-border] pt-5 flex flex-col gap-3 text-sm text-[--footer-text-faint] sm:mt-10 sm:pt-6 lg:flex-row lg:items-center lg:justify-between";
22
+ const legalCls = "flex flex-wrap items-center gap-2 sm:gap-3";
23
+ const sepCls = "text-[var(--footer-sep)]";
24
+
25
+ const DEFAULT_COLUMNS = [
26
+ {
27
+ title: "Lorem",
28
+ links: [
29
+ { label: "Lorem", href: "#" },
30
+ { label: "Ipsum", href: "#" },
31
+ { label: "Dolor", href: "#" },
32
+ { label: "Sit", href: "#" },
33
+ { label: "Amet", href: "#" },
34
+ ],
35
+ },
36
+ {
37
+ title: "Ipsum",
38
+ links: [
39
+ { label: "Consectetur", href: "#" },
40
+ { label: "Adipiscing", href: "#" },
41
+ { label: "Elit Sed", href: "#" },
42
+ { label: "Tempor", href: "#" },
43
+ ],
44
+ },
45
+ {
46
+ title: "Dolor",
47
+ links: [
48
+ { label: "lorem@ipsum.test", href: "mailto:lorem@ipsum.test" },
49
+ { label: "+00 000 0000", href: "tel:+000000000" },
50
+ { label: "Lorem ipsum dolor" },
51
+ { label: "Sit amet consectetur" },
52
+ ],
53
+ },
54
+ ];
55
+
56
+ const DEFAULT_LEGAL_LINKS = [
57
+ { label: "Lorem Policy", href: "#" },
58
+ { label: "Ipsum Terms", href: "#" },
59
+ ];
60
+
61
+ function renderColumnItem(item, index) {
62
+ if (item?.href) {
63
+ return React.createElement(
64
+ "li",
65
+ { key: `${item.label}-${index}` },
66
+ React.createElement(
67
+ "a",
68
+ { href: item.href, className: linkCls },
69
+ item.label,
70
+ ),
71
+ );
72
+ }
73
+
74
+ return React.createElement(
75
+ "li",
76
+ { key: `${item?.label ?? "item"}-${index}`, className: textItemCls },
77
+ item?.label,
78
+ );
79
+ }
80
+
81
+ export function Footer({
82
+ className = "",
83
+ brandName = "Lorem Ipsum Co.",
84
+ brandDescription = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
85
+ ctaLabel = "Lorem Ipsum CTA \u2192",
86
+ ctaHref = "#",
87
+ columns = DEFAULT_COLUMNS,
88
+ copyrightText = "\u00a9 2026 Lorem Ipsum Co.",
89
+ legalLinks = DEFAULT_LEGAL_LINKS,
90
+ poweredByText = "Powered by Lorem Ipsum",
91
+ poweredByHref = "#",
92
+ mark,
93
+ }) {
94
+ const cols = Array.isArray(columns) ? columns : DEFAULT_COLUMNS;
95
+ const legal = Array.isArray(legalLinks) ? legalLinks : DEFAULT_LEGAL_LINKS;
96
+
97
+ return React.createElement(
98
+ "footer",
99
+ { className: `${footerCls} ${className}`.trim() },
100
+ React.createElement(
101
+ "div",
102
+ {
103
+ className: containerCls,
104
+ style: {
105
+ maxWidth: "var(--footer-container-max-width)",
106
+ paddingLeft: "var(--footer-container-padding-x)",
107
+ paddingRight: "var(--footer-container-padding-x)",
108
+ paddingTop: "var(--footer-container-padding-y)",
109
+ paddingBottom: "var(--footer-container-padding-y)",
110
+ },
111
+ },
112
+ React.createElement(
113
+ "div",
114
+ { className: topGridCls },
115
+ React.createElement(
116
+ "div",
117
+ { className: "space-y-5 md:col-span-2 lg:col-span-1" },
118
+ React.createElement(
119
+ "div",
120
+ { className: brandRowCls },
121
+ mark
122
+ ? mark
123
+ : React.createElement(
124
+ "span",
125
+ {
126
+ className: markCls,
127
+ "aria-hidden": "true",
128
+ style: {
129
+ width: "var(--footer-mark-width)",
130
+ height: "var(--footer-mark-height)",
131
+ },
132
+ },
133
+ brandName.charAt(0).toUpperCase(),
134
+ ),
135
+ React.createElement("h2", { className: brandNameCls }, brandName),
136
+ ),
137
+ React.createElement(
138
+ "p",
139
+ { className: brandTextCls },
140
+ brandDescription,
141
+ ),
142
+ React.createElement(
143
+ "a",
144
+ {
145
+ href: ctaHref,
146
+ className: ctaCls,
147
+ style: {
148
+ paddingLeft: "var(--footer-cta-padding-x)",
149
+ paddingRight: "var(--footer-cta-padding-x)",
150
+ paddingTop: "var(--footer-cta-padding-y)",
151
+ paddingBottom: "var(--footer-cta-padding-y)",
152
+ fontSize: "var(--footer-cta-font-size)",
153
+ },
154
+ },
155
+ ctaLabel,
156
+ ),
157
+ ),
158
+ ...cols.slice(0, 3).map((column, index) =>
159
+ React.createElement(
160
+ "nav",
161
+ {
162
+ key: `${column?.title ?? "column"}-${index}`,
163
+ "aria-label": column?.title ?? `Footer column ${index + 1}`,
164
+ className: "min-w-0",
165
+ },
166
+ React.createElement(
167
+ "h3",
168
+ {
169
+ className: columnTitleCls,
170
+ style: {
171
+ fontSize: "var(--footer-column-title-font-size)",
172
+ textTransform: "var(--footer-column-title-text-transform)",
173
+ letterSpacing: "var(--footer-column-title-text-tracking)",
174
+ fontFamily: "var(--footer-column-title-font-family)",
175
+ },
176
+ },
177
+ column?.title,
178
+ ),
179
+ React.createElement(
180
+ "ul",
181
+ { className: columnListCls },
182
+ ...(column?.links ?? []).map(renderColumnItem),
183
+ ),
184
+ ),
185
+ ),
186
+ ),
187
+ React.createElement(
188
+ "div",
189
+ { className: bottomBarCls },
190
+ React.createElement(
191
+ "div",
192
+ { className: legalCls },
193
+ React.createElement("span", null, copyrightText),
194
+ legal.length > 0
195
+ ? React.createElement("span", { className: sepCls }, "\u2022")
196
+ : null,
197
+ ...legal.flatMap((item, index) => {
198
+ const node = item?.href
199
+ ? React.createElement(
200
+ "a",
201
+ { href: item.href, className: linkCls },
202
+ item.label,
203
+ )
204
+ : React.createElement("span", null, item?.label);
205
+
206
+ if (index === legal.length - 1) {
207
+ return [
208
+ React.createElement("span", { key: `legal-${index}` }, node),
209
+ ];
210
+ }
211
+
212
+ return [
213
+ React.createElement("span", { key: `legal-${index}` }, node),
214
+ React.createElement(
215
+ "span",
216
+ { key: `sep-${index}`, className: sepCls },
217
+ "\u2022",
218
+ ),
219
+ ];
220
+ }),
221
+ ),
222
+ poweredByHref
223
+ ? React.createElement(
224
+ "a",
225
+ {
226
+ href: poweredByHref,
227
+ className: `${linkCls} lg:text-right`.trim(),
228
+ },
229
+ poweredByText,
230
+ )
231
+ : React.createElement(
232
+ "p",
233
+ { className: "lg:text-right" },
234
+ poweredByText,
235
+ ),
236
+ ),
237
+ ),
238
+ );
239
+ }
@@ -0,0 +1,142 @@
1
+ import assert from "node:assert";
2
+ import { after, test } from "node:test";
3
+ import { JSDOM } from "jsdom";
4
+
5
+ const dom = new JSDOM(
6
+ "<!DOCTYPE html><html><body><div id='root'></div></body></html>",
7
+ {
8
+ url: "http://localhost",
9
+ pretendToBeVisual: true,
10
+ },
11
+ );
12
+
13
+ function setGlobal(key, value) {
14
+ Object.defineProperty(globalThis, key, {
15
+ value,
16
+ writable: true,
17
+ configurable: true,
18
+ });
19
+ }
20
+
21
+ setGlobal("document", dom.window.document);
22
+ setGlobal("window", dom.window);
23
+ setGlobal("navigator", dom.window.navigator);
24
+ setGlobal("HTMLElement", dom.window.HTMLElement);
25
+ setGlobal("Node", dom.window.Node);
26
+ setGlobal("IS_REACT_ACT_ENVIRONMENT", true);
27
+
28
+ const { createElement } = await import("react");
29
+ const { createRoot } = await import("react-dom/client");
30
+ const { act } = await import("react");
31
+ const { FormField } = await import("./form-field.js");
32
+
33
+ after(() => {
34
+ delete globalThis.document;
35
+ delete globalThis.window;
36
+ delete globalThis.navigator;
37
+ delete globalThis.HTMLElement;
38
+ delete globalThis.Node;
39
+ delete globalThis.IS_REACT_ACT_ENVIRONMENT;
40
+ });
41
+
42
+ function setupContainer() {
43
+ const container = dom.window.document.createElement("div");
44
+ dom.window.document.body.appendChild(container);
45
+ return container;
46
+ }
47
+
48
+ test("FormField - renders label and input", async () => {
49
+ const container = setupContainer();
50
+ const root = createRoot(container);
51
+ await act(() =>
52
+ root.render(createElement(FormField, { label: "Name", name: "name" })),
53
+ );
54
+ const label = container.querySelector("label");
55
+ const input = container.querySelector("input");
56
+ assert.ok(label, "should render a label");
57
+ assert.ok(input, "should render an input");
58
+ assert.strictEqual(label.textContent, "Name");
59
+ assert.strictEqual(input.getAttribute("name"), "name");
60
+ root.unmount();
61
+ });
62
+
63
+ test("FormField - connects label to input via htmlFor/id", async () => {
64
+ const container = setupContainer();
65
+ const root = createRoot(container);
66
+ await act(() =>
67
+ root.render(
68
+ createElement(FormField, {
69
+ label: "Email",
70
+ name: "email",
71
+ id: "custom-id",
72
+ }),
73
+ ),
74
+ );
75
+ const label = container.querySelector("label");
76
+ const input = container.querySelector("input");
77
+ assert.strictEqual(label.getAttribute("for"), "custom-id");
78
+ assert.strictEqual(input.getAttribute("id"), "custom-id");
79
+ root.unmount();
80
+ });
81
+
82
+ test("FormField - renders error message with role alert", async () => {
83
+ const container = setupContainer();
84
+ const root = createRoot(container);
85
+ await act(() =>
86
+ root.render(
87
+ createElement(FormField, {
88
+ label: "Email",
89
+ name: "email",
90
+ error: "Required",
91
+ }),
92
+ ),
93
+ );
94
+ const errorEl = container.querySelector("[role='alert']");
95
+ assert.ok(errorEl, "should render an error element with role alert");
96
+ assert.strictEqual(errorEl.textContent, "Required");
97
+ root.unmount();
98
+ });
99
+
100
+ test("FormField - sets aria-invalid and aria-describedby when error present", async () => {
101
+ const container = setupContainer();
102
+ const root = createRoot(container);
103
+ await act(() =>
104
+ root.render(
105
+ createElement(FormField, {
106
+ label: "Email",
107
+ name: "email",
108
+ error: "Required",
109
+ id: "email-field",
110
+ }),
111
+ ),
112
+ );
113
+ const wrapper = container.querySelector('[aria-invalid="true"]');
114
+ assert.ok(wrapper, "should set aria-invalid on wrapper");
115
+ const errorId = wrapper.getAttribute("aria-describedby");
116
+ assert.ok(errorId, "should set aria-describedby");
117
+ const errorEl = container.querySelector(`#${errorId}`);
118
+ assert.ok(errorEl, "aria-describedby should point to existing error element");
119
+ root.unmount();
120
+ });
121
+
122
+ test("FormField - renders children instead of default input", async () => {
123
+ const container = setupContainer();
124
+ const root = createRoot(container);
125
+ await act(() =>
126
+ root.render(
127
+ createElement(
128
+ FormField,
129
+ { label: "Bio", name: "bio" },
130
+ createElement("textarea", { "data-testid": "custom" }),
131
+ ),
132
+ ),
133
+ );
134
+ const textarea = container.querySelector("textarea");
135
+ assert.ok(textarea, "should render custom children instead of input");
136
+ assert.strictEqual(
137
+ container.querySelector("input"),
138
+ null,
139
+ "should not render default input",
140
+ );
141
+ root.unmount();
142
+ });
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import React, { useId } from "react";
3
+ import { Input } from "./input.js";
4
+ import { Label } from "./label.js";
5
+
6
+ export function FormField({
7
+ label,
8
+ name,
9
+ error,
10
+ children,
11
+ className = "",
12
+ ...props
13
+ }) {
14
+ const generatedId = useId();
15
+ const fieldId = props.id ?? generatedId;
16
+
17
+ const errorId = `${fieldId}-error`;
18
+
19
+ const inputProps = { id: fieldId, name, ...props };
20
+ if (error) {
21
+ inputProps["aria-invalid"] = true;
22
+ inputProps["aria-describedby"] = errorId;
23
+ }
24
+
25
+ const childrenWrapperProps = {};
26
+ if (error) {
27
+ childrenWrapperProps["aria-invalid"] = true;
28
+ childrenWrapperProps["aria-describedby"] = errorId;
29
+ }
30
+
31
+ return React.createElement(
32
+ "div",
33
+ { className: `min-w-0 space-y-1.5 ${className}`.trim() },
34
+ label ? React.createElement(Label, { htmlFor: fieldId }, label) : null,
35
+ children
36
+ ? React.createElement("div", childrenWrapperProps, children)
37
+ : React.createElement(Input, inputProps),
38
+ error
39
+ ? React.createElement(
40
+ "p",
41
+ {
42
+ id: errorId,
43
+ className: "text-xs text-[--error-text]",
44
+ role: "alert",
45
+ },
46
+ error,
47
+ )
48
+ : null,
49
+ );
50
+ }
@@ -0,0 +1,24 @@
1
+ export * from "./badge.js";
2
+ export * from "./button.js";
3
+ export * from "./card.js";
4
+ export * from "./checkbox.js";
5
+ export * from "./container.js";
6
+ export * from "./dialog.js";
7
+ export * from "./error-banner.js";
8
+ export * from "./footer.js";
9
+ export * from "./form-field.js";
10
+ export * from "./input.js";
11
+ export * from "./label.js";
12
+ export * from "./lightbox.js";
13
+ export * from "./logo.js";
14
+ export * from "./navbar.js";
15
+ export * from "./password-input.js";
16
+ export * from "./rich-text.js";
17
+ export * from "./select.js";
18
+ export * from "./skeleton.js";
19
+ export * from "./spinner.js";
20
+ export * from "./table.js";
21
+ export * from "./textarea.js";
22
+ export * from "./theme.js";
23
+ export * from "./theme-constants.js";
24
+ export * from "./toast.js";
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+
3
+ const base =
4
+ "block h-10 w-full border border-[--input-border] bg-[--input-bg] px-3 text-sm text-[--input-text] placeholder:text-[--input-placeholder] transition-colors duration-200 linear hover:border-[--input-border-hover] focus-visible:border-[--input-border-focus] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--input-ring-focus)] disabled:opacity-[--input-disabled-opacity] disabled:cursor-not-allowed";
5
+
6
+ export function Input({ className = "", ...props }) {
7
+ return React.createElement("input", {
8
+ className: `${base} rounded-[--radius-default] ${className}`.trim(),
9
+ ...props,
10
+ });
11
+ }
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+
3
+ const base = "block text-xs font-medium text-[--label-text]";
4
+
5
+ export function Label({ className = "", ...props }) {
6
+ return React.createElement("label", {
7
+ className: `${base} ${className}`.trim(),
8
+ style: {
9
+ textTransform: "var(--label-text-transform)",
10
+ letterSpacing: "var(--label-text-tracking)",
11
+ },
12
+ ...props,
13
+ });
14
+ }
@@ -0,0 +1,167 @@
1
+ import assert from "node:assert";
2
+ import { after, test } from "node:test";
3
+ import { JSDOM } from "jsdom";
4
+
5
+ const dom = new JSDOM(
6
+ "<!DOCTYPE html><html><body><div id='root'></div></body></html>",
7
+ {
8
+ url: "http://localhost",
9
+ pretendToBeVisual: true,
10
+ },
11
+ );
12
+
13
+ function setGlobal(key, value) {
14
+ Object.defineProperty(globalThis, key, {
15
+ value,
16
+ writable: true,
17
+ configurable: true,
18
+ });
19
+ }
20
+
21
+ setGlobal("document", dom.window.document);
22
+ setGlobal("window", dom.window);
23
+ setGlobal("navigator", dom.window.navigator);
24
+ setGlobal("HTMLElement", dom.window.HTMLElement);
25
+ setGlobal("Node", dom.window.Node);
26
+ setGlobal("requestAnimationFrame", dom.window.requestAnimationFrame);
27
+ setGlobal("cancelAnimationFrame", dom.window.cancelAnimationFrame);
28
+ setGlobal("KeyboardEvent", dom.window.KeyboardEvent);
29
+ setGlobal("IS_REACT_ACT_ENVIRONMENT", true);
30
+
31
+ const { createElement } = await import("react");
32
+ const { createRoot } = await import("react-dom/client");
33
+ const { act } = await import("react");
34
+ const { Lightbox } = await import("./lightbox.js");
35
+
36
+ after(() => {
37
+ delete globalThis.document;
38
+ delete globalThis.window;
39
+ delete globalThis.navigator;
40
+ delete globalThis.HTMLElement;
41
+ delete globalThis.Node;
42
+ delete globalThis.requestAnimationFrame;
43
+ delete globalThis.cancelAnimationFrame;
44
+ delete globalThis.KeyboardEvent;
45
+ delete globalThis.IS_REACT_ACT_ENVIRONMENT;
46
+ });
47
+
48
+ function setupContainer() {
49
+ const container = dom.window.document.createElement("div");
50
+ dom.window.document.body.appendChild(container);
51
+ return container;
52
+ }
53
+
54
+ test("Lightbox - renders nothing when closed", async () => {
55
+ const container = setupContainer();
56
+ const root = createRoot(container);
57
+ await act(() => root.render(createElement(Lightbox, { open: false })));
58
+ assert.strictEqual(container.innerHTML, "");
59
+ root.unmount();
60
+ });
61
+
62
+ test("Lightbox - renders overlay when open", async () => {
63
+ const container = setupContainer();
64
+ const root = createRoot(container);
65
+ await act(() =>
66
+ root.render(
67
+ createElement(Lightbox, {
68
+ open: true,
69
+ src: "/test.jpg",
70
+ alt: "Test image",
71
+ onClose: () => {},
72
+ }),
73
+ ),
74
+ );
75
+ const img = container.querySelector("img");
76
+ assert.ok(img, "should render an img element");
77
+ assert.strictEqual(img.getAttribute("src"), "/test.jpg");
78
+ assert.strictEqual(img.getAttribute("alt"), "Test image");
79
+ root.unmount();
80
+ });
81
+
82
+ test("Lightbox - calls onClose when close button clicked", async () => {
83
+ const container = setupContainer();
84
+ const root = createRoot(container);
85
+ let closed = false;
86
+ await act(() =>
87
+ root.render(
88
+ createElement(Lightbox, {
89
+ open: true,
90
+ src: "/test.jpg",
91
+ onClose: () => {
92
+ closed = true;
93
+ },
94
+ }),
95
+ ),
96
+ );
97
+ const closeBtn = container.querySelector(
98
+ "button[aria-label='Close lightbox']",
99
+ );
100
+ assert.ok(closeBtn, "should have a close button");
101
+ await act(() => closeBtn.click());
102
+ assert.ok(closed, "onClose should be called");
103
+ root.unmount();
104
+ });
105
+
106
+ test("Lightbox - shows prev/next buttons and counter", async () => {
107
+ const container = setupContainer();
108
+ const root = createRoot(container);
109
+ await act(() =>
110
+ root.render(
111
+ createElement(Lightbox, {
112
+ open: true,
113
+ src: "/img2.jpg",
114
+ showPrevious: true,
115
+ showNext: true,
116
+ currentIndex: 2,
117
+ totalCount: 5,
118
+ onPrevious: () => {},
119
+ onNext: () => {},
120
+ onClose: () => {},
121
+ }),
122
+ ),
123
+ );
124
+ assert.ok(
125
+ container.textContent.includes("Previous"),
126
+ "should show Previous button",
127
+ );
128
+ assert.ok(container.textContent.includes("Next"), "should show Next button");
129
+ assert.ok(container.textContent.includes("2 / 5"), "should show counter");
130
+ root.unmount();
131
+ });
132
+
133
+ test("Lightbox - calls onPrevious and onNext on button clicks", async () => {
134
+ const container = setupContainer();
135
+ const root = createRoot(container);
136
+ let prev = false;
137
+ let next = false;
138
+ await act(() =>
139
+ root.render(
140
+ createElement(Lightbox, {
141
+ open: true,
142
+ src: "/img2.jpg",
143
+ showPrevious: true,
144
+ showNext: true,
145
+ onPrevious: () => {
146
+ prev = true;
147
+ },
148
+ onNext: () => {
149
+ next = true;
150
+ },
151
+ onClose: () => {},
152
+ }),
153
+ ),
154
+ );
155
+ const buttons = container.querySelectorAll("button");
156
+ const prevBtn = Array.from(buttons).find((b) =>
157
+ b.textContent.includes("Previous"),
158
+ );
159
+ const nextBtn = Array.from(buttons).find((b) =>
160
+ b.textContent.includes("Next"),
161
+ );
162
+ await act(() => prevBtn.click());
163
+ assert.ok(prev, "onPrevious should be called");
164
+ await act(() => nextBtn.click());
165
+ assert.ok(next, "onNext should be called");
166
+ root.unmount();
167
+ });