feat: clerk theming and preset selector

This commit is contained in:
2026-01-03 13:31:10 +01:00
parent 8995979eb1
commit 145c3b1136
21 changed files with 381 additions and 22 deletions

View File

@@ -1,5 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@import '@clerk/themes/shadcn.css';
@custom-variant dark (&:is(.dark *));
@@ -123,4 +124,8 @@
h1 {
@apply scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl;
}
}
}
.cl-avatarBox {
@apply size-8;
}

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Plus } from 'lucide-vue-next';
const router = useRouter()
</script>
<template>
<Select>
<SelectTrigger class="w-[180px]">
<SelectValue placeholder="Select a preset" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">
Default
</SelectItem>
<SelectItem value="create-new" @click="router.push('/presets/new')">
<div class="font-bold flex gap-2 items-center">
<Plus class="size-4" />
Create New Preset
</div>
</SelectItem>
<!-- add database provided presets here -->
</SelectContent>
</Select>
</template>

View File

@@ -8,11 +8,17 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { LogIn } from "lucide-vue-next";
</script>
<template>
<Dialog>
<DialogTrigger>Sign In</DialogTrigger>
<DialogTrigger>
<Button size="icon">
<LogIn />
</Button>
</DialogTrigger>
<DialogContent>
<SignIn />
</DialogContent>

View File

@@ -7,12 +7,14 @@ const colorMode = useColorMode()
</script>
<template>
<div class="fixed top-4 right-4 z-50">
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline">
<Icon icon="radix-icons:moon" class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Icon icon="radix-icons:sun" class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<Button size="icon" variant="outline">
<Icon icon="radix-icons:moon"
class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Icon icon="radix-icons:sun"
class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
@@ -29,4 +31,4 @@ const colorMode = useColorMode()
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { SelectRootEmits, SelectRootProps } from "reka-ui"
import { SelectRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<SelectRootProps>()
const emits = defineEmits<SelectRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<SelectRoot
v-slot="slotProps"
data-slot="select"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</SelectRoot>
</template>

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
import type { SelectContentEmits, SelectContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
SelectContent,
SelectPortal,
SelectViewport,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import { SelectScrollDownButton, SelectScrollUpButton } from "."
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(
defineProps<SelectContentProps & { class?: HTMLAttributes["class"] }>(),
{
position: "popper",
},
)
const emits = defineEmits<SelectContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SelectPortal>
<SelectContent
data-slot="select-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
position === 'popper'
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
props.class,
)
"
>
<SelectScrollUpButton />
<SelectViewport :class="cn('p-1', position === 'popper' && 'h-[var(--reka-select-trigger-height)] w-full min-w-[var(--reka-select-trigger-width)] scroll-my-1')">
<slot />
</SelectViewport>
<SelectScrollDownButton />
</SelectContent>
</SelectPortal>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { SelectGroupProps } from "reka-ui"
import { SelectGroup } from "reka-ui"
const props = defineProps<SelectGroupProps>()
</script>
<template>
<SelectGroup
data-slot="select-group"
v-bind="props"
>
<slot />
</SelectGroup>
</template>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import type { SelectItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Check } from "lucide-vue-next"
import {
SelectItem,
SelectItemIndicator,
SelectItemText,
useForwardProps,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SelectItemProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectItem
data-slot="select-item"
v-bind="forwardedProps"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
props.class,
)
"
>
<span class="absolute right-2 flex size-3.5 items-center justify-center">
<SelectItemIndicator>
<slot name="indicator-icon">
<Check class="size-4" />
</slot>
</SelectItemIndicator>
</span>
<SelectItemText>
<slot />
</SelectItemText>
</SelectItem>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { SelectItemTextProps } from "reka-ui"
import { SelectItemText } from "reka-ui"
const props = defineProps<SelectItemTextProps>()
</script>
<template>
<SelectItemText
data-slot="select-item-text"
v-bind="props"
>
<slot />
</SelectItemText>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { SelectLabelProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { SelectLabel } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes["class"] }>()
</script>
<template>
<SelectLabel
data-slot="select-label"
:class="cn('text-muted-foreground px-2 py-1.5 text-xs', props.class)"
>
<slot />
</SelectLabel>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import type { SelectScrollDownButtonProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronDown } from "lucide-vue-next"
import { SelectScrollDownButton, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectScrollDownButton
data-slot="select-scroll-down-button"
v-bind="forwardedProps"
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
>
<slot>
<ChevronDown class="size-4" />
</slot>
</SelectScrollDownButton>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import type { SelectScrollUpButtonProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronUp } from "lucide-vue-next"
import { SelectScrollUpButton, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectScrollUpButton
data-slot="select-scroll-up-button"
v-bind="forwardedProps"
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
>
<slot>
<ChevronUp class="size-4" />
</slot>
</SelectScrollUpButton>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { SelectSeparatorProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { SelectSeparator } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<SelectSeparator
data-slot="select-separator"
v-bind="delegatedProps"
:class="cn('bg-border pointer-events-none -mx-1 my-1 h-px', props.class)"
/>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { SelectTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronDown } from "lucide-vue-next"
import { SelectIcon, SelectTrigger, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(
defineProps<SelectTriggerProps & { class?: HTMLAttributes["class"], size?: "sm" | "default" }>(),
{ size: "default" },
)
const delegatedProps = reactiveOmit(props, "class", "size")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectTrigger
data-slot="select-trigger"
:data-size="size"
v-bind="forwardedProps"
:class="cn(
'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)"
>
<slot />
<SelectIcon as-child>
<ChevronDown class="size-4 opacity-50" />
</SelectIcon>
</SelectTrigger>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { SelectValueProps } from "reka-ui"
import { SelectValue } from "reka-ui"
const props = defineProps<SelectValueProps>()
</script>
<template>
<SelectValue
data-slot="select-value"
v-bind="props"
>
<slot />
</SelectValue>
</template>

View File

@@ -0,0 +1,11 @@
export { default as Select } from "./Select.vue"
export { default as SelectContent } from "./SelectContent.vue"
export { default as SelectGroup } from "./SelectGroup.vue"
export { default as SelectItem } from "./SelectItem.vue"
export { default as SelectItemText } from "./SelectItemText.vue"
export { default as SelectLabel } from "./SelectLabel.vue"
export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue"
export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue"
export { default as SelectSeparator } from "./SelectSeparator.vue"
export { default as SelectTrigger } from "./SelectTrigger.vue"
export { default as SelectValue } from "./SelectValue.vue"

View File

@@ -5,7 +5,8 @@ import ThemeDropdown from '~/components/ui/ThemeDropdown.vue';
<template>
<div>
<header class="flex justify-end p-4">
<header class="flex justify-end p-4 space-x-4">
<ThemeDropdown />
<SignedOut>
<SignInDialog />
</SignedOut>
@@ -13,7 +14,6 @@ import ThemeDropdown from '~/components/ui/ThemeDropdown.vue';
<UserButton />
</SignedIn>
</header>
<ThemeDropdown />
<slot />
</div>
</template>

View File

@@ -3,10 +3,11 @@ import { useWebSocket } from '@vueuse/core';
import { Button } from "@/components/ui/button"
import { useStreamerStore } from '~/state/streamer';
import { useWebSocketUrl } from '~/composables/useWebSocketUrl';
import PresetSelect from '~/components/app/PresetSelect.vue';
const streamerStore = useStreamerStore()
const videofeedRef = ref<HTMLVideoElement|null>(null);
const localStream = ref<MediaStream|null>(null);
const videofeedRef = ref<HTMLVideoElement | null>(null);
const localStream = ref<MediaStream | null>(null);
const wsUrl = useWebSocketUrl()
const { send } = useWebSocket(wsUrl, {
@@ -17,11 +18,11 @@ const { send } = useWebSocket(wsUrl, {
},
onMessage: async (ws, ev) => {
const message = JSON.parse(ev.data)
if (message.event === 'room-created') {
streamerStore.setCode(message.roomId)
}
if (message.event === 'viewer-joined') {
const peerConnection = new RTCPeerConnection({
iceServers: [
@@ -57,14 +58,12 @@ const { send } = useWebSocket(wsUrl, {
});
streamerStore.addPeerConnection(message.viewerId, peerConnection)
// Add media tracks to peer connection
if (localStream.value) {
localStream.value.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream.value!);
});
}
// Handle ICE candidates
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
send(JSON.stringify({
@@ -77,21 +76,21 @@ const { send } = useWebSocket(wsUrl, {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
send(JSON.stringify({
event: 'offer',
targetId: message.viewerId,
sdp: offer,
}))
}
if (message.event === 'ice-candidate') {
const pc = streamerStore.peerConnections[message.from];
if (pc) {
await pc.addIceCandidate(new RTCIceCandidate(message.candidate));
}
}
if (message.event === 'answer') {
const pc = streamerStore.peerConnections[message.from];
if (pc) {
@@ -121,9 +120,12 @@ async function startScreenShare() {
<template>
<div class="flex flex-col items-center justify-center gap-6 mt-10 px-4">
<Button @click="startScreenShare">
screenshare
</Button>
<div class="flex space-x-4 items-center">
<Button @click="startScreenShare">
screenshare
</Button>
<PresetSelect />
</div>
<p v-if="streamerStore.code" class="font-mono">{{ streamerStore.code }}</p>
<video ref="videofeedRef" autoplay playsinline muted></video>
</div>

View File

@@ -1,4 +1,5 @@
import tailwindcss from "@tailwindcss/vite";
import { shadcn } from "@clerk/themes";
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
@@ -30,4 +31,9 @@ export default defineNuxtConfig({
websocket: true
}
},
})
clerk: {
appearance: {
theme: shadcn
}
}
})

View File

@@ -13,6 +13,7 @@
},
"dependencies": {
"@clerk/nuxt": "^1.13.10",
"@clerk/themes": "^2.4.46",
"@neondatabase/serverless": "^1.0.2",
"@pinia/nuxt": "0.11.2",
"@tailwindcss/vite": "^4.1.16",

15
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@clerk/nuxt':
specifier: ^1.13.10
version: 1.13.10(magicast@0.5.0)(react@19.2.3)(vue@3.5.22(typescript@5.9.3))
'@clerk/themes':
specifier: ^2.4.46
version: 2.4.46(react@19.2.3)
'@neondatabase/serverless':
specifier: ^1.0.2
version: 1.0.2
@@ -236,6 +239,10 @@ packages:
react-dom:
optional: true
'@clerk/themes@2.4.46':
resolution: {integrity: sha512-26U+aInnWJwYHrT/LYX7sGRrLJwLk4NvfEoUyVc5EXUl7ue/TZ88r7ZiKDv0KutBTNopaz+p+7KD6F1j72nodA==}
engines: {node: '>=18.17.0'}
'@clerk/types@4.101.9':
resolution: {integrity: sha512-RO00JqqmkIoI1o0XCtvudjaLpqEoe8PRDHlLS1r/aNZazUQCO0TT6nZOx1F3X+QJDjqYVY7YmYl3mtO2QVEk1g==}
engines: {node: '>=18.17.0'}
@@ -4388,6 +4395,14 @@ snapshots:
optionalDependencies:
react: 19.2.3
'@clerk/themes@2.4.46(react@19.2.3)':
dependencies:
'@clerk/shared': 3.41.1(react@19.2.3)
tslib: 2.8.1
transitivePeerDependencies:
- react
- react-dom
'@clerk/types@4.101.9(react@19.2.3)':
dependencies:
'@clerk/shared': 3.41.1(react@19.2.3)