Skip to content

Commit ebc74b7

Browse files
committed
feat(design): migrate shorts pages to new design system
1 parent 91f05f8 commit ebc74b7

4 files changed

Lines changed: 140 additions & 59 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<script setup lang="ts">
2+
import { Slider } from "@ark-ui/vue";
3+
4+
/*
5+
* No slider exists in the admin-web design system. Built on Ark UI's Slider
6+
* primitive, token-styled. Single-value (number) model; Ark works with arrays.
7+
*/
8+
interface Props {
9+
min?: number;
10+
max?: number;
11+
step?: number;
12+
disabled?: boolean;
13+
}
14+
15+
withDefaults(defineProps<Props>(), {
16+
min: 0,
17+
max: 100,
18+
step: 1,
19+
});
20+
21+
const model = defineModel<number>({ default: 0 });
22+
23+
const arrayModel = computed<number[]>({
24+
get: () => [model.value],
25+
set: (v) => {
26+
model.value = v[0] ?? 0;
27+
},
28+
});
29+
</script>
30+
31+
<template>
32+
<Slider.Root
33+
v-model="arrayModel"
34+
:min="min"
35+
:max="max"
36+
:step="step"
37+
:disabled="disabled"
38+
class="w-full disabled:cursor-not-allowed disabled:opacity-50"
39+
>
40+
<Slider.Control class="relative flex items-center py-1">
41+
<Slider.Track class="bg-border-1 h-1.5 flex-1 rounded-full">
42+
<Slider.Range class="bg-primary-contrast h-full rounded-full" />
43+
</Slider.Track>
44+
<Slider.Thumb
45+
:index="0"
46+
class="gradient-border shadow-resting ds-focus-ring bg-surface-raise block size-5 cursor-grab rounded-full active:cursor-grabbing"
47+
>
48+
<Slider.HiddenInput />
49+
</Slider.Thumb>
50+
</Slider.Control>
51+
</Slider.Root>
52+
</template>

frontend/app/pages/shorts/generate.vue

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ watch([duration, scrubberWidth], ([d, s]) => {
101101
zoom.value = s / d;
102102
});
103103
104-
const toast = useToast();
104+
const toaster = useDesignToaster();
105105
const confirmSubmit = ref(false);
106106
async function submit() {
107107
try {
@@ -110,10 +110,9 @@ async function submit() {
110110
InSeconds: startTime.value,
111111
OutSeconds: endTime.value,
112112
});
113-
toast.add({
114-
icon: "tabler:check",
113+
toaster.create({
115114
title: "Short submitted successfully",
116-
color: "success",
115+
type: "success",
117116
});
118117
navigateTo("/shorts");
119118
confirmSubmit.value = false;
@@ -151,44 +150,40 @@ useVideoKeyboardControls({
151150
<div class="mx-auto flex w-full max-w-7xl flex-col gap-4 p-8">
152151
<header class="mb-4 flex items-center justify-between">
153152
<div>
154-
<h1 class="text-2xl font-bold">
153+
<h1 class="text-heading-3 text-text-default">
155154
{{ $t("shorts.generation.title") }}
156155
</h1>
157-
<p class="text-muted text-sm">
156+
<p class="text-text-muted text-sm">
158157
{{ $t("shorts.generation.description") }}
159158
</p>
160159
</div>
161-
<UButton @click="confirmSubmit = true">
162-
<UIcon name="tabler:send" class="text-dimmed" />
160+
<DesignButton icon="tabler:send" @click="confirmSubmit = true">
163161
{{ $t("shorts.generation.submit") }}
164-
</UButton>
165-
<UModal
162+
</DesignButton>
163+
<DesignDialog
166164
v-model:open="confirmSubmit"
167165
:title="$t('shorts.generation.submitConfirmationTitle')"
168166
:description="$t('shorts.generation.submitConfirmationMessage')"
169167
>
170-
<template #footer>
171-
<div class="flex w-full justify-end gap-2">
172-
<UButton @click="confirmSubmit = false" variant="ghost">
173-
{{
174-
$t("shorts.generation.submitConfirmationCancel")
175-
}}
176-
</UButton>
177-
<UButton @click="submit">
178-
{{
179-
$t("shorts.generation.submitConfirmationSubmit")
180-
}}
181-
</UButton>
182-
</div>
183-
</template>
184-
</UModal>
168+
<div class="flex w-full justify-end gap-2">
169+
<DesignButton
170+
variant="tertiary"
171+
@click="confirmSubmit = false"
172+
>
173+
{{ $t("shorts.generation.submitConfirmationCancel") }}
174+
</DesignButton>
175+
<DesignButton variant="primary" @click="submit">
176+
{{ $t("shorts.generation.submitConfirmationSubmit") }}
177+
</DesignButton>
178+
</div>
179+
</DesignDialog>
185180
</header>
186181
<template v-if="status == 'success'">
187182
<video
188183
ref="videoElement"
189184
:src="videoUrl"
190185
controls
191-
class="bg-default aspect-video w-full shadow-xl"
186+
class="bg-surface-default aspect-video w-full shadow-xl"
192187
/>
193188
<div class="flex items-center gap-2">
194189
<div class="tabular-nums">
@@ -211,24 +206,32 @@ useVideoKeyboardControls({
211206
</p>
212207
<p
213208
v-if="startTime != undefined && endTime != undefined"
214-
class="text-dimmed text-sm"
209+
class="text-text-hint text-sm"
215210
>
216211
{{ formatTime(startTime) }} - {{ formatTime(endTime) }}
217212
</p>
218213
</div>
219-
<UButton
220-
class="ml-auto"
221-
variant="soft"
214+
<DesignButton
215+
class="border-border-1 ml-auto border"
216+
variant="secondary"
222217
@click="startTime = currentTime"
223218
>
224219
{{ $t("shorts.generation.setStartPoint") }}
225-
</UButton>
226-
<UButton variant="soft" @click="endTime = currentTime">
220+
</DesignButton>
221+
<DesignButton
222+
class="border-border-1 border"
223+
variant="secondary"
224+
@click="endTime = currentTime"
225+
>
227226
{{ $t("shorts.generation.setEndPoint") }}
228-
</UButton>
229-
<UButton variant="soft" @click="previewShort">
227+
</DesignButton>
228+
<DesignButton
229+
class="border-border-1 border"
230+
variant="secondary"
231+
@click="previewShort"
232+
>
230233
{{ $t("shorts.generation.previewShort") }}
231-
</UButton>
234+
</DesignButton>
232235
</div>
233236
<ShortsTimelineScrubber
234237
v-if="
@@ -244,7 +247,7 @@ useVideoKeyboardControls({
244247
v-model:start="startTime"
245248
v-model:end="endTime"
246249
/>
247-
<USlider v-model="zoom" :min="0.1" :max="10" :step="0.01" />
250+
<DesignSlider v-model="zoom" :min="0.1" :max="10" :step="0.01" />
248251
</template>
249252
<template v-if="status != 'success'">
250253
<USkeleton class="aspect-video w-full" />
@@ -261,10 +264,8 @@ useVideoKeyboardControls({
261264
<USkeleton class="h-2 w-full" />
262265
</template>
263266

264-
<UModal v-model:open="showManual">
265-
<template #body>
266-
<img :src="manualGif" />
267-
</template>
268-
</UModal>
267+
<DesignDialog v-model:open="showManual">
268+
<img :src="manualGif" class="w-full rounded-lg" />
269+
</DesignDialog>
269270
</div>
270271
</template>
Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<script setup lang="ts">
22
import { z } from "zod";
3-
import type { FormSubmitEvent } from "@nuxt/ui";
43
54
const analytics = useAnalytics();
65
onMounted(() => {
@@ -15,7 +14,7 @@ useHead({
1514
});
1615
1716
const schema = z.object({
18-
vxId: z.string().regex(/^VX-[a-zA-Z0-9]+$/),
17+
vxId: z.string().regex(/^VX-[a-zA-Z0-9]+$/, "Must look like VX-123456"),
1918
});
2019
2120
type Schema = z.output<typeof schema>;
@@ -24,34 +23,40 @@ const state = reactive<Partial<Schema>>({
2423
vxId: undefined,
2524
});
2625
27-
function onSubmit(event: FormSubmitEvent<Schema>) {
26+
const error = ref<string>();
27+
28+
function onSubmit() {
29+
const result = schema.safeParse(state);
30+
if (!result.success) {
31+
error.value = result.error.issues[0]?.message ?? "Invalid";
32+
return;
33+
}
34+
error.value = undefined;
2835
navigateTo({
2936
name: "shorts-generate",
3037
query: {
31-
id: event.data.vxId,
38+
id: result.data.vxId,
3239
},
3340
});
3441
}
3542
</script>
3643

3744
<template>
3845
<div class="flex justify-center py-8">
39-
<UForm
40-
:state
41-
:schema
42-
@submit="onSubmit"
46+
<form
4347
class="flex w-full max-w-xs flex-col gap-2"
48+
@submit.prevent="onSubmit"
4449
>
45-
<UFormField label="VX ID" name="vxId" size="lg">
46-
<UInput
47-
v-model="state.vxId"
48-
class="w-full"
49-
placeholder="VX-123456"
50-
/>
51-
</UFormField>
52-
<UButton type="submit" block>
50+
<DesignInput
51+
v-model="state.vxId"
52+
label="VX ID"
53+
placeholder="VX-123456"
54+
:invalid="!!error"
55+
:error-text="error"
56+
/>
57+
<DesignButton type="submit" class="w-full">
5358
{{ $t("shorts.generation.loadFromMediabanken") }}
54-
</UButton>
55-
</UForm>
59+
</DesignButton>
60+
</form>
5661
</div>
5762
</template>

frontend/docs/design-system-migration.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,29 @@ self-closing); `UFormField`→`<label>`+control; utilities → tokens (`bg-defau
281281
(comma display + truncation in the narrow `w-24`/`w-32` filter selects), the 15 switches with
282282
descriptions, the filter search input (leading icon + trailing Clear button), and dark mode.
283283

284+
### 2026-06-23 — shorts/index migrated (UForm → native form + zod)
285+
286+
`app/pages/shorts/index.vue`: replaced `UForm` (Nuxt UI form + schema) with a native
287+
`<form @submit.prevent>` that runs the same zod schema via `safeParse` on submit; errors surface
288+
through `DesignInput`'s `:invalid` + `:error-text` (no `UFormField`). `UButton block``class="w-full"`.
289+
No new components. Removed the `@nuxt/ui` `FormSubmitEvent` import. `pnpm typecheck` ✅.
290+
291+
**Pattern for `UForm` elsewhere:** there's no Design form component — use a native `<form>`, validate
292+
with the existing schema (`safeParse`) on submit, and feed the first issue message into the field's
293+
`error-text`. Reuse this for other `UForm` pages.
294+
295+
### 2026-06-23 — shorts/generate migrated (+ DesignSlider)
296+
297+
`app/pages/shorts/generate.vue` (the biggest page, 18 `U*`): `useToast``useDesignToaster`;
298+
6 `UButton``DesignButton` (`soft``secondary`, header submit uses `icon="tabler:send"` so the
299+
inline `UIcon` is dropped); 2 `UModal``DesignDialog` (the manual popup used `#body` → now plain
300+
default-slot `<img>`); `USlider`→new `DesignSlider`; utilities→tokens (`bg-default`
301+
`bg-surface-default`, `text-muted`/`text-dimmed`→tokens). `USkeleton` kept (8×). Build + typecheck ✅.
302+
303+
**New component:** `DesignSlider.vue` — Ark UI `Slider`, single-value `number` model (Ark uses a
304+
`number[]` internally, bridged), `min`/`max`/`step`/`disabled`, `ds-focus-ring` on the thumb. Anatomy
305+
matches Ark docs (Root › Control › Track › Range, Thumb[:index=0] › HiddenInput). Not in admin-web.
306+
284307
### Next steps (pick up here)
285308

286309
1. **Human visual review** of `/export` (with and without `?id=`) in light + dark. Compare against

0 commit comments

Comments
 (0)