@lukoweb/apitogo 0.2.30 → 0.2.34

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 (44) hide show
  1. package/dist/cli/cli.js +794 -248
  2. package/dist/declarations/app/entry.server.d.ts +2 -2
  3. package/dist/declarations/app/main.d.ts +3 -1
  4. package/dist/declarations/cli/platform/client/platform-api.d.ts +16 -5
  5. package/dist/declarations/cli/platform/helpers/deploy-artifact-hash.d.ts +1 -1
  6. package/dist/declarations/cli/platform/helpers/package-files.d.ts +1 -1
  7. package/dist/declarations/cli/platform/helpers/platform-plan.d.ts +1 -1
  8. package/dist/declarations/cli/platform/helpers/zip.d.ts +1 -1
  9. package/dist/declarations/cli/platform/tools/infrastructure.d.ts +0 -4
  10. package/dist/declarations/config/effective-redirects.d.ts +6 -0
  11. package/dist/declarations/config/header-navigation-guard.d.ts +2 -0
  12. package/dist/declarations/config/resolve-modules.d.ts +1 -4
  13. package/dist/declarations/config/validators/HeaderNavigationSchema.d.ts +1 -2
  14. package/docs/cli/commands.md +151 -75
  15. package/docs/cli/go-live.md +135 -123
  16. package/docs/cli/walkthrough.md +40 -1
  17. package/docs/configuration/gateway.md +52 -0
  18. package/docs/guides/customer-account.md +33 -2
  19. package/docs/mcp/tools.md +101 -102
  20. package/package.json +2 -1
  21. package/src/app/demo.tsx +78 -77
  22. package/src/app/entry.client.tsx +115 -107
  23. package/src/app/entry.server.tsx +164 -160
  24. package/src/app/main.tsx +175 -167
  25. package/src/app/standalone.tsx +61 -60
  26. package/src/config/effective-redirects.ts +31 -0
  27. package/src/config/header-navigation-guard.ts +13 -0
  28. package/src/config/resolve-modules.ts +331 -354
  29. package/src/config/v2/home-redirect.ts +104 -98
  30. package/src/config/v2/index.ts +127 -126
  31. package/src/config/v2/ledger.ts +242 -235
  32. package/src/config/v2/resolve.ts +12 -4
  33. package/src/config/v2/rules.ts +716 -714
  34. package/src/config/v2/services.ts +799 -663
  35. package/src/config/v2/workspace.ts +17 -1
  36. package/src/config/validators/HeaderNavigationSchema.ts +42 -44
  37. package/src/lib/assets/powered-by-icon.png +0 -0
  38. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +667 -602
  39. package/src/lib/authentication/providers/dev-portal.tsx +476 -448
  40. package/src/lib/components/HeaderNavigation.tsx +176 -176
  41. package/src/lib/components/MobileTopNavigation.tsx +283 -283
  42. package/src/lib/components/navigation/PoweredByZudoku.tsx +42 -39
  43. package/src/vite/plugin-offroot-resolve.ts +68 -67
  44. package/src/vite/prerender/prerender.ts +242 -242
@@ -1,283 +1,283 @@
1
- import { Button } from "@lukoweb/apitogo/ui/Button.js";
2
- import { Separator } from "@lukoweb/apitogo/ui/Separator.js";
3
- import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
4
- import { deepEqual } from "fast-equals";
5
- import {
6
- ChevronDownIcon,
7
- LogOutIcon,
8
- MenuIcon,
9
- type LucideIcon,
10
- } from "lucide-react";
11
- import { useState } from "react";
12
- import { Link } from "react-router";
13
- import {
14
- isHeaderNavGroup,
15
- type HeaderNavItem,
16
- type HeaderNavLinkItem,
17
- } from "../../config/validators/HeaderNavigationSchema.js";
18
- import { withoutMarketingCtaHeaderNavigation } from "../authentication/header-nav-filter.js";
19
- import { useAuth } from "../authentication/hook.js";
20
- import {
21
- Collapsible,
22
- CollapsibleContent,
23
- CollapsibleTrigger,
24
- } from "../ui/Collapsible.js";
25
- import {
26
- Drawer,
27
- DrawerContent,
28
- DrawerTitle,
29
- DrawerTrigger,
30
- } from "../ui/Drawer.js";
31
- import { AnchorLink } from "./AnchorLink.js";
32
- import { useCurrentNavigation, useZudoku } from "./context/ZudokuContext.js";
33
- import { PoweredByZudoku } from "./navigation/PoweredByZudoku.js";
34
- import { getFirstMatchingPath, shouldShowItem } from "./navigation/utils.js";
35
- import { PageProgress } from "./PageProgress.js";
36
- import { Slot } from "./Slot.js";
37
- import { ThemeSwitch } from "./ThemeSwitch.js";
38
-
39
- const MobileHeaderNavLink = ({
40
- item,
41
- onClick,
42
- }: {
43
- item: HeaderNavLinkItem;
44
- onClick: () => void;
45
- }) => {
46
- const Icon = item.icon as LucideIcon | undefined;
47
- return (
48
- <AnchorLink
49
- to={item.to}
50
- target={item.target}
51
- rel={item.target === "_blank" ? "noopener noreferrer" : undefined}
52
- onClick={onClick}
53
- className="flex items-center font-medium gap-2 py-2 text-foreground/80 hover:text-foreground"
54
- >
55
- {Icon && <Icon size={16} />}
56
- {item.label}
57
- </AnchorLink>
58
- );
59
- };
60
-
61
- const MobileHeaderNavItem = ({
62
- item,
63
- onNavigate,
64
- }: {
65
- item: HeaderNavItem;
66
- onNavigate: () => void;
67
- }) => {
68
- if ("to" in item) {
69
- const Icon = item.icon as LucideIcon | undefined;
70
- return (
71
- <li className="w-full">
72
- <AnchorLink
73
- to={item.to}
74
- target={item.target}
75
- rel={item.target === "_blank" ? "noopener noreferrer" : undefined}
76
- onClick={onNavigate}
77
- className="flex items-center gap-2 py-2 text-base font-medium"
78
- >
79
- {Icon && <Icon size={16} />}
80
- {item.label}
81
- </AnchorLink>
82
- </li>
83
- );
84
- }
85
-
86
- return (
87
- <li className="w-full">
88
- <Collapsible>
89
- <CollapsibleTrigger className="flex items-center justify-between w-full py-2 text-base font-medium group">
90
- {item.label}
91
- <ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
92
- </CollapsibleTrigger>
93
- <CollapsibleContent>
94
- <ul className="flex flex-col border-l ms-1 ps-3 my-1">
95
- {item.items.map((subItem) =>
96
- isHeaderNavGroup(subItem) ? (
97
- <li key={subItem.label} className="flex flex-col">
98
- <div className="text-sm text-muted-foreground py-2">
99
- {subItem.label}
100
- </div>
101
- {subItem.items.map((link) => (
102
- <MobileHeaderNavLink
103
- key={link.to}
104
- item={link}
105
- onClick={onNavigate}
106
- />
107
- ))}
108
- </li>
109
- ) : (
110
- <li key={subItem.to}>
111
- <MobileHeaderNavLink item={subItem} onClick={onNavigate} />
112
- </li>
113
- ),
114
- )}
115
- </ul>
116
- </CollapsibleContent>
117
- </Collapsible>
118
- </li>
119
- );
120
- };
121
-
122
- export const MobileTopNavigation = () => {
123
- const context = useZudoku();
124
- const authState = useAuth();
125
- const currentNav = useCurrentNavigation();
126
-
127
- const {
128
- options: { header, navigation = [], site },
129
- getProfileMenuItems,
130
- } = context;
131
- const headerNavigation = withoutMarketingCtaHeaderNavigation(
132
- header?.navigation ?? [],
133
- );
134
- const { isAuthenticated, profile, isAuthEnabled, login, signup } = authState;
135
- const [drawerOpen, setDrawerOpen] = useState(false);
136
-
137
- const accountItems = getProfileMenuItems();
138
- const filteredItems = navigation.filter(
139
- shouldShowItem({ auth: authState, context }),
140
- );
141
-
142
- return (
143
- <Drawer
144
- direction={site?.dir === "rtl" ? "left" : "right"}
145
- open={drawerOpen}
146
- onOpenChange={setDrawerOpen}
147
- >
148
- <div className="flex lg:hidden justify-self-end">
149
- <DrawerTrigger className="lg:hidden">
150
- <MenuIcon size={22} />
151
- </DrawerTrigger>
152
- <PageProgress />
153
- </div>
154
- <DrawerContent
155
- className="lg:hidden h-dvh inset-e-0 start-auto w-[340px] rounded-none"
156
- aria-describedby={undefined}
157
- >
158
- <div className="py-2 h-full flex flex-col">
159
- <div className="flex-1 overflow-y-auto overscroll-none">
160
- <VisuallyHidden>
161
- <DrawerTitle>Navigation</DrawerTitle>
162
- </VisuallyHidden>
163
- <ul className="flex flex-col gap-1 px-4">
164
- {filteredItems.map((item) => {
165
- if (item.type === "separator") {
166
- return <Separator className="my-2" key={item.label} />;
167
- }
168
- if (item.type === "section" || item.type === "filter") {
169
- return null;
170
- }
171
- const path = getFirstMatchingPath(item);
172
- const isActive = deepEqual(currentNav.topNavItem, item);
173
- return (
174
- <li key={item.label}>
175
- <Link
176
- to={path}
177
- onClick={() => setDrawerOpen(false)}
178
- className={`flex items-center gap-2 py-2 text-base font-medium ${isActive ? "text-foreground" : "text-foreground/75 hover:text-foreground"}`}
179
- >
180
- {item.icon && <item.icon size={16} />}
181
- {item.label}
182
- </Link>
183
- </li>
184
- );
185
- })}
186
- {filteredItems.length > 0 && headerNavigation.length > 0 && (
187
- <Separator className="my-2" />
188
- )}
189
- <li className="empty:hidden">
190
- <Slot.Target name="top-navigation-side" />
191
- </li>
192
- {headerNavigation.map((item) => (
193
- <MobileHeaderNavItem
194
- key={item.label}
195
- item={item}
196
- onNavigate={() => setDrawerOpen(false)}
197
- />
198
- ))}
199
- {isAuthEnabled && isAuthenticated && (
200
- <>
201
- <Separator className="my-2" />
202
- <li className="py-2">
203
- <div className="text-base font-medium">
204
- {profile?.name ?? "My Account"}
205
- </div>
206
- {profile?.email && profile.email !== profile?.name && (
207
- <div className="text-sm text-muted-foreground">
208
- {profile.email}
209
- </div>
210
- )}
211
- </li>
212
- {accountItems.map((i) => (
213
- <li key={i.label}>
214
- <Link
215
- to={i.path ?? ""}
216
- target={i.target}
217
- rel={
218
- i.target === "_blank"
219
- ? "noopener noreferrer"
220
- : undefined
221
- }
222
- onClick={() => setDrawerOpen(false)}
223
- className="flex items-center py-2 text-base font-medium text-foreground/75 hover:text-foreground"
224
- >
225
- {i.label}
226
- </Link>
227
- </li>
228
- ))}
229
- </>
230
- )}
231
- </ul>
232
- </div>
233
- <div className="border-t shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)] px-4 pt-3 flex flex-col gap-2">
234
- <div className="flex items-center justify-between gap-2">
235
- {isAuthEnabled &&
236
- (isAuthenticated ? (
237
- <Button asChild variant="outline">
238
- <Link
239
- to="/signout"
240
- onClick={() => setDrawerOpen(false)}
241
- className="flex items-center gap-2"
242
- >
243
- <LogOutIcon
244
- size={16}
245
- strokeWidth={1}
246
- absoluteStrokeWidth
247
- />
248
- Log out
249
- </Link>
250
- </Button>
251
- ) : (
252
- <div className="flex flex-1 items-center gap-2">
253
- <Button
254
- variant="ghost"
255
- className="font-semibold"
256
- onClick={() => {
257
- setDrawerOpen(false);
258
- void login();
259
- }}
260
- >
261
- Log in
262
- </Button>
263
- <Button
264
- onClick={() => {
265
- setDrawerOpen(false);
266
- void signup();
267
- }}
268
- >
269
- Get started
270
- </Button>
271
- </div>
272
- ))}
273
- <ThemeSwitch variant="compact" />
274
- </div>
275
- {site?.showPoweredBy !== false && (
276
- <PoweredByZudoku className="grow-0 justify-center gap-1" />
277
- )}
278
- </div>
279
- </div>
280
- </DrawerContent>
281
- </Drawer>
282
- );
283
- };
1
+ import { Button } from "@lukoweb/apitogo/ui/Button.js";
2
+ import { Separator } from "@lukoweb/apitogo/ui/Separator.js";
3
+ import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
4
+ import { deepEqual } from "fast-equals";
5
+ import {
6
+ ChevronDownIcon,
7
+ LogOutIcon,
8
+ MenuIcon,
9
+ type LucideIcon,
10
+ } from "lucide-react";
11
+ import { useState } from "react";
12
+ import { Link } from "react-router";
13
+ import { isHeaderNavGroup } from "../../config/header-navigation-guard.js";
14
+ import type {
15
+ HeaderNavItem,
16
+ HeaderNavLinkItem,
17
+ } from "../../config/validators/HeaderNavigationSchema.js";
18
+ import { withoutMarketingCtaHeaderNavigation } from "../authentication/header-nav-filter.js";
19
+ import { useAuth } from "../authentication/hook.js";
20
+ import {
21
+ Collapsible,
22
+ CollapsibleContent,
23
+ CollapsibleTrigger,
24
+ } from "../ui/Collapsible.js";
25
+ import {
26
+ Drawer,
27
+ DrawerContent,
28
+ DrawerTitle,
29
+ DrawerTrigger,
30
+ } from "../ui/Drawer.js";
31
+ import { AnchorLink } from "./AnchorLink.js";
32
+ import { useCurrentNavigation, useZudoku } from "./context/ZudokuContext.js";
33
+ import { PoweredByZudoku } from "./navigation/PoweredByZudoku.js";
34
+ import { getFirstMatchingPath, shouldShowItem } from "./navigation/utils.js";
35
+ import { PageProgress } from "./PageProgress.js";
36
+ import { Slot } from "./Slot.js";
37
+ import { ThemeSwitch } from "./ThemeSwitch.js";
38
+
39
+ const MobileHeaderNavLink = ({
40
+ item,
41
+ onClick,
42
+ }: {
43
+ item: HeaderNavLinkItem;
44
+ onClick: () => void;
45
+ }) => {
46
+ const Icon = item.icon as LucideIcon | undefined;
47
+ return (
48
+ <AnchorLink
49
+ to={item.to}
50
+ target={item.target}
51
+ rel={item.target === "_blank" ? "noopener noreferrer" : undefined}
52
+ onClick={onClick}
53
+ className="flex items-center font-medium gap-2 py-2 text-foreground/80 hover:text-foreground"
54
+ >
55
+ {Icon && <Icon size={16} />}
56
+ {item.label}
57
+ </AnchorLink>
58
+ );
59
+ };
60
+
61
+ const MobileHeaderNavItem = ({
62
+ item,
63
+ onNavigate,
64
+ }: {
65
+ item: HeaderNavItem;
66
+ onNavigate: () => void;
67
+ }) => {
68
+ if ("to" in item) {
69
+ const Icon = item.icon as LucideIcon | undefined;
70
+ return (
71
+ <li className="w-full">
72
+ <AnchorLink
73
+ to={item.to}
74
+ target={item.target}
75
+ rel={item.target === "_blank" ? "noopener noreferrer" : undefined}
76
+ onClick={onNavigate}
77
+ className="flex items-center gap-2 py-2 text-base font-medium"
78
+ >
79
+ {Icon && <Icon size={16} />}
80
+ {item.label}
81
+ </AnchorLink>
82
+ </li>
83
+ );
84
+ }
85
+
86
+ return (
87
+ <li className="w-full">
88
+ <Collapsible>
89
+ <CollapsibleTrigger className="flex items-center justify-between w-full py-2 text-base font-medium group">
90
+ {item.label}
91
+ <ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
92
+ </CollapsibleTrigger>
93
+ <CollapsibleContent>
94
+ <ul className="flex flex-col border-l ms-1 ps-3 my-1">
95
+ {item.items.map((subItem) =>
96
+ isHeaderNavGroup(subItem) ? (
97
+ <li key={subItem.label} className="flex flex-col">
98
+ <div className="text-sm text-muted-foreground py-2">
99
+ {subItem.label}
100
+ </div>
101
+ {subItem.items.map((link) => (
102
+ <MobileHeaderNavLink
103
+ key={link.to}
104
+ item={link}
105
+ onClick={onNavigate}
106
+ />
107
+ ))}
108
+ </li>
109
+ ) : (
110
+ <li key={subItem.to}>
111
+ <MobileHeaderNavLink item={subItem} onClick={onNavigate} />
112
+ </li>
113
+ ),
114
+ )}
115
+ </ul>
116
+ </CollapsibleContent>
117
+ </Collapsible>
118
+ </li>
119
+ );
120
+ };
121
+
122
+ export const MobileTopNavigation = () => {
123
+ const context = useZudoku();
124
+ const authState = useAuth();
125
+ const currentNav = useCurrentNavigation();
126
+
127
+ const {
128
+ options: { header, navigation = [], site },
129
+ getProfileMenuItems,
130
+ } = context;
131
+ const headerNavigation = withoutMarketingCtaHeaderNavigation(
132
+ header?.navigation ?? [],
133
+ );
134
+ const { isAuthenticated, profile, isAuthEnabled, login, signup } = authState;
135
+ const [drawerOpen, setDrawerOpen] = useState(false);
136
+
137
+ const accountItems = getProfileMenuItems();
138
+ const filteredItems = navigation.filter(
139
+ shouldShowItem({ auth: authState, context }),
140
+ );
141
+
142
+ return (
143
+ <Drawer
144
+ direction={site?.dir === "rtl" ? "left" : "right"}
145
+ open={drawerOpen}
146
+ onOpenChange={setDrawerOpen}
147
+ >
148
+ <div className="flex lg:hidden justify-self-end">
149
+ <DrawerTrigger className="lg:hidden">
150
+ <MenuIcon size={22} />
151
+ </DrawerTrigger>
152
+ <PageProgress />
153
+ </div>
154
+ <DrawerContent
155
+ className="lg:hidden h-dvh inset-e-0 start-auto w-[340px] rounded-none"
156
+ aria-describedby={undefined}
157
+ >
158
+ <div className="py-2 h-full flex flex-col">
159
+ <div className="flex-1 overflow-y-auto overscroll-none">
160
+ <VisuallyHidden>
161
+ <DrawerTitle>Navigation</DrawerTitle>
162
+ </VisuallyHidden>
163
+ <ul className="flex flex-col gap-1 px-4">
164
+ {filteredItems.map((item) => {
165
+ if (item.type === "separator") {
166
+ return <Separator className="my-2" key={item.label} />;
167
+ }
168
+ if (item.type === "section" || item.type === "filter") {
169
+ return null;
170
+ }
171
+ const path = getFirstMatchingPath(item);
172
+ const isActive = deepEqual(currentNav.topNavItem, item);
173
+ return (
174
+ <li key={item.label}>
175
+ <Link
176
+ to={path}
177
+ onClick={() => setDrawerOpen(false)}
178
+ className={`flex items-center gap-2 py-2 text-base font-medium ${isActive ? "text-foreground" : "text-foreground/75 hover:text-foreground"}`}
179
+ >
180
+ {item.icon && <item.icon size={16} />}
181
+ {item.label}
182
+ </Link>
183
+ </li>
184
+ );
185
+ })}
186
+ {filteredItems.length > 0 && headerNavigation.length > 0 && (
187
+ <Separator className="my-2" />
188
+ )}
189
+ <li className="empty:hidden">
190
+ <Slot.Target name="top-navigation-side" />
191
+ </li>
192
+ {headerNavigation.map((item) => (
193
+ <MobileHeaderNavItem
194
+ key={item.label}
195
+ item={item}
196
+ onNavigate={() => setDrawerOpen(false)}
197
+ />
198
+ ))}
199
+ {isAuthEnabled && isAuthenticated && (
200
+ <>
201
+ <Separator className="my-2" />
202
+ <li className="py-2">
203
+ <div className="text-base font-medium">
204
+ {profile?.name ?? "My Account"}
205
+ </div>
206
+ {profile?.email && profile.email !== profile?.name && (
207
+ <div className="text-sm text-muted-foreground">
208
+ {profile.email}
209
+ </div>
210
+ )}
211
+ </li>
212
+ {accountItems.map((i) => (
213
+ <li key={i.label}>
214
+ <Link
215
+ to={i.path ?? ""}
216
+ target={i.target}
217
+ rel={
218
+ i.target === "_blank"
219
+ ? "noopener noreferrer"
220
+ : undefined
221
+ }
222
+ onClick={() => setDrawerOpen(false)}
223
+ className="flex items-center py-2 text-base font-medium text-foreground/75 hover:text-foreground"
224
+ >
225
+ {i.label}
226
+ </Link>
227
+ </li>
228
+ ))}
229
+ </>
230
+ )}
231
+ </ul>
232
+ </div>
233
+ <div className="border-t shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)] px-4 pt-3 flex flex-col gap-2">
234
+ <div className="flex items-center justify-between gap-2">
235
+ {isAuthEnabled &&
236
+ (isAuthenticated ? (
237
+ <Button asChild variant="outline">
238
+ <Link
239
+ to="/signout"
240
+ onClick={() => setDrawerOpen(false)}
241
+ className="flex items-center gap-2"
242
+ >
243
+ <LogOutIcon
244
+ size={16}
245
+ strokeWidth={1}
246
+ absoluteStrokeWidth
247
+ />
248
+ Log out
249
+ </Link>
250
+ </Button>
251
+ ) : (
252
+ <div className="flex flex-1 items-center gap-2">
253
+ <Button
254
+ variant="ghost"
255
+ className="font-semibold"
256
+ onClick={() => {
257
+ setDrawerOpen(false);
258
+ void login();
259
+ }}
260
+ >
261
+ Log in
262
+ </Button>
263
+ <Button
264
+ onClick={() => {
265
+ setDrawerOpen(false);
266
+ void signup();
267
+ }}
268
+ >
269
+ Get started
270
+ </Button>
271
+ </div>
272
+ ))}
273
+ <ThemeSwitch variant="compact" />
274
+ </div>
275
+ {site?.showPoweredBy !== false && (
276
+ <PoweredByZudoku className="grow-0 justify-center gap-1" />
277
+ )}
278
+ </div>
279
+ </div>
280
+ </DrawerContent>
281
+ </Drawer>
282
+ );
283
+ };
@@ -1,39 +1,42 @@
1
- import { ChevronRightIcon } from "lucide-react";
2
- import { cn } from "../../util/cn.js";
3
- import ZuploLogo from "./ZuploLogo.js";
4
-
5
- export const PoweredByZudoku = ({ className }: { className?: string }) => (
6
- <a
7
- href={
8
- import.meta.env.IS_ZUPLO ? "https://zuplo.com" : "https://apitogo.com"
9
- }
10
- target="_blank"
11
- rel="noopener noreferrer"
12
- className={cn(
13
- "flex justify-between items-center w-full border border-transparent hover:border-border rounded-full hover:shadow-xs h-7 px-3 text-nowrap hover:bg-muted/80 transition-all",
14
- className,
15
- )}
16
- >
17
- <div className="opacity-70 hover:opacity-100 transition-opacity gap-1.5 text-[11px] font-medium rounded-full h-7 flex items-center text-nowrap">
18
- {import.meta.env.IS_ZUPLO ? (
19
- <ZuploLogo className="w-3.5 h-3.5 dark:fill-white" />
20
- ) : (
21
- <img
22
- src="/Icon.png"
23
- alt=""
24
- aria-hidden="true"
25
- className="w-3.5 h-3.5"
26
- />
27
- )}
28
- powered by {import.meta.env.IS_ZUPLO ? "Zuplo" : "apitogo"}
29
- </div>
30
- <div className="text-xs font-medium opacity-70 hover:text-foreground transition-colors cursor-pointer">
31
- <ChevronRightIcon
32
- size={12}
33
- absoluteStrokeWidth
34
- strokeWidth={1.5}
35
- className="rtl:rotate-180"
36
- />
37
- </div>
38
- </a>
39
- );
1
+ import { ChevronRightIcon } from "lucide-react";
2
+ // Inlined: a file URL is root-relative in the server render and absolute in the browser when the build has a CDN base,
3
+ // which React reports as a hydration mismatch on every page that shows this link.
4
+ import poweredByIcon from "../../assets/powered-by-icon.png?inline";
5
+ import { cn } from "../../util/cn.js";
6
+ import ZuploLogo from "./ZuploLogo.js";
7
+
8
+ export const PoweredByZudoku = ({ className }: { className?: string }) => (
9
+ <a
10
+ href={
11
+ import.meta.env.IS_ZUPLO ? "https://zuplo.com" : "https://apitogo.com"
12
+ }
13
+ target="_blank"
14
+ rel="noopener noreferrer"
15
+ className={cn(
16
+ "flex justify-between items-center w-full border border-transparent hover:border-border rounded-full hover:shadow-xs h-7 px-3 text-nowrap hover:bg-muted/80 transition-all",
17
+ className,
18
+ )}
19
+ >
20
+ <div className="opacity-70 hover:opacity-100 transition-opacity gap-1.5 text-[11px] font-medium rounded-full h-7 flex items-center text-nowrap">
21
+ {import.meta.env.IS_ZUPLO ? (
22
+ <ZuploLogo className="w-3.5 h-3.5 dark:fill-white" />
23
+ ) : (
24
+ <img
25
+ src={poweredByIcon}
26
+ alt=""
27
+ aria-hidden="true"
28
+ className="w-3.5 h-3.5"
29
+ />
30
+ )}
31
+ powered by {import.meta.env.IS_ZUPLO ? "Zuplo" : "apitogo"}
32
+ </div>
33
+ <div className="text-xs font-medium opacity-70 hover:text-foreground transition-colors cursor-pointer">
34
+ <ChevronRightIcon
35
+ size={12}
36
+ absoluteStrokeWidth
37
+ strokeWidth={1.5}
38
+ className="rtl:rotate-180"
39
+ />
40
+ </div>
41
+ </a>
42
+ );