diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/.gitignore b/apps/react/permissions/on-demand/custom/permissions-demo/.gitignore new file mode 100644 index 00000000..8f322f0d --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/README.md b/apps/react/permissions/on-demand/custom/permissions-demo/README.md new file mode 100644 index 00000000..6b233627 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/README.md @@ -0,0 +1,112 @@ +# permissions-demo + +## Overview + +This demo showcases **custom** for **permissions** (on-demand) in **react**. + +## Path + +``` +apps/react/permissions/on-demand/custom/permissions-demo/ +``` + +## Package Name + +`@apps/react-permissions-on-demand-custom-permissions-demo` + +## Directory Structure + +``` +permissions-demo/ +├── app/ +│ ├── layout.tsx # Root layout +│ └── page.tsx # Main page +├── components/ +│ ├── header/ # Header components (Velt notifications, etc.) +│ │ └── header.tsx +│ ├── sidebar/ # Sidebar components +│ │ └── sidebar.tsx +│ └── document/ # Main document/canvas logic +│ └── document-canvas.tsx +├── hooks/ # Custom React hooks +├── lib/ # Utility functions +│ └── utils.ts +├── public/ # Static assets +├── styles/ # Global styles +│ └── globals.css +├── .npmrc # pnpm config to prevent Tailwind v4 hoisting +├── next.config.js +├── tailwind.config.js +├── tsconfig.json +├── components.json # shadcn/ui configuration +└── package.json +``` + +## Getting Started + +### Install Dependencies + +From the monorepo root: + +```bash +pnpm install +``` + +### Run Development Server + +```bash +cd apps/react/permissions/on-demand/custom/permissions-demo +pnpm dev +``` + +Or from the root: + +```bash +pnpm --filter @apps/react-permissions-on-demand-custom-permissions-demo dev +``` + +### Build for Production + +```bash +pnpm --filter @apps/react-permissions-on-demand-custom-permissions-demo build +``` + +## Structure + +- **Framework**: react +- **Feature**: permissions +- **Document**: on-demand +- **Library**: custom +- **Demo**: permissions-demo + +## Component Organization + +- **`components/header/`** - Contains Velt components like notifications, presence indicators, header buttons +- **`components/sidebar/`** - Contains sidebar-related components +- **`components/document/`** - Contains the main application logic and custom integration +- **`hooks/`** - Custom React hooks for state management and side effects +- **`lib/`** - Utility functions and helpers + +## Important Configuration + +### .npmrc File +This demo includes a `.npmrc` file that prevents pnpm from hoisting Tailwind CSS v4 from other workspace packages. This is necessary because: +- This demo uses Tailwind CSS v3.4.x with traditional PostCSS configuration +- Other apps in the monorepo may use Tailwind CSS v4 +- Without the `.npmrc`, pnpm would hoist v4 and cause PostCSS errors + +**Do not delete the `.npmrc` file** - it ensures the correct Tailwind version is used. + +## Next Steps + +1. Add your custom implementation in `components/document/` +2. Add Velt collaboration features in `components/header/` +3. Update this README with specific usage instructions +4. Add the demo to `master-sample-app` if it should be showcased +5. Update deployment configs (Vercel, GitHub Actions) if needed + +## Learn More + +- [Monorepo Structure Guide](../../../../../README_MONOREPO.md) +- [Structure Documentation](../../../../../docs/structure.md) +- [Velt Documentation](https://docs.velt.dev) diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/app/api/check-permissions/route.ts b/apps/react/permissions/on-demand/custom/permissions-demo/app/api/check-permissions/route.ts new file mode 100644 index 00000000..7735d11b --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/app/api/check-permissions/route.ts @@ -0,0 +1,177 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { + computeEffectiveAccess, + loadPermissionSettings, + loadSelectedUser, + PermissionSettings, + UserRole, + NodeType, +} from '@/lib/permissions-data'; + +// Velt Permission Query types +interface PermissionResource { + type: NodeType | 'context'; + id: string; + source: string; + organizationId: string; + context?: Record; +} + +interface PermissionQueryRequest { + userId: string; + resource: PermissionResource; +} + +interface PermissionQueryBody { + data: { + requests: PermissionQueryRequest[]; + }; +} + +// Velt Permission Result types +interface PermissionResultItem { + userId: string; + resourceId: string; + type: string; + organizationId: string; + hasAccess: boolean; + accessRole?: 'viewer' | 'editor'; + expiresAt?: number; +} + +interface PermissionResultResponse { + data: PermissionResultItem[]; + success: boolean; + statusCode: number; + message?: string; +} + +// Map Velt resource IDs to our internal IDs +function mapResourceId(resourceId: string, resourceType: string): string { + // Our demo uses IDs like 'org-a', 'folder-a', 'doc-a' + // Velt might send different IDs, so we need to map them + + // If it's already our format, return as-is + if (resourceId.startsWith('org-') || resourceId.startsWith('folder-') || resourceId.startsWith('doc-')) { + return resourceId; + } + + // For the demo, we'll use the organization ID 'org-a' for organization type + if (resourceType === 'organization') { + return 'org-a'; + } + + return resourceId; +} + +export async function POST(request: NextRequest): Promise> { + try { + const body: PermissionQueryBody = await request.json(); + const { requests } = body.data; + + // Load current permission settings and user from the request headers or defaults + // In a real app, these would come from your database + // For this demo, we read from localStorage via cookies/headers or use defaults + + let permissionSettings: PermissionSettings; + let selectedUser: UserRole; + + // Try to get settings from custom headers (for demo purposes) + const settingsHeader = request.headers.get('x-demo-permission-settings'); + const userHeader = request.headers.get('x-demo-selected-user'); + + if (settingsHeader) { + try { + permissionSettings = JSON.parse(settingsHeader); + } catch { + permissionSettings = loadPermissionSettings(); + } + } else { + permissionSettings = loadPermissionSettings(); + } + + if (userHeader && ['Intern', 'Owner', 'Custom'].includes(userHeader)) { + selectedUser = userHeader as UserRole; + } else { + selectedUser = loadSelectedUser(); + } + + // Process each permission request + const permissions: PermissionResultItem[] = []; + + for (const req of requests) { + const { userId, resource } = req; + const { type, id, organizationId } = resource; + + // Handle context-based requests (return access based on org membership) + if (type === 'context') { + const orgAccess = computeEffectiveAccess('org-a', selectedUser, permissionSettings); + permissions.push({ + userId, + resourceId: id, + type: 'context', + organizationId, + hasAccess: orgAccess.hasAccess, + }); + continue; + } + + // Map the resource ID to our internal ID + const internalId = mapResourceId(id, type); + + // Compute access for this resource + const access = computeEffectiveAccess(internalId, selectedUser, permissionSettings); + + const result: PermissionResultItem = { + userId, + resourceId: id, + type, + organizationId, + hasAccess: access.hasAccess, + }; + + // Add accessRole for documents + if (type === 'document' && access.hasAccess) { + result.accessRole = access.accessRole; + // Set expiration 10 minutes from now for demo + result.expiresAt = Date.now() + 10 * 60 * 1000; + } + + permissions.push(result); + } + + // Return response in Velt's expected format + const response: PermissionResultResponse = { + data: permissions, + success: true, + statusCode: 200, + message: 'Permissions validated successfully', + }; + + return NextResponse.json(response, { status: 200 }); + } catch (error) { + console.error('Permission check error:', error); + + return NextResponse.json( + { + data: [], + success: false, + statusCode: 500, + message: 'Internal server error', + }, + { status: 500 } + ); + } +} + +// Handle OPTIONS for CORS +export async function OPTIONS(): Promise { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-demo-permission-settings, x-demo-selected-user', + }, + }); +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/app/api/velt/token/route.ts b/apps/react/permissions/on-demand/custom/permissions-demo/app/api/velt/token/route.ts new file mode 100644 index 00000000..2eaff7bd --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/app/api/velt/token/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server'; + +// [Velt] Replace with your own API key and auth token from https://console.velt.dev +const NEXT_PUBLIC_VELT_API_KEY = "YOUR_VELT_API_KEY"; +const VELT_AUTH_TOKEN = "YOUR_VELT_AUTH_TOKEN"; + +export async function POST(req: NextRequest) { + try { + const { userId, organizationId, email, isAdmin } = await req.json(); + if (!userId || !organizationId) { + return NextResponse.json({ error: 'Missing userId or organizationId' }, { status: 400 }); + } + if (!VELT_AUTH_TOKEN) { + return NextResponse.json({ error: 'Server configuration error: missing VELT_AUTH_TOKEN' }, { status: 500 }); + } + const body = { + data: { + userId, + userProperties: { + organizationId, + ...(typeof isAdmin === 'boolean' ? { isAdmin } : {}), + ...(email ? { email } : {}), + }, + }, + }; + const res = await fetch('https://api.velt.dev/v2/auth/token/get', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-velt-api-key': NEXT_PUBLIC_VELT_API_KEY, 'x-velt-auth-token': VELT_AUTH_TOKEN }, + body: JSON.stringify(body), + }); + const json = await res.json(); + const token = json?.result?.data?.token; + if (!res.ok || !token) { + return NextResponse.json({ error: json?.error?.message || 'Failed to generate token' }, { status: 500 }); + } + return NextResponse.json({ token }); + } catch { + return NextResponse.json({ error: 'Internal error' }, { status: 500 }); + } +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/app/document/DocumentContext.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/app/document/DocumentContext.tsx new file mode 100644 index 00000000..b09a9140 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/app/document/DocumentContext.tsx @@ -0,0 +1,77 @@ +'use client'; +import { useMemo, useEffect, useState, useRef } from 'react'; + +/** + * ⚠️ IMPORTANT DISCLAIMER FOR DEVELOPERS ⚠️ + * + * This documentId logic using URL parameters is ONLY for demo sharing purposes. + * It allows users to share a link to a specific document in this demo app. + * + * IN YOUR REAL APPLICATION: + * - Use your own document ID system (e.g., from your database, CMS, or file system) + * - You do NOT need to use URLs as document IDs + * - You do NOT need to store document IDs in localStorage or URL params + * - Simply pass your existing document identifier to Velt + * + * Examples of real-world document IDs: + * - Database record ID: "user-doc-12345" + * - File path: "/projects/my-project/document.txt" + * - UUID: "550e8400-e29b-41d4-a716-446655440000" + * - Any unique identifier from your system + * + * The URL-based approach here is purely for convenience in this demo environment + * to enable sharing and collaboration testing. Do not feel obligated to replicate + * this pattern in your production application. + */ + +// [Velt] Minimal hard-coded current document hook +export type CurrentDocument = { + documentId: string | null; + documentName: string; +}; + +export function useCurrentDocument(): CurrentDocument { + const [documentId, setDocumentId] = useState(null); + const isInitialized = useRef(false); + + useEffect(() => { + // Prevent double initialization (React Strict Mode, HMR, etc.) + if (isInitialized.current) return; + + // 1. Check URL for documentId parameter first + const urlParams = new URLSearchParams(window.location.search); + let docId = urlParams.get('documentId'); + + if (docId) { + // Use document ID from URL (shareable link) + setDocumentId(docId); + localStorage.setItem('velt-demo-document-id', docId); + } else { + // 2. Check localStorage for existing document + const stored = localStorage.getItem('velt-demo-document-id'); + if (stored) { + docId = stored; + } else { + // 3. Generate new document ID + docId = `doc-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + localStorage.setItem('velt-demo-document-id', docId); + } + + // Update URL with document ID for shareability + const newUrl = `${window.location.pathname}?documentId=${docId}`; + window.history.pushState({}, '', newUrl); + + setDocumentId(docId); + } + + isInitialized.current = true; + }, []); + + return useMemo( + () => ({ + documentId: documentId, + documentName: "My Document", + }), + [documentId] + ); +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/app/document/useCurrentDocument.ts b/apps/react/permissions/on-demand/custom/permissions-demo/app/document/useCurrentDocument.ts new file mode 100644 index 00000000..b36de938 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/app/document/useCurrentDocument.ts @@ -0,0 +1,4 @@ +'use client'; + +export { useCurrentDocument } from './DocumentContext'; +export type { CurrentDocument } from './DocumentContext'; diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/app/layout.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/app/layout.tsx new file mode 100644 index 00000000..195bb6c9 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from 'next' +import '../styles/globals.css' +import { Providers } from './providers' + +export const metadata: Metadata = { + title: 'permissions-demo', + description: 'On-Demand Permissions Demo with Velt', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + {children} + + + ) +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/app/page.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/app/page.tsx new file mode 100644 index 00000000..f8a88c64 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/app/page.tsx @@ -0,0 +1,9 @@ +import DocumentCanvas from '@/components/document/document-canvas' + +export default function Home() { + return ( +
+ +
+ ) +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/app/providers.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/app/providers.tsx new file mode 100644 index 00000000..8a5d90f3 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/app/providers.tsx @@ -0,0 +1,26 @@ +'use client' + +import { VeltProvider } from '@veltdev/react' +import { AppProviders } from './userAuth/AppProviders' + +export function Providers({ children }: { children: React.ReactNode }) { + const apiKey = process.env.NEXT_PUBLIC_VELT_API_KEY || '' + + return ( + + {children} + + ) +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/app/userAuth/AppProviders.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/app/userAuth/AppProviders.tsx new file mode 100644 index 00000000..a17dc9e8 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/app/userAuth/AppProviders.tsx @@ -0,0 +1,15 @@ +"use client"; + +import React from "react"; +import { AppUserProvider } from "./AppUserContext"; + +/** + * Client component wrapper for AppUserProvider + */ +export function AppProviders({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/app/userAuth/AppUserContext.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/app/userAuth/AppUserContext.tsx new file mode 100644 index 00000000..6f157cc7 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/app/userAuth/AppUserContext.tsx @@ -0,0 +1,182 @@ +"use client"; + +import type { User } from "@veltdev/types"; +import React, { useCallback, useContext, useEffect, useState } from "react"; + +/** + * ⚠️ IMPORTANT DISCLAIMER FOR DEVELOPERS ⚠️ + * + * This user authentication logic is ONLY for demo purposes. + * It generates random users and stores them in localStorage/sessionStorage to simulate + * a multi-user collaboration environment for demonstration. + * + * IN YOUR REAL APPLICATION: + * - Fetch the currently logged in user from YOUR existing authentication system + * - Use your own user management (Auth0, Firebase Auth, custom backend, etc.) + * - Simply pass your authenticated user object to Velt's identify() method + * - You do NOT need to generate random users or store users in localStorage + * + * Example of real-world usage: + * ```typescript + * // Get your authenticated user from your auth system + * const currentUser = await yourAuthSystem.getCurrentUser(); + * + * // Pass it to Velt via authProvider (see VeltInitializeUser.tsx for full implementation) + * const authProvider: VeltAuthProvider = { user: currentUser, generateToken: ... }; + * ``` + * + * The random user generation and storage logic here is purely for demo convenience + * to simulate multiple users without requiring actual authentication infrastructure. + * Do not replicate this pattern in your production application. + */ + +type AppUserContextValue = { + user: User | undefined; + login: (u: User) => void; + logout: () => void; + isUserLoggedIn?: boolean; +}; + +const AppUserContext = React.createContext( + undefined +); + +// Simple hash function to convert userId to a consistent number +function hashStringToIndex(str: string, arrayLength: number): number { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32-bit integer + } + return Math.abs(hash) % arrayLength; +} + +// Generate a deterministic hash from a string (for userId generation) +function hashString(str: string): string { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32-bit integer + } + // Convert to base36 and pad to ensure consistent length + return Math.abs(hash).toString(36).padStart(8, '0'); +} + +// Generate a random user with deterministic ID based on name +function generateRandomUser(): User { + const avatarColors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E2']; + + const firstNames = ['Alex', 'Sam', 'Jordan', 'Taylor', 'Casey', 'Morgan', 'Riley', 'Avery']; + const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis']; + + // Randomly select first and last name + const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]; + const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]; + const fullName = `${firstName} ${lastName}`; + + // Generate deterministic userId from the full name + // This ensures "Alex Smith" always gets the same ID + const userId = `user-${hashString(fullName)}`; + + // Use deterministic color based on userId for consistent photoUrl + const colorIndex = hashStringToIndex(userId, avatarColors.length); + const avatarColor = avatarColors[colorIndex]; + + return { + userId: userId, + name: fullName, + email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`, + organizationId: "sample-apps-demo-org", + photoUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(fullName)}&background=${avatarColor.substring(1)}&color=fff&size=128`, + }; +} + +export function AppUserProvider({ + children +}: { + children: React.ReactNode; +}) { + const [user, setUser] = useState(undefined); + const [isUserLoggedIn, setIsUserLoggedIn] = useState( + undefined + ); + + useEffect(() => { + if (typeof window === 'undefined') return; // Guard against SSR + + try { + // Detect if running in iframe (for master-sample-app dual view) + const isInIframe = window.self !== window.top; + + // Choose storage based on iframe context: + // - In iframe: use sessionStorage (each origin/port is isolated automatically) + // - Not in iframe: use localStorage (same user across tabs) + const storage = isInIframe ? sessionStorage : localStorage; + const STORAGE_KEY = 'velt-demo-user'; + + // Check storage for existing user + const stored = storage.getItem(STORAGE_KEY); + + let selectedUser: User; + if (stored) { + // Use existing user from storage + selectedUser = JSON.parse(stored); + } else { + // Generate NEW random user + selectedUser = generateRandomUser(); + storage.setItem(STORAGE_KEY, JSON.stringify(selectedUser)); + } + + setUser(selectedUser); + setIsUserLoggedIn(true); + } catch {} + }, []); + + const login = useCallback((next: User) => { + if (typeof window === 'undefined') return; + + try { + const isInIframe = window.self !== window.top; + const storage = isInIframe ? sessionStorage : localStorage; + const STORAGE_KEY = 'velt-demo-user'; + + setUser(next); + setIsUserLoggedIn(true); + storage.setItem(STORAGE_KEY, JSON.stringify(next)); + } catch {} + }, []); + + const logout = useCallback(() => { + if (typeof window === 'undefined') return; + + try { + const isInIframe = window.self !== window.top; + const storage = isInIframe ? sessionStorage : localStorage; + const STORAGE_KEY = 'velt-demo-user'; + + setUser(undefined); + setIsUserLoggedIn(false); + storage.removeItem(STORAGE_KEY); + } catch {} + }, []); + + // Memoize the context value to prevent unnecessary re-renders + const contextValue = React.useMemo( + () => ({ user, login, logout, isUserLoggedIn }), + [user, login, logout, isUserLoggedIn] + ); + + return ( + + {children} + + ); +} + +export function useAppUser() { + const ctx = useContext(AppUserContext); + if (!ctx) throw new Error("useAppUser must be used within AppUserProvider"); + return ctx; +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/app/userAuth/useAppUser.ts b/apps/react/permissions/on-demand/custom/permissions-demo/app/userAuth/useAppUser.ts new file mode 100644 index 00000000..aa7ef0aa --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/app/userAuth/useAppUser.ts @@ -0,0 +1,3 @@ +'use client'; + +export { AppUserProvider, useAppUser } from './AppUserContext'; diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/components.json b/apps/react/permissions/on-demand/custom/permissions-demo/components.json new file mode 100644 index 00000000..0ad98211 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "styles/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/components/document/document-canvas.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/components/document/document-canvas.tsx new file mode 100644 index 00000000..23cb55be --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/components/document/document-canvas.tsx @@ -0,0 +1,644 @@ +'use client' + +import { useState, useEffect, useRef } from 'react' +import { + LegoIcon, + HierarchyIcon, + HierarchyIconBlack, + FolderOpenIcon, + FolderOpenIconBlack, + FileDescriptionIcon, + FileDescriptionIconBlack, + ChevronDownIcon, + EyeIcon, + PencilIcon, + ClockIcon, + SmallArrowIcon, +} from './icons' +import { + PermissionSetting, + UserRole, + AccessRole, + PermissionSettings, + AccessRoleSettings, + HIERARCHY_NODES, + DEFAULT_PERMISSION_SETTINGS, + DEFAULT_ACCESS_ROLE_SETTINGS, + PERMISSION_OPTIONS, + USER_OPTIONS, + computeEffectiveAccess, + getPermissionBadgeColor, + savePermissionSettings, + loadPermissionSettings, + saveSelectedUser, + loadSelectedUser, + saveAccessRoleSettings, + loadAccessRoleSettings, + EffectiveAccess, +} from '@/lib/permissions-data' + +// Dropdown component for permission selection +function PermissionDropdown({ + value, + onChange, + nodeId, +}: { + value: PermissionSetting + onChange: (value: PermissionSetting) => void + nodeId: string +}) { + const [isOpen, setIsOpen] = useState(false) + const dropdownRef = useRef(null) + + const colors = getPermissionBadgeColor(value) + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + + return ( +
+ + + {isOpen && ( +
+ {PERMISSION_OPTIONS.map((option) => ( + + ))} +
+ )} +
+ ) +} + +// Dropdown component for user selection +function UserDropdown({ + value, + onChange, +}: { + value: UserRole + onChange: (value: UserRole) => void +}) { + const [isOpen, setIsOpen] = useState(false) + const dropdownRef = useRef(null) + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + + return ( +
+ + + {isOpen && ( +
+ {USER_OPTIONS.map((option) => ( + + ))} +
+ )} +
+ ) +} + +// Access badge toggle between viewer/editor +function AccessBadge({ + access, + accessRole, + onChange, +}: { + access: EffectiveAccess + accessRole: AccessRole + onChange: (role: AccessRole) => void +}) { + if (!access.hasAccess) { + return ( +
+
+ +
+
+

+ no access +

+
+
+ ) + } + + const isViewer = accessRole === 'viewer' + const isEditor = accessRole === 'editor' + + return ( +
+ {/* Viewer toggle */} + + {/* Editor toggle */} + +
+ ) +} + +export default function DocumentCanvas() { + const [selectedUser, setSelectedUser] = useState('Intern') + const [permissionSettings, setPermissionSettings] = useState(DEFAULT_PERMISSION_SETTINGS) + const [accessRoleSettings, setAccessRoleSettings] = useState(DEFAULT_ACCESS_ROLE_SETTINGS) + const [isInitialized, setIsInitialized] = useState(false) + + // Load initial state from localStorage + useEffect(() => { + setSelectedUser(loadSelectedUser()) + setPermissionSettings(loadPermissionSettings()) + setAccessRoleSettings(loadAccessRoleSettings()) + setIsInitialized(true) + }, []) + + // Listen for cross-tab updates via storage events + useEffect(() => { + function handleStorageChange(event: StorageEvent) { + if (event.key === 'velt-demo-permission-settings' && event.newValue) { + try { + setPermissionSettings(JSON.parse(event.newValue)) + } catch { + // Ignore parse errors + } + } + if (event.key === 'velt-demo-selected-user' && event.newValue) { + if (['Intern', 'Owner', 'Custom'].includes(event.newValue)) { + setSelectedUser(event.newValue as UserRole) + } + } + if (event.key === 'velt-demo-access-role-settings' && event.newValue) { + try { + setAccessRoleSettings(JSON.parse(event.newValue)) + } catch { + // Ignore parse errors + } + } + } + + window.addEventListener('storage', handleStorageChange) + return () => window.removeEventListener('storage', handleStorageChange) + }, []) + + // Handle user change + function handleUserChange(user: UserRole) { + setSelectedUser(user) + saveSelectedUser(user) + } + + // Handle permission change + function handlePermissionChange(nodeId: string, permission: PermissionSetting) { + const newSettings = { ...permissionSettings, [nodeId]: permission } + setPermissionSettings(newSettings) + savePermissionSettings(newSettings) + } + + // Handle access role change + function handleAccessRoleChange(nodeId: string, role: AccessRole) { + const newSettings = { ...accessRoleSettings, [nodeId]: role } + setAccessRoleSettings(newSettings) + saveAccessRoleSettings(newSettings) + } + + // Compute access for all nodes + const accessMap: Record = {} + for (const node of HIERARCHY_NODES) { + accessMap[node.id] = computeEffectiveAccess(node.id, selectedUser, permissionSettings) + } + + // Get node data helpers + const orgA = HIERARCHY_NODES.find((n) => n.id === 'org-a')! + const folderA = HIERARCHY_NODES.find((n) => n.id === 'folder-a')! + const folderB = HIERARCHY_NODES.find((n) => n.id === 'folder-b')! + const docA = HIERARCHY_NODES.find((n) => n.id === 'doc-a')! + const docB = HIERARCHY_NODES.find((n) => n.id === 'doc-b')! + const docC = HIERARCHY_NODES.find((n) => n.id === 'doc-c')! + + // Don't render until initialized to prevent hydration mismatch + if (!isInitialized) { + return ( +
+
+
+
+
+
+
+ ) + } + + return ( +
+ {/* Dot Grid Background */} +
+
+
+
+
+
+
+
+
+
+ + {/* Left Section: User and Permissions List */} + {/* User Section */} +
+ +
+ +

+ is part of Organization A +

+
+
+ + {/* Permissions List */} +
+ {/* Organization Section */} +
+
+ +

+ Organization +

+
+
+
+

+ {orgA.name} +

+ handlePermissionChange('org-a', v)} + nodeId="org-a" + /> +
+ handleAccessRoleChange('org-a', role)} /> + +
+
+ + {/* Folders Section */} +
+
+ +

+ Folders +

+
+ + {/* Folder A */} +
+
+

+ {folderA.name} +

+ handlePermissionChange('folder-a', v)} + nodeId="folder-a" + /> +
+ handleAccessRoleChange('folder-a', role)} /> + +
+ + {/* Folder B */} +
+
+

+ {folderB.name} +

+ handlePermissionChange('folder-b', v)} + nodeId="folder-b" + /> +
+ handleAccessRoleChange('folder-b', role)} /> + +
+
+ + {/* Documents Section */} +
+
+ +

+ Documents +

+
+ + {/* Document A */} +
+
+

+ {docA.name} +

+ handlePermissionChange('doc-a', v)} + nodeId="doc-a" + /> +
+ handleAccessRoleChange('doc-a', role)} /> + +
+ + {/* Document B */} +
+
+

+ {docB.name} +

+ handlePermissionChange('doc-b', v)} + nodeId="doc-b" + /> +
+ handleAccessRoleChange('doc-b', role)} /> + +
+ + {/* Document C */} +
+
+

+ {docC.name} +

+ handlePermissionChange('doc-c', v)} + nodeId="doc-c" + /> +
+ handleAccessRoleChange('doc-c', role)} /> + +
+
+
+ + {/* Right Section: Hierarchy Diagram */} + {/* Organization A Badge */} +
+
+ +
+

+ {orgA.name} +

+
+ + {/* Folder A Badge */} +
+
+ +
+

+ {folderA.name} +

+
+ + {/* Folder B Badge */} +
+
+ +
+

+ {folderB.name} +

+
+ + {/* Document A Badge */} +
+
+ +
+

+ {docA.name} +

+
+ + {/* Document B Badge */} +
+
+ +
+

+ {docB.name} +

+
+ + {/* Document C Badge */} +
+
+ +
+

+ {docC.name} +

+
+ + {/* Connection Lines */} + {/* From Organization A to Folder A - curved line */} + + + + + {/* From Organization A to Folder B - vertical dashed line */} + + + + + {/* From Folder B area to Document A - curved dashed line */} + + + + + {/* From Folder A to Document B - vertical line */} + + + + + {/* From Folder B to Document C - vertical line */} + + + +
+ ) +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/components/document/icons/index.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/components/document/icons/index.tsx new file mode 100644 index 00000000..21abcbf0 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/components/document/icons/index.tsx @@ -0,0 +1,91 @@ +import React from 'react'; + +interface IconProps { + className?: string; + style?: React.CSSProperties; +} + +// Lego user icon (yellow face) - from Figma tabler-icon-lego +export const LegoIcon = ({ className, style }: IconProps) => ( + + + +); + +// Hierarchy icon for organization - white version for left panel +export const HierarchyIcon = ({ className, style }: IconProps) => ( + + + +); + +// Hierarchy icon for graph badge - black version +export const HierarchyIconBlack = ({ className, style }: IconProps) => ( + + + +); + +// Folder open icon - white version for left panel +export const FolderOpenIcon = ({ className, style }: IconProps) => ( + + + +); + +// Folder open icon - black version for graph badge +export const FolderOpenIconBlack = ({ className, style }: IconProps) => ( + + + +); + +// File description icon - white/pink version for left panel +export const FileDescriptionIcon = ({ className, style }: IconProps) => ( + + + +); + +// File description icon - black version for graph badge +export const FileDescriptionIconBlack = ({ className, style }: IconProps) => ( + + + +); + +// Chevron down icon for dropdowns +export const ChevronDownIcon = ({ className, style }: IconProps) => ( + + + +); + +// Eye icon for access badge +export const EyeIcon = ({ className, style }: IconProps) => ( + + + + +); + +// Pencil icon for editor badge +export const PencilIcon = ({ className, style }: IconProps) => ( + + + +); + +// Clock icon +export const ClockIcon = ({ className, style }: IconProps) => ( + + + +); + +// Small arrow icon for user dropdown +export const SmallArrowIcon = ({ className, style }: IconProps) => ( + + + +); diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/VeltCollaboration.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/VeltCollaboration.tsx new file mode 100644 index 00000000..a970ef6d --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/VeltCollaboration.tsx @@ -0,0 +1,41 @@ +"use client"; +import { useVeltClient, VeltComments, VeltCommentsSidebar } from "@veltdev/react"; +import VeltInitializeDocument from "./VeltInitializeDocument"; +import { VeltCustomization } from "./ui-customization/VeltCustomization"; +import { useEffect } from "react"; +import { useAppUser } from "@/app/userAuth/AppUserContext"; + +export function VeltCollaboration() { + const { isUserLoggedIn } = useAppUser(); + // [Velt] Get Velt client instance + const { client } = useVeltClient(); + + // [Velt] Sign out user when user logs out, getting user login state from host app + useEffect(() => { + if (isUserLoggedIn === false && client) { + client.signOutUser(); + } + }, [isUserLoggedIn, client]); + + const groupConfig = { + enable: false + }; + + return ( + <> + + + + + + ); +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/VeltInitializeDocument.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/VeltInitializeDocument.tsx new file mode 100644 index 00000000..e6553775 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/VeltInitializeDocument.tsx @@ -0,0 +1,23 @@ +'use client'; +import { useEffect } from 'react'; +import { useSetDocuments } from '@veltdev/react'; +import { useCurrentDocument } from '@/app/document/useCurrentDocument'; +import { useAppUser } from '@/app/userAuth/useAppUser'; + +export default function VeltInitializeDocument() { + const { documentId, documentName } = useCurrentDocument(); + const { user } = useAppUser(); + + // [Velt] Get document setter hook + const { setDocuments } = useSetDocuments(); + + // [Velt] Set document in Velt. This is the resource where all Velt collaboration data will be scoped. + useEffect(() => { + if (!user || !documentId || !documentName) return; + setDocuments([ + { id: documentId, metadata: { documentName: documentName || 'Untitled' } }, + ]); + }, [user, setDocuments, documentId, documentName]); + + return null; +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/VeltInitializeUser.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/VeltInitializeUser.tsx new file mode 100644 index 00000000..33c9cc87 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/VeltInitializeUser.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { useAppUser } from "@/app/userAuth/useAppUser"; +import type { VeltAuthProvider } from "@veltdev/types"; +import { useMemo } from "react"; + +// [Velt] Call your backend API to generate a JWT token for the user +async function getVeltJwtFromBackend(user: { + userId: string; + organizationId: string; + email?: string; +}) { + const resp = await fetch("/api/velt/token", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + userId: user.userId, + organizationId: user.organizationId, + email: user.email, + isAdmin: false, + }), + cache: "no-store", + }); + if (!resp.ok) { + const err = await resp.json().catch(() => ({})); + throw new Error(`Token API failed: ${err?.error || resp.statusText}`); + } + const { token } = await resp.json(); + if (!token) throw new Error("No token in response"); + return token as string; +} + +export function useVeltAuthProvider() { + // [Velt] Get your app's current authenticated user to authenticate with Velt. + const { user } = useAppUser(); + + // [Velt] Create auth provider object to pass to VeltProvider + const authProvider: VeltAuthProvider | undefined = useMemo(() => { + if (!user) return undefined; + return { + user, + retryConfig: { retryCount: 3, retryDelay: 1000 }, + generateToken: async () => { + return await getVeltJwtFromBackend({ + userId: user.userId as string, + organizationId: user.organizationId as string, + email: user.email, + }); + }, + }; + }, [user]); + + return { authProvider }; +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/VeltTools.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/VeltTools.tsx new file mode 100644 index 00000000..f3968964 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/VeltTools.tsx @@ -0,0 +1,29 @@ +"use client"; +import { + VeltPresence, + VeltSidebarButton, + VeltNotificationsTool, +} from "@veltdev/react"; + +function VeltTools() { + return ( + <> + {/* [Velt] Show online users */} + + {/* [Velt] Toggle comments sidebar */} + + {/* [Velt] Notifications panel */} + + + ); +} + +export default VeltTools; diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltCommentBubbleWf.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltCommentBubbleWf.tsx new file mode 100644 index 00000000..cefbb258 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltCommentBubbleWf.tsx @@ -0,0 +1,73 @@ +"use client"; +import { VeltCommentBubbleWireframe } from '@veltdev/react'; + +const VeltCommentBubbleWf = () => { + return ( + // [Velt] Custom wireframe for comment bubble UI + +
+
+
+
+
+

+ +

+
+
+ + ); +}; + +export default VeltCommentBubbleWf; diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltCommentToolWf.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltCommentToolWf.tsx new file mode 100644 index 00000000..9241b097 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltCommentToolWf.tsx @@ -0,0 +1,50 @@ +"use client"; +import { VeltCommentToolWireframe } from '@veltdev/react'; + +const VeltCommentToolWf = () => { + return ( + +
+
+
+
+
+ + ); +}; + +export default VeltCommentToolWf; diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltCustomization.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltCustomization.tsx new file mode 100644 index 00000000..69223f4c --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltCustomization.tsx @@ -0,0 +1,29 @@ +"use client"; +import { useVeltClient, VeltWireframe } from "@veltdev/react"; +import VeltCommentBubbleWf from "./VeltCommentBubbleWf"; +import VeltCommentToolWf from "./VeltCommentToolWf"; +import VeltNotificationsToolWf from "./VeltNotificationsToolWf"; +import VeltSidebarButtonWf from "./VeltSidebarButtonWf"; +import "./styles.css"; +import { useEffect } from "react"; + +export function VeltCustomization() { + // [Velt] Get Velt client instance + const { client } = useVeltClient(); + + // [Velt] Enable dark mode + useEffect(() => { + if (client) { + client.setDarkMode(true); + } + }, [client]); + + return ( + + + + + + + ); +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltNotificationsToolWf.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltNotificationsToolWf.tsx new file mode 100644 index 00000000..c7086688 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltNotificationsToolWf.tsx @@ -0,0 +1,59 @@ +"use client"; +import { VeltNotificationsToolWireframe } from '@veltdev/react'; + +const bellIcon = "/icons/bell-icon.svg"; + +const VeltNotificationsToolWf = () => { + return ( + +
+
+ +
+
+ +
+
+
+ ); +}; + +export default VeltNotificationsToolWf; diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltSidebarButtonWf.tsx b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltSidebarButtonWf.tsx new file mode 100644 index 00000000..98c8a1c8 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/VeltSidebarButtonWf.tsx @@ -0,0 +1,60 @@ +"use client"; +import { VeltSidebarButtonWireframe } from '@veltdev/react'; + +const inboxIcon = "/icons/inbox-icon.svg"; + +const VeltSidebarButtonWf = () => { + return ( + +
+
+ +
+
+ +
+
+
+ ); +}; + +export default VeltSidebarButtonWf; diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/styles.css b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/styles.css new file mode 100644 index 00000000..c7868b22 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/components/velt/ui-customization/styles.css @@ -0,0 +1,163 @@ +/* Place Velt UI customization styles here */ + +/* Theme Configuration - Based on Figma Design */ + +/* Typography */ +--velt-default-font-family: Poppins; + +/* Border Radius */ +--velt-border-radius-2xs: 0.061rem; /* 0.972px */ +--velt-border-radius-xs: 0.125rem; /* 2px */ +--velt-border-radius-sm: 0.375rem; /* 6px */ +--velt-border-radius-md: 0.750rem; /* 12px */ +--velt-border-radius-lg: 1.000rem; /* 16px */ +--velt-border-radius-xl: 1.167rem; /* 18.667px - circular buttons */ +--velt-border-radius-2xl: 1.500rem; /* 24px - pill shape */ +--velt-border-radius-3xl: 2.000rem; /* 32px */ +--velt-border-radius-full: 5.000rem; /* 80px */ + +/* Light Mode */ +/* Accent Colors */ +--velt-light-mode-accent: #141414; +--velt-light-mode-accent-text: #ffffff; +--velt-light-mode-accent-hover: #000000; +--velt-light-mode-accent-foreground: #ffffff; +--velt-light-mode-accent-light: #1f1f1f; +--velt-light-mode-accent-transparent: rgba(20, 20, 20, 0.08); + +/* Background Shades */ +--velt-light-mode-background-0: #ffffff; +--velt-light-mode-background-1: #fafafa; +--velt-light-mode-background-2: #f5f5f5; +--velt-light-mode-background-3: #f0f0f0; +--velt-light-mode-background-4: #ebebeb; +--velt-light-mode-background-5: #e5e5e5; +--velt-light-mode-background-6: #e0e0e0; +--velt-light-mode-background-7: #dbdbdb; +--velt-light-mode-background-8: #d6d6d6; +--velt-light-mode-background-9: #d1d1d1; +--velt-light-mode-background-10: #cccccc; + +/* Text Shades */ +--velt-light-mode-text-0: #ffffff; +--velt-light-mode-text-1: #f2f2f2; +--velt-light-mode-text-2: #e6e6e6; +--velt-light-mode-text-3: #d9d9d9; +--velt-light-mode-text-4: #cccccc; +--velt-light-mode-text-5: #bfbfbf; +--velt-light-mode-text-6: #b3b3b3; +--velt-light-mode-text-7: #a6a6a6; +--velt-light-mode-text-8: #999999; +--velt-light-mode-text-9: #8c8c8c; +--velt-light-mode-text-10: #808080; +--velt-light-mode-text-11: #737373; +--velt-light-mode-text-12: #666666; + +/* Border Shades */ +--velt-light-mode-border-0: #ffffff; +--velt-light-mode-border-1: #f5f5f5; +--velt-light-mode-border-2: #ebebeb; +--velt-light-mode-border-3: #e0e0e0; +--velt-light-mode-border-4: #d6d6d6; +--velt-light-mode-border-5: #cccccc; +--velt-light-mode-border-6: #c2c2c2; +--velt-light-mode-border-7: #b8b8b8; +--velt-light-mode-border-8: #adadad; +--velt-light-mode-border-9: #a3a3a3; +--velt-light-mode-border-10: #999999; + +/* Semantic Colors */ +--velt-light-mode-error: #FF7162; +--velt-light-mode-error-hover: #DE5041; +--velt-light-mode-error-foreground: #ffffff; +--velt-light-mode-error-light: #FFF4F2; +--velt-light-mode-warning: #FFCD2E; +--velt-light-mode-warning-hover: #C69400; +--velt-light-mode-warning-foreground: #141414; +--velt-light-mode-warning-light: #FFFBEE; +--velt-light-mode-success: #198F65; +--velt-light-mode-success-hover: #006B41; +--velt-light-mode-success-foreground: #ffffff; +--velt-light-mode-success-light: #EDF6F3; + +/* Dark Mode - Matching Figma Design */ +/* Accent Colors */ +--velt-dark-mode-accent: #141414; +--velt-dark-mode-accent-text: #ffffff; +--velt-dark-mode-accent-hover: #000000; +--velt-dark-mode-accent-foreground: #ffffff; +--velt-dark-mode-accent-light: #1f1f1f; +--velt-dark-mode-accent-transparent: rgba(20, 20, 20, 0.08); + +/* Background Shades */ +--velt-dark-mode-background-0: #000000; +--velt-dark-mode-background-1: #0a0a0a; +--velt-dark-mode-background-2: #141414; +--velt-dark-mode-background-3: #1a1a1a; +--velt-dark-mode-background-4: #1f1f1f; +--velt-dark-mode-background-5: #242424; +--velt-dark-mode-background-6: #292929; +--velt-dark-mode-background-7: #2e2e2e; +--velt-dark-mode-background-8: #333333; +--velt-dark-mode-background-9: #383838; +--velt-dark-mode-background-10: #3d3d3d; + +/* Text Shades */ +--velt-dark-mode-text-0: #ffffff; +--velt-dark-mode-text-1: #f2f2f2; +--velt-dark-mode-text-2: #e6e6e6; +--velt-dark-mode-text-3: #d9d9d9; +--velt-dark-mode-text-4: #cccccc; +--velt-dark-mode-text-5: #bfbfbf; +--velt-dark-mode-text-6: #b3b3b3; +--velt-dark-mode-text-7: #a6a6a6; +--velt-dark-mode-text-8: #999999; +--velt-dark-mode-text-9: #8c8c8c; +--velt-dark-mode-text-10: #808080; +--velt-dark-mode-text-11: #737373; +--velt-dark-mode-text-12: #666666; + +/* Border Shades */ +--velt-dark-mode-border-0: #0f0f0f; +--velt-dark-mode-border-1: #141414; +--velt-dark-mode-border-2: #1a1a1a; +--velt-dark-mode-border-3: #1f1f1f; +--velt-dark-mode-border-4: #242424; +--velt-dark-mode-border-5: #292929; +--velt-dark-mode-border-6: #2e2e2e; +--velt-dark-mode-border-7: #333333; +--velt-dark-mode-border-8: #383838; +--velt-dark-mode-border-9: #3d3d3d; +--velt-dark-mode-border-10: #424242; + +/* Semantic Colors */ +--velt-dark-mode-error: #FF7162; +--velt-dark-mode-error-hover: #DE5041; +--velt-dark-mode-error-foreground: #ffffff; +--velt-dark-mode-error-light: #2a1917; +--velt-dark-mode-warning: #FFCD2E; +--velt-dark-mode-warning-hover: #C69400; +--velt-dark-mode-warning-foreground: #141414; +--velt-dark-mode-warning-light: #2a2512; +--velt-dark-mode-success: #198F65; +--velt-dark-mode-success-hover: #006B41; +--velt-dark-mode-success-foreground: #ffffff; +--velt-dark-mode-success-light: #0f2319; + +/* Spacing - Based on Figma measurements */ +--velt-spacing-2xs: 0.125rem; /* 2px */ +--velt-spacing-xs: 0.194rem; /* 3.111px */ +--velt-spacing-sm: 0.291rem; /* 4.667px - vertical padding */ +--velt-spacing-md: 0.375rem; /* 6px - gap between components */ +--velt-spacing-lg: 0.389rem; /* 6.222px - horizontal padding */ +--velt-spacing-xl: 0.583rem; /* 9.333px - right padding for badge */ +--velt-spacing-2xl: 1rem; /* 16px */ + +/* Font Size - Based on Figma */ +--velt-font-size-2xs: 0.625rem; /* 10px */ +--velt-font-size-xs: 0.681rem; /* 10.89px - badge text */ +--velt-font-size-sm: 0.875rem; /* 14px */ +--velt-font-size-md: 0.972rem; /* 15.556px - icon size */ +--velt-font-size-lg: 1.167rem; /* 18.667px - icon container */ +--velt-font-size-xl: 1.75rem; /* 28px - component height */ +--velt-font-size-2xl: 2rem; /* 32px */ diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/hooks/.gitkeep b/apps/react/permissions/on-demand/custom/permissions-demo/hooks/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/lib/permissions-data.ts b/apps/react/permissions/on-demand/custom/permissions-demo/lib/permissions-data.ts new file mode 100644 index 00000000..36d07582 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/lib/permissions-data.ts @@ -0,0 +1,274 @@ +// Permission Settings that can be assigned to nodes +export type PermissionSetting = 'Inherit' | 'Public' | 'OrganizationPrivate' | 'Restricted'; + +// User roles for the demo +export type UserRole = 'Intern' | 'Owner' | 'Custom'; + +// Node types in the hierarchy +export type NodeType = 'organization' | 'folder' | 'document'; + +// Access role for Velt +export type AccessRole = 'viewer' | 'editor'; + +// Node in the hierarchy +export interface HierarchyNode { + id: string; + name: string; + type: NodeType; + parentId: string | null; +} + +// Permission settings state +export interface PermissionSettings { + [nodeId: string]: PermissionSetting; +} + +// Access role settings state +export interface AccessRoleSettings { + [nodeId: string]: AccessRole; +} + +// Effective access result +export interface EffectiveAccess { + hasAccess: boolean; + accessRole: AccessRole; + effectivePermission: PermissionSetting; +} + +// Static hierarchy data matching Figma +export const HIERARCHY_NODES: HierarchyNode[] = [ + { id: 'org-a', name: 'Organization A', type: 'organization', parentId: null }, + { id: 'folder-a', name: 'Folder A', type: 'folder', parentId: 'org-a' }, + { id: 'folder-b', name: 'Folder B', type: 'folder', parentId: 'org-a' }, + { id: 'doc-a', name: 'Document A', type: 'document', parentId: 'folder-b' }, + { id: 'doc-b', name: 'Document B', type: 'document', parentId: 'folder-a' }, + { id: 'doc-c', name: 'Document C', type: 'document', parentId: 'org-a' }, +]; + +// Default permission settings matching Figma initial state +export const DEFAULT_PERMISSION_SETTINGS: PermissionSettings = { + 'org-a': 'OrganizationPrivate', + 'folder-a': 'Inherit', + 'folder-b': 'Restricted', + 'doc-a': 'Restricted', + 'doc-b': 'Inherit', + 'doc-c': 'Public', +}; + +// Default access role settings (all start as editor) +export const DEFAULT_ACCESS_ROLE_SETTINGS: AccessRoleSettings = { + 'org-a': 'editor', + 'folder-a': 'editor', + 'folder-b': 'editor', + 'doc-a': 'editor', + 'doc-b': 'editor', + 'doc-c': 'editor', +}; + +// User role configurations +export const USER_CONFIGS: Record = { + 'Intern': { + orgMember: true, + isOwner: false + }, + 'Owner': { + orgMember: true, + isOwner: true + }, + 'Custom': { + orgMember: false, + isOwner: false, + customAccess: ['doc-c'] // Only public docs and explicitly granted + }, +}; + +// Get node by ID +export function getNodeById(nodeId: string): HierarchyNode | undefined { + return HIERARCHY_NODES.find(n => n.id === nodeId); +} + +// Get parent chain for a node (from node to root) +export function getParentChain(nodeId: string): HierarchyNode[] { + const chain: HierarchyNode[] = []; + let currentId: string | null = nodeId; + + while (currentId) { + const node = getNodeById(currentId); + if (!node) break; + chain.push(node); + currentId = node.parentId; + } + + return chain; +} + +// Resolve the effective permission setting (handling Inherit) +export function resolveEffectivePermission( + nodeId: string, + settings: PermissionSettings +): PermissionSetting { + const chain = getParentChain(nodeId); + + for (const node of chain) { + const setting = settings[node.id]; + if (setting && setting !== 'Inherit') { + return setting; + } + } + + // Default to OrganizationPrivate if nothing found + return 'OrganizationPrivate'; +} + +// Compute effective access for a user on a node +export function computeEffectiveAccess( + nodeId: string, + userRole: UserRole, + settings: PermissionSettings +): EffectiveAccess { + const effectivePermission = resolveEffectivePermission(nodeId, settings); + const userConfig = USER_CONFIGS[userRole]; + const node = getNodeById(nodeId); + + let hasAccess = false; + let accessRole: AccessRole = 'viewer'; + + switch (effectivePermission) { + case 'Public': + // Everyone has access to public resources + hasAccess = true; + accessRole = userConfig.isOwner ? 'editor' : 'viewer'; + break; + + case 'OrganizationPrivate': + // Only org members have access + hasAccess = userConfig.orgMember; + accessRole = userConfig.isOwner ? 'editor' : 'editor'; // Org members get editor + break; + + case 'Restricted': + // Only owners or users with explicit custom access + if (userConfig.isOwner) { + hasAccess = true; + accessRole = 'editor'; + } else if (userConfig.customAccess?.includes(nodeId)) { + hasAccess = true; + accessRole = 'viewer'; + } else { + hasAccess = false; + } + break; + + case 'Inherit': + // This shouldn't happen after resolution, but handle it + hasAccess = userConfig.orgMember; + accessRole = 'viewer'; + break; + } + + return { hasAccess, accessRole, effectivePermission }; +} + +// LocalStorage key for permission settings +const STORAGE_KEY = 'velt-demo-permission-settings'; +const USER_STORAGE_KEY = 'velt-demo-selected-user'; +const ACCESS_ROLE_STORAGE_KEY = 'velt-demo-access-role-settings'; + +// Save permission settings to localStorage +export function savePermissionSettings(settings: PermissionSettings): void { + if (typeof window !== 'undefined') { + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + // Dispatch a custom event for same-tab updates + window.dispatchEvent(new CustomEvent('permission-settings-changed', { detail: settings })); + } +} + +// Load permission settings from localStorage +export function loadPermissionSettings(): PermissionSettings { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + try { + return JSON.parse(stored); + } catch { + // Fall back to defaults + } + } + } + return { ...DEFAULT_PERMISSION_SETTINGS }; +} + +// Save selected user to localStorage +export function saveSelectedUser(user: UserRole): void { + if (typeof window !== 'undefined') { + localStorage.setItem(USER_STORAGE_KEY, user); + window.dispatchEvent(new CustomEvent('selected-user-changed', { detail: user })); + } +} + +// Load selected user from localStorage +export function loadSelectedUser(): UserRole { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem(USER_STORAGE_KEY); + if (stored && ['Intern', 'Owner', 'Custom'].includes(stored)) { + return stored as UserRole; + } + } + return 'Intern'; +} + +// Save access role settings to localStorage +export function saveAccessRoleSettings(settings: AccessRoleSettings): void { + if (typeof window !== 'undefined') { + localStorage.setItem(ACCESS_ROLE_STORAGE_KEY, JSON.stringify(settings)); + window.dispatchEvent(new CustomEvent('access-role-settings-changed', { detail: settings })); + } +} + +// Load access role settings from localStorage +export function loadAccessRoleSettings(): AccessRoleSettings { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem(ACCESS_ROLE_STORAGE_KEY); + if (stored) { + try { + return JSON.parse(stored); + } catch { + // Fall back to defaults + } + } + } + return { ...DEFAULT_ACCESS_ROLE_SETTINGS }; +} + +// Permission setting options for dropdowns +export const PERMISSION_OPTIONS: PermissionSetting[] = [ + 'Inherit', + 'Public', + 'OrganizationPrivate', + 'Restricted', +]; + +// User role options for dropdown +export const USER_OPTIONS: UserRole[] = ['Intern', 'Owner', 'Custom']; + +// Get color for permission badge based on setting +export function getPermissionBadgeColor(setting: PermissionSetting): { + bg: string; + text: string; +} { + switch (setting) { + case 'Public': + return { bg: 'rgba(152,246,255,0.08)', text: '#98f6ff' }; + case 'Restricted': + return { bg: 'rgba(246,158,35,0.08)', text: '#f69e23' }; + case 'OrganizationPrivate': + return { bg: 'rgba(255,255,255,0.08)', text: 'white' }; + case 'Inherit': + default: + return { bg: 'rgba(255,255,255,0.08)', text: 'rgba(255,255,255,0.75)' }; + } +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/lib/utils.ts b/apps/react/permissions/on-demand/custom/permissions-demo/lib/utils.ts new file mode 100644 index 00000000..d32b0fe6 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/next.config.js b/apps/react/permissions/on-demand/custom/permissions-demo/next.config.js new file mode 100644 index 00000000..767719fc --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/package.json b/apps/react/permissions/on-demand/custom/permissions-demo/package.json new file mode 100644 index 00000000..8c3caa4c --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/package.json @@ -0,0 +1,31 @@ +{ + "name": "@apps/react-permissions-on-demand-custom-permissions-demo", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@veltdev/react": "^4.5.2-beta.2", + "@veltdev/reactflow-crdt": "4.5.0-beta.6", + "@xyflow/react": "^12.3.0", + "clsx": "^2", + "next": "^15", + "react": "^19", + "react-dom": "^19", + "tailwind-merge": "^2", + "yjs": "^13.6.27" + }, + "devDependencies": { + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "autoprefixer": "^10", + "postcss": "^8", + "tailwindcss": "^3.4.0", + "typescript": "^5" + } +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/postcss.config.js b/apps/react/permissions/on-demand/custom/permissions-demo/postcss.config.js new file mode 100644 index 00000000..33ad091d --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/public/.gitkeep b/apps/react/permissions/on-demand/custom/permissions-demo/public/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/public/icons/bell-icon.svg b/apps/react/permissions/on-demand/custom/permissions-demo/public/icons/bell-icon.svg new file mode 100644 index 00000000..880fb29e --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/public/icons/bell-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/public/icons/inbox-icon.svg b/apps/react/permissions/on-demand/custom/permissions-demo/public/icons/inbox-icon.svg new file mode 100644 index 00000000..e6c7dfe0 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/public/icons/inbox-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/styles/globals.css b/apps/react/permissions/on-demand/custom/permissions-demo/styles/globals.css new file mode 100644 index 00000000..f48962dd --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/styles/globals.css @@ -0,0 +1,61 @@ +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Urbanist:wght@600&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/tailwind.config.js b/apps/react/permissions/on-demand/custom/permissions-demo/tailwind.config.js new file mode 100644 index 00000000..a7b23253 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/tailwind.config.js @@ -0,0 +1,58 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ['class'], + content: [ + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + './app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + fontFamily: { + 'ibm-plex-mono': ['"IBM Plex Mono"', 'monospace'], + 'urbanist': ['"Urbanist"', 'sans-serif'], + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + }, + }, + }, + plugins: [], +} diff --git a/apps/react/permissions/on-demand/custom/permissions-demo/tsconfig.json b/apps/react/permissions/on-demand/custom/permissions-demo/tsconfig.json new file mode 100644 index 00000000..d81d4ee1 --- /dev/null +++ b/apps/react/permissions/on-demand/custom/permissions-demo/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + }, + "target": "ES2017" + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72c8bbac..875dae93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -255,7 +255,7 @@ importers: dependencies: '@veltdev/react': specifier: ^4.6.5 - version: 4.6.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 4.6.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) clsx: specifier: ^2 version: 2.1.1 @@ -983,7 +983,7 @@ importers: version: 2.26.4 '@veltdev/blocknote-crdt-react': specifier: 4.5.0-beta.3 - version: 4.5.0-beta.3(@veltdev/blocknote-crdt@4.5.0-beta.5)(@veltdev/react@4.6.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@veltdev/types@4.5.8-beta.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 4.5.0-beta.3(@veltdev/blocknote-crdt@4.5.0-beta.5)(@veltdev/react@4.6.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@veltdev/types@4.5.8-beta.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -1132,13 +1132,13 @@ importers: version: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) '@veltdev/react': specifier: ^4.6.1 - version: 4.6.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 4.6.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@veltdev/tiptap-crdt': specifier: ^4.5.9-beta.2 version: 4.5.9-beta.2(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/extension-collaboration-caret@3.10.7(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@tiptap/extension-collaboration@3.9.0(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) '@veltdev/tiptap-crdt-react': specifier: ^4.5.9-beta.2 - version: 4.5.9-beta.2(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/extension-collaboration-caret@3.10.7(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@tiptap/extension-collaboration@3.9.0(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(@veltdev/react@4.6.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@veltdev/tiptap-crdt@4.5.9-beta.2(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/extension-collaboration-caret@3.10.7(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@tiptap/extension-collaboration@3.9.0(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@veltdev/types@4.5.8-beta.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 4.5.9-beta.2(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/extension-collaboration-caret@3.10.7(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@tiptap/extension-collaboration@3.9.0(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(@veltdev/react@4.6.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@veltdev/tiptap-crdt@4.5.9-beta.2(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/extension-collaboration-caret@3.10.7(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@tiptap/extension-collaboration@3.9.0(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@veltdev/types@4.5.8-beta.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@veltdev/tiptap-velt-comments': specifier: 4.5.8-beta.1 version: 4.5.8-beta.1(@tiptap/core@3.10.7(@tiptap/pm@3.10.7)) @@ -1195,6 +1195,58 @@ importers: specifier: ^5 version: 5.9.3 + apps/react/permissions/on-demand/custom/permissions-demo: + dependencies: + '@veltdev/react': + specifier: ^4.5.2-beta.2 + version: 4.5.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@veltdev/reactflow-crdt': + specifier: 4.5.0-beta.6 + version: 4.5.0-beta.6(yjs@13.6.27) + '@xyflow/react': + specifier: ^12.3.0 + version: 12.9.1(@types/react@19.2.2)(immer@10.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + clsx: + specifier: ^2 + version: 2.1.1 + next: + specifier: ^15 + version: 15.5.7(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: + specifier: ^19 + version: 19.2.0 + react-dom: + specifier: ^19 + version: 19.2.0(react@19.2.0) + tailwind-merge: + specifier: ^2 + version: 2.6.0 + yjs: + specifier: ^13.6.27 + version: 13.6.27 + devDependencies: + '@types/node': + specifier: ^22 + version: 22.18.13 + '@types/react': + specifier: ^19 + version: 19.2.2 + '@types/react-dom': + specifier: ^19 + version: 19.2.2(@types/react@19.2.2) + autoprefixer: + specifier: ^10 + version: 10.4.21(postcss@8.5.6) + postcss: + specifier: ^8 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.0 + version: 3.4.18(tsx@4.20.6) + typescript: + specifier: ^5 + version: 5.9.3 + packages: '@alloc/quick-lru@5.2.0': @@ -3522,17 +3574,16 @@ packages: react: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.0.0 react-dom: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.0.0 - '@veltdev/react@4.6.1': - resolution: {integrity: sha512-aO+UTKIbtpoqHe/OtajI24zXcQFCCwPFRixLzDdqsMFADIM802SPtmJ2YJLp/EK+sr5/4+PQ/5MnsogCG0wjlA==} + '@veltdev/react@4.6.6': + resolution: {integrity: sha512-8l3+MNP5tFgDY1D3T7WOmR1l+QlP6nS+493FbjGR1cgHeEwXAE4TWngfshmhwxbrQ6vxpA+jp5xijTfVJV4toQ==} peerDependencies: react: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.0.0 react-dom: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.0.0 - '@veltdev/react@4.6.5': - resolution: {integrity: sha512-VD1U+LdRCvvkVvLLj5ARIfU8zW2ltHckzNBs1/NEXxdOhSBJ5XBIdHBYOcsyIF0Mz4QE5J4WTmaol7yS6bI3/g==} + '@veltdev/reactflow-crdt@4.5.0-beta.6': + resolution: {integrity: sha512-a0Hakanz5zJ0wisCStSMaixH8mX4n+P1fdQqvsh34Dhc//8pPQnqlXp1MG2tIotEMcEuSjPgh+9H4Egt0BBuuw==} peerDependencies: - react: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.0.0 - react-dom: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.0.0 + yjs: ^13.6.20 '@veltdev/reactflow-crdt@4.5.8-beta.1': resolution: {integrity: sha512-UH9bLByjB22LpU9jIZidEcykHz7xX8fGQglcOIXicwH5Qu3IftZp/fjBDwNSu0+eVLQtJXR4l7N9TkjqYCNEiw==} @@ -8885,10 +8936,10 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@veltdev/blocknote-crdt-react@4.5.0-beta.3(@veltdev/blocknote-crdt@4.5.0-beta.5)(@veltdev/react@4.6.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@veltdev/types@4.5.8-beta.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@veltdev/blocknote-crdt-react@4.5.0-beta.3(@veltdev/blocknote-crdt@4.5.0-beta.5)(@veltdev/react@4.6.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@veltdev/types@4.5.8-beta.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@veltdev/blocknote-crdt': 4.5.0-beta.5 - '@veltdev/react': 4.6.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@veltdev/react': 4.6.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@veltdev/types': 4.5.8-beta.8 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) @@ -8921,15 +8972,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - '@veltdev/react@4.6.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@veltdev/react@4.6.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - '@veltdev/react@4.6.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@veltdev/reactflow-crdt@4.5.0-beta.6(yjs@13.6.27)': dependencies: - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + yjs: 13.6.27 '@veltdev/reactflow-crdt@4.5.8-beta.1(yjs@13.6.27)': dependencies: @@ -8942,14 +8992,14 @@ snapshots: slate: 0.103.0 slate-react: 0.110.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(slate@0.103.0) - ? '@veltdev/tiptap-crdt-react@4.5.9-beta.2(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/extension-collaboration-caret@3.10.7(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@tiptap/extension-collaboration@3.9.0(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(@veltdev/react@4.6.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@veltdev/tiptap-crdt@4.5.9-beta.2(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/extension-collaboration-caret@3.10.7(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@tiptap/extension-collaboration@3.9.0(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@veltdev/types@4.5.8-beta.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)' + ? '@veltdev/tiptap-crdt-react@4.5.9-beta.2(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/extension-collaboration-caret@3.10.7(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@tiptap/extension-collaboration@3.9.0(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(@veltdev/react@4.6.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@veltdev/tiptap-crdt@4.5.9-beta.2(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/extension-collaboration-caret@3.10.7(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@tiptap/extension-collaboration@3.9.0(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@veltdev/types@4.5.8-beta.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)' : dependencies: '@tiptap/core': 3.10.7(@tiptap/pm@3.10.7) '@tiptap/extension-collaboration': 3.9.0(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27) '@tiptap/extension-collaboration-caret': 3.10.7(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) '@tiptap/pm': 3.10.7 '@tiptap/y-tiptap': 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) - '@veltdev/react': 4.6.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@veltdev/react': 4.6.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@veltdev/tiptap-crdt': 4.5.9-beta.2(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/extension-collaboration-caret@3.10.7(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@tiptap/extension-collaboration@3.9.0(@tiptap/core@3.10.7(@tiptap/pm@3.10.7))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/pm@3.10.7)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) '@veltdev/types': 4.5.8-beta.8 react: 19.2.0