diff --git a/apps/overlay/public/assets/NOTICE.md b/apps/overlay/public/assets/NOTICE.md index 0a984d8..180d761 100644 --- a/apps/overlay/public/assets/NOTICE.md +++ b/apps/overlay/public/assets/NOTICE.md @@ -25,3 +25,6 @@ The overlay uses these SVGs as recolorable CSS masks inside message cards and bundles them locally, so OBS never needs to load the source websites. + +`moonlit-edge.svg` is an original repository asset. It is a transparent, monochrome mask shared by +the Moonlit Water danmaku, song-request, and gift-menu components. diff --git a/apps/overlay/public/assets/moonlit-edge.svg b/apps/overlay/public/assets/moonlit-edge.svg new file mode 100644 index 0000000..ce861b6 --- /dev/null +++ b/apps/overlay/public/assets/moonlit-edge.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/overlay/src/api.ts b/apps/overlay/src/api.ts index 80f8130..9f7732b 100644 --- a/apps/overlay/src/api.ts +++ b/apps/overlay/src/api.ts @@ -214,9 +214,17 @@ export function normalizeSettings(value: unknown): OverlaySettings { ...(settings as Partial), themeId: normalizeThemeId(settings.themeId), fontFamily: normalizeFontFamilyId(settings.fontFamily), + viewerColor: normalizeOptionalHexColor(settings.viewerColor), + danmakuColor: normalizeOptionalHexColor(settings.danmakuColor), } } +function normalizeOptionalHexColor(value: unknown): string | null { + if (typeof value !== 'string') return null + const normalized = value.trim().toUpperCase() + return /^#[0-9A-F]{6}$/.test(normalized) ? normalized : null +} + export function normalizeSongRequestSettings(value: unknown): SongRequestSettings { const root = object(value) const settings = object(root.settings ?? value) @@ -225,6 +233,8 @@ export function normalizeSongRequestSettings(value: unknown): SongRequestSetting ...(settings as Partial), themeId: normalizeThemeId(settings.themeId), fontFamily: normalizeFontFamilyId(settings.fontFamily), + requesterColor: normalizeOptionalHexColor(settings.requesterColor), + songTitleColor: normalizeOptionalHexColor(settings.songTitleColor), } } diff --git a/apps/overlay/src/control.css b/apps/overlay/src/control.css index 858a26b..62879f3 100644 --- a/apps/overlay/src/control.css +++ b/apps/overlay/src/control.css @@ -741,6 +741,11 @@ body, color: var(--theme-text, #e7fff9); } +.card.danmaku .copy > .danmaku-content, +.card.danmaku .copy > .danmaku-content .danmaku-text { + color: var(--component-danmaku-color, var(--theme-text, #e7fff9)); +} + .danmaku-content { line-height: 1.35; } @@ -852,7 +857,7 @@ body, } .card.danmaku.expanded .copy b { - color: var(--theme-user, #f0fffb); + color: var(--component-viewer-color, var(--theme-user, #f0fffb)); font-size: 0.78em; letter-spacing: 0.06em; } @@ -884,7 +889,7 @@ body, .card.danmaku.compact .copy b { max-width: none; flex: 0 0 auto; - color: var(--theme-compact-user, #aeece1); + color: var(--component-viewer-color, var(--theme-compact-user, #aeece1)); } .card.danmaku.compact .copy span { @@ -1069,15 +1074,21 @@ body, .theme-moonlit-water .card.danmaku .copy > b { grid-column: 2; grid-row: 1; + color: var(--component-viewer-color, #b7ad83); line-height: 1.25; } .theme-moonlit-water .card.danmaku .copy > .danmaku-content { grid-column: 1 / -1; grid-row: 2; + color: var(--component-danmaku-color, #c6ae7a); line-height: 1.35; } +.theme-moonlit-water .card.danmaku .copy > .danmaku-content .danmaku-text { + color: var(--component-danmaku-color, #c6ae7a); +} + .theme-moonlit-water .card.danmaku .viewer-avatar { display: grid; grid-column: 1; @@ -2030,6 +2041,36 @@ select { margin-bottom: 24px; } +.danmaku-color-settings { + max-width: 780px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin: -8px 0 24px; +} + +.color-picker-control { + display: grid; + grid-template-columns: 54px minmax(82px, 1fr) auto; + gap: 10px; + align-items: center; +} + +.field-grid .color-picker-control input[type='color'] { + width: 54px; + min-height: 44px; + padding: 4px; + cursor: pointer; +} + +.color-picker-control code { + color: #d9eee8; + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; + font-size: 13px; +} + +.color-picker-control button { + white-space: nowrap; +} + .typography-brightness > span { display: flex; justify-content: space-between; @@ -2291,6 +2332,7 @@ td:last-child { .totp-enrollment, .slider-grid, .typography-settings, + .danmaku-color-settings, .two-columns { grid-template-columns: 1fr; } diff --git a/apps/overlay/src/control.tsx b/apps/overlay/src/control.tsx index e7efc61..9f78c9c 100644 --- a/apps/overlay/src/control.tsx +++ b/apps/overlay/src/control.tsx @@ -115,6 +115,50 @@ function TypographySettingsFields({ ) } +type TextColorField = { + labelKey: string + color: string | null + themeColor: string + onChange: (color: string | null) => void +} + +/** Shared theme-aware color editor used by text-led OBS components. */ +function TextColorSettingsFields({ fields }: { fields: TextColorField[] }) { + const field = ( + labelKey: string, + color: string | null, + themeColor: string, + onChange: (color: string | null) => void, + ) => ( + + ) + + return ( +
+ {fields.map(item => field(item.labelKey, item.color, item.themeColor, item.onChange))} +
+ ) +} + function isDanmakuKind(kind: string): boolean { return kind === 'danmaku_overlay' || kind === 'danmaku' } @@ -280,6 +324,22 @@ function SettingsEditor({ onFontFamily={value => edit('fontFamily', value)} onFontBrightness={value => edit('fontBrightness', value)} /> + edit('viewerColor', value), + }, + { + labelKey: 'settings.danmaku_color', + color: settings.danmakuColor, + themeColor: selectedTheme.palette.text, + onChange: value => edit('danmakuColor', value), + }, + ]} + />