@package-uploader/ui 1.1.2 → 1.1.3
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/assets/{index-95fEc7BA.js → index-BZdhKDnY.js} +13 -13
- package/dist/assets/{index-CcXisJMx.css → index-BfowR104.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/components/CourseStructureStep.tsx +307 -140
- package/src/index.css +49 -95
- package/src/components/BlockGroupingPanel.tsx +0 -264
package/src/index.css
CHANGED
|
@@ -1687,123 +1687,64 @@ body {
|
|
|
1687
1687
|
}
|
|
1688
1688
|
|
|
1689
1689
|
/* ========================================
|
|
1690
|
-
BLOCK
|
|
1690
|
+
BLOCK CARDS (visual block preview)
|
|
1691
1691
|
======================================== */
|
|
1692
1692
|
|
|
1693
|
-
.
|
|
1694
|
-
margin-top: 0.5rem;
|
|
1695
|
-
display: inline-flex;
|
|
1696
|
-
align-items: center;
|
|
1697
|
-
gap: 0.375rem;
|
|
1698
|
-
background: var(--color-bg);
|
|
1699
|
-
border: 1px solid var(--color-border);
|
|
1700
|
-
color: var(--color-text-muted);
|
|
1701
|
-
transition: all 0.15s;
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
.bgp-toggle-btn:hover {
|
|
1705
|
-
border-color: var(--color-primary);
|
|
1706
|
-
color: var(--color-primary);
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
.bgp-toggle-btn.active {
|
|
1710
|
-
border-color: var(--color-primary);
|
|
1711
|
-
color: var(--color-primary);
|
|
1712
|
-
background: rgba(37, 99, 235, 0.05);
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
.bgp-toggle-count {
|
|
1716
|
-
background: var(--color-primary);
|
|
1717
|
-
color: white;
|
|
1718
|
-
font-size: 0.625rem;
|
|
1719
|
-
padding: 0.0625rem 0.375rem;
|
|
1720
|
-
border-radius: 999px;
|
|
1721
|
-
font-weight: 600;
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
.block-grouping-panel {
|
|
1725
|
-
margin-top: 0.5rem;
|
|
1726
|
-
border: 1px solid var(--color-border);
|
|
1727
|
-
border-radius: var(--radius);
|
|
1728
|
-
background: var(--color-bg);
|
|
1729
|
-
overflow: hidden;
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
.bgp-header {
|
|
1693
|
+
.block-cards {
|
|
1733
1694
|
display: flex;
|
|
1734
|
-
|
|
1735
|
-
gap:
|
|
1736
|
-
padding: 0.
|
|
1737
|
-
background: var(--color-bg-secondary);
|
|
1738
|
-
border-bottom: 1px solid var(--color-border);
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
.bgp-title {
|
|
1742
|
-
font-size: 0.8rem;
|
|
1743
|
-
font-weight: 600;
|
|
1744
|
-
color: var(--color-text);
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
.bgp-count {
|
|
1748
|
-
font-size: 0.7rem;
|
|
1749
|
-
color: var(--color-primary);
|
|
1750
|
-
background: rgba(37, 99, 235, 0.1);
|
|
1751
|
-
padding: 0.0625rem 0.375rem;
|
|
1752
|
-
border-radius: 999px;
|
|
1695
|
+
flex-direction: column;
|
|
1696
|
+
gap: 3px;
|
|
1697
|
+
padding: 0.375rem 0;
|
|
1753
1698
|
}
|
|
1754
1699
|
|
|
1755
|
-
|
|
1756
|
-
.bgp-preview {
|
|
1757
|
-
padding: 0.5rem;
|
|
1700
|
+
.block-card {
|
|
1758
1701
|
display: flex;
|
|
1759
1702
|
flex-direction: column;
|
|
1760
1703
|
gap: 0.25rem;
|
|
1761
|
-
|
|
1762
|
-
overflow-y: auto;
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
.bgp-block {
|
|
1766
|
-
display: flex;
|
|
1767
|
-
align-items: center;
|
|
1768
|
-
gap: 0.375rem;
|
|
1769
|
-
padding: 0.375rem 0.5rem;
|
|
1704
|
+
padding: 0.5rem 0.625rem;
|
|
1770
1705
|
border-radius: var(--radius-sm);
|
|
1706
|
+
border: 1px solid var(--color-border);
|
|
1707
|
+
border-left: 4px solid var(--block-accent, #6b7280);
|
|
1708
|
+
background: var(--color-bg);
|
|
1771
1709
|
cursor: pointer;
|
|
1772
1710
|
transition: all 0.15s;
|
|
1773
|
-
border: 1px solid transparent;
|
|
1774
|
-
font-size: 0.75rem;
|
|
1775
1711
|
}
|
|
1776
1712
|
|
|
1777
|
-
.
|
|
1778
|
-
|
|
1779
|
-
border-color: var(--
|
|
1713
|
+
.block-card:hover:not(.grouped) {
|
|
1714
|
+
border-color: var(--color-primary);
|
|
1715
|
+
border-left-color: var(--block-accent, #6b7280);
|
|
1716
|
+
background: rgba(37, 99, 235, 0.03);
|
|
1780
1717
|
}
|
|
1781
1718
|
|
|
1782
|
-
.
|
|
1719
|
+
.block-card.selected {
|
|
1783
1720
|
background: rgba(37, 99, 235, 0.08);
|
|
1784
1721
|
border-color: var(--color-primary);
|
|
1722
|
+
border-left-color: var(--block-accent, #6b7280);
|
|
1723
|
+
box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.2);
|
|
1785
1724
|
}
|
|
1786
1725
|
|
|
1787
|
-
.
|
|
1726
|
+
.block-card.grouped {
|
|
1788
1727
|
cursor: default;
|
|
1789
|
-
opacity: 0.
|
|
1728
|
+
opacity: 0.75;
|
|
1729
|
+
background: var(--color-bg-secondary);
|
|
1790
1730
|
}
|
|
1791
1731
|
|
|
1792
|
-
.
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
flex-shrink: 0;
|
|
1732
|
+
.block-card-header {
|
|
1733
|
+
display: flex;
|
|
1734
|
+
align-items: center;
|
|
1735
|
+
gap: 0.5rem;
|
|
1736
|
+
min-height: 1.25rem;
|
|
1798
1737
|
}
|
|
1799
1738
|
|
|
1800
|
-
.
|
|
1801
|
-
font-
|
|
1739
|
+
.block-card-type {
|
|
1740
|
+
font-size: 0.8rem;
|
|
1741
|
+
font-weight: 600;
|
|
1802
1742
|
color: var(--color-text);
|
|
1803
1743
|
white-space: nowrap;
|
|
1804
1744
|
}
|
|
1805
1745
|
|
|
1806
|
-
.
|
|
1746
|
+
.block-card-title {
|
|
1747
|
+
font-size: 0.75rem;
|
|
1807
1748
|
color: var(--color-text-muted);
|
|
1808
1749
|
overflow: hidden;
|
|
1809
1750
|
text-overflow: ellipsis;
|
|
@@ -1812,22 +1753,33 @@ body {
|
|
|
1812
1753
|
min-width: 0;
|
|
1813
1754
|
}
|
|
1814
1755
|
|
|
1815
|
-
.
|
|
1756
|
+
.block-card-check {
|
|
1816
1757
|
color: var(--color-primary);
|
|
1817
|
-
font-weight:
|
|
1758
|
+
font-weight: 700;
|
|
1759
|
+
font-size: 0.875rem;
|
|
1818
1760
|
flex-shrink: 0;
|
|
1819
1761
|
margin-left: auto;
|
|
1820
1762
|
}
|
|
1821
1763
|
|
|
1822
|
-
|
|
1823
|
-
|
|
1764
|
+
.block-card .tree-class-input-sm {
|
|
1765
|
+
margin-left: 0;
|
|
1766
|
+
max-width: 100%;
|
|
1767
|
+
margin-top: 0;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
/* Group container wrapping adjacent block cards */
|
|
1771
|
+
.block-group-container {
|
|
1824
1772
|
border: 2px dashed var(--color-primary);
|
|
1825
1773
|
border-radius: var(--radius);
|
|
1826
|
-
padding: 0.
|
|
1774
|
+
padding: 0.375rem;
|
|
1827
1775
|
margin: 0.25rem 0;
|
|
1828
1776
|
background: rgba(37, 99, 235, 0.02);
|
|
1829
1777
|
}
|
|
1830
1778
|
|
|
1779
|
+
.block-group-container .block-card {
|
|
1780
|
+
border-left-width: 3px;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1831
1783
|
.bgp-group-header {
|
|
1832
1784
|
display: flex;
|
|
1833
1785
|
align-items: center;
|
|
@@ -1924,7 +1876,9 @@ body {
|
|
|
1924
1876
|
/* Create group controls */
|
|
1925
1877
|
.bgp-create {
|
|
1926
1878
|
padding: 0.5rem 0.75rem;
|
|
1927
|
-
|
|
1879
|
+
margin-top: 0.375rem;
|
|
1880
|
+
border: 1px solid var(--color-border);
|
|
1881
|
+
border-radius: var(--radius-sm);
|
|
1928
1882
|
background: var(--color-bg-secondary);
|
|
1929
1883
|
display: flex;
|
|
1930
1884
|
flex-direction: column;
|
|
@@ -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">✓</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
|
-
}
|