@leviosa-ai/detail-page-editor 0.2.2 → 0.3.0
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/components/cardnews/color-input.tsx +9 -9
- package/components/detail-page/ai-generate-panel.tsx +37 -37
- package/components/detail-page/annotation-canvas.tsx +9 -9
- package/components/detail-page/annotation-dialog.tsx +18 -18
- package/components/detail-page/canvas-context-menu.tsx +4 -4
- package/components/detail-page/chart-inspector.tsx +23 -23
- package/components/detail-page/design-reference-picker.tsx +22 -22
- package/components/detail-page/detail-page-basic-shapes.tsx +4 -4
- package/components/detail-page/detail-page-brand-gifs-panel.tsx +10 -10
- package/components/detail-page/detail-page-brand-kit-panel.tsx +5 -5
- package/components/detail-page/detail-page-brand-panel-header.tsx +7 -4
- package/components/detail-page/detail-page-charts-panel.tsx +4 -4
- package/components/detail-page/detail-page-decorations-panel.tsx +5 -5
- package/components/detail-page/detail-page-download-dialog.tsx +19 -19
- package/components/detail-page/detail-page-editor.tsx +131 -76
- package/components/detail-page/detail-page-elements-panel.tsx +4 -4
- package/components/detail-page/detail-page-font-picker.tsx +14 -14
- package/components/detail-page/detail-page-history-buttons.tsx +3 -3
- package/components/detail-page/detail-page-host-context.tsx +63 -0
- package/components/detail-page/detail-page-icons-panel.tsx +13 -13
- package/components/detail-page/detail-page-konva-json-preview.tsx +9 -9
- package/components/detail-page/detail-page-layers-panel.tsx +14 -14
- package/components/detail-page/detail-page-my-images-panel.tsx +29 -14
- package/components/detail-page/detail-page-my-shapes-panel.tsx +8 -8
- package/components/detail-page/detail-page-page-toolbar.tsx +10 -10
- package/components/detail-page/detail-page-pages-panel.tsx +13 -13
- package/components/detail-page/detail-page-pages-timeline.tsx +4 -4
- package/components/detail-page/detail-page-photos-panel.tsx +13 -13
- package/components/detail-page/detail-page-properties-panel.tsx +55 -55
- package/components/detail-page/detail-page-qr-panel.tsx +13 -13
- package/components/detail-page/detail-page-references-panel.tsx +9 -9
- package/components/detail-page/detail-page-sidebar-sections.tsx +1 -1
- package/components/detail-page/detail-page-tables-panel.tsx +4 -4
- package/components/detail-page/detail-page-text-panel.tsx +4 -4
- package/components/detail-page/edit-quota-ui.tsx +3 -3
- package/components/detail-page/element-recents-strip.tsx +6 -6
- package/components/detail-page/fill-control.tsx +12 -12
- package/components/detail-page/find-replace-panel.tsx +8 -8
- package/components/detail-page/generation-progress-fill.tsx +4 -4
- package/components/detail-page/gif-effect-picker.tsx +9 -9
- package/components/detail-page/group-prompt-edit-panel.tsx +3 -3
- package/components/detail-page/inspector-controls.tsx +9 -9
- package/components/detail-page/panel-search-input.tsx +2 -2
- package/components/detail-page/prompt-edit-panel.tsx +3 -3
- package/components/detail-page/section-reauthor-controller.tsx +2 -2
- package/components/detail-page/spec-prompt-edit-panel.tsx +3 -3
- package/components/detail-page/svg-prompt-edit-panel.tsx +3 -3
- package/components/detail-page/table-inspector.tsx +24 -24
- package/components/ui/button.tsx +6 -6
- package/components/ui/command.tsx +5 -5
- package/components/ui/dialog.tsx +3 -3
- package/components/ui/label.tsx +1 -1
- package/components/ui/select.tsx +3 -3
- package/components/ui/slider.tsx +1 -1
- package/components/ui/tooltip.tsx +1 -1
- package/lib/detail-page/use-progressive-reveal.ts +82 -0
- package/package.json +4 -1
- package/styles/canvas-bridge.css +35 -0
- package/styles/tokens.css +103 -0
package/components/ui/button.tsx
CHANGED
|
@@ -5,13 +5,13 @@ import { Slot } from "radix-ui"
|
|
|
5
5
|
import { cn } from "../../lib/utils"
|
|
6
6
|
|
|
7
7
|
const buttonVariants = cva(
|
|
8
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-dpe-md text-sm font-dpe-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
12
12
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
13
|
destructive:
|
|
14
|
-
"bg-destructive text-
|
|
14
|
+
"bg-destructive text-dpe-on-accent hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
15
15
|
outline:
|
|
16
16
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
17
17
|
secondary:
|
|
@@ -22,11 +22,11 @@ const buttonVariants = cva(
|
|
|
22
22
|
},
|
|
23
23
|
size: {
|
|
24
24
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
25
|
-
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
26
|
-
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
27
|
-
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
25
|
+
xs: "h-6 gap-1 rounded-dpe-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
26
|
+
sm: "h-8 rounded-dpe-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
27
|
+
lg: "h-10 rounded-dpe-md px-6 has-[>svg]:px-4",
|
|
28
28
|
icon: "size-9",
|
|
29
|
-
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
|
29
|
+
"icon-xs": "size-6 rounded-dpe-md [&_svg:not([class*='size-'])]:size-3",
|
|
30
30
|
"icon-sm": "size-8",
|
|
31
31
|
"icon-lg": "size-10",
|
|
32
32
|
},
|
|
@@ -20,7 +20,7 @@ const Command = React.forwardRef<
|
|
|
20
20
|
<CommandPrimitive
|
|
21
21
|
ref={ref}
|
|
22
22
|
className={cn(
|
|
23
|
-
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
|
23
|
+
"flex h-full w-full flex-col overflow-hidden rounded-dpe-md bg-popover text-popover-foreground",
|
|
24
24
|
className,
|
|
25
25
|
)}
|
|
26
26
|
{...props}
|
|
@@ -44,7 +44,7 @@ const CommandDialog = ({
|
|
|
44
44
|
Search and run available commands.
|
|
45
45
|
</DialogDescription>
|
|
46
46
|
</DialogHeader>
|
|
47
|
-
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
47
|
+
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-dpe-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
48
48
|
{children}
|
|
49
49
|
</Command>
|
|
50
50
|
</DialogContent>
|
|
@@ -66,7 +66,7 @@ const CommandInput = React.forwardRef<
|
|
|
66
66
|
<CommandPrimitive.Input
|
|
67
67
|
ref={ref}
|
|
68
68
|
className={cn(
|
|
69
|
-
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
69
|
+
"flex h-11 w-full rounded-dpe-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
70
70
|
className,
|
|
71
71
|
)}
|
|
72
72
|
{...props}
|
|
@@ -109,7 +109,7 @@ const CommandGroup = React.forwardRef<
|
|
|
109
109
|
<CommandPrimitive.Group
|
|
110
110
|
ref={ref}
|
|
111
111
|
className={cn(
|
|
112
|
-
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
112
|
+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-dpe-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
113
113
|
className,
|
|
114
114
|
)}
|
|
115
115
|
{...props}
|
|
@@ -137,7 +137,7 @@ const CommandItem = React.forwardRef<
|
|
|
137
137
|
<CommandPrimitive.Item
|
|
138
138
|
ref={ref}
|
|
139
139
|
className={cn(
|
|
140
|
-
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
|
|
140
|
+
"relative flex cursor-default select-none items-center rounded-dpe-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
|
|
141
141
|
className,
|
|
142
142
|
)}
|
|
143
143
|
{...props}
|
package/components/ui/dialog.tsx
CHANGED
|
@@ -39,7 +39,7 @@ function DialogOverlay({
|
|
|
39
39
|
<DialogPrimitive.Overlay
|
|
40
40
|
data-slot="dialog-overlay"
|
|
41
41
|
className={cn(
|
|
42
|
-
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[115] bg-
|
|
42
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[115] bg-dpe-scrim/50",
|
|
43
43
|
className,
|
|
44
44
|
)}
|
|
45
45
|
{...props}
|
|
@@ -64,7 +64,7 @@ function DialogContent({
|
|
|
64
64
|
<DialogPrimitive.Content
|
|
65
65
|
data-slot="dialog-content"
|
|
66
66
|
className={cn(
|
|
67
|
-
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[115] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
|
67
|
+
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[115] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-dpe-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
|
68
68
|
className,
|
|
69
69
|
)}
|
|
70
70
|
onOpenAutoFocus={(event) => {
|
|
@@ -134,7 +134,7 @@ function DialogTitle({
|
|
|
134
134
|
return (
|
|
135
135
|
<DialogPrimitive.Title
|
|
136
136
|
data-slot="dialog-title"
|
|
137
|
-
className={cn("text-lg leading-none font-semibold", className)}
|
|
137
|
+
className={cn("text-lg leading-none font-dpe-semibold", className)}
|
|
138
138
|
{...props}
|
|
139
139
|
/>
|
|
140
140
|
);
|
package/components/ui/label.tsx
CHANGED
|
@@ -13,7 +13,7 @@ function Label({
|
|
|
13
13
|
<LabelPrimitive.Root
|
|
14
14
|
data-slot="label"
|
|
15
15
|
className={cn(
|
|
16
|
-
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
16
|
+
"flex items-center gap-2 text-sm leading-none font-dpe-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
17
17
|
className
|
|
18
18
|
)}
|
|
19
19
|
{...props}
|
package/components/ui/select.tsx
CHANGED
|
@@ -37,7 +37,7 @@ function SelectTrigger({
|
|
|
37
37
|
data-slot="select-trigger"
|
|
38
38
|
data-size={size}
|
|
39
39
|
className={cn(
|
|
40
|
-
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
40
|
+
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-dpe-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
41
41
|
className
|
|
42
42
|
)}
|
|
43
43
|
{...props}
|
|
@@ -62,7 +62,7 @@ function SelectContent({
|
|
|
62
62
|
<SelectPrimitive.Content
|
|
63
63
|
data-slot="select-content"
|
|
64
64
|
className={cn(
|
|
65
|
-
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
|
65
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-dpe-md border shadow-md",
|
|
66
66
|
position === "popper" &&
|
|
67
67
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
68
68
|
className
|
|
@@ -109,7 +109,7 @@ function SelectItem({
|
|
|
109
109
|
<SelectPrimitive.Item
|
|
110
110
|
data-slot="select-item"
|
|
111
111
|
className={cn(
|
|
112
|
-
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
112
|
+
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-dpe-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
113
113
|
className
|
|
114
114
|
)}
|
|
115
115
|
{...props}
|
package/components/ui/slider.tsx
CHANGED
|
@@ -53,7 +53,7 @@ function Slider({
|
|
|
53
53
|
<SliderPrimitive.Thumb
|
|
54
54
|
data-slot="slider-thumb"
|
|
55
55
|
key={index}
|
|
56
|
-
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-
|
|
56
|
+
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-dpe-surface shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
|
57
57
|
/>
|
|
58
58
|
))}
|
|
59
59
|
</SliderPrimitive.Root>
|
|
@@ -42,7 +42,7 @@ function TooltipContent({
|
|
|
42
42
|
data-slot="tooltip-content"
|
|
43
43
|
sideOffset={sideOffset}
|
|
44
44
|
className={cn(
|
|
45
|
-
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
45
|
+
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-dpe-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
46
46
|
className
|
|
47
47
|
)}
|
|
48
48
|
{...props}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 긴 목록을 스크롤한 만큼만 그리는 훅.
|
|
7
|
+
*
|
|
8
|
+
* 브랜드 자산 서랍은 목록을 통째로 그렸다. 항목 하나가 곧 `<img>` 하나라 브랜드에
|
|
9
|
+
* 쌓인 사진이 수백 장이면 패널을 여는 순간 그만큼의 요청이 한꺼번에 나갔다. 편집기가
|
|
10
|
+
* 정작 필요한 것(문서 이미지·폰트)을 그 뒤에 줄 세우는 셈이다.
|
|
11
|
+
*
|
|
12
|
+
* 그래서 처음엔 한 화면 분량만 그리고, 바닥에 둔 감시자가 보일 때마다 조금씩 늘린다.
|
|
13
|
+
*
|
|
14
|
+
* **다 드러난 뒤에도 감시를 붙여 두면 안 된다.** 그리고 늘어난 뒤에는 감시자를 다시
|
|
15
|
+
* 붙여야 한다 — IntersectionObserver 는 "보이는 상태가 바뀔 때" 부르지, 계속 보이는
|
|
16
|
+
* 동안 다시 부르지 않는다. 짧은 목록에서 감시자가 화면에 남아 있으면 한 번 늘리고
|
|
17
|
+
* 멈춰 버린다. 아래 effect 가 `count` 마다 감시자를 새로 만드는 이유다.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const REVEAL_INITIAL = 12;
|
|
21
|
+
export const REVEAL_STEP = 12;
|
|
22
|
+
|
|
23
|
+
/** 다음에 몇 개까지 보일지. 전체를 넘지 않는다. */
|
|
24
|
+
export function nextRevealCount(
|
|
25
|
+
current: number,
|
|
26
|
+
total: number,
|
|
27
|
+
step: number = REVEAL_STEP,
|
|
28
|
+
): number {
|
|
29
|
+
return Math.min(total, current + Math.max(1, step));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ProgressiveReveal {
|
|
33
|
+
/** 지금 그려야 할 개수. `items.slice(0, visible)`. */
|
|
34
|
+
visible: number;
|
|
35
|
+
/** 목록 바닥에 둘 감시자. 더 없으면 그리지 않는다. */
|
|
36
|
+
sentinelRef: (node: HTMLElement | null) => void;
|
|
37
|
+
hasMore: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useProgressiveReveal(
|
|
41
|
+
total: number,
|
|
42
|
+
opts?: { initial?: number; step?: number; resetKey?: unknown },
|
|
43
|
+
): ProgressiveReveal {
|
|
44
|
+
const initial = opts?.initial ?? REVEAL_INITIAL;
|
|
45
|
+
const step = opts?.step ?? REVEAL_STEP;
|
|
46
|
+
const resetKey = opts?.resetKey;
|
|
47
|
+
const [count, setCount] = useState(initial);
|
|
48
|
+
const [node, setNode] = useState<HTMLElement | null>(null);
|
|
49
|
+
|
|
50
|
+
// 목록이 갈아끼워지면(브랜드 전환·새로고침) 다시 처음부터 센다. 안 그러면 200장을
|
|
51
|
+
// 보고 온 사람이 다른 브랜드로 옮겨도 200장을 그대로 요청한다.
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
setCount(initial);
|
|
54
|
+
}, [resetKey, initial]);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!node || count >= total) return;
|
|
58
|
+
// 서버 렌더나 옛 브라우저처럼 감시자가 없는 곳에서는 그냥 다 그린다 — 안 그리면
|
|
59
|
+
// 스크롤해도 영영 안 나온다.
|
|
60
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
61
|
+
setCount(total);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const observer = new IntersectionObserver(
|
|
65
|
+
(entries) => {
|
|
66
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
67
|
+
setCount((current) => nextRevealCount(current, total, step));
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
// 바닥에 닿기 전에 미리 굽는다 — 스크롤이 빈칸에 부딪히지 않게.
|
|
71
|
+
{ rootMargin: "200px" },
|
|
72
|
+
);
|
|
73
|
+
observer.observe(node);
|
|
74
|
+
return () => observer.disconnect();
|
|
75
|
+
}, [node, count, total, step]);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
visible: Math.min(count, total),
|
|
79
|
+
sentinelRef: setNode,
|
|
80
|
+
hasMore: count < total,
|
|
81
|
+
};
|
|
82
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leviosa-ai/detail-page-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Leviosa detail-page editor shell. Source-only: consumers compile it through Next transpilePackages. Every app coupling (sourcing server, brand store, cache keys, toast) arrives through the DetailPageHost interface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"exports": {
|
|
18
18
|
".": "./index.ts",
|
|
19
19
|
"./package.json": "./package.json",
|
|
20
|
+
"./tokens.css": "./styles/tokens.css",
|
|
21
|
+
"./canvas-bridge.css": "./styles/canvas-bridge.css",
|
|
20
22
|
"./host": "./components/detail-page/detail-page-host-context.tsx",
|
|
21
23
|
"./types": "./types/detail-page-canvas.ts",
|
|
22
24
|
"./i18n": "./i18n/index.ts",
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
},
|
|
35
37
|
"files": [
|
|
36
38
|
"index.ts",
|
|
39
|
+
"styles",
|
|
37
40
|
"components",
|
|
38
41
|
"config",
|
|
39
42
|
"i18n",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 캔버스 껍데기(좌측 레일 · 배율 버튼)를 편집기 토큰에 물린다.
|
|
3
|
+
*
|
|
4
|
+
* 레일은 `@leviosa-ai/canvas` 가 그리고, 그쪽은 Tailwind 를 안 쓴다(인라인 style 이다).
|
|
5
|
+
* 그래서 자기 변수 `--lc-*` 를 따로 읽는다. 이 파일은 그 변수를 편집기 토큰으로 이어
|
|
6
|
+
* 주므로, `--color-dpe-*` 하나만 바꿔도 레일까지 같이 따라온다.
|
|
7
|
+
*
|
|
8
|
+
* @import "@leviosa-ai/detail-page-editor/tokens.css";
|
|
9
|
+
* @import "@leviosa-ai/detail-page-editor/canvas-bridge.css";
|
|
10
|
+
*
|
|
11
|
+
* **기본으로 딸려 오지 않는 이유**: 이어 붙이면 색 공간이 달라 지금 화면과 아주 조금
|
|
12
|
+
* 어긋난다(레일 활성 배경이 sRGB 혼합 → oklab 혼합). 갈아입힐 사람에게는 무의미한
|
|
13
|
+
* 차이지만, 그대로 쓰는 앱의 화면을 말없이 바꿀 이유는 없다. 그래서 부르는 쪽이 고른다.
|
|
14
|
+
*
|
|
15
|
+
* 이걸 안 쓰고 레일만 따로 잡고 싶으면 딱지를 쓴다:
|
|
16
|
+
*
|
|
17
|
+
* [data-lc-part="rail"] { ... }
|
|
18
|
+
* [data-lc-part="rail-tab"] { ... }
|
|
19
|
+
* [data-lc-part="panel"] { ... }
|
|
20
|
+
* [data-lc-part="zoom"] { ... }
|
|
21
|
+
*/
|
|
22
|
+
[data-dpe-root] {
|
|
23
|
+
--lc-surface: var(--color-dpe-surface);
|
|
24
|
+
--lc-border: var(--color-dpe-ink-200);
|
|
25
|
+
--lc-fg: var(--color-dpe-ink-700);
|
|
26
|
+
--lc-fg-muted: var(--color-dpe-ink-500);
|
|
27
|
+
--lc-tab-fg: var(--color-dpe-ink-600);
|
|
28
|
+
--lc-tab-active-fg: var(--color-dpe-active-700);
|
|
29
|
+
--lc-tab-active-bg: color-mix(
|
|
30
|
+
in oklab,
|
|
31
|
+
var(--color-dpe-active-600) 10%,
|
|
32
|
+
transparent
|
|
33
|
+
);
|
|
34
|
+
--lc-radius: var(--radius-dpe-md);
|
|
35
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 편집기 크롬의 디자인 토큰.
|
|
3
|
+
*
|
|
4
|
+
* 소비자 앱이 자기 globals.css 에서 이 파일을 @import 하면 `dpe-*` 유틸리티가 생긴다.
|
|
5
|
+
*
|
|
6
|
+
* @import "@leviosa-ai/detail-page-editor/tokens.css";
|
|
7
|
+
*
|
|
8
|
+
* 기본값은 지금까지의 색 그대로다 — 아무것도 안 하면 겉보기가 안 바뀐다.
|
|
9
|
+
*
|
|
10
|
+
* 갈아 끼우는 법은 두 가지다.
|
|
11
|
+
*
|
|
12
|
+
* 1. 값만 바꾼다. 편집기 뿌리에만 먹이려면 `[data-dpe-root]` 로 좁힌다:
|
|
13
|
+
*
|
|
14
|
+
* [data-dpe-root] {
|
|
15
|
+
* --color-dpe-surface: #faf8f5;
|
|
16
|
+
* --color-dpe-ink-200: #e0dbd4;
|
|
17
|
+
* --radius-dpe-md: 0;
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* 2. 토큰으로 안 되는 것(간격·테두리 유무·글꼴)은 `data-dpe-part` 딱지를 CSS 로 잡는다:
|
|
21
|
+
*
|
|
22
|
+
* [data-dpe-part="header"] { border-bottom: none; }
|
|
23
|
+
* [data-dpe-part="inspector"] { font-family: var(--font-serif); }
|
|
24
|
+
*
|
|
25
|
+
* 글꼴은 토큰이 없다 — 편집기는 글꼴을 지정하지 않고 물려받으므로,
|
|
26
|
+
* `[data-dpe-root] { font-family: ... }` 한 줄이면 전부 따라온다.
|
|
27
|
+
*/
|
|
28
|
+
@theme {
|
|
29
|
+
/* 면과 글자의 회색 램프. 편집기 크롬 대부분이 여기 달려 있다. */
|
|
30
|
+
--color-dpe-ink-50: oklch(98.5% 0 0);
|
|
31
|
+
--color-dpe-ink-100: oklch(97% 0 0);
|
|
32
|
+
--color-dpe-ink-200: oklch(92.2% 0 0);
|
|
33
|
+
--color-dpe-ink-300: oklch(87% 0 0);
|
|
34
|
+
--color-dpe-ink-400: oklch(70.8% 0 0);
|
|
35
|
+
--color-dpe-ink-500: oklch(55.6% 0 0);
|
|
36
|
+
--color-dpe-ink-600: oklch(43.9% 0 0);
|
|
37
|
+
--color-dpe-ink-700: oklch(37.1% 0 0);
|
|
38
|
+
--color-dpe-ink-800: oklch(26.9% 0 0);
|
|
39
|
+
--color-dpe-ink-900: oklch(20.5% 0 0);
|
|
40
|
+
--color-dpe-ink-950: oklch(14.5% 0 0);
|
|
41
|
+
|
|
42
|
+
/* 면(패널·카드·헤더)의 바탕. */
|
|
43
|
+
--color-dpe-surface: #ffffff;
|
|
44
|
+
|
|
45
|
+
/* 어두운 바탕 위 글자·아이콘. */
|
|
46
|
+
--color-dpe-on-accent: #ffffff;
|
|
47
|
+
|
|
48
|
+
/* 오버레이 막. 투명도는 쓰는 쪽이 `/70` 처럼 붙인다. */
|
|
49
|
+
--color-dpe-scrim: #000000;
|
|
50
|
+
|
|
51
|
+
/* 지우기·실패. */
|
|
52
|
+
--color-dpe-danger-50: oklch(97.1% 0.013 17.38);
|
|
53
|
+
--color-dpe-danger-100: oklch(93.6% 0.032 17.717);
|
|
54
|
+
--color-dpe-danger-200: oklch(88.5% 0.062 18.334);
|
|
55
|
+
--color-dpe-danger-500: oklch(63.7% 0.237 25.331);
|
|
56
|
+
--color-dpe-danger-600: oklch(57.7% 0.245 27.325);
|
|
57
|
+
--color-dpe-danger-700: oklch(50.5% 0.213 27.518);
|
|
58
|
+
|
|
59
|
+
/* 주의·한도. */
|
|
60
|
+
--color-dpe-warn-50: oklch(98.7% 0.022 95.277);
|
|
61
|
+
--color-dpe-warn-100: oklch(96.2% 0.059 95.617);
|
|
62
|
+
--color-dpe-warn-200: oklch(92.4% 0.12 95.746);
|
|
63
|
+
--color-dpe-warn-500: oklch(76.9% 0.188 70.08);
|
|
64
|
+
--color-dpe-warn-600: oklch(66.6% 0.179 58.318);
|
|
65
|
+
--color-dpe-warn-700: oklch(55.5% 0.163 48.998);
|
|
66
|
+
--color-dpe-warn-800: oklch(47.3% 0.137 46.201);
|
|
67
|
+
|
|
68
|
+
/* 저장됨·성공. */
|
|
69
|
+
--color-dpe-ok-50: oklch(97.9% 0.021 166.113);
|
|
70
|
+
--color-dpe-ok-100: oklch(95% 0.052 163.051);
|
|
71
|
+
--color-dpe-ok-600: oklch(59.6% 0.145 163.225);
|
|
72
|
+
--color-dpe-ok-700: oklch(50.8% 0.118 165.612);
|
|
73
|
+
|
|
74
|
+
/* 선택·드롭 위치 표시. */
|
|
75
|
+
--color-dpe-select-50: oklch(97.7% 0.013 236.62);
|
|
76
|
+
--color-dpe-select-100: oklch(95.1% 0.026 236.824);
|
|
77
|
+
--color-dpe-select-200: oklch(90.1% 0.058 230.902);
|
|
78
|
+
--color-dpe-select-400: oklch(74.6% 0.16 232.661);
|
|
79
|
+
--color-dpe-select-500: oklch(68.5% 0.169 237.323);
|
|
80
|
+
--color-dpe-select-900: oklch(39.1% 0.09 240.876);
|
|
81
|
+
--color-dpe-select-950: oklch(29.3% 0.066 243.157);
|
|
82
|
+
|
|
83
|
+
/* 켜져 있는 상태. */
|
|
84
|
+
--color-dpe-active-50: oklch(97% 0.014 254.604);
|
|
85
|
+
--color-dpe-active-600: oklch(54.6% 0.245 262.881);
|
|
86
|
+
--color-dpe-active-700: oklch(48.8% 0.243 264.376);
|
|
87
|
+
|
|
88
|
+
/* AI 표식(생성·재작성). */
|
|
89
|
+
--color-dpe-ai: oklch(66.7% 0.295 322.15);
|
|
90
|
+
--color-dpe-ai-alt: oklch(60.6% 0.25 292.717);
|
|
91
|
+
|
|
92
|
+
/* 모서리. `rounded-full` 은 알약·아바타라 토큰이 아니다. */
|
|
93
|
+
--radius-dpe-lg: 0.5rem;
|
|
94
|
+
--radius-dpe-md: 0.375rem;
|
|
95
|
+
--radius-dpe-sm: 0.25rem;
|
|
96
|
+
--radius-dpe-xl: 0.75rem;
|
|
97
|
+
|
|
98
|
+
/* 글자 굵기. */
|
|
99
|
+
--font-weight-dpe-bold: 700;
|
|
100
|
+
--font-weight-dpe-medium: 500;
|
|
101
|
+
--font-weight-dpe-normal: 400;
|
|
102
|
+
--font-weight-dpe-semibold: 600;
|
|
103
|
+
}
|