diff --git a/apps/overlay/public/assets/lotus-mark.svg b/apps/overlay/public/assets/lotus-mark.svg new file mode 100644 index 0000000..487e6c1 --- /dev/null +++ b/apps/overlay/public/assets/lotus-mark.svg @@ -0,0 +1,12 @@ + diff --git a/apps/overlay/src/ThemeEdges.tsx b/apps/overlay/src/ThemeEdges.tsx new file mode 100644 index 0000000..59f86a9 --- /dev/null +++ b/apps/overlay/src/ThemeEdges.tsx @@ -0,0 +1,58 @@ +/** + * Shared Moonlit Water edge ornament. + * + * Keeping the artwork inline lets the persisted decoration setting control + * real SVG stroke width. A raster image or CSS-scaled mask would only distort + * the motif and could not make its calligraphic lines genuinely heavier. + */ +function EdgeArtwork({ position }: { position: 'top' | 'bottom' }) { + return ( + + ) +} + +/** Decorative chrome is ignored by assistive technology and never captures input. */ +export function ThemeEdges() { + return ( +
+ ) +} diff --git a/apps/overlay/src/auth.tsx b/apps/overlay/src/auth.tsx index 60e0cb9..0064b32 100644 --- a/apps/overlay/src/auth.tsx +++ b/apps/overlay/src/auth.tsx @@ -51,7 +51,7 @@ function AuthShell({ ) } -function TotpQr({ enrollment }: { enrollment: TotpEnrollment }) { +export function TotpQr({ enrollment }: { enrollment: TotpEnrollment }) { const { t } = useI18n() const source = useMemo(() => { if (enrollment.qrDataUrl) return enrollment.qrDataUrl diff --git a/apps/overlay/src/control.css b/apps/overlay/src/control.css index 62879f3..8efe44e 100644 --- a/apps/overlay/src/control.css +++ b/apps/overlay/src/control.css @@ -374,7 +374,17 @@ body, } .wall { - justify-content: flex-start; + height: 100%; + min-height: 0; + justify-content: flex-end; + overflow: hidden; +} + +.wall > .cards { + /* Keep the chronological stack pinned to the bottom. Once it is taller + than the OBS viewport, its oldest rows overflow above the clipping edge + while each newly appended row remains visible at the bottom. */ + flex: 0 0 auto; } .card { @@ -804,6 +814,22 @@ body, background 0.24s ease; } +.danmaku-lotus { + position: absolute; + z-index: 1; + top: 50%; + right: clamp(8px, 1.6vw, 18px); + width: clamp(26px, 7%, 44px); + aspect-ratio: 4 / 3; + pointer-events: none; + background: linear-gradient(145deg, var(--theme-accent), var(--theme-user)); + filter: drop-shadow(0 0 5px color-mix(in srgb, var(--theme-accent) 48%, transparent)); + opacity: 0.62; + transform: translateY(-50%); + -webkit-mask: var(--theme-pattern-lotus) center / contain no-repeat; + mask: var(--theme-pattern-lotus) center / contain no-repeat; +} + .card.danmaku.expanded { min-height: 92px; padding: 14px; @@ -826,7 +852,7 @@ body, inset-inline: -1px; z-index: 2; pointer-events: none; - border-inline: 4px solid rgba(123, 238, 214, 0.72); + border-inline: var(--component-decoration-rail, 4px) solid rgba(123, 238, 214, 0.72); border-radius: 11px; background: linear-gradient( 90deg, @@ -901,6 +927,11 @@ body, white-space: normal; } +.card.danmaku.expanded .copy, +.card.danmaku.compact .copy { + padding-inline-end: clamp(38px, 8%, 58px); +} + .short .cards .card:nth-child(n + 4) { display: flex; } @@ -1071,6 +1102,13 @@ body, width: 100%; } +.theme-moonlit-water .danmaku-lotus { + width: clamp(30px, 8%, 52px); + background: linear-gradient(145deg, #e8d59a, #9cb7a5 72%); + filter: drop-shadow(0 0 5px rgba(231, 201, 130, 0.34)); + opacity: 0.82; +} + .theme-moonlit-water .card.danmaku .copy > b { grid-column: 2; grid-row: 1; @@ -1099,7 +1137,7 @@ body, height: clamp(38px, 2.3em, 56px); overflow: hidden; place-items: center; - border: 1px solid rgba(231, 201, 130, 0.58); + border: var(--component-decoration-line, 1px) solid rgba(231, 201, 130, 0.58); border-radius: 50%; background: radial-gradient(circle at 36% 28%, rgba(224, 233, 205, 0.32), rgba(22, 72, 76, 0.5)); color: #d2b976; @@ -1190,6 +1228,11 @@ body, height: clamp(32px, 1.9em, 44px); } +.theme-moonlit-water .card.danmaku.expanded .copy, +.theme-moonlit-water .card.danmaku.compact .copy { + padding-inline-end: clamp(42px, 9%, 66px); +} + .theme-moonlit-water .danmaku-emoticon { filter: drop-shadow(0 1px 3px rgba(10, 34, 38, 0.56)); } diff --git a/apps/overlay/src/control.tsx b/apps/overlay/src/control.tsx index 9f78c9c..2519dd4 100644 --- a/apps/overlay/src/control.tsx +++ b/apps/overlay/src/control.tsx @@ -19,11 +19,14 @@ import { normalizeGiftEffectSettings, normalizeGiftMenuSettings, normalizeInvitations, + normalizeEnrollment, + normalizeRecoveryCodes, normalizeSettings, normalizeSongRequestPage, normalizeSongRequestSettings, normalizeSource, } from './api' +import { TotpQr } from './auth' import { Overlay } from './overlay' import { GiftEffectOverlay } from './giftEffect' import { GiftMenuOverlay } from './giftMenu' @@ -56,6 +59,7 @@ import type { SongRequestItem, SongRequestPage, SongRequestSettings, + TotpEnrollment, } from './types' const previewPresets = [ @@ -115,6 +119,32 @@ function TypographySettingsFields({ ) } +/** Shared percentage control for theme borders, dividers, and edge ornaments. */ +function DecorationLineWeightField({ + value, + onChange, +}: { + value: number + onChange: (value: number) => void +}) { + return ( + + ) +} + type TextColorField = { labelKey: string color: string | null @@ -341,6 +371,10 @@ function SettingsEditor({ ]} />