|
|
<script lang="ts">
|
|
|
|
|
|
import { Block } from "@gradio/atoms";
|
|
|
import { StatusTracker } from "@gradio/statustracker";
|
|
|
import type { LoadingStatus } from "@gradio/statustracker";
|
|
|
import type { Gradio } from "@gradio/utils";
|
|
|
import { onMount, tick } from "svelte";
|
|
|
|
|
|
|
|
|
|
|
|
export let value: Array<{ group_name: string, properties: any[] }> = [];
|
|
|
|
|
|
export let label: string | undefined = undefined;
|
|
|
|
|
|
export let show_group_name_only_one: boolean = true;
|
|
|
|
|
|
export let disable_accordion: boolean = false;
|
|
|
|
|
|
export let visible: boolean = true;
|
|
|
|
|
|
export let open: boolean = true;
|
|
|
|
|
|
export let elem_id: string = "";
|
|
|
|
|
|
export let elem_classes: string[] = [];
|
|
|
|
|
|
export let container: boolean = false;
|
|
|
|
|
|
export let scale: number | null = null;
|
|
|
|
|
|
export let min_width: number | undefined = undefined;
|
|
|
|
|
|
export let width: number | undefined = undefined;
|
|
|
|
|
|
export let height: number | undefined = undefined;
|
|
|
|
|
|
export let loading_status: LoadingStatus | undefined = undefined;
|
|
|
|
|
|
export let interactive: boolean = true;
|
|
|
|
|
|
export let gradio: Gradio<{ change: any, reset: any, input: any, clear_status: never, expand: never, collapse: never, undo: any }>;
|
|
|
|
|
|
|
|
|
|
|
|
let groupVisibility: Record<string, boolean> = {};
|
|
|
|
|
|
let sliderElements: Record<string, HTMLInputElement> = {};
|
|
|
|
|
|
$: final_classes = ["propertysheet-wrapper", ...elem_classes];
|
|
|
|
|
|
let validationState: Record<string, boolean> = {};
|
|
|
|
|
|
let lastValue: string | null = null;
|
|
|
|
|
|
let isResetting: boolean = false;
|
|
|
|
|
|
let initialValues: Record<string, any> = {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function validate_prop(prop: any) {
|
|
|
if (prop.minimum === undefined && prop.maximum === undefined) {
|
|
|
if (validationState[prop.name] !== true) {
|
|
|
validationState[prop.name] = true;
|
|
|
validationState = { ...validationState };
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
const numValue = Number(prop.value);
|
|
|
let is_valid = true;
|
|
|
if (prop.minimum !== undefined && numValue < prop.minimum) is_valid = false;
|
|
|
if (prop.maximum !== undefined && numValue > prop.maximum) is_valid = false;
|
|
|
if (validationState[prop.name] !== is_valid) {
|
|
|
validationState[prop.name] = is_valid;
|
|
|
validationState = { ...validationState };
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
let dynamic_height: number | undefined;
|
|
|
$: dynamic_height = open ? height : undefined;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateAllSliders() {
|
|
|
if (!Array.isArray(value)) return;
|
|
|
for (const group of value) {
|
|
|
if (Array.isArray(group.properties)) {
|
|
|
for (const prop of group.properties) {
|
|
|
if (prop.component === 'slider' && sliderElements[prop.name]) {
|
|
|
updateSliderBackground(prop, sliderElements[prop.name]);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$: if (Array.isArray(value)) {
|
|
|
if (JSON.stringify(value) !== lastValue) {
|
|
|
lastValue = JSON.stringify(value);
|
|
|
for (const group of value) {
|
|
|
if (groupVisibility[group.group_name] === undefined) {
|
|
|
groupVisibility[group.group_name] = true;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
for (const group of value) {
|
|
|
if (Array.isArray(group.properties)) {
|
|
|
for (const prop of group.properties) {
|
|
|
if (prop.component?.startsWith("number") || prop.component === 'slider') {
|
|
|
validate_prop(prop);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
tick().then(updateAllSliders);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateSliderBackground(prop: any, element: HTMLInputElement) {
|
|
|
if (!element) return;
|
|
|
const min = prop.minimum ?? 0;
|
|
|
const max = prop.maximum ?? 100;
|
|
|
const val = Number(prop.value);
|
|
|
const percentage = val <= min ? 0 : ((val - min) * 100) / (max - min);
|
|
|
element.style.setProperty('--slider-progress', `${percentage}%`);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handle_toggle() {
|
|
|
open = !open;
|
|
|
if (open) gradio.dispatch("expand");
|
|
|
else gradio.dispatch("collapse");
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function toggleGroup(groupName: string) {
|
|
|
groupVisibility[groupName] = !groupVisibility[groupName];
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function get_prop_value(prop_name: string) {
|
|
|
if (!Array.isArray(value) || !prop_name) return undefined;
|
|
|
|
|
|
for (const group of value) {
|
|
|
if (!Array.isArray(group.properties)) continue;
|
|
|
const found_prop = group.properties.find(p => p.name === prop_name);
|
|
|
if (found_prop) {
|
|
|
return found_prop.value;
|
|
|
}
|
|
|
}
|
|
|
return undefined;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function dispatch_update(event_name: "change" | "input", changed_prop: any) {
|
|
|
if (validationState[changed_prop.name] === false) {
|
|
|
return;
|
|
|
}
|
|
|
const payload: Record<string, any> = {};
|
|
|
let final_value = changed_prop.value;
|
|
|
if (changed_prop.component?.startsWith("number") || changed_prop.component === "slider") {
|
|
|
final_value = Number(changed_prop.value);
|
|
|
} else if (changed_prop.component === "checkbox") {
|
|
|
final_value = changed_prop.value;
|
|
|
}
|
|
|
payload[changed_prop.name] = final_value;
|
|
|
gradio.dispatch(event_name, payload);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function handle_dropdown_change(event: Event, prop_to_change: any) {
|
|
|
const new_prop_value = (event.target as HTMLSelectElement).value;
|
|
|
|
|
|
|
|
|
value = value.map(group => {
|
|
|
if (!group.properties) return group;
|
|
|
return {
|
|
|
...group,
|
|
|
properties: group.properties.map(prop => {
|
|
|
if (prop.name === prop_to_change.name) {
|
|
|
return { ...prop, value: new_prop_value };
|
|
|
}
|
|
|
return prop;
|
|
|
})
|
|
|
};
|
|
|
});
|
|
|
|
|
|
|
|
|
await tick();
|
|
|
|
|
|
gradio.dispatch("change", value);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function handle_multiselect_change() {
|
|
|
|
|
|
await tick();
|
|
|
|
|
|
gradio.dispatch("change", value);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handle_reset_prop(propName: string) {
|
|
|
if (isResetting) return;
|
|
|
isResetting = true;
|
|
|
if (!(propName in initialValues)) {
|
|
|
isResetting = false;
|
|
|
return;
|
|
|
}
|
|
|
let updatedValue = value.map(group => {
|
|
|
if (group.properties) {
|
|
|
group.properties = group.properties.map(prop => {
|
|
|
if (prop.name === propName) {
|
|
|
return { ...prop, value: initialValues[propName] };
|
|
|
}
|
|
|
return prop;
|
|
|
});
|
|
|
}
|
|
|
return group;
|
|
|
});
|
|
|
value = updatedValue;
|
|
|
gradio.dispatch("undo", updatedValue);
|
|
|
setTimeout(() => { isResetting = false; }, 100);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function storeInitialValues() {
|
|
|
lastValue = JSON.stringify(value);
|
|
|
if (Array.isArray(value)) {
|
|
|
value.forEach(group => {
|
|
|
if (Array.isArray(group.properties)) {
|
|
|
group.properties.forEach(prop => {
|
|
|
initialValues[prop.name] = prop.value;
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
setTimeout(updateAllSliders, 50);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
storeInitialValues();
|
|
|
});
|
|
|
$: if (open && groupVisibility) {
|
|
|
tick().then(updateAllSliders);
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<!-- The HTML template renders the component's UI based on the `value` prop. -->
|
|
|
<Block {visible} {elem_id} elem_classes={final_classes} {container} {scale} {min_width} {width}>
|
|
|
{#if loading_status}
|
|
|
<StatusTracker
|
|
|
autoscroll={gradio.autoscroll}
|
|
|
i18n={gradio.i18n}
|
|
|
{...loading_status}
|
|
|
on:clear_status={() => gradio.dispatch("clear_status")}
|
|
|
/>
|
|
|
{/if}
|
|
|
|
|
|
<!-- Main accordion header that toggles the entire component's content -->
|
|
|
<button class="accordion-header" on:click={handle_toggle} disabled={disable_accordion}>
|
|
|
{#if label}
|
|
|
<span class="label">{label}</span>
|
|
|
{/if}
|
|
|
{#if !disable_accordion}
|
|
|
<span class="accordion-icon" style:transform={open ? "rotate(0)" : "rotate(-90deg)"}>▼</span>
|
|
|
{/if}
|
|
|
</button>
|
|
|
|
|
|
<!-- Content wrapper that is shown or hidden based on the 'open' state -->
|
|
|
<div class:closed={!open} class="content-wrapper">
|
|
|
{#if open}
|
|
|
<div class="container" style="--show-group-name: {value.length > 1 || (show_group_name_only_one && value.length === 1) ? 'none' : '1px solid var(--border-color-primary)'}; --sheet-max-height: {height ? `${height}px` : 'none'}">
|
|
|
{#if Array.isArray(value)}
|
|
|
|
|
|
{#each value as group (group.group_name)}
|
|
|
{#if value.length > 1 || (show_group_name_only_one && value.length === 1)}
|
|
|
<button class="group-header" on:click={() => toggleGroup(group.group_name)}>
|
|
|
<span class="group-title">{group.group_name}</span>
|
|
|
<span class="group-toggle-icon">{groupVisibility[group.group_name] ? '−' : '+'}</span>
|
|
|
</button>
|
|
|
{/if}
|
|
|
{#if groupVisibility[group.group_name]}
|
|
|
<div class="properties-grid">
|
|
|
|
|
|
{#if Array.isArray(group.properties)}
|
|
|
{#each group.properties as prop (prop.name)}
|
|
|
{#if prop.visible ?? true}
|
|
|
|
|
|
{@const i_condition = prop.interactive_if}
|
|
|
{@const v_condition = prop.visible_if}
|
|
|
|
|
|
{@const i_parent_value = i_condition ? get_prop_value(i_condition.field) : null}
|
|
|
{@const v_parent_value = v_condition ? get_prop_value(v_condition.field) : null}
|
|
|
|
|
|
{@const is_interactive = interactive && (
|
|
|
!i_condition ?
|
|
|
true
|
|
|
:
|
|
|
i_condition.value !== undefined ?
|
|
|
Array.isArray(i_condition.value) ?
|
|
|
i_condition.value.includes(i_parent_value)
|
|
|
:
|
|
|
i_parent_value === i_condition.value
|
|
|
:
|
|
|
i_condition.neq !== undefined ?
|
|
|
i_parent_value !== i_condition.neq
|
|
|
:
|
|
|
true
|
|
|
)}
|
|
|
|
|
|
{@const is_visible = (prop.visible ?? true) && (
|
|
|
!v_condition ?
|
|
|
true
|
|
|
:
|
|
|
v_condition.value !== undefined ?
|
|
|
Array.isArray(v_condition.value) ?
|
|
|
v_condition.value.includes(v_parent_value)
|
|
|
:
|
|
|
v_parent_value === v_condition.value
|
|
|
:
|
|
|
v_condition.neq !== undefined ?
|
|
|
v_parent_value !== v_condition.neq
|
|
|
:
|
|
|
true // Fallback
|
|
|
)}
|
|
|
{#if is_visible}
|
|
|
<label class="prop-label" for={prop.name}>
|
|
|
<div class="prop-label-wrapper">
|
|
|
<span>{prop.label}</span>
|
|
|
|
|
|
{#if prop.help}
|
|
|
<div class="tooltip-container">
|
|
|
<span class="tooltip-icon">?</span>
|
|
|
<span class="tooltip-text">{prop.help}</span>
|
|
|
</div>
|
|
|
{/if}
|
|
|
</div>
|
|
|
</label>
|
|
|
|
|
|
<div class="prop-control">
|
|
|
|
|
|
{#if prop.component === 'string'}
|
|
|
<input
|
|
|
type="text"
|
|
|
bind:value={prop.value}
|
|
|
disabled={!is_interactive}
|
|
|
on:change={() => dispatch_update("change", prop)}
|
|
|
on:input={() => dispatch_update("input", prop)}
|
|
|
/>
|
|
|
{:else if prop.component === 'password'}
|
|
|
<input
|
|
|
type="password"
|
|
|
bind:value={prop.value}
|
|
|
disabled={!is_interactive}
|
|
|
on:change={() => dispatch_update("change", prop)}
|
|
|
on:input={() => dispatch_update("input", prop)}
|
|
|
/>
|
|
|
{:else if prop.component === 'checkbox'}
|
|
|
<input
|
|
|
type="checkbox"
|
|
|
bind:checked={prop.value}
|
|
|
disabled={!is_interactive}
|
|
|
on:change={() => dispatch_update("change", prop)}
|
|
|
/>
|
|
|
{:else if prop.component === 'number_integer' || prop.component === 'number_float'}
|
|
|
<input
|
|
|
class:invalid={validationState[prop.name] === false}
|
|
|
class:disabled={!is_interactive}
|
|
|
type="number"
|
|
|
step={prop.step || 1}
|
|
|
bind:value={prop.value}
|
|
|
disabled={!is_interactive}
|
|
|
on:change={() => dispatch_update("change", prop)}
|
|
|
on:input={() => {
|
|
|
validate_prop(prop);
|
|
|
dispatch_update("input", prop);
|
|
|
}}
|
|
|
/>
|
|
|
{:else if prop.component === 'slider'}
|
|
|
<div class="slider-container" class:disabled={!is_interactive}>
|
|
|
<input
|
|
|
type="range"
|
|
|
min={prop.minimum}
|
|
|
max={prop.maximum}
|
|
|
step={prop.step || 1}
|
|
|
bind:value={prop.value}
|
|
|
bind:this={sliderElements[prop.name]}
|
|
|
disabled={!is_interactive}
|
|
|
on:input={() => {
|
|
|
validate_prop(prop);
|
|
|
updateSliderBackground(prop, sliderElements[prop.name]);
|
|
|
dispatch_update("input", prop);
|
|
|
}}
|
|
|
on:change={() => dispatch_update("change", prop)}
|
|
|
/>
|
|
|
<span class="slider-value">{prop.value}</span>
|
|
|
</div>
|
|
|
{:else if prop.component === 'colorpicker'}
|
|
|
<div class="color-picker-container" class:disabled={!is_interactive}>
|
|
|
<input
|
|
|
type="color"
|
|
|
class="color-picker-input"
|
|
|
bind:value={prop.value}
|
|
|
disabled={!is_interactive}
|
|
|
on:change={() => dispatch_update("change", prop)}
|
|
|
/>
|
|
|
<span class="color-picker-value">{prop.value}</span>
|
|
|
</div>
|
|
|
{:else if prop.component === 'dropdown'}
|
|
|
<div class="dropdown-wrapper" class:disabled={!is_interactive}>
|
|
|
<select
|
|
|
disabled={!is_interactive}
|
|
|
value={prop.value}
|
|
|
on:change={(e) => handle_dropdown_change(e, prop)}
|
|
|
>
|
|
|
{#if Array.isArray(prop.choices)}
|
|
|
{#each prop.choices as choice}
|
|
|
<option value={choice} selected={prop.value === choice}>
|
|
|
{choice}
|
|
|
</option>
|
|
|
{/each}
|
|
|
{/if}
|
|
|
</select>
|
|
|
<div class="dropdown-arrow-icon"></div>
|
|
|
</div>
|
|
|
{:else if prop.component === 'radio'}
|
|
|
<div class="radio-group" class:disabled={!is_interactive} on:change={() => dispatch_update('change', prop)}>
|
|
|
{#if Array.isArray(prop.choices)}
|
|
|
{#each prop.choices as choice}
|
|
|
<div class="radio-item">
|
|
|
<input
|
|
|
type="radio"
|
|
|
id="{prop.name}-{choice}"
|
|
|
name={prop.name}
|
|
|
value={choice}
|
|
|
bind:group={prop.value}
|
|
|
disabled={!is_interactive}
|
|
|
>
|
|
|
<label for="{prop.name}-{choice}">{choice}</label>
|
|
|
</div>
|
|
|
{/each}
|
|
|
{/if}
|
|
|
</div>
|
|
|
{:else if prop.component === 'multiselect_checkbox'}
|
|
|
<div class="multiselect-group" class:disabled={!is_interactive}>
|
|
|
{#if Array.isArray(prop.choices)}
|
|
|
{#each prop.choices as choice}
|
|
|
<div class="multiselect-item">
|
|
|
<input
|
|
|
type="checkbox"
|
|
|
id="{prop.name}-{choice}"
|
|
|
value={choice}
|
|
|
bind:group={prop.value}
|
|
|
disabled={!is_interactive}
|
|
|
on:change={() => handle_multiselect_change()}
|
|
|
>
|
|
|
<label for="{prop.name}-{choice}">{choice}</label>
|
|
|
</div>
|
|
|
{/each}
|
|
|
{/if}
|
|
|
</div>
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
{#if prop.component !== 'checkbox'}
|
|
|
<button
|
|
|
class="reset-button-prop"
|
|
|
class:visible={initialValues[prop.name] !== prop.value}
|
|
|
title="Reset to default"
|
|
|
on:click|stopPropagation={() => handle_reset_prop(prop.name)}
|
|
|
disabled={!is_interactive}
|
|
|
>
|
|
|
↺
|
|
|
</button>
|
|
|
{/if}
|
|
|
</div>
|
|
|
{/if}
|
|
|
{/if}
|
|
|
{/each}
|
|
|
{/if}
|
|
|
</div>
|
|
|
{/if}
|
|
|
{/each}
|
|
|
{/if}
|
|
|
</div>
|
|
|
{/if}
|
|
|
</div>
|
|
|
</Block>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
:host {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
height: 100%;
|
|
|
}
|
|
|
:global(.propertysheet-wrapper) {
|
|
|
overflow: hidden !important;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
flex-grow: 1;
|
|
|
padding: var(--spacing-lg) !important;
|
|
|
}
|
|
|
.accordion-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
width: 100%;
|
|
|
cursor: pointer;
|
|
|
padding: var(--block-title-padding);
|
|
|
background: var(--block-title-background-fill);
|
|
|
color: var(--block-title-text-color);
|
|
|
border-width: 0;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
.accordion-icon{
|
|
|
margin-left: auto;
|
|
|
}
|
|
|
.content-wrapper {
|
|
|
flex-grow: 1;
|
|
|
min-height: 0;
|
|
|
}
|
|
|
.container {
|
|
|
overflow-y: auto;
|
|
|
height: auto;
|
|
|
max-height: var(--sheet-max-height, 500px);
|
|
|
border-radius: 0 !important;
|
|
|
border: 1px solid var(--border-color-primary);
|
|
|
border-top: var(--show-group-name);
|
|
|
border-bottom-left-radius: var(--radius-lg);
|
|
|
border-bottom-right-radius: var(--radius-lg);
|
|
|
background-color: var(--background-fill-secondary);
|
|
|
}
|
|
|
.closed {
|
|
|
display: none;
|
|
|
}
|
|
|
.group-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
width: 100%;
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
background-color: var(--input-background-fill);
|
|
|
color: var(--body-text-color);
|
|
|
text-align: left;
|
|
|
cursor: pointer;
|
|
|
font-size: var(--text-md);
|
|
|
font-weight: var(--font-weight-bold);
|
|
|
border: 1px solid var(--border-color-primary);
|
|
|
}
|
|
|
.properties-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: 1fr 2fr;
|
|
|
gap: 0;
|
|
|
padding: 0;
|
|
|
}
|
|
|
.prop-label,
|
|
|
.prop-control {
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
border-bottom: 1px solid var(--background-fill-secondary);
|
|
|
}
|
|
|
|
|
|
.prop-label {
|
|
|
background-color: var(--background-fill-primary);
|
|
|
color: var(--body-text-color);
|
|
|
opacity: 3.7;
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
font-size: var(--text-xs);
|
|
|
text-align: right;
|
|
|
justify-content: flex-end;
|
|
|
word-break: break-word;
|
|
|
}
|
|
|
.prop-control {
|
|
|
gap: var(--spacing-sm);
|
|
|
}
|
|
|
.properties-grid > :nth-last-child(-n+2) {
|
|
|
border-bottom: none;
|
|
|
}
|
|
|
.prop-control input[type="text"],
|
|
|
.prop-control input[type="password"],
|
|
|
.prop-control input[type="number"] {
|
|
|
background-color: var(--input-background-fill);
|
|
|
border: var(--input-border-width) solid var(--border-color-primary);
|
|
|
box-shadow: var(--input-shadow);
|
|
|
color: var(--input-text-color);
|
|
|
font-size: var(--input-text-size);
|
|
|
border-radius: 0;
|
|
|
width: 100%;
|
|
|
padding-top: var(--spacing-1);
|
|
|
padding-bottom: var(--spacing-1);
|
|
|
padding-left: var(--spacing-md);
|
|
|
padding-right: var(--spacing-3);
|
|
|
}
|
|
|
.prop-control input[type="text"]:focus,
|
|
|
.prop-control input[type="number"]:focus {
|
|
|
box-shadow: var(--input-shadow-focus);
|
|
|
border-color: var(--input-border-color-focus);
|
|
|
background-color: var(--input-background-fill-focus);
|
|
|
outline: none;
|
|
|
}
|
|
|
.dropdown-wrapper {
|
|
|
position: relative;
|
|
|
width: 100%;
|
|
|
}
|
|
|
.dropdown-wrapper select {
|
|
|
-webkit-appearance: none;
|
|
|
appearance: none;
|
|
|
background-color: var(--input-background-fill);
|
|
|
border: var(--input-border-width) solid var(--border-color-primary);
|
|
|
box-shadow: var(--input-shadow);
|
|
|
color: var(--input-text-color);
|
|
|
font-size: var(--input-text-size);
|
|
|
width: 100%;
|
|
|
cursor: pointer;
|
|
|
border-radius: 0;
|
|
|
padding-top: var(--spacing-1);
|
|
|
padding-bottom: var(--spacing-1);
|
|
|
padding-left: var(--spacing-md);
|
|
|
padding-right: calc(var(--spacing-3) + 1.2em);
|
|
|
}
|
|
|
.dropdown-arrow-icon {
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
right: var(--spacing-3);
|
|
|
transform: translateY(-50%);
|
|
|
width: 1em;
|
|
|
height: 1em;
|
|
|
pointer-events: none;
|
|
|
z-index: 1;
|
|
|
background-color: var(--body-text-color-subdued);
|
|
|
-webkit-mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
|
|
mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M6 8l4 4 4-4'%3e%3c/svg%3e");
|
|
|
}
|
|
|
.dropdown-wrapper select:focus {
|
|
|
box-shadow: var(--input-shadow-focus);
|
|
|
border-color: var(--input-border-color-focus);
|
|
|
background-color: var(--input-background-fill-focus);
|
|
|
outline: none;
|
|
|
}
|
|
|
.dropdown-wrapper select option {
|
|
|
background: var(--input-background-fill);
|
|
|
color: var(--body-text-color);
|
|
|
}
|
|
|
.prop-control input[type="checkbox"] {
|
|
|
-webkit-appearance: none;
|
|
|
appearance: none;
|
|
|
position: relative;
|
|
|
width: var(--size-4);
|
|
|
height: var(--size-4);
|
|
|
border-radius: 5px !important;
|
|
|
border: 1px solid var(--checkbox-border-color);
|
|
|
background-color: var(--checkbox-background-color);
|
|
|
box-shadow: var(--checkbox-shadow);
|
|
|
cursor: pointer;
|
|
|
margin: 0;
|
|
|
transition: background-color 0.2s, border-color 0.2s;
|
|
|
}
|
|
|
.prop-control input[type="checkbox"]:hover {
|
|
|
border-color: var(--checkbox-border-color-hover);
|
|
|
background-color: var(--checkbox-background-color-hover);
|
|
|
}
|
|
|
.prop-control input[type="checkbox"]:focus {
|
|
|
border-color: var(--checkbox-border-color-focus);
|
|
|
background-color: var(--checkbox-background-color-focus);
|
|
|
outline: none;
|
|
|
}
|
|
|
.prop-control input[type="checkbox"]:checked {
|
|
|
background-color: var(--checkbox-background-color-selected);
|
|
|
border-color: var(--checkbox-border-color-focus);
|
|
|
}
|
|
|
.prop-control input[type="checkbox"]:checked::after {
|
|
|
content: "";
|
|
|
position: absolute;
|
|
|
display: block;
|
|
|
top: 50%;
|
|
|
left: 50%;
|
|
|
width: 4px;
|
|
|
height: 8px;
|
|
|
border: solid var(--checkbox-label-text-color-selected);
|
|
|
border-width: 0 2px 2px 0;
|
|
|
transform: translate(-50%, -60%) rotate(45deg);
|
|
|
|
|
|
}
|
|
|
.slider-container {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--spacing-md);
|
|
|
width: 100%;
|
|
|
}
|
|
|
.slider-container input[type="range"] {
|
|
|
--slider-progress: 0%;
|
|
|
-webkit-appearance: none;
|
|
|
appearance: none;
|
|
|
background: transparent;
|
|
|
cursor: pointer;
|
|
|
width: 100%;
|
|
|
}
|
|
|
.slider-container input[type="range"]::-webkit-slider-runnable-track {
|
|
|
height: 8px;
|
|
|
border-radius: var(--radius-lg);
|
|
|
background: linear-gradient( to right, var(--slider-color) var(--slider-progress), var(--input-background-fill) var(--slider-progress) );
|
|
|
}
|
|
|
.slider-container input[type="range"]::-webkit-slider-thumb {
|
|
|
-webkit-appearance: none;
|
|
|
appearance: none;
|
|
|
margin-top: -4px;
|
|
|
background-color: white;
|
|
|
border-radius: 50%;
|
|
|
height: 16px;
|
|
|
width: 16px;
|
|
|
border: 1px solid var(--border-color-primary);
|
|
|
box-shadow: var(--shadow-drop);
|
|
|
}
|
|
|
.slider-container input[type="range"]::-moz-range-track {
|
|
|
height: 8px;
|
|
|
border-radius: var(--radius-lg);
|
|
|
background: linear-gradient( to right, var(--slider-color) var(--slider-progress), var(--input-background-fill) var(--slider-progress) );
|
|
|
}
|
|
|
.slider-container input[type="range"]::-moz-range-thumb {
|
|
|
background-color: white;
|
|
|
border-radius: 50%;
|
|
|
height: 16px;
|
|
|
width: 16px;
|
|
|
border: 1px solid var(--border-color-primary);
|
|
|
box-shadow: var(--shadow-drop);
|
|
|
}
|
|
|
.slider-value {
|
|
|
min-width: 40px;
|
|
|
text-align: right;
|
|
|
font-family: var(--font-mono);
|
|
|
font-size: var(--text-xs);
|
|
|
}
|
|
|
.prop-label-wrapper {
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
align-items: center;
|
|
|
gap: var(--spacing-sm);
|
|
|
width: 100%;
|
|
|
}
|
|
|
.tooltip-container {
|
|
|
position: relative;
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
.tooltip-icon {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
width: 14px;
|
|
|
height: 14px;
|
|
|
border-radius: 50%;
|
|
|
background-color: var(--body-text-color-subdued);
|
|
|
color: var(--background-fill-primary);
|
|
|
font-size: 10px;
|
|
|
font-weight: bold;
|
|
|
cursor: help;
|
|
|
user-select: none;
|
|
|
}
|
|
|
.tooltip-text {
|
|
|
visibility: hidden;
|
|
|
width: 200px;
|
|
|
background-color: var(--body-text-color);
|
|
|
color: var(--background-fill-primary);
|
|
|
text-align: center;
|
|
|
border-radius: var(--radius-md);
|
|
|
padding: var(--spacing-md);
|
|
|
position: absolute;
|
|
|
z-index: 50;
|
|
|
bottom: -50%;
|
|
|
left: 100%;
|
|
|
transform: translateX(-50%);
|
|
|
opacity: 0;
|
|
|
transition: opacity 0.3s;
|
|
|
}
|
|
|
.tooltip-container:hover .tooltip-text {
|
|
|
visibility: visible;
|
|
|
opacity: 1.0;
|
|
|
}
|
|
|
.color-picker-container {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--spacing-md);
|
|
|
width: 100%;
|
|
|
}
|
|
|
.color-picker-input {
|
|
|
width: 50px;
|
|
|
height: 28px;
|
|
|
background-color: transparent;
|
|
|
border: 1px solid var(--border-color-primary);
|
|
|
border-radius: var(--radius-sm);
|
|
|
cursor: pointer;
|
|
|
padding: 0;
|
|
|
}
|
|
|
.color-picker-input::-webkit-color-swatch-wrapper {
|
|
|
padding: 2px;
|
|
|
}
|
|
|
.color-picker-input::-moz-padding {
|
|
|
padding: 2px;
|
|
|
}
|
|
|
.color-picker-input::-webkit-color-swatch {
|
|
|
border: none;
|
|
|
border-radius: var(--radius-sm);
|
|
|
}
|
|
|
.color-picker-input::-moz-color-swatch {
|
|
|
border: none;
|
|
|
border-radius: var(--radius-sm);
|
|
|
}
|
|
|
.color-picker-value {
|
|
|
font-family: var(--font-mono);
|
|
|
font-size: var(--text-sm);
|
|
|
color: var(--body-text-color-subdued);
|
|
|
}
|
|
|
.prop-control input.invalid {
|
|
|
border-color: var(--error-border-color, red) !important;
|
|
|
box-shadow: 0 0 0 1px var(--error-border-color, red) !important;
|
|
|
}
|
|
|
.reset-button-prop {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
background: none;
|
|
|
border: none;
|
|
|
border-left: 1px solid var(--border-color-primary);
|
|
|
cursor: pointer;
|
|
|
color: var(--body-text-color-subdued);
|
|
|
font-size: var(--text-lg);
|
|
|
padding: 0 var(--spacing-2);
|
|
|
visibility: hidden;
|
|
|
opacity: 0;
|
|
|
transition: opacity 150ms ease-in-out, color 150ms ease-in-out;
|
|
|
}
|
|
|
.reset-button-prop.visible {
|
|
|
visibility: visible;
|
|
|
opacity: 1;
|
|
|
}
|
|
|
.reset-button-prop:hover {
|
|
|
color: var(--body-text-color);
|
|
|
background-color: var(--background-fill-secondary-hover);
|
|
|
}
|
|
|
.reset-button-prop:disabled {
|
|
|
color: var(--body-text-color-subdued) !important;
|
|
|
opacity: 0.5;
|
|
|
cursor: not-allowed;
|
|
|
background-color: transparent !important;
|
|
|
}
|
|
|
.prop-control .disabled {
|
|
|
opacity: 0.5;
|
|
|
pointer-events: none;
|
|
|
cursor: not-allowed;
|
|
|
}
|
|
|
.prop-control .disabled input {
|
|
|
cursor: not-allowed;
|
|
|
}
|
|
|
.reset-button-prop:disabled {
|
|
|
opacity: 0.3;
|
|
|
cursor: not-allowed;
|
|
|
background-color: transparent !important;
|
|
|
}
|
|
|
|
|
|
.radio-group {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
gap: var(--spacing-sm);
|
|
|
width: 100%;
|
|
|
}
|
|
|
.radio-item input[type="radio"] {
|
|
|
|
|
|
display: none;
|
|
|
}
|
|
|
.radio-item label {
|
|
|
display: inline-block;
|
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
|
border: 1px solid var(--border-color-primary);
|
|
|
border-radius: 5px !important;
|
|
|
background-color: var(--input-background-fill);
|
|
|
color: var(--body-text-color);
|
|
|
font-size: var(--text-xs);
|
|
|
cursor: pointer;
|
|
|
user-select: none;
|
|
|
transition: background-color 0.2s, border-color 0.2s, color 0.2s;
|
|
|
}
|
|
|
.radio-group.disabled .radio-item label {
|
|
|
cursor: not-allowed;
|
|
|
}
|
|
|
.radio-item input[type="radio"]:hover + label {
|
|
|
border-color: var(--border-color-accent-subdued);
|
|
|
background-color: var(--background-fill-secondary-hover);
|
|
|
}
|
|
|
.radio-item input[type="radio"]:checked + label {
|
|
|
background-color: var(--primary-500);
|
|
|
border-color: var(--primary-500);
|
|
|
color: white;
|
|
|
font-weight: var(--font-weight-bold);
|
|
|
}
|
|
|
.radio-group.disabled .radio-item input[type="radio"]:checked + label {
|
|
|
background-color: var(--neutral-300);
|
|
|
border-color: var(--neutral-300);
|
|
|
color: var(--neutral-500);
|
|
|
}
|
|
|
.multiselect-group {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--spacing-sm);
|
|
|
width: 100%;
|
|
|
max-height: 150px;
|
|
|
overflow-y: auto;
|
|
|
border: 1px solid var(--border-color-primary);
|
|
|
padding: var(--spacing-sm);
|
|
|
background-color: var(--input-background-fill);
|
|
|
}
|
|
|
|
|
|
.multiselect-item {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--spacing-sm);
|
|
|
}
|
|
|
|
|
|
.multiselect-item label {
|
|
|
font-size: var(--text-sm);
|
|
|
color: var(--body-text-color);
|
|
|
cursor: pointer;
|
|
|
user-select: none;
|
|
|
}
|
|
|
|
|
|
.multiselect-group.disabled .multiselect-item label {
|
|
|
cursor: not-allowed;
|
|
|
}
|
|
|
</style> |