diff --git a/packages/broadcast-panel/README.md b/packages/broadcast-panel/README.md index 42ee24f..5ca2877 100644 --- a/packages/broadcast-panel/README.md +++ b/packages/broadcast-panel/README.md @@ -63,8 +63,7 @@ src/components/ ├── Header.module.css # Estilos del header ├── TransmissionsTable.tsx # Tabla de transmisiones ├── TransmissionsTable.module.css -├── NewTransmissionModal.tsx # Modal de creación -└── NewTransmissionModal.module.css +└── NewTransmissionModal (migrado a `shared/components/NewTransmissionModal.tsx`) ``` ## 🚀 Desarrollo diff --git a/packages/broadcast-panel/README_UPDATED.md b/packages/broadcast-panel/README_UPDATED.md index d43608a..7f096fb 100644 --- a/packages/broadcast-panel/README_UPDATED.md +++ b/packages/broadcast-panel/README_UPDATED.md @@ -188,8 +188,7 @@ src/components/ ├── Header.module.css ├── TransmissionsTable.tsx # Tabla con filtrado ├── TransmissionsTable.module.css -├── NewTransmissionModal.tsx # Modal de creación -└── NewTransmissionModal.module.css +└── (Ahora usa `shared/components/NewTransmissionModal.tsx` y su CSS compartida) ``` ## 🚀 Scripts diff --git a/packages/broadcast-panel/src/components/NewTransmissionModal.tsx b/packages/broadcast-panel/src/components/NewTransmissionModal.tsx index 1bda404..1ec0aec 100644 --- a/packages/broadcast-panel/src/components/NewTransmissionModal.tsx +++ b/packages/broadcast-panel/src/components/NewTransmissionModal.tsx @@ -27,6 +27,10 @@ interface Props { onCreate: (t: Transmission) => void onUpdate?: (t: Transmission) => void transmission?: Transmission // Transmisión a editar + // If provided, modal can be used only to add a destination. When true, selecting a platform + // will call onAddDestination with the created DestinationData and close the modal. + onlyAddDestination?: boolean + onAddDestination?: (d: DestinationData) => void } interface DestinationData { @@ -137,6 +141,12 @@ const NewTransmissionModal: React.FC = ({ open, onClose, onCreate, onUpda icon: platformData[platform]?.icon || , badge: platform === 'YouTube' ? : undefined } + // If this modal is used only to add a destination, call the callback and close + if (onlyAddDestination && onAddDestination) { + onAddDestination(newDest) + onClose() + return + } setDestinations([...destinations, newDest]) setView('main') diff --git a/packages/broadcast-panel/src/components/PageContainer.tsx b/packages/broadcast-panel/src/components/PageContainer.tsx index c33c1bb..389e0b4 100644 --- a/packages/broadcast-panel/src/components/PageContainer.tsx +++ b/packages/broadcast-panel/src/components/PageContainer.tsx @@ -7,7 +7,7 @@ import styles from './PageContainer.module.css' import Sidebar from './Sidebar' import Header from './Header' import TransmissionsTable from './TransmissionsTable' -import NewTransmissionModal from './NewTransmissionModal' +import { NewTransmissionModal } from '@shared/components' import Studio from './Studio' import type { Transmission } from '../types' diff --git a/packages/broadcast-panel/src/components/TransmissionsTable.tsx b/packages/broadcast-panel/src/components/TransmissionsTable.tsx index 375f25f..d6af868 100644 --- a/packages/broadcast-panel/src/components/TransmissionsTable.tsx +++ b/packages/broadcast-panel/src/components/TransmissionsTable.tsx @@ -5,7 +5,7 @@ import { FaYoutube, FaFacebook, FaTwitch, FaLinkedin } from 'react-icons/fa' import { SkeletonTable } from './Skeleton' import styles from './TransmissionsTable.module.css' import InviteGuestsModal from './InviteGuestsModal' -import NewTransmissionModal from './NewTransmissionModal' +import { NewTransmissionModal } from '@shared/components' import type { Transmission } from '../types' interface Props { diff --git a/packages/broadcast-panel/src/env.d.ts b/packages/broadcast-panel/src/env.d.ts new file mode 100644 index 0000000..2bdfffa --- /dev/null +++ b/packages/broadcast-panel/src/env.d.ts @@ -0,0 +1,10 @@ +// Local env declarations for broadcast-panel (Vite-style import.meta.env) +interface ImportMetaEnv { + readonly VITE_TOKEN_SERVER_URL?: string + readonly VITE_STUDIO_URL?: string + [key: string]: string | undefined +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/packages/broadcast-panel/src/types.ts b/packages/broadcast-panel/src/types.ts index db6dbdd..ab2053a 100644 --- a/packages/broadcast-panel/src/types.ts +++ b/packages/broadcast-panel/src/types.ts @@ -1,7 +1,2 @@ -export interface Transmission { - id: string - title: string - platform: string - scheduled: string - createdAt?: string // Fecha de creación -} +export type { Transmission } from '@shared/types' + diff --git a/packages/broadcast-panel/tsconfig.json b/packages/broadcast-panel/tsconfig.json index 9532bdd..e5f85ff 100644 --- a/packages/broadcast-panel/tsconfig.json +++ b/packages/broadcast-panel/tsconfig.json @@ -6,7 +6,8 @@ "baseUrl": "../..", "paths": { "@/*": ["packages/broadcast-panel/src/*"], - "@shared/*": ["shared/*"] + "@shared/*": ["shared/*"], + "@shared": ["shared"] } }, "include": ["src", "../../shared"], diff --git a/packages/studio-panel/src/components/ui/DestinationModal.tsx b/packages/studio-panel/src/components/ui/DestinationModal.tsx deleted file mode 100644 index 02cb47b..0000000 --- a/packages/studio-panel/src/components/ui/DestinationModal.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react' -import { useDestinations, Destination } from '../../hooks/useDestinations' - -const PLATFORMS = [ - { id: 'youtube', label: 'YouTube', color: 'bg-red-600' }, - { id: 'twitch', label: 'Twitch', color: 'bg-purple-700' }, - { id: 'facebook', label: 'Facebook', color: 'bg-blue-600' }, - { id: 'linkedin', label: 'LinkedIn', color: 'bg-indigo-700' }, -] - -type Props = { - open: boolean - onClose: () => void - editing?: Destination | null - onSaved?: (d: Destination) => void -} - -const DestinationModal: React.FC = ({ open, onClose, editing = null, onSaved }) => { - const { addDestination, updateDestination } = useDestinations() - const backdropRef = useRef(null) - const [platform, setPlatform] = useState(PLATFORMS[0].id) - const [label, setLabel] = useState('') - const [url, setUrl] = useState('') - const [error, setError] = useState(null) - - useEffect(() => { - if (editing) { - setPlatform(editing.platform) - setLabel(editing.label) - setUrl(editing.url || '') - } else { - setPlatform(PLATFORMS[0].id) - setLabel('') - setUrl('') - } - setError(null) - }, [editing, open]) - - // Close on Escape - useEffect(() => { - if (!open) return - const onKey = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose() - } - window.addEventListener('keydown', onKey) - return () => window.removeEventListener('keydown', onKey) - }, [open, onClose]) - - if (!open) return null - - const validate = () => { - if (!label || label.trim().length === 0) { - setError('La etiqueta es obligatoria') - return false - } - if (url && !/^https?:\/\//.test(url)) { - setError('La URL debe comenzar con http:// o https://') - return false - } - setError(null) - return true - } - - const handleAdd = () => { - if (!validate()) return - if (editing) { - updateDestination(editing.id, { platform, label, url: url || undefined }) - onSaved && onSaved({ ...editing, platform, label, url: url || undefined }) - } else { - const newD = addDestination({ platform, label, url: url || undefined }) - onSaved && onSaved(newD) - } - onClose() - } - - return ( -
{ - // close when clicking on the backdrop (but not when clicking inside the dialog) - if (e.target === backdropRef.current) onClose() - }} - className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" - > -
e.stopPropagation()} className="w-full max-w-md bg-white dark:bg-gray-900 rounded-md p-4"> -

{editing ? 'Editar destino' : 'Agregar destino'}

-
- -
- {PLATFORMS.map((p) => ( - - ))} -
- - - setLabel(e.target.value)} className="px-3 py-2 rounded-md bg-gray-100 dark:bg-gray-800" /> - - - setUrl(e.target.value)} className="px-3 py-2 rounded-md bg-gray-100 dark:bg-gray-800" /> - - {error &&
{error}
} - -
- - -
-
-
-
- ) -} - -export default DestinationModal diff --git a/packages/studio-panel/src/components/ui/Header.tsx b/packages/studio-panel/src/components/ui/Header.tsx index 0a4c281..8dcf23d 100644 --- a/packages/studio-panel/src/components/ui/Header.tsx +++ b/packages/studio-panel/src/components/ui/Header.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import DestinationModal from './DestinationModal' +import { NewTransmissionModal } from '@shared/components' import { useDestinations, Destination } from '../../hooks/useDestinations' const PlatformBadge: React.FC<{ color: string; children: React.ReactNode }> = ({ color, children }) => ( @@ -8,8 +8,7 @@ const PlatformBadge: React.FC<{ color: string; children: React.ReactNode }> = ({ const Header: React.FC = () => { const [open, setOpen] = useState(false) - const [editing, setEditing] = useState(null) - const { destinations } = useDestinations() + const { destinations, addDestination } = useDestinations() return ( <> @@ -50,13 +49,16 @@ const Header: React.FC = () => { {/* Destinations list removed from inline layout to avoid deforming the header/container. Destinations are managed via the DestinationModal (overlay) which does not affect layout. */} - { + onClose={() => setOpen(false)} + onCreate={() => {}} + onlyAddDestination + onAddDestination={(d: { id: string; platform: string }) => { + const dest: Destination = { id: d.id, platform: d.platform, label: d.platform, url: undefined } + addDestination(dest) setOpen(false) - setEditing(null) }} - editing={editing} /> ) diff --git a/packages/studio-panel/src/hooks/useDestinations.ts b/packages/studio-panel/src/hooks/useDestinations.ts index 81d3896..76ed987 100644 --- a/packages/studio-panel/src/hooks/useDestinations.ts +++ b/packages/studio-panel/src/hooks/useDestinations.ts @@ -1,11 +1,7 @@ import { useEffect, useState } from 'react' +import type { Destination as SharedDestination } from '@shared/types' -export type Destination = { - id: string - platform: string - label: string - url?: string -} +export type Destination = SharedDestination const STORAGE_KEY = 'studio_panel_destinations' diff --git a/packages/studio-panel/tsconfig.json b/packages/studio-panel/tsconfig.json index a7fc6fb..8e30153 100644 --- a/packages/studio-panel/tsconfig.json +++ b/packages/studio-panel/tsconfig.json @@ -8,6 +8,11 @@ /* Bundler mode */ "moduleResolution": "bundler", + "baseUrl": "..", + "paths": { + "@shared/*": ["../shared/*", "../../shared/*"], + "@shared": ["../shared", "../../shared"] + }, "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, diff --git a/shared/components/NewTransmissionModal.module.css b/shared/components/NewTransmissionModal.module.css new file mode 100644 index 0000000..3671de2 --- /dev/null +++ b/shared/components/NewTransmissionModal.module.css @@ -0,0 +1,168 @@ +/* NewTransmissionModal - StreamYard style (migrated to shared) */ + +.modalWrapper { + position: relative; +} + +.modalWrapper.hasBackButton :global(.modalHeader) { + padding-left: 56px; +} + +.backButton { + position: absolute; + top: 50%; + transform: translateY(-50%); + left: 16px; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: none; + border: none; + border-radius: 50%; + color: #5f6368; + cursor: pointer; + transition: all 0.15s; + z-index: 10; +} + +.backButton:hover { + background-color: #f1f3f4; + color: #202124; +} + +.content { + display: flex; + flex-direction: column; + gap: 24px; + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +.platformGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 16px; + padding: 0 24px 24px 24px; + margin-top: -4px; +} + +.destinations { + display: flex; + gap: 16px; + flex-wrap: wrap; +} + +.skipNowContainer { + margin-top: 16px; + text-align: right; +} + +.selectedDestination { + position: relative; +} + +.selectedDestination::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translate(-28px, -3px); + width: 56px; + height: 56px; + border: 3px solid #1a73e8; + border-radius: 50%; + pointer-events: none; + box-sizing: border-box; +} + +.footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 24px; + border-top: 1px solid #e8eaed; + margin: 0 -24px -20px -24px; + gap: 16px; +} + +.footerNote { + color: #5f6368; + font-size: 12px; + margin: 0; + line-height: 1.4; + flex: 1; + max-width: 320px; +} + +.createButton { + padding: 8px 24px; + background-color: #1a73e8; + color: white; + border: none; + border-radius: 4px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.15s; + min-width: 120px; +} + +.createButton:hover:not(:disabled) { + background-color: #1765cc; +} + +.createButton:disabled { + background-color: #e8eaed; + color: #80868b; + cursor: not-allowed; +} + +/* Dark mode */ +[data-theme="dark"] .backButton { + color: #9aa0a6; +} + +[data-theme="dark"] .backButton:hover { + background-color: #3c4043; + color: #e8eaed; +} + +[data-theme="dark"] .footer { + border-top-color: #5f6368; +} + +[data-theme="dark"] .createButton { + background-color: #8ab4f8; + color: #202124; +} + +[data-theme="dark"] .createButton:hover:not(:disabled) { + background-color: #aecbfa; +} + +[data-theme="dark"] .createButton:disabled { + background-color: #3c4043; + color: #5f6368; +} + +/* Blank stream form */ +.blankStreamForm { + display: flex; + flex-direction: column; + gap: 20px; + margin-top: 8px; +} + +.blankStreamDescription { + color: #5f6368; + font-size: 14px; + line-height: 1.5; + margin: 0; +} + +[data-theme="dark"] .blankStreamDescription { + color: #9aa0a6; +} diff --git a/shared/components/NewTransmissionModal.module.css.d.ts b/shared/components/NewTransmissionModal.module.css.d.ts new file mode 100644 index 0000000..1942c82 --- /dev/null +++ b/shared/components/NewTransmissionModal.module.css.d.ts @@ -0,0 +1,2 @@ +declare const styles: { [className: string]: string } +export default styles diff --git a/shared/components/NewTransmissionModal.tsx b/shared/components/NewTransmissionModal.tsx new file mode 100644 index 0000000..370bde7 --- /dev/null +++ b/shared/components/NewTransmissionModal.tsx @@ -0,0 +1,313 @@ +import React, { useState, useEffect } from 'react' +import { + Modal, + ModalRadioGroup, + ModalSection, + ModalDestinationButton, + ModalInput, + ModalTextarea, + ModalSelect, + ModalCheckbox, + ModalLink, + ModalDateTimeGroup, + ModalButton, + ModalButtonGroup, + ModalPlatformCard +} from '.' +import { MdVideocam, MdVideoLibrary, MdAdd, MdImage, MdAutoAwesome, MdArrowBack } from 'react-icons/md' +import { FaYoutube, FaFacebook, FaLinkedin, FaTwitch, FaInstagram, FaKickstarterK } from 'react-icons/fa' +import { FaXTwitter } from 'react-icons/fa6' +import { BsInfoCircle } from 'react-icons/bs' +import styles from './NewTransmissionModal.module.css' +import type { Transmission, Destination } from '../types' + +interface Props { + open: boolean + onClose: () => void + onCreate?: (t: Transmission) => void + onUpdate?: (t: Transmission) => void + transmission?: Transmission + onlyAddDestination?: boolean + onAddDestination?: (d: DestinationData) => void +} + +type DestinationData = Destination & { + id: string + icon: React.ReactNode + badge?: React.ReactNode +} + +const NewTransmissionModal: React.FC = ({ open, onClose, onCreate, onUpdate, transmission, onlyAddDestination, onAddDestination }) => { + const [view, setView] = useState<'main' | 'add-destination'>('main') + const [source, setSource] = useState('studio') + const isEditMode = !!transmission + + const [destinations, setDestinations] = useState([ + { + id: 'yt_1', + platform: 'YouTube', + icon: , + badge: + } + ]) + const [selectedDestination, setSelectedDestination] = useState(null) + + const [blankTitle, setBlankTitle] = useState('') + const [title, setTitle] = useState('') + const [description, setDescription] = useState('') + const [privacy, setPrivacy] = useState('Pública') + const [category, setCategory] = useState('') + const [addReferral, setAddReferral] = useState(true) + const [scheduleForLater, setScheduleForLater] = useState(false) + + const [scheduledDate, setScheduledDate] = useState('') + const [scheduledHour, setScheduledHour] = useState('01') + const [scheduledMinute, setScheduledMinute] = useState('10') + + useEffect(() => { + if (transmission && open) { + setTitle(transmission.title) + if (transmission.platform === 'Genérico') { + setSelectedDestination('blank') + setBlankTitle(transmission.title) + } else { + setSelectedDestination(null) + } + } else if (!open) { + resetForm() + } + }, [transmission, open]) + + const resetForm = () => { + setView('main') + setSource('studio') + setSelectedDestination(null) + setTitle('') + setDescription('') + setPrivacy('Pública') + setCategory('') + setAddReferral(true) + setScheduleForLater(false) + setScheduledDate('') + setScheduledHour('01') + setScheduledMinute('10') + setBlankTitle('') + } + + const generateId = () => `t_${Date.now()}_${Math.floor(Math.random()*1000)}` + + const handleAddDestination = () => setView('add-destination') + const handleBackToMain = () => setView('main') + const handleSkipForNow = () => setSelectedDestination('blank') + + const handlePlatformSelect = (platform: string) => { + const platformData: Record = { + 'YouTube': { icon: , color: '#FF0000' }, + 'Facebook': { icon: , color: '#1877F2' }, + 'LinkedIn': { icon: , color: '#0A66C2' }, + 'X (Twitter)': { icon: , color: '#000000' }, + 'Twitch': { icon: , color: '#9146FF' }, + 'Instagram Live': { icon: , color: '#E4405F' }, + 'Kick': { icon: , color: '#53FC18' }, + 'Brightcove': { icon:
B
, color: '#000000' } + } + + const newDest: DestinationData = { + id: `dest_${Date.now()}`, + platform, + icon: platformData[platform]?.icon || , + badge: platform === 'YouTube' ? : undefined + } + + if (onlyAddDestination && onAddDestination) { + onAddDestination(newDest) + onClose() + return + } + + setDestinations([...destinations, newDest]) + setView('main') + } + + const handleDestinationClick = (destId: string) => { + if (selectedDestination === destId) setSelectedDestination(null) + else setSelectedDestination(destId) + } + + const handleCreate = () => { + if (!selectedDestination) { + alert('Por favor selecciona un destino de transmisión') + return + } + + if (selectedDestination === 'blank') { + const blankTransmission: Transmission = { + id: isEditMode && transmission ? transmission.id : generateId(), + title: blankTitle || 'Transmisión en vivo', + platform: 'Genérico', + scheduled: 'Próximamente', + createdAt: isEditMode && transmission?.createdAt ? transmission.createdAt : new Date().toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: 'numeric' }) + } + + if (isEditMode && onUpdate) onUpdate(blankTransmission) + else if (onCreate) onCreate(blankTransmission) + + resetForm() + onClose() + return + } + + const t: Transmission = { + id: isEditMode && transmission ? transmission.id : generateId(), + title: title || 'Nueva transmisión', + platform: destinations.find(d => d.id === selectedDestination)?.platform || 'YouTube', + scheduled: '', + createdAt: isEditMode && transmission?.createdAt ? transmission.createdAt : new Date().toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: 'numeric' }) + } + + if (isEditMode && onUpdate) onUpdate(t) + else if (onCreate) onCreate(t) + + resetForm() + onClose() + } + + const modalTitle = view === 'add-destination' ? 'Agregar destino' : (isEditMode ? 'Editar transmisión' : 'Crear transmisión en vivo') + const showBackButton = view === 'add-destination' + + return ( +
+ + {showBackButton && ( + + )} + + {view === 'main' && ( + <> +
+ + } + name="source" + value={source} + onChange={setSource} + options={[ + { value: 'studio', label: 'Estudio', icon: }, + { value: 'prerecorded', label: 'Video pregrabado', icon: } + ]} + /> + + + +
+ {destinations.map((dest) => ( +
+ handleDestinationClick(dest.id)} + title={dest.platform} + /> +
+ ))} + + } + label="" + onClick={handleAddDestination} + title="Agregar destino" + /> + {!selectedDestination && ( +
+ Omitir por ahora +
+ )} +
+
+ + {selectedDestination && selectedDestination !== 'blank' && ( + <> + + + + + + + + + + } subtext="Gana $25 en crédito por cada referencia exitosa." /> + + + + + + + + + + + + + + + {scheduleForLater && ( + <> + + } dateValue={scheduledDate} hourValue={scheduledHour} minuteValue={scheduledMinute} onDateChange={setScheduledDate} onHourChange={setScheduledHour} onMinuteChange={setScheduledMinute} timezone="GMT-7" /> + + + + + }>Subir imagen en miniatura + }>Crear con IA + + + + )} + + )} + + {selectedDestination === 'blank' && ( +
+

Empezarás una transmisión en el estudio sin configurar ningún destino. Podrás agregar destinos más tarde desde el estudio.

+ +
+ )} +
+ +
+ {selectedDestination && selectedDestination !== 'blank' && ( +

Esta transmisión no se grabará en StreamYard. Para grabar, tendrás que pasarte a un plan superior.

+ )} + +
+ + )} + + {view === 'add-destination' && ( +
+ } label="YouTube" onClick={() => handlePlatformSelect('YouTube')} /> + } label="Facebook" onClick={() => handlePlatformSelect('Facebook')} /> + } label="LinkedIn" onClick={() => handlePlatformSelect('LinkedIn')} /> + } label="X (Twitter)" onClick={() => handlePlatformSelect('X (Twitter)')} /> + } label="Twitch" onClick={() => handlePlatformSelect('Twitch')} /> + } label="Instagram Live" onClick={() => handlePlatformSelect('Instagram Live')} /> + } label="Kick" onClick={() => handlePlatformSelect('Kick')} badge="pro" /> + B
} label="Brightcove" onClick={() => handlePlatformSelect('Brightcove')} /> + RTMP
} label="Otras plataformas" onClick={() => handlePlatformSelect('RTMP')} badge="pro" /> + + )} + + + ) +} + +export { NewTransmissionModal } +export default NewTransmissionModal diff --git a/shared/components/css-modules.d.ts b/shared/components/css-modules.d.ts new file mode 100644 index 0000000..f874339 --- /dev/null +++ b/shared/components/css-modules.d.ts @@ -0,0 +1,4 @@ +declare module '*.module.css' { + const classes: { [className: string]: string } + export default classes +} diff --git a/shared/components/index.ts b/shared/components/index.ts index af9b394..131e9df 100644 --- a/shared/components/index.ts +++ b/shared/components/index.ts @@ -22,5 +22,8 @@ export { default as ModalButton } from './modal-parts/ModalButton'; export { default as ModalButtonGroup } from './modal-parts/ModalButtonGroup'; export { default as ModalPlatformCard } from './modal-parts/ModalPlatformCard'; +// Shared NewTransmissionModal (re-export) +export { NewTransmissionModal } from './NewTransmissionModal'; + diff --git a/shared/types/index.ts b/shared/types/index.ts index 428e001..846b443 100644 --- a/shared/types/index.ts +++ b/shared/types/index.ts @@ -1,3 +1,19 @@ +export interface Transmission { + id: string + title: string + platform: string + scheduled: string + createdAt?: string +} + +export type Destination = { + id: string + platform: string + label?: string + url?: string +} + +export default {} as unknown // User types export interface User { id: string; diff --git a/tsconfig.json b/tsconfig.json index 2c2be4c..ee31f93 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,10 +11,15 @@ "forceConsistentCasingInFileNames": true, "module": "ESNext", "moduleResolution": "Bundler", + "baseUrl": ".", + "paths": { + "@shared/*": ["shared/*"], + "@shared": ["shared"] + }, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, - "include": ["src", "packages/*/src", "types"] + "include": ["src", "packages/*/src", "types", "shared"] } diff --git a/types/env.d.ts b/types/env.d.ts index 34eccd5..8bafa6d 100644 --- a/types/env.d.ts +++ b/types/env.d.ts @@ -1,3 +1,16 @@ +// Type declarations for Vite-style environment variables used via import.meta.env +// Placing this under `types/` so the compiler picks it up for all packages. + +interface ImportMetaEnv { + readonly VITE_TOKEN_SERVER_URL?: string + readonly VITE_STUDIO_URL?: string + // add other VITE_ variables here as needed + [key: string]: string | undefined +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} interface ImportMetaEnv { readonly VITE_TOKEN_SERVER_URL?: string readonly VITE_LIVEKIT_URL?: string