Files
supabase/apps/ui-library/public/r/realtime-cursor-nuxtjs.json
Ivan Vasilov 69ce915a9e chore: Rename SUPABASE_PUBLISHABLE_OR_ANON_KEY to SUPABASE_PUBLISHABLE_KEY for all blocks (#42652)
This PR renames all `SUPABASE_PUBLISHABLE_OR_ANON_KEY` env vars into
`SUPABASE_PUBLISHABLE_KEY` to make the new API keys default. This is in
coordination with the rest of the docs.

I've also cleaned up the `blocks/vue` package from unused files.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Breaking Changes**
* Public environment variable names renamed from PUBLISHABLE_OR_ANON_KEY
→ PUBLISHABLE_KEY across all framework integrations; update your
environment configs.

* **Documentation**
* All framework guides, .env examples and registry docs updated to use
the new variable names.

* **Chores**
* Cleaned up UI registry/templates: some example Vue registry items and
autogenerated registry artifacts were removed or simplified.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-11 10:23:16 +01:00

32 lines
6.9 KiB
JSON

{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "realtime-cursor-nuxtjs",
"type": "registry:component",
"title": "Realtime Cursor for Nuxt and Supabase",
"description": "Component which renders realtime cursors from other users in a room.",
"dependencies": [
"@supabase/supabase-js@latest",
"@vueuse/core",
"lucide-vue-next"
],
"files": [
{
"path": "registry/default/realtime-cursor/nuxtjs/app/components/cursor.vue",
"content": "<script setup lang=\"ts\">\nimport { MousePointer2 } from 'lucide-vue-next'\nimport type { CSSProperties } from 'vue'\n// @ts-ignore\nimport { cn } from '@/lib/utils'\n\ndefineProps<{\n className?: string\n style?: CSSProperties\n color: string\n name: string\n}>()\n</script>\n\n<template>\n <div\n :class=\"cn('pointer-events-none', className)\"\n :style=\"style\"\n >\n <MousePointer2\n :color=\"color\"\n :fill=\"color\"\n :size=\"30\"\n />\n\n <div\n class=\"mt-1 px-2 py-1 rounded text-xs font-bold text-white text-center\"\n :style=\"{ backgroundColor: color }\"\n >\n {{ name }}\n </div>\n </div>\n</template>\n",
"type": "registry:component",
"target": "app/components/cursor.vue"
},
{
"path": "registry/default/realtime-cursor/nuxtjs/app/components/realtime-cursors.vue",
"content": "<script setup lang=\"ts\">\n// @ts-ignore\nimport { Cursor } from '@/components/cursor'\n// @ts-ignore\nimport { useRealtimeCursors } from '@/composables/useRealtimeCursors'\n\nconst THROTTLE_MS = 50\n\nconst props = defineProps<{\n roomName: string\n username: string\n}>()\n\nconst { cursors } = useRealtimeCursors({\n roomName: props.roomName,\n username: props.username,\n throttleMs: THROTTLE_MS,\n})\n</script>\n\n<template>\n <div>\n <Cursor\n v-for=\"(cursor, id) in cursors\"\n :key=\"id\"\n className=\"fixed transition-transform ease-in-out z-50\"\n :style=\"{\n transitionDuration: '20ms',\n top: '0px',\n left: '0px',\n transform: `translate(${cursor.position.x}px, ${cursor.position.y}px)`,\n }\"\n :color=\"cursor.color\"\n :name=\"cursor.user.name\"\n />\n </div>\n</template>\n",
"type": "registry:component",
"target": "app/components/realtime-cursors.vue"
},
{
"path": "registry/default/realtime-cursor/nuxtjs/app/composables/useRealtimeCursors.ts",
"content": "import { REALTIME_SUBSCRIBE_STATES, type RealtimeChannel } from '@supabase/supabase-js'\nimport { onMounted, onUnmounted, reactive, ref } from 'vue'\n\n// @ts-ignore\nimport { createClient } from '@/lib/supabase/client'\n\n/**\n * Throttle a callback to a certain delay.\n * It will only call the callback if the delay has passed,\n * using the arguments from the last call.\n */\nfunction useThrottleCallback<Params extends unknown[]>(\n callback: (...args: Params) => void,\n delay: number\n) {\n let lastCall = 0\n let timeout: ReturnType<typeof setTimeout> | null = null\n\n const run = (...args: Params) => {\n const now = Date.now()\n const remainingTime = delay - (now - lastCall)\n\n if (remainingTime <= 0) {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n lastCall = now\n callback(...args)\n } else if (!timeout) {\n timeout = setTimeout(() => {\n lastCall = Date.now()\n timeout = null\n callback(...args)\n }, remainingTime)\n }\n }\n\n const cancel = () => {\n if (timeout) {\n clearTimeout(timeout)\n timeout = null\n }\n }\n\n return { run, cancel }\n}\n\nconst supabase = createClient()\n\nconst generateRandomColor = () => `hsl(${Math.floor(Math.random() * 360)}, 100%, 70%)`\n\nconst generateRandomNumber = () => Math.floor(Math.random() * 100)\n\nconst EVENT_NAME = 'realtime-cursor-move'\n\nexport type CursorEventPayload = {\n position: { x: number; y: number }\n user: { id: number; name: string }\n color: string\n timestamp: number\n}\n\nexport function useRealtimeCursors({\n roomName,\n username,\n throttleMs,\n}: {\n roomName: string\n username: string\n throttleMs: number\n}) {\n const color = generateRandomColor()\n const userId = generateRandomNumber()\n\n const cursors = reactive<Record<string, CursorEventPayload>>({})\n const cursorPayload = ref<CursorEventPayload | null>(null)\n const channelRef = ref<RealtimeChannel | null>(null)\n\n const sendCursor = (event: MouseEvent) => {\n const payload: CursorEventPayload = {\n position: {\n x: event.clientX,\n y: event.clientY,\n },\n user: {\n id: userId,\n name: username,\n },\n color,\n timestamp: Date.now(),\n }\n\n cursorPayload.value = payload\n\n channelRef.value?.send({\n type: 'broadcast',\n event: EVENT_NAME,\n payload,\n })\n }\n\n const { run: handleMouseMove, cancel: cancelThrottle } = useThrottleCallback(\n sendCursor,\n throttleMs\n )\n\n onMounted(() => {\n const channel = supabase.channel(roomName)\n\n channel\n .on('system', {}, (payload: CursorEventPayload) => {\n console.error('Realtime system error:', payload)\n\n // Defensive cleanup\n Object.keys(cursors).forEach((k) => delete cursors[k])\n channelRef.value = null\n })\n .on(\n 'presence',\n { event: 'leave' },\n ({ leftPresences }: { leftPresences: Array<{ key: string }> }) => {\n leftPresences.forEach(({ key }) => {\n delete cursors[key]\n })\n }\n )\n .on('presence', { event: 'join' }, () => {\n if (!cursorPayload.value) return\n\n channelRef.value?.send({\n type: 'broadcast',\n event: EVENT_NAME,\n payload: cursorPayload.value,\n })\n })\n .on('broadcast', { event: EVENT_NAME }, ({ payload }: { payload: CursorEventPayload }) => {\n if (payload.user.id === userId) return\n\n cursors[payload.user.id] = payload\n })\n .subscribe(async (status: REALTIME_SUBSCRIBE_STATES) => {\n if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) {\n try {\n await channel.track({ key: userId })\n channelRef.value = channel\n } catch (err) {\n console.error('Failed to track presence for current user:', err)\n channelRef.value = null\n }\n } else {\n Object.keys(cursors).forEach((k) => delete cursors[k])\n channelRef.value = null\n }\n })\n\n window.addEventListener('mousemove', handleMouseMove)\n })\n\n onUnmounted(() => {\n window.removeEventListener('mousemove', handleMouseMove)\n\n cancelThrottle()\n\n if (channelRef.value) {\n channelRef.value.unsubscribe()\n channelRef.value = null\n }\n\n Object.keys(cursors).forEach((k) => delete cursors[k])\n })\n\n return { cursors }\n}\n",
"type": "registry:component",
"target": "app/composables/useRealtimeCursors.ts"
}
]
}