@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
@@ -33,13 +33,11 @@ import {
33
33
  X,
34
34
  Loader2,
35
35
  Link as LinkIcon,
36
+ Twitter,
37
+ Github,
38
+ Linkedin,
36
39
  } from "lucide-react"
37
40
 
38
- // Inline SVG for renamed/removed lucide icons
39
- const Twitter = (props: any) => <svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"/></svg>
40
- const Github = (props: any) => <svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
41
- const Linkedin = (props: any) => <svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/><rect width="4" height="12" x="2" y="9"/><circle cx="4" cy="4" r="2"/></svg>
42
-
43
41
  // ============================================
44
42
  // TYPES
45
43
  // ============================================
@@ -0,0 +1,182 @@
1
+ /**
2
+ * WakaStart Editor Blocks — Custom Plate.js block elements for the WakaStart ecosystem.
3
+ *
4
+ * These blocks extend the standard Plate editor with domain-specific elements
5
+ * designed for PaaS project management, specification writing, and technical documentation.
6
+ *
7
+ * Usage:
8
+ * ```ts
9
+ * import { WakaUserStoryBlock, createUserStoryPlugin } from "@wakastellar/ui/editor"
10
+ * ```
11
+ *
12
+ * Each block provides:
13
+ * - A React component for rendering in the Plate editor
14
+ * - A node factory function for creating Slate nodes
15
+ * - A plugin factory for registering with the Plate editor
16
+ * - Full TypeScript types for the Slate element shape
17
+ */
18
+
19
+ // ── User Story Block ────────────────────────────────────────────────────────
20
+ export {
21
+ WakaUserStoryBlock,
22
+ createUserStoryNodes,
23
+ createUserStoryPlugin,
24
+ USER_STORY_BLOCK_TYPE,
25
+ type UserStoryElement,
26
+ type WakaUserStoryBlockProps,
27
+ } from "./waka-user-story-block"
28
+
29
+ // ── Acceptance Criteria Block ───────────────────────────────────────────────
30
+ export {
31
+ WakaAcceptanceCriteriaBlock,
32
+ createAcceptanceCriteriaNodes,
33
+ createAcceptanceCriteriaPlugin,
34
+ ACCEPTANCE_CRITERIA_BLOCK_TYPE,
35
+ type AcceptanceCriterion,
36
+ type AcceptanceCriteriaElement,
37
+ type WakaAcceptanceCriteriaBlockProps,
38
+ } from "./waka-acceptance-criteria-block"
39
+
40
+ // ── API Endpoint Block ──────────────────────────────────────────────────────
41
+ export {
42
+ WakaApiEndpointBlock,
43
+ createApiEndpointNodes,
44
+ createApiEndpointPlugin,
45
+ API_ENDPOINT_BLOCK_TYPE,
46
+ type HttpMethod,
47
+ type ApiParam,
48
+ type ApiResponse,
49
+ type ApiEndpointElement,
50
+ type WakaApiEndpointBlockProps,
51
+ } from "./waka-api-endpoint-block"
52
+
53
+ // ── Diagram Block ───────────────────────────────────────────────────────────
54
+ export {
55
+ WakaDiagramBlock,
56
+ createDiagramNodes,
57
+ createDiagramPlugin,
58
+ DIAGRAM_BLOCK_TYPE,
59
+ type DiagramSyntax,
60
+ type DiagramElement,
61
+ type WakaDiagramBlockProps,
62
+ } from "./waka-diagram-block"
63
+
64
+ // ── AI Assist Block ─────────────────────────────────────────────────────────
65
+ export {
66
+ WakaAiAssistBlock,
67
+ createAiAssistNodes,
68
+ createAiAssistPlugin,
69
+ AI_ASSIST_BLOCK_TYPE,
70
+ type AiAssistStatus,
71
+ type AiAssistElement,
72
+ type WakaAiAssistBlockProps,
73
+ } from "./waka-ai-assist-block"
74
+
75
+ // ── Version Diff Block ──────────────────────────────────────────────────────
76
+ export {
77
+ WakaVersionDiffBlock,
78
+ createVersionDiffNodes,
79
+ createVersionDiffPlugin,
80
+ VERSION_DIFF_BLOCK_TYPE,
81
+ type DiffLine,
82
+ type DiffHunk,
83
+ type VersionDiffElement,
84
+ type WakaVersionDiffBlockProps,
85
+ } from "./waka-version-diff-block"
86
+
87
+ // ── Embed Block ─────────────────────────────────────────────────────────────
88
+ export {
89
+ WakaEmbedBlock,
90
+ createEmbedNodes,
91
+ createEmbedPlugin,
92
+ detectEmbedProvider,
93
+ resolveEmbedUrl,
94
+ EMBED_BLOCK_TYPE,
95
+ type EmbedProvider,
96
+ type EmbedElement,
97
+ type WakaEmbedBlockProps,
98
+ } from "./waka-embed-block"
99
+
100
+ // ── Code Playground Block ───────────────────────────────────────────────────
101
+ export {
102
+ WakaCodePlaygroundBlock,
103
+ createCodePlaygroundNodes,
104
+ createCodePlaygroundPlugin,
105
+ CODE_PLAYGROUND_BLOCK_TYPE,
106
+ type PlaygroundLanguage,
107
+ type ExecutionStatus,
108
+ type CodePlaygroundElement,
109
+ type WakaCodePlaygroundBlockProps,
110
+ } from "./waka-code-playground-block"
111
+
112
+ // ── Comment Thread Block ────────────────────────────────────────────────────
113
+ export {
114
+ WakaCommentThreadBlock,
115
+ createCommentThreadNodes,
116
+ createCommentThreadPlugin,
117
+ COMMENT_THREAD_BLOCK_TYPE,
118
+ type ThreadComment,
119
+ type ThreadStatus,
120
+ type CommentThreadElement,
121
+ type WakaCommentThreadBlockProps,
122
+ } from "./waka-comment-thread-block"
123
+
124
+ // ── Slash Menu with WakaStart Blocks ────────────────────────────────────────
125
+ export {
126
+ WakaSlashMenuBlock,
127
+ getAllWakaSlashCommands,
128
+ createWakaSlashPlugins,
129
+ WAKA_BLOCK_COMMANDS,
130
+ type WakaSlashBlockCommand,
131
+ type WakaSlashMenuBlockProps,
132
+ } from "./waka-slash-menu-block"
133
+
134
+ // ── Convenience: load all block plugins at once ─────────────────────────────
135
+
136
+ /**
137
+ * Loads all WakaStart block plugins at once.
138
+ * Returns an array of Plate plugins to spread into your editor configuration.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * const blockPlugins = await loadAllWakaBlockPlugins()
143
+ * const editor = usePlateEditor({
144
+ * plugins: [...corePlugins, ...blockPlugins],
145
+ * })
146
+ * ```
147
+ */
148
+ export async function loadAllWakaBlockPlugins(options?: {
149
+ /** Mermaid/diagram render function */
150
+ diagramRenderer?: (source: string, syntax: string) => Promise<string>
151
+ /** AI generation function */
152
+ aiGenerator?: (prompt: string) => Promise<string>
153
+ /** Code execution function */
154
+ codeExecutor?: (code: string, language: string) => Promise<string>
155
+ /** Current user ID for comments */
156
+ currentUserId?: string
157
+ /** Comment reply handler */
158
+ onCommentReply?: (threadId: string, content: string) => void
159
+ }): Promise<unknown[]> {
160
+ const plugins: unknown[] = []
161
+
162
+ const loaders = [
163
+ createUserStoryPlugin(),
164
+ createAcceptanceCriteriaPlugin(),
165
+ createApiEndpointPlugin(),
166
+ createDiagramPlugin(options?.diagramRenderer as never),
167
+ createAiAssistPlugin(options?.aiGenerator),
168
+ createVersionDiffPlugin(),
169
+ createEmbedPlugin(),
170
+ createCodePlaygroundPlugin(options?.codeExecutor as never),
171
+ createCommentThreadPlugin(options?.currentUserId, options?.onCommentReply),
172
+ ]
173
+
174
+ const results = await Promise.allSettled(loaders)
175
+ for (const result of results) {
176
+ if (result.status === "fulfilled" && result.value) {
177
+ plugins.push(result.value)
178
+ }
179
+ }
180
+
181
+ return plugins
182
+ }
@@ -0,0 +1,326 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../../utils/cn"
5
+ import type { PlateElementProps } from "../waka-editor-elements"
6
+
7
+ // ============================================================================
8
+ // Types
9
+ // ============================================================================
10
+
11
+ export const ACCEPTANCE_CRITERIA_BLOCK_TYPE = "acceptance_criteria" as const
12
+
13
+ /** A single acceptance criterion in Gherkin format */
14
+ export interface AcceptanceCriterion {
15
+ /** Unique ID (e.g. "AC-01") */
16
+ id: string
17
+ /** Scenario name */
18
+ scenario: string
19
+ /** Given (context / precondition) */
20
+ given: string
21
+ /** When (action / trigger) */
22
+ when: string
23
+ /** Then (expected result) */
24
+ then: string
25
+ /** Whether this criterion has been validated */
26
+ validated: boolean
27
+ }
28
+
29
+ /** Slate node for acceptance criteria block */
30
+ export interface AcceptanceCriteriaElement {
31
+ type: typeof ACCEPTANCE_CRITERIA_BLOCK_TYPE
32
+ /** Block title */
33
+ title: string
34
+ /** List of criteria */
35
+ criteria: AcceptanceCriterion[]
36
+ children: Array<{ text: string }>
37
+ }
38
+
39
+ export interface WakaAcceptanceCriteriaBlockProps extends PlateElementProps {
40
+ element?: AcceptanceCriteriaElement & Record<string, unknown>
41
+ /** Whether the block is read-only */
42
+ readOnly?: boolean
43
+ }
44
+
45
+ // ============================================================================
46
+ // Gherkin keyword badge
47
+ // ============================================================================
48
+
49
+ function GherkinKeyword({ keyword, variant }: { keyword: string; variant: "given" | "when" | "then" }) {
50
+ const styles = {
51
+ given: "bg-violet-100 text-violet-700 dark:bg-violet-500/15 dark:text-violet-400",
52
+ when: "bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-400",
53
+ then: "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400",
54
+ }
55
+
56
+ return (
57
+ <span className={cn(
58
+ "inline-block text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded shrink-0 w-[52px] text-center",
59
+ styles[variant]
60
+ )}>
61
+ {keyword}
62
+ </span>
63
+ )
64
+ }
65
+
66
+ // ============================================================================
67
+ // Criterion Row
68
+ // ============================================================================
69
+
70
+ interface CriterionRowProps {
71
+ criterion: AcceptanceCriterion
72
+ index: number
73
+ readOnly?: boolean
74
+ }
75
+
76
+ function CriterionRow({ criterion, index, readOnly }: CriterionRowProps) {
77
+ const [isExpanded, setIsExpanded] = React.useState(true)
78
+
79
+ return (
80
+ <div className={cn(
81
+ "rounded-lg border transition-all duration-200",
82
+ criterion.validated
83
+ ? "border-emerald-200 dark:border-emerald-800/40 bg-emerald-50/30 dark:bg-emerald-950/10"
84
+ : "border-border bg-card/50"
85
+ )}>
86
+ {/* Header */}
87
+ <div className="flex items-center gap-3 px-3 py-2">
88
+ {/* Checkbox */}
89
+ <button
90
+ type="button"
91
+ disabled={readOnly}
92
+ className={cn(
93
+ "flex-shrink-0 h-5 w-5 rounded-md border-2 flex items-center justify-center transition-all duration-200",
94
+ criterion.validated
95
+ ? "bg-emerald-500 border-emerald-500 text-white"
96
+ : "border-border hover:border-emerald-400",
97
+ readOnly && "cursor-default"
98
+ )}
99
+ aria-label={criterion.validated ? "Mark as not validated" : "Mark as validated"}
100
+ aria-checked={criterion.validated}
101
+ role="checkbox"
102
+ >
103
+ {criterion.validated && (
104
+ <svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={3}>
105
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
106
+ </svg>
107
+ )}
108
+ </button>
109
+
110
+ {/* ID badge */}
111
+ <span className="text-[10px] font-mono font-bold text-muted-foreground bg-muted px-1.5 py-0.5 rounded shrink-0">
112
+ {criterion.id}
113
+ </span>
114
+
115
+ {/* Scenario name */}
116
+ <span className={cn(
117
+ "text-sm font-medium flex-1 min-w-0 truncate",
118
+ criterion.validated && "line-through text-muted-foreground"
119
+ )}>
120
+ {criterion.scenario || "Unnamed scenario"}
121
+ </span>
122
+
123
+ {/* Toggle */}
124
+ <button
125
+ type="button"
126
+ onClick={() => setIsExpanded(!isExpanded)}
127
+ className="text-muted-foreground hover:text-foreground transition-colors p-0.5"
128
+ aria-label={isExpanded ? "Collapse" : "Expand"}
129
+ >
130
+ <svg
131
+ className={cn("h-3.5 w-3.5 transition-transform duration-200", isExpanded && "rotate-180")}
132
+ viewBox="0 0 24 24"
133
+ fill="none"
134
+ stroke="currentColor"
135
+ strokeWidth={2}
136
+ >
137
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
138
+ </svg>
139
+ </button>
140
+ </div>
141
+
142
+ {/* Gherkin steps */}
143
+ {isExpanded && (
144
+ <div className="px-3 pb-3 pt-1 space-y-1.5 border-t border-border/50">
145
+ <div className="flex items-start gap-2">
146
+ <GherkinKeyword keyword="Given" variant="given" />
147
+ <span className="text-sm text-foreground/80 pt-0.5">
148
+ {criterion.given || <span className="italic text-muted-foreground">precondition...</span>}
149
+ </span>
150
+ </div>
151
+ <div className="flex items-start gap-2">
152
+ <GherkinKeyword keyword="When" variant="when" />
153
+ <span className="text-sm text-foreground/80 pt-0.5">
154
+ {criterion.when || <span className="italic text-muted-foreground">action...</span>}
155
+ </span>
156
+ </div>
157
+ <div className="flex items-start gap-2">
158
+ <GherkinKeyword keyword="Then" variant="then" />
159
+ <span className="text-sm text-foreground/80 pt-0.5">
160
+ {criterion.then || <span className="italic text-muted-foreground">expected result...</span>}
161
+ </span>
162
+ </div>
163
+ </div>
164
+ )}
165
+ </div>
166
+ )
167
+ }
168
+
169
+ // ============================================================================
170
+ // Element Component
171
+ // ============================================================================
172
+
173
+ /**
174
+ * WakaAcceptanceCriteriaBlock - A Plate.js block for acceptance criteria in
175
+ * Gherkin format (Given/When/Then). Each criterion has a checkbox for
176
+ * validation tracking.
177
+ *
178
+ * Register in Plate editor:
179
+ * ```ts
180
+ * components: {
181
+ * [ACCEPTANCE_CRITERIA_BLOCK_TYPE]: WakaAcceptanceCriteriaBlock,
182
+ * }
183
+ * ```
184
+ */
185
+ export function WakaAcceptanceCriteriaBlock({
186
+ attributes,
187
+ children,
188
+ element,
189
+ className,
190
+ }: WakaAcceptanceCriteriaBlockProps) {
191
+ const el = element as AcceptanceCriteriaElement | undefined
192
+ const criteria = el?.criteria || []
193
+ const title = el?.title || "Acceptance Criteria"
194
+
195
+ const validatedCount = criteria.filter((c) => c.validated).length
196
+ const totalCount = criteria.length
197
+ const progress = totalCount > 0 ? (validatedCount / totalCount) * 100 : 0
198
+
199
+ return (
200
+ <div
201
+ {...attributes}
202
+ contentEditable={false}
203
+ className={cn(
204
+ "my-4 rounded-lg overflow-hidden",
205
+ "border border-border",
206
+ "bg-card/30",
207
+ "shadow-sm",
208
+ className
209
+ )}
210
+ >
211
+ {/* Header */}
212
+ <div className="flex items-center justify-between px-4 py-2.5 bg-muted/30 border-b border-border">
213
+ <div className="flex items-center gap-2">
214
+ <svg
215
+ className="h-4 w-4 text-emerald-600 dark:text-emerald-400"
216
+ viewBox="0 0 24 24"
217
+ fill="none"
218
+ stroke="currentColor"
219
+ strokeWidth={2}
220
+ aria-hidden="true"
221
+ >
222
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
223
+ </svg>
224
+ <span className="text-xs font-bold uppercase tracking-wider text-foreground">
225
+ {title}
226
+ </span>
227
+ </div>
228
+
229
+ {/* Progress */}
230
+ <div className="flex items-center gap-2">
231
+ <span className="text-[10px] font-medium text-muted-foreground">
232
+ {validatedCount}/{totalCount}
233
+ </span>
234
+ <div className="w-16 h-1.5 bg-muted rounded-full overflow-hidden">
235
+ <div
236
+ className="h-full bg-emerald-500 rounded-full transition-all duration-500"
237
+ style={{ width: `${progress}%` }}
238
+ />
239
+ </div>
240
+ </div>
241
+ </div>
242
+
243
+ {/* Criteria list */}
244
+ <div className="p-3 space-y-2">
245
+ {criteria.length > 0 ? (
246
+ criteria.map((criterion, index) => (
247
+ <CriterionRow
248
+ key={criterion.id}
249
+ criterion={criterion}
250
+ index={index}
251
+ readOnly
252
+ />
253
+ ))
254
+ ) : (
255
+ <div className="py-6 text-center text-sm text-muted-foreground italic">
256
+ No acceptance criteria defined yet.
257
+ </div>
258
+ )}
259
+ </div>
260
+
261
+ {/* Hidden Slate children */}
262
+ <div className="hidden">{children}</div>
263
+ </div>
264
+ )
265
+ }
266
+
267
+ WakaAcceptanceCriteriaBlock.displayName = "WakaAcceptanceCriteriaBlock"
268
+
269
+ // ============================================================================
270
+ // Node Factory
271
+ // ============================================================================
272
+
273
+ /**
274
+ * Creates Slate nodes for an acceptance criteria block.
275
+ */
276
+ export function createAcceptanceCriteriaNodes(options?: {
277
+ title?: string
278
+ criteria?: AcceptanceCriterion[]
279
+ }): AcceptanceCriteriaElement[] {
280
+ return [
281
+ {
282
+ type: ACCEPTANCE_CRITERIA_BLOCK_TYPE,
283
+ title: options?.title || "Acceptance Criteria",
284
+ criteria: options?.criteria || [
285
+ {
286
+ id: "AC-01",
287
+ scenario: "Nominal case",
288
+ given: "The user is authenticated",
289
+ when: "They perform the action",
290
+ then: "The expected result is displayed",
291
+ validated: false,
292
+ },
293
+ {
294
+ id: "AC-02",
295
+ scenario: "Error case",
296
+ given: "The service is unavailable",
297
+ when: "They perform the action",
298
+ then: "An error message is displayed",
299
+ validated: false,
300
+ },
301
+ ],
302
+ children: [{ text: "" }],
303
+ },
304
+ ]
305
+ }
306
+
307
+ /**
308
+ * Creates a Plate plugin for acceptance criteria blocks.
309
+ */
310
+ export async function createAcceptanceCriteriaPlugin() {
311
+ try {
312
+ const { createPlatePlugin } = await import("platejs/react")
313
+ return createPlatePlugin({
314
+ key: ACCEPTANCE_CRITERIA_BLOCK_TYPE,
315
+ node: {
316
+ isElement: true,
317
+ isVoid: true,
318
+ type: ACCEPTANCE_CRITERIA_BLOCK_TYPE,
319
+ component: WakaAcceptanceCriteriaBlock,
320
+ },
321
+ })
322
+ } catch {
323
+ console.warn("[WakaAcceptanceCriteriaBlock] platejs not installed")
324
+ return null
325
+ }
326
+ }