Skip to content

Commit 3d33431

Browse files
committed
feat: add theme management feature drag-and-drop reordering
1 parent 8c5afa7 commit 3d33431

4 files changed

Lines changed: 100 additions & 5 deletions

File tree

app/assets/docs/quick-start.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,33 @@
1212

1313
---
1414

15+
## Managing Saved Themes
16+
17+
### Reordering Themes
18+
19+
- **Drag and drop** any saved theme to reorder them in your list
20+
- The entire theme card is draggable - just click and drag to your preferred position
21+
- Great for organizing themes by priority or project
22+
23+
### Theme Actions
24+
25+
Each saved theme has quick actions:
26+
27+
- **🎨 Apply** - Load the theme color into the color picker for editing
28+
- **⭐ Star** - Set as the default theme (marked with a star icon)
29+
- **🗑️ Remove** - Delete the theme from your collection
30+
31+
### Tips
32+
33+
- **Hover** over truncated theme names to see the full name in a tooltip
34+
- **Export/Import** your entire theme collection as JSON to share or backup
35+
- Exports include all themes, the default theme, and your export options (Tailwind/Nuxt UI
36+
settings)
37+
- Importing restores themes and settings automatically
38+
- The **default theme** (marked with ⭐) is highlighted in exports
39+
40+
---
41+
1542
## Generated Files
1643

1744
When you click **Download ZIP**, you get three ready-to-use files:

app/components/ThemeList.vue

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,30 @@
2626
<div class="text-xs text-muted font-medium uppercase tracking-wide">Saved themes</div>
2727

2828
<div
29-
v-for="theme in themes"
29+
v-for="(theme, index) in themes"
3030
:key="theme.name"
31-
class="flex items-center gap-2 px-3 py-2 rounded-lg border border-default bg-elevated hover:bg-accented transition-colors"
31+
draggable="true"
32+
class="flex items-center gap-2.5 px-3 py-2 rounded-lg border border-default bg-elevated hover:bg-accented transition-colors cursor-move"
33+
:class="{
34+
'opacity-50': draggedIndex === index,
35+
'border-primary': dropTargetIndex === index
36+
}"
37+
@dragstart="handleDragStart(index, $event)"
38+
@dragend="handleDragEnd"
39+
@dragover="handleDragOver(index, $event)"
40+
@dragleave="handleDragLeave"
41+
@drop="handleDrop(index, $event)"
3242
>
3343
<!-- Color dot -->
3444
<div
3545
class="w-3 h-3 rounded-full shrink-0 ring-1 ring-default"
3646
:style="{ backgroundColor: theme.shades['500'] }"
3747
/>
3848

39-
<!-- Name -->
40-
<span class="flex-1 text-sm font-mono truncate">{{ theme.name }}</span>
49+
<!-- Name with tooltip -->
50+
<UTooltip :text="theme.name" class="flex-1 min-w-0">
51+
<span class="block text-sm font-mono truncate">{{ theme.name }}</span>
52+
</UTooltip>
4153

4254
<!-- Apply to picker -->
4355
<UTooltip text="Apply to color picker">
@@ -125,6 +137,7 @@ const {
125137
removeTheme,
126138
setDefault,
127139
setColor,
140+
reorderThemes,
128141
exportThemes,
129142
importThemes
130143
} = useThemeStore();
@@ -133,6 +146,48 @@ const fileInput = ref<HTMLInputElement | null>(null);
133146
const importMessage = ref('');
134147
const importError = ref(false);
135148
149+
// Drag and drop state
150+
const draggedIndex = ref<number | null>(null);
151+
const dropTargetIndex = ref<number | null>(null);
152+
153+
function handleDragStart(index: number, event: DragEvent) {
154+
draggedIndex.value = index;
155+
if (event.dataTransfer) {
156+
event.dataTransfer.effectAllowed = 'move';
157+
event.dataTransfer.setData('text/plain', index.toString());
158+
}
159+
}
160+
161+
function handleDragEnd() {
162+
draggedIndex.value = null;
163+
dropTargetIndex.value = null;
164+
}
165+
166+
function handleDragOver(index: number, event: DragEvent) {
167+
event.preventDefault();
168+
if (event.dataTransfer) {
169+
event.dataTransfer.dropEffect = 'move';
170+
}
171+
if (draggedIndex.value !== null && draggedIndex.value !== index) {
172+
dropTargetIndex.value = index;
173+
}
174+
}
175+
176+
function handleDragLeave() {
177+
dropTargetIndex.value = null;
178+
}
179+
180+
function handleDrop(toIndex: number, event: DragEvent) {
181+
event.preventDefault();
182+
183+
if (draggedIndex.value !== null && draggedIndex.value !== toIndex) {
184+
reorderThemes(draggedIndex.value, toIndex);
185+
}
186+
187+
draggedIndex.value = null;
188+
dropTargetIndex.value = null;
189+
}
190+
136191
function applyTheme(theme: ThemeEntry) {
137192
// Apply the theme's base color (500 shade) to the color picker
138193
const baseColor = theme.shades['500'];

app/composables/useThemeStore.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ function setDefault(name: string) {
7070
defaultTheme.value = name;
7171
}
7272

73+
function reorderThemes(fromIndex: number, toIndex: number) {
74+
if (fromIndex === toIndex) return;
75+
if (fromIndex < 0 || fromIndex >= themes.value.length) return;
76+
if (toIndex < 0 || toIndex >= themes.value.length) return;
77+
78+
const newThemes = [...themes.value];
79+
const [movedTheme] = newThemes.splice(fromIndex, 1);
80+
if (!movedTheme) return;
81+
newThemes.splice(toIndex, 0, movedTheme);
82+
themes.value = newThemes;
83+
}
84+
7385
// Security validation functions
7486
function isValidHexColor(color: unknown): color is string {
7587
return typeof color === 'string' && HEX_COLOR_REGEX.test(color);
@@ -318,6 +330,7 @@ export function useThemeStore() {
318330
addTheme,
319331
removeTheme,
320332
setDefault,
333+
reorderThemes,
321334
exportThemes,
322335
importThemes
323336
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"marked": "^18.0.2",
3939
"marked-highlight": "^2.2.4",
4040
"nuxt": "^4.4.2",
41-
"tailwindcss": "^4.2.3",
41+
"tailwindcss": "^4.2.4",
4242
"vue": "^3.5.32",
4343
"vue-router": "^5.0.4"
4444
},

0 commit comments

Comments
 (0)