@wakastellar/ui 3.3.3 → 3.5.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/dist/badge-BbwO7QeZ.js +1 -0
- package/dist/badge-BfiocODp.mjs +23 -0
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +1 -1
- package/dist/chunk-14q5BKub.js +1 -0
- package/dist/{chunk-BH6uBOac.mjs → chunk-Cr9pTUWm.mjs} +5 -5
- package/dist/cn-DEtaFQsA.js +1 -0
- package/dist/cn-DUn6aSIQ.mjs +24 -0
- package/dist/doc.cjs.js +2 -2
- package/dist/doc.es.js +19 -19
- package/dist/editor.cjs.js +48 -0
- package/dist/editor.d.ts +1 -0
- package/dist/editor.es.js +6551 -0
- package/dist/{exceljs.min-DG9M8IZ1.mjs → exceljs.min-DL1XYDll.mjs} +1 -1
- package/dist/{exceljs.min-BuefmDRS.js → exceljs.min-qeIfSCbF.js} +1 -1
- package/dist/export.cjs.js +1 -1
- package/dist/export.es.js +1 -1
- package/dist/index.cjs.js +150 -150
- package/dist/index.es.js +26782 -27591
- package/dist/input-BfaSAGVw.js +1 -0
- package/dist/input-DVr_Qkl8.mjs +14 -0
- package/dist/rich-text.cjs.js +1 -1
- package/dist/rich-text.es.js +1 -1
- package/dist/security-CyBpuklN.mjs +122 -0
- package/dist/security-bFWwDrlg.js +1 -0
- package/dist/separator-NrkltulH.js +1 -0
- package/dist/separator-ibN2mycs.mjs +51 -0
- package/dist/src/components/editor/blocks/index.d.ts +51 -0
- package/dist/src/components/editor/blocks/waka-acceptance-criteria-block.d.ts +60 -0
- package/dist/src/components/editor/blocks/waka-ai-assist-block.d.ts +58 -0
- package/dist/src/components/editor/blocks/waka-api-endpoint-block.d.ts +63 -0
- package/dist/src/components/editor/blocks/waka-code-playground-block.d.ts +61 -0
- package/dist/src/components/editor/blocks/waka-comment-thread-block.d.ts +85 -0
- package/dist/src/components/editor/blocks/waka-diagram-block.d.ts +52 -0
- package/dist/src/components/editor/blocks/waka-embed-block.d.ts +58 -0
- package/dist/src/components/editor/blocks/waka-slash-menu-block.d.ts +67 -0
- package/dist/src/components/editor/blocks/waka-user-story-block.d.ts +79 -0
- package/dist/src/components/editor/blocks/waka-version-diff-block.d.ts +73 -0
- package/dist/src/components/editor/index.d.ts +66 -0
- package/dist/src/components/editor/waka-ai-writer.d.ts +80 -0
- package/dist/src/components/editor/waka-collaborative-editor.d.ts +93 -0
- package/dist/src/components/editor/waka-diff-viewer.d.ts +71 -0
- package/dist/src/components/editor/waka-dnd-editor.d.ts +64 -0
- package/dist/src/components/editor/waka-document-editor.d.ts +92 -0
- package/dist/src/components/editor/waka-editor-elements.d.ts +79 -0
- package/dist/src/components/editor/waka-editor-leaves.d.ts +39 -0
- package/dist/src/components/editor/waka-editor-plugins.d.ts +41 -0
- package/dist/src/components/editor/waka-editor-toolbar.d.ts +20 -0
- package/dist/src/components/editor/waka-editor.d.ts +59 -0
- package/dist/src/components/editor/waka-floating-toolbar.d.ts +47 -0
- package/dist/src/components/editor/waka-markdown-editor.d.ts +60 -0
- package/dist/src/components/editor/waka-mention-editor.d.ts +125 -0
- package/dist/src/components/editor/waka-slash-menu.d.ts +70 -0
- package/dist/src/components/editor/waka-spec-editor.d.ts +88 -0
- package/dist/src/components/index.d.ts +1 -15
- package/dist/src/editor.d.ts +26 -0
- package/dist/textarea-CdQWggYG.js +1 -0
- package/dist/textarea-DJDXJ3nd.mjs +23 -0
- package/dist/types-C2St0wOW.js +1 -0
- package/dist/{types-B6GVaSIP.mjs → types-JnqoLyuv.mjs} +214 -211
- package/dist/{useDataTableImport-BPvfo--2.mjs → useDataTableImport-BWUFesPi.mjs} +3 -3
- package/dist/{useDataTableImport-Cm_pCKnO.js → useDataTableImport-T7ddpN5k.js} +3 -3
- package/dist/waka-doc-renderer-CTxC7Trf.js +3 -0
- package/dist/{waka-doc-renderer-BkIvas3z.mjs → waka-doc-renderer-Cw-Xnyen.mjs} +264 -281
- package/dist/waka-editor-plugins-DR6tpsUC.mjs +135 -0
- package/dist/waka-editor-plugins-sGSh9hn2.js +1 -0
- package/dist/waka-rich-text-editor-BlIdtknG.js +1 -0
- package/dist/waka-rich-text-editor-D1uA3zbB.js +1 -0
- package/dist/waka-rich-text-editor-DgSWiXMW.mjs +342 -0
- package/dist/waka-rich-text-editor-DndVJuDw.mjs +2 -0
- package/package.json +87 -2
- package/src/blocks/footer/index.tsx +1 -6
- package/src/blocks/login/index.tsx +1 -7
- package/src/blocks/profile/index.tsx +3 -5
- package/src/components/editor/blocks/index.ts +182 -0
- package/src/components/editor/blocks/waka-acceptance-criteria-block.tsx +326 -0
- package/src/components/editor/blocks/waka-ai-assist-block.tsx +284 -0
- package/src/components/editor/blocks/waka-api-endpoint-block.tsx +382 -0
- package/src/components/editor/blocks/waka-code-playground-block.tsx +331 -0
- package/src/components/editor/blocks/waka-comment-thread-block.tsx +448 -0
- package/src/components/editor/blocks/waka-diagram-block.tsx +293 -0
- package/src/components/editor/blocks/waka-embed-block.tsx +416 -0
- package/src/components/editor/blocks/waka-slash-menu-block.tsx +432 -0
- package/src/components/editor/blocks/waka-user-story-block.tsx +295 -0
- package/src/components/editor/blocks/waka-version-diff-block.tsx +426 -0
- package/src/components/editor/index.ts +279 -0
- package/src/components/editor/waka-ai-writer.tsx +434 -0
- package/src/components/editor/waka-collaborative-editor.tsx +426 -0
- package/src/components/editor/waka-diff-viewer.tsx +352 -0
- package/src/components/editor/waka-dnd-editor.tsx +284 -0
- package/src/components/editor/waka-document-editor.tsx +502 -0
- package/src/components/editor/waka-editor-elements.tsx +312 -0
- package/src/components/editor/waka-editor-leaves.tsx +101 -0
- package/src/components/editor/waka-editor-plugins.ts +207 -0
- package/src/components/editor/waka-editor-toolbar.tsx +358 -0
- package/src/components/editor/waka-editor.tsx +431 -0
- package/src/components/editor/waka-floating-toolbar.tsx +268 -0
- package/src/components/editor/waka-markdown-editor.tsx +395 -0
- package/src/components/editor/waka-mention-editor.tsx +459 -0
- package/src/components/editor/waka-slash-menu.tsx +392 -0
- package/src/components/editor/waka-spec-editor.tsx +657 -0
- package/src/components/index.ts +1 -18
- package/dist/chunk-BDDJmn7V.js +0 -1
- package/dist/cn-DnPbmOCy.js +0 -1
- package/dist/cn-DpLcAzrf.mjs +0 -22
- package/dist/separator-BDReXBvI.mjs +0 -59
- package/dist/separator-BKjNl9sI.js +0 -1
- package/dist/src/components/waka-actor-badge/index.d.ts +0 -8
- package/dist/src/components/waka-actors-list/index.d.ts +0 -18
- package/dist/src/components/waka-ai-assistant-button/index.d.ts +0 -8
- package/dist/src/components/waka-document-flyover/index.d.ts +0 -10
- package/dist/src/components/waka-document-preview-popup/index.d.ts +0 -26
- package/dist/src/components/waka-hour-balance-badge/index.d.ts +0 -8
- package/dist/src/components/waka-hour-consumption-table/index.d.ts +0 -15
- package/dist/src/components/waka-hour-pack-dialog/index.d.ts +0 -8
- package/dist/src/components/waka-project-stats-header/index.d.ts +0 -15
- package/dist/src/components/waka-step-comment-bubble/index.d.ts +0 -13
- package/dist/src/components/waka-step-comment-panel/index.d.ts +0 -20
- package/dist/src/components/waka-step-permission-matrix/index.d.ts +0 -12
- package/dist/src/components/waka-time-entry-dialog/index.d.ts +0 -16
- package/dist/src/components/waka-time-tracking-flyover/index.d.ts +0 -11
- package/dist/types-BH9cQRqZ.js +0 -1
- package/dist/waka-doc-renderer-BZ2-SqyT.js +0 -3
- package/dist/waka-rich-text-editor-BJGlQgpq.js +0 -1
- package/dist/waka-rich-text-editor-BJzzxeP1.mjs +0 -361
- package/dist/waka-rich-text-editor-wnXLwvUo.js +0 -1
- package/src/components/waka-actor-badge/index.tsx +0 -34
- package/src/components/waka-actors-list/index.tsx +0 -125
- package/src/components/waka-ai-assistant-button/index.tsx +0 -31
- package/src/components/waka-document-flyover/index.tsx +0 -36
- package/src/components/waka-document-preview-popup/index.tsx +0 -103
- package/src/components/waka-hour-balance-badge/index.tsx +0 -43
- package/src/components/waka-hour-consumption-table/index.tsx +0 -72
- package/src/components/waka-hour-pack-dialog/index.tsx +0 -72
- package/src/components/waka-project-stats-header/index.tsx +0 -69
- package/src/components/waka-step-comment-bubble/index.tsx +0 -71
- package/src/components/waka-step-comment-panel/index.tsx +0 -106
- package/src/components/waka-step-permission-matrix/index.tsx +0 -65
- package/src/components/waka-time-entry-dialog/index.tsx +0 -131
- package/src/components/waka-time-tracking-flyover/index.tsx +0 -41
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../../utils/cn"
|
|
5
|
+
import { sanitizeUrl } from "../../../utils/security"
|
|
6
|
+
import type { PlateElementProps } from "../waka-editor-elements"
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export const EMBED_BLOCK_TYPE = "embed" as const
|
|
13
|
+
|
|
14
|
+
/** Supported embed provider types */
|
|
15
|
+
export type EmbedProvider =
|
|
16
|
+
| "figma"
|
|
17
|
+
| "youtube"
|
|
18
|
+
| "vimeo"
|
|
19
|
+
| "codesandbox"
|
|
20
|
+
| "codepen"
|
|
21
|
+
| "stackblitz"
|
|
22
|
+
| "loom"
|
|
23
|
+
| "miro"
|
|
24
|
+
| "notion"
|
|
25
|
+
| "github-gist"
|
|
26
|
+
| "generic"
|
|
27
|
+
|
|
28
|
+
/** Slate node for embed blocks */
|
|
29
|
+
export interface EmbedElement {
|
|
30
|
+
type: typeof EMBED_BLOCK_TYPE
|
|
31
|
+
/** Original URL pasted by the user */
|
|
32
|
+
url: string
|
|
33
|
+
/** Detected embed provider */
|
|
34
|
+
provider: EmbedProvider
|
|
35
|
+
/** Resolved embed/iframe URL */
|
|
36
|
+
embedUrl: string
|
|
37
|
+
/** Title/caption */
|
|
38
|
+
title?: string
|
|
39
|
+
/** Aspect ratio (e.g. "16/9", "4/3", "1/1") */
|
|
40
|
+
aspectRatio?: string
|
|
41
|
+
/** Optional maximum height in pixels */
|
|
42
|
+
maxHeight?: number
|
|
43
|
+
children: Array<{ text: string }>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface WakaEmbedBlockProps extends PlateElementProps {
|
|
47
|
+
element?: EmbedElement & Record<string, unknown>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Provider Configuration
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
interface ProviderConfig {
|
|
55
|
+
label: string
|
|
56
|
+
icon: string
|
|
57
|
+
color: string
|
|
58
|
+
bgColor: string
|
|
59
|
+
/** Transform original URL to embed URL */
|
|
60
|
+
transform: (url: string) => string | null
|
|
61
|
+
/** Default aspect ratio */
|
|
62
|
+
defaultAspect: string
|
|
63
|
+
/** Allow list for CSP (informational) */
|
|
64
|
+
domain: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const PROVIDERS: Record<EmbedProvider, ProviderConfig> = {
|
|
68
|
+
figma: {
|
|
69
|
+
label: "Figma",
|
|
70
|
+
icon: "M12 2a4 4 0 00-4 4v4a4 4 0 004-4V2zm0 12a4 4 0 01-4 4v2h4v-6zm8-4a4 4 0 01-4 4H12v-8h4a4 4 0 014 4z",
|
|
71
|
+
color: "text-purple-600 dark:text-purple-400",
|
|
72
|
+
bgColor: "bg-purple-50 dark:bg-purple-500/10",
|
|
73
|
+
transform: (url: string) => {
|
|
74
|
+
if (url.includes("figma.com")) {
|
|
75
|
+
return `https://www.figma.com/embed?embed_host=wakastart&url=${encodeURIComponent(url)}`
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
},
|
|
79
|
+
defaultAspect: "16/9",
|
|
80
|
+
domain: "figma.com",
|
|
81
|
+
},
|
|
82
|
+
youtube: {
|
|
83
|
+
label: "YouTube",
|
|
84
|
+
icon: "M19.615 3.184c-3.604-.246-11.631-.245-15.23 0C.488 3.45.029 5.804 0 12c.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0C23.512 20.55 23.971 18.196 24 12c-.029-6.185-.484-8.549-4.385-8.816zM9 16V8l8 4-8 4z",
|
|
85
|
+
color: "text-red-600 dark:text-red-400",
|
|
86
|
+
bgColor: "bg-red-50 dark:bg-red-500/10",
|
|
87
|
+
transform: (url: string) => {
|
|
88
|
+
const match = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]+)/)
|
|
89
|
+
if (match) return `https://www.youtube.com/embed/${match[1]}`
|
|
90
|
+
return null
|
|
91
|
+
},
|
|
92
|
+
defaultAspect: "16/9",
|
|
93
|
+
domain: "youtube.com",
|
|
94
|
+
},
|
|
95
|
+
vimeo: {
|
|
96
|
+
label: "Vimeo",
|
|
97
|
+
icon: "M22.875 10.063c-.098 2.141-1.592 5.075-4.483 8.8-2.988 3.891-5.517 5.836-7.588 5.836-1.282 0-2.367-1.184-3.258-3.553L6.14 15.15c-.589-2.369-1.221-3.553-1.896-3.553-.147 0-.663.31-1.546.928l-.925-1.19c.973-.855 1.932-1.71 2.877-2.565 1.297-1.12 2.27-1.71 2.921-1.77 1.534-.147 2.479.903 2.834 3.148.384 2.422.65 3.928.801 4.519.445 2.022.934 3.033 1.468 3.033.414 0 1.037-.656 1.868-1.967.831-1.311 1.276-2.31 1.336-2.998.119-1.14-.328-1.71-1.342-1.71-.478 0-.97.109-1.477.327.981-3.212 2.855-4.773 5.625-4.684 2.054.059 3.024 1.394 2.908 4.003z",
|
|
98
|
+
color: "text-sky-600 dark:text-sky-400",
|
|
99
|
+
bgColor: "bg-sky-50 dark:bg-sky-500/10",
|
|
100
|
+
transform: (url: string) => {
|
|
101
|
+
const match = url.match(/vimeo\.com\/(\d+)/)
|
|
102
|
+
if (match) return `https://player.vimeo.com/video/${match[1]}`
|
|
103
|
+
return null
|
|
104
|
+
},
|
|
105
|
+
defaultAspect: "16/9",
|
|
106
|
+
domain: "vimeo.com",
|
|
107
|
+
},
|
|
108
|
+
codesandbox: {
|
|
109
|
+
label: "CodeSandbox",
|
|
110
|
+
icon: "M2 6l10-4 10 4v12l-10 4L2 18V6zm10 2L4 4v8l8 4V8zm8-4l-8 4v8l8-4V4z",
|
|
111
|
+
color: "text-gray-700 dark:text-gray-300",
|
|
112
|
+
bgColor: "bg-gray-50 dark:bg-gray-500/10",
|
|
113
|
+
transform: (url: string) => {
|
|
114
|
+
const match = url.match(/codesandbox\.io\/(?:s|p)\/([a-zA-Z0-9-]+)/)
|
|
115
|
+
if (match) return `https://codesandbox.io/embed/${match[1]}?fontsize=14&hidenavigation=1&theme=dark`
|
|
116
|
+
return null
|
|
117
|
+
},
|
|
118
|
+
defaultAspect: "16/9",
|
|
119
|
+
domain: "codesandbox.io",
|
|
120
|
+
},
|
|
121
|
+
codepen: {
|
|
122
|
+
label: "CodePen",
|
|
123
|
+
icon: "M12 2L2 8.5v7L12 22l10-6.5v-7L12 2zm0 3.311L18.26 9 12 12.689 5.74 9 12 5.311zM4 10.289l6 3.9v5.522l-6-3.9v-5.522zm8 9.422v-5.522l6-3.9v5.522l-6 3.9z",
|
|
124
|
+
color: "text-gray-700 dark:text-gray-300",
|
|
125
|
+
bgColor: "bg-gray-50 dark:bg-gray-500/10",
|
|
126
|
+
transform: (url: string) => {
|
|
127
|
+
const match = url.match(/codepen\.io\/([^/]+)\/pen\/([a-zA-Z0-9]+)/)
|
|
128
|
+
if (match) return `https://codepen.io/${match[1]}/embed/${match[2]}?default-tab=result&theme-id=dark`
|
|
129
|
+
return null
|
|
130
|
+
},
|
|
131
|
+
defaultAspect: "16/9",
|
|
132
|
+
domain: "codepen.io",
|
|
133
|
+
},
|
|
134
|
+
stackblitz: {
|
|
135
|
+
label: "StackBlitz",
|
|
136
|
+
icon: "M13 10V3L4 14h7v7l9-11h-7z",
|
|
137
|
+
color: "text-blue-600 dark:text-blue-400",
|
|
138
|
+
bgColor: "bg-blue-50 dark:bg-blue-500/10",
|
|
139
|
+
transform: (url: string) => {
|
|
140
|
+
if (url.includes("stackblitz.com")) {
|
|
141
|
+
return url.replace("stackblitz.com/edit", "stackblitz.com/embed")
|
|
142
|
+
}
|
|
143
|
+
return null
|
|
144
|
+
},
|
|
145
|
+
defaultAspect: "16/9",
|
|
146
|
+
domain: "stackblitz.com",
|
|
147
|
+
},
|
|
148
|
+
loom: {
|
|
149
|
+
label: "Loom",
|
|
150
|
+
icon: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z",
|
|
151
|
+
color: "text-purple-600 dark:text-purple-400",
|
|
152
|
+
bgColor: "bg-purple-50 dark:bg-purple-500/10",
|
|
153
|
+
transform: (url: string) => {
|
|
154
|
+
const match = url.match(/loom\.com\/share\/([a-zA-Z0-9]+)/)
|
|
155
|
+
if (match) return `https://www.loom.com/embed/${match[1]}`
|
|
156
|
+
return null
|
|
157
|
+
},
|
|
158
|
+
defaultAspect: "16/9",
|
|
159
|
+
domain: "loom.com",
|
|
160
|
+
},
|
|
161
|
+
miro: {
|
|
162
|
+
label: "Miro",
|
|
163
|
+
icon: "M4 3h16a1 1 0 011 1v16a1 1 0 01-1 1H4a1 1 0 01-1-1V4a1 1 0 011-1z",
|
|
164
|
+
color: "text-yellow-600 dark:text-yellow-400",
|
|
165
|
+
bgColor: "bg-yellow-50 dark:bg-yellow-500/10",
|
|
166
|
+
transform: (url: string) => {
|
|
167
|
+
const match = url.match(/miro\.com\/app\/board\/([^/?]+)/)
|
|
168
|
+
if (match) return `https://miro.com/app/live-embed/${match[1]}/`
|
|
169
|
+
return null
|
|
170
|
+
},
|
|
171
|
+
defaultAspect: "16/9",
|
|
172
|
+
domain: "miro.com",
|
|
173
|
+
},
|
|
174
|
+
notion: {
|
|
175
|
+
label: "Notion",
|
|
176
|
+
icon: "M4 4a2 2 0 012-2h12a2 2 0 012 2v16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm4 2v4h8V6H8zm0 6v2h8v-2H8zm0 4v2h5v-2H8z",
|
|
177
|
+
color: "text-gray-800 dark:text-gray-200",
|
|
178
|
+
bgColor: "bg-gray-50 dark:bg-gray-500/10",
|
|
179
|
+
transform: () => null,
|
|
180
|
+
defaultAspect: "16/9",
|
|
181
|
+
domain: "notion.so",
|
|
182
|
+
},
|
|
183
|
+
"github-gist": {
|
|
184
|
+
label: "GitHub Gist",
|
|
185
|
+
icon: "M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.009-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.161 22 16.416 22 12c0-5.523-4.477-10-10-10z",
|
|
186
|
+
color: "text-gray-800 dark:text-gray-200",
|
|
187
|
+
bgColor: "bg-gray-50 dark:bg-gray-500/10",
|
|
188
|
+
transform: () => null,
|
|
189
|
+
defaultAspect: "4/3",
|
|
190
|
+
domain: "gist.github.com",
|
|
191
|
+
},
|
|
192
|
+
generic: {
|
|
193
|
+
label: "Embed",
|
|
194
|
+
icon: "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14",
|
|
195
|
+
color: "text-muted-foreground",
|
|
196
|
+
bgColor: "bg-muted",
|
|
197
|
+
transform: () => null,
|
|
198
|
+
defaultAspect: "16/9",
|
|
199
|
+
domain: "",
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Detect provider from a URL.
|
|
205
|
+
*/
|
|
206
|
+
export function detectEmbedProvider(url: string): EmbedProvider {
|
|
207
|
+
const lower = url.toLowerCase()
|
|
208
|
+
if (lower.includes("figma.com")) return "figma"
|
|
209
|
+
if (lower.includes("youtube.com") || lower.includes("youtu.be")) return "youtube"
|
|
210
|
+
if (lower.includes("vimeo.com")) return "vimeo"
|
|
211
|
+
if (lower.includes("codesandbox.io")) return "codesandbox"
|
|
212
|
+
if (lower.includes("codepen.io")) return "codepen"
|
|
213
|
+
if (lower.includes("stackblitz.com")) return "stackblitz"
|
|
214
|
+
if (lower.includes("loom.com")) return "loom"
|
|
215
|
+
if (lower.includes("miro.com")) return "miro"
|
|
216
|
+
if (lower.includes("notion.so")) return "notion"
|
|
217
|
+
if (lower.includes("gist.github.com")) return "github-gist"
|
|
218
|
+
return "generic"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Transform a URL into an embeddable iframe URL.
|
|
223
|
+
*/
|
|
224
|
+
export function resolveEmbedUrl(url: string, provider: EmbedProvider): string {
|
|
225
|
+
const config = PROVIDERS[provider]
|
|
226
|
+
const transformed = config.transform(url)
|
|
227
|
+
return transformed || url
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// Element Component
|
|
232
|
+
// ============================================================================
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* WakaEmbedBlock - A Plate.js block for embedding external content (Figma, YouTube,
|
|
236
|
+
* CodeSandbox, Loom, etc.) inline in a document. The user pastes a URL and the
|
|
237
|
+
* block auto-detects the provider and renders the appropriate embed.
|
|
238
|
+
*
|
|
239
|
+
* Uses `@platejs/media` patterns. All URLs are sanitized before embedding.
|
|
240
|
+
*
|
|
241
|
+
* Register in Plate editor:
|
|
242
|
+
* ```ts
|
|
243
|
+
* components: {
|
|
244
|
+
* [EMBED_BLOCK_TYPE]: WakaEmbedBlock,
|
|
245
|
+
* }
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
export function WakaEmbedBlock({
|
|
249
|
+
attributes,
|
|
250
|
+
children,
|
|
251
|
+
element,
|
|
252
|
+
className,
|
|
253
|
+
}: WakaEmbedBlockProps) {
|
|
254
|
+
const el = element as EmbedElement | undefined
|
|
255
|
+
const provider = el?.provider || "generic"
|
|
256
|
+
const config = PROVIDERS[provider]
|
|
257
|
+
const embedUrl = el?.embedUrl || ""
|
|
258
|
+
const aspectRatio = el?.aspectRatio || config.defaultAspect
|
|
259
|
+
|
|
260
|
+
// Sanitize the embed URL
|
|
261
|
+
const safeUrl = React.useMemo(() => {
|
|
262
|
+
if (!embedUrl) return ""
|
|
263
|
+
return sanitizeUrl(embedUrl)
|
|
264
|
+
}, [embedUrl])
|
|
265
|
+
|
|
266
|
+
const [isLoaded, setIsLoaded] = React.useState(false)
|
|
267
|
+
const [hasError, setHasError] = React.useState(false)
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div
|
|
271
|
+
{...attributes}
|
|
272
|
+
contentEditable={false}
|
|
273
|
+
className={cn(
|
|
274
|
+
"my-4 rounded-lg overflow-hidden border border-border shadow-sm",
|
|
275
|
+
className
|
|
276
|
+
)}
|
|
277
|
+
>
|
|
278
|
+
{/* Header */}
|
|
279
|
+
<div className="flex items-center justify-between px-3 py-2 bg-muted/30 border-b border-border">
|
|
280
|
+
<div className="flex items-center gap-2">
|
|
281
|
+
<svg
|
|
282
|
+
className={cn("h-4 w-4", config.color)}
|
|
283
|
+
viewBox="0 0 24 24"
|
|
284
|
+
fill="currentColor"
|
|
285
|
+
aria-hidden="true"
|
|
286
|
+
>
|
|
287
|
+
<path d={config.icon} />
|
|
288
|
+
</svg>
|
|
289
|
+
<span className={cn("text-[10px] font-bold uppercase tracking-wider", config.color)}>
|
|
290
|
+
{config.label}
|
|
291
|
+
</span>
|
|
292
|
+
{el?.title && (
|
|
293
|
+
<span className="text-xs text-muted-foreground ml-1 truncate max-w-[300px]">
|
|
294
|
+
{el.title}
|
|
295
|
+
</span>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
{el?.url && (
|
|
300
|
+
<a
|
|
301
|
+
href={sanitizeUrl(el.url)}
|
|
302
|
+
target="_blank"
|
|
303
|
+
rel="noopener noreferrer"
|
|
304
|
+
className="text-[10px] text-primary hover:underline flex items-center gap-1"
|
|
305
|
+
>
|
|
306
|
+
Open
|
|
307
|
+
<svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
308
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
309
|
+
</svg>
|
|
310
|
+
</a>
|
|
311
|
+
)}
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{/* Embed iframe */}
|
|
315
|
+
<div
|
|
316
|
+
className="relative bg-muted/10"
|
|
317
|
+
style={{ aspectRatio, maxHeight: el?.maxHeight ? `${el.maxHeight}px` : undefined }}
|
|
318
|
+
>
|
|
319
|
+
{/* Loading skeleton */}
|
|
320
|
+
{!isLoaded && !hasError && safeUrl && (
|
|
321
|
+
<div className="absolute inset-0 flex items-center justify-center bg-muted/20">
|
|
322
|
+
<svg className="h-8 w-8 animate-spin text-muted-foreground/30" viewBox="0 0 24 24" fill="none">
|
|
323
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
324
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
325
|
+
</svg>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
{/* Error state */}
|
|
330
|
+
{hasError && (
|
|
331
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center bg-muted/10 text-muted-foreground">
|
|
332
|
+
<svg className="h-8 w-8 mb-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
|
|
333
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
334
|
+
</svg>
|
|
335
|
+
<span className="text-xs">Failed to load embed</span>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
|
|
339
|
+
{/* No URL state */}
|
|
340
|
+
{!safeUrl && (
|
|
341
|
+
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
|
342
|
+
<svg className="h-10 w-10 mb-3 text-muted-foreground/30" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
|
|
343
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
|
344
|
+
</svg>
|
|
345
|
+
<span className="text-sm">Paste a URL to embed content</span>
|
|
346
|
+
<span className="text-[10px] mt-1 text-muted-foreground/60">
|
|
347
|
+
Supports Figma, YouTube, CodeSandbox, Loom, and more
|
|
348
|
+
</span>
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
|
|
352
|
+
{/* Iframe */}
|
|
353
|
+
{safeUrl && (
|
|
354
|
+
<iframe
|
|
355
|
+
src={safeUrl}
|
|
356
|
+
title={el?.title || `${config.label} embed`}
|
|
357
|
+
className={cn(
|
|
358
|
+
"w-full h-full border-0",
|
|
359
|
+
!isLoaded && "opacity-0"
|
|
360
|
+
)}
|
|
361
|
+
loading="lazy"
|
|
362
|
+
allowFullScreen
|
|
363
|
+
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
|
|
364
|
+
onLoad={() => setIsLoaded(true)}
|
|
365
|
+
onError={() => setHasError(true)}
|
|
366
|
+
/>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
{/* Hidden Slate children */}
|
|
371
|
+
<div className="hidden">{children}</div>
|
|
372
|
+
</div>
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
WakaEmbedBlock.displayName = "WakaEmbedBlock"
|
|
377
|
+
|
|
378
|
+
// ============================================================================
|
|
379
|
+
// Node Factory
|
|
380
|
+
// ============================================================================
|
|
381
|
+
|
|
382
|
+
export function createEmbedNodes(url: string, options?: { title?: string; aspectRatio?: string; maxHeight?: number }): EmbedElement[] {
|
|
383
|
+
const provider = detectEmbedProvider(url)
|
|
384
|
+
const embedUrl = resolveEmbedUrl(url, provider)
|
|
385
|
+
|
|
386
|
+
return [
|
|
387
|
+
{
|
|
388
|
+
type: EMBED_BLOCK_TYPE,
|
|
389
|
+
url,
|
|
390
|
+
provider,
|
|
391
|
+
embedUrl,
|
|
392
|
+
title: options?.title,
|
|
393
|
+
aspectRatio: options?.aspectRatio,
|
|
394
|
+
maxHeight: options?.maxHeight,
|
|
395
|
+
children: [{ text: "" }],
|
|
396
|
+
},
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function createEmbedPlugin() {
|
|
401
|
+
try {
|
|
402
|
+
const { createPlatePlugin } = await import("platejs/react")
|
|
403
|
+
return createPlatePlugin({
|
|
404
|
+
key: EMBED_BLOCK_TYPE,
|
|
405
|
+
node: {
|
|
406
|
+
isElement: true,
|
|
407
|
+
isVoid: true,
|
|
408
|
+
type: EMBED_BLOCK_TYPE,
|
|
409
|
+
component: WakaEmbedBlock,
|
|
410
|
+
},
|
|
411
|
+
})
|
|
412
|
+
} catch {
|
|
413
|
+
console.warn("[WakaEmbedBlock] platejs not installed")
|
|
414
|
+
return null
|
|
415
|
+
}
|
|
416
|
+
}
|