@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,317 @@
1
+ "use client";
2
+ import { Check, ChevronDown } from "lucide-react";
3
+ import React, {
4
+ useCallback,
5
+ useEffect,
6
+ useId,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+
12
+ const triggerCls =
13
+ "flex h-10 w-full items-center justify-between rounded-[--radius-default] border border-[--select-border] bg-[--select-bg] px-3 text-sm text-[--select-text] shadow-sm transition-all duration-200 cursor-pointer hover:border-[--select-border-hover] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--select-ring)] disabled:cursor-not-allowed disabled:opacity-50";
14
+
15
+ const panelCls =
16
+ "absolute z-30 max-h-60 w-full overflow-auto rounded-[--radius-default] border border-[--select-border] bg-[--select-panel-bg] shadow-xl origin-top transition-all duration-200 ease-out";
17
+
18
+ const optionBaseCls =
19
+ "flex w-full items-center justify-between rounded-[--radius-default] px-2.5 py-2 text-left text-sm transition-colors duration-150";
20
+
21
+ const optionIdleCls =
22
+ "cursor-pointer text-[--select-text] hover:bg-[--select-option-hover-bg] hover:text-[--select-option-hover-text]";
23
+
24
+ const optionActiveCls =
25
+ "bg-[--select-option-active-bg] text-[--select-option-active-text]";
26
+
27
+ function normalizeValue(value) {
28
+ if (value === undefined || value === null) return "";
29
+ return String(value);
30
+ }
31
+
32
+ function toText(value) {
33
+ if (value === undefined || value === null) return "";
34
+ if (typeof value === "string" || typeof value === "number") {
35
+ return String(value);
36
+ }
37
+ if (Array.isArray(value)) {
38
+ return value
39
+ .map((item) => toText(item))
40
+ .join(" ")
41
+ .trim();
42
+ }
43
+ if (React.isValidElement(value)) {
44
+ return toText(value.props?.children);
45
+ }
46
+ return "";
47
+ }
48
+
49
+ function collectOptions(children) {
50
+ const items = React.Children.toArray(children);
51
+
52
+ return items.flatMap((child, index) => {
53
+ if (!React.isValidElement(child)) return [];
54
+ if (child.type === React.Fragment) {
55
+ return collectOptions(child.props?.children);
56
+ }
57
+ if (
58
+ typeof child.type !== "string" ||
59
+ child.type.toLowerCase() !== "option"
60
+ ) {
61
+ return [];
62
+ }
63
+
64
+ const label = toText(child.props?.children);
65
+ const rawValue = child.props?.value ?? label;
66
+
67
+ return [
68
+ {
69
+ key: child.key ?? `${rawValue}-${index}`,
70
+ value: normalizeValue(rawValue),
71
+ label,
72
+ disabled: Boolean(child.props?.disabled),
73
+ },
74
+ ];
75
+ });
76
+ }
77
+
78
+ function createChangeEvent(nextValue, name, id) {
79
+ const target = {
80
+ value: nextValue,
81
+ name,
82
+ id,
83
+ };
84
+
85
+ return {
86
+ type: "change",
87
+ target,
88
+ currentTarget: target,
89
+ };
90
+ }
91
+
92
+ export function Select({
93
+ className = "",
94
+ children,
95
+ value,
96
+ defaultValue,
97
+ onChange,
98
+ disabled = false,
99
+ id,
100
+ name,
101
+ placeholder = "Choose...",
102
+ required = false,
103
+ onBlur,
104
+ onFocus,
105
+ ...props
106
+ }) {
107
+ const options = useMemo(() => collectOptions(children), [children]);
108
+ const generatedId = useId();
109
+ const selectId = id ?? generatedId;
110
+ const panelId = `${selectId}-panel`;
111
+ const isControlled = value !== undefined;
112
+
113
+ const initialValue = useMemo(() => {
114
+ if (isControlled) return normalizeValue(value);
115
+ if (defaultValue !== undefined) return normalizeValue(defaultValue);
116
+ return options[0]?.value ?? "";
117
+ }, [isControlled, value, defaultValue, options]);
118
+
119
+ const [open, setOpen] = useState(false);
120
+ const [internalValue, setInternalValue] = useState(initialValue);
121
+ const rootRef = useRef(null);
122
+
123
+ const selectedValue = isControlled ? normalizeValue(value) : internalValue;
124
+ const selectedOption = options.find(
125
+ (option) => option.value === selectedValue,
126
+ );
127
+ const triggerDisabled = disabled || options.length === 0;
128
+ const panelStateCls = open
129
+ ? "opacity-100 scale-y-100 pointer-events-auto"
130
+ : "opacity-0 scale-y-0 pointer-events-none";
131
+
132
+ useEffect(() => {
133
+ if (isControlled) {
134
+ setInternalValue(normalizeValue(value));
135
+ }
136
+ }, [isControlled, value]);
137
+
138
+ useEffect(() => {
139
+ if (isControlled) return;
140
+ const hasValue = options.some((option) => option.value === internalValue);
141
+ if (hasValue) return;
142
+ setInternalValue(initialValue);
143
+ }, [isControlled, options, internalValue, initialValue]);
144
+
145
+ useEffect(() => {
146
+ if (!open) return;
147
+
148
+ function onPointerDown(event) {
149
+ if (rootRef.current?.contains(event.target)) return;
150
+ setOpen(false);
151
+ }
152
+
153
+ function onEscape(event) {
154
+ if (event.key === "Escape") setOpen(false);
155
+ }
156
+
157
+ document.addEventListener("mousedown", onPointerDown);
158
+ document.addEventListener("keydown", onEscape);
159
+
160
+ return () => {
161
+ document.removeEventListener("mousedown", onPointerDown);
162
+ document.removeEventListener("keydown", onEscape);
163
+ };
164
+ }, [open]);
165
+
166
+ const commitValue = useCallback(
167
+ (nextValue) => {
168
+ if (!isControlled) {
169
+ setInternalValue(nextValue);
170
+ }
171
+ onChange?.(createChangeEvent(nextValue, name, selectId));
172
+ },
173
+ [isControlled, onChange, name, selectId],
174
+ );
175
+
176
+ const toggleOpen = useCallback(() => {
177
+ if (triggerDisabled) return;
178
+ setOpen((state) => !state);
179
+ }, [triggerDisabled]);
180
+
181
+ const handleTriggerKeyDown = useCallback(
182
+ (event) => {
183
+ if (triggerDisabled) return;
184
+
185
+ if (
186
+ event.key === "ArrowDown" ||
187
+ event.key === "Enter" ||
188
+ event.key === " "
189
+ ) {
190
+ event.preventDefault();
191
+ setOpen(true);
192
+ }
193
+
194
+ if (event.key === "Escape") {
195
+ event.preventDefault();
196
+ setOpen(false);
197
+ }
198
+ },
199
+ [triggerDisabled],
200
+ );
201
+
202
+ return React.createElement(
203
+ "div",
204
+ { className: "relative", ref: rootRef },
205
+ name
206
+ ? React.createElement(
207
+ "select",
208
+ {
209
+ name,
210
+ value: selectedValue,
211
+ required,
212
+ disabled: triggerDisabled,
213
+ tabIndex: -1,
214
+ "aria-hidden": "true",
215
+ onChange: (event) => commitValue(event.target.value),
216
+ className: "sr-only",
217
+ },
218
+ options.map((option) =>
219
+ React.createElement(
220
+ "option",
221
+ {
222
+ key: option.key,
223
+ value: option.value,
224
+ disabled: option.disabled,
225
+ },
226
+ option.label || option.value,
227
+ ),
228
+ ),
229
+ )
230
+ : null,
231
+ React.createElement(
232
+ "button",
233
+ {
234
+ type: "button",
235
+ id: selectId,
236
+ disabled: triggerDisabled,
237
+ onBlur,
238
+ onFocus,
239
+ onClick: toggleOpen,
240
+ onKeyDown: handleTriggerKeyDown,
241
+ "aria-haspopup": "listbox",
242
+ "aria-expanded": open,
243
+ "aria-controls": panelId,
244
+ "aria-required": required,
245
+ className: `${triggerCls} ${className}`.trim(),
246
+ ...props,
247
+ },
248
+ React.createElement(
249
+ "span",
250
+ {
251
+ className: selectedOption
252
+ ? "text-[--select-text]"
253
+ : "text-[--select-text-faint]",
254
+ },
255
+ selectedOption?.label ??
256
+ (options.length === 0 ? "No options" : placeholder),
257
+ ),
258
+ React.createElement(
259
+ "span",
260
+ {
261
+ "aria-hidden": "true",
262
+ className:
263
+ `ml-2 text-[--select-text-faint] transition-transform duration-200 ease-in-out ${open ? "rotate-180" : "rotate-0"}`.trim(),
264
+ },
265
+ React.createElement(ChevronDown, { size: 16 }),
266
+ ),
267
+ ),
268
+ React.createElement(
269
+ "div",
270
+ {
271
+ id: panelId,
272
+ role: "listbox",
273
+ "aria-hidden": !open,
274
+ className: `${panelCls} ${panelStateCls}`.trim(),
275
+ },
276
+ options.length === 0
277
+ ? React.createElement(
278
+ "p",
279
+ { className: "px-2.5 py-2 text-sm text-[--select-text-faint]" },
280
+ "No options",
281
+ )
282
+ : options.map((option) => {
283
+ const selected = option.value === selectedValue;
284
+ const isOptionDisabled = option.disabled || !open;
285
+ return React.createElement(
286
+ "button",
287
+ {
288
+ type: "button",
289
+ key: option.key,
290
+ role: "option",
291
+ "aria-selected": selected,
292
+ disabled: isOptionDisabled,
293
+ tabIndex: open ? 0 : -1,
294
+ onClick: () => {
295
+ if (option.disabled) return;
296
+ commitValue(option.value);
297
+ setOpen(false);
298
+ },
299
+ className:
300
+ `${optionBaseCls} ${selected ? optionActiveCls : optionIdleCls} ${option.disabled ? "cursor-not-allowed opacity-50" : ""}`.trim(),
301
+ },
302
+ React.createElement("span", null, option.label || option.value),
303
+ selected
304
+ ? React.createElement(
305
+ "span",
306
+ {
307
+ "aria-hidden": "true",
308
+ className: "text-[--select-option-active-text]",
309
+ },
310
+ React.createElement(Check, { size: 16 }),
311
+ )
312
+ : null,
313
+ );
314
+ }),
315
+ ),
316
+ );
317
+ }
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+
3
+ const base = "animate-pulse rounded-lg bg-[--skeleton-bg]";
4
+
5
+ export function Skeleton({ className = "", ...props }) {
6
+ return React.createElement("div", {
7
+ className: `${base} ${className}`.trim(),
8
+ ...props,
9
+ });
10
+ }
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+
3
+ /**
4
+ * Animated SVG spinner for loading states.
5
+ * Server Component-safe — uses Tailwind's animate-spin.
6
+ */
7
+ export function Spinner({ className = "h-4 w-4", label = "Loading" }) {
8
+ return React.createElement(
9
+ "svg",
10
+ {
11
+ role: "img",
12
+ "aria-label": label,
13
+ className: `animate-spin ${className}`,
14
+ viewBox: "0 0 24 24",
15
+ fill: "none",
16
+ },
17
+ React.createElement("circle", {
18
+ className: "opacity-25",
19
+ cx: "12",
20
+ cy: "12",
21
+ r: "10",
22
+ stroke: "currentColor",
23
+ strokeWidth: "4",
24
+ }),
25
+ React.createElement("path", {
26
+ className: "opacity-75",
27
+ fill: "currentColor",
28
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z",
29
+ }),
30
+ );
31
+ }
@@ -0,0 +1,169 @@
1
+ import { ChevronDown, ChevronsUpDown, ChevronUp } from "lucide-react";
2
+ import React from "react";
3
+
4
+ const wrapperCls =
5
+ "w-full overflow-auto rounded-[--radius-default] border border-[--table-border] bg-[--table-bg]";
6
+
7
+ const tableCls = "w-full caption-bottom text-sm";
8
+
9
+ const headerCls = "border-b border-[--table-border] bg-[--table-header-bg]";
10
+
11
+ const bodyCls = "[&_tr:last-child]:border-0";
12
+
13
+ const rowCls =
14
+ "border-b border-[var(--table-border)] transition-colors hover:bg-[var(--table-row-hover-bg)] active:bg-[var(--table-row-active-bg)]";
15
+
16
+ const headCls =
17
+ "h-11 text-left align-middle font-medium text-[--table-head-text]";
18
+
19
+ const cellCls = "align-middle text-[--table-cell-text]";
20
+
21
+ const sortButtonCls =
22
+ "group inline-flex w-full items-center justify-between gap-2 text-left outline-none focus-visible:ring-2 focus-visible:ring-[var(--table-ring)] rounded-[--radius-default]";
23
+
24
+ const sortIconCls =
25
+ "text-[--table-sort-icon] transition-colors group-hover:text-[--table-sort-icon-hover]";
26
+
27
+ function toAriaSort(direction) {
28
+ if (direction === "asc") return "ascending";
29
+ if (direction === "desc") return "descending";
30
+ return "none";
31
+ }
32
+
33
+ function nextDirection(direction) {
34
+ if (direction === "none") return "asc";
35
+ if (direction === "asc") return "desc";
36
+ return "none";
37
+ }
38
+
39
+ function sortIndicator(direction) {
40
+ if (direction === "asc") return React.createElement(ChevronUp, { size: 14 });
41
+ if (direction === "desc")
42
+ return React.createElement(ChevronDown, { size: 14 });
43
+ return React.createElement(ChevronsUpDown, { size: 14 });
44
+ }
45
+
46
+ function headingText(children) {
47
+ if (typeof children === "string" || typeof children === "number") {
48
+ return String(children);
49
+ }
50
+ return "column";
51
+ }
52
+
53
+ export function Table({ className = "", ...props }) {
54
+ return React.createElement(
55
+ "div",
56
+ { className: wrapperCls },
57
+ React.createElement("table", {
58
+ className: `${tableCls} ${className}`.trim(),
59
+ ...props,
60
+ }),
61
+ );
62
+ }
63
+
64
+ export function TableHeader({ className = "", ...props }) {
65
+ return React.createElement("thead", {
66
+ className: `${headerCls} ${className}`.trim(),
67
+ ...props,
68
+ });
69
+ }
70
+
71
+ export function TableBody({ className = "", ...props }) {
72
+ return React.createElement("tbody", {
73
+ className: `${bodyCls} ${className}`.trim(),
74
+ ...props,
75
+ });
76
+ }
77
+
78
+ export function TableRow({ className = "", ...props }) {
79
+ return React.createElement("tr", {
80
+ className: `${rowCls} ${className}`.trim(),
81
+ ...props,
82
+ });
83
+ }
84
+
85
+ export function TableHead({
86
+ className = "",
87
+ sortable = false,
88
+ sortDirection = "none",
89
+ onSort,
90
+ sortLabel,
91
+ children,
92
+ ...props
93
+ }) {
94
+ const direction =
95
+ sortDirection === "asc" || sortDirection === "desc"
96
+ ? sortDirection
97
+ : "none";
98
+
99
+ if (!sortable) {
100
+ return React.createElement(
101
+ "th",
102
+ {
103
+ className: `${headCls} ${className}`.trim(),
104
+ style: {
105
+ paddingLeft: "var(--table-head-padding-x)",
106
+ paddingRight: "var(--table-head-padding-x)",
107
+ fontSize: "var(--table-head-font-size)",
108
+ textTransform: "var(--table-head-text-transform)",
109
+ letterSpacing: "var(--table-head-text-tracking)",
110
+ fontFamily: "var(--table-head-font-family)",
111
+ },
112
+ ...props,
113
+ },
114
+ children,
115
+ );
116
+ }
117
+
118
+ return React.createElement(
119
+ "th",
120
+ {
121
+ className: `${headCls} ${className}`.trim(),
122
+ style: {
123
+ paddingLeft: "var(--table-head-padding-x)",
124
+ paddingRight: "var(--table-head-padding-x)",
125
+ fontSize: "var(--table-head-font-size)",
126
+ textTransform: "var(--table-head-text-transform)",
127
+ letterSpacing: "var(--table-head-text-tracking)",
128
+ fontFamily: "var(--table-head-font-family)",
129
+ },
130
+ "aria-sort": toAriaSort(direction),
131
+ ...props,
132
+ },
133
+ React.createElement(
134
+ "button",
135
+ {
136
+ type: "button",
137
+ onClick:
138
+ typeof onSort === "function"
139
+ ? () => onSort(nextDirection(direction))
140
+ : undefined,
141
+ disabled: typeof onSort !== "function",
142
+ "aria-label": sortLabel ?? `Sort by ${headingText(children)}`,
143
+ className: `${sortButtonCls} ${typeof onSort !== "function" ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`,
144
+ },
145
+ React.createElement("span", null, children),
146
+ React.createElement(
147
+ "span",
148
+ {
149
+ "aria-hidden": "true",
150
+ className:
151
+ `${sortIconCls} ${direction !== "none" ? "text-[--table-sort-active]" : ""}`.trim(),
152
+ },
153
+ sortIndicator(direction),
154
+ ),
155
+ ),
156
+ );
157
+ }
158
+
159
+ export function TableCell({ className = "", ...props }) {
160
+ return React.createElement("td", {
161
+ className: `${cellCls} ${className}`.trim(),
162
+ style: {
163
+ padding: "var(--table-cell-padding)",
164
+ fontSize: "var(--table-cell-font-size)",
165
+ fontFamily: "var(--table-cell-font-family)",
166
+ },
167
+ ...props,
168
+ });
169
+ }
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+
3
+ const base =
4
+ "block w-full rounded-[--radius-default] border border-[--input-border] bg-[--input-bg] px-3 py-2 text-sm text-[--input-text] placeholder:text-[--input-placeholder] transition-colors duration-200 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 resize-y";
5
+
6
+ export function Textarea({ className = "", ...props }) {
7
+ return React.createElement("textarea", {
8
+ className: `${base} ${className}`.trim(),
9
+ ...props,
10
+ });
11
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Theme system constants for @techstream/quark-ui.
3
+ *
4
+ * These three values form the contract between ThemeProvider and any
5
+ * out-of-tree component (e.g. HomeThemeToggle in apps/web) that interoperates
6
+ * with it via the shared localStorage key, HTML attribute, and DOM event.
7
+ *
8
+ * An identical copy lives in apps/web/src/lib/theme.js - each package owns
9
+ * its own copy so there is no cross-package import. If you rename any of
10
+ * these, update both files and the CSS selectors in globals.css.
11
+ */
12
+
13
+ /** Key used to persist the user's explicit theme choice in localStorage. */
14
+ export const THEME_STORAGE_KEY = "quark-theme";
15
+
16
+ /**
17
+ * Attribute set on <html> so CSS custom properties can react to JS state.
18
+ * Use via element.setAttribute(THEME_ATTR, value) / element.getAttribute(THEME_ATTR).
19
+ * Referenced in globals.css as [data-theme="light"] / [data-theme="dark"].
20
+ */
21
+ export const THEME_ATTR = "data-theme";
22
+
23
+ /** CustomEvent name dispatched when the theme changes outside a ThemeProvider. */
24
+ export const THEME_CHANGE_EVENT = "quark-theme-change";