diff --git a/packages/avanza-ui/rollup.config.js b/packages/avanza-ui/rollup.config.js new file mode 100644 index 0000000..0d8dce5 --- /dev/null +++ b/packages/avanza-ui/rollup.config.js @@ -0,0 +1,29 @@ +import peerDepsExternal from 'rollup-plugin-peer-deps-external'; +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import typescript from '@rollup/plugin-typescript'; +import postcss from 'rollup-plugin-postcss'; + +export default { + input: 'src/index.ts', + output: [ + { + file: 'dist/index.cjs.js', + format: 'cjs', + sourcemap: true, + }, + { + file: 'dist/index.esm.js', + format: 'es', + sourcemap: true, + }, + ], + plugins: [ + peerDepsExternal(), + resolve({ extensions: ['.js', '.ts', '.tsx'] }), + commonjs(), + typescript({ tsconfig: './tsconfig.json' }), + postcss({ extract: true, minimize: true }), + ], +}; + diff --git a/packages/avanza-ui/src/components/ControlBar.tsx b/packages/avanza-ui/src/components/ControlBar.tsx index dde6e8c..be3317c 100644 --- a/packages/avanza-ui/src/components/ControlBar.tsx +++ b/packages/avanza-ui/src/components/ControlBar.tsx @@ -8,7 +8,7 @@ export interface ControlBarProps { export const ControlBar: React.FC = ({ children, className }) => { return ( -
+
{children} diff --git a/packages/avanza-ui/src/components/ControlButton.module.css b/packages/avanza-ui/src/components/ControlButton.module.css new file mode 100644 index 0000000..c60e4d7 --- /dev/null +++ b/packages/avanza-ui/src/components/ControlButton.module.css @@ -0,0 +1,30 @@ +/* ...existing code... */ +.root { + display: inline-flex; + align-items: center; + gap: 8px; + border: none; + background: transparent; + cursor: pointer; + padding: 8px 12px; + border-radius: 8px; + color: #1f2937; + font-size: 14px; +} + +.row { flex-direction: row; } +.column { flex-direction: column; } + +.default { background: rgba(0,0,0,0.03); } +.studio { background: #fff; box-shadow: 0 1px 0 rgba(0,0,0,0.02); } + +.active { box-shadow: 0 0 0 2px rgba(59,130,246,0.2); } +.danger { color: #ef4444; } + +.icon { display: inline-flex; align-items: center; justify-content: center; } +.label { display: inline-block; } + +.sm { padding: 6px 8px; font-size: 12px; } +.md { padding: 8px 12px; font-size: 14px; } +.lg { padding: 10px 14px; font-size: 16px; } + diff --git a/packages/avanza-ui/src/components/ControlButton.tsx b/packages/avanza-ui/src/components/ControlButton.tsx new file mode 100644 index 0000000..278b176 --- /dev/null +++ b/packages/avanza-ui/src/components/ControlButton.tsx @@ -0,0 +1,50 @@ +// ...existing code... +import React from 'react' +import cx from 'clsx' +import styles from './ControlButton.module.css' + +export type ControlButtonProps = { + className?: string + icon?: React.ReactNode + label?: string + active?: boolean + danger?: boolean + layout?: 'row' | 'column' + variant?: 'studio' | 'default' + onClick?: () => void + hint?: string + size?: 'sm' | 'md' | 'lg' +} + +export const ControlButton: React.FC = ({ + className, + icon, + label, + active = false, + danger = false, + layout = 'row', + variant = 'default', + onClick, + hint, + size = 'md' +}) => { + return ( + + ) +} + +// default export for convenience +export default ControlButton + diff --git a/packages/avanza-ui/src/components/MicrophoneMeter.tsx b/packages/avanza-ui/src/components/MicrophoneMeter.tsx new file mode 100644 index 0000000..55d9090 --- /dev/null +++ b/packages/avanza-ui/src/components/MicrophoneMeter.tsx @@ -0,0 +1,15 @@ +// ...existing code... +import React from 'react' + +export type MicrophoneMeterProps = { level?: number } + +export const MicrophoneMeter: React.FC = ({ level = 0 }) => { + return ( +
+
+
+ ) +} + +export default MicrophoneMeter + diff --git a/packages/avanza-ui/src/components/index.ts b/packages/avanza-ui/src/components/index.ts index 800e9c6..db433be 100644 --- a/packages/avanza-ui/src/components/index.ts +++ b/packages/avanza-ui/src/components/index.ts @@ -67,10 +67,13 @@ export type { StudioHeaderProps } from './StudioHeader'; export { ControlButton } from './ControlButton'; export type { ControlButtonProps } from './ControlButton'; +export { ControlBar } from './ControlBar'; +export { ControlGroup } from './ControlGroup'; +export { IconButton } from './IconButton'; +export { MicrophoneMeter } from './MicrophoneMeter'; export { SceneCard } from './SceneCard'; export type { SceneCardProps } from './SceneCard'; export { VideoTile } from './VideoTile'; export type { VideoTileProps, ConnectionQuality } from './VideoTile'; - diff --git a/packages/avanza-ui/src/index.ts b/packages/avanza-ui/src/index.ts new file mode 100644 index 0000000..be26324 --- /dev/null +++ b/packages/avanza-ui/src/index.ts @@ -0,0 +1,3 @@ +export * from './components' +export * from './types' +export * from './utils/platform' diff --git a/packages/avanza-ui/src/styles/globals.css b/packages/avanza-ui/src/styles/globals.css new file mode 100644 index 0000000..39da01c --- /dev/null +++ b/packages/avanza-ui/src/styles/globals.css @@ -0,0 +1,14 @@ +/* Placeholder globals used by components during build */ +:root { + --au-primary: #4f46e5; + --au-primary-hover: #4338ca; + --au-gray-900: #0f172a; + --au-gray-950: #0b1220; + --au-radius-md: 8px; + --au-font-bold: 700; + --au-text-primary: #f1f5f9; + --au-text-secondary: #cbd5e1; +} + +body { font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; } + diff --git a/packages/avanza-ui/src/utils/platform.ts b/packages/avanza-ui/src/utils/platform.ts new file mode 100644 index 0000000..40a4413 --- /dev/null +++ b/packages/avanza-ui/src/utils/platform.ts @@ -0,0 +1,10 @@ +// ...existing code... +export function isMacPlatform() { + if (typeof navigator === 'undefined') return false + return /Mac|iPhone|iPad|iPod/.test(navigator.platform) +} + +export function modifierKeyLabel() { + return isMacPlatform() ? { key: 'Meta', display: '⌘' } : { key: 'Control', display: 'Ctrl' } +} + diff --git a/packages/avanza-ui/tsconfig.json b/packages/avanza-ui/tsconfig.json index 5f61fa8..9e9a307 100644 --- a/packages/avanza-ui/tsconfig.json +++ b/packages/avanza-ui/tsconfig.json @@ -8,10 +8,10 @@ /* Bundler mode */ "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + "allowImportingTsExtensions": false, "resolveJsonModule": true, "isolatedModules": true, - "noEmit": true, + "noEmit": false, "jsx": "react-jsx", /* Linting */ @@ -26,9 +26,10 @@ "forceConsistentCasingInFileNames": true, "declaration": true, "declarationMap": true, - "sourceMap": true + "sourceMap": true, + "outDir": "dist", + "declarationDir": "dist/types" }, "include": ["src"], "exclude": ["node_modules", "dist"] } - diff --git a/packages/backend-api/src/index.ts b/packages/backend-api/src/index.ts index 50103d5..698a1ee 100644 --- a/packages/backend-api/src/index.ts +++ b/packages/backend-api/src/index.ts @@ -301,30 +301,30 @@ async function createLivekitTokenFor(room: string, username: string) { const header = token.split('.')[0]; const padded = header + '='.repeat((4 - (header.length % 4)) % 4); const decoded = Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'); - let h = decoded; - try { h = JSON.parse(decoded); } - catch (e) { /* keep raw */ } + // `decoded` may be a string or JSON; use `any` to avoid strict TS errors when inspecting header fields + let h: any = decoded; + try { h = JSON.parse(decoded); } catch (e) { /* keep raw string if parsing fails */ } console.log('[LIVEKIT] token header:', h); - // If SDK produced a token with non-HS256 alg, and we have LIVEKIT_API_SECRET, re-sign payload with HS256 - try { - const secretForResign = LIVEKIT_API_SECRET || process.env.LIVEKIT_API_SECRET; - if (secretForResign && typeof h === 'object' && h.alg && String(h.alg).toUpperCase() !== 'HS256') { - try { - const parts = (token as string).split('.'); - const pad = (s: string) => s + '='.repeat((4 - (s.length % 4)) % 4); - const payloadRaw = parts[1] || ''; + // If SDK produced a token with non-HS256 alg, and we have LIVEKIT_API_SECRET, re-sign payload with HS256 + try { + const secretForResign = LIVEKIT_API_SECRET || process.env.LIVEKIT_API_SECRET; + if (secretForResign && typeof h === 'object' && (h as any).alg && String((h as any).alg).toUpperCase() !== 'HS256') { + try { + const parts = (token as string).split('.'); + const pad = (s: string) => s + '='.repeat((4 - (s.length % 4)) % 4); + const payloadRaw = parts[1] || ''; const payloadJson = JSON.parse(Buffer.from(pad(payloadRaw).replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8')); - // preserve iat/exp if present, stringify payload as-is and sign with HS256 - const jwtLib = require('jsonwebtoken'); - const newToken = jwtLib.sign(payloadJson, secretForResign, { algorithm: 'HS256' }); - console.log('[LIVEKIT] SDK token used alg=' + (h.alg || 'unknown') + ' — re-signed with HS256 fallback'); - return { token: newToken, url: LIVEKIT_WS || (LIVEKIT_HTTP ? LIVEKIT_HTTP.replace(/^https?:\/\//i, (m) => m.toLowerCase().startsWith('https') ? 'wss://' : 'ws://') : 'ws://localhost:7880') }; - } catch (e) { - console.warn('[LIVEKIT] failed to re-sign token with HS256 fallback', String(e)); - } - } - } catch (e) { /* ignore fallback errors */ } + // preserve iat/exp if present, stringify payload as-is and sign with HS256 + const jwtLib = require('jsonwebtoken'); + const newToken = jwtLib.sign(payloadJson, secretForResign, { algorithm: 'HS256' }); + console.log('[LIVEKIT] SDK token used alg=' + (h.alg || 'unknown') + ' — re-signed with HS256 fallback'); + return { token: newToken, url: LIVEKIT_WS || (LIVEKIT_HTTP ? LIVEKIT_HTTP.replace(/^https?:\/\//i, (m) => m.toLowerCase().startsWith('https') ? 'wss://' : 'ws://') : 'ws://localhost:7880') }; + } catch (e) { + console.warn('[LIVEKIT] failed to re-sign token with HS256 fallback', String(e)); + } + } + } catch (e) { /* ignore fallback errors */ } } } catch (e) { console.warn('[LIVEKIT] failed to decode token header', String(e)); } // Prefer explicit websocket URL env, else derive from LIVEKIT_URL (http(s) -> ws(s)) @@ -419,8 +419,10 @@ app.all('/api/session/validate', async (req, res) => { const token = (req.method === 'GET' ? req.query.token : req.body?.token) || req.query.token || req.body?.token; if (!token || typeof token !== 'string') return res.status(400).json({ error: 'missing_token' }); - const result = await validateTokenWithLiveKit(token); - return res.status(result.status).json({ ok: result.ok, status: result.status, body: result.body }); + const result: any = await validateTokenWithLiveKit(token); + // validateTokenWithLiveKit may return { ok: false, error: '...' } without numeric status + const statusCode: number = (result && typeof result.status === 'number') ? result.status : (result && result.ok === false ? 502 : 200); + return res.status(statusCode).json({ ok: !!result.ok, status: result.status ?? statusCode, body: result.body ?? result.error }); } catch (err) { console.error('[backend-api] validate proxy failed', err); return res.status(500).json({ error: 'validate_failed', details: String(err) }); diff --git a/packages/broadcast-panel/src/features/studio/PreJoin.module.css b/packages/broadcast-panel/src/features/studio/PreJoin.module.css index b4756f5..a8781bc 100644 --- a/packages/broadcast-panel/src/features/studio/PreJoin.module.css +++ b/packages/broadcast-panel/src/features/studio/PreJoin.module.css @@ -1,11 +1,10 @@ /* filepath: /home/xesar/Documentos/Nextream/AvanzaCast/packages/broadcast-panel/src/features/studio/PreJoin.module.css */ :root{ - --card-bg: var(--studio-bg-elevated, #ffffff); - --muted: var(--studio-text-secondary, #6b7280); - --accent: var(--studio-accent, #4f46e5); - --badge-bg: rgba(99,102,241,0.9); /* keep template purple */ - --danger: var(--studio-danger, #ef4444); - --danger-700: #b91c1c; + --card-bg: #ffffff; + --muted: #666666; + --accent: #6366f1; + --badge-bg: rgba(99,102,241,0.9); + --danger: #dc2626; } .prejoinContainer{ @@ -29,41 +28,37 @@ .header > div:first-child{ font-size: 28px; font-weight: 600; - color: var(--studio-text-primary, #1a1a1a); + color: #1a1a1a; margin-bottom: 8px; } .note{ font-size: 14px; - color: var(--studio-text-secondary, #666666); + color: var(--muted); line-height: 1.5; } -/* layout: video + mic-status side panel like template */ .contentRow{ display: flex; gap: 16px; margin-bottom: 24px; - align-items: flex-start; } -.previewColumn{ - flex: 1; -} +.previewColumn{ flex: 1 } .previewCard{ border-radius: 12px; overflow: hidden; - background: var(--studio-bg-tertiary, #0a0a1a); + background: #0a0a1a; position: relative; aspect-ratio: 16/9; } .videoEl{ - width: 100%; - height: 100%; - object-fit: cover; - background: #0b0b0b; + width:100%; + height:100%; + object-fit:cover; + background:#0b0b0b; } .badge{ @@ -76,184 +71,95 @@ font-size: 14px; font-weight: 500; border-radius: 20px; - box-shadow: none; } .micPanel{ - background-color: var(--studio-bg-secondary, #f8f9fa); + background-color: #f8f9fa; border-radius: 12px; padding: 20px; min-width: 180px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.mic-icon{ - width: 48px; - height: 48px; - background-color: #e8e8e8; - border-radius: 50%; display:flex; + flex-direction:column; align-items:center; justify-content:center; - margin-bottom:12px; } -.mic-meter{ - width: 32px; - height: 80px; - background-color: #e8e8e8; - border-radius: 16px; - margin-bottom: 12px; - position: relative; - overflow: hidden; +.micPanelInner{ + display:flex; + flex-direction:column; + align-items:center; + gap:8px; } -.mic-level{ - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 20%; - background: linear-gradient(to top, #22c55e, #86efac); - border-radius: 16px; - transition: height 0.1s ease-out; -} +.mic-icon{ width:48px; height:48px; border-radius:50%; background:#e8e8e8; display:flex; align-items:center; justify-content:center; margin-bottom:12px } -.micStatus{ - color: #22c55e; - font-weight: 500; - font-size: 14px; - text-align: center; - margin-bottom: 4px; -} +.mic-meter{ width:32px; height:80px; background:#e8e8e8; border-radius:16px; margin-bottom:12px; position:relative; overflow:hidden } +.mic-level{ position:absolute; bottom:0; left:0; right:0; height:20%; background: linear-gradient(to top, #22c55e, #86efac); border-radius:16px; transition:height 0.1s ease-out } -.mic-device{ - font-size: 11px; - color: var(--studio-text-disabled, #999999); - text-align: center; -} +.micStatus{ color: #22c55e; font-weight:500; font-size:14px; text-align:center; margin-bottom:4px } +.mic-device{ font-size:11px; color:#999999; text-align:center } -/* Controls wrapper (segmented) - copiar exactamente del template */ .controlsRow{ - display: inline-flex; - justify-content: center; - gap: 8px; - padding: 12px; - background-color: var(--card-bg); - border: 1px solid var(--studio-border, #e5e5e5); - border-radius: 12px; - margin-bottom: 24px; - margin-left: auto; - margin-right: auto; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); -} - -.controlButtonLocal{ - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - background: transparent; - border: none; - cursor: pointer; - color: var(--muted); - font-size: 13px; - transition: all 0.2s; - padding: 12px 20px; - border-radius: 8px; - position: relative; -} - -/* hover and active states like template */ -.controlButtonLocal:hover{ - color: var(--studio-text-primary, #1a1a1a); - background-color: rgba(254,226,226,1); /* template fee2e2 */ -} - -/* disabled / error */ -.controlsRow > button[data-active="false"], -.controlButtonLocal.disabled{ - color: var(--danger, #dc2626); - background-color: rgba(254,202,202,1); -} -.controlButtonLocal.disabled:hover, -.controlsRow > button[data-active="false"]:hover{ - color: var(--danger-700, #b91c1c); - background-color: rgba(252,165,165,1); -} - -.controlButtonLocal > span:first-child{ - width:24px; - height:24px; display:inline-flex; - align-items:center; justify-content:center; + gap:8px; + padding:12px; + background-color:var(--card-bg); + border:1px solid #e5e5e5; + border-radius:12px; + margin-bottom:24px; + margin-left:auto; + margin-right:auto; + box-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06); } -.controlButtonLocal > span:first-child svg{ - width:24px; - height:24px; -} +.controlButtonLocal{ display:flex; flex-direction:column; align-items:center; gap:8px; background:transparent; border:none; cursor:pointer; color:var(--muted); font-size:13px; transition:all .2s; padding:12px 20px; border-radius:8px } +.controlButtonLocal:hover{ color:#1a1a1a; background-color:#fee2e2 } -.controlButtonLocal .controlButtonLabel{ - font-size:13px; - margin:0; -} +.controlsRow > button[data-active="false"], .controlButtonLocal.disabled{ color:var(--danger); background-color:#fecaca } +.controlButtonLocal.disabled:hover, .controlsRow > button[data-active="false"]:hover{ color:#b91c1c; background-color:#fca5a5 } -/* hints (tooltip) */ -.control-hint{ - position: absolute; - bottom: 100%; - left: 50%; - transform: translateX(-50%); - background-color: var(--studio-text-primary, #1a1a1a); - color: white; - padding:6px 12px; - border-radius:6px; - font-size:12px; - white-space:nowrap; - opacity:0; - pointer-events:none; - transition: opacity 0.2s; - margin-bottom:8px; -} +.controlButtonLocal > span:first-child{ width:24px; height:24px; display:inline-flex; align-items:center; justify-content:center } +.controlButtonLocal > span:first-child svg{ width:24px; height:24px } +.control-hint{ position:absolute; bottom:100%; left:50%; transform:translateX(-50%); background-color:#1a1a1a; color:white; padding:6px 12px; border-radius:6px; font-size:12px; white-space:nowrap; opacity:0; pointer-events:none; transition:opacity .2s; margin-bottom:8px } .controlButtonLocal:hover .control-hint{ opacity:1 } -/* Form elements */ -.roomTitle{ - margin-top: 8px; - margin-bottom: 8px; - font-weight:500; - color: var(--studio-text-primary, #1a1a1a); -} +.roomTitle{ margin-top:8px; margin-bottom:8px; font-weight:500; color:#1a1a1a } +.input{ width:100%; padding:12px 16px; border-radius:8px; border:1px solid #d1d5db; font-size:14px; margin-bottom:16px } -.input{ - width:100%; - padding:12px 16px; +.shortcutsLegend{ text-align:center; margin-top:12px; color:var(--muted) } +.kbd{ background-color:#374151; padding:2px 6px; border-radius:3px; font-family:monospace; font-size:11px; color:#fff } + +.checkboxRow{ margin-top:12px; margin-bottom:12px; display:flex; align-items:center; gap:8px } + +.actions{ display:flex; gap:12px; margin-top:16px } +.cancelBtn{ background:transparent; border:1px solid #e5e7eb; padding:10px 14px; border-radius:8px; cursor:pointer } +.primaryBtn{ background:#2563eb; color:#fff; border:none; padding:12px 18px; border-radius:8px; cursor:pointer } +.primaryBtn:disabled{ opacity:0.7; cursor:not-allowed } + +/* small error box */ +.error{ + background:#fff5f5; + border:1px solid #fecaca; + color:#b91c1c; + padding:10px 12px; border-radius:8px; - border:1px solid var(--studio-border, #d1d5db); - font-size:14px; - margin-bottom:16px; + margin-bottom:12px; + font-size:13px; } -.input:focus{ outline:none; box-shadow: 0 0 0 3px rgba(59,130,246,0.1); border-color:var(--studio-accent, #3b82f6) } - -.actions{ display:flex; gap:12px; margin-top:8px } - -.cancelBtn{ background:transparent; border: none; padding:10px 14px; border-radius:8px } -.primaryBtn{ background:var(--studio-accent, #2563eb); color:#fff; padding:12px 18px; border-radius:8px; border:none } - -.shortcutsLegend{ text-align:center; color: var(--studio-text-muted, #9ca3af); margin-top: 8px } -.kbd{ background-color: #374151; padding: 2px 6px; border-radius:3px; font-family: monospace; font-size:11px; margin: 0 2px } - -@media (max-width: 768px){ - .contentRow{ flex-direction: column; } - .controlsRow{ display:flex; flex-direction: column; gap:8px; padding:0; background:transparent; box-shadow:none; border:none } - .controlButtonLocal{ width:100%; padding:12px 16px } - .previewCard{ aspect-ratio: 16/9; } - .micPanel{ min-width: auto; width: 100%; } +/* Side column (form & actions) */ +.sideColumn{ + width: 320px; + display: flex; + flex-direction: column; +} + +/* ensure controls row centers on small screens */ +@media (max-width:800px){ + .contentRow{ flex-direction:column } + .micPanel{ min-width:unset; width:100% } + .sideColumn{ width:100% } + .controlsRow{ width:100%; justify-content:space-around } } diff --git a/packages/broadcast-panel/src/features/studio/PreJoin.tsx b/packages/broadcast-panel/src/features/studio/PreJoin.tsx index 47ee231..df9f1a6 100644 --- a/packages/broadcast-panel/src/features/studio/PreJoin.tsx +++ b/packages/broadcast-panel/src/features/studio/PreJoin.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react' import styles from './PreJoin.module.css' -import { ControlBar, ControlButton, MicrophoneMeter, modifierKeyLabel, isMacPlatform } from 'avanza-ui' +import { ControlButton, MicrophoneMeter, modifierKeyLabel, isMacPlatform } from 'avanza-ui' import { FiMic, FiVideo, FiSettings } from 'react-icons/fi' type Props = { @@ -11,7 +11,7 @@ type Props = { token?: string } -export default function PreJoin({ roomName, onProceed, onCancel }: Props) { +export default function PreJoin({ roomName: _roomName, onProceed, onCancel }: Props) { const videoRef = useRef(null) const [name, setName] = useState(() => { try { return localStorage.getItem('avanzacast_user') || '' } catch { return '' } @@ -42,7 +42,10 @@ export default function PreJoin({ roomName, onProceed, onCancel }: Props) { if (!navigator?.mediaDevices?.getUserMedia) return localStream = await navigator.mediaDevices.getUserMedia({ audio: micEnabled, video: camEnabled }) if (!mounted) { - try { localStream.getTracks().forEach(t => t.stop()) } catch (e) {} + try { + const tracks = localStream?.getTracks?.() + if (tracks && tracks.length) tracks.forEach(t => t.stop()) + } catch (e) {} return } setPreviewStream(localStream) @@ -87,7 +90,10 @@ export default function PreJoin({ roomName, onProceed, onCancel }: Props) { return () => { mounted = false if (localStream) { - try { localStream.getTracks().forEach(t => t.stop()) } catch (e) {} + try { + const tracks = localStream?.getTracks?.() + if (tracks && tracks.length) tracks.forEach(t => t.stop()) + } catch (e) {} } // clear previewStream state setPreviewStream(null) diff --git a/packages/studio-panel/src/stories/PreJoin.stories.tsx b/packages/studio-panel/src/stories/PreJoin.stories.tsx new file mode 100644 index 0000000..3bf7993 --- /dev/null +++ b/packages/studio-panel/src/stories/PreJoin.stories.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import PreJoin from '../../../../packages/broadcast-panel/src/features/studio/PreJoin'; + +const meta: Meta = { + title: 'Broadcast/PreJoin', + component: PreJoin, +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + roomName: 'Sala de prueba', + onProceed: () => alert('Proceed clicked'), + onCancel: () => alert('Cancel clicked'), + }, +}; + diff --git a/scripts/tsc-runner.js b/scripts/tsc-runner.js new file mode 100644 index 0000000..3909f02 --- /dev/null +++ b/scripts/tsc-runner.js @@ -0,0 +1,43 @@ +const ts = require('typescript'); +const path = require('path'); +const fs = require('fs'); + +function report(d) { + const file = d.file ? path.relative(process.cwd(), d.file.fileName) : null; + const msg = ts.flattenDiagnosticMessageText(d.messageText, '\n'); + const line = d.file && d.start !== undefined ? d.file.getLineAndCharacterOfPosition(d.start).line + 1 : null; + const col = d.file && d.start !== undefined ? d.file.getLineAndCharacterOfPosition(d.start).character + 1 : null; + console.log([file ? file : '', line ? ':' + line : '', col ? ':' + col : '', '-', msg].join('')); +} + +async function run(tsconfigPath) { + const abs = path.resolve(tsconfigPath); + if (!fs.existsSync(abs)) { + console.error('tsconfig not found:', abs); + process.exit(2); + } + const configFile = ts.readConfigFile(abs, ts.sys.readFile); + if (configFile.error) { + report(configFile.error); + process.exit(1); + } + const parseConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(abs)); + if (parseConfig.errors && parseConfig.errors.length) { + parseConfig.errors.forEach(report); + process.exit(1); + } + const program = ts.createProgram(parseConfig.fileNames, parseConfig.options); + const emitResult = program.emit(); + const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics); + if (allDiagnostics.length === 0) { + console.log('TSC: no errors'); + process.exit(0); + } + allDiagnostics.forEach(report); + console.log('TSC: errors=', allDiagnostics.length); + process.exit(1); +} + +const arg = process.argv[2] || 'packages/backend-api/tsconfig.json'; +run(arg).catch(e => { console.error('runner failed', e); process.exit(3); }); +