@linktr.ee/linkapp 0.0.36 → 0.0.38

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.
package/README.md CHANGED
@@ -190,6 +190,9 @@ export default defineConfig({
190
190
  title: 'My LinkApp Settings',
191
191
  uses_url: true,
192
192
  featured_chin_position: 'below',
193
+ featured_head_allow_unlocked_aspect_ratio: true,
194
+ sheet_behavior: 'expandGeneric',
195
+ featured_head_click_behavior: 'expand',
193
196
  elements: [
194
197
  {
195
198
  type: 'text',
@@ -20,7 +20,7 @@ declare global {
20
20
  }
21
21
  }
22
22
 
23
- // Preview props injected by dev server via Vite define
23
+ // Preview props injected by dev server via define
24
24
  declare const __PREVIEW_PROPS__: Record<string, unknown>;
25
25
 
26
26
  // Extract just the variables from THEME_PRESETS for theme lookups
@@ -1,15 +1,15 @@
1
- import { StrictMode } from 'react'
2
- import { createRoot } from 'react-dom/client'
3
- import Preview from './Preview'
4
- import './preview.css'
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import Preview from "./preview";
4
+ import "./preview.css";
5
5
 
6
- const rootElement = document.getElementById('root')
6
+ const rootElement = document.getElementById("root");
7
7
  if (!rootElement) {
8
- throw new Error('Root element not found')
8
+ throw new Error("Root element not found");
9
9
  }
10
10
 
11
11
  createRoot(rootElement).render(
12
12
  <StrictMode>
13
13
  <Preview />
14
- </StrictMode>
15
- )
14
+ </StrictMode>,
15
+ );
@@ -0,0 +1,417 @@
1
+ import { Portal } from "@radix-ui/react-portal";
2
+ import IframeResizer, { type IFrameComponent } from "iframe-resizer-react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { SettingsPreview } from "../components/settings-preview";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogHeader,
9
+ DialogOverlay,
10
+ DialogTitle,
11
+ } from "../components/ui/dialog";
12
+ import {
13
+ Tabs,
14
+ TabsContent,
15
+ TabsList,
16
+ TabsTrigger,
17
+ } from "../components/ui/tabs";
18
+ import { cn, renderCssVariables } from "../lib/utils";
19
+ import { THEME_PRESETS } from "../shared/theme-presets";
20
+
21
+ function Chin({ title }: { title?: string }) {
22
+ if (!title) return null;
23
+
24
+ return (
25
+ <div
26
+ data-testid="NewLinkChin"
27
+ data-animation-target="link-chin"
28
+ className="relative flex min-h-16 flex-col justify-center gap-[1px] py-2 tracking-[-0.2px] items-center px-8"
29
+ >
30
+ <div className="w-full overflow-hidden overflow-ellipsis text-balance text-[14px] font-medium leading-[1.2] text-[var(--button-style-text)] sm:text-[16px] line-clamp-2 text-center">
31
+ {title}
32
+ </div>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ export default function Preview() {
38
+ // Initialize state from localStorage
39
+ const [selectedTab, setSelectedTab] = useState<
40
+ "sheet" | "featured" | "settings"
41
+ >(() => {
42
+ const saved = localStorage.getItem("linkapp-preview-tab");
43
+ return (saved as "sheet" | "featured" | "settings") || "sheet";
44
+ });
45
+ const [selectedTheme, setSelectedTheme] = useState<
46
+ keyof typeof THEME_PRESETS
47
+ >(() => {
48
+ const saved = localStorage.getItem("linkapp-preview-theme");
49
+ return (saved as keyof typeof THEME_PRESETS) || "default";
50
+ });
51
+ const [selectedGroupLayoutOption, setSelectedGroupLayoutOption] = useState<
52
+ string | undefined
53
+ >(() => {
54
+ const saved = localStorage.getItem("linkapp-preview-groupLayoutOption");
55
+ return saved || undefined;
56
+ });
57
+
58
+ // Chin configuration from build-time constants
59
+ const chinPosition = __SETTINGS_CONFIG__?.featured_chin_position;
60
+ const chinTitle = __PREVIEW_PROPS__?.linkTitle as string | undefined;
61
+ const isOverlay =
62
+ chinPosition === "overlayAbove" || chinPosition === "overlayBelow";
63
+
64
+ // Popup dialog state
65
+ const [isPopupOpen, setIsPopupOpen] = useState(false);
66
+
67
+ // Generate unique IDs for iframes using timestamp
68
+ const sheetIframeId = useMemo(() => `preview-iframe-sheet-${Date.now()}`, []);
69
+ const featuredIframeId = useMemo(
70
+ () => `preview-iframe-featured-${Date.now()}`,
71
+ [],
72
+ );
73
+ const popupIframeId = useMemo(() => `preview-iframe-popup-${Date.now()}`, []);
74
+
75
+ // Save selected tab to localStorage
76
+ useEffect(() => {
77
+ localStorage.setItem("linkapp-preview-tab", selectedTab);
78
+ }, [selectedTab]);
79
+
80
+ // Save selected theme to localStorage
81
+ useEffect(() => {
82
+ localStorage.setItem("linkapp-preview-theme", selectedTheme);
83
+ }, [selectedTheme]);
84
+
85
+ // Save selected groupLayoutOption to localStorage
86
+ useEffect(() => {
87
+ if (selectedGroupLayoutOption) {
88
+ localStorage.setItem(
89
+ "linkapp-preview-groupLayoutOption",
90
+ selectedGroupLayoutOption,
91
+ );
92
+ } else {
93
+ localStorage.removeItem("linkapp-preview-groupLayoutOption");
94
+ }
95
+ }, [selectedGroupLayoutOption]);
96
+
97
+ // Handle iframe resize events
98
+ const handleResized = useCallback(
99
+ (ev: {
100
+ iframe: IFrameComponent;
101
+ height: number;
102
+ width: number;
103
+ type: string;
104
+ }): void => {
105
+ // IframeResizer automatically handles height adjustments
106
+ // This callback is available for debugging if needed
107
+ },
108
+ [],
109
+ );
110
+
111
+ // Handle postMessage from featured iframe for EXPAND_LINK_APP
112
+ const handleMessage = useCallback((event: MessageEvent) => {
113
+ if (
114
+ event.data &&
115
+ typeof event.data === "object" &&
116
+ event.data.source === "linkapp" &&
117
+ event.data.type === "EXPAND_LINK_APP"
118
+ ) {
119
+ setIsPopupOpen(true);
120
+ }
121
+ }, []);
122
+
123
+ // Add message listener
124
+ useEffect(() => {
125
+ window.addEventListener("message", handleMessage);
126
+ return () => window.removeEventListener("message", handleMessage);
127
+ }, [handleMessage]);
128
+
129
+ const renderedCssVariables = useMemo(() => {
130
+ const themeVariables =
131
+ THEME_PRESETS[selectedTheme] || THEME_PRESETS.default;
132
+ return renderCssVariables(themeVariables.variables);
133
+ }, [selectedTheme]);
134
+
135
+ return (
136
+ <>
137
+ <style>{`:root {
138
+ ${renderedCssVariables}
139
+ }`}</style>
140
+
141
+ <div
142
+ className={cn("min-h-screen", {
143
+ "bg-black/50": selectedTab === "sheet",
144
+ "bg-linktree-frame": selectedTab === "featured",
145
+ })}
146
+ id="preview"
147
+ >
148
+ <Tabs
149
+ value={selectedTab}
150
+ onValueChange={(value) =>
151
+ setSelectedTab(value as "sheet" | "featured" | "settings")
152
+ }
153
+ >
154
+ <Portal>
155
+ <div
156
+ className="fixed top-0 left-0 right-0 p-4 flex justify-center gap-4 bg-background border-b"
157
+ style={{ zIndex: 1000000 }}
158
+ >
159
+ <TabsList>
160
+ <TabsTrigger value="sheet">Sheet</TabsTrigger>
161
+ <TabsTrigger value="featured">Featured</TabsTrigger>
162
+ <TabsTrigger value="settings">Settings</TabsTrigger>
163
+ </TabsList>
164
+
165
+ {/* Theme Switcher */}
166
+ <div className="flex items-center gap-2">
167
+ <label htmlFor="theme-select" className="text-sm font-medium">
168
+ Theme:
169
+ </label>
170
+ <select
171
+ id="theme-select"
172
+ value={selectedTheme}
173
+ onChange={(e) =>
174
+ setSelectedTheme(
175
+ e.target.value as keyof typeof THEME_PRESETS,
176
+ )
177
+ }
178
+ className="h-9 px-3 py-1 text-sm border border-input bg-background rounded-md focus:outline-none focus:ring-2 focus:ring-ring"
179
+ >
180
+ {Object.entries(THEME_PRESETS).map(([key, theme]) => (
181
+ <option key={key} value={key}>
182
+ {theme.name}
183
+ </option>
184
+ ))}
185
+ </select>
186
+ </div>
187
+
188
+ {/* Group Layout Option Switcher - Only show for Featured tab */}
189
+ {/*{selectedTab === "featured" && (
190
+ <div className="flex items-center gap-2">
191
+ <label htmlFor="groupLayoutOption-select" className="text-sm font-medium">
192
+ Layout:
193
+ </label>
194
+ <select
195
+ id="groupLayoutOption-select"
196
+ value={selectedGroupLayoutOption || "default"}
197
+ onChange={(e) =>
198
+ setSelectedGroupLayoutOption(e.target.value === "default" ? undefined : e.target.value)
199
+ }
200
+ className="h-9 px-3 py-1 text-sm border border-input bg-background rounded-md focus:outline-none focus:ring-2 focus:ring-ring"
201
+ >
202
+ <option value="default">Default</option>
203
+ <option value="carousel">Carousel</option>
204
+ </select>
205
+ </div>
206
+ )}*/}
207
+ </div>
208
+ </Portal>
209
+
210
+ {/* Main Content with Padding for Fixed Tabs */}
211
+ <div className="pt-20">
212
+ <TabsContent value="sheet" className="m-0">
213
+ {/* Sheet Modal - Always Open */}
214
+ <Dialog open={true} modal={false}>
215
+ <DialogOverlay />
216
+ <DialogContent
217
+ className="h-[calc(100dvh-2rem)] max-w-[608px] md:min-h-[25vh] md:h-[80%] md:max-h-[900px] overflow-auto"
218
+ showCloseButton={false}
219
+ >
220
+ <DialogHeader className="sticky top-0 bg-white px-4">
221
+ <div className="grid h-16 grid-cols-[32px_auto_32px] items-center gap-4">
222
+ <button className="flex size-8 items-center justify-center rounded-sm focus-visible:outline-none">
223
+ <svg
224
+ width="16"
225
+ height="16"
226
+ viewBox="0 0 16 16"
227
+ fill="none"
228
+ xmlns="http://www.w3.org/2000/svg"
229
+ className=" "
230
+ role="img"
231
+ aria-hidden="true"
232
+ >
233
+ <path
234
+ fill="currentColor"
235
+ d="m10.65 3.85.35.36.7-.71-.35-.35-3-3h-.7l-3 3-.36.35.71.7.35-.35L7.5 1.71V10h1V1.7l2.15 2.15ZM1 5.5l.5-.5H4v1H2v9h12V6h-2V5h2.5l.5.5v10l-.5.5h-13l-.5-.5v-10Z"
236
+ ></path>
237
+ </svg>
238
+ </button>
239
+
240
+ <DialogTitle className="self-center truncate py-3 text-center">
241
+ Sheet
242
+ </DialogTitle>
243
+
244
+ <button className="flex size-8 items-center justify-center rounded-sm focus-visible:outline-none">
245
+ <svg
246
+ width="16"
247
+ height="16"
248
+ viewBox="0 0 16 16"
249
+ fill="none"
250
+ xmlns="http://www.w3.org/2000/svg"
251
+ className=" "
252
+ role="img"
253
+ aria-hidden="true"
254
+ >
255
+ <path
256
+ fill="currentColor"
257
+ d="m13.63 3.12.37-.38-.74-.74-.38.37.75.75ZM2.37 12.89l-.37.37.74.74.38-.37-.75-.75Zm.75-10.52L2.74 2 2 2.74l.37.38.75-.75Zm9.76 11.26.38.37.74-.74-.37-.38-.75.75Zm0-11.26L2.38 12.9l.74.74 10.5-10.51-.74-.75Zm-10.5.75 10.5 10.5.75-.73L3.12 2.37l-.75.75Z"
258
+ ></path>
259
+ </svg>
260
+ </button>
261
+ </div>
262
+ </DialogHeader>
263
+
264
+ <div className="flex h-[calc(100%-64px)] flex-1 flex-col justify-between overflow-hidden">
265
+ <div className="h-full overflow-y-auto overflow-x-hidden">
266
+ <IframeResizer
267
+ key={`sheet-${selectedTheme}`}
268
+ id={sheetIframeId}
269
+ src={`/sheet?theme=${selectedTheme}`}
270
+ style={{
271
+ height: "0px",
272
+ width: "1px",
273
+ minWidth: "100%",
274
+ border: 0,
275
+ }}
276
+ checkOrigin={false}
277
+ onResized={handleResized}
278
+ heightCalculationMethod="max"
279
+ />
280
+ </div>
281
+ </div>
282
+ </DialogContent>
283
+ </Dialog>
284
+ </TabsContent>
285
+
286
+ <TabsContent
287
+ value="featured"
288
+ className="m-0 flex justify-center p-8"
289
+ >
290
+ <div className="w-full max-w-[580px] flex flex-col px-[28px] py-7 items-center bg-gray-100">
291
+ <div className="w-full max-w-[524px]">
292
+ {chinPosition === "above" && <Chin title={chinTitle} />}
293
+
294
+ <div
295
+ className={cn(
296
+ "bg-linktree-button-bg hover:bg-linktree-button-bg-hover border-linktree-button-border text-linktree-button-text rounded-linktree-button shadow-linktree-button overflow-hidden",
297
+ isOverlay && "relative",
298
+ )}
299
+ style={
300
+ !__SETTINGS_CONFIG__?.featured_head_allow_unlocked_aspect_ratio
301
+ ? { aspectRatio: "16 / 9" }
302
+ : undefined
303
+ }
304
+ >
305
+ {chinPosition === "overlay_above" && (
306
+ <div className="absolute top-0 left-0 right-0 z-10">
307
+ <Chin title={chinTitle} />
308
+ </div>
309
+ )}
310
+
311
+ <IframeResizer
312
+ key={`featured-${selectedTheme}-${selectedGroupLayoutOption || "default"}`}
313
+ id={featuredIframeId}
314
+ src={`/featured?theme=${selectedTheme}${selectedGroupLayoutOption ? `&groupLayoutOption=${selectedGroupLayoutOption}` : ""}`}
315
+ style={{
316
+ height: "0px",
317
+ width: "1px",
318
+ minWidth: "100%",
319
+ border: 0,
320
+ }}
321
+ checkOrigin={false}
322
+ onResized={handleResized}
323
+ heightCalculationMethod="max"
324
+ />
325
+
326
+ {chinPosition === "overlayBelow" && (
327
+ <div className="absolute bottom-0 left-0 right-0 z-10">
328
+ <Chin title={chinTitle} />
329
+ </div>
330
+ )}
331
+ </div>
332
+
333
+ {chinPosition === "below" && <Chin title={chinTitle} />}
334
+ </div>
335
+ </div>
336
+ </TabsContent>
337
+
338
+ <TabsContent value="settings" className="m-0">
339
+ <SettingsPreview settings={__SETTINGS_CONFIG__} />
340
+ </TabsContent>
341
+ </div>
342
+ </Tabs>
343
+
344
+ {/* Popup Dialog for EXPAND_LINK_APP message */}
345
+ <Dialog open={isPopupOpen} onOpenChange={setIsPopupOpen} modal={false}>
346
+ <DialogContent
347
+ className="h-[calc(100dvh-2rem)] max-w-[608px] md:min-h-[25vh] md:h-[80%] md:max-h-[900px] overflow-auto"
348
+ showCloseButton={false}
349
+ >
350
+ <DialogHeader className="sticky top-0 bg-white px-4">
351
+ <div className="grid h-16 grid-cols-[32px_auto_32px] items-center gap-4">
352
+ <button className="flex size-8 items-center justify-center rounded-sm focus-visible:outline-none">
353
+ <svg
354
+ width="16"
355
+ height="16"
356
+ viewBox="0 0 16 16"
357
+ fill="none"
358
+ xmlns="http://www.w3.org/2000/svg"
359
+ className=" "
360
+ role="img"
361
+ aria-hidden="true"
362
+ >
363
+ <path
364
+ fill="currentColor"
365
+ d="m10.65 3.85.35.36.7-.71-.35-.35-3-3h-.7l-3 3-.36.35.71.7.35-.35L7.5 1.71V10h1V1.7l2.15 2.15ZM1 5.5l.5-.5H4v1H2v9h12V6h-2V5h2.5l.5.5v10l-.5.5h-13l-.5-.5v-10Z"
366
+ ></path>
367
+ </svg>
368
+ </button>
369
+
370
+ <DialogTitle className="self-center truncate py-3 text-center">
371
+ Sheet
372
+ </DialogTitle>
373
+
374
+ <button className="flex size-8 items-center justify-center rounded-sm focus-visible:outline-none">
375
+ <svg
376
+ width="16"
377
+ height="16"
378
+ viewBox="0 0 16 16"
379
+ fill="none"
380
+ xmlns="http://www.w3.org/2000/svg"
381
+ className=" "
382
+ role="img"
383
+ aria-hidden="true"
384
+ >
385
+ <path
386
+ fill="currentColor"
387
+ d="m13.63 3.12.37-.38-.74-.74-.38.37.75.75ZM2.37 12.89l-.37.37.74.74.38-.37-.75-.75Zm.75-10.52L2.74 2 2 2.74l.37.38.75-.75Zm9.76 11.26.38.37.74-.74-.37-.38-.75.75Zm0-11.26L2.38 12.9l.74.74 10.5-10.51-.74-.75Zm-10.5.75 10.5 10.5.75-.73L3.12 2.37l-.75.75Z"
388
+ ></path>
389
+ </svg>
390
+ </button>
391
+ </div>
392
+ </DialogHeader>
393
+
394
+ <div className="flex h-[calc(100%-64px)] flex-1 flex-col justify-between overflow-hidden">
395
+ <div className="h-full overflow-y-auto overflow-x-hidden">
396
+ <IframeResizer
397
+ key={`popup-${selectedTheme}`}
398
+ id={popupIframeId}
399
+ src={`/sheet?theme=${selectedTheme}`}
400
+ style={{
401
+ height: "0px",
402
+ width: "1px",
403
+ minWidth: "100%",
404
+ border: 0,
405
+ }}
406
+ checkOrigin={false}
407
+ onResized={handleResized}
408
+ heightCalculationMethod="max"
409
+ />
410
+ </div>
411
+ </div>
412
+ </DialogContent>
413
+ </Dialog>
414
+ </div>
415
+ </>
416
+ );
417
+ }
@@ -12,7 +12,7 @@ declare global {
12
12
  }
13
13
  }
14
14
 
15
- // Preview props injected by dev server via Vite define
15
+ // Preview props injected by dev server via define
16
16
  declare const __PREVIEW_PROPS__: Record<string, unknown>;
17
17
 
18
18
  // Extract just the variables from THEME_PRESETS for theme lookups
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAoBA,UAAU,UAAU;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAkXD,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,iBAsSnD"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAmBA,UAAU,UAAU;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAmSD,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,iBAsUnD"}