feat: add PreJoin UI components, styles, platform utils, and build config; update token validation and refactor PreJoin logic
This commit is contained in:
parent
1e67f1ca36
commit
f8516a5330
29
packages/avanza-ui/rollup.config.js
Normal file
29
packages/avanza-ui/rollup.config.js
Normal file
@ -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 }),
|
||||
],
|
||||
};
|
||||
|
||||
@ -8,7 +8,7 @@ export interface ControlBarProps {
|
||||
|
||||
export const ControlBar: React.FC<ControlBarProps> = ({ children, className }) => {
|
||||
return (
|
||||
<div style={{display:'flex',justifyContent:'center',padding:'8px'}} className={className}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }} className={className}>
|
||||
<ControlGroup className="controls-inner">
|
||||
{children}
|
||||
</ControlGroup>
|
||||
|
||||
30
packages/avanza-ui/src/components/ControlButton.module.css
Normal file
30
packages/avanza-ui/src/components/ControlButton.module.css
Normal file
@ -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; }
|
||||
|
||||
50
packages/avanza-ui/src/components/ControlButton.tsx
Normal file
50
packages/avanza-ui/src/components/ControlButton.tsx
Normal file
@ -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<ControlButtonProps> = ({
|
||||
className,
|
||||
icon,
|
||||
label,
|
||||
active = false,
|
||||
danger = false,
|
||||
layout = 'row',
|
||||
variant = 'default',
|
||||
onClick,
|
||||
hint,
|
||||
size = 'md'
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cx(styles.root, className, styles[layout], styles[variant], styles[size], {
|
||||
[styles.active]: active,
|
||||
[styles.danger]: danger
|
||||
})}
|
||||
aria-pressed={active}
|
||||
title={hint}
|
||||
>
|
||||
<span className={styles.icon}>{icon}</span>
|
||||
{label && <span className={styles.label}>{label}</span>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// default export for convenience
|
||||
export default ControlButton
|
||||
|
||||
15
packages/avanza-ui/src/components/MicrophoneMeter.tsx
Normal file
15
packages/avanza-ui/src/components/MicrophoneMeter.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
// ...existing code...
|
||||
import React from 'react'
|
||||
|
||||
export type MicrophoneMeterProps = { level?: number }
|
||||
|
||||
export const MicrophoneMeter: React.FC<MicrophoneMeterProps> = ({ level = 0 }) => {
|
||||
return (
|
||||
<div style={{ width: 48, height: 24, background: '#eee', borderRadius: 6 }} aria-hidden>
|
||||
<div style={{ width: `${Math.min(100, Math.max(0, level * 100))}%`, height: '100%', background: '#10b981', borderRadius: 6 }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MicrophoneMeter
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
3
packages/avanza-ui/src/index.ts
Normal file
3
packages/avanza-ui/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './components'
|
||||
export * from './types'
|
||||
export * from './utils/platform'
|
||||
14
packages/avanza-ui/src/styles/globals.css
Normal file
14
packages/avanza-ui/src/styles/globals.css
Normal file
@ -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; }
|
||||
|
||||
10
packages/avanza-ui/src/utils/platform.ts
Normal file
10
packages/avanza-ui/src/utils/platform.ts
Normal file
@ -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' }
|
||||
}
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
|
||||
@ -301,15 +301,15 @@ 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') {
|
||||
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);
|
||||
@ -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) });
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
@ -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<HTMLVideoElement | null>(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)
|
||||
|
||||
20
packages/studio-panel/src/stories/PreJoin.stories.tsx
Normal file
20
packages/studio-panel/src/stories/PreJoin.stories.tsx
Normal file
@ -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<typeof PreJoin> = {
|
||||
title: 'Broadcast/PreJoin',
|
||||
component: PreJoin,
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof PreJoin>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
roomName: 'Sala de prueba',
|
||||
onProceed: () => alert('Proceed clicked'),
|
||||
onCancel: () => alert('Cancel clicked'),
|
||||
},
|
||||
};
|
||||
|
||||
43
scripts/tsc-runner.js
Normal file
43
scripts/tsc-runner.js
Normal file
@ -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); });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user