@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,602 @@
1
+ "use client";
2
+ import React, { useEffect, useMemo, useRef, useState } from "react";
3
+
4
+ const shellCls =
5
+ "relative w-full border-b border-[--navbar-border] bg-[--navbar-bg] shadow-[var(--navbar-inset-shadow)] backdrop-blur";
6
+ const mobileShellCls =
7
+ "relative w-full border-b border-[--navbar-border] bg-[--navbar-mobile-bg] shadow-[var(--navbar-inset-shadow)]";
8
+ const mobileShellOpenCls =
9
+ "relative w-full border-b border-transparent bg-[--navbar-mobile-bg] shadow-[var(--navbar-inset-shadow)]";
10
+ const containerCls = "mx-auto w-full px-4 sm:px-6 lg:px-8";
11
+ const desktopInnerCls = "flex h-[4.5rem] items-center justify-center gap-6";
12
+ const leftZoneCls = "flex items-center mr-auto";
13
+ const rightZoneCls = "flex items-center justify-end ml-auto";
14
+ const logoCls =
15
+ "inline-flex items-center gap-2.5 rounded-[--radius-default] px-1.5 py-1 font-semibold tracking-wide text-[--navbar-text] transition-colors hover:text-[--navbar-logo-hover]";
16
+ const markCls =
17
+ "inline-flex h-8 w-8 items-center justify-center rounded-[--radius-default] border border-[--navbar-mark-border] bg-[--navbar-mark-bg] font-bold text-[--navbar-mark-text]";
18
+ const centerNavCls = "flex min-w-0 justify-center";
19
+ const desktopListCls = "flex items-center gap-3";
20
+ const desktopLinkCls =
21
+ "inline-flex h-10 items-center rounded-[--radius-default] px-3.5 font-medium text-[--navbar-text-muted] transition-colors hover:bg-[--navbar-hover-bg] hover:text-[--navbar-hover-text]";
22
+
23
+ const desktopDropdownWrapCls =
24
+ "group inline-flex cursor-pointer items-center rounded-[--radius-default] transition-colors hover:bg-[--navbar-hover-bg]";
25
+ const desktopDropdownLinkCls =
26
+ "flex items-center font-medium text-[--navbar-text-muted] transition-colors group-hover:text-[--navbar-hover-text]";
27
+ const desktopDropdownChevronCls =
28
+ "flex items-center px-2 pt-1 text-[--navbar-text-muted] transition-colors group-hover:text-[--navbar-hover-text] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--navbar-ring)] rounded-r-[--radius-default]";
29
+ const actionCls =
30
+ "inline-flex h-11 items-center justify-center whitespace-nowrap rounded-[--radius-default] border border-[--navbar-action-border] bg-[--navbar-action-bg] px-5 font-semibold text-[--navbar-action-text] transition-colors hover:border-[--navbar-action-hover-border] hover:bg-[--navbar-action-hover-bg] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--navbar-ring)]";
31
+
32
+ const mobileInnerCls = "flex h-14 items-center justify-between gap-4";
33
+ const mobileToggleCls =
34
+ "inline-flex h-10 w-10 items-center justify-center rounded-[--radius-default] text-[--navbar-text-muted] transition-colors hover:text-[--navbar-hover-text] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--navbar-ring)]";
35
+ const mobilePanelWrapCls =
36
+ "absolute inset-x-0 top-full z-[80] overflow-y-auto overscroll-contain [touch-action:pan-y] [-webkit-overflow-scrolling:touch] origin-top";
37
+ const mobilePanelCls =
38
+ "border-y border-[--navbar-border] bg-[--navbar-mobile-panel-bg] will-change-transform transition-all duration-300 ease-out";
39
+ const mobileLinkCls =
40
+ "flex min-h-12 w-full items-center justify-between px-4 text-left font-medium text-[--navbar-text-muted] transition-colors hover:bg-[--navbar-hover-bg] hover:text-[--navbar-hover-text]";
41
+ const mobileSubLinkCls =
42
+ "block px-6 py-2 text-[--navbar-text-muted] transition-colors hover:bg-[--navbar-hover-bg] hover:text-[--navbar-hover-text]";
43
+ const mobileActionWrapCls = "px-3 pb-3 pt-2";
44
+ const mobileActionCls =
45
+ "inline-flex h-10 w-full items-center justify-center rounded-[--radius-default] border border-[--navbar-action-border] bg-[--navbar-action-bg] px-4 font-semibold text-[--navbar-action-text] transition-colors hover:border-[--navbar-action-hover-border] hover:bg-[--navbar-action-hover-bg] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--navbar-ring)]";
46
+
47
+ const DEFAULT_LINKS = [
48
+ { label: "Home", href: "#" },
49
+ {
50
+ label: "Services",
51
+ items: [
52
+ { label: "Web Development", href: "#" },
53
+ { label: "Design Systems", href: "#" },
54
+ { label: "Consulting", href: "#" },
55
+ ],
56
+ },
57
+ { label: "Pricing", href: "#" },
58
+ {
59
+ label: "Company",
60
+ items: [
61
+ { label: "About", href: "#" },
62
+ { label: "Careers", href: "#" },
63
+ { label: "Contact", href: "#" },
64
+ ],
65
+ },
66
+ ];
67
+
68
+ const DEFAULT_ACTION = { label: "Contact", href: "#" };
69
+
70
+ function hasSubItems(link) {
71
+ return Array.isArray(link?.items) && link.items.length > 0;
72
+ }
73
+
74
+ function safeLinks(links) {
75
+ return links === undefined
76
+ ? DEFAULT_LINKS
77
+ : Array.isArray(links)
78
+ ? links
79
+ : [];
80
+ }
81
+
82
+ function renderLogo(logo) {
83
+ if (React.isValidElement(logo)) {
84
+ return logo;
85
+ }
86
+
87
+ const text = String(logo ?? "Logo");
88
+ return React.createElement(
89
+ React.Fragment,
90
+ null,
91
+ React.createElement(
92
+ "span",
93
+ {
94
+ "aria-hidden": "true",
95
+ className: markCls,
96
+ style: {
97
+ textTransform: "var(--navbar-mark-text-transform)",
98
+ letterSpacing: "var(--navbar-mark-text-tracking)",
99
+ },
100
+ },
101
+ text.slice(0, 1),
102
+ ),
103
+ React.createElement("span", { className: "text-lg" }, text),
104
+ );
105
+ }
106
+
107
+ function renderAction(action, className = actionCls) {
108
+ if (!action) return null;
109
+ const label = action.label ?? "Action";
110
+
111
+ if (action.href) {
112
+ return React.createElement("a", { href: action.href, className }, label);
113
+ }
114
+
115
+ return React.createElement(
116
+ "button",
117
+ { type: "button", className, onClick: action.onClick },
118
+ label,
119
+ );
120
+ }
121
+
122
+ function Chevron({ open = false, className = "h-3.5 w-3.5" }) {
123
+ return React.createElement(
124
+ "svg",
125
+ {
126
+ "aria-hidden": "true",
127
+ viewBox: "0 0 20 20",
128
+ fill: "none",
129
+ className:
130
+ `${className} transition-transform duration-200 ${open ? "rotate-180" : "rotate-0"}`.trim(),
131
+ },
132
+ React.createElement("path", {
133
+ d: "M5 7.5L10 12.5L15 7.5",
134
+ stroke: "currentColor",
135
+ strokeWidth: "1.8",
136
+ strokeLinecap: "round",
137
+ strokeLinejoin: "round",
138
+ }),
139
+ );
140
+ }
141
+
142
+ export function Navbar({
143
+ className = "",
144
+ logo = "Lorem Ipsum",
145
+ logoHref = "#",
146
+ links = DEFAULT_LINKS,
147
+ action = DEFAULT_ACTION,
148
+ maxWidthClassName = "max-w-6xl",
149
+ }) {
150
+ const rootRef = useRef(null);
151
+ const leaveTimerRef = useRef(null);
152
+ const [openIndex, setOpenIndex] = useState(null);
153
+ const navLinks = useMemo(() => safeLinks(links), [links]);
154
+ const toggleDesktopDropdown = (index) => {
155
+ setOpenIndex((state) => (state === index ? null : index));
156
+ };
157
+
158
+ useEffect(() => {
159
+ if (openIndex === null) return;
160
+
161
+ function onPointerDown(event) {
162
+ if (rootRef.current?.contains(event.target)) return;
163
+ setOpenIndex(null);
164
+ }
165
+
166
+ function onEscape(event) {
167
+ if (event.key === "Escape") setOpenIndex(null);
168
+ }
169
+
170
+ document.addEventListener("mousedown", onPointerDown);
171
+ document.addEventListener("keydown", onEscape);
172
+ return () => {
173
+ document.removeEventListener("mousedown", onPointerDown);
174
+ document.removeEventListener("keydown", onEscape);
175
+ };
176
+ }, [openIndex]);
177
+
178
+ return React.createElement(
179
+ "header",
180
+ {
181
+ ref: rootRef,
182
+ className: `${shellCls} ${className}`.trim(),
183
+ },
184
+ React.createElement(
185
+ "div",
186
+ { className: `${containerCls} ${maxWidthClassName}`.trim() },
187
+ React.createElement(
188
+ "div",
189
+ { className: desktopInnerCls },
190
+ React.createElement(
191
+ "div",
192
+ { className: leftZoneCls },
193
+ React.createElement(
194
+ "a",
195
+ { href: logoHref, className: logoCls },
196
+ renderLogo(logo),
197
+ ),
198
+ ),
199
+ React.createElement(
200
+ "nav",
201
+ { className: centerNavCls, "aria-label": "Primary navigation" },
202
+ React.createElement(
203
+ "ul",
204
+ { className: desktopListCls },
205
+ ...navLinks.map((link, index) => {
206
+ if (!hasSubItems(link)) {
207
+ return React.createElement(
208
+ "li",
209
+ { key: `${link.label}-${index}` },
210
+ React.createElement(
211
+ "a",
212
+ { href: link.href ?? "#", className: desktopLinkCls },
213
+ link.label,
214
+ ),
215
+ );
216
+ }
217
+
218
+ const isOpen = openIndex === index;
219
+ return React.createElement(
220
+ "li",
221
+ {
222
+ key: `${link.label}-${index}`,
223
+ className: "relative",
224
+ onMouseEnter: () => {
225
+ if (leaveTimerRef.current)
226
+ clearTimeout(leaveTimerRef.current);
227
+ setOpenIndex(index);
228
+ },
229
+ onMouseLeave: () => {
230
+ leaveTimerRef.current = setTimeout(() => {
231
+ setOpenIndex((state) => (state === index ? null : state));
232
+ }, 150);
233
+ },
234
+ },
235
+ React.createElement(
236
+ "div",
237
+ { className: desktopDropdownWrapCls },
238
+ React.createElement(
239
+ "a",
240
+ {
241
+ href: link.href ?? "#",
242
+ className: `${desktopDropdownLinkCls} rounded-l-[--radius-default] px-3.5 py-2`,
243
+ },
244
+ link.label,
245
+ ),
246
+ React.createElement(
247
+ "button",
248
+ {
249
+ type: "button",
250
+ className: desktopDropdownChevronCls,
251
+ "aria-expanded": isOpen,
252
+ "aria-haspopup": "menu",
253
+ "aria-label": `${link.label} submenu`,
254
+ onClick: () => toggleDesktopDropdown(index),
255
+ },
256
+ React.createElement(Chevron, { open: isOpen }),
257
+ ),
258
+ ),
259
+ isOpen
260
+ ? React.createElement(
261
+ "div",
262
+ {
263
+ role: "menu",
264
+ className:
265
+ "absolute left-0 top-[calc(100%+0.6rem)] z-40 w-48 rounded-[--radius-default] border border-[--navbar-border] bg-[--navbar-dropdown-bg] shadow-[var(--navbar-dropdown-shadow)]",
266
+ },
267
+ ...(link.items ?? []).map((item, itemIndex) =>
268
+ React.createElement(
269
+ "a",
270
+ {
271
+ key: `${item.label}-${itemIndex}`,
272
+ href: item.href ?? "#",
273
+ role: "menuitem",
274
+ className:
275
+ "block rounded-[--radius-default] px-3.5 py-2.5 text-base text-[--navbar-text-muted] transition-colors hover:bg-[--navbar-hover-bg] hover:text-[--navbar-hover-text]",
276
+ onClick: () => setOpenIndex(null),
277
+ },
278
+ item.label,
279
+ ),
280
+ ),
281
+ )
282
+ : null,
283
+ );
284
+ }),
285
+ ),
286
+ ),
287
+ React.createElement(
288
+ "div",
289
+ { className: rightZoneCls },
290
+ renderAction(action),
291
+ ),
292
+ ),
293
+ ),
294
+ );
295
+ }
296
+
297
+ export function MobileNavbar({
298
+ className = "",
299
+ logo = "Lorem Ipsum",
300
+ logoHref = "#",
301
+ links = DEFAULT_LINKS,
302
+ action = DEFAULT_ACTION,
303
+ maxWidthClassName = "max-w-6xl",
304
+ }) {
305
+ const rootRef = useRef(null);
306
+ const barRef = useRef(null);
307
+ const [menuOpen, setMenuOpen] = useState(false);
308
+ const [isMenuVisible, setIsMenuVisible] = useState(false);
309
+ const [isPanelActive, setIsPanelActive] = useState(false);
310
+ const [openSubmenus, setOpenSubmenus] = useState({});
311
+ const [panelMaxHeight, setPanelMaxHeight] = useState(0);
312
+ const navLinks = useMemo(() => safeLinks(links), [links]);
313
+ const menuAnimatingVisible = menuOpen || isMenuVisible;
314
+
315
+ useEffect(() => {
316
+ if (menuOpen) {
317
+ setIsMenuVisible(true);
318
+ let secondFrame = 0;
319
+ const firstFrame = requestAnimationFrame(() => {
320
+ secondFrame = requestAnimationFrame(() => {
321
+ setIsPanelActive(true);
322
+ });
323
+ });
324
+ return () => {
325
+ cancelAnimationFrame(firstFrame);
326
+ if (secondFrame) cancelAnimationFrame(secondFrame);
327
+ };
328
+ }
329
+
330
+ setIsPanelActive(false);
331
+ const closeTimer = setTimeout(() => {
332
+ setIsMenuVisible(false);
333
+ setOpenSubmenus({});
334
+ }, 320);
335
+ return () => clearTimeout(closeTimer);
336
+ }, [menuOpen]);
337
+
338
+ useEffect(() => {
339
+ if (!menuOpen) return;
340
+
341
+ function updatePanelLayout() {
342
+ if (!barRef.current) return;
343
+ const rect = barRef.current.getBoundingClientRect();
344
+ const viewportHeight = window.innerHeight;
345
+ const availableHeight = Math.max(0, viewportHeight - rect.bottom);
346
+ setPanelMaxHeight(availableHeight);
347
+ }
348
+
349
+ updatePanelLayout();
350
+ window.addEventListener("resize", updatePanelLayout);
351
+ window.addEventListener("scroll", updatePanelLayout, true);
352
+ return () => {
353
+ window.removeEventListener("resize", updatePanelLayout);
354
+ window.removeEventListener("scroll", updatePanelLayout, true);
355
+ };
356
+ }, [menuOpen]);
357
+
358
+ useEffect(() => {
359
+ if (!menuOpen) return;
360
+
361
+ function onPointerDown(event) {
362
+ if (rootRef.current?.contains(event.target)) return;
363
+ setMenuOpen(false);
364
+ }
365
+
366
+ function onEscape(event) {
367
+ if (event.key === "Escape") {
368
+ setMenuOpen(false);
369
+ }
370
+ }
371
+
372
+ document.addEventListener("mousedown", onPointerDown);
373
+ document.addEventListener("keydown", onEscape);
374
+ return () => {
375
+ document.removeEventListener("mousedown", onPointerDown);
376
+ document.removeEventListener("keydown", onEscape);
377
+ };
378
+ }, [menuOpen]);
379
+
380
+ function toggleSubmenu(index) {
381
+ setOpenSubmenus((state) => ({
382
+ ...state,
383
+ [index]: !state[index],
384
+ }));
385
+ }
386
+
387
+ return React.createElement(
388
+ "header",
389
+ {
390
+ ref: rootRef,
391
+ className:
392
+ `${menuAnimatingVisible ? mobileShellOpenCls : mobileShellCls} ${className}`.trim(),
393
+ },
394
+ React.createElement(
395
+ "div",
396
+ {
397
+ className: `${containerCls} ${maxWidthClassName} relative`.trim(),
398
+ style: {
399
+ paddingLeft: "var(--navbar-container-padding-x)",
400
+ paddingRight: "var(--navbar-container-padding-x)",
401
+ },
402
+ },
403
+ React.createElement(
404
+ "div",
405
+ {
406
+ ref: barRef,
407
+ className: mobileInnerCls,
408
+ style: { height: "var(--navbar-mobile-height)" },
409
+ },
410
+ React.createElement(
411
+ "a",
412
+ { href: logoHref, className: logoCls },
413
+ renderLogo(logo),
414
+ ),
415
+ React.createElement(
416
+ "button",
417
+ {
418
+ type: "button",
419
+ className: mobileToggleCls,
420
+ "aria-label": menuOpen
421
+ ? "Close navigation menu"
422
+ : "Open navigation menu",
423
+ "aria-expanded": menuOpen,
424
+ onClick: () => {
425
+ setMenuOpen((state) => !state);
426
+ },
427
+ },
428
+ React.createElement(
429
+ "span",
430
+ {
431
+ "aria-hidden": "true",
432
+ className: "relative block h-4 w-4",
433
+ },
434
+ React.createElement("span", {
435
+ className: `absolute left-0 top-[2px] block h-[2px] w-4 bg-current transition-all duration-200 ease-out ${
436
+ menuOpen ? "translate-y-[5px] rotate-45" : ""
437
+ }`,
438
+ }),
439
+ React.createElement("span", {
440
+ className: `absolute left-0 top-[7px] block h-[2px] w-4 bg-current transition-all duration-200 ease-out ${
441
+ menuOpen ? "opacity-0" : "opacity-100"
442
+ }`,
443
+ }),
444
+ React.createElement("span", {
445
+ className: `absolute left-0 top-[12px] block h-[2px] w-4 bg-current transition-all duration-200 ease-out ${
446
+ menuOpen ? "-translate-y-[5px] -rotate-45" : ""
447
+ }`,
448
+ }),
449
+ ),
450
+ ),
451
+ ),
452
+ isMenuVisible
453
+ ? React.createElement(
454
+ "div",
455
+ {
456
+ className: `${mobilePanelWrapCls} ${
457
+ isPanelActive ? "pointer-events-auto" : "pointer-events-none"
458
+ }`,
459
+ style: {
460
+ maxHeight: `${panelMaxHeight}px`,
461
+ paddingBottom: "env(safe-area-inset-bottom)",
462
+ },
463
+ },
464
+ React.createElement(
465
+ "div",
466
+ { className: "overflow-visible" },
467
+ React.createElement(
468
+ "nav",
469
+ {
470
+ className: `${mobilePanelCls} ${
471
+ isPanelActive
472
+ ? "translate-y-0 opacity-100"
473
+ : "-translate-y-6 opacity-0"
474
+ }`,
475
+ "aria-label": "Mobile navigation",
476
+ },
477
+ React.createElement(
478
+ "ul",
479
+ { className: "divide-y divide-[--navbar-divide-border]" },
480
+ ...navLinks.map((link, index) => {
481
+ if (!hasSubItems(link)) {
482
+ return React.createElement(
483
+ "li",
484
+ { key: `${link.label}-${index}` },
485
+ React.createElement(
486
+ "a",
487
+ {
488
+ href: link.href ?? "#",
489
+ className: mobileLinkCls,
490
+ style: {
491
+ minHeight: "var(--navbar-mobile-link-min-height)",
492
+ paddingLeft:
493
+ "var(--navbar-mobile-link-padding-x)",
494
+ paddingRight:
495
+ "var(--navbar-mobile-link-padding-x)",
496
+ fontSize: "var(--navbar-mobile-link-font-size)",
497
+ },
498
+ onClick: () => setMenuOpen(false),
499
+ },
500
+ link.label,
501
+ ),
502
+ );
503
+ }
504
+
505
+ const submenuOpen = Boolean(openSubmenus[index]);
506
+ return React.createElement(
507
+ "li",
508
+ { key: `${link.label}-${index}` },
509
+ React.createElement(
510
+ "button",
511
+ {
512
+ type: "button",
513
+ className: mobileLinkCls,
514
+ style: {
515
+ minHeight: "var(--navbar-mobile-link-min-height)",
516
+ paddingLeft: "var(--navbar-mobile-link-padding-x)",
517
+ paddingRight: "var(--navbar-mobile-link-padding-x)",
518
+ fontSize: "var(--navbar-mobile-link-font-size)",
519
+ },
520
+ "aria-expanded": submenuOpen,
521
+ onClick: () => toggleSubmenu(index),
522
+ },
523
+ link.label,
524
+ React.createElement(Chevron, {
525
+ open: submenuOpen,
526
+ className: "h-4 w-4",
527
+ }),
528
+ ),
529
+ React.createElement(
530
+ "div",
531
+ {
532
+ className:
533
+ "grid transition-all duration-200 ease-out",
534
+ style: {
535
+ gridTemplateRows: submenuOpen ? "1fr" : "0fr",
536
+ opacity: submenuOpen ? 1 : 0,
537
+ },
538
+ },
539
+ React.createElement(
540
+ "div",
541
+ { className: "overflow-hidden" },
542
+ React.createElement(
543
+ "ul",
544
+ { className: "space-y-0 pb-2 pt-1" },
545
+ ...(link.items ?? []).map((item, itemIndex) =>
546
+ React.createElement(
547
+ "li",
548
+ { key: `${item.label}-${itemIndex}` },
549
+ React.createElement(
550
+ "a",
551
+ {
552
+ href: item.href ?? "#",
553
+ className: mobileSubLinkCls,
554
+ style: {
555
+ paddingLeft:
556
+ "var(--navbar-mobile-sublink-padding-x)",
557
+ paddingRight:
558
+ "var(--navbar-mobile-sublink-padding-x)",
559
+ paddingTop:
560
+ "var(--navbar-mobile-sublink-padding-y)",
561
+ paddingBottom:
562
+ "var(--navbar-mobile-sublink-padding-y)",
563
+ fontSize:
564
+ "var(--navbar-mobile-sublink-font-size)",
565
+ },
566
+ onClick: () => {
567
+ setMenuOpen(false);
568
+ setOpenSubmenus({});
569
+ },
570
+ },
571
+ item.label,
572
+ ),
573
+ ),
574
+ ),
575
+ ),
576
+ ),
577
+ ),
578
+ );
579
+ }),
580
+ ),
581
+ action
582
+ ? React.createElement(
583
+ "div",
584
+ {
585
+ className: mobileActionWrapCls,
586
+ style: {
587
+ paddingLeft: "var(--navbar-container-padding-x)",
588
+ paddingRight: "var(--navbar-container-padding-x)",
589
+ paddingTop: "0.5rem",
590
+ paddingBottom: "0.75rem",
591
+ },
592
+ },
593
+ renderAction(action, mobileActionCls),
594
+ )
595
+ : null,
596
+ ),
597
+ ),
598
+ )
599
+ : null,
600
+ ),
601
+ );
602
+ }
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import { Eye, EyeOff } from "lucide-react";
4
+ import React from "react";
5
+ import { Input } from "./input.js";
6
+
7
+ export function PasswordInput({ className = "", ...props }) {
8
+ const [visible, setVisible] = React.useState(false);
9
+ const Icon = visible ? EyeOff : Eye;
10
+
11
+ return React.createElement(
12
+ "div",
13
+ { className: `relative ${className}`.trim() },
14
+ React.createElement(Input, {
15
+ type: visible ? "text" : "password",
16
+ className: "pr-10",
17
+ ...props,
18
+ }),
19
+ React.createElement(
20
+ "button",
21
+ {
22
+ type: "button",
23
+ onClick: () => setVisible((v) => !v),
24
+ "aria-label": visible ? "Hide password" : "Show password",
25
+ className:
26
+ "absolute right-2.5 top-1/2 -translate-y-1/2 text-[--input-icon] hover:text-[--input-icon-hover] transition-colors",
27
+ },
28
+ React.createElement(Icon, { size: 16, "aria-hidden": true }),
29
+ ),
30
+ );
31
+ }