mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
feat: Enhance Table component with new props and styles for better customization and usability
feat: Implement StreamCast package with detailed module structure and initialization refactor: Simplify player module by re-exporting single-function modules for better maintainability feat: Add detailed documentation for player controls and overlay components feat: Improve scene management with new component structures and enhanced functionality feat: Revamp scheduling facade with modular rendering and action handling
This commit is contained in:
@@ -1,21 +1,59 @@
|
||||
import React from 'react'
|
||||
import styles from '../../styles/components/Table.module.scss'
|
||||
|
||||
export type TableSize = 'small' | 'medium' | 'large'
|
||||
export type TablePadding = 'checkbox' | 'none' | 'normal'
|
||||
export type TableAlign = 'left' | 'center' | 'right' | 'justify'
|
||||
|
||||
export interface TableProps extends React.TableHTMLAttributes<HTMLTableElement> {
|
||||
children?: React.ReactNode
|
||||
/** Size variant */
|
||||
size?: TableSize
|
||||
/** Enable sticky header */
|
||||
stickyHeader?: boolean
|
||||
/** Enable striped rows */
|
||||
striped?: boolean
|
||||
/** Enable bordered variant */
|
||||
bordered?: boolean
|
||||
/** Compact mode */
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
export const Table: React.FC<TableProps> = ({ children, className = '', ...props }) => (
|
||||
<table className={`table ${className}`} {...props}>
|
||||
{children}
|
||||
</table>
|
||||
)
|
||||
export const Table: React.FC<TableProps> = ({
|
||||
children,
|
||||
className = '',
|
||||
size = 'medium',
|
||||
stickyHeader = false,
|
||||
striped = false,
|
||||
bordered = false,
|
||||
compact = false,
|
||||
...props
|
||||
}) => {
|
||||
const classes = [
|
||||
styles.table,
|
||||
styles[size],
|
||||
stickyHeader && styles.stickyHeader,
|
||||
striped && styles.striped,
|
||||
bordered && styles.bordered,
|
||||
compact && styles.compact,
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
return (
|
||||
<table className={classes} {...props}>
|
||||
{children}
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
export interface TableHeadProps extends React.HTMLAttributes<HTMLTableSectionElement> {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const TableHead: React.FC<TableHeadProps> = ({ children, className = '', ...props }) => (
|
||||
<thead className={`table-head ${className}`} {...props}>
|
||||
<thead className={`${styles.head} ${className}`} {...props}>
|
||||
{children}
|
||||
</thead>
|
||||
)
|
||||
@@ -25,7 +63,7 @@ export interface TableBodyProps extends React.HTMLAttributes<HTMLTableSectionEle
|
||||
}
|
||||
|
||||
export const TableBody: React.FC<TableBodyProps> = ({ children, className = '', ...props }) => (
|
||||
<tbody className={`table-body ${className}`} {...props}>
|
||||
<tbody className={`${styles.body} ${className}`} {...props}>
|
||||
{children}
|
||||
</tbody>
|
||||
)
|
||||
@@ -35,36 +73,88 @@ export interface TableFooterProps extends React.HTMLAttributes<HTMLTableSectionE
|
||||
}
|
||||
|
||||
export const TableFooter: React.FC<TableFooterProps> = ({ children, className = '', ...props }) => (
|
||||
<tfoot className={`table-footer ${className}`} {...props}>
|
||||
<tfoot className={`${styles.footer} ${className}`} {...props}>
|
||||
{children}
|
||||
</tfoot>
|
||||
)
|
||||
|
||||
export interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
||||
children?: React.ReactNode
|
||||
/** Enable hover effect */
|
||||
hover?: boolean
|
||||
/** Selected state */
|
||||
selected?: boolean
|
||||
/** Make row clickable */
|
||||
onClick?: React.MouseEventHandler<HTMLTableRowElement>
|
||||
}
|
||||
|
||||
export const TableRow: React.FC<TableRowProps> = ({ children, hover, selected, className = '', ...props }) => (
|
||||
<tr
|
||||
className={`table-row ${hover ? 'table-row--hover' : ''} ${selected ? 'table-row--selected' : ''} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
)
|
||||
export const TableRow: React.FC<TableRowProps> = ({
|
||||
children,
|
||||
hover,
|
||||
selected,
|
||||
onClick,
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const classes = [
|
||||
styles.row,
|
||||
hover && styles.hover,
|
||||
selected && styles.selected,
|
||||
onClick && styles.clickable,
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
return (
|
||||
<tr className={classes} onClick={onClick} {...props}>
|
||||
{children}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
export interface TableCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
|
||||
children?: React.ReactNode
|
||||
/** Render as th element */
|
||||
header?: boolean
|
||||
align?: 'left' | 'center' | 'right'
|
||||
/** Text alignment */
|
||||
align?: TableAlign
|
||||
/** Cell padding variant */
|
||||
padding?: TablePadding
|
||||
/** Scope for header cells */
|
||||
scope?: 'col' | 'row' | 'colgroup' | 'rowgroup'
|
||||
/** Enable sorting on this cell */
|
||||
sortDirection?: 'asc' | 'desc' | false
|
||||
}
|
||||
|
||||
export const TableCell: React.FC<TableCellProps> = ({ children, header, align, className = '', ...props }) => {
|
||||
export const TableCell: React.FC<TableCellProps> = ({
|
||||
children,
|
||||
header,
|
||||
align = 'left',
|
||||
padding = 'normal',
|
||||
scope,
|
||||
sortDirection,
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const Tag = header ? 'th' : 'td'
|
||||
const classes = [
|
||||
styles.cell,
|
||||
align && styles[`align${align.charAt(0).toUpperCase() + align.slice(1)}`],
|
||||
padding === 'checkbox' && styles.paddingCheckbox,
|
||||
padding === 'none' && styles.paddingNone,
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
return (
|
||||
<Tag className={`table-cell ${align ? `table-cell--${align}` : ''} ${className}`} {...props}>
|
||||
<Tag
|
||||
className={classes}
|
||||
scope={header ? scope : undefined}
|
||||
aria-sort={sortDirection ? (sortDirection === 'asc' ? 'ascending' : 'descending') : undefined}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
)
|
||||
@@ -72,10 +162,22 @@ export const TableCell: React.FC<TableCellProps> = ({ children, header, align, c
|
||||
|
||||
export interface TableContainerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode
|
||||
/** Max height for scrollable container */
|
||||
maxHeight?: number | string
|
||||
}
|
||||
|
||||
export const TableContainer: React.FC<TableContainerProps> = ({ children, className = '', ...props }) => (
|
||||
<div className={`table-container ${className}`} {...props}>
|
||||
export const TableContainer: React.FC<TableContainerProps> = ({
|
||||
children,
|
||||
maxHeight,
|
||||
className = '',
|
||||
style,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={`${styles.tableContainer} ${className}`}
|
||||
style={{ ...style, maxHeight }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -84,19 +186,34 @@ export interface TablePaginationLabelDisplayedRowsArgs {
|
||||
from: number
|
||||
to: number
|
||||
count: number
|
||||
page: number
|
||||
}
|
||||
|
||||
export interface TablePaginationProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Total number of rows */
|
||||
count?: number
|
||||
/** Current page (0-indexed) */
|
||||
page?: number
|
||||
/** Number of rows per page */
|
||||
rowsPerPage?: number
|
||||
/** Options for rows per page dropdown */
|
||||
rowsPerPageOptions?: number[]
|
||||
onPageChange?: (event: React.MouseEvent<HTMLButtonElement>, page: number) => void
|
||||
/** Callback for page change */
|
||||
onPageChange?: (event: React.MouseEvent<HTMLButtonElement> | null, page: number) => void
|
||||
/** Callback for rows per page change */
|
||||
onRowsPerPageChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
||||
labelRowsPerPage?: string
|
||||
labelDisplayedRows?: (args: TablePaginationLabelDisplayedRowsArgs) => string
|
||||
/** Label for rows per page dropdown */
|
||||
labelRowsPerPage?: React.ReactNode
|
||||
/** Custom label for displayed rows */
|
||||
labelDisplayedRows?: (args: TablePaginationLabelDisplayedRowsArgs) => React.ReactNode
|
||||
/** Show first page button */
|
||||
showFirstButton?: boolean
|
||||
/** Show last page button */
|
||||
showLastButton?: boolean
|
||||
/** Disable the back button beyond first page */
|
||||
backIconButtonDisabled?: boolean
|
||||
/** Disable the next button beyond last page */
|
||||
nextIconButtonDisabled?: boolean
|
||||
}
|
||||
|
||||
export const TablePagination: React.FC<TablePaginationProps> = ({
|
||||
@@ -107,23 +224,46 @@ export const TablePagination: React.FC<TablePaginationProps> = ({
|
||||
onPageChange,
|
||||
onRowsPerPageChange,
|
||||
labelRowsPerPage = 'Rows per page:',
|
||||
labelDisplayedRows = ({ from, to, count }) => `${from}–${to} of ${count}`,
|
||||
labelDisplayedRows = ({ from, to, count }) =>
|
||||
`${from}–${to} of ${count !== -1 ? count : `more than ${to}`}`,
|
||||
showFirstButton = false,
|
||||
showLastButton = false,
|
||||
backIconButtonDisabled,
|
||||
nextIconButtonDisabled,
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const from = count === 0 ? 0 : page * rowsPerPage + 1
|
||||
const to = Math.min((page + 1) * rowsPerPage, count)
|
||||
const totalPages = Math.ceil(count / rowsPerPage)
|
||||
const to = count !== -1 ? Math.min((page + 1) * rowsPerPage, count) : (page + 1) * rowsPerPage
|
||||
const totalPages = count !== -1 ? Math.ceil(count / rowsPerPage) : -1
|
||||
|
||||
const handleFirstPage = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onPageChange?.(e, 0)
|
||||
}
|
||||
|
||||
const handlePrevPage = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onPageChange?.(e, page - 1)
|
||||
}
|
||||
|
||||
const handleNextPage = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onPageChange?.(e, page + 1)
|
||||
}
|
||||
|
||||
const handleLastPage = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onPageChange?.(e, Math.max(0, totalPages - 1))
|
||||
}
|
||||
|
||||
const isBackDisabled = backIconButtonDisabled ?? page === 0
|
||||
const isNextDisabled = nextIconButtonDisabled ?? (totalPages !== -1 && page >= totalPages - 1)
|
||||
|
||||
return (
|
||||
<div className={`table-pagination ${className}`} {...props}>
|
||||
<span className="table-pagination-label">{labelRowsPerPage}</span>
|
||||
<div className={`${styles.pagination} ${className}`} {...props}>
|
||||
<span className={styles.paginationLabel}>{labelRowsPerPage}</span>
|
||||
<select
|
||||
className="table-pagination-select"
|
||||
className={styles.paginationSelect}
|
||||
value={rowsPerPage}
|
||||
onChange={(e) => onRowsPerPageChange?.(e)}
|
||||
aria-label="Rows per page"
|
||||
>
|
||||
{rowsPerPageOptions.map((opt) => (
|
||||
<option key={opt} value={opt}>
|
||||
@@ -131,38 +271,58 @@ export const TablePagination: React.FC<TablePaginationProps> = ({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="table-pagination-displayed">{labelDisplayedRows({ from, to, count })}</span>
|
||||
<div className="table-pagination-actions">
|
||||
<span className={styles.paginationDisplayed}>
|
||||
{labelDisplayedRows({ from, to, count, page })}
|
||||
</span>
|
||||
<div className={styles.paginationActions}>
|
||||
{showFirstButton && (
|
||||
<button
|
||||
className="table-pagination-btn"
|
||||
disabled={page === 0}
|
||||
onClick={(e) => onPageChange?.(e, 0)}
|
||||
className={styles.paginationBtn}
|
||||
disabled={isBackDisabled}
|
||||
onClick={handleFirstPage}
|
||||
aria-label="Go to first page"
|
||||
type="button"
|
||||
>
|
||||
⟨⟨
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="11 17 6 12 11 7" />
|
||||
<polyline points="18 17 13 12 18 7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="table-pagination-btn"
|
||||
disabled={page === 0}
|
||||
onClick={(e) => onPageChange?.(e, page - 1)}
|
||||
className={styles.paginationBtn}
|
||||
disabled={isBackDisabled}
|
||||
onClick={handlePrevPage}
|
||||
aria-label="Go to previous page"
|
||||
type="button"
|
||||
>
|
||||
‹
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="table-pagination-btn"
|
||||
disabled={page >= totalPages - 1}
|
||||
onClick={(e) => onPageChange?.(e, page + 1)}
|
||||
className={styles.paginationBtn}
|
||||
disabled={isNextDisabled}
|
||||
onClick={handleNextPage}
|
||||
aria-label="Go to next page"
|
||||
type="button"
|
||||
>
|
||||
›
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
</button>
|
||||
{showLastButton && (
|
||||
<button
|
||||
className="table-pagination-btn"
|
||||
disabled={page >= totalPages - 1}
|
||||
onClick={(e) => onPageChange?.(e, totalPages - 1)}
|
||||
className={styles.paginationBtn}
|
||||
disabled={isNextDisabled}
|
||||
onClick={handleLastPage}
|
||||
aria-label="Go to last page"
|
||||
type="button"
|
||||
>
|
||||
⟩⟩
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="13 17 18 12 13 7" />
|
||||
<polyline points="6 17 11 12 6 7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -171,12 +331,18 @@ export const TablePagination: React.FC<TablePaginationProps> = ({
|
||||
}
|
||||
|
||||
export interface TableSortLabelProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
/** Whether this column is currently sorted */
|
||||
active?: boolean
|
||||
/** Sort direction */
|
||||
direction?: 'asc' | 'desc'
|
||||
/** Click handler */
|
||||
onClick?: (event: React.MouseEvent<HTMLSpanElement> | React.KeyboardEvent<HTMLSpanElement>) => void
|
||||
/** Label content */
|
||||
children?: React.ReactNode
|
||||
/** Hide sort icon when inactive */
|
||||
hideSortIcon?: boolean
|
||||
IconComponent?: React.ComponentType<{ className?: string }>
|
||||
/** Custom icon component */
|
||||
IconComponent?: React.ComponentType<{ className?: string; direction?: 'asc' | 'desc' }>
|
||||
}
|
||||
|
||||
export const TableSortLabel: React.FC<TableSortLabelProps> = ({
|
||||
@@ -189,23 +355,37 @@ export const TableSortLabel: React.FC<TableSortLabelProps> = ({
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const classes = [styles.sortLabel, active && styles.active, className].filter(Boolean).join(' ')
|
||||
|
||||
const sortIcon = IconComponent ? (
|
||||
<IconComponent className="table-sort-icon" />
|
||||
<IconComponent className={`${styles.sortIcon} ${styles[direction]}`} direction={direction} />
|
||||
) : (
|
||||
<span className="table-sort-icon">{direction === 'asc' ? '↑' : '↓'}</span>
|
||||
<span className={`${styles.sortIcon} ${styles[direction]}`}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 5v14M5 12l7-7 7 7" />
|
||||
</svg>
|
||||
</span>
|
||||
)
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onClick?.(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`table-sort-label ${active ? 'table-sort-label--active' : ''} ${className}`}
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onClick?.(e)}
|
||||
aria-pressed={active}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{!hideSortIcon && (active || !hideSortIcon) && sortIcon}
|
||||
{(!hideSortIcon || active) && sortIcon}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
347
fakemui/styles/components/Table.module.scss
Normal file
347
fakemui/styles/components/Table.module.scss
Normal file
@@ -0,0 +1,347 @@
|
||||
/* Table component styles */
|
||||
|
||||
/* ===== Table Container ===== */
|
||||
.tableContainer {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
border-radius: var(--shape-border-radius, 4px);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--color-background-default, #f5f5f5);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--color-text-disabled, #bdbdbd);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Base Table ===== */
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
font-family: var(--font-family-body, 'IBM Plex Sans', sans-serif);
|
||||
font-size: var(--font-size-body1, 14px);
|
||||
background-color: var(--color-background-paper, #fff);
|
||||
|
||||
/* Size variants */
|
||||
&.small {
|
||||
.cell {
|
||||
padding: 6px 12px;
|
||||
font-size: var(--font-size-body2, 13px);
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
.cell {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
.cell {
|
||||
padding: 16px 20px;
|
||||
font-size: var(--font-size-body1, 14px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Sticky header */
|
||||
&.stickyHeader {
|
||||
.head {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background-color: var(--color-background-paper, #fff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Table Head ===== */
|
||||
.head {
|
||||
background-color: var(--color-background-default, #fafafa);
|
||||
|
||||
.cell {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary, rgba(0, 0, 0, 0.87));
|
||||
border-bottom: 2px solid var(--color-divider, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Table Body ===== */
|
||||
.body {
|
||||
.row:last-child .cell {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Table Footer ===== */
|
||||
.footer {
|
||||
background-color: var(--color-background-default, #fafafa);
|
||||
|
||||
.cell {
|
||||
font-weight: 500;
|
||||
border-top: 2px solid var(--color-divider, rgba(0, 0, 0, 0.12));
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Table Row ===== */
|
||||
.row {
|
||||
transition: background-color 150ms ease-in-out;
|
||||
|
||||
&.hover:hover {
|
||||
background-color: var(--color-action-hover, rgba(0, 0, 0, 0.04));
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--color-primary-light, rgba(25, 118, 210, 0.08));
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-light, rgba(25, 118, 210, 0.12));
|
||||
}
|
||||
}
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
/* Striped rows */
|
||||
.striped .body .row:nth-child(odd) {
|
||||
background-color: var(--color-action-hover, rgba(0, 0, 0, 0.02));
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-action-hover, rgba(0, 0, 0, 0.06));
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Table Cell ===== */
|
||||
.cell {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--color-divider, rgba(0, 0, 0, 0.12));
|
||||
vertical-align: middle;
|
||||
color: var(--color-text-primary, rgba(0, 0, 0, 0.87));
|
||||
|
||||
/* Alignment */
|
||||
&.alignLeft {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.alignCenter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.alignRight {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.alignJustify {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
/* Padding variants */
|
||||
&.paddingCheckbox {
|
||||
width: 48px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
&.paddingNone {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Sort Label ===== */
|
||||
.sortLabel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
transition: color 150ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-primary, rgba(0, 0, 0, 0.87));
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
color: var(--color-primary-main, #1976d2);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-text-primary, rgba(0, 0, 0, 0.87));
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
color: var(--color-text-secondary, rgba(0, 0, 0, 0.6));
|
||||
|
||||
.sortIcon {
|
||||
opacity: 0;
|
||||
transition: opacity 150ms ease-in-out;
|
||||
}
|
||||
|
||||
&:hover .sortIcon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sortIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
opacity: 1;
|
||||
transition: transform 200ms ease-in-out, opacity 150ms ease-in-out;
|
||||
|
||||
&.asc {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
&.desc {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Pagination ===== */
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 12px 16px;
|
||||
gap: 16px;
|
||||
font-size: var(--font-size-body2, 13px);
|
||||
color: var(--color-text-secondary, rgba(0, 0, 0, 0.6));
|
||||
border-top: 1px solid var(--color-divider, rgba(0, 0, 0, 0.12));
|
||||
|
||||
&.toolbar {
|
||||
min-height: 52px;
|
||||
padding: 8px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.paginationLabel {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.paginationSelect {
|
||||
appearance: none;
|
||||
padding: 6px 24px 6px 8px;
|
||||
border: 1px solid var(--color-divider, rgba(0, 0, 0, 0.23));
|
||||
border-radius: 4px;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
background-color: var(--color-background-paper, #fff);
|
||||
cursor: pointer;
|
||||
transition: border-color 150ms ease-in-out;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 4px center;
|
||||
background-size: 16px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-text-primary, rgba(0, 0, 0, 0.87));
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary-main, #1976d2);
|
||||
}
|
||||
}
|
||||
|
||||
.paginationSpacer {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.paginationDisplayed {
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.paginationActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.paginationBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
color: var(--color-text-secondary, rgba(0, 0, 0, 0.6));
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms ease-in-out, color 150ms ease-in-out;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--color-action-hover, rgba(0, 0, 0, 0.04));
|
||||
color: var(--color-text-primary, rgba(0, 0, 0, 0.87));
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Bordered variant ===== */
|
||||
.bordered {
|
||||
border: 1px solid var(--color-divider, rgba(0, 0, 0, 0.12));
|
||||
border-radius: var(--shape-border-radius, 4px);
|
||||
|
||||
.cell {
|
||||
border: 1px solid var(--color-divider, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Compact variant ===== */
|
||||
.compact {
|
||||
.cell {
|
||||
padding: 4px 8px;
|
||||
font-size: var(--font-size-caption, 12px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Dark mode support ===== */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.table {
|
||||
background-color: var(--color-background-paper-dark, #1e1e1e);
|
||||
}
|
||||
|
||||
.head {
|
||||
background-color: var(--color-background-default-dark, #121212);
|
||||
}
|
||||
|
||||
.cell {
|
||||
color: var(--color-text-primary-dark, #fff);
|
||||
border-color: var(--color-divider-dark, rgba(255, 255, 255, 0.12));
|
||||
}
|
||||
|
||||
.row.hover:hover {
|
||||
background-color: var(--color-action-hover-dark, rgba(255, 255, 255, 0.08));
|
||||
}
|
||||
|
||||
.paginationSelect {
|
||||
background-color: var(--color-background-paper-dark, #1e1e1e);
|
||||
border-color: var(--color-divider-dark, rgba(255, 255, 255, 0.23));
|
||||
color: var(--color-text-primary-dark, #fff);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,45 @@
|
||||
-- Stream Cast initialization
|
||||
--- Stream Cast package entry point
|
||||
--- Provides streaming and broadcasting components
|
||||
---@class StreamCast
|
||||
---@field name string Package name
|
||||
---@field version string Package version
|
||||
---@field player StreamPlayerModule Player module
|
||||
---@field scenes StreamScenesModule Scenes module
|
||||
---@field schedule StreamScheduleModule Schedule module
|
||||
---@field init fun(): { name: string, version: string, loaded: boolean }
|
||||
|
||||
---@class StreamPlayerModule
|
||||
---@field PLAYING PlayerState
|
||||
---@field PAUSED PlayerState
|
||||
---@field BUFFERING PlayerState
|
||||
---@field OFFLINE PlayerState
|
||||
---@field render fun(stream: Stream): VideoPlayerComponent
|
||||
---@field render_controls fun(state: PlayerState): PlayerControlsComponent
|
||||
---@field render_status fun(state: PlayerState, viewers: number): StatusBarComponent
|
||||
|
||||
---@class StreamScenesModule
|
||||
---@field render_scene fun(scene: Scene): ScenePreviewComponent
|
||||
---@field render_list fun(scenes: Scene[]): SceneListComponent
|
||||
---@field switch fun(scene_id: string): SwitchSceneAction
|
||||
---@field create fun(name: string, sources?: table[]): CreateSceneAction
|
||||
|
||||
---@class StreamScheduleModule
|
||||
---@field render_item fun(stream: ScheduledStream): ScheduleItemComponent
|
||||
---@field render_list fun(streams: ScheduledStream[]): ScheduleListComponent
|
||||
---@field create fun(data: { title: string, start_time: string, duration?: number }): ScheduleStreamAction
|
||||
---@field cancel fun(stream_id: string): CancelStreamAction
|
||||
|
||||
local M = {}
|
||||
|
||||
M.name = "stream_cast"
|
||||
M.version = "1.0.0"
|
||||
|
||||
M.player = require("player")
|
||||
M.scenes = require("scenes")
|
||||
M.schedule = require("schedule")
|
||||
|
||||
--- Initialize the stream cast module
|
||||
---@return { name: string, version: string, loaded: boolean }
|
||||
function M.init()
|
||||
return {
|
||||
name = M.name,
|
||||
|
||||
@@ -1,48 +1,24 @@
|
||||
-- Stream player controls
|
||||
--- Stream player facade
|
||||
--- Re-exports single-function modules for backward compatibility
|
||||
|
||||
---@type PlayerState
|
||||
local PLAYING = "playing"
|
||||
---@type PlayerState
|
||||
local PAUSED = "paused"
|
||||
---@type PlayerState
|
||||
local BUFFERING = "buffering"
|
||||
---@type PlayerState
|
||||
local OFFLINE = "offline"
|
||||
|
||||
local M = {}
|
||||
|
||||
M.PLAYING = "playing"
|
||||
M.PAUSED = "paused"
|
||||
M.BUFFERING = "buffering"
|
||||
M.OFFLINE = "offline"
|
||||
M.PLAYING = PLAYING
|
||||
M.PAUSED = PAUSED
|
||||
M.BUFFERING = BUFFERING
|
||||
M.OFFLINE = OFFLINE
|
||||
|
||||
function M.render(stream)
|
||||
return {
|
||||
type = "video_player",
|
||||
props = {
|
||||
src = stream.url,
|
||||
poster = stream.thumbnail,
|
||||
autoplay = true,
|
||||
controls = true
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function M.render_controls(state)
|
||||
return {
|
||||
type = "player_controls",
|
||||
children = {
|
||||
{ type = "button", props = { icon = state == M.PLAYING and "pause" or "play" } },
|
||||
{ type = "volume_slider", props = { min = 0, max = 100 } },
|
||||
{ type = "button", props = { icon = "maximize" } }
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function M.render_status(state, viewers)
|
||||
local colors = {
|
||||
playing = "success",
|
||||
paused = "warning",
|
||||
buffering = "info",
|
||||
offline = "error"
|
||||
}
|
||||
return {
|
||||
type = "status_bar",
|
||||
children = {
|
||||
{ type = "badge", props = { label = state, color = colors[state] } },
|
||||
{ type = "text", props = { text = viewers .. " viewers" } }
|
||||
}
|
||||
}
|
||||
end
|
||||
M.render = require("render_player")
|
||||
M.render_controls = require("render_player_controls")
|
||||
M.render_status = require("render_status")
|
||||
|
||||
return M
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
-- Stream player controls
|
||||
--- Stream player controls configuration
|
||||
---@param show_play? boolean Show play button (default true)
|
||||
---@param show_volume? boolean Show volume slider (default true)
|
||||
---@param show_fullscreen? boolean Show fullscreen button (default true)
|
||||
---@return PlayerControlsConfig Controls configuration
|
||||
local function controls(show_play, show_volume, show_fullscreen)
|
||||
return {
|
||||
type = "player_controls",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
-- Stream player module
|
||||
--- Stream player sub-module
|
||||
--- Provides controls and overlay components
|
||||
---@class StreamPlayerSubModule
|
||||
---@field controls fun(show_play?: boolean, show_volume?: boolean, show_fullscreen?: boolean): PlayerControlsConfig
|
||||
---@field overlay fun(title: string, subtitle: string): PlayerOverlayComponent
|
||||
|
||||
local player = {
|
||||
controls = require("player.controls"),
|
||||
overlay = require("player.overlay")
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
-- Stream player overlay
|
||||
--- Stream player overlay component
|
||||
---@param title string Overlay title
|
||||
---@param subtitle string Overlay subtitle
|
||||
---@return PlayerOverlayComponent Overlay component
|
||||
local function overlay(title, subtitle)
|
||||
return {
|
||||
type = "player_overlay",
|
||||
|
||||
@@ -1,44 +1,11 @@
|
||||
-- Scene management
|
||||
--- Scene management facade
|
||||
--- Re-exports single-function modules for backward compatibility
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.render_scene(scene)
|
||||
return {
|
||||
type = "scene_preview",
|
||||
props = {
|
||||
id = scene.id,
|
||||
name = scene.name,
|
||||
thumbnail = scene.thumbnail,
|
||||
active = scene.active or false
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function M.render_list(scenes)
|
||||
local items = {}
|
||||
for _, scene in ipairs(scenes) do
|
||||
table.insert(items, M.render_scene(scene))
|
||||
end
|
||||
return {
|
||||
type = "scene_list",
|
||||
children = items
|
||||
}
|
||||
end
|
||||
|
||||
function M.switch(scene_id)
|
||||
return {
|
||||
action = "switch_scene",
|
||||
scene_id = scene_id
|
||||
}
|
||||
end
|
||||
|
||||
function M.create(name, sources)
|
||||
return {
|
||||
action = "create_scene",
|
||||
data = {
|
||||
name = name,
|
||||
sources = sources or {}
|
||||
}
|
||||
}
|
||||
end
|
||||
M.render_scene = require("render_scene")
|
||||
M.render_list = require("render_scene_list")
|
||||
M.switch = require("switch_scene")
|
||||
M.create = require("create_scene")
|
||||
|
||||
return M
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
-- Stream scene camera component
|
||||
local function camera(id, label, source)
|
||||
--- Stream scene camera component
|
||||
---@param id string Camera ID
|
||||
---@param label string Camera label
|
||||
---@param source string Camera source
|
||||
---@return SceneCameraComponent Camera component
|
||||
---@class SceneCameraComponent
|
||||
---@field type "scene_camera" Component type
|
||||
---@field id string Camera ID
|
||||
---@field label string Camera label
|
||||
---@field source string Source identifier
|
||||
|
||||
---Create a camera scene source
|
||||
---@param id string Camera ID
|
||||
---@param label string Camera label
|
||||
---@param source string Source identifier
|
||||
---@return SceneCameraComponentlocal function camera(id, label, source)
|
||||
return {
|
||||
type = "scene_camera",
|
||||
id = id,
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
-- Stream scenes module
|
||||
--- Stream scenes sub-module
|
||||
--- Provides camera, screen, and layout components
|
||||
---@class StreamScenesSubModule
|
||||
---@field camera fun(id: string, label: string, source: string): SceneCameraComponent
|
||||
---@field screen fun(id: string, label: string): SceneScreenComponent
|
||||
---@field layout fun(type: "single"|"pip"|"split", sources?: table[]): SceneLayoutComponent
|
||||
|
||||
local scenes = {
|
||||
camera = require("scenes.camera"),
|
||||
screen = require("scenes.screen"),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
-- Stream scene layout component
|
||||
--- Stream scene layout component
|
||||
---@param type "single"|"pip"|"split" Layout type
|
||||
---@param sources? table[] Layout sources
|
||||
---@return SceneLayoutComponent Layout component
|
||||
local function layout(type, sources)
|
||||
return {
|
||||
type = "scene_layout",
|
||||
layoutType = type, -- "single", "pip", "split"
|
||||
layoutType = type,
|
||||
sources = sources or {}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
-- Stream scene screen share component
|
||||
--- Stream scene screen share component
|
||||
---@param id string Screen ID
|
||||
---@param label string Screen label
|
||||
---@return SceneScreenComponent Screen component
|
||||
local function screen(id, label)
|
||||
return {
|
||||
type = "scene_screen",
|
||||
|
||||
@@ -1,45 +1,11 @@
|
||||
-- Stream scheduling
|
||||
--- Stream scheduling facade
|
||||
--- Re-exports single-function modules for backward compatibility
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.render_item(stream)
|
||||
return {
|
||||
type = "schedule_item",
|
||||
props = {
|
||||
title = stream.title,
|
||||
start_time = stream.start_time,
|
||||
duration = stream.duration,
|
||||
thumbnail = stream.thumbnail
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function M.render_list(streams)
|
||||
local items = {}
|
||||
for _, stream in ipairs(streams) do
|
||||
table.insert(items, M.render_item(stream))
|
||||
end
|
||||
return {
|
||||
type = "schedule_list",
|
||||
children = items
|
||||
}
|
||||
end
|
||||
|
||||
function M.create(data)
|
||||
return {
|
||||
action = "schedule_stream",
|
||||
data = {
|
||||
title = data.title,
|
||||
start_time = data.start_time,
|
||||
duration = data.duration or 60
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function M.cancel(stream_id)
|
||||
return {
|
||||
action = "cancel_stream",
|
||||
stream_id = stream_id
|
||||
}
|
||||
end
|
||||
M.render_item = require("render_schedule_item")
|
||||
M.render_list = require("render_schedule_list")
|
||||
M.create = require("schedule_stream")
|
||||
M.cancel = require("cancel_stream")
|
||||
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user