@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.
Files changed (140) hide show
  1. package/dist/badge-BbwO7QeZ.js +1 -0
  2. package/dist/badge-BfiocODp.mjs +23 -0
  3. package/dist/charts.cjs.js +1 -1
  4. package/dist/charts.es.js +1 -1
  5. package/dist/chunk-14q5BKub.js +1 -0
  6. package/dist/{chunk-BH6uBOac.mjs → chunk-Cr9pTUWm.mjs} +5 -5
  7. package/dist/cn-DEtaFQsA.js +1 -0
  8. package/dist/cn-DUn6aSIQ.mjs +24 -0
  9. package/dist/doc.cjs.js +2 -2
  10. package/dist/doc.es.js +19 -19
  11. package/dist/editor.cjs.js +48 -0
  12. package/dist/editor.d.ts +1 -0
  13. package/dist/editor.es.js +6551 -0
  14. package/dist/{exceljs.min-DG9M8IZ1.mjs → exceljs.min-DL1XYDll.mjs} +1 -1
  15. package/dist/{exceljs.min-BuefmDRS.js → exceljs.min-qeIfSCbF.js} +1 -1
  16. package/dist/export.cjs.js +1 -1
  17. package/dist/export.es.js +1 -1
  18. package/dist/index.cjs.js +150 -150
  19. package/dist/index.es.js +26782 -27591
  20. package/dist/input-BfaSAGVw.js +1 -0
  21. package/dist/input-DVr_Qkl8.mjs +14 -0
  22. package/dist/rich-text.cjs.js +1 -1
  23. package/dist/rich-text.es.js +1 -1
  24. package/dist/security-CyBpuklN.mjs +122 -0
  25. package/dist/security-bFWwDrlg.js +1 -0
  26. package/dist/separator-NrkltulH.js +1 -0
  27. package/dist/separator-ibN2mycs.mjs +51 -0
  28. package/dist/src/components/editor/blocks/index.d.ts +51 -0
  29. package/dist/src/components/editor/blocks/waka-acceptance-criteria-block.d.ts +60 -0
  30. package/dist/src/components/editor/blocks/waka-ai-assist-block.d.ts +58 -0
  31. package/dist/src/components/editor/blocks/waka-api-endpoint-block.d.ts +63 -0
  32. package/dist/src/components/editor/blocks/waka-code-playground-block.d.ts +61 -0
  33. package/dist/src/components/editor/blocks/waka-comment-thread-block.d.ts +85 -0
  34. package/dist/src/components/editor/blocks/waka-diagram-block.d.ts +52 -0
  35. package/dist/src/components/editor/blocks/waka-embed-block.d.ts +58 -0
  36. package/dist/src/components/editor/blocks/waka-slash-menu-block.d.ts +67 -0
  37. package/dist/src/components/editor/blocks/waka-user-story-block.d.ts +79 -0
  38. package/dist/src/components/editor/blocks/waka-version-diff-block.d.ts +73 -0
  39. package/dist/src/components/editor/index.d.ts +66 -0
  40. package/dist/src/components/editor/waka-ai-writer.d.ts +80 -0
  41. package/dist/src/components/editor/waka-collaborative-editor.d.ts +93 -0
  42. package/dist/src/components/editor/waka-diff-viewer.d.ts +71 -0
  43. package/dist/src/components/editor/waka-dnd-editor.d.ts +64 -0
  44. package/dist/src/components/editor/waka-document-editor.d.ts +92 -0
  45. package/dist/src/components/editor/waka-editor-elements.d.ts +79 -0
  46. package/dist/src/components/editor/waka-editor-leaves.d.ts +39 -0
  47. package/dist/src/components/editor/waka-editor-plugins.d.ts +41 -0
  48. package/dist/src/components/editor/waka-editor-toolbar.d.ts +20 -0
  49. package/dist/src/components/editor/waka-editor.d.ts +59 -0
  50. package/dist/src/components/editor/waka-floating-toolbar.d.ts +47 -0
  51. package/dist/src/components/editor/waka-markdown-editor.d.ts +60 -0
  52. package/dist/src/components/editor/waka-mention-editor.d.ts +125 -0
  53. package/dist/src/components/editor/waka-slash-menu.d.ts +70 -0
  54. package/dist/src/components/editor/waka-spec-editor.d.ts +88 -0
  55. package/dist/src/components/index.d.ts +1 -15
  56. package/dist/src/editor.d.ts +26 -0
  57. package/dist/textarea-CdQWggYG.js +1 -0
  58. package/dist/textarea-DJDXJ3nd.mjs +23 -0
  59. package/dist/types-C2St0wOW.js +1 -0
  60. package/dist/{types-B6GVaSIP.mjs → types-JnqoLyuv.mjs} +214 -211
  61. package/dist/{useDataTableImport-BPvfo--2.mjs → useDataTableImport-BWUFesPi.mjs} +3 -3
  62. package/dist/{useDataTableImport-Cm_pCKnO.js → useDataTableImport-T7ddpN5k.js} +3 -3
  63. package/dist/waka-doc-renderer-CTxC7Trf.js +3 -0
  64. package/dist/{waka-doc-renderer-BkIvas3z.mjs → waka-doc-renderer-Cw-Xnyen.mjs} +264 -281
  65. package/dist/waka-editor-plugins-DR6tpsUC.mjs +135 -0
  66. package/dist/waka-editor-plugins-sGSh9hn2.js +1 -0
  67. package/dist/waka-rich-text-editor-BlIdtknG.js +1 -0
  68. package/dist/waka-rich-text-editor-D1uA3zbB.js +1 -0
  69. package/dist/waka-rich-text-editor-DgSWiXMW.mjs +342 -0
  70. package/dist/waka-rich-text-editor-DndVJuDw.mjs +2 -0
  71. package/package.json +87 -2
  72. package/src/blocks/footer/index.tsx +1 -6
  73. package/src/blocks/login/index.tsx +1 -7
  74. package/src/blocks/profile/index.tsx +3 -5
  75. package/src/components/editor/blocks/index.ts +182 -0
  76. package/src/components/editor/blocks/waka-acceptance-criteria-block.tsx +326 -0
  77. package/src/components/editor/blocks/waka-ai-assist-block.tsx +284 -0
  78. package/src/components/editor/blocks/waka-api-endpoint-block.tsx +382 -0
  79. package/src/components/editor/blocks/waka-code-playground-block.tsx +331 -0
  80. package/src/components/editor/blocks/waka-comment-thread-block.tsx +448 -0
  81. package/src/components/editor/blocks/waka-diagram-block.tsx +293 -0
  82. package/src/components/editor/blocks/waka-embed-block.tsx +416 -0
  83. package/src/components/editor/blocks/waka-slash-menu-block.tsx +432 -0
  84. package/src/components/editor/blocks/waka-user-story-block.tsx +295 -0
  85. package/src/components/editor/blocks/waka-version-diff-block.tsx +426 -0
  86. package/src/components/editor/index.ts +279 -0
  87. package/src/components/editor/waka-ai-writer.tsx +434 -0
  88. package/src/components/editor/waka-collaborative-editor.tsx +426 -0
  89. package/src/components/editor/waka-diff-viewer.tsx +352 -0
  90. package/src/components/editor/waka-dnd-editor.tsx +284 -0
  91. package/src/components/editor/waka-document-editor.tsx +502 -0
  92. package/src/components/editor/waka-editor-elements.tsx +312 -0
  93. package/src/components/editor/waka-editor-leaves.tsx +101 -0
  94. package/src/components/editor/waka-editor-plugins.ts +207 -0
  95. package/src/components/editor/waka-editor-toolbar.tsx +358 -0
  96. package/src/components/editor/waka-editor.tsx +431 -0
  97. package/src/components/editor/waka-floating-toolbar.tsx +268 -0
  98. package/src/components/editor/waka-markdown-editor.tsx +395 -0
  99. package/src/components/editor/waka-mention-editor.tsx +459 -0
  100. package/src/components/editor/waka-slash-menu.tsx +392 -0
  101. package/src/components/editor/waka-spec-editor.tsx +657 -0
  102. package/src/components/index.ts +1 -18
  103. package/dist/chunk-BDDJmn7V.js +0 -1
  104. package/dist/cn-DnPbmOCy.js +0 -1
  105. package/dist/cn-DpLcAzrf.mjs +0 -22
  106. package/dist/separator-BDReXBvI.mjs +0 -59
  107. package/dist/separator-BKjNl9sI.js +0 -1
  108. package/dist/src/components/waka-actor-badge/index.d.ts +0 -8
  109. package/dist/src/components/waka-actors-list/index.d.ts +0 -18
  110. package/dist/src/components/waka-ai-assistant-button/index.d.ts +0 -8
  111. package/dist/src/components/waka-document-flyover/index.d.ts +0 -10
  112. package/dist/src/components/waka-document-preview-popup/index.d.ts +0 -26
  113. package/dist/src/components/waka-hour-balance-badge/index.d.ts +0 -8
  114. package/dist/src/components/waka-hour-consumption-table/index.d.ts +0 -15
  115. package/dist/src/components/waka-hour-pack-dialog/index.d.ts +0 -8
  116. package/dist/src/components/waka-project-stats-header/index.d.ts +0 -15
  117. package/dist/src/components/waka-step-comment-bubble/index.d.ts +0 -13
  118. package/dist/src/components/waka-step-comment-panel/index.d.ts +0 -20
  119. package/dist/src/components/waka-step-permission-matrix/index.d.ts +0 -12
  120. package/dist/src/components/waka-time-entry-dialog/index.d.ts +0 -16
  121. package/dist/src/components/waka-time-tracking-flyover/index.d.ts +0 -11
  122. package/dist/types-BH9cQRqZ.js +0 -1
  123. package/dist/waka-doc-renderer-BZ2-SqyT.js +0 -3
  124. package/dist/waka-rich-text-editor-BJGlQgpq.js +0 -1
  125. package/dist/waka-rich-text-editor-BJzzxeP1.mjs +0 -361
  126. package/dist/waka-rich-text-editor-wnXLwvUo.js +0 -1
  127. package/src/components/waka-actor-badge/index.tsx +0 -34
  128. package/src/components/waka-actors-list/index.tsx +0 -125
  129. package/src/components/waka-ai-assistant-button/index.tsx +0 -31
  130. package/src/components/waka-document-flyover/index.tsx +0 -36
  131. package/src/components/waka-document-preview-popup/index.tsx +0 -103
  132. package/src/components/waka-hour-balance-badge/index.tsx +0 -43
  133. package/src/components/waka-hour-consumption-table/index.tsx +0 -72
  134. package/src/components/waka-hour-pack-dialog/index.tsx +0 -72
  135. package/src/components/waka-project-stats-header/index.tsx +0 -69
  136. package/src/components/waka-step-comment-bubble/index.tsx +0 -71
  137. package/src/components/waka-step-comment-panel/index.tsx +0 -106
  138. package/src/components/waka-step-permission-matrix/index.tsx +0 -65
  139. package/src/components/waka-time-entry-dialog/index.tsx +0 -131
  140. 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
+ }