@package-uploader/ui 1.1.2 → 1.1.4

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.
@@ -1,264 +0,0 @@
1
- import { useState } from 'react';
2
- import { useBlockGrouping, type BlockGroup } from '../hooks/useBlockGrouping';
3
-
4
- interface Block {
5
- id: string;
6
- type: string;
7
- family?: string;
8
- variant?: string;
9
- title?: string;
10
- }
11
-
12
- interface BlockGroupingPanelProps {
13
- lessonId: string;
14
- blocks: Block[];
15
- groups: BlockGroup[];
16
- onGroupsChange: (groups: BlockGroup[]) => void;
17
- }
18
-
19
- /** Color palette for block families */
20
- function familyColor(family?: string): string {
21
- switch (family) {
22
- case 'text': return '#3b82f6';
23
- case 'media': return '#22c55e';
24
- case 'interactive': return '#a855f7';
25
- case 'knowledge-check': return '#f59e0b';
26
- case 'quote': return '#ec4899';
27
- default: return '#6b7280';
28
- }
29
- }
30
-
31
- export default function BlockGroupingPanel({
32
- lessonId,
33
- blocks,
34
- groups,
35
- onGroupsChange,
36
- }: BlockGroupingPanelProps) {
37
- const [newGroupClass, setNewGroupClass] = useState('');
38
- const [newGroupLabel, setNewGroupLabel] = useState('');
39
- const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
40
- const [editClass, setEditClass] = useState('');
41
- const [editLabel, setEditLabel] = useState('');
42
-
43
- const lessonBlockIds = blocks.map((b) => b.id);
44
-
45
- const {
46
- selectedBlockIds,
47
- toggleBlock,
48
- clearSelection,
49
- canCreateGroup,
50
- validationError,
51
- createGroup,
52
- removeGroup,
53
- updateGroup,
54
- isBlockGrouped,
55
- } = useBlockGrouping({
56
- lessonBlockIds,
57
- lessonId,
58
- groups,
59
- onGroupsChange,
60
- });
61
-
62
- const handleCreateGroup = () => {
63
- if (!canCreateGroup || !newGroupClass.trim()) return;
64
- createGroup(newGroupClass, newGroupLabel || undefined);
65
- setNewGroupClass('');
66
- setNewGroupLabel('');
67
- };
68
-
69
- const startEditGroup = (group: BlockGroup) => {
70
- setEditingGroupId(group.id);
71
- setEditClass(group.className);
72
- setEditLabel(group.label || '');
73
- };
74
-
75
- const saveEditGroup = () => {
76
- if (editingGroupId) {
77
- updateGroup(editingGroupId, { className: editClass, label: editLabel });
78
- setEditingGroupId(null);
79
- }
80
- };
81
-
82
- const cancelEdit = () => {
83
- setEditingGroupId(null);
84
- };
85
-
86
- // Build blockId -> group lookup
87
- const blockGroupMap = new Map<string, BlockGroup>();
88
- for (const group of groups) {
89
- for (const bid of group.blockIds) {
90
- blockGroupMap.set(bid, group);
91
- }
92
- }
93
-
94
- // Render a single block rectangle
95
- const renderBlock = (block: Block, isGrouped: boolean) => {
96
- const isSelected = selectedBlockIds.has(block.id);
97
- const canSelect = !isGrouped;
98
- const color = familyColor(block.family);
99
- const label = [block.type, block.variant].filter(Boolean).join(' / ');
100
-
101
- return (
102
- <div
103
- key={block.id}
104
- className={`bgp-block${isSelected ? ' selected' : ''}${isGrouped ? ' grouped' : ''}`}
105
- onClick={() => canSelect && toggleBlock(block.id)}
106
- style={{ '--block-color': color } as React.CSSProperties}
107
- title={canSelect ? 'Click to select' : 'Already in a group'}
108
- >
109
- <span className="bgp-block-color" />
110
- <span className="bgp-block-label">{label}</span>
111
- {block.title && (
112
- <span className="bgp-block-title">
113
- {block.title.length > 50 ? block.title.substring(0, 50) + '...' : block.title}
114
- </span>
115
- )}
116
- {isSelected && <span className="bgp-block-check">&#10003;</span>}
117
- </div>
118
- );
119
- };
120
-
121
- // Render group header (with edit/ungroup controls)
122
- const renderGroupHeader = (group: BlockGroup) => {
123
- const isEditing = editingGroupId === group.id;
124
-
125
- if (isEditing) {
126
- return (
127
- <div className="bgp-group-header">
128
- <div className="bgp-group-edit">
129
- <input
130
- type="text"
131
- value={editClass}
132
- onChange={(e) => setEditClass(e.target.value)}
133
- placeholder="CSS class(es)"
134
- className="bgp-edit-input"
135
- />
136
- <input
137
- type="text"
138
- value={editLabel}
139
- onChange={(e) => setEditLabel(e.target.value)}
140
- placeholder="Label (optional)"
141
- className="bgp-edit-input bgp-edit-input-sm"
142
- />
143
- <button className="bgp-btn bgp-btn-save" onClick={saveEditGroup}>Save</button>
144
- <button className="bgp-btn bgp-btn-cancel" onClick={cancelEdit}>Cancel</button>
145
- </div>
146
- </div>
147
- );
148
- }
149
-
150
- return (
151
- <div className="bgp-group-header">
152
- <span className="bgp-group-label">
153
- {group.label || group.className}
154
- </span>
155
- <span className="bgp-group-class">.{group.className}</span>
156
- <button
157
- className="bgp-btn bgp-btn-edit"
158
- onClick={() => startEditGroup(group)}
159
- title="Edit group"
160
- >
161
- Edit
162
- </button>
163
- <button
164
- className="bgp-btn bgp-btn-ungroup"
165
- onClick={() => removeGroup(group.id)}
166
- title="Remove group"
167
- >
168
- Ungroup
169
- </button>
170
- </div>
171
- );
172
- };
173
-
174
- // Build flat display list, rendering groups as nested containers
175
- const renderedGroups = new Set<string>();
176
- const previewItems: React.ReactNode[] = [];
177
-
178
- for (const block of blocks) {
179
- const group = blockGroupMap.get(block.id);
180
-
181
- if (group) {
182
- // Skip blocks already rendered as part of their group
183
- if (renderedGroups.has(group.id)) continue;
184
- renderedGroups.add(group.id);
185
-
186
- // Render the entire group container
187
- const groupBlocks = group.blockIds
188
- .map((bid) => blocks.find((b) => b.id === bid))
189
- .filter(Boolean) as Block[];
190
-
191
- previewItems.push(
192
- <div key={`group-${group.id}`} className="bgp-group-wrapper">
193
- {renderGroupHeader(group)}
194
- {groupBlocks.map((b) => renderBlock(b, true))}
195
- </div>
196
- );
197
- } else {
198
- previewItems.push(renderBlock(block, false));
199
- }
200
- }
201
-
202
- const hasSelection = selectedBlockIds.size > 0;
203
-
204
- return (
205
- <div className="block-grouping-panel">
206
- <div className="bgp-header">
207
- <span className="bgp-title">Block Groups</span>
208
- {groups.length > 0 && (
209
- <span className="bgp-count">{groups.length} group{groups.length !== 1 ? 's' : ''}</span>
210
- )}
211
- </div>
212
-
213
- {/* Schematic preview */}
214
- <div className="bgp-preview">
215
- {previewItems}
216
- </div>
217
-
218
- {/* Create group controls */}
219
- {hasSelection && (
220
- <div className="bgp-create">
221
- <div className="bgp-create-info">
222
- {selectedBlockIds.size} block{selectedBlockIds.size !== 1 ? 's' : ''} selected
223
- {validationError && <span className="bgp-validation-error"> — {validationError}</span>}
224
- </div>
225
- <div className="bgp-create-inputs">
226
- <input
227
- type="text"
228
- value={newGroupClass}
229
- onChange={(e) => setNewGroupClass(e.target.value)}
230
- placeholder="CSS class(es) for wrapper..."
231
- className="bgp-create-class"
232
- onKeyDown={(e) => e.key === 'Enter' && handleCreateGroup()}
233
- />
234
- <input
235
- type="text"
236
- value={newGroupLabel}
237
- onChange={(e) => setNewGroupLabel(e.target.value)}
238
- placeholder="Label (optional)"
239
- className="bgp-create-label"
240
- />
241
- </div>
242
- <div className="bgp-create-actions">
243
- <button
244
- className="btn btn-sm btn-primary"
245
- onClick={handleCreateGroup}
246
- disabled={!canCreateGroup || !newGroupClass.trim()}
247
- >
248
- Create Group
249
- </button>
250
- <button className="btn btn-sm btn-secondary" onClick={clearSelection}>
251
- Cancel
252
- </button>
253
- </div>
254
- </div>
255
- )}
256
-
257
- {!hasSelection && groups.length === 0 && (
258
- <div className="bgp-hint">
259
- Click adjacent blocks above to select them, then create a group to wrap them in a container div.
260
- </div>
261
- )}
262
- </div>
263
- );
264
- }