@bupple/vss-plugin-typography 1.0.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.
@@ -0,0 +1,318 @@
1
+ import { getCategories } from './chunk-YI7CKNOM.js';
2
+ import { createDefaultTransform } from '@bupple/vss/core';
3
+ import { useIconMap, useStudioStore, findAvailableTrack, cn, useItemDrag, ensureFontLoaded } from '@bupple/vss/ui';
4
+ import { useState, useCallback, useMemo, useEffect } from 'react';
5
+ import { jsxs, jsx } from 'react/jsx-runtime';
6
+
7
+ function TextPanel() {
8
+ const icons = useIconMap();
9
+ const {
10
+ chevronDown: ChevronDownIcon,
11
+ add: PlusIcon,
12
+ search: SearchIcon,
13
+ itemText: TypeIcon,
14
+ close: XIcon
15
+ } = icons;
16
+ const settings = useStudioStore((s) => s.project.settings);
17
+ const addItem = useStudioStore((s) => s.addItem);
18
+ const addTrack = useStudioStore((s) => s.addTrack);
19
+ const currentFrame = useStudioStore((s) => s.player.currentFrame);
20
+ const tracks = useStudioStore((s) => s.timeline.tracks);
21
+ const items = useStudioStore((s) => s.timeline.items);
22
+ const itemIdsByTrack = useStudioStore((s) => s.timeline.itemIdsByTrack);
23
+ const [search, setSearch] = useState("");
24
+ const [collapsedCategories, setCollapsedCategories] = useState(
25
+ /* @__PURE__ */ new Set()
26
+ );
27
+ const toggleCategory = useCallback((id) => {
28
+ setCollapsedCategories((prev) => {
29
+ const next = new Set(prev);
30
+ if (next.has(id)) next.delete(id);
31
+ else next.add(id);
32
+ return next;
33
+ });
34
+ }, []);
35
+ const handleAddText = useCallback(
36
+ (preset) => {
37
+ const fps = settings.fps;
38
+ const durationFrames = fps * 5;
39
+ let resolvedTrackId;
40
+ const available = findAvailableTrack(
41
+ tracks,
42
+ itemIdsByTrack,
43
+ items,
44
+ "track",
45
+ currentFrame,
46
+ durationFrames
47
+ );
48
+ if (available) {
49
+ resolvedTrackId = available.id;
50
+ } else {
51
+ const existingCount = tracks.filter((t) => t.kind === "track").length;
52
+ resolvedTrackId = addTrack({
53
+ name: `Track ${existingCount + 1}`,
54
+ kind: "track",
55
+ order: tracks.length,
56
+ locked: false,
57
+ muted: false,
58
+ hidden: false
59
+ });
60
+ }
61
+ const item = {
62
+ type: "text",
63
+ trackId: resolvedTrackId,
64
+ startFrame: currentFrame,
65
+ durationFrames,
66
+ zIndex: 10,
67
+ locked: false,
68
+ disabled: false,
69
+ source: {},
70
+ transform: {
71
+ ...createDefaultTransform(settings),
72
+ width: settings.width * 0.8,
73
+ height: 0,
74
+ x: settings.width * 0.1,
75
+ y: settings.height * 0.4
76
+ },
77
+ style: { ...preset.style },
78
+ filters: [],
79
+ metadata: { presetId: preset.id }
80
+ };
81
+ addItem(item);
82
+ },
83
+ [tracks, settings, currentFrame, addItem, addTrack, items, itemIdsByTrack]
84
+ );
85
+ const handleAddBlank = useCallback(() => {
86
+ handleAddText({
87
+ id: "blank",
88
+ label: "Text",
89
+ category: "body",
90
+ style: {
91
+ text: "Type something...",
92
+ fontFamily: "Inter",
93
+ fontSize: 48,
94
+ fontWeight: 400,
95
+ fontStyle: "normal",
96
+ fontColor: "#ffffff",
97
+ textDecoration: [],
98
+ textTransform: "none",
99
+ backgroundColor: "transparent",
100
+ textAlign: "center",
101
+ verticalText: false,
102
+ lineHeight: 1.3,
103
+ letterSpacing: 0,
104
+ background: null,
105
+ shadow: null,
106
+ stroke: null,
107
+ glow: null,
108
+ curve: null
109
+ }
110
+ });
111
+ }, [handleAddText]);
112
+ const filteredCategories = useMemo(() => {
113
+ const categories = getCategories();
114
+ if (!search.trim()) return categories;
115
+ const q = search.toLowerCase();
116
+ return categories.map((cat) => ({
117
+ ...cat,
118
+ presets: cat.presets.filter(
119
+ (p) => p.label.toLowerCase().includes(q) || p.style.fontFamily?.toLowerCase().includes(q)
120
+ )
121
+ })).filter((cat) => cat.presets.length > 0);
122
+ }, [search]);
123
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col", children: [
124
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-b", children: [
125
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
126
+ /* @__PURE__ */ jsx(SearchIcon, { className: "absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground pointer-events-none" }),
127
+ /* @__PURE__ */ jsx(
128
+ "input",
129
+ {
130
+ placeholder: "Search presets...",
131
+ value: search,
132
+ onChange: (e) => setSearch(e.target.value),
133
+ className: "w-full pl-7 pr-7 h-7 text-xs bg-muted/40 rounded-md border-none outline-none focus:ring-1 focus:ring-ring"
134
+ }
135
+ ),
136
+ search && /* @__PURE__ */ jsx(
137
+ "button",
138
+ {
139
+ className: "absolute right-2 top-1/2 -translate-y-1/2 opacity-50 hover:opacity-100",
140
+ onClick: () => setSearch(""),
141
+ children: /* @__PURE__ */ jsx(XIcon, { className: "size-3" })
142
+ }
143
+ )
144
+ ] }),
145
+ /* @__PURE__ */ jsx(
146
+ "button",
147
+ {
148
+ onClick: handleAddBlank,
149
+ className: "flex size-7 shrink-0 items-center justify-center rounded-md bg-primary/10 hover:bg-primary/20 text-primary transition-colors",
150
+ title: "Add blank text",
151
+ children: /* @__PURE__ */ jsx(PlusIcon, { className: "size-3.5" })
152
+ }
153
+ )
154
+ ] }),
155
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: filteredCategories.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2 py-12", children: [
156
+ /* @__PURE__ */ jsx(TypeIcon, { className: "size-6 opacity-20" }),
157
+ /* @__PURE__ */ jsxs("p", { className: "text-xs opacity-40", children: [
158
+ 'No presets match "',
159
+ search,
160
+ '"'
161
+ ] })
162
+ ] }) : filteredCategories.map((category) => {
163
+ const isCollapsed = collapsedCategories.has(category.id);
164
+ return /* @__PURE__ */ jsxs("div", { children: [
165
+ /* @__PURE__ */ jsxs(
166
+ "button",
167
+ {
168
+ onClick: () => toggleCategory(category.id),
169
+ className: "flex w-full items-center gap-1.5 px-3 py-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground transition-colors",
170
+ children: [
171
+ /* @__PURE__ */ jsx(
172
+ ChevronDownIcon,
173
+ {
174
+ className: cn(
175
+ "size-3 transition-transform",
176
+ isCollapsed && "-rotate-90"
177
+ )
178
+ }
179
+ ),
180
+ category.name,
181
+ /* @__PURE__ */ jsx("span", { className: "ml-auto text-[9px] opacity-50", children: category.presets.length })
182
+ ]
183
+ }
184
+ ),
185
+ !isCollapsed && /* @__PURE__ */ jsx("div", { className: "px-2 pb-2 flex flex-col gap-1.5", children: category.presets.map((preset) => /* @__PURE__ */ jsx(
186
+ PresetCard,
187
+ {
188
+ preset,
189
+ onClick: () => handleAddText(preset)
190
+ },
191
+ preset.id
192
+ )) })
193
+ ] }, category.id);
194
+ }) })
195
+ ] });
196
+ }
197
+ function hexToRgba(hex, alpha) {
198
+ const r = parseInt(hex.slice(1, 3), 16);
199
+ const g = parseInt(hex.slice(3, 5), 16);
200
+ const b = parseInt(hex.slice(5, 7), 16);
201
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
202
+ }
203
+ function buildPreviewShadow(shadow, scale) {
204
+ if (!shadow) return void 0;
205
+ const rad = (shadow.angle ?? 135) * Math.PI / 180;
206
+ const dist = (shadow.distance ?? 4) * scale;
207
+ const ox = Math.round(Math.cos(rad) * dist);
208
+ const oy = Math.round(Math.sin(rad) * dist);
209
+ const blur = (shadow.blur ?? 0) * scale;
210
+ const alpha = (shadow.opacity ?? 100) / 100;
211
+ const color = shadow.color?.startsWith("#") ? hexToRgba(shadow.color, alpha) : shadow.color;
212
+ return `${ox}px ${oy}px ${blur}px ${color}`;
213
+ }
214
+ function buildPreviewGlow(glow, scale) {
215
+ if (!glow) return void 0;
216
+ const alpha = (glow.intensity ?? 50) / 100;
217
+ const color = glow.color?.startsWith("#") ? hexToRgba(glow.color, alpha) : glow.color;
218
+ const blur = (glow.range ?? 10) * scale;
219
+ return `${(glow.offsetX ?? 0) * scale}px ${(glow.offsetY ?? 0) * scale}px ${blur}px ${color}`;
220
+ }
221
+ function PresetCard({
222
+ preset,
223
+ onClick
224
+ }) {
225
+ const s = preset.style;
226
+ const scale = Math.min(s.fontSize / 3.5, 24) / s.fontSize;
227
+ const previewFontSize = s.fontSize * scale;
228
+ const bg = s.background;
229
+ const hasAdvancedBg = bg && bg.color;
230
+ const [fontLoaded, setFontLoaded] = useState(false);
231
+ const { dragProps, isDragging } = useItemDrag({
232
+ getData: () => ({
233
+ itemType: "text",
234
+ source: {},
235
+ style: { ...preset.style },
236
+ durationSeconds: 3,
237
+ name: preset.label
238
+ })
239
+ });
240
+ useEffect(() => {
241
+ if (s.fontFamily) {
242
+ ensureFontLoaded(s.fontFamily).then(() => setFontLoaded(true));
243
+ }
244
+ }, [s.fontFamily]);
245
+ const combinedTextShadow = (() => {
246
+ const parts = [];
247
+ const shadowStr = buildPreviewShadow(s.shadow, scale);
248
+ if (shadowStr) parts.push(shadowStr);
249
+ const glowStr = buildPreviewGlow(s.glow, scale);
250
+ if (glowStr) parts.push(glowStr);
251
+ return parts.length > 0 ? parts.join(", ") : void 0;
252
+ })();
253
+ const advancedBgStyle = hasAdvancedBg ? (() => {
254
+ const alpha = (bg.opacity ?? 100) / 100;
255
+ const color = bg.color.startsWith("#") ? hexToRgba(bg.color, alpha) : bg.color;
256
+ return {
257
+ backgroundColor: color,
258
+ borderRadius: `${bg.borderRadius ?? 0}%`,
259
+ padding: `${bg.height ?? 0}% ${bg.width ?? 0}%`
260
+ };
261
+ })() : void 0;
262
+ return /* @__PURE__ */ jsxs(
263
+ "div",
264
+ {
265
+ ...dragProps,
266
+ onClick,
267
+ className: cn(
268
+ "group rounded-lg border border-border/50 bg-muted/10 hover:bg-muted/40 overflow-hidden transition-colors text-left w-full cursor-grab active:cursor-grabbing select-none",
269
+ isDragging && "opacity-50"
270
+ ),
271
+ children: [
272
+ /* @__PURE__ */ jsxs(
273
+ "div",
274
+ {
275
+ className: "relative flex items-center justify-center h-12 px-3 overflow-hidden",
276
+ style: {
277
+ background: !hasAdvancedBg ? "linear-gradient(135deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.08) 100%)" : void 0
278
+ },
279
+ children: [
280
+ advancedBgStyle && /* @__PURE__ */ jsx(
281
+ "div",
282
+ {
283
+ className: "absolute inset-0 flex items-center justify-center",
284
+ style: advancedBgStyle
285
+ }
286
+ ),
287
+ /* @__PURE__ */ jsx(
288
+ "span",
289
+ {
290
+ className: "truncate max-w-full relative z-[1]",
291
+ style: {
292
+ fontFamily: fontLoaded ? s.fontFamily : "inherit",
293
+ fontSize: previewFontSize,
294
+ fontWeight: s.fontWeight,
295
+ fontStyle: s.fontStyle || void 0,
296
+ color: s.fontColor,
297
+ letterSpacing: s.letterSpacing ? `${s.letterSpacing * scale}px` : void 0,
298
+ textShadow: combinedTextShadow,
299
+ textDecoration: s.textDecoration?.includes("underline") ? "underline" : s.textDecoration?.includes("line-through") ? "line-through" : void 0,
300
+ textTransform: s.textTransform || void 0,
301
+ WebkitTextStroke: s.stroke ? `${Math.max(0.5, s.stroke.width * scale)}px ${s.stroke.color}` : void 0
302
+ },
303
+ children: s.text
304
+ }
305
+ )
306
+ ]
307
+ }
308
+ ),
309
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-2.5 py-1.5 border-t border-border/30", children: [
310
+ /* @__PURE__ */ jsx("p", { className: "text-[10px] font-medium truncate", children: preset.label }),
311
+ /* @__PURE__ */ jsx("p", { className: "text-[9px] opacity-40 ml-auto shrink-0", children: s.fontFamily })
312
+ ] })
313
+ ]
314
+ }
315
+ );
316
+ }
317
+
318
+ export { TextPanel as default };
@@ -0,0 +1,198 @@
1
+ 'use strict';
2
+
3
+ var chunk5IHSZBVF_cjs = require('./chunk-5IHSZBVF.cjs');
4
+ var core = require('@bupple/vss/core');
5
+ var ui = require('@bupple/vss/ui');
6
+ var react = require('react');
7
+ var jsxRuntime = require('react/jsx-runtime');
8
+
9
+ function TextPanelMobile() {
10
+ const icons = ui.useIconMap();
11
+ const settings = ui.useStudioStore((s) => s.project.settings);
12
+ const addItem = ui.useStudioStore((s) => s.addItem);
13
+ const addTrack = ui.useStudioStore((s) => s.addTrack);
14
+ const currentFrame = ui.useStudioStore((s) => s.player.currentFrame);
15
+ const tracks = ui.useStudioStore((s) => s.timeline.tracks);
16
+ const items = ui.useStudioStore((s) => s.timeline.items);
17
+ const itemIdsByTrack = ui.useStudioStore((s) => s.timeline.itemIdsByTrack);
18
+ const selectItem = ui.useStudioStore((s) => s.selectItem);
19
+ const categories = chunk5IHSZBVF_cjs.getCategories();
20
+ const [search, setSearch] = react.useState("");
21
+ react.useMemo(
22
+ () => categories.flatMap((c) => c.presets),
23
+ [categories]
24
+ );
25
+ const filteredCategories = react.useMemo(() => {
26
+ if (!search.trim()) return categories;
27
+ const q = search.toLowerCase();
28
+ return categories.map((cat) => ({
29
+ ...cat,
30
+ presets: cat.presets.filter(
31
+ (p) => p.label.toLowerCase().includes(q) || (p.style.fontFamily ?? "").toLowerCase().includes(q)
32
+ )
33
+ })).filter((cat) => cat.presets.length > 0);
34
+ }, [categories, search]);
35
+ const handleAddText = react.useCallback(
36
+ (preset) => {
37
+ const fps = settings.fps;
38
+ const durationFrames = fps * 5;
39
+ const available = ui.findAvailableTrack(
40
+ tracks,
41
+ itemIdsByTrack,
42
+ items,
43
+ "track",
44
+ currentFrame,
45
+ durationFrames
46
+ );
47
+ let trackId;
48
+ if (available) {
49
+ trackId = available.id;
50
+ } else {
51
+ trackId = addTrack({
52
+ name: "Text",
53
+ kind: "track",
54
+ order: tracks.length,
55
+ locked: false,
56
+ muted: false,
57
+ hidden: false
58
+ });
59
+ }
60
+ const transform = core.createDefaultTransform(settings);
61
+ transform.width = Math.round(settings.width * 0.8);
62
+ transform.height = Math.round(settings.height * 0.3);
63
+ transform.x = Math.round(settings.width * 0.1);
64
+ transform.y = Math.round(settings.height * 0.4);
65
+ const itemId = addItem({
66
+ type: "text",
67
+ trackId,
68
+ startFrame: currentFrame,
69
+ durationFrames,
70
+ zIndex: 10,
71
+ locked: false,
72
+ disabled: false,
73
+ source: {},
74
+ transform,
75
+ style: preset.style,
76
+ filters: [],
77
+ metadata: { name: preset.label, presetId: preset.id }
78
+ });
79
+ if (itemId) selectItem(itemId, false);
80
+ },
81
+ [
82
+ settings,
83
+ currentFrame,
84
+ tracks,
85
+ items,
86
+ itemIdsByTrack,
87
+ addItem,
88
+ addTrack,
89
+ selectItem
90
+ ]
91
+ );
92
+ const handleAddBlank = react.useCallback(() => {
93
+ handleAddText({
94
+ id: "blank",
95
+ label: "Text",
96
+ category: "basic",
97
+ style: {
98
+ text: "Your text here",
99
+ fontFamily: "Inter",
100
+ fontSize: 48,
101
+ fontWeight: 400,
102
+ color: "#ffffff",
103
+ textAlign: "center"
104
+ }
105
+ });
106
+ }, [handleAddText]);
107
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
108
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
109
+ /* @__PURE__ */ jsxRuntime.jsx(icons.search, { className: "absolute left-3 top-1/2 -translate-y-1/2 size-4 text-white/30" }),
110
+ /* @__PURE__ */ jsxRuntime.jsx(
111
+ "input",
112
+ {
113
+ type: "text",
114
+ value: search,
115
+ onChange: (e) => setSearch(e.target.value),
116
+ placeholder: "Search text styles...",
117
+ className: "w-full h-10 pl-9 pr-3 bg-white/5 border border-white/10 rounded-xl text-sm text-white placeholder:text-white/30 focus:outline-none focus:ring-1 focus:ring-white/20"
118
+ }
119
+ )
120
+ ] }),
121
+ /* @__PURE__ */ jsxRuntime.jsxs(
122
+ "button",
123
+ {
124
+ onClick: handleAddBlank,
125
+ className: "w-full h-11 rounded-xl bg-white/10 text-sm font-medium text-white flex items-center justify-center gap-2 active:bg-white/15 transition-colors",
126
+ children: [
127
+ /* @__PURE__ */ jsxRuntime.jsx(icons.add, { className: "size-4" }),
128
+ "Add blank text"
129
+ ]
130
+ }
131
+ ),
132
+ filteredCategories.map((category) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
133
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-white/40 uppercase tracking-wider font-medium", children: category.name }),
134
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-1.5", children: category.presets.map((preset) => /* @__PURE__ */ jsxRuntime.jsx(
135
+ PresetCard,
136
+ {
137
+ preset,
138
+ onAdd: handleAddText
139
+ },
140
+ preset.id
141
+ )) })
142
+ ] }, category.id)),
143
+ filteredCategories.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center py-8", children: [
144
+ /* @__PURE__ */ jsxRuntime.jsx(icons.itemText, { className: "size-6 text-white/15 mb-2" }),
145
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-white/30", children: "No presets found" })
146
+ ] })
147
+ ] });
148
+ }
149
+ function PresetCard({
150
+ preset,
151
+ onAdd
152
+ }) {
153
+ const style = preset.style;
154
+ react.useEffect(() => {
155
+ if (style.fontFamily) ui.ensureFontLoaded(style.fontFamily);
156
+ }, [style.fontFamily]);
157
+ return /* @__PURE__ */ jsxRuntime.jsxs(
158
+ "button",
159
+ {
160
+ onClick: () => onAdd(preset),
161
+ className: "rounded-xl bg-white/5 border border-white/5 overflow-hidden active:bg-white/10 transition-colors",
162
+ children: [
163
+ /* @__PURE__ */ jsxRuntime.jsx(
164
+ "div",
165
+ {
166
+ className: "h-14 flex items-center justify-center px-2 overflow-hidden",
167
+ style: {
168
+ backgroundColor: style.backgroundColor ?? "transparent"
169
+ },
170
+ children: /* @__PURE__ */ jsxRuntime.jsx(
171
+ "span",
172
+ {
173
+ className: "truncate",
174
+ style: {
175
+ fontFamily: style.fontFamily ?? "Inter",
176
+ fontSize: Math.min((style.fontSize ?? 48) / 2.5, 20),
177
+ fontWeight: style.fontWeight ?? 400,
178
+ fontStyle: style.fontStyle,
179
+ color: style.color ?? style.fontColor ?? "#ffffff",
180
+ letterSpacing: style.letterSpacing ? style.letterSpacing / 2 : void 0,
181
+ textTransform: style.textTransform,
182
+ textShadow: style.shadow ? `${style.shadow.offsetX ?? 0}px ${style.shadow.offsetY ?? 2}px ${style.shadow.blur ?? 4}px ${style.shadow.color ?? "rgba(0,0,0,0.5)"}` : void 0
183
+ },
184
+ children: style.text ?? preset.label
185
+ }
186
+ )
187
+ }
188
+ ),
189
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-2 py-1.5 border-t border-white/5", children: [
190
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-white/50 truncate", children: preset.label }),
191
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[9px] text-white/25 truncate", children: style.fontFamily ?? "Inter" })
192
+ ] })
193
+ ]
194
+ }
195
+ );
196
+ }
197
+
198
+ module.exports = TextPanelMobile;