mirror of
https://github.com/ilyhalight/voice-over-translation.git
synced 2026-10-19 07:43:33 +00:00
refactor: complete solid migration
This commit is contained in:
parent
58b7ed8956
commit
737dd3168e
72 changed files with 1273 additions and 1610 deletions
|
|
@ -37,3 +37,5 @@ Only use the new localization key after it has been added through this command.
|
|||
## Commits
|
||||
|
||||
ALWAYS write commit messages in English. You MUST use the semantic commits format. NEVER use `src/localization/locales/*` files to write commit messages, except in the case where these are the only changes.
|
||||
|
||||
NEVER make push or pull requests without ASK an user!
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
"test": "bun test",
|
||||
"test:unit": "bun test",
|
||||
"test:unit:watch": "bun test --watch",
|
||||
"test:ui": "vite build --config vite/vite.test-ui.config.ts",
|
||||
"test:ui": "vitest run --project storybook --maxWorkers=4",
|
||||
"build:gm:default": "vite build --config vite/vite.config.ts",
|
||||
"build:gm:min": "vite build --mode minify --config vite/vite.config.ts",
|
||||
"build:gm": "run-p build:gm:default build:gm:min",
|
||||
|
|
|
|||
|
|
@ -1 +1,38 @@
|
|||
@use "../../styles/components/details.scss";
|
||||
@use "../mixins" as shared;
|
||||
|
||||
.vot-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: rgba(var(--vot-onsurface-rgb), 0.87);
|
||||
@include shared.vot-font-family;
|
||||
font-size: 16px;
|
||||
border-radius: 0.5em !important;
|
||||
padding: 0.5em !important;
|
||||
margin: -0.5em !important;
|
||||
line-height: 1.5;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background var(--vot-duration-medium) var(--vot-easing-standard);
|
||||
|
||||
&-arrow-icon {
|
||||
width: 20px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: scale(1.25) rotate(-90deg);
|
||||
fill: rgba(var(--vot-onsurface-rgb), 0.87);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&:not([aria-disabled="true"]):hover {
|
||||
background: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.06);
|
||||
}
|
||||
|
||||
&[aria-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.38;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,43 @@
|
|||
@use "../../styles/components/buttons/contained.scss";
|
||||
@use "./mixins" as button;
|
||||
|
||||
.vot-button {
|
||||
--vot-helper-theme: var(
|
||||
--vot-theme-rgb,
|
||||
var(--vot-primary-rgb, 33, 150, 243)
|
||||
);
|
||||
--vot-helper-ontheme: var(
|
||||
--vot-ontheme-rgb,
|
||||
var(--vot-onprimary-rgb, 255, 255, 255)
|
||||
);
|
||||
@include button.vot-button-base(
|
||||
$radius: var(--vot-radius-s),
|
||||
$padding: 0 var(--vot-space-4),
|
||||
$min-width: 64px,
|
||||
$height: 36px,
|
||||
$line-height: 36px
|
||||
);
|
||||
border: none !important;
|
||||
color: rgb(var(--vot-helper-ontheme));
|
||||
background-color: rgb(var(--vot-helper-theme));
|
||||
box-shadow: var(--vot-shadow-1);
|
||||
transition: box-shadow var(--vot-duration-medium) var(--vot-easing-standard);
|
||||
@include button.vot-button-layers(rgb(var(--vot-helper-ontheme)));
|
||||
@include button.vot-button-layer-hover(0.08);
|
||||
@include button.vot-button-layer-active(0.32);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--vot-shadow-2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: var(--vot-shadow-2);
|
||||
}
|
||||
|
||||
&[disabled="true"] {
|
||||
background-color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.12);
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
box-shadow: none;
|
||||
cursor: initial;
|
||||
@include button.vot-button-layer-reset-disabled;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@use "../../styles/components/buttons/mixins" as button;
|
||||
@use "./mixins" as button;
|
||||
|
||||
.vot-hotkey {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -1 +1,36 @@
|
|||
@use "../../styles/components/buttons/icon.scss";
|
||||
@use "./mixins" as button;
|
||||
|
||||
.vot-icon-button {
|
||||
--vot-helper-onsurface: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.87);
|
||||
@include button.vot-button-base(
|
||||
$radius: 50%,
|
||||
$padding: 0,
|
||||
$min-width: 36px,
|
||||
$height: 36px,
|
||||
$line-height: 36px,
|
||||
$margin: 0 !important
|
||||
);
|
||||
border: none !important;
|
||||
width: 36px;
|
||||
fill: var(--vot-helper-onsurface);
|
||||
color: var(--vot-helper-onsurface);
|
||||
background-color: transparent;
|
||||
@include button.vot-button-layers(var(--vot-helper-onsurface));
|
||||
@include button.vot-button-layer-hover(0.04);
|
||||
@include button.vot-button-layer-active(0.32);
|
||||
&[disabled="true"],
|
||||
&[aria-disabled="true"] {
|
||||
background-color: transparent;
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
fill: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
cursor: initial;
|
||||
@include button.vot-button-layer-reset-disabled;
|
||||
}
|
||||
|
||||
& svg {
|
||||
fill: inherit;
|
||||
stroke: inherit;
|
||||
width: 24px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,30 @@
|
|||
@use "../../styles/components/buttons/outlined.scss";
|
||||
@use "./mixins" as button;
|
||||
|
||||
.vot-outlined-button {
|
||||
--vot-helper-theme: var(
|
||||
--vot-theme-rgb,
|
||||
var(--vot-primary-rgb, 33, 150, 243)
|
||||
);
|
||||
@include button.vot-button-base(
|
||||
$radius: var(--vot-radius-s),
|
||||
$padding: 0 var(--vot-space-4),
|
||||
$min-width: 64px,
|
||||
$height: 36px,
|
||||
$line-height: 34px,
|
||||
$margin: 0 !important
|
||||
);
|
||||
border: solid 1px var(--vot-border-color) !important;
|
||||
color: rgb(var(--vot-helper-theme));
|
||||
background-color: transparent;
|
||||
@include button.vot-button-layers(rgb(var(--vot-helper-theme)));
|
||||
@include button.vot-button-layer-hover(0.04);
|
||||
@include button.vot-button-layer-active(0.16);
|
||||
|
||||
&[disabled="true"],
|
||||
&[aria-disabled="true"] {
|
||||
background-color: transparent;
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
cursor: initial;
|
||||
@include button.vot-button-layer-reset-disabled;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import { ProgressIconButton } from "./ProgressIconButton";
|
|||
const meta = {
|
||||
component: ProgressIconButton,
|
||||
render: (args) => (
|
||||
<div style="display: flex;color:#fff;font-size: 40px">
|
||||
<vot-block style="display: flex;color:#fff;font-size: 40px">
|
||||
<ProgressIconButton {...args} />
|
||||
</div>
|
||||
</vot-block>
|
||||
),
|
||||
} satisfies Meta<typeof ProgressIconButton>;
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ export const ProgressIconButtonChanging: Story = {
|
|||
const [progress, setProgress] = createSignal(0);
|
||||
|
||||
return (
|
||||
<div style="display: flex; color: #fff; font-size: 40px">
|
||||
<vot-block style="display: flex; color: #fff; font-size: 40px">
|
||||
<ProgressIconButton
|
||||
{...args}
|
||||
showProgress={showProgress()}
|
||||
|
|
@ -56,7 +56,7 @@ export const ProgressIconButtonChanging: Story = {
|
|||
>
|
||||
<DownloadIcon />
|
||||
</ProgressIconButton>
|
||||
</div>
|
||||
</vot-block>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type {
|
|||
DataAttributes,
|
||||
OnClickEvent,
|
||||
} from "../../types/components/shared";
|
||||
import { isKeyboardActivation } from "../../ui/components/componentShared";
|
||||
import { isKeyboardActivation } from "../componentShared";
|
||||
|
||||
export type RawButtonProps = {
|
||||
class: string;
|
||||
|
|
|
|||
|
|
@ -1 +1,30 @@
|
|||
@use "../../styles/components/buttons/text.scss";
|
||||
@use "./mixins" as button;
|
||||
|
||||
.vot-text-button {
|
||||
--vot-helper-theme: var(
|
||||
--vot-theme-rgb,
|
||||
var(--vot-primary-rgb, 33, 150, 243)
|
||||
);
|
||||
@include button.vot-button-base(
|
||||
$radius: var(--vot-radius-s),
|
||||
$padding: 0 var(--vot-space-2),
|
||||
$min-width: 64px,
|
||||
$height: 36px,
|
||||
$line-height: 36px,
|
||||
$margin: 0 !important
|
||||
);
|
||||
border: none !important;
|
||||
color: rgb(var(--vot-helper-theme));
|
||||
background-color: transparent;
|
||||
@include button.vot-button-layers(rgb(var(--vot-helper-theme)));
|
||||
@include button.vot-button-layer-hover(0.04);
|
||||
@include button.vot-button-layer-active(0.16);
|
||||
|
||||
&[disabled="true"],
|
||||
&[aria-disabled="true"] {
|
||||
background-color: transparent;
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
cursor: initial;
|
||||
@include button.vot-button-layer-reset-disabled;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@use "../../mixins" as shared;
|
||||
@use "../mixins" as shared;
|
||||
|
||||
@mixin vot-button-base(
|
||||
$radius,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.vot-select_new {
|
||||
.vot-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
border-color: var(--vot-border-color-hover);
|
||||
}
|
||||
|
||||
&[aria-expanded="true"] .vot-select_new-outer__arrow {
|
||||
&[aria-expanded="true"] .vot-select-outer__arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,12 +84,12 @@ export const SelectNearViewportBottom: Story = {
|
|||
</vot-block>
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const trigger = canvasElement.querySelector<HTMLElement>(
|
||||
".vot-select_new-outer",
|
||||
);
|
||||
const popup = canvasElement.ownerDocument.querySelector<HTMLElement>(
|
||||
".vot-select_new-inner",
|
||||
);
|
||||
const trigger =
|
||||
canvasElement.querySelector<HTMLElement>(".vot-select-outer");
|
||||
const popup =
|
||||
canvasElement.ownerDocument.querySelector<HTMLElement>(
|
||||
".vot-select-inner",
|
||||
);
|
||||
|
||||
await expect(trigger).not.toBeNull();
|
||||
await expect(popup).not.toBeNull();
|
||||
|
|
@ -119,7 +119,7 @@ export const SelectNearViewportBottom: Story = {
|
|||
expect(popup.hidden).toBe(true);
|
||||
});
|
||||
|
||||
const select = trigger.closest<HTMLElement>(".vot-select_new");
|
||||
const select = trigger.closest<HTMLElement>(".vot-select");
|
||||
await expect(select).not.toBeNull();
|
||||
if (!select?.parentNode) {
|
||||
return;
|
||||
|
|
@ -166,7 +166,7 @@ export const SelectLoading: Story = {
|
|||
play: async () => {
|
||||
const listbox = document.querySelector('[role="listbox"]');
|
||||
const loader = document.querySelector(
|
||||
'.vot-select_new-inner__no-options[data-searching="true"]',
|
||||
'.vot-select-inner__no-options[data-searching="true"]',
|
||||
);
|
||||
|
||||
await expect(listbox).toHaveAttribute("aria-busy", "true");
|
||||
|
|
@ -198,12 +198,9 @@ export const SelectStaysOpenWhenOptionsChange: Story = {
|
|||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const trigger = canvasElement.querySelector<HTMLElement>(
|
||||
".vot-select_new-outer",
|
||||
);
|
||||
const popup = canvasElement.querySelector<HTMLElement>(
|
||||
".vot-select_new-inner",
|
||||
);
|
||||
const trigger =
|
||||
canvasElement.querySelector<HTMLElement>(".vot-select-outer");
|
||||
const popup = canvasElement.querySelector<HTMLElement>(".vot-select-inner");
|
||||
|
||||
await expect(trigger).not.toBeNull();
|
||||
await expect(popup).not.toBeNull();
|
||||
|
|
@ -233,9 +230,8 @@ export const SelectWithSearchWithCustomProvider: Story = {
|
|||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const trigger = canvasElement.querySelector<HTMLElement>(
|
||||
".vot-select_new-outer",
|
||||
);
|
||||
const trigger =
|
||||
canvasElement.querySelector<HTMLElement>(".vot-select-outer");
|
||||
await expect(trigger).not.toBeNull();
|
||||
if (!trigger) {
|
||||
return;
|
||||
|
|
@ -261,7 +257,7 @@ export const SelectWithSearchWithCustomProvider: Story = {
|
|||
: initialPopupRect.bottom;
|
||||
|
||||
const input = canvasElement.ownerDocument.querySelector<HTMLInputElement>(
|
||||
".vot-select_new-inner input",
|
||||
".vot-select-inner input",
|
||||
);
|
||||
await expect(input).not.toBeNull();
|
||||
if (!input) {
|
||||
|
|
@ -275,7 +271,7 @@ export const SelectWithSearchWithCustomProvider: Story = {
|
|||
providerOption =
|
||||
Array.from(
|
||||
canvasElement.ownerDocument.querySelectorAll<HTMLElement>(
|
||||
".vot-select_new-inner__option",
|
||||
".vot-select-inner__option",
|
||||
),
|
||||
).find((option) => option.textContent === "123") ?? null;
|
||||
expect(providerOption).not.toBeNull();
|
||||
|
|
|
|||
|
|
@ -335,17 +335,15 @@ export function Select(props: SelectProps): JSX.Element {
|
|||
return (
|
||||
<vot-block
|
||||
ref={finalProps.ref}
|
||||
class="vot-select_new"
|
||||
class="vot-select"
|
||||
aria-disabled={finalProps.disabled}
|
||||
>
|
||||
<Show when={finalProps.children}>
|
||||
<vot-block class="vot-select_new-label">
|
||||
{finalProps.children}
|
||||
</vot-block>
|
||||
<vot-block class="vot-select-label">{finalProps.children}</vot-block>
|
||||
</Show>
|
||||
<RawButton
|
||||
ref={(el) => (outerRef = el)}
|
||||
class="vot-select_new-outer"
|
||||
class="vot-select-outer"
|
||||
buttonProps={{
|
||||
"aria-haspopup": "listbox",
|
||||
"aria-controls": selectId,
|
||||
|
|
@ -361,15 +359,13 @@ export function Select(props: SelectProps): JSX.Element {
|
|||
finalProps.onOpen?.();
|
||||
}}
|
||||
>
|
||||
<vot-block class="vot-select_new-outer__title">
|
||||
{visibleTitle()}
|
||||
</vot-block>
|
||||
<vot-block class="vot-select_new-outer__arrow">
|
||||
<vot-block class="vot-select-outer__title">{visibleTitle()}</vot-block>
|
||||
<vot-block class="vot-select-outer__arrow">
|
||||
<ChevronIcon />
|
||||
</vot-block>
|
||||
</RawButton>
|
||||
<vot-block
|
||||
class="vot-select_new-inner"
|
||||
class="vot-select-inner"
|
||||
ref={innerRef}
|
||||
id={selectId}
|
||||
hidden={!isOpen()}
|
||||
|
|
@ -382,7 +378,7 @@ export function Select(props: SelectProps): JSX.Element {
|
|||
/>
|
||||
</Show>
|
||||
<vot-block
|
||||
class="vot-select_new-inner__options"
|
||||
class="vot-select-inner__options"
|
||||
role="listbox"
|
||||
aria-busy={finalProps.loading ? "true" : undefined}
|
||||
aria-multiselectable={finalProps.multiple ? "true" : undefined}
|
||||
|
|
@ -390,7 +386,7 @@ export function Select(props: SelectProps): JSX.Element {
|
|||
<For each={filteredOptions()}>
|
||||
{(option) => (
|
||||
<RawButton
|
||||
class="vot-select_new-inner__option"
|
||||
class="vot-select-inner__option"
|
||||
disabled={option.disabled || finalProps.disabled}
|
||||
buttonProps={{
|
||||
role: "option",
|
||||
|
|
@ -410,16 +406,13 @@ export function Select(props: SelectProps): JSX.Element {
|
|||
</For>
|
||||
</vot-block>
|
||||
<Show when={finalProps.loading}>
|
||||
<vot-block
|
||||
class="vot-select_new-inner__no-options"
|
||||
data-searching="true"
|
||||
>
|
||||
<vot-block class="vot-select-inner__no-options" data-searching="true">
|
||||
<LoadingDotsIcon />
|
||||
</vot-block>
|
||||
</Show>
|
||||
<Show when={filteredOptions().length === 0 && !finalProps.loading}>
|
||||
<vot-block
|
||||
class="vot-select_new-inner__no-options"
|
||||
class="vot-select-inner__no-options"
|
||||
data-searching={isSearching()}
|
||||
>
|
||||
<Show
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.vot-slider_new {
|
||||
.vot-slider {
|
||||
--vot-slider-track-bg: rgba(var(--vot-onsurface-rgb, 227, 227, 227), 0.15);
|
||||
--vot-slider-track-progress-bg: rgb(var(--vot-primary-rgb, 33, 150, 243));
|
||||
--vot-slider-size: 1em;
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
&[aria-disabled="true"] {
|
||||
user-select: none;
|
||||
opacity: 0.5;
|
||||
.vot-slider_new__control {
|
||||
.vot-slider__control {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
|
@ -62,8 +62,8 @@
|
|||
}
|
||||
|
||||
&[data-dragging] {
|
||||
.vot-slider_new__track-progress,
|
||||
.vot-slider_new__handle {
|
||||
.vot-slider__track-progress,
|
||||
.vot-slider__handle {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,13 +49,13 @@ export function Slider(props: SliderProps): JSX.Element {
|
|||
return (
|
||||
<vot-block
|
||||
ref={finalProps.ref}
|
||||
class="vot-slider_new"
|
||||
class="vot-slider"
|
||||
data-dragging={dragging() ? "" : undefined}
|
||||
style={{ "--vot-progress": progress() }}
|
||||
aria-disabled={finalProps.disabled}
|
||||
>
|
||||
<input
|
||||
class="vot-slider_new__control"
|
||||
class="vot-slider__control"
|
||||
type="range"
|
||||
min={finalProps.min}
|
||||
max={finalProps.max}
|
||||
|
|
@ -97,9 +97,9 @@ export function Slider(props: SliderProps): JSX.Element {
|
|||
finalProps.onInput?.(newValue);
|
||||
}}
|
||||
/>
|
||||
<vot-block class="vot-slider_new__track" />
|
||||
<vot-block class="vot-slider_new__track vot-slider_new__track-progress" />
|
||||
<vot-block class="vot-slider_new__handle" />
|
||||
<vot-block class="vot-slider__track" />
|
||||
<vot-block class="vot-slider__track vot-slider__track-progress" />
|
||||
<vot-block class="vot-slider__handle" />
|
||||
</vot-block>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.vot-slider_new-label {
|
||||
.vot-slider-label {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: baseline;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export type SliderLabelDescProps = {
|
|||
|
||||
export function SliderLabelDesc(props: SliderLabelDescProps): JSX.Element {
|
||||
return (
|
||||
<vot-block ref={props.ref} class="vot-slider_new-label__text-desc">
|
||||
<vot-block ref={props.ref} class="vot-slider-label__text-desc">
|
||||
{props.children}
|
||||
</vot-block>
|
||||
);
|
||||
|
|
@ -34,15 +34,13 @@ export function SliderLabel(props: SliderLabelProps): JSX.Element {
|
|||
return (
|
||||
<vot-block
|
||||
ref={finalProps.ref}
|
||||
class="vot-slider_new-label"
|
||||
class="vot-slider-label"
|
||||
aria-disabled={finalProps.disabled}
|
||||
>
|
||||
<vot-block class="vot-slider_new-label__text">
|
||||
<vot-block class="vot-slider-label__text">
|
||||
{finalProps.children}
|
||||
</vot-block>
|
||||
<vot-block class="vot-slider_new-label__value">
|
||||
{finalProps.value}
|
||||
</vot-block>
|
||||
<vot-block class="vot-slider-label__value">{finalProps.value}</vot-block>
|
||||
</vot-block>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.vot-slider_new-wrapper {
|
||||
.vot-slider-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25em;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export type SliderWrapperProps = {
|
|||
|
||||
export function SliderWrapper(props: SliderWrapperProps): JSX.Element {
|
||||
return (
|
||||
<vot-block ref={props.ref} class="vot-slider_new-wrapper">
|
||||
<vot-block ref={props.ref} class="vot-slider-wrapper">
|
||||
{props.children}
|
||||
</vot-block>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@use "../../styles/mixins" as shared;
|
||||
@use "../mixins" as shared;
|
||||
|
||||
.vot-switch {
|
||||
--vot-switch-outline: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.6);
|
||||
|
|
|
|||
|
|
@ -63,22 +63,25 @@ export function Switch(props: SwitchProps): JSX.Element {
|
|||
}}
|
||||
/>
|
||||
<Show when={finalProps.heading || finalProps.description}>
|
||||
<span class="vot-switch-text">
|
||||
<vot-block class="vot-switch-text">
|
||||
<Show when={finalProps.heading}>
|
||||
<span class="vot-switch-heading" id={`${textId}-heading`}>
|
||||
<vot-block class="vot-switch-heading" id={`${textId}-heading`}>
|
||||
{finalProps.heading}
|
||||
</span>
|
||||
</vot-block>
|
||||
</Show>
|
||||
<Show when={finalProps.description}>
|
||||
<span class="vot-switch-description" id={`${textId}-description`}>
|
||||
<vot-block
|
||||
class="vot-switch-description"
|
||||
id={`${textId}-description`}
|
||||
>
|
||||
{finalProps.description}
|
||||
</span>
|
||||
</vot-block>
|
||||
</Show>
|
||||
</span>
|
||||
</vot-block>
|
||||
</Show>
|
||||
<span class="vot-switch-track" data-checked={checked()}>
|
||||
<span class="vot-switch-handle"></span>
|
||||
</span>
|
||||
<vot-block class="vot-switch-track" data-checked={checked()}>
|
||||
<vot-block class="vot-switch-handle" />
|
||||
</vot-block>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@use "../../styles/mixins" as shared;
|
||||
@use "../mixins" as shared;
|
||||
|
||||
.vot-dialog {
|
||||
--vot-helper-surface-rgb: var(--vot-surface-rgb, 255, 255, 255);
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { ProgressIcon } from "./ProgressIcon";
|
|||
const meta = {
|
||||
component: ProgressIcon,
|
||||
render: (args) => (
|
||||
<div style="display: flex;color:#fff;font-size: 40px">
|
||||
<vot-block style="display: flex;color:#fff;font-size: 40px">
|
||||
<ProgressIcon {...args} />
|
||||
</div>
|
||||
</vot-block>
|
||||
),
|
||||
} satisfies Meta<typeof ProgressIcon>;
|
||||
|
||||
|
|
@ -53,9 +53,9 @@ export const ProgressIconChanging: Story = {
|
|||
onCleanup(() => window.clearInterval(intervalId));
|
||||
|
||||
return (
|
||||
<div style="display: flex; color: #fff; font-size: 40px">
|
||||
<vot-block style="display: flex; color: #fff; font-size: 40px">
|
||||
<ProgressIcon progress={progress()} />
|
||||
</div>
|
||||
</vot-block>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { TranslateIcon } from "./TranslateIcon";
|
|||
const meta = {
|
||||
component: TranslateIcon,
|
||||
render: (args) => (
|
||||
<div style="color: white">
|
||||
<vot-block style="color: white">
|
||||
<TranslateIcon {...args} />
|
||||
</div>
|
||||
</vot-block>
|
||||
),
|
||||
} satisfies Meta<typeof TranslateIcon>;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { expect, fn, userEvent } from "storybook/test";
|
||||
import type { Meta, StoryObj } from "storybook-solidjs-vite";
|
||||
import { setSettings, settings } from "../stores/settings";
|
||||
import { setSettings, settings } from "../../stores/settings";
|
||||
import { OverlayView, type OverlayViewControls } from "./OverlayView";
|
||||
|
||||
const onSettingsClick = fn();
|
||||
|
|
@ -10,7 +10,7 @@ let smallLayoutControls: OverlayViewControls | undefined;
|
|||
const meta = {
|
||||
component: OverlayView,
|
||||
render: (args) => (
|
||||
<div style="display: flex;padding: 20px 200px 200px;height: 90vh;">
|
||||
<vot-block style="display: flex;padding: 20px 200px 200px;height: 90vh;">
|
||||
<OverlayView
|
||||
{...args}
|
||||
baseOpacity={1}
|
||||
|
|
@ -28,7 +28,7 @@ const meta = {
|
|||
>
|
||||
change direction
|
||||
</button>
|
||||
</div>
|
||||
</vot-block>
|
||||
),
|
||||
} satisfies Meta<typeof OverlayView>;
|
||||
|
||||
|
|
@ -9,29 +9,29 @@ import {
|
|||
onMount,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import type { SelectOption } from "../components/Control/Select";
|
||||
import { PreviewSegmentedButtonOverlay } from "../components/SegmentedButton/PreviewSegmentedButtonOverlay";
|
||||
import type { SegmentedButtonControls } from "../components/SegmentedButton/SegmentedButton";
|
||||
import type { SegmentedButtonMenuControls } from "../components/SegmentedButton/SegmentedButtonMenu";
|
||||
import {
|
||||
SegmentedButtonMenuOverlay,
|
||||
type SegmentedButtonMenuOverlayProps,
|
||||
} from "../components/SegmentedButton/SegmentedButtonMenuOverlay";
|
||||
import {
|
||||
SegmentedButtonOverlay,
|
||||
type SegmentedButtonOverlayProps,
|
||||
} from "../components/SegmentedButton/SegmentedButtonOverlay";
|
||||
import { localizationProvider } from "../localization/localizationProvider";
|
||||
import { setSettings, settings } from "../stores/settings";
|
||||
import type { Position, Status } from "../types/components/votButton";
|
||||
import { localizationProvider } from "../../localization/localizationProvider";
|
||||
import { setSettings, settings } from "../../stores/settings";
|
||||
import type { Position, Status } from "../../types/components/votButton";
|
||||
import {
|
||||
getButtonDirection,
|
||||
isSideButtonPosition,
|
||||
normalizeButtonPosition,
|
||||
resolveButtonPositionFromPointer,
|
||||
} from "../ui/buttonPlacement";
|
||||
import { votStorage } from "../utils/storage";
|
||||
import { isPiPAvailable } from "../utils/utils";
|
||||
} from "../../ui/buttonPlacement";
|
||||
import { votStorage } from "../../utils/storage";
|
||||
import { isPiPAvailable } from "../../utils/utils";
|
||||
import type { SelectOption } from "../Control/Select";
|
||||
import { PreviewSegmentedButtonOverlay } from "../SegmentedButton/PreviewSegmentedButtonOverlay";
|
||||
import type { SegmentedButtonControls } from "../SegmentedButton/SegmentedButton";
|
||||
import type { SegmentedButtonMenuControls } from "../SegmentedButton/SegmentedButtonMenu";
|
||||
import {
|
||||
SegmentedButtonMenuOverlay,
|
||||
type SegmentedButtonMenuOverlayProps,
|
||||
} from "../SegmentedButton/SegmentedButtonMenuOverlay";
|
||||
import {
|
||||
SegmentedButtonOverlay,
|
||||
type SegmentedButtonOverlayProps,
|
||||
} from "../SegmentedButton/SegmentedButtonOverlay";
|
||||
|
||||
type ButtonDragState = {
|
||||
pointerId: number;
|
||||
|
|
@ -87,7 +87,6 @@ export type OverlayViewControls = {
|
|||
>;
|
||||
|
||||
export type OverlayViewProps = {
|
||||
ref?: (element: HTMLElement) => void;
|
||||
controlsRef?: (controls: OverlayViewControls) => void;
|
||||
isBigContainer?: boolean;
|
||||
/**
|
||||
|
|
@ -572,11 +571,11 @@ export function OverlayView(props: OverlayViewProps): JSX.Element {
|
|||
}
|
||||
|
||||
const path = event.composedPath();
|
||||
// select_new-inner can be outside of these overlay
|
||||
// Select options can be portaled outside of these overlays.
|
||||
const isClickInsideSelect = path.some(
|
||||
(element) =>
|
||||
element instanceof HTMLElement &&
|
||||
element.classList.contains("vot-select_new-inner"),
|
||||
element.classList.contains("vot-select-inner"),
|
||||
);
|
||||
if (
|
||||
path.includes(buttonOverlay) ||
|
||||
|
|
@ -642,7 +641,6 @@ export function OverlayView(props: OverlayViewProps): JSX.Element {
|
|||
<vot-block
|
||||
ref={(element) => {
|
||||
rootElement = element;
|
||||
finalProps.ref?.(element);
|
||||
}}
|
||||
>
|
||||
<SegmentedButtonOverlay
|
||||
|
|
@ -5,9 +5,9 @@ import { MenuHeaderContent } from "./SegmentedButtonMenu";
|
|||
const meta = {
|
||||
component: MenuHeaderContent,
|
||||
render: (args) => (
|
||||
<div style="display: flex;padding: 20px 200px 200px;">
|
||||
<vot-block style="display: flex;padding: 20px 200px 200px;">
|
||||
<MenuHeaderContent {...args} />
|
||||
</div>
|
||||
</vot-block>
|
||||
),
|
||||
} satisfies Meta<typeof MenuHeaderContent>;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { PreviewSegmentedButton } from "./PreviewSegmentedButton";
|
|||
const meta = {
|
||||
component: PreviewSegmentedButton,
|
||||
render: (args) => (
|
||||
<div style="display: flex;background: gray;padding: 20px 200px 200px;">
|
||||
<vot-block style="display: flex;background: gray;padding: 20px 200px 200px;">
|
||||
<PreviewSegmentedButton {...args} />
|
||||
</div>
|
||||
</vot-block>
|
||||
),
|
||||
} satisfies Meta<typeof PreviewSegmentedButton>;
|
||||
|
||||
|
|
@ -61,8 +61,8 @@ export const PreviewSegmentedButtonAsColumn: Story = {
|
|||
direction: "column",
|
||||
},
|
||||
render: (args) => (
|
||||
<div style="display: flex;background: gray;padding: 200px;padding-left: 20px;">
|
||||
<vot-block style="display: flex;background: gray;padding: 200px;padding-left: 20px;">
|
||||
<PreviewSegmentedButton {...args} />
|
||||
</div>
|
||||
</vot-block>
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
@use "../../styles/mixins" as shared;
|
||||
@use "../../styles/components/buttons/mixins" as button;
|
||||
@use "../mixins" as shared;
|
||||
@use "../Button/mixins" as button;
|
||||
|
||||
.vot-segmented-button {
|
||||
--vot-helper-theme-rgb: var(--vot-onsurface-rgb, 0, 0, 0);
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ function dispatchPrimaryPointerUp(
|
|||
const meta = {
|
||||
component: SegmentedButton,
|
||||
render: (args) => (
|
||||
<div style="display: flex;background: gray;padding: 20px 200px 200px;">
|
||||
<vot-block style="display: flex;background: gray;padding: 20px 200px 200px;">
|
||||
<SegmentedButton {...args} />
|
||||
</div>
|
||||
</vot-block>
|
||||
),
|
||||
} satisfies Meta<typeof SegmentedButton>;
|
||||
|
||||
|
|
@ -137,9 +137,9 @@ export const SegmentedButtonAsColumn: Story = {
|
|||
tooltipPos: "right",
|
||||
},
|
||||
render: (args) => (
|
||||
<div style="display: flex;background: gray;padding: 200px;padding-left: 20px;">
|
||||
<vot-block style="display: flex;background: gray;padding: 200px;padding-left: 20px;">
|
||||
<SegmentedButton {...args} />
|
||||
</div>
|
||||
</vot-block>
|
||||
),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ import "./SegmentedButton.scss";
|
|||
import { localizationProvider } from "../../localization/localizationProvider";
|
||||
import { setSettings, settings } from "../../stores/settings";
|
||||
import type { Direction, Status } from "../../types/components/votButton";
|
||||
import { isTouchFirstInput } from "../../utils/inputDevice";
|
||||
import { RawButton } from "../Button/RawButton";
|
||||
import {
|
||||
isKeyboardActivation,
|
||||
isPrimaryPointerAction,
|
||||
} from "../../ui/components/componentShared";
|
||||
import { isTouchFirstInput } from "../../utils/inputDevice";
|
||||
import { RawButton } from "../Button/RawButton";
|
||||
} from "../componentShared";
|
||||
import { ChevronIcon } from "../Icons/ChevronIcon";
|
||||
import { MenuIcon } from "../Icons/MenuIcon";
|
||||
import { PiPIcon } from "../Icons/PiPIcon";
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { SegmentedButtonMenu } from "./SegmentedButtonMenu";
|
|||
const meta = {
|
||||
component: SegmentedButtonMenu,
|
||||
render: (args) => (
|
||||
<div style="display: flex;background: gray;padding: 20px 200px 200px;">
|
||||
<vot-block style="display: flex;background: gray;padding: 20px 200px 200px;">
|
||||
<SegmentedButtonMenu {...args} />
|
||||
</div>
|
||||
</vot-block>
|
||||
),
|
||||
} satisfies Meta<typeof SegmentedButtonMenu>;
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,319 @@
|
|||
@use "../../styles/components/voice-popover.scss";
|
||||
@use "../mixins" as shared;
|
||||
|
||||
.vot-voice-icon {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
|
||||
.vot-eq-bar {
|
||||
transform-origin: 50% 100%;
|
||||
transform-box: fill-box;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
&--standard .vot-eq-bar {
|
||||
fill: rgba(var(--vot-onsurface-rgb, 227, 227, 227), 0.4);
|
||||
}
|
||||
|
||||
&--live .vot-eq-bar {
|
||||
fill: #e040a0;
|
||||
}
|
||||
}
|
||||
|
||||
.vot-voice-popover {
|
||||
--vot-helper-surface-rgb: var(--vot-surface-rgb, 32, 33, 36);
|
||||
|
||||
--vot-helper-surface: rgb(var(--vot-helper-surface-rgb));
|
||||
|
||||
--vot-helper-onsurface-rgb: var(--vot-onsurface-rgb, 227, 227, 227);
|
||||
|
||||
--vot-helper-onsurface: rgba(var(--vot-helper-onsurface-rgb), 0.87);
|
||||
|
||||
--vot-helper-onsurface-secondary: rgba(var(--vot-helper-onsurface-rgb), 0.55);
|
||||
|
||||
--vot-voice-active-standard-bg: rgba(
|
||||
var(--vot-primary-rgb, 139, 180, 245),
|
||||
0.1
|
||||
);
|
||||
|
||||
--vot-voice-active-standard-fg: rgb(var(--vot-primary-rgb, 139, 180, 245));
|
||||
|
||||
--vot-voice-active-live-bg: rgba(224, 64, 160, 0.1);
|
||||
|
||||
--vot-voice-active-live-fg: #e040a0;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
z-index: 2147483647;
|
||||
display: block;
|
||||
|
||||
background: var(--vot-helper-surface);
|
||||
|
||||
border: 1px solid var(--vot-border-color) !important;
|
||||
|
||||
border-radius: var(--vot-radius-m) !important;
|
||||
|
||||
box-shadow: var(--vot-shadow-2) !important;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
min-width: 230px;
|
||||
max-width: var(--vot-voice-popover-max-width, 310px);
|
||||
max-height: var(--vot-voice-popover-max-height, calc(100vh - 16px));
|
||||
width: max-content;
|
||||
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
transform-origin: top left;
|
||||
|
||||
@include shared.vot-font-family;
|
||||
text-align: left !important;
|
||||
|
||||
&,
|
||||
& * {
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transform: none;
|
||||
|
||||
visibility: hidden;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
transition: none;
|
||||
|
||||
&[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&:not([hidden]),
|
||||
&.is-open,
|
||||
&[aria-hidden="false"] {
|
||||
opacity: 1;
|
||||
|
||||
transform: none;
|
||||
|
||||
visibility: visible;
|
||||
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&[data-placement="top"] {
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
&[data-placement="bottom"] {
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
&[data-placement="left"] {
|
||||
transform-origin: right center;
|
||||
}
|
||||
|
||||
&[data-placement="right"] {
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
&.is-closing {
|
||||
opacity: 0;
|
||||
|
||||
transform: none;
|
||||
|
||||
visibility: visible;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
padding: 14px 44px 14px 16px !important;
|
||||
min-height: 62px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
color: var(--vot-helper-onsurface);
|
||||
|
||||
outline: none;
|
||||
|
||||
transition: none;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(var(--vot-helper-onsurface-rgb), 0.045),
|
||||
rgba(var(--vot-helper-onsurface-rgb), 0.06)
|
||||
);
|
||||
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.03),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
&:hover::before,
|
||||
&:focus-visible::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: inset 0 0 0 1px
|
||||
rgba(var(--vot-primary-rgb, 139, 180, 245), 0.18);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
|
||||
right: 14px;
|
||||
top: 50%;
|
||||
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transform: translateY(-50%) scale(0.88);
|
||||
|
||||
transition: none;
|
||||
|
||||
background-color: currentColor;
|
||||
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E")
|
||||
center / contain no-repeat;
|
||||
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
|
||||
&--active {
|
||||
font-weight: 500 !important;
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__item[data-voice="standard"].vot-voice-popover__item--active {
|
||||
background-color: var(--vot-voice-active-standard-bg);
|
||||
|
||||
color: var(--vot-voice-active-standard-fg);
|
||||
|
||||
.vot-voice-popover__item-title {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.vot-voice-icon--standard .vot-eq-bar {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
&__item[data-voice="live"].vot-voice-popover__item--active {
|
||||
background-color: var(--vot-voice-active-live-bg);
|
||||
|
||||
color: var(--vot-voice-active-live-fg);
|
||||
|
||||
.vot-voice-popover__item-title {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&__item-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&__item-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 2px;
|
||||
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__item-title {
|
||||
font-size: 15px;
|
||||
|
||||
font-weight: 400 !important;
|
||||
|
||||
line-height: 1.3;
|
||||
|
||||
color: inherit;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&__item-subtitle {
|
||||
font-size: 12px;
|
||||
|
||||
line-height: 1.4;
|
||||
|
||||
color: var(--vot-helper-onsurface-secondary);
|
||||
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
height: 1px;
|
||||
|
||||
background: var(--vot-border-color);
|
||||
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.vot-voice-icon .vot-eq-bar {
|
||||
animation: none !important;
|
||||
transform: scaleY(1) !important;
|
||||
}
|
||||
|
||||
.vot-voice-popover,
|
||||
.vot-voice-popover *,
|
||||
.vot-voice-popover::before,
|
||||
.vot-voice-popover::after {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.vot-voice-popover {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.vot-voice-popover.is-closing,
|
||||
.vot-voice-popover:not([hidden]),
|
||||
.vot-voice-popover.is-open,
|
||||
.vot-voice-popover[aria-hidden="false"] {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,67 @@
|
|||
@use "../Button/DetailsButton.scss";
|
||||
@use "../../styles/components/settings.scss";
|
||||
.vot-settings-section {
|
||||
border: 1px solid var(--vot-border-color);
|
||||
border-radius: var(--vot-radius-l);
|
||||
padding: var(--vot-space-2);
|
||||
background: rgba(var(--vot-helper-onsurface-rgb), 0.03);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// Avoid `gap` for predictable layout across host pages.
|
||||
& > * {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
// Spacing between header and content.
|
||||
& > * + * {
|
||||
margin-top: var(--vot-space-2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.vot-settings-section__header {
|
||||
margin: 0 !important;
|
||||
padding: 0.45em 0.5em !important;
|
||||
border-radius: var(--vot-radius-m);
|
||||
}
|
||||
|
||||
.vot-settings-section__header .vot-details-arrow-icon {
|
||||
transition: transform var(--vot-duration-medium) var(--vot-easing-standard);
|
||||
}
|
||||
|
||||
.vot-settings-section__header[data-open="true"] .vot-details-arrow-icon {
|
||||
transform: scale(1.25) rotate(0deg);
|
||||
}
|
||||
|
||||
.vot-settings-section__content {
|
||||
// Shared grid variables for "settings rows".
|
||||
// Components (select/slider) read these to align their right column.
|
||||
// Controls are no longer pushed to the far right edge of the dialog.
|
||||
// We still use a "comfort" width so dropdown pills are consistent.
|
||||
--vot-settings-control-width: 200px;
|
||||
// Slightly tighter vertical rhythm for long settings pages.
|
||||
--vot-settings-row-gap: var(--vot-space-2);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--vot-settings-row-gap);
|
||||
padding: 0 var(--vot-space-1) var(--vot-space-1);
|
||||
|
||||
& > * {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
// Consistent padding + full-row hit area.
|
||||
// IMPORTANT: never override component `display` values here. Some controls
|
||||
// (hotkey, select, slider) rely on flex/grid internally.
|
||||
// Apply only to *direct* children.
|
||||
// Some rows are composite controls (e.g. Select/Slider with a checkbox as
|
||||
// the label element). Styling nested `.vot-switch` would add double
|
||||
// padding and break alignment.
|
||||
& > .vot-switch,
|
||||
& > .vot-hotkey,
|
||||
& > .vot-select,
|
||||
& > .vot-slider-wrapper {
|
||||
width: 100% !important;
|
||||
padding: var(--vot-space-1);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ export default meta;
|
|||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const SectionChildren = () => (
|
||||
<p>
|
||||
<vot-block>
|
||||
Hello <b>World</b>!
|
||||
</p>
|
||||
</vot-block>
|
||||
);
|
||||
|
||||
export const SettingsSectionDefault: Story = {
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ export function SettingsTranslationSection(
|
|||
>
|
||||
{localizationProvider.get("VOTTranslationTextService")}
|
||||
<br />
|
||||
<vot-block class="vot-select_new-label__description">
|
||||
<vot-block class="vot-select-label__description">
|
||||
{localizationProvider.get("VOTNotAffectToVoice")}
|
||||
</vot-block>
|
||||
</Select>
|
||||
|
|
|
|||
|
|
@ -37,9 +37,11 @@ function SubtitleTokenInfo(props: {
|
|||
.replace("{0}", props.translationService)}
|
||||
</vot-block>
|
||||
<vot-block class="vot-subtitles-info-title">
|
||||
<span class="vot-subtitles-info-source">{props.source}</span>
|
||||
<span class="vot-subtitles-info-divider">—</span>
|
||||
<span class="vot-subtitles-info-header">{props.header()}</span>
|
||||
<vot-block class="vot-subtitles-info-source">{props.source}</vot-block>
|
||||
<vot-block class="vot-subtitles-info-divider">—</vot-block>
|
||||
<vot-block class="vot-subtitles-info-header">
|
||||
{props.header()}
|
||||
</vot-block>
|
||||
</vot-block>
|
||||
<vot-block class="vot-subtitles-info-context">
|
||||
{props.context()}
|
||||
|
|
|
|||
|
|
@ -1 +1,229 @@
|
|||
@use "../../styles/components/textfield.scss";
|
||||
.vot-textfield {
|
||||
--vot-helper-theme: rgb(
|
||||
var(--vot-theme-rgb, var(--vot-primary-rgb, 33, 150, 243))
|
||||
) !important;
|
||||
--vot-helper-safari1: rgba(
|
||||
var(--vot-onsurface-rgb, 0, 0, 0),
|
||||
0.38
|
||||
) !important;
|
||||
--vot-helper-safari2: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.6) !important;
|
||||
--vot-helper-safari3: rgba(
|
||||
var(--vot-onsurface-rgb, 0, 0, 0),
|
||||
0.87
|
||||
) !important;
|
||||
position: relative !important;
|
||||
display: inline-block;
|
||||
padding-top: 6px !important;
|
||||
font-family: var(
|
||||
--vot-font-family,
|
||||
"Roboto",
|
||||
"Segoe UI",
|
||||
system-ui,
|
||||
sans-serif
|
||||
) !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.5 !important;
|
||||
text-align: start !important;
|
||||
|
||||
& > :is(input, textarea) {
|
||||
box-sizing: border-box !important;
|
||||
margin: 0 !important;
|
||||
border-style: solid !important;
|
||||
border-width: 1px !important;
|
||||
border-color: transparent var(--vot-helper-safari2)
|
||||
var(--vot-helper-safari2) !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 15px 13px 15px !important;
|
||||
width: 100% !important;
|
||||
height: inherit !important;
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.87) !important;
|
||||
-webkit-text-fill-color: currentColor !important; /* Safari */
|
||||
background-color: transparent !important;
|
||||
box-shadow:
|
||||
inset 1px 0 transparent,
|
||||
inset -1px 0 transparent,
|
||||
inset 0 -1px transparent !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
line-height: inherit !important;
|
||||
caret-color: var(--vot-helper-theme) !important;
|
||||
transition:
|
||||
border 0.2s,
|
||||
box-shadow 0.2s !important;
|
||||
|
||||
&:not(:focus):not(
|
||||
:is(.vot-show-placeholder, .vot-show-placeholer)
|
||||
)::placeholder {
|
||||
color: transparent !important;
|
||||
}
|
||||
|
||||
&:not(:focus) {
|
||||
&:placeholder-shown {
|
||||
border-top-color: var(--vot-helper-safari2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
& + .vot-textfield__label {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
display: flex !important;
|
||||
width: 100% !important;
|
||||
max-height: 100% !important;
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.6) !important;
|
||||
font-size: 75% !important;
|
||||
font-family: inherit;
|
||||
line-height: 15px !important;
|
||||
cursor: text !important;
|
||||
transition:
|
||||
color 0.2s,
|
||||
font-size 0.2s,
|
||||
line-height 0.2s !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
&:not(:focus):placeholder-shown + .vot-textfield__label {
|
||||
font-size: inherit !important;
|
||||
line-height: 68px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Corners */
|
||||
& > input,
|
||||
& > textarea {
|
||||
& + .vot-textfield__label::before,
|
||||
& + .vot-textfield__label::after {
|
||||
content: "" !important;
|
||||
display: block !important;
|
||||
box-sizing: border-box !important;
|
||||
margin-top: 6px !important;
|
||||
border-top: solid 1px var(--vot-helper-safari2) !important;
|
||||
min-width: 10px !important;
|
||||
height: 8px !important;
|
||||
pointer-events: none !important;
|
||||
box-shadow: inset 0 1px transparent !important;
|
||||
transition:
|
||||
border 0.2s,
|
||||
box-shadow 0.2s !important;
|
||||
}
|
||||
|
||||
& + .vot-textfield__label::before {
|
||||
margin-right: 4px !important;
|
||||
border-left: solid 1px transparent !important;
|
||||
border-radius: 4px 0 !important;
|
||||
}
|
||||
|
||||
& + .vot-textfield__label::after {
|
||||
flex-grow: 1 !important;
|
||||
margin-left: 4px !important;
|
||||
border-right: solid 1px transparent !important;
|
||||
border-radius: 0 4px !important;
|
||||
}
|
||||
|
||||
&:is(.vot-show-placeholder, .vot-show-placeholer) + .vot-textfield__label {
|
||||
&::before {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:is(:not(:focus):placeholder-shown) {
|
||||
& + .vot-textfield__label::before,
|
||||
& + .vot-textfield__label::after {
|
||||
border-top-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > textarea {
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
& > input:not(:disabled),
|
||||
& > textarea:not(:disabled) {
|
||||
border-color: transparent var(--vot-helper-safari3)
|
||||
var(--vot-helper-safari3) !important;
|
||||
|
||||
& + .vot-textfield__label::before,
|
||||
& + .vot-textfield__label::after {
|
||||
border-top-color: var(--vot-helper-safari3) !important;
|
||||
}
|
||||
|
||||
&:not(:focus):placeholder-shown {
|
||||
border-color: var(--vot-helper-safari3) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > input:focus,
|
||||
& > textarea:focus {
|
||||
border-color: transparent var(--vot-helper-theme) var(--vot-helper-theme) !important;
|
||||
box-shadow:
|
||||
inset 1px 0 var(--vot-helper-theme),
|
||||
inset -1px 0 var(--vot-helper-theme),
|
||||
inset 0 -1px var(--vot-helper-theme) !important;
|
||||
outline: none !important;
|
||||
|
||||
& + .vot-textfield__label {
|
||||
color: var(--vot-helper-theme) !important;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
border-top-color: var(--vot-helper-theme) !important;
|
||||
box-shadow: inset 0 1px var(--vot-helper-theme) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > input:disabled,
|
||||
& > textarea:disabled {
|
||||
&,
|
||||
& + .vot-textfield__label {
|
||||
border-color: transparent var(--vot-helper-safari1)
|
||||
var(--vot-helper-safari1) !important;
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38) !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
& + .vot-textfield__label {
|
||||
&::before,
|
||||
&::after {
|
||||
border-top-color: var(--vot-helper-safari1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:placeholder-shown {
|
||||
&,
|
||||
& + .vot-textfield__label {
|
||||
border-top-color: var(--vot-helper-safari1) !important;
|
||||
}
|
||||
|
||||
& + .vot-textfield__label {
|
||||
&::before,
|
||||
&::after {
|
||||
border-top-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Faster transition in Safari for less noticable fractional font-size issue */
|
||||
@media not all and (min-resolution: 0.001dpcm) {
|
||||
@supports (-webkit-appearance: none) {
|
||||
.vot-textfield > input,
|
||||
.vot-textfield > input + .vot-textfield__label,
|
||||
.vot-textfield > textarea,
|
||||
.vot-textfield > textarea + .vot-textfield__label,
|
||||
.vot-textfield > input + .vot-textfield__label::before,
|
||||
.vot-textfield > input + .vot-textfield__label::after,
|
||||
.vot-textfield > textarea + .vot-textfield__label::before,
|
||||
.vot-textfield > textarea + .vot-textfield__label::after {
|
||||
transition-duration: 0.1s !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,9 +77,13 @@ export function Textfield(props: TextfieldProps): JSX.Element {
|
|||
<Show when={finalProps.multiline} fallback={<input {...common} />}>
|
||||
<textarea {...common} />
|
||||
</Show>
|
||||
<span ref={finalProps.labelRef} id={labelId}>
|
||||
<vot-block
|
||||
ref={finalProps.labelRef}
|
||||
id={labelId}
|
||||
class="vot-textfield__label"
|
||||
>
|
||||
{finalProps.labelText}
|
||||
</span>
|
||||
</vot-block>
|
||||
</vot-block>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@use "../../styles/mixins" as shared;
|
||||
@use "../mixins" as shared;
|
||||
|
||||
.vot-menu {
|
||||
--vot-helper-surface-rgb: var(--vot-surface-rgb, 255, 255, 255);
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { Menu } from "./Menu";
|
|||
const meta = {
|
||||
component: Menu,
|
||||
render: (args) => (
|
||||
<div style="display: flex;background: gray;padding: 20px 200px 200px;">
|
||||
<vot-block style="display: flex;background: gray;padding: 20px 200px 200px;">
|
||||
<Menu {...args} />
|
||||
</div>
|
||||
</vot-block>
|
||||
),
|
||||
} satisfies Meta<typeof Menu>;
|
||||
|
||||
|
|
@ -17,14 +17,14 @@ type Story = StoryObj<typeof meta>;
|
|||
export const MenuDefault: Story = {
|
||||
args: {
|
||||
title: "Settings",
|
||||
children: <div>hello world</div>,
|
||||
children: <vot-block>hello world</vot-block>,
|
||||
},
|
||||
};
|
||||
|
||||
export const MenuWithHeaderAndFooter: Story = {
|
||||
args: {
|
||||
title: "Settings",
|
||||
children: <div>hello world</div>,
|
||||
children: <vot-block>hello world</vot-block>,
|
||||
headerChildren: "header",
|
||||
footerChildren: "footer",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ type Story = StoryObj<typeof meta>;
|
|||
|
||||
export const OrBlockDefault: Story = {
|
||||
args: {
|
||||
children: <p>hello world</p>,
|
||||
children: <vot-block>hello world</vot-block>,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@use "../../styles/mixins" as shared;
|
||||
@use "../mixins" as shared;
|
||||
|
||||
.vot-overlay {
|
||||
--vot-overlay-default-top: 0;
|
||||
|
|
|
|||
7
src/components/componentShared.ts
Normal file
7
src/components/componentShared.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export function isPrimaryPointerAction(event: PointerEvent): boolean {
|
||||
return event.isPrimary && event.button === 0;
|
||||
}
|
||||
|
||||
export function isKeyboardActivation(event: KeyboardEvent): boolean {
|
||||
return event.key === "Enter" || event.key === " ";
|
||||
}
|
||||
|
|
@ -88,6 +88,56 @@ vot-block {
|
|||
}
|
||||
|
||||
// Keep host-page typography rules from overriding VOT font weights.
|
||||
vot-block * {
|
||||
font-weight: inherit !important;
|
||||
}
|
||||
vot-block * {
|
||||
font-weight: inherit !important;
|
||||
}
|
||||
|
||||
.vot-portal-local,
|
||||
.vot-subtitles-widget {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
vot-block:focus,
|
||||
vot-block *:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
html.vot-keyboard-nav vot-block:focus-visible,
|
||||
html.vot-keyboard-nav vot-block *:focus-visible {
|
||||
box-shadow: var(--vot-focus-ring), var(--vot-focus-ring-offset) !important;
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
html.vot-keyboard-nav vot-block:focus,
|
||||
html.vot-keyboard-nav vot-block *:focus {
|
||||
box-shadow: var(--vot-focus-ring), var(--vot-focus-ring-offset) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.vot-portal-local *,
|
||||
.vot-portal *,
|
||||
.vot-subtitles-widget * {
|
||||
transition-duration: 0.001ms !important;
|
||||
animation-duration: 0.001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.vot-portal {
|
||||
display: contents;
|
||||
|
||||
&-local {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2147483647;
|
||||
}
|
||||
}
|
||||
|
||||
.vot-segmented-button,
|
||||
.vot-menu {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { OverlayViewControls } from "../views/OverlayView";
|
||||
import type { OverlayViewControls } from "../components/OverlayView/OverlayView";
|
||||
|
||||
export type LifecycleTranslationResetHost = {
|
||||
videoData?: unknown;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import type { VideoDataSubtitle } from "@vot.js/core/types/client";
|
||||
import type { ServiceConf, VideoService } from "@vot.js/ext/types/service";
|
||||
import type { RequestLang, ResponseLang } from "@vot.js/shared/types/data";
|
||||
|
||||
import type { OverlayViewControls } from "../components/OverlayView/OverlayView";
|
||||
import type { StorageData } from "../types/storage";
|
||||
import debug from "../utils/debug";
|
||||
import { containsCrossShadow } from "../utils/dom";
|
||||
import type { VideoData } from "../videoHandler/shared";
|
||||
import type { OverlayViewControls } from "../views/OverlayView";
|
||||
import { findConnectedContainerBySelector } from "./containerResolution";
|
||||
import { hideLifecycleOverlay, resetAndHideLifecycle } from "./lifecycleShared";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
@use "./mixins" as button;
|
||||
|
||||
.vot-button {
|
||||
--vot-helper-theme: var(
|
||||
--vot-theme-rgb,
|
||||
var(--vot-primary-rgb, 33, 150, 243)
|
||||
);
|
||||
--vot-helper-ontheme: var(
|
||||
--vot-ontheme-rgb,
|
||||
var(--vot-onprimary-rgb, 255, 255, 255)
|
||||
);
|
||||
@include button.vot-button-base(
|
||||
$radius: var(--vot-radius-s),
|
||||
$padding: 0 var(--vot-space-4),
|
||||
$min-width: 64px,
|
||||
$height: 36px,
|
||||
$line-height: 36px
|
||||
);
|
||||
border: none !important;
|
||||
color: rgb(var(--vot-helper-ontheme));
|
||||
background-color: rgb(var(--vot-helper-theme));
|
||||
box-shadow: var(--vot-shadow-1);
|
||||
transition: box-shadow var(--vot-duration-medium) var(--vot-easing-standard);
|
||||
@include button.vot-button-layers(rgb(var(--vot-helper-ontheme)));
|
||||
@include button.vot-button-layer-hover(0.08);
|
||||
@include button.vot-button-layer-active(0.32);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--vot-shadow-2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: var(--vot-shadow-2);
|
||||
}
|
||||
|
||||
&[disabled="true"] {
|
||||
background-color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.12);
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
box-shadow: none;
|
||||
cursor: initial;
|
||||
@include button.vot-button-layer-reset-disabled;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
@use "./mixins" as button;
|
||||
|
||||
.vot-icon-button {
|
||||
--vot-helper-onsurface: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.87);
|
||||
@include button.vot-button-base(
|
||||
$radius: 50%,
|
||||
$padding: 0,
|
||||
$min-width: 36px,
|
||||
$height: 36px,
|
||||
$line-height: 36px,
|
||||
$margin: 0 !important
|
||||
);
|
||||
border: none !important;
|
||||
width: 36px;
|
||||
fill: var(--vot-helper-onsurface);
|
||||
color: var(--vot-helper-onsurface);
|
||||
background-color: transparent;
|
||||
@include button.vot-button-layers(var(--vot-helper-onsurface));
|
||||
@include button.vot-button-layer-hover(0.04);
|
||||
@include button.vot-button-layer-active(0.32);
|
||||
&[disabled="true"],
|
||||
&[aria-disabled="true"] {
|
||||
background-color: transparent;
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
fill: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
cursor: initial;
|
||||
@include button.vot-button-layer-reset-disabled;
|
||||
}
|
||||
|
||||
& svg {
|
||||
fill: inherit;
|
||||
stroke: inherit;
|
||||
width: 24px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
@use "./mixins" as button;
|
||||
|
||||
.vot-outlined-button {
|
||||
--vot-helper-theme: var(
|
||||
--vot-theme-rgb,
|
||||
var(--vot-primary-rgb, 33, 150, 243)
|
||||
);
|
||||
@include button.vot-button-base(
|
||||
$radius: var(--vot-radius-s),
|
||||
$padding: 0 var(--vot-space-4),
|
||||
$min-width: 64px,
|
||||
$height: 36px,
|
||||
$line-height: 34px,
|
||||
$margin: 0 !important
|
||||
);
|
||||
border: solid 1px var(--vot-border-color) !important;
|
||||
color: rgb(var(--vot-helper-theme));
|
||||
background-color: transparent;
|
||||
@include button.vot-button-layers(rgb(var(--vot-helper-theme)));
|
||||
@include button.vot-button-layer-hover(0.04);
|
||||
@include button.vot-button-layer-active(0.16);
|
||||
|
||||
&[disabled="true"],
|
||||
&[aria-disabled="true"] {
|
||||
background-color: transparent;
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
cursor: initial;
|
||||
@include button.vot-button-layer-reset-disabled;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
@use "./mixins" as button;
|
||||
|
||||
.vot-text-button {
|
||||
--vot-helper-theme: var(
|
||||
--vot-theme-rgb,
|
||||
var(--vot-primary-rgb, 33, 150, 243)
|
||||
);
|
||||
@include button.vot-button-base(
|
||||
$radius: var(--vot-radius-s),
|
||||
$padding: 0 var(--vot-space-2),
|
||||
$min-width: 64px,
|
||||
$height: 36px,
|
||||
$line-height: 36px,
|
||||
$margin: 0 !important
|
||||
);
|
||||
border: none !important;
|
||||
color: rgb(var(--vot-helper-theme));
|
||||
background-color: transparent;
|
||||
@include button.vot-button-layers(rgb(var(--vot-helper-theme)));
|
||||
@include button.vot-button-layer-hover(0.04);
|
||||
@include button.vot-button-layer-active(0.16);
|
||||
|
||||
&[disabled="true"],
|
||||
&[aria-disabled="true"] {
|
||||
background-color: transparent;
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
cursor: initial;
|
||||
@include button.vot-button-layer-reset-disabled;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
@use "../mixins" as shared;
|
||||
|
||||
.vot-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: rgba(var(--vot-onsurface-rgb), 0.87);
|
||||
@include shared.vot-font-family;
|
||||
font-size: 16px;
|
||||
border-radius: 0.5em !important;
|
||||
padding: 0.5em !important;
|
||||
margin: -0.5em !important;
|
||||
line-height: 1.5;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background var(--vot-duration-medium) var(--vot-easing-standard);
|
||||
|
||||
&-arrow-icon {
|
||||
width: 20px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: scale(1.25) rotate(-90deg);
|
||||
fill: rgba(var(--vot-onsurface-rgb), 0.87);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&:not([aria-disabled="true"]):hover {
|
||||
background: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.06);
|
||||
}
|
||||
|
||||
&[aria-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.38;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
.vot-settings-section {
|
||||
border: 1px solid var(--vot-border-color);
|
||||
border-radius: var(--vot-radius-l);
|
||||
padding: var(--vot-space-2);
|
||||
background: rgba(var(--vot-helper-onsurface-rgb), 0.03);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// Avoid `gap` for predictable layout across host pages.
|
||||
& > * {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
// Spacing between header and content.
|
||||
& > * + * {
|
||||
margin-top: var(--vot-space-2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.vot-settings-section-header,
|
||||
.vot-settings-section__header {
|
||||
margin: 0 !important;
|
||||
padding: 0.45em 0.5em !important;
|
||||
border-radius: var(--vot-radius-m);
|
||||
}
|
||||
|
||||
.vot-settings-section-header .vot-details-arrow-icon,
|
||||
.vot-settings-section__header .vot-details-arrow-icon {
|
||||
transition: transform var(--vot-duration-medium) var(--vot-easing-standard);
|
||||
}
|
||||
|
||||
.vot-settings-section-header[data-open="true"] .vot-details-arrow-icon,
|
||||
.vot-settings-section__header[data-open="true"] .vot-details-arrow-icon {
|
||||
transform: scale(1.25) rotate(0deg);
|
||||
}
|
||||
|
||||
.vot-settings-section-content,
|
||||
.vot-settings-section__content {
|
||||
// Shared grid variables for "settings rows".
|
||||
// Components (select/slider) read these to align their right column.
|
||||
// Controls are no longer pushed to the far right edge of the dialog.
|
||||
// We still use a "comfort" width so dropdown pills are consistent.
|
||||
--vot-settings-control-width: 200px;
|
||||
// Slightly tighter vertical rhythm for long settings pages.
|
||||
--vot-settings-row-gap: var(--vot-space-2);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--vot-settings-row-gap);
|
||||
padding: 0 var(--vot-space-1) var(--vot-space-1);
|
||||
|
||||
& > * {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
// Consistent padding + full-row hit area.
|
||||
// IMPORTANT: never override component `display` values here. Some controls
|
||||
// (hotkey, select, slider) rely on flex/grid internally.
|
||||
// Apply only to *direct* children.
|
||||
// Some rows are composite controls (e.g. Select/Slider with a checkbox as
|
||||
// the label element). Styling nested `.vot-checkbox` would add double
|
||||
// padding and break alignment.
|
||||
& > .vot-checkbox,
|
||||
& > .vot-hotkey,
|
||||
& > .vot-select,
|
||||
& > .vot-slider {
|
||||
width: 100% !important;
|
||||
padding: var(--vot-space-1);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
.vot-textfield {
|
||||
--vot-helper-theme: rgb(
|
||||
var(--vot-theme-rgb, var(--vot-primary-rgb, 33, 150, 243))
|
||||
) !important;
|
||||
--vot-helper-safari1: rgba(
|
||||
var(--vot-onsurface-rgb, 0, 0, 0),
|
||||
0.38
|
||||
) !important;
|
||||
--vot-helper-safari2: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.6) !important;
|
||||
--vot-helper-safari3: rgba(
|
||||
var(--vot-onsurface-rgb, 0, 0, 0),
|
||||
0.87
|
||||
) !important;
|
||||
position: relative !important;
|
||||
display: inline-block;
|
||||
padding-top: 6px !important;
|
||||
font-family: var(
|
||||
--vot-font-family,
|
||||
"Roboto",
|
||||
"Segoe UI",
|
||||
system-ui,
|
||||
sans-serif
|
||||
) !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.5 !important;
|
||||
text-align: start !important;
|
||||
|
||||
& > :is(input, textarea) {
|
||||
box-sizing: border-box !important;
|
||||
margin: 0 !important;
|
||||
border-style: solid !important;
|
||||
border-width: 1px !important;
|
||||
border-color: transparent var(--vot-helper-safari2)
|
||||
var(--vot-helper-safari2) !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 15px 13px 15px !important;
|
||||
width: 100% !important;
|
||||
height: inherit !important;
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.87) !important;
|
||||
-webkit-text-fill-color: currentColor !important; /* Safari */
|
||||
background-color: transparent !important;
|
||||
box-shadow:
|
||||
inset 1px 0 transparent,
|
||||
inset -1px 0 transparent,
|
||||
inset 0 -1px transparent !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
line-height: inherit !important;
|
||||
caret-color: var(--vot-helper-theme) !important;
|
||||
transition:
|
||||
border 0.2s,
|
||||
box-shadow 0.2s !important;
|
||||
|
||||
&:not(:focus):not(
|
||||
:is(.vot-show-placeholder, .vot-show-placeholer)
|
||||
)::placeholder {
|
||||
color: transparent !important;
|
||||
}
|
||||
|
||||
&:not(:focus) {
|
||||
&:placeholder-shown {
|
||||
border-top-color: var(--vot-helper-safari2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
& + span {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
display: flex !important;
|
||||
width: 100% !important;
|
||||
max-height: 100% !important;
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.6) !important;
|
||||
font-size: 75% !important;
|
||||
font-family: inherit;
|
||||
line-height: 15px !important;
|
||||
cursor: text !important;
|
||||
transition:
|
||||
color 0.2s,
|
||||
font-size 0.2s,
|
||||
line-height 0.2s !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
&:not(:focus):placeholder-shown + span {
|
||||
font-size: inherit !important;
|
||||
line-height: 68px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Corners */
|
||||
& > input,
|
||||
& > textarea {
|
||||
& + span::before,
|
||||
& + span::after {
|
||||
content: "" !important;
|
||||
display: block !important;
|
||||
box-sizing: border-box !important;
|
||||
margin-top: 6px !important;
|
||||
border-top: solid 1px var(--vot-helper-safari2) !important;
|
||||
min-width: 10px !important;
|
||||
height: 8px !important;
|
||||
pointer-events: none !important;
|
||||
box-shadow: inset 0 1px transparent !important;
|
||||
transition:
|
||||
border 0.2s,
|
||||
box-shadow 0.2s !important;
|
||||
}
|
||||
|
||||
& + span::before {
|
||||
margin-right: 4px !important;
|
||||
border-left: solid 1px transparent !important;
|
||||
border-radius: 4px 0 !important;
|
||||
}
|
||||
|
||||
& + span::after {
|
||||
flex-grow: 1 !important;
|
||||
margin-left: 4px !important;
|
||||
border-right: solid 1px transparent !important;
|
||||
border-radius: 0 4px !important;
|
||||
}
|
||||
|
||||
&:is(.vot-show-placeholder, .vot-show-placeholer) + span {
|
||||
&::before {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:is(:not(:focus):placeholder-shown) {
|
||||
& + span::before,
|
||||
& + span::after {
|
||||
border-top-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > textarea {
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
& > input:not(:disabled),
|
||||
& > textarea:not(:disabled) {
|
||||
border-color: transparent var(--vot-helper-safari3)
|
||||
var(--vot-helper-safari3) !important;
|
||||
|
||||
& + span::before,
|
||||
& + span::after {
|
||||
border-top-color: var(--vot-helper-safari3) !important;
|
||||
}
|
||||
|
||||
&:not(:focus):placeholder-shown {
|
||||
border-color: var(--vot-helper-safari3) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > input:focus,
|
||||
& > textarea:focus {
|
||||
border-color: transparent var(--vot-helper-theme) var(--vot-helper-theme) !important;
|
||||
box-shadow:
|
||||
inset 1px 0 var(--vot-helper-theme),
|
||||
inset -1px 0 var(--vot-helper-theme),
|
||||
inset 0 -1px var(--vot-helper-theme) !important;
|
||||
outline: none !important;
|
||||
|
||||
& + span {
|
||||
color: var(--vot-helper-theme) !important;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
border-top-color: var(--vot-helper-theme) !important;
|
||||
box-shadow: inset 0 1px var(--vot-helper-theme) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > input:disabled,
|
||||
& > textarea:disabled {
|
||||
&,
|
||||
& + span {
|
||||
border-color: transparent var(--vot-helper-safari1)
|
||||
var(--vot-helper-safari1) !important;
|
||||
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38) !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
& + span {
|
||||
&::before,
|
||||
&::after {
|
||||
border-top-color: var(--vot-helper-safari1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:placeholder-shown {
|
||||
&,
|
||||
& + span {
|
||||
border-top-color: var(--vot-helper-safari1) !important;
|
||||
}
|
||||
|
||||
& + span {
|
||||
&::before,
|
||||
&::after {
|
||||
border-top-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Faster transition in Safari for less noticable fractional font-size issue */
|
||||
@media not all and (min-resolution: 0.001dpcm) {
|
||||
@supports (-webkit-appearance: none) {
|
||||
.vot-textfield > input,
|
||||
.vot-textfield > input + span,
|
||||
.vot-textfield > textarea,
|
||||
.vot-textfield > textarea + span,
|
||||
.vot-textfield > input + span::before,
|
||||
.vot-textfield > input + span::after,
|
||||
.vot-textfield > textarea + span::before,
|
||||
.vot-textfield > textarea + span::after {
|
||||
transition-duration: 0.1s !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
@use "../mixins" as shared;
|
||||
|
||||
.vot-tooltip {
|
||||
--vot-helper-theme-rgb: var(--vot-onsurface-rgb, 0, 0, 0);
|
||||
--vot-helper-theme: rgba(var(--vot-helper-theme-rgb), 0.87);
|
||||
--vot-helper-ondialog: rgb(var(--vot-ondialog-rgb, 37, 38, 40));
|
||||
--vot-helper-border: rgb(var(--vot-tooltip-border, 69, 69, 69));
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgb(var(--vot-surface-rgb, 255, 255, 255));
|
||||
color: var(--vot-helper-theme);
|
||||
fill: var(--vot-helper-theme);
|
||||
border-radius: 4px !important;
|
||||
@include shared.vot-font-family;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
cursor: default;
|
||||
transition: opacity 0.5s;
|
||||
position: absolute;
|
||||
z-index: 2147483647;
|
||||
inset: 0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||
width: max-content;
|
||||
max-width: calc(100vw - 10px);
|
||||
height: max-content;
|
||||
padding: 4px 8px !important;
|
||||
opacity: 0;
|
||||
|
||||
&[data-trigger="click"] {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
&[data-mode="follow"] {
|
||||
align-items: stretch;
|
||||
pointer-events: auto;
|
||||
user-select: text;
|
||||
|
||||
&,
|
||||
& * {
|
||||
user-select: text !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.vot-tooltip-bordered {
|
||||
border: 1px solid var(--vot-helper-border);
|
||||
}
|
||||
|
||||
& * {
|
||||
box-sizing: border-box !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
&.vot-tooltip--subtitles-info {
|
||||
padding: 0 !important;
|
||||
overflow: visible;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 18px !important;
|
||||
|
||||
.vot-subtitles-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: max-content;
|
||||
max-width: min(420px, calc(100vw - 24px));
|
||||
padding: 14px 16px !important;
|
||||
border-radius: 16px !important;
|
||||
background: rgba(31, 32, 36, 0.96) !important;
|
||||
color: rgba(255, 255, 255, 0.92) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
box-shadow:
|
||||
0 12px 30px rgba(0, 0, 0, 0.28),
|
||||
0 2px 6px rgba(0, 0, 0, 0.22) !important;
|
||||
font-size: 13px !important;
|
||||
line-height: 1.35 !important;
|
||||
letter-spacing: 0 !important;
|
||||
}
|
||||
|
||||
.vot-subtitles-info-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
font-size: 15px !important;
|
||||
line-height: 1.35 !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.vot-subtitles-info-source {
|
||||
color: rgba(255, 255, 255, 0.94) !important;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.vot-subtitles-info-divider {
|
||||
color: rgba(255, 255, 255, 0.42) !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.vot-subtitles-info-header {
|
||||
color: rgb(var(--vot-primary-rgb, 255, 83, 151)) !important;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.vot-subtitles-info-context {
|
||||
max-width: 100%;
|
||||
color: rgba(255, 255, 255, 0.68) !important;
|
||||
font-size: 13px !important;
|
||||
line-height: 1.45 !important;
|
||||
font-weight: 400 !important;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,319 +0,0 @@
|
|||
@use "../mixins" as shared;
|
||||
|
||||
.vot-voice-icon {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
|
||||
.vot-eq-bar {
|
||||
transform-origin: 50% 100%;
|
||||
transform-box: fill-box;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
&--standard .vot-eq-bar {
|
||||
fill: rgba(var(--vot-onsurface-rgb, 227, 227, 227), 0.4);
|
||||
}
|
||||
|
||||
&--live .vot-eq-bar {
|
||||
fill: #e040a0;
|
||||
}
|
||||
}
|
||||
|
||||
.vot-voice-popover {
|
||||
--vot-helper-surface-rgb: var(--vot-surface-rgb, 32, 33, 36);
|
||||
|
||||
--vot-helper-surface: rgb(var(--vot-helper-surface-rgb));
|
||||
|
||||
--vot-helper-onsurface-rgb: var(--vot-onsurface-rgb, 227, 227, 227);
|
||||
|
||||
--vot-helper-onsurface: rgba(var(--vot-helper-onsurface-rgb), 0.87);
|
||||
|
||||
--vot-helper-onsurface-secondary: rgba(var(--vot-helper-onsurface-rgb), 0.55);
|
||||
|
||||
--vot-voice-active-standard-bg: rgba(
|
||||
var(--vot-primary-rgb, 139, 180, 245),
|
||||
0.1
|
||||
);
|
||||
|
||||
--vot-voice-active-standard-fg: rgb(var(--vot-primary-rgb, 139, 180, 245));
|
||||
|
||||
--vot-voice-active-live-bg: rgba(224, 64, 160, 0.1);
|
||||
|
||||
--vot-voice-active-live-fg: #e040a0;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
z-index: 2147483647;
|
||||
display: block;
|
||||
|
||||
background: var(--vot-helper-surface);
|
||||
|
||||
border: 1px solid var(--vot-border-color) !important;
|
||||
|
||||
border-radius: var(--vot-radius-m) !important;
|
||||
|
||||
box-shadow: var(--vot-shadow-2) !important;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
min-width: 230px;
|
||||
max-width: var(--vot-voice-popover-max-width, 310px);
|
||||
max-height: var(--vot-voice-popover-max-height, calc(100vh - 16px));
|
||||
width: max-content;
|
||||
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
transform-origin: top left;
|
||||
|
||||
@include shared.vot-font-family;
|
||||
text-align: left !important;
|
||||
|
||||
&,
|
||||
& * {
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transform: none;
|
||||
|
||||
visibility: hidden;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
transition: none;
|
||||
|
||||
&[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&:not([hidden]),
|
||||
&.is-open,
|
||||
&[aria-hidden="false"] {
|
||||
opacity: 1;
|
||||
|
||||
transform: none;
|
||||
|
||||
visibility: visible;
|
||||
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&[data-placement="top"] {
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
&[data-placement="bottom"] {
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
&[data-placement="left"] {
|
||||
transform-origin: right center;
|
||||
}
|
||||
|
||||
&[data-placement="right"] {
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
&.is-closing {
|
||||
opacity: 0;
|
||||
|
||||
transform: none;
|
||||
|
||||
visibility: visible;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
padding: 14px 44px 14px 16px !important;
|
||||
min-height: 62px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
color: var(--vot-helper-onsurface);
|
||||
|
||||
outline: none;
|
||||
|
||||
transition: none;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(var(--vot-helper-onsurface-rgb), 0.045),
|
||||
rgba(var(--vot-helper-onsurface-rgb), 0.06)
|
||||
);
|
||||
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.03),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
&:hover::before,
|
||||
&:focus-visible::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: inset 0 0 0 1px
|
||||
rgba(var(--vot-primary-rgb, 139, 180, 245), 0.18);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
|
||||
right: 14px;
|
||||
top: 50%;
|
||||
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transform: translateY(-50%) scale(0.88);
|
||||
|
||||
transition: none;
|
||||
|
||||
background-color: currentColor;
|
||||
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E")
|
||||
center / contain no-repeat;
|
||||
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
|
||||
&--active {
|
||||
font-weight: 500 !important;
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__item[data-voice="standard"].vot-voice-popover__item--active {
|
||||
background-color: var(--vot-voice-active-standard-bg);
|
||||
|
||||
color: var(--vot-voice-active-standard-fg);
|
||||
|
||||
.vot-voice-popover__item-title {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.vot-voice-icon--standard .vot-eq-bar {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
&__item[data-voice="live"].vot-voice-popover__item--active {
|
||||
background-color: var(--vot-voice-active-live-bg);
|
||||
|
||||
color: var(--vot-voice-active-live-fg);
|
||||
|
||||
.vot-voice-popover__item-title {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&__item-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&__item-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 2px;
|
||||
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__item-title {
|
||||
font-size: 15px;
|
||||
|
||||
font-weight: 400 !important;
|
||||
|
||||
line-height: 1.3;
|
||||
|
||||
color: inherit;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&__item-subtitle {
|
||||
font-size: 12px;
|
||||
|
||||
line-height: 1.4;
|
||||
|
||||
color: var(--vot-helper-onsurface-secondary);
|
||||
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
height: 1px;
|
||||
|
||||
background: var(--vot-border-color);
|
||||
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.vot-voice-icon .vot-eq-bar {
|
||||
animation: none !important;
|
||||
transform: scaleY(1) !important;
|
||||
}
|
||||
|
||||
.vot-voice-popover,
|
||||
.vot-voice-popover *,
|
||||
.vot-voice-popover::before,
|
||||
.vot-voice-popover::after {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.vot-voice-popover {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.vot-voice-popover.is-closing,
|
||||
.vot-voice-popover:not([hidden]),
|
||||
.vot-voice-popover.is-open,
|
||||
.vot-voice-popover[aria-hidden="false"] {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
@use "components/buttons/contained.scss";
|
||||
@use "components/buttons/outlined.scss";
|
||||
@use "components/buttons/text.scss";
|
||||
@use "components/buttons/icon.scss";
|
||||
|
||||
@use "components/textfield.scss";
|
||||
@use "components/details.scss";
|
||||
@use "components/settings.scss";
|
||||
@use "components/tooltip.scss";
|
||||
@use "components/voice-popover.scss";
|
||||
|
||||
:root {
|
||||
--vot-font-family: "Roboto", "Segoe UI", system-ui, sans-serif;
|
||||
|
||||
--vot-primary-rgb: 139, 180, 245;
|
||||
--vot-onprimary-rgb: 32, 33, 36;
|
||||
--vot-surface-rgb: 32, 33, 36;
|
||||
--vot-onsurface-rgb: 227, 227, 227;
|
||||
|
||||
--vot-subtitles-color: rgb(var(--vot-onsurface-rgb, 227, 227, 227));
|
||||
--vot-subtitles-passed-color: rgb(var(--vot-primary-rgb, 33, 150, 243));
|
||||
|
||||
/*
|
||||
* UI design tokens (used by menus, dialogs and controls).
|
||||
*
|
||||
* NOTE: Subtitle rendering & styling is out of scope for this sprint.
|
||||
* Keep subtitle variables above unchanged.
|
||||
*/
|
||||
--vot-space-1: 4px;
|
||||
--vot-space-2: 8px;
|
||||
--vot-space-3: 12px;
|
||||
--vot-space-4: 16px;
|
||||
--vot-space-5: 20px;
|
||||
--vot-space-6: 24px;
|
||||
|
||||
--vot-radius-xs: 6px;
|
||||
--vot-radius-s: 10px;
|
||||
--vot-radius-m: 14px;
|
||||
--vot-radius-l: 18px;
|
||||
|
||||
--vot-border-color: rgba(var(--vot-onsurface-rgb, 227, 227, 227), 0.14);
|
||||
--vot-border-color-hover: rgba(var(--vot-onsurface-rgb, 227, 227, 227), 0.22);
|
||||
|
||||
--vot-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.18), 0 8px 24px rgba(0, 0, 0, 0.14);
|
||||
--vot-shadow-2:
|
||||
0 2px 4px rgba(0, 0, 0, 0.22), 0 12px 32px rgba(0, 0, 0, 0.22);
|
||||
|
||||
--vot-duration-fast: 120ms;
|
||||
--vot-duration-medium: 200ms;
|
||||
--vot-duration-slow: 320ms;
|
||||
--vot-easing-standard: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
--vot-focus-ring-color: rgba(var(--vot-primary-rgb, 139, 180, 245), 0.9);
|
||||
--vot-focus-ring: 0 0 0 2px var(--vot-focus-ring-color);
|
||||
--vot-focus-ring-offset: 0 0 0 4px
|
||||
rgba(var(--vot-surface-rgb, 32, 33, 36), 0.9);
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Global baseline for injected UI
|
||||
// ------------------------------
|
||||
|
||||
// Create an explicit stacking / blending boundary for overlays.
|
||||
.vot-portal-local,
|
||||
.vot-subtitles-widget {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Focus handling & interaction polish
|
||||
// ------------------------------
|
||||
//
|
||||
// Requirements:
|
||||
// - No "halo" focus outlines for pointer interactions (mouse/touch).
|
||||
// - Keep a visible focus ring for keyboard navigation (Tab) for accessibility.
|
||||
//
|
||||
// JS toggles `html.vot-keyboard-nav` only when the user presses Tab.
|
||||
// See `src/ui.ts`.
|
||||
|
||||
// Default: suppress native focus outlines/halos.
|
||||
// (We will re-introduce an explicit ring only for keyboard navigation below.)
|
||||
vot-block:focus,
|
||||
vot-block *:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
// Keyboard navigation: show a consistent focus ring.
|
||||
html.vot-keyboard-nav vot-block:focus-visible,
|
||||
html.vot-keyboard-nav vot-block *:focus-visible {
|
||||
box-shadow: var(--vot-focus-ring), var(--vot-focus-ring-offset) !important;
|
||||
}
|
||||
|
||||
// Fallback for browsers without `:focus-visible` support.
|
||||
@supports not selector(:focus-visible) {
|
||||
html.vot-keyboard-nav vot-block:focus,
|
||||
html.vot-keyboard-nav vot-block *:focus {
|
||||
box-shadow: var(--vot-focus-ring), var(--vot-focus-ring-offset) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Respect reduced-motion preference globally.
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.vot-portal-local *,
|
||||
.vot-portal *,
|
||||
.vot-subtitles-widget * {
|
||||
transition-duration: 0.001ms !important;
|
||||
animation-duration: 0.001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.vot-portal {
|
||||
display: contents;
|
||||
|
||||
&-local {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2147483647;
|
||||
}
|
||||
}
|
||||
|
||||
// The overlay mount itself stays click-through so host-page controls remain
|
||||
// usable around the injected UI. Explicitly re-enable hit-testing for the
|
||||
// actual interactive overlay surfaces inside the shadow root.
|
||||
.vot-segmented-button,
|
||||
.vot-menu {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import type { IntervalIdleChecker } from "../../utils/intervalIdleChecker";
|
||||
import type { VideoHandler } from "../../VideoHandler";
|
||||
import type { LanguageSelectKey } from "../components/select";
|
||||
import type { StorageData } from "../storage";
|
||||
import type { OverlayMount } from "../uiManager";
|
||||
|
||||
export type OverlayViewProps = {
|
||||
mount: OverlayMount;
|
||||
data?: Partial<StorageData>;
|
||||
videoHandler?: VideoHandler;
|
||||
intervalIdleChecker: IntervalIdleChecker;
|
||||
};
|
||||
|
||||
export type OverlayViewEventMap = {
|
||||
"click:settings": [];
|
||||
"click:pip": [];
|
||||
"click:subtitles": [];
|
||||
"click:downloadTranslation": [];
|
||||
"click:downloadSubtitles": [];
|
||||
"click:translate": [];
|
||||
"input:videoVolume": [volume: number];
|
||||
"input:translationVolume": [volume: number];
|
||||
"select:fromLanguage": [item: LanguageSelectKey];
|
||||
"select:toLanguage": [item: LanguageSelectKey];
|
||||
"select:subtitles": [item: string];
|
||||
"select:voiceType": [useLive: boolean];
|
||||
};
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import type { LangOverride } from "../../localization/localizationProvider";
|
||||
import type { SubtitleFontFamily } from "../../types/subtitles";
|
||||
import type { VideoHandler } from "../../VideoHandler";
|
||||
import type {
|
||||
Account,
|
||||
ResponseLanguageSubtitles,
|
||||
StorageData,
|
||||
TranslateProxyStatus,
|
||||
} from "../storage";
|
||||
import type { TranslateService } from "../translateApis";
|
||||
|
||||
export type SettingsViewProps = {
|
||||
globalPortal: HTMLElement;
|
||||
data?: Partial<StorageData>;
|
||||
videoHandler?: VideoHandler;
|
||||
};
|
||||
|
||||
export type SettingsViewEventMap = {
|
||||
"click:bugReport": [];
|
||||
"click:resetSettings": [];
|
||||
"update:account": [account: Partial<Account> | undefined];
|
||||
"change:autoTranslate": [checked: boolean];
|
||||
"change:autoSubtitles": [checked: boolean];
|
||||
"change:showVideoVolume": [checked: boolean];
|
||||
"change:audioBooster": [checked: boolean];
|
||||
"change:syncVolume": [checked: boolean];
|
||||
"change:subtitlesHighlightWords": [checked: boolean];
|
||||
"change:subtitlesSmartLayout": [checked: boolean];
|
||||
"select:responseLanguageSubtitles": [item: ResponseLanguageSubtitles];
|
||||
"select:subtitlesFontFamily": [item: SubtitleFontFamily];
|
||||
"change:proxyWorkerHost": [value: string];
|
||||
"change:useNewAudioPlayer": [checked: boolean];
|
||||
"change:onlyBypassMediaCSP": [checked: boolean];
|
||||
"change:showPiPButton": [checked: boolean];
|
||||
"input:subtitlesMaxLength": [value: number];
|
||||
"input:subtitlesFontSize": [value: number];
|
||||
"input:subtitlesBackgroundOpacity": [value: number];
|
||||
"input:autoHideButtonDelay": [value: number];
|
||||
"select:proxyTranslationStatus": [item: TranslateProxyStatus];
|
||||
"select:translationTextService": [item: TranslateService];
|
||||
"select:menuLanguage": [item: LangOverride];
|
||||
};
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
export function createDomId(prefix: string): string {
|
||||
const suffix =
|
||||
typeof crypto !== "undefined" && "randomUUID" in crypto
|
||||
? crypto.randomUUID()
|
||||
: Math.random().toString(36).slice(2);
|
||||
|
||||
return `${prefix}-${suffix}`;
|
||||
}
|
||||
|
||||
export function isEventInside(event: Event, element: HTMLElement): boolean {
|
||||
const target = event.target;
|
||||
if (target instanceof Node && element.contains(target)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
typeof event.composedPath === "function" &&
|
||||
event.composedPath().includes(element)
|
||||
);
|
||||
}
|
||||
|
||||
export function isPrimaryPointerAction(event: PointerEvent): boolean {
|
||||
return event.isPrimary && event.button === 0;
|
||||
}
|
||||
|
||||
export function isKeyboardActivation(event: KeyboardEvent): boolean {
|
||||
return event.key === "Enter" || event.key === " ";
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ import type { VideoHandler } from "../VideoHandler";
|
|||
import type { VideoData } from "../videoHandler/shared";
|
||||
import { safeSetPlayerVolume } from "../videoHandler/translationVolume";
|
||||
import { applyOverlayMountUpdate } from "./mount";
|
||||
import { OverlayController } from "./overlayController";
|
||||
import { SettingsController } from "./settingsController";
|
||||
import {
|
||||
createShadowMount,
|
||||
destroyShadowMount,
|
||||
|
|
@ -25,8 +27,6 @@ import {
|
|||
type ShadowMount,
|
||||
} from "./shadowMount";
|
||||
import { handleTranslationButtonCommand } from "./translationCommands";
|
||||
import { OverlayView } from "./views/overlay";
|
||||
import { SettingsView } from "./views/settings";
|
||||
|
||||
export class UIManager {
|
||||
mount: OverlayMount;
|
||||
|
|
@ -36,16 +36,15 @@ export class UIManager {
|
|||
private readonly intervalIdleChecker: IntervalIdleChecker;
|
||||
data: Partial<StorageData>;
|
||||
|
||||
votGlobalPortal?: HTMLElement;
|
||||
private globalPortalMount?: ShadowMount;
|
||||
/**
|
||||
* Contains all elements over video player e.g. button, menu and etc
|
||||
*/
|
||||
votOverlayView?: OverlayView;
|
||||
votOverlayView?: OverlayController;
|
||||
/**
|
||||
* Dialog settings menu
|
||||
*/
|
||||
votSettingsView?: SettingsView;
|
||||
votSettingsView?: SettingsController;
|
||||
|
||||
constructor({
|
||||
mount,
|
||||
|
|
@ -59,22 +58,13 @@ export class UIManager {
|
|||
this.intervalIdleChecker = intervalIdleChecker;
|
||||
}
|
||||
|
||||
get root(): HTMLElement | ShadowRoot {
|
||||
return this.mount.root;
|
||||
}
|
||||
|
||||
get portalContainer(): HTMLElement {
|
||||
return this.mount.portalContainer;
|
||||
}
|
||||
|
||||
getSubtitlesMountContainer(): HTMLElement | ShadowRoot {
|
||||
return this.votOverlayView?.root ?? this.mount.subtitlesMountContainer;
|
||||
}
|
||||
|
||||
isInitialized(): this is {
|
||||
votGlobalPortal: HTMLElement;
|
||||
votOverlayView: OverlayView;
|
||||
votSettingsView: SettingsView;
|
||||
votOverlayView: OverlayController;
|
||||
votSettingsView: SettingsController;
|
||||
} {
|
||||
return this.initialized;
|
||||
}
|
||||
|
|
@ -116,17 +106,16 @@ export class UIManager {
|
|||
this.votOverlayView = undefined;
|
||||
this.votSettingsView = undefined;
|
||||
this.globalPortalMount = undefined;
|
||||
this.votGlobalPortal = undefined;
|
||||
}
|
||||
|
||||
private buildUI() {
|
||||
this.globalPortalMount = createShadowMount({
|
||||
const globalPortalMount = createShadowMount({
|
||||
parent: this.getGlobalPortalHost(this.mount),
|
||||
rootClasses: ["vot-portal"],
|
||||
});
|
||||
this.votGlobalPortal = this.globalPortalMount.root;
|
||||
this.globalPortalMount = globalPortalMount;
|
||||
|
||||
this.votOverlayView = new OverlayView({
|
||||
this.votOverlayView = new OverlayController({
|
||||
mount: this.mount,
|
||||
data: this.data,
|
||||
videoHandler: this.videoHandler,
|
||||
|
|
@ -136,8 +125,8 @@ export class UIManager {
|
|||
// (e.g. when changing the menu language).
|
||||
this.votOverlayView.initUI();
|
||||
|
||||
this.votSettingsView = new SettingsView({
|
||||
globalPortal: this.votGlobalPortal,
|
||||
this.votSettingsView = new SettingsController({
|
||||
globalPortal: globalPortalMount.root,
|
||||
data: this.data,
|
||||
videoHandler: this.videoHandler,
|
||||
});
|
||||
|
|
@ -638,7 +627,7 @@ export class UIManager {
|
|||
|
||||
async reloadMenu() {
|
||||
if (!this.votOverlayView?.isInitialized()) {
|
||||
throw new Error("[VOT] OverlayView isn't initialized");
|
||||
throw new Error("[VOT] OverlayController isn't initialized");
|
||||
}
|
||||
|
||||
// Preserve overlay state across UI rebuild.
|
||||
|
|
@ -699,7 +688,7 @@ export class UIManager {
|
|||
|
||||
async handleTranslationBtnClick() {
|
||||
if (!this.votOverlayView?.isInitialized()) {
|
||||
throw new Error("[VOT] OverlayView isn't initialized");
|
||||
throw new Error("[VOT] OverlayController isn't initialized");
|
||||
}
|
||||
|
||||
await handleTranslationButtonCommand({
|
||||
|
|
@ -725,7 +714,7 @@ export class UIManager {
|
|||
|
||||
transformBtn(status: Status, text: string) {
|
||||
if (!this.votOverlayView?.isInitialized()) {
|
||||
throw new Error("[VOT] OverlayView isn't initialized");
|
||||
throw new Error("[VOT] OverlayController isn't initialized");
|
||||
}
|
||||
|
||||
this.votOverlayView.overlayViewControls?.setStatus(status);
|
||||
|
|
@ -748,7 +737,6 @@ export class UIManager {
|
|||
this.votSettingsView?.release();
|
||||
if (this.globalPortalMount) destroyShadowMount(this.globalPortalMount);
|
||||
this.globalPortalMount = undefined;
|
||||
this.votGlobalPortal = undefined;
|
||||
|
||||
this.initialized = false;
|
||||
return this;
|
||||
|
|
|
|||
|
|
@ -1,30 +1,49 @@
|
|||
import { FullscreenHelper } from "../../core/fullscreenHelper";
|
||||
import { setSettings } from "../../stores/settings";
|
||||
import type { StorageData } from "../../types/storage";
|
||||
import type { OverlayMount } from "../../types/uiManager";
|
||||
import type {
|
||||
OverlayViewEventMap,
|
||||
OverlayViewProps,
|
||||
} from "../../types/views/overlay";
|
||||
import { containsCrossShadow, getDeepActiveElement } from "../../utils/dom";
|
||||
import { EventImpl } from "../../utils/eventImpl";
|
||||
import { hasTouchScreen, isTouchFirstInput } from "../../utils/inputDevice";
|
||||
import type { IntervalIdleChecker } from "../../utils/intervalIdleChecker";
|
||||
import { votStorage } from "../../utils/storage";
|
||||
import type { VideoHandler } from "../../VideoHandler";
|
||||
import {
|
||||
OverlayView as OverlayViewComponent,
|
||||
type OverlayViewControls,
|
||||
} from "../../views/OverlayView";
|
||||
} from "../components/OverlayView/OverlayView";
|
||||
import { FullscreenHelper } from "../core/fullscreenHelper";
|
||||
import { setSettings } from "../stores/settings";
|
||||
import type { LanguageSelectKey } from "../types/components/select";
|
||||
import type { StorageData } from "../types/storage";
|
||||
import type { OverlayMount } from "../types/uiManager";
|
||||
import { containsCrossShadow, getDeepActiveElement } from "../utils/dom";
|
||||
import { EventImpl } from "../utils/eventImpl";
|
||||
import { hasTouchScreen, isTouchFirstInput } from "../utils/inputDevice";
|
||||
import type { IntervalIdleChecker } from "../utils/intervalIdleChecker";
|
||||
import { votStorage } from "../utils/storage";
|
||||
import type { VideoHandler } from "../VideoHandler";
|
||||
import {
|
||||
createShadowMount,
|
||||
destroyShadowMount,
|
||||
reparentShadowMount,
|
||||
type ShadowMount,
|
||||
} from "../shadowMount";
|
||||
import { type MountedComponent, mountComponent } from "../solid/mountComponent";
|
||||
} from "./shadowMount";
|
||||
import { render } from "./solid/renderer";
|
||||
|
||||
export class OverlayView {
|
||||
type OverlayControllerProps = {
|
||||
mount: OverlayMount;
|
||||
data?: Partial<StorageData>;
|
||||
videoHandler?: VideoHandler;
|
||||
intervalIdleChecker: IntervalIdleChecker;
|
||||
};
|
||||
|
||||
type OverlayControllerEventMap = {
|
||||
"click:settings": [];
|
||||
"click:pip": [];
|
||||
"click:subtitles": [];
|
||||
"click:downloadTranslation": [];
|
||||
"click:downloadSubtitles": [];
|
||||
"click:translate": [];
|
||||
"input:videoVolume": [volume: number];
|
||||
"input:translationVolume": [volume: number];
|
||||
"select:fromLanguage": [item: LanguageSelectKey];
|
||||
"select:toLanguage": [item: LanguageSelectKey];
|
||||
"select:subtitles": [item: string];
|
||||
"select:voiceType": [useLive: boolean];
|
||||
};
|
||||
|
||||
export class OverlayController {
|
||||
private static readonly BIG_CONTAINER_WIDTH_PX = 550;
|
||||
private resizeObserver?: ResizeObserver;
|
||||
private readonly fullscreenHelper: FullscreenHelper;
|
||||
|
|
@ -39,38 +58,46 @@ export class OverlayView {
|
|||
private readonly intervalIdleChecker: IntervalIdleChecker;
|
||||
private overlayMount?: ShadowMount;
|
||||
overlayViewControls?: OverlayViewControls;
|
||||
private overlayViewComponent?: MountedComponent<HTMLDivElement>;
|
||||
private disposeOverlay?: () => void;
|
||||
|
||||
private readonly events: {
|
||||
[K in keyof OverlayViewEventMap]: EventImpl<OverlayViewEventMap[K]>;
|
||||
[K in keyof OverlayControllerEventMap]: EventImpl<
|
||||
OverlayControllerEventMap[K]
|
||||
>;
|
||||
} = {
|
||||
"click:settings": new EventImpl<OverlayViewEventMap["click:settings"]>(),
|
||||
"click:pip": new EventImpl<OverlayViewEventMap["click:pip"]>(),
|
||||
"click:subtitles": new EventImpl<OverlayViewEventMap["click:subtitles"]>(),
|
||||
"click:settings": new EventImpl<
|
||||
OverlayControllerEventMap["click:settings"]
|
||||
>(),
|
||||
"click:pip": new EventImpl<OverlayControllerEventMap["click:pip"]>(),
|
||||
"click:subtitles": new EventImpl<
|
||||
OverlayControllerEventMap["click:subtitles"]
|
||||
>(),
|
||||
"click:downloadTranslation": new EventImpl<
|
||||
OverlayViewEventMap["click:downloadTranslation"]
|
||||
OverlayControllerEventMap["click:downloadTranslation"]
|
||||
>(),
|
||||
"click:downloadSubtitles": new EventImpl<
|
||||
OverlayViewEventMap["click:downloadSubtitles"]
|
||||
OverlayControllerEventMap["click:downloadSubtitles"]
|
||||
>(),
|
||||
"click:translate": new EventImpl<
|
||||
OverlayControllerEventMap["click:translate"]
|
||||
>(),
|
||||
"click:translate": new EventImpl<OverlayViewEventMap["click:translate"]>(),
|
||||
"input:videoVolume": new EventImpl<
|
||||
OverlayViewEventMap["input:videoVolume"]
|
||||
OverlayControllerEventMap["input:videoVolume"]
|
||||
>(),
|
||||
"input:translationVolume": new EventImpl<
|
||||
OverlayViewEventMap["input:translationVolume"]
|
||||
OverlayControllerEventMap["input:translationVolume"]
|
||||
>(),
|
||||
"select:fromLanguage": new EventImpl<
|
||||
OverlayViewEventMap["select:fromLanguage"]
|
||||
OverlayControllerEventMap["select:fromLanguage"]
|
||||
>(),
|
||||
"select:toLanguage": new EventImpl<
|
||||
OverlayViewEventMap["select:toLanguage"]
|
||||
OverlayControllerEventMap["select:toLanguage"]
|
||||
>(),
|
||||
"select:subtitles": new EventImpl<
|
||||
OverlayViewEventMap["select:subtitles"]
|
||||
OverlayControllerEventMap["select:subtitles"]
|
||||
>(),
|
||||
"select:voiceType": new EventImpl<
|
||||
OverlayViewEventMap["select:voiceType"]
|
||||
OverlayControllerEventMap["select:voiceType"]
|
||||
>(),
|
||||
};
|
||||
|
||||
|
|
@ -79,7 +106,7 @@ export class OverlayView {
|
|||
data = {},
|
||||
videoHandler,
|
||||
intervalIdleChecker,
|
||||
}: OverlayViewProps) {
|
||||
}: OverlayControllerProps) {
|
||||
this.mount = mount;
|
||||
this.data = data;
|
||||
this.videoHandler = videoHandler;
|
||||
|
|
@ -95,10 +122,6 @@ export class OverlayView {
|
|||
return this.overlayMount?.root ?? this.mount.root;
|
||||
}
|
||||
|
||||
get portalContainer(): HTMLElement {
|
||||
return this.mount.portalContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update mount points when the player container changes.
|
||||
* Moves already-mounted UI nodes and rebinds root-bound listeners (dragging).
|
||||
|
|
@ -125,17 +148,17 @@ export class OverlayView {
|
|||
return this.initialized;
|
||||
}
|
||||
|
||||
addEventListener<K extends keyof OverlayViewEventMap>(
|
||||
addEventListener<K extends keyof OverlayControllerEventMap>(
|
||||
type: K,
|
||||
listener: (...data: OverlayViewEventMap[K]) => void,
|
||||
listener: (...data: OverlayControllerEventMap[K]) => void,
|
||||
): this {
|
||||
this.events[type].addListener(listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
removeEventListener<K extends keyof OverlayViewEventMap>(
|
||||
removeEventListener<K extends keyof OverlayControllerEventMap>(
|
||||
type: K,
|
||||
listener: (...data: OverlayViewEventMap[K]) => void,
|
||||
listener: (...data: OverlayControllerEventMap[K]) => void,
|
||||
): this {
|
||||
this.events[type].removeListener(listener);
|
||||
return this;
|
||||
|
|
@ -167,7 +190,7 @@ export class OverlayView {
|
|||
|
||||
initUI() {
|
||||
if (this.isInitialized()) {
|
||||
throw new Error("[VOT] OverlayView is already initialized");
|
||||
throw new Error("[VOT] OverlayController is already initialized");
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
|
|
@ -194,112 +217,111 @@ export class OverlayView {
|
|||
? this.videoHandler.getVideoVolume() * 100
|
||||
: 100;
|
||||
|
||||
this.overlayViewComponent = mountComponent<HTMLDivElement>((rootRef) =>
|
||||
OverlayViewComponent({
|
||||
ref: rootRef,
|
||||
controlsRef: (controls) => {
|
||||
this.overlayViewControls = controls;
|
||||
},
|
||||
isBigContainer: this.isBigContainer,
|
||||
detectedLanguage: this.videoHandler?.videoData?.detectedLanguage,
|
||||
responseLanguage: this.data.responseLanguage,
|
||||
videoVolume,
|
||||
onButtonDragActivity: (source) =>
|
||||
this.intervalIdleChecker.markActivity(source),
|
||||
onButtonDragEnd: () => this.queueButtonAutoHideAfterInteraction(),
|
||||
onButtonPositionChange: (position) => {
|
||||
this.data.buttonPos = position;
|
||||
},
|
||||
onTranslateClick: () => {
|
||||
this.events["click:translate"].dispatch();
|
||||
},
|
||||
onPiPClick: () => {
|
||||
this.events["click:pip"].dispatch();
|
||||
},
|
||||
onSubtitlesClick: () => {
|
||||
this.events["click:subtitles"].dispatch();
|
||||
},
|
||||
onSubtitlesOpen: () =>
|
||||
this.videoHandler?.ensureSubtitlesForCurrentLangPair(),
|
||||
onSubtitlesSelect: (value) => {
|
||||
this.events["select:subtitles"].dispatch(value);
|
||||
},
|
||||
onVoiceChange: (voice) => {
|
||||
const useLive = voice === "live";
|
||||
this.data.useLivelyVoice = useLive;
|
||||
void votStorage.set("useLivelyVoice", useLive);
|
||||
this.events["select:voiceType"].dispatch(useLive);
|
||||
queueMicrotask(() => this.queueButtonAutoHideAfterInteraction());
|
||||
},
|
||||
onVideoVolumeInput: (volume) => {
|
||||
this.events["input:videoVolume"].dispatch(volume);
|
||||
},
|
||||
onTranslationVolumeInput: (volume) => {
|
||||
if (this.data.defaultVolume !== volume) {
|
||||
this.data.defaultVolume = volume;
|
||||
this.scheduleDefaultVolumePersist();
|
||||
}
|
||||
this.disposeOverlay = render(
|
||||
() =>
|
||||
OverlayViewComponent({
|
||||
controlsRef: (controls) => {
|
||||
this.overlayViewControls = controls;
|
||||
},
|
||||
isBigContainer: this.isBigContainer,
|
||||
detectedLanguage: this.videoHandler?.videoData?.detectedLanguage,
|
||||
responseLanguage: this.data.responseLanguage,
|
||||
videoVolume,
|
||||
onButtonDragActivity: (source) =>
|
||||
this.intervalIdleChecker.markActivity(source),
|
||||
onButtonDragEnd: () => this.queueButtonAutoHideAfterInteraction(),
|
||||
onButtonPositionChange: (position) => {
|
||||
this.data.buttonPos = position;
|
||||
},
|
||||
onTranslateClick: () => {
|
||||
this.events["click:translate"].dispatch();
|
||||
},
|
||||
onPiPClick: () => {
|
||||
this.events["click:pip"].dispatch();
|
||||
},
|
||||
onSubtitlesClick: () => {
|
||||
this.events["click:subtitles"].dispatch();
|
||||
},
|
||||
onSubtitlesOpen: () =>
|
||||
this.videoHandler?.ensureSubtitlesForCurrentLangPair(),
|
||||
onSubtitlesSelect: (value) => {
|
||||
this.events["select:subtitles"].dispatch(value);
|
||||
},
|
||||
onVoiceChange: (voice) => {
|
||||
const useLive = voice === "live";
|
||||
this.data.useLivelyVoice = useLive;
|
||||
void votStorage.set("useLivelyVoice", useLive);
|
||||
this.events["select:voiceType"].dispatch(useLive);
|
||||
queueMicrotask(() => this.queueButtonAutoHideAfterInteraction());
|
||||
},
|
||||
onVideoVolumeInput: (volume) => {
|
||||
this.events["input:videoVolume"].dispatch(volume);
|
||||
},
|
||||
onTranslationVolumeInput: (volume) => {
|
||||
if (this.data.defaultVolume !== volume) {
|
||||
this.data.defaultVolume = volume;
|
||||
this.scheduleDefaultVolumePersist();
|
||||
}
|
||||
|
||||
this.events["input:translationVolume"].dispatch(volume);
|
||||
},
|
||||
onDownloadTranslationClick: () => {
|
||||
this.events["click:downloadTranslation"].dispatch();
|
||||
},
|
||||
onDownloadSubtitlesClick: () => {
|
||||
this.events["click:downloadSubtitles"].dispatch();
|
||||
},
|
||||
onSettingsClick: () => {
|
||||
this.events["click:settings"].dispatch();
|
||||
},
|
||||
onDetectedLanguageSelect: (language) => {
|
||||
if (this.videoHandler?.videoData) {
|
||||
this.videoHandler.videoData.detectedLanguage = language;
|
||||
this.videoHandler.videoManager.rememberUserLanguageSelection(
|
||||
this.videoHandler.videoData.videoId,
|
||||
language,
|
||||
);
|
||||
}
|
||||
this.events["select:fromLanguage"].dispatch(language);
|
||||
},
|
||||
onResponseLanguageSelect: async (language) => {
|
||||
if (this.videoHandler?.videoData) {
|
||||
this.videoHandler.translateToLang =
|
||||
this.videoHandler.videoData.responseLanguage = language;
|
||||
}
|
||||
this.events["input:translationVolume"].dispatch(volume);
|
||||
},
|
||||
onDownloadTranslationClick: () => {
|
||||
this.events["click:downloadTranslation"].dispatch();
|
||||
},
|
||||
onDownloadSubtitlesClick: () => {
|
||||
this.events["click:downloadSubtitles"].dispatch();
|
||||
},
|
||||
onSettingsClick: () => {
|
||||
this.events["click:settings"].dispatch();
|
||||
},
|
||||
onDetectedLanguageSelect: (language) => {
|
||||
if (this.videoHandler?.videoData) {
|
||||
this.videoHandler.videoData.detectedLanguage = language;
|
||||
this.videoHandler.videoManager.rememberUserLanguageSelection(
|
||||
this.videoHandler.videoData.videoId,
|
||||
language,
|
||||
);
|
||||
}
|
||||
this.events["select:fromLanguage"].dispatch(language);
|
||||
},
|
||||
onResponseLanguageSelect: async (language) => {
|
||||
if (this.videoHandler?.videoData) {
|
||||
this.videoHandler.translateToLang =
|
||||
this.videoHandler.videoData.responseLanguage = language;
|
||||
}
|
||||
|
||||
const prevResponseLanguage = this.data.responseLanguage;
|
||||
if (prevResponseLanguage !== language) {
|
||||
this.data.responseLanguage = language;
|
||||
await votStorage.set(
|
||||
"responseLanguage",
|
||||
this.data.responseLanguage,
|
||||
);
|
||||
}
|
||||
const prevResponseLanguage = this.data.responseLanguage;
|
||||
if (prevResponseLanguage !== language) {
|
||||
this.data.responseLanguage = language;
|
||||
await votStorage.set(
|
||||
"responseLanguage",
|
||||
this.data.responseLanguage,
|
||||
);
|
||||
}
|
||||
|
||||
// UX: keep the "Don't translate from selected languages" list in sync
|
||||
// with the selected response language, but only while the list still
|
||||
// looks like the old default.
|
||||
// TODO: recheck it later
|
||||
if (
|
||||
Array.isArray(this.data.dontTranslateLanguages) &&
|
||||
this.data.dontTranslateLanguages.length === 1 &&
|
||||
prevResponseLanguage !== language &&
|
||||
typeof prevResponseLanguage === "string" &&
|
||||
this.data.dontTranslateLanguages[0] === prevResponseLanguage
|
||||
) {
|
||||
setSettings("dontTranslateLanguages", [language]);
|
||||
this.data.dontTranslateLanguages = [language];
|
||||
await votStorage.set(
|
||||
"dontTranslateLanguages",
|
||||
this.data.dontTranslateLanguages,
|
||||
);
|
||||
}
|
||||
this.events["select:toLanguage"].dispatch(language);
|
||||
},
|
||||
}),
|
||||
// UX: keep the "Don't translate from selected languages" list in sync
|
||||
// with the selected response language, but only while the list still
|
||||
// looks like the old default.
|
||||
// TODO: recheck it later
|
||||
if (
|
||||
Array.isArray(this.data.dontTranslateLanguages) &&
|
||||
this.data.dontTranslateLanguages.length === 1 &&
|
||||
prevResponseLanguage !== language &&
|
||||
typeof prevResponseLanguage === "string" &&
|
||||
this.data.dontTranslateLanguages[0] === prevResponseLanguage
|
||||
) {
|
||||
setSettings("dontTranslateLanguages", [language]);
|
||||
this.data.dontTranslateLanguages = [language];
|
||||
await votStorage.set(
|
||||
"dontTranslateLanguages",
|
||||
this.data.dontTranslateLanguages,
|
||||
);
|
||||
}
|
||||
this.events["select:toLanguage"].dispatch(language);
|
||||
},
|
||||
}) as Node,
|
||||
this.root,
|
||||
);
|
||||
|
||||
this.root.appendChild(this.overlayViewComponent.root);
|
||||
this.setupResizeObserver();
|
||||
// #endregion VOT Menu
|
||||
return this;
|
||||
|
|
@ -442,8 +464,8 @@ export class OverlayView {
|
|||
private doReleaseUI(): void {
|
||||
this.resizeObserver?.disconnect();
|
||||
this.resizeObserver = undefined;
|
||||
this.overlayViewComponent?.dispose();
|
||||
this.overlayViewComponent = undefined;
|
||||
this.disposeOverlay?.();
|
||||
this.disposeOverlay = undefined;
|
||||
this.overlayViewControls = undefined;
|
||||
|
||||
this.fullscreenHelper.destroy();
|
||||
|
|
@ -475,7 +497,7 @@ export class OverlayView {
|
|||
|
||||
get isBigContainer() {
|
||||
return this.fullscreenHelper.isBigContainer(
|
||||
OverlayView.BIG_CONTAINER_WIDTH_PX,
|
||||
OverlayController.BIG_CONTAINER_WIDTH_PX,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import debug from "../utils/debug";
|
||||
import { containsCrossShadow, getDeepActiveElement } from "../utils/dom";
|
||||
import type { IntervalIdleChecker } from "../utils/intervalIdleChecker";
|
||||
import type { OverlayView } from "./views/overlay";
|
||||
import type { OverlayController } from "./overlayController";
|
||||
|
||||
export interface OverlayVisibilityDependencies {
|
||||
checker: IntervalIdleChecker;
|
||||
getOverlayView(): OverlayView | null | undefined;
|
||||
getOverlayView(): OverlayController | null | undefined;
|
||||
getAutoHideDelay(): number;
|
||||
isInteractiveNode(node: unknown): boolean;
|
||||
nowMs?: () => number;
|
||||
|
|
@ -49,7 +49,7 @@ export class OverlayVisibilityController {
|
|||
/**
|
||||
* Ensures overlay is visible immediately and returns current view.
|
||||
*/
|
||||
show(): OverlayView | null {
|
||||
show(): OverlayController | null {
|
||||
const view = this.getView();
|
||||
if (!view) {
|
||||
return null;
|
||||
|
|
@ -235,12 +235,14 @@ export class OverlayVisibilityController {
|
|||
this.deps.checker.markActivity("overlay-focus-in");
|
||||
}
|
||||
|
||||
private getView(): OverlayView | null {
|
||||
private getView(): OverlayController | null {
|
||||
const view = this.deps.getOverlayView();
|
||||
return view?.isInitialized() ? view : null;
|
||||
}
|
||||
|
||||
private isOverlayHoverActive(view: OverlayView | null = this.getView()) {
|
||||
private isOverlayHoverActive(
|
||||
view: OverlayController | null = this.getView(),
|
||||
) {
|
||||
return Boolean(
|
||||
view &&
|
||||
(this.pointerInsideOverlay || view.shouldKeepVisibleForInteraction()),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const SETTINGS_EVENT_KEYS: Array<keyof SettingsViewEventMap> = [
|
||||
const SETTINGS_EVENT_KEYS: Array<keyof SettingsControllerEventMap> = [
|
||||
"click:bugReport",
|
||||
"click:resetSettings",
|
||||
"update:account",
|
||||
|
|
@ -24,10 +24,14 @@ const SETTINGS_EVENT_KEYS: Array<keyof SettingsViewEventMap> = [
|
|||
"select:menuLanguage",
|
||||
];
|
||||
function createSettingsEvents(): {
|
||||
[K in keyof SettingsViewEventMap]: EventImpl<SettingsViewEventMap[K]>;
|
||||
[K in keyof SettingsControllerEventMap]: EventImpl<
|
||||
SettingsControllerEventMap[K]
|
||||
>;
|
||||
} {
|
||||
const events = {} as {
|
||||
[K in keyof SettingsViewEventMap]: EventImpl<SettingsViewEventMap[K]>;
|
||||
[K in keyof SettingsControllerEventMap]: EventImpl<
|
||||
SettingsControllerEventMap[K]
|
||||
>;
|
||||
};
|
||||
for (const key of SETTINGS_EVENT_KEYS) {
|
||||
(events as Record<string, EventImpl<unknown[]>>)[key] = new EventImpl<
|
||||
|
|
@ -38,34 +42,64 @@ function createSettingsEvents(): {
|
|||
}
|
||||
|
||||
import { type Accessor, createSignal, type Setter } from "solid-js";
|
||||
import { SettingsDialog } from "../../components/Settings/SettingsDialog";
|
||||
import { PROXY_WORKER_HOST } from "../../config/config";
|
||||
import { isAuthRefreshMessage } from "../../core/authRefreshMessage";
|
||||
import { openAuthWindow } from "../../core/authWindow";
|
||||
import { SettingsDialog } from "../components/Settings/SettingsDialog";
|
||||
import { PROXY_WORKER_HOST } from "../config/config";
|
||||
import { isAuthRefreshMessage } from "../core/authRefreshMessage";
|
||||
import { openAuthWindow } from "../core/authWindow";
|
||||
import {
|
||||
type LangOverride,
|
||||
localizationProvider,
|
||||
} from "../../localization/localizationProvider";
|
||||
import { account, resetAccount, updateAccount } from "../../stores/account";
|
||||
import { setLocale } from "../../stores/locale";
|
||||
import { setSettings } from "../../stores/settings";
|
||||
import type { SubtitleFormat } from "../../subtitles/types";
|
||||
import type { Position } from "../../types/components/votButton";
|
||||
} from "../localization/localizationProvider";
|
||||
import { account, resetAccount, updateAccount } from "../stores/account";
|
||||
import { setLocale } from "../stores/locale";
|
||||
import { setSettings } from "../stores/settings";
|
||||
import type { SubtitleFormat } from "../subtitles/types";
|
||||
import type { Position } from "../types/components/votButton";
|
||||
import type {
|
||||
Account,
|
||||
ResponseLanguageSubtitles,
|
||||
StorageData,
|
||||
TranslateProxyStatus,
|
||||
} from "../../types/storage";
|
||||
import type {
|
||||
SettingsViewEventMap,
|
||||
SettingsViewProps,
|
||||
} from "../../types/views/settings";
|
||||
import debug from "../../utils/debug";
|
||||
import { EventImpl } from "../../utils/eventImpl";
|
||||
import { votStorage } from "../../utils/storage";
|
||||
import type { VideoHandler } from "../../VideoHandler";
|
||||
import { type MountedComponent, mountComponent } from "../solid/mountComponent";
|
||||
} from "../types/storage";
|
||||
import type { SubtitleFontFamily } from "../types/subtitles";
|
||||
import type { TranslateService } from "../types/translateApis";
|
||||
import debug from "../utils/debug";
|
||||
import { EventImpl } from "../utils/eventImpl";
|
||||
import { votStorage } from "../utils/storage";
|
||||
import type { VideoHandler } from "../VideoHandler";
|
||||
import { render } from "./solid/renderer";
|
||||
|
||||
type SettingsControllerProps = {
|
||||
globalPortal: HTMLElement;
|
||||
data?: Partial<StorageData>;
|
||||
videoHandler?: VideoHandler;
|
||||
};
|
||||
|
||||
type SettingsControllerEventMap = {
|
||||
"click:bugReport": [];
|
||||
"click:resetSettings": [];
|
||||
"update:account": [account: Partial<Account> | undefined];
|
||||
"change:autoTranslate": [checked: boolean];
|
||||
"change:autoSubtitles": [checked: boolean];
|
||||
"change:showVideoVolume": [checked: boolean];
|
||||
"change:audioBooster": [checked: boolean];
|
||||
"change:syncVolume": [checked: boolean];
|
||||
"change:subtitlesHighlightWords": [checked: boolean];
|
||||
"change:subtitlesSmartLayout": [checked: boolean];
|
||||
"select:responseLanguageSubtitles": [item: ResponseLanguageSubtitles];
|
||||
"select:subtitlesFontFamily": [item: SubtitleFontFamily];
|
||||
"change:proxyWorkerHost": [value: string];
|
||||
"change:useNewAudioPlayer": [checked: boolean];
|
||||
"change:onlyBypassMediaCSP": [checked: boolean];
|
||||
"change:showPiPButton": [checked: boolean];
|
||||
"input:subtitlesMaxLength": [value: number];
|
||||
"input:subtitlesFontSize": [value: number];
|
||||
"input:subtitlesBackgroundOpacity": [value: number];
|
||||
"input:autoHideButtonDelay": [value: number];
|
||||
"select:proxyTranslationStatus": [item: TranslateProxyStatus];
|
||||
"select:translationTextService": [item: TranslateService];
|
||||
"select:menuLanguage": [item: LangOverride];
|
||||
};
|
||||
|
||||
type BufferedNumericStorageKey =
|
||||
| "autoVolume"
|
||||
|
|
@ -74,14 +108,16 @@ type BufferedNumericStorageKey =
|
|||
| "subtitlesOpacity"
|
||||
| "autoHideButtonDelay";
|
||||
|
||||
export class SettingsView {
|
||||
export class SettingsController {
|
||||
private static readonly PERSIST_DELAY_MS = 250;
|
||||
globalPortal: HTMLElement;
|
||||
private initialized = false;
|
||||
private readonly data: Partial<StorageData>;
|
||||
private readonly videoHandler?: VideoHandler;
|
||||
private readonly events: {
|
||||
[K in keyof SettingsViewEventMap]: EventImpl<SettingsViewEventMap[K]>;
|
||||
[K in keyof SettingsControllerEventMap]: EventImpl<
|
||||
SettingsControllerEventMap[K]
|
||||
>;
|
||||
} = createSettingsEvents();
|
||||
private persistTimerIds: Partial<
|
||||
Record<BufferedNumericStorageKey, ReturnType<typeof setTimeout>>
|
||||
|
|
@ -94,16 +130,20 @@ export class SettingsView {
|
|||
void this.refreshAccountFromStorage();
|
||||
};
|
||||
root?: HTMLElement;
|
||||
private settingsDialog?: MountedComponent<HTMLElement>;
|
||||
private disposeSettingsDialog?: () => void;
|
||||
private dialogOpen?: Accessor<boolean>;
|
||||
private setDialogOpen?: Setter<boolean>;
|
||||
private accountStorageListenerCleanup?: () => void;
|
||||
constructor({ globalPortal, data = {}, videoHandler }: SettingsViewProps) {
|
||||
constructor({
|
||||
globalPortal,
|
||||
data = {},
|
||||
videoHandler,
|
||||
}: SettingsControllerProps) {
|
||||
this.globalPortal = globalPortal;
|
||||
this.data = data;
|
||||
this.videoHandler = videoHandler;
|
||||
}
|
||||
isInitialized(): this is Required<SettingsView> {
|
||||
isInitialized(): this is Required<SettingsController> {
|
||||
return this.initialized;
|
||||
}
|
||||
private scheduleStoragePersist(
|
||||
|
|
@ -117,7 +157,7 @@ export class SettingsView {
|
|||
this.persistTimerIds[key] = globalThis.setTimeout(() => {
|
||||
this.persistTimerIds[key] = undefined;
|
||||
void votStorage.set(key, value);
|
||||
}, SettingsView.PERSIST_DELAY_MS);
|
||||
}, SettingsController.PERSIST_DELAY_MS);
|
||||
}
|
||||
private flushStoragePersists(): void {
|
||||
for (const key of Object.keys(this.persistTimerIds) as Array<
|
||||
|
|
@ -210,14 +250,16 @@ export class SettingsView {
|
|||
|
||||
initUI() {
|
||||
if (this.isInitialized()) {
|
||||
throw new Error("[VOT] SettingsView is already initialized");
|
||||
throw new Error("[VOT] SettingsController is already initialized");
|
||||
}
|
||||
this.settingsDialog = mountComponent<HTMLElement>((rootRef) => {
|
||||
this.disposeSettingsDialog = render(() => {
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
this.dialogOpen = isOpen;
|
||||
this.setDialogOpen = setIsOpen;
|
||||
return SettingsDialog({
|
||||
ref: rootRef,
|
||||
ref: (element) => {
|
||||
this.root = element;
|
||||
},
|
||||
get isOpen() {
|
||||
return isOpen();
|
||||
},
|
||||
|
|
@ -424,39 +466,42 @@ export class SettingsView {
|
|||
onResetSettingsClick: () =>
|
||||
this.events["click:resetSettings"].dispatch(),
|
||||
},
|
||||
});
|
||||
});
|
||||
this.root = this.settingsDialog.root;
|
||||
this.globalPortal.appendChild(this.settingsDialog.root);
|
||||
}) as Node;
|
||||
}, this.globalPortal);
|
||||
if (!this.root) {
|
||||
this.disposeSettingsDialog();
|
||||
this.disposeSettingsDialog = undefined;
|
||||
throw new Error("[VOT] Settings dialog did not expose a root element");
|
||||
}
|
||||
this.initialized = true;
|
||||
return this;
|
||||
}
|
||||
initUIEvents() {
|
||||
if (!this.isInitialized()) {
|
||||
throw new Error("[VOT] SettingsView isn't initialized");
|
||||
throw new Error("[VOT] SettingsController isn't initialized");
|
||||
}
|
||||
globalThis.addEventListener("message", this.onAuthRefreshMessage);
|
||||
this.bindAccountStorageListener();
|
||||
return this;
|
||||
}
|
||||
addEventListener<K extends keyof SettingsViewEventMap>(
|
||||
addEventListener<K extends keyof SettingsControllerEventMap>(
|
||||
type: K,
|
||||
listener: (...data: SettingsViewEventMap[K]) => void,
|
||||
listener: (...data: SettingsControllerEventMap[K]) => void,
|
||||
): this {
|
||||
this.events[type].addListener(listener);
|
||||
return this;
|
||||
}
|
||||
removeEventListener<K extends keyof SettingsViewEventMap>(
|
||||
removeEventListener<K extends keyof SettingsControllerEventMap>(
|
||||
type: K,
|
||||
listener: (...data: SettingsViewEventMap[K]) => void,
|
||||
listener: (...data: SettingsControllerEventMap[K]) => void,
|
||||
): this {
|
||||
this.events[type].removeListener(listener);
|
||||
return this;
|
||||
}
|
||||
private doReleaseUI(): void {
|
||||
this.settingsDialog?.dispose();
|
||||
this.settingsDialog?.root.remove();
|
||||
this.settingsDialog = undefined;
|
||||
this.disposeSettingsDialog?.();
|
||||
this.disposeSettingsDialog = undefined;
|
||||
this.root?.remove();
|
||||
this.dialogOpen = undefined;
|
||||
this.setDialogOpen = undefined;
|
||||
this.root = undefined;
|
||||
|
|
@ -477,19 +522,19 @@ export class SettingsView {
|
|||
}
|
||||
updateAccountInfo() {
|
||||
if (!this.isInitialized())
|
||||
throw new Error("[VOT] SettingsView isn't initialized");
|
||||
throw new Error("[VOT] SettingsController isn't initialized");
|
||||
this.events["update:account"].dispatch(this.data.account);
|
||||
return this;
|
||||
}
|
||||
open() {
|
||||
if (!this.isInitialized())
|
||||
throw new Error("[VOT] SettingsView isn't initialized");
|
||||
throw new Error("[VOT] SettingsController isn't initialized");
|
||||
this.setDialogOpen(true);
|
||||
return this;
|
||||
}
|
||||
close() {
|
||||
if (!this.isInitialized())
|
||||
throw new Error("[VOT] SettingsView isn't initialized");
|
||||
throw new Error("[VOT] SettingsController isn't initialized");
|
||||
this.setDialogOpen(false);
|
||||
return this;
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import solidSCSS from "../components/solid.scss?inline";
|
||||
import mainScss from "../styles/main.scss?inline";
|
||||
|
||||
type InlineStyleMap = Partial<Record<string, string>>;
|
||||
type ElementConfig = {
|
||||
|
|
@ -25,7 +24,7 @@ type CreateShadowMountOptions = {
|
|||
delegatesFocus?: boolean;
|
||||
};
|
||||
|
||||
const shadowScopedCssText = scopeCssForShadowRoots(`${mainScss}\n${solidSCSS}`);
|
||||
const shadowScopedCssText = scopeCssForShadowRoots(solidSCSS);
|
||||
let sharedShadowStyleSheet: CSSStyleSheet | null = null;
|
||||
let sharedShadowStyleSheetReady = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
import type { JSX } from "solid-js";
|
||||
import { render } from "./renderer";
|
||||
|
||||
export type MountedComponent<T extends HTMLElement> = {
|
||||
root: T;
|
||||
dispose: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compatibility bridge while callers still expect a concrete root element.
|
||||
* The JSX tree remains owned by a Solid root even after the returned element
|
||||
* is moved into a shadow root or portal.
|
||||
*/
|
||||
export function mountComponent<T extends HTMLElement>(
|
||||
view: (setRoot: (element: T) => void) => JSX.Element,
|
||||
): MountedComponent<T> {
|
||||
const host = document.createElement("vot-block");
|
||||
let root: T | undefined;
|
||||
const dispose = render(
|
||||
() => view((element) => (root = element)) as Node,
|
||||
host,
|
||||
);
|
||||
if (!root) {
|
||||
dispose();
|
||||
throw new Error("[VOT] Solid component did not expose a root element");
|
||||
}
|
||||
return { root, dispose };
|
||||
}
|
||||
|
|
@ -311,7 +311,7 @@ function bindGlobalDismissAndHotkeys(ctx: ExtraEventsContext): void {
|
|||
const isSelectInner = path.some(
|
||||
(element) =>
|
||||
element instanceof HTMLElement &&
|
||||
element.classList.contains("vot-select_new-inner"),
|
||||
element.classList.contains("vot-select-inner"),
|
||||
);
|
||||
debug.log(
|
||||
`[document click] ${isButton} ${isMenu} ${isVideo} ${isSettings}`,
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ export async function init(this: VideoHandler) {
|
|||
subtitlesHotkey: this.data.subtitlesHotkey,
|
||||
// subtitles
|
||||
responseLanguageSubtitles: this.data.responseLanguageSubtitles,
|
||||
subtitlesDownloadFormat: this.data.subtitlesDownloadFormat,
|
||||
highlightWords: this.data.highlightWords,
|
||||
subtitlesSmartLayout: this.data.subtitlesSmartLayout,
|
||||
subtitlesFontFamily: this.data.subtitlesFontFamily,
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"name": "[VOT] - Test UI",
|
||||
"description": "Test ui",
|
||||
"version": "1.0.0",
|
||||
"author": "sodapng, mynovelhost, Toil, SashaXser, MrSoczekXD",
|
||||
"namespace": "vot-test-ui",
|
||||
"icon": "https://translate.yandex.ru/icons/favicon.ico",
|
||||
"match": ["*://toil.cc/*"],
|
||||
"require": [
|
||||
"https://gist.githubusercontent.com/ilyhalight/6eb5bb4dffc7ca9e3c57d6933e2452f3/raw/7ab38af2228d0bed13912e503bc8a9ee4b11828d/gm-addstyle-polyfill.js"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { defineConfig, type UserConfig } from "vite";
|
||||
import { buildDefine } from "./lib/env";
|
||||
import { distDir, singleFileBuildOptions, testsDir } from "./lib/paths";
|
||||
import { formatSimpleUserscriptHeader } from "./lib/userscript/headers";
|
||||
import { createBaseViteConfig } from "./lib/vite-base-config";
|
||||
|
||||
const TEST_UI_ENTRY = path.resolve(testsDir, "ui.js");
|
||||
const TEST_UI_HEADERS_PATH = path.resolve(testsDir, "headers.json");
|
||||
|
||||
export default defineConfig(async () => {
|
||||
const baseConfig = createBaseViteConfig({ cacheName: "test-ui" });
|
||||
const testMeta = JSON.parse(
|
||||
await fs.readFile(TEST_UI_HEADERS_PATH, "utf8"),
|
||||
) as Record<string, unknown>;
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
define: buildDefine({
|
||||
debug: true,
|
||||
isExtension: false,
|
||||
availableLocales: [],
|
||||
repoBranch: "master",
|
||||
version: "",
|
||||
authors: "",
|
||||
crxjsBuild: false,
|
||||
}),
|
||||
build: {
|
||||
...baseConfig.build,
|
||||
...singleFileBuildOptions,
|
||||
outDir: distDir,
|
||||
emptyOutDir: false,
|
||||
sourcemap: false,
|
||||
minify: false,
|
||||
lib: {
|
||||
entry: TEST_UI_ENTRY,
|
||||
name: "testUi",
|
||||
formats: ["iife"],
|
||||
fileName: () => "test-ui.user.js",
|
||||
},
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
postBanner: formatSimpleUserscriptHeader(testMeta),
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies UserConfig;
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue