Files
scrying-pool/styles/components/_roster-strip.less
T
uberwald 5dc9b3b8d4
CI / ci (push) Failing after 7s
Module cleanup and tests
2026-05-24 23:13:45 +02:00

507 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// All selectors MUST be scoped under .scrying-pool.
// Use --sp-* tokens only — no Foundry --color-* / --font-* / --border-* tokens allowed.
// Implemented in story 1.5.
// ============================================================
// CSS Custom Properties (State Tokens)
// ============================================================
:root {
--sp-state-active: hsl(140, 60%, 55%);
--sp-state-hidden: hsl(0, 0%, 50%);
--sp-state-self-muted: hsl(200, 60%, 55%);
--sp-state-cam-lost: hsl(30, 80%, 55%);
--sp-state-pending: hsl(50, 90%, 55%);
--sp-urgency-director: hsl(38, 90%, 55%);
--sp-state-color: hsl(140, 60%, 55%); // default, overridden per state
}
// ============================================================
// ScryingPoolStrip Layout
// ============================================================
// Outer Foundry Application window (has .scrying-pool-strip via defaultOptions.classes).
// Only visual appearance — sizing is controlled by JS setPosition().
// WARNING: do NOT add max-width or overflow here; the outer window also carries this class
// and would clip the expanded inner content.
.scrying-pool-strip {
background: var(--sp-bg, hsl(220, 15%, 12%));
border-radius: 8px;
// Hide Foundry's default window header — replaced by a lightweight in-content button.
header.window-header { display: none; }
// Remove window-content padding so the strip fills the frame edge-to-edge.
.window-content {
padding: 0;
overflow: hidden;
}
}
// Inner template div (has BOTH .scrying-pool AND .scrying-pool-strip).
// Controls the expand/collapse behaviour; safe to use max-width + overflow here.
.scrying-pool.scrying-pool-strip {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
max-width: 44px;
overflow: hidden;
transition: max-width 200ms ease-in-out;
&.is-expanded {
max-width: 240px;
}
}
// ── Drag grip (top bar, replaces window header drag affordance) ────────────────
.sp-strip__grip {
width: 100%;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
color: var(--sp-text, hsl(0, 0%, 80%));
opacity: 0.35;
font-size: 10px;
flex-shrink: 0;
transition: opacity 0.15s;
user-select: none;
&:hover { opacity: 0.75; }
&:active { cursor: grabbing; opacity: 1; }
}
// ── Lightweight close button (replaces window header) ─────────────────────────
.sp-strip__close-btn {
position: absolute;
top: 4px;
right: 4px;
z-index: 10;
width: 18px;
height: 18px;
padding: 0;
line-height: 18px;
font-size: 13px;
font-weight: 400;
background: transparent;
color: var(--sp-text, hsl(0, 0%, 80%));
border: none;
border-radius: 3px;
cursor: pointer;
opacity: 0.45;
transition: opacity 0.15s, background 0.15s;
&:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.1);
}
&:active { opacity: 0.75; }
}
// ── Toolbar row: toggle + Director's Board on the same line ───────────────────
.sp-strip__toolbar {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
flex-shrink: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.sp-strip__toggle {
width: 44px;
min-width: 44px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
color: var(--sp-text, hsl(0, 0%, 80%));
font-size: 11px;
flex-shrink: 0;
opacity: 0.6;
&:hover { opacity: 1; }
}
// ── Director's Board CTA button (shown when sidebar injection not available) ──
.sp-strip__directors-board-cta {
flex: 1;
min-width: 0;
height: 28px;
display: flex;
align-items: center;
gap: 6px;
padding: 0 8px;
background: none;
border: none;
border-left: 1px solid rgba(255, 255, 255, 0.06);
cursor: pointer;
color: var(--sp-text-secondary, #7a8390);
font-size: 11px;
text-align: left;
transition: background 0.15s, color 0.15s;
flex-shrink: 0;
i { font-size: 12px; flex-shrink: 0; }
span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
&:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--sp-text-primary, #dde2e8);
}
}
.sp-strip__participants {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
display: flex;
flex-direction: column;
gap: 4px;
}
.sp-strip__first-tip {
font-size: 0.75rem;
color: var(--sp-text-muted, hsl(0, 0%, 60%));
padding: 4px 8px;
margin: 0;
}
// ============================================================
// ParticipantAvatar (44×44px container, 32px rounded image)
// ============================================================
.sp-participant-avatar {
position: relative;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: flex-start;
background: none;
border: none;
cursor: pointer;
padding: 6px;
border-radius: 4px;
gap: 8px;
overflow: hidden;
&:focus-visible {
outline: 2px solid var(--sp-focus-ring, hsl(200, 80%, 60%));
outline-offset: 2px;
}
.is-expanded & {
width: 100%;
height: 135px; // 16:9 at 240 px strip width
padding: 0;
align-items: flex-end;
background: hsl(220, 15%, 14%);
// Gradient scrim so name text is legible over any video
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 48px;
background: linear-gradient(transparent, hsla(0, 0%, 0%, 0.72));
z-index: 2;
pointer-events: none;
border-radius: 0 0 4px 4px;
}
.sp-avatar__img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
width: 48px;
height: 48px;
}
.sp-avatar__name {
position: absolute;
bottom: 20px;
left: 8px;
right: 8px;
z-index: 3;
color: hsl(0, 0%, 95%);
}
.sp-avatar__state-label {
position: absolute;
bottom: 6px;
left: 8px;
z-index: 3;
color: hsla(0, 0%, 85%, 0.85);
}
.sp-avatar__corner-badge {
bottom: 6px;
right: 6px;
z-index: 4;
width: 10px;
height: 10px;
}
}
}
// Video container for WebRTC stream (full AV replacement mode)
.sp-participant-video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
z-index: 1;
}
.sp-avatar__img {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
// Video element styling
.sp-participant-video__element {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
background: hsl(220, 15%, 18%);
.is-expanded & {
border-radius: 4px;
}
}
// Hide avatar image when video stream is active (has video element)
.sp-participant-video:not(:empty) ~ .sp-avatar__img {
display: none;
}
.sp-avatar__corner-badge {
position: absolute;
bottom: 2px;
right: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--sp-state-color);
font-size: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.sp-avatar__name {
font-size: 0.85rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--sp-text, hsl(0, 0%, 85%));
}
.sp-avatar__state-label {
font-size: 0.7rem;
color: var(--sp-text-muted, hsl(0, 0%, 60%));
}
// ============================================================
// StateRing variants (applied as class on .sp-participant-avatar)
// ============================================================
.sp-participant-avatar.sp-state-active,
.sp-participant-avatar.sp-state-self-muted {
--sp-state-color: var(--sp-state-active);
.sp-avatar__img {
box-shadow: 0 0 0 2px var(--sp-state-color);
}
}
.sp-participant-avatar.sp-state-hidden,
.sp-participant-avatar.sp-state-cam-lost {
--sp-state-color: var(--sp-state-hidden);
.sp-avatar__img {
outline: 2px dashed var(--sp-state-color);
outline-offset: 2px;
}
}
.sp-participant-avatar.sp-state-pending {
--sp-state-color: var(--sp-state-pending);
.sp-avatar__img {
box-shadow: 0 0 0 2px var(--sp-state-color);
}
}
// ============================================================
// StateRing animations — gated under no-preference (AC-16)
// ============================================================
@media (prefers-reduced-motion: no-preference) {
.sp-participant-avatar.sp-state-pending .sp-avatar__img {
animation: sp-pulse 2s ease-in-out infinite;
}
@keyframes sp-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
// Revert flash (200ms amber, then restore)
.sp-participant-avatar.sp-state-revert .sp-avatar__img {
animation: sp-revert-flash 200ms ease-out forwards;
}
@keyframes sp-revert-flash {
0% { box-shadow: 0 0 0 3px var(--sp-urgency-director); }
100% { box-shadow: 0 0 0 2px var(--sp-state-color); }
}
}
// ============================================================
// EmptyStatePanel (AC-11)
// ============================================================
.sp-strip__empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
gap: 8px;
color: var(--sp-text-muted, hsl(0, 0%, 60%));
}
.sp-empty__icon {
font-size: 1.5rem;
display: block;
}
.sp-empty__text {
font-size: 0.75rem;
text-align: center;
}
@media (prefers-reduced-motion: no-preference) {
.sp-empty__icon {
animation: sp-breathe 3s ease-in-out infinite;
}
@keyframes sp-breathe {
0%, 100% { opacity: 0.6; transform: scale(1); }
50% { opacity: 1.0; transform: scale(1.05); }
}
}
// ============================================================
// AV Tile overlays (applied to .camera-view[data-user-id="..."])
// ============================================================
.sp-lock-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: hsla(0, 0%, 0%, 0.45);
pointer-events: none;
z-index: 10;
&::before {
content: '\f023'; // fa-lock
font-family: 'Font Awesome 6 Free';
font-weight: 900;
font-size: 1.2rem;
color: hsl(0, 0%, 85%);
}
}
.camera-view.sp-state-hidden {
opacity: 0.55;
position: relative;
}
.sp-portrait-fallback {
position: absolute;
inset: 0;
background: var(--sp-bg, hsl(220, 15%, 18%)) center/cover no-repeat;
pointer-events: none;
}
// ============================================================
// Context menu
// ============================================================
.sp-context-menu {
background: var(--sp-bg, hsl(220, 15%, 15%));
border: 1px solid hsl(0, 0%, 30%);
border-radius: 4px;
padding: 4px 0;
min-width: 160px;
z-index: 1000;
box-shadow: 0 4px 12px hsla(0, 0%, 0%, 0.4);
.sp-context-menu__item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 6px 12px;
background: none;
border: none;
cursor: pointer;
color: var(--sp-text, hsl(0, 0%, 85%));
font-size: 0.875rem;
text-align: left;
&:hover,
&:focus-visible {
background: hsla(200, 60%, 55%, 0.15);
}
}
}
// ============================================================
// ActionPopover (<dialog>)
// ============================================================
.sp-action-popover {
background: var(--sp-bg, hsl(220, 15%, 15%));
border: 1px solid hsl(0, 0%, 30%);
border-radius: 6px;
padding: 12px;
min-width: 160px;
box-shadow: 0 4px 16px hsla(0, 0%, 0%, 0.5);
color: var(--sp-text, hsl(0, 0%, 85%));
.sp-action-popover__cta {
display: block;
width: 100%;
padding: 8px 16px;
background: hsl(200, 60%, 40%);
border: none;
border-radius: 4px;
cursor: pointer;
color: hsl(0, 0%, 95%);
font-size: 0.875rem;
&:hover:not(:disabled) {
background: hsl(200, 60%, 50%);
}
&:disabled {
opacity: 0.45;
cursor: not-allowed;
}
}
}