chore: standardize web controls

This commit is contained in:
3252a8
2026-05-30 12:16:02 +03:00
parent c6c5352813
commit 8c0e778388
21 changed files with 343 additions and 118 deletions
@@ -1,4 +1,5 @@
<script>
import { Input } from "$components/ui/index.js";
import { Trash2 } from "$components/ui/icons.js";
import { getContext, onMount } from "svelte";
import Dialog from "$components/ui/dialog.svelte";
@@ -106,7 +107,7 @@
<div class="admin-form" data-dialog-content>
<div class="admin-dialog-form-section">
<AdminField label={at("ad_label_source", {}, "Источник")}>
<input
<Input
class="input"
type="text"
placeholder="telegram_ads"
@@ -118,7 +119,7 @@
label={at("ad_label_param", {}, "start-параметр")}
hint={at("ad_hint_param", {}, "Передаётся в /start, должен быть уникален")}
>
<input
<Input
class="input"
type="text"
placeholder="ads_summer25"
@@ -129,7 +130,7 @@
</div>
<div class="admin-dialog-form-section">
<AdminField label={at("ad_label_cost", {}, "Стоимость, RUB")}>
<input
<Input
class="input"
type="number"
step="0.01"
@@ -6,7 +6,7 @@
AdminEmptyState,
AdminSelect,
} from "$components/patterns/admin/index.js";
import { Checkbox } from "$components/ui/index.js";
import { Checkbox, ColorInput, FileInput, Input, RangeInput } from "$components/ui/index.js";
import { Switch } from "$components/ui/primitives.js";
import { getContext, onDestroy, onMount } from "svelte";
@@ -444,10 +444,9 @@
<div class="appearance-controls">
<section class="appearance-control-card">
<input
bind:this={logoFileInput}
<FileInput
bind:element={logoFileInput}
class="appearance-file-input"
type="file"
accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml,image/x-icon"
onchange={handleLogoFileChange}
/>
@@ -461,7 +460,7 @@
{at("appearance_logo_upload_file", {}, "Загрузить файл")}
</AdminButton>
<div class="appearance-url-row">
<input
<Input
class="input appearance-control"
type="url"
placeholder="https://example.com/logo.png"
@@ -490,7 +489,7 @@
<span>{at("appearance_use_emoji_logo", {}, "Использовать emoji-логотип")}</span>
</label>
<div class="appearance-emoji-grid">
<input
<Input
class="input appearance-control"
type="text"
maxlength="8"
@@ -547,10 +546,9 @@
>{at("appearance_use_custom_favicon", {}, "Использовать отдельную favicon")}</span
>
</label>
<input
bind:this={faviconFileInput}
<FileInput
bind:element={faviconFileInput}
class="appearance-file-input"
type="file"
accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml,image/x-icon,.ico"
onchange={handleFaviconFileChange}
/>
@@ -564,7 +562,7 @@
{at("appearance_favicon_upload_file", {}, "Загрузить favicon")}
</AdminButton>
<div class="appearance-url-row">
<input
<Input
class="input appearance-control"
type="url"
placeholder="https://example.com/icon.png"
@@ -657,18 +655,17 @@
</span>
<label class="admin-theme-card-option appearance-color-row">
<span>{at("appearance_theme_accent", {}, "Accent")}</span>
<input
class="admin-color"
class:is-empty={!isThemeAccentSet(theme)}
type="color"
<ColorInput
class={`admin-color${!isThemeAccentSet(theme) ? " is-empty" : ""}`}
value={pickerHex(theme.tokens?.accent)}
ariaLabel={at("appearance_theme_accent", {}, "Accent")}
title={isThemeAccentSet(theme)
? theme.tokens?.accent
: at("appearance_theme_accent_empty", {}, "Не задан")}
onclick={() => openThemeAccentPicker(theme)}
oninput={(event) => setThemeAccent(theme, event.currentTarget.value)}
/>
<input
<Input
class="input appearance-color-text"
type="text"
placeholder={at("appearance_theme_accent_placeholder", {}, "Не задан")}
@@ -693,17 +690,17 @@
"Логотип на главной и входе"
)}</span
>
<input
<RangeInput
class="appearance-logo-scale-range"
type="range"
min="50"
max="300"
step="5"
ariaLabel={at("appearance_theme_home_logo_scale", {}, "Home logo scale")}
value={homeLogoScale(theme)}
oninput={(event) => setThemeHomeLogoScale(theme, event.currentTarget.value)}
onValueChange={(value) => setThemeHomeLogoScale(theme, value)}
/>
<span class="appearance-logo-scale-value">
<input
<Input
class="input"
type="number"
min="50"
@@ -814,7 +811,7 @@
background: color-mix(in srgb, var(--admin-surface-2) 40%, transparent);
}
.appearance-file-input {
:global(.appearance-file-input) {
display: none;
}
@@ -949,7 +946,7 @@
width: 100%;
}
.appearance-color-text {
:global(.appearance-color-text) {
min-width: 0;
}
@@ -959,7 +956,7 @@
width: 100%;
}
.appearance-logo-scale-range {
:global(.appearance-logo-scale-range) {
width: 100%;
accent-color: var(--accent);
}
@@ -971,14 +968,14 @@
color: var(--admin-text);
}
.appearance-logo-scale-value .input {
:global(.appearance-logo-scale-value .input) {
width: 70px;
min-height: 32px;
padding: 4px 8px;
font-size: 12px;
}
.admin-color.is-empty {
:global(.admin-color.is-empty) {
opacity: 0.42;
filter: grayscale(1);
}
@@ -8,7 +8,7 @@
AdminTable,
AdminTableSkeleton,
} from "$components/patterns/admin/index.js";
import { Checkbox, RadioGroup, RadioGroupItem } from "$components/ui/index.js";
import { Checkbox, FileInput, RadioGroup, RadioGroupItem } from "$components/ui/index.js";
import {
CheckCircle2,
Database,
@@ -178,10 +178,9 @@
? at("backups_uploading", {}, "Загрузка...")
: at("backups_upload", {}, "Загрузить архив")}
</AdminButton>
<input
bind:this={fileInput}
<FileInput
bind:element={fileInput}
class="backups-file-input"
type="file"
accept=".zip,application/zip"
on:change={uploadSelectedFile}
/>
@@ -362,7 +361,7 @@
align-items: center;
}
.backups-file-input {
:global(.backups-file-input) {
position: absolute;
width: 1px;
height: 1px;
@@ -1,4 +1,5 @@
<script>
import { Textarea } from "$components/ui/index.js";
import { Send } from "$components/ui/icons.js";
import { getContext } from "svelte";
import { Label } from "$components/ui/primitives.js";
@@ -31,12 +32,12 @@
<Label.Root class="admin-field-label">
<span>{at("broadcast_label_text", {}, "Текст сообщения")}</span>
<small>{at("broadcast_hint_text", {}, "Поддерживается HTML-разметка Telegram")}</small>
<textarea
<Textarea
class="admin-textarea"
rows="6"
value={broadcastText}
on:input={(e) => broadcastStore.updateField({ broadcastText: e.target.value })}
></textarea>
/>
</Label.Root>
<div style="display:flex; gap:8px; align-items:center;">
<AdminButton
@@ -1,4 +1,5 @@
<script>
import { Input } from "$components/ui/index.js";
import { getContext, onMount } from "svelte";
import {
AdminButton,
@@ -31,7 +32,7 @@
<div class="admin-toolbar admin-toolbar-card">
<div class="admin-toolbar-search admin-toolbar-search-actions">
<input
<Input
type="search"
class="input"
placeholder={at("logs_user_filter_placeholder", {}, "Фильтр по ID пользователя")}
@@ -1,4 +1,5 @@
<script>
import { Input } from "$components/ui/index.js";
import { Trash2 } from "$components/ui/icons.js";
import { getContext, onMount } from "svelte";
import Dialog from "$components/ui/dialog.svelte";
@@ -110,7 +111,7 @@
<div class="admin-form" data-dialog-content>
<div class="admin-dialog-form-section">
<AdminField label={at("promo_label_code", {}, "Код")}>
<input
<Input
type="text"
class="input"
value={promoDraft.code}
@@ -122,7 +123,7 @@
<div class="admin-dialog-form-section">
<div class="admin-form-row-2">
<AdminField label={at("promo_label_bonus_days", {}, "Бонус (дней)")}>
<input
<Input
type="number"
class="input"
min="1"
@@ -131,7 +132,7 @@
/>
</AdminField>
<AdminField label={at("promo_label_max_activations", {}, "Макс. активаций")}>
<input
<Input
type="number"
class="input"
min="1"
@@ -141,7 +142,7 @@
</AdminField>
</div>
<AdminField label={at("promo_label_valid_days", {}, "Срок действия (дней от текущего)")}>
<input
<Input
type="number"
class="input"
min="1"
@@ -1,4 +1,5 @@
<script>
import { ColorInput, FileInput, Input, Textarea } from "$components/ui/index.js";
import {
Check,
ChevronRight,
@@ -515,13 +516,13 @@
>
</div>
{:else if field.type === "color"}
<input
<ColorInput
class="admin-color"
type="color"
value={valueFor(field) || "#00fe7a"}
ariaLabel={fieldLabelText(field)}
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
/>
<input
<Input
class="input"
type="text"
value={valueFor(field) || ""}
@@ -560,7 +561,7 @@
onValueChange={(value) => settingsStore.markDirty(field.key, value)}
/>
{:else if field.type === "int" || field.type === "float"}
<input
<Input
class="input"
type="number"
step={field.type === "float" ? "0.1" : "1"}
@@ -571,19 +572,18 @@
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
/>
{:else if field.type === "text"}
<textarea
<Textarea
class="admin-setting-textarea"
rows="4"
placeholder={fieldPlaceholderText(field)}
value={valueFor(field) ?? ""}
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
></textarea>
/>
{:else if field.type === "json"}
<div class="admin-json-toolbar">
<input
<FileInput
id={"json-file-" + field.key}
class="admin-json-file-input"
type="file"
accept="application/json,.json"
onchange={(event) => handleJsonFile(field, event)}
/>
@@ -605,16 +605,16 @@
</AdminButton>
{/if}
</div>
<textarea
<Textarea
class="admin-setting-textarea admin-setting-json-textarea"
rows="10"
spellcheck="false"
placeholder={fieldPlaceholderText(field)}
value={valueFor(field) ?? ""}
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
></textarea>
/>
{:else if field.secret}
<input
<Input
class="input"
type={revealed ? "text" : "password"}
placeholder={secretPlaceholder(field)}
@@ -631,7 +631,7 @@
{#if revealed}<EyeOff size={13} />{:else}<Eye size={13} />{/if}
</AdminButton>
{:else}
<input
<Input
class="input"
type="text"
placeholder={fieldPlaceholderText(field)}
@@ -816,7 +816,7 @@
<div class="admin-icon-picker-toolbar">
<label class="admin-icon-picker-search">
<Search size={15} />
<input
<Input
bind:value={iconPickerSearch}
class="input"
type="text"
@@ -11,7 +11,7 @@
import { TicketMessageBubble } from "$components/patterns/webapp/index.js";
import Dialog from "$components/ui/dialog.svelte";
import { Search } from "$components/ui/icons.js";
import { ScrollArea, Skeleton } from "$components/ui/index.js";
import { Input, ScrollArea, Skeleton } from "$components/ui/index.js";
export let at = (key) => key;
export let initialTicketId = null;
@@ -215,7 +215,7 @@
<div class="support-admin-toolbar admin-toolbar-card">
<label class="support-admin-search">
<Search size={16} />
<input
<Input
class="input"
type="search"
placeholder={at("support_search", {}, "Поиск")}
@@ -1,4 +1,5 @@
<script>
import { Input } from "$components/ui/index.js";
import { Tabs, Switch, Label } from "$components/ui/primitives.js";
import Dialog from "$components/ui/dialog.svelte";
import { Plus, Save, Trash2, X } from "$components/ui/icons.js";
@@ -71,7 +72,7 @@
"Латиницей, без пробелов. Используется в платежах и подписках, менять после публикации не рекомендуется"
)}</small
>
<input
<Input
class="input"
type="text"
placeholder="standard"
@@ -127,7 +128,7 @@
<div class="admin-form-row admin-form-row-2">
<Label.Root class="admin-field-label">
<span>{at("tariff_label_name_ru", {}, "Название · RU")}</span>
<input
<Input
class="input"
type="text"
placeholder={at("tariff_placeholder_name_ru", {}, "Стандарт")}
@@ -136,7 +137,7 @@
</Label.Root>
<Label.Root class="admin-field-label">
<span>{at("tariff_label_name_en", {}, "Название · EN")}</span>
<input
<Input
class="input"
type="text"
placeholder={at("tariff_placeholder_name_en", {}, "Standard")}
@@ -148,7 +149,7 @@
<div class="admin-form-row admin-form-row-2">
<Label.Root class="admin-field-label">
<span>{at("tariff_label_desc_ru", {}, "Описание · RU")}</span>
<input
<Input
class="input"
type="text"
placeholder={at("tariff_placeholder_desc_ru", {}, "Базовый набор серверов")}
@@ -157,7 +158,7 @@
</Label.Root>
<Label.Root class="admin-field-label">
<span>{at("tariff_label_desc_en", {}, "Описание · EN")}</span>
<input
<Input
class="input"
type="text"
placeholder={at("tariff_placeholder_desc_en", {}, "Base server pool")}
@@ -211,7 +212,7 @@
"Сколько устройств может одновременно использовать подписку. Пусто — взять значение из .env, 0 — без ограничений"
)}</small
>
<input
<Input
class="input"
type="number"
min="0"
@@ -229,7 +230,7 @@
"Сколько GB включено в тариф на каждый месяц. 0 — безлимитный трафика. Сверху можно докупать пакеты на вкладке «Докупки»"
)}</small
>
<input
<Input
class="input"
type="number"
min="0"
@@ -248,7 +249,7 @@
"По этому курсу остаток подписки пересчитывается в гигабайты при переходе пользователя с тарифа «Период» на «Трафик»"
)}</small
>
<input
<Input
class="input"
type="number"
min="0"
@@ -287,7 +288,7 @@
"Эта строка заменит «Premium-серверы» в кабинете, докупках и карточках лимитов."
)}</small
>
<input
<Input
class="input"
type="text"
placeholder={at("tariff_placeholder_premium_name_ru", {}, "Premium-серверы")}
@@ -303,7 +304,7 @@
"Опционально для английского интерфейса."
)}</small
>
<input
<Input
class="input"
type="text"
placeholder={at("tariff_placeholder_premium_name_en", {}, "Premium servers")}
@@ -359,7 +360,7 @@
"Сколько GB через premium-сквады включено в тариф каждый месяц. 0 или пусто — отдельного premium-лимита нет (premium-нодами можно пользоваться без ограничения)"
)}</small
>
<input
<Input
class="input"
type="number"
min="0"
@@ -409,7 +410,7 @@
{/if}
{#each tariffDraft.premiumTopupRubRows as row, index}
<div class="admin-row-editor-line">
<input
<Input
class="input"
type="number"
min="0.1"
@@ -418,7 +419,7 @@
bind:value={row.gb}
aria-label={at("tariff_col_volume_gb", {}, "Объём premium-пакета в GB")}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -449,7 +450,7 @@
{/if}
{#each tariffDraft.premiumTopupStarsRows as row, index}
<div class="admin-row-editor-line">
<input
<Input
class="input"
type="number"
min="0.1"
@@ -458,7 +459,7 @@
bind:value={row.gb}
aria-label={at("tariff_col_volume_gb", {}, "Объём premium-пакета в GB")}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -533,7 +534,7 @@
</div>
{#each tariffDraft.periodRows as row, index}
<div class="admin-row-editor-line admin-row-editor-6">
<input
<Input
class="input"
type="number"
min="1"
@@ -541,7 +542,7 @@
bind:value={row.months}
aria-label={at("tariff_col_period_months", {}, "Срок (месяцы)")}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -550,7 +551,7 @@
bind:value={row.rub}
aria-label={at("tariff_label_price_rub", {}, "Цена в рублях")}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -559,7 +560,7 @@
bind:value={row.stars}
aria-label={at("tariff_label_price_stars", {}, "Цена в Telegram Stars")}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -568,7 +569,7 @@
bind:value={row.referral_inviter}
aria-label={at("tariff_label_ref_inviter", {}, "Бонус приглашающему")}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -629,7 +630,7 @@
{/if}
{#each tariffDraft.trafficRubRows as row, index}
<div class="admin-row-editor-line">
<input
<Input
class="input"
type="number"
min="0.1"
@@ -638,7 +639,7 @@
bind:value={row.gb}
aria-label={at("tariff_col_volume_gb", {}, "Объём пакета в GB")}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -669,7 +670,7 @@
{/if}
{#each tariffDraft.trafficStarsRows as row, index}
<div class="admin-row-editor-line">
<input
<Input
class="input"
type="number"
min="0.1"
@@ -678,7 +679,7 @@
bind:value={row.gb}
aria-label={at("tariff_col_volume_gb", {}, "Объём пакета в GB")}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -743,7 +744,7 @@
{/if}
{#each tariffDraft.topupRubRows as row, index}
<div class="admin-row-editor-line">
<input
<Input
class="input"
type="number"
min="0.1"
@@ -752,7 +753,7 @@
bind:value={row.gb}
aria-label={at("tariff_col_volume_gb", {}, "Объём пакета в GB")}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -783,7 +784,7 @@
{/if}
{#each tariffDraft.topupStarsRows as row, index}
<div class="admin-row-editor-line">
<input
<Input
class="input"
type="number"
min="0.1"
@@ -792,7 +793,7 @@
bind:value={row.gb}
aria-label={at("tariff_col_volume_gb", {}, "Объём пакета в GB")}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -867,7 +868,7 @@
{/if}
{#each tariffDraft.hwidRubRows as row, index}
<div class="admin-row-editor-line">
<input
<Input
class="input"
type="number"
min="1"
@@ -880,7 +881,7 @@
"Сколько устройств добавляет пакет"
)}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -911,7 +912,7 @@
{/if}
{#each tariffDraft.hwidStarsRows as row, index}
<div class="admin-row-editor-line">
<input
<Input
class="input"
type="number"
min="1"
@@ -924,7 +925,7 @@
"Сколько устройств добавляет пакет"
)}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -1,4 +1,5 @@
<script>
import { Input } from "$components/ui/index.js";
import {
ChevronRight,
RefreshCw,
@@ -403,7 +404,7 @@
<code>TRIAL_DURATION_DAYS</code>
</div>
<div class="admin-setting-control">
<input
<Input
class="input"
type="number"
min="0"
@@ -439,7 +440,7 @@
<code>TRIAL_TRAFFIC_LIMIT_GB</code>
</div>
<div class="admin-setting-control">
<input
<Input
class="input"
type="number"
min="0"
@@ -591,7 +592,7 @@
onValueChange={handleTrialSquadSelect}
/>
{/key}
<input
<Input
class="input"
type="text"
placeholder={valueForKey("USER_SQUAD_UUIDS", settingsDirty, settingsFieldMap) ||
@@ -837,7 +838,7 @@
<Switch.Thumb class="admin-switch-thumb" />
</Switch.Root>
</div>
<input
<Input
class="input"
type="number"
min="0"
@@ -845,7 +846,7 @@
value={valueForKey(rubKey, settingsDirty, settingsFieldMap)}
oninput={(event) => setSetting(rubKey, event.currentTarget.value)}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -853,7 +854,7 @@
value={valueForKey(starsKey, settingsDirty, settingsFieldMap)}
oninput={(event) => setSetting(starsKey, event.currentTarget.value)}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -861,7 +862,7 @@
value={valueForKey(inviterKey, settingsDirty, settingsFieldMap)}
oninput={(event) => setSetting(inviterKey, event.currentTarget.value)}
/>
<input
<Input
class="input"
type="number"
min="0"
@@ -877,7 +878,7 @@
<label class="admin-field-label admin-field-label-compact">
<span>{at("tariffs_legacy_traffic_packages", {}, "Traffic packages")}</span>
<small>{at("tariffs_legacy_traffic_hint", {}, "Format: 10:199,50:799")}</small>
<input
<Input
class="input"
type="text"
value={valueForKey("TRAFFIC_PACKAGES", settingsDirty, settingsFieldMap)}
@@ -889,7 +890,7 @@
>{at("tariffs_legacy_stars_traffic_packages", {}, "Traffic packages, Stars")}</span
>
<small>{at("tariffs_legacy_traffic_hint", {}, "Format: 10:199,50:799")}</small>
<input
<Input
class="input"
type="text"
value={valueForKey("STARS_TRAFFIC_PACKAGES", settingsDirty, settingsFieldMap)}
@@ -1,4 +1,5 @@
<script>
import { Input, Textarea } from "$components/ui/index.js";
import { ChevronRight, Languages, Plus, Search, X } from "$components/ui/icons.js";
import { AdminBadge, AdminButton, AdminEmptyState } from "$components/patterns/admin/index.js";
import { getContext, onDestroy, onMount } from "svelte";
@@ -396,7 +397,7 @@
{#if expanded}
<div class="admin-translation-locale-body" transition:slide={{ duration: 130 }}>
<textarea
<Textarea
class="admin-setting-textarea admin-translation-textarea"
rows="3"
spellcheck="false"
@@ -404,7 +405,7 @@
value={localeValue(item, lang, dirtyEntry)}
oninput={(event) =>
translationsStore.markDirty(lang, item.key, event.currentTarget.value)}
></textarea>
/>
<div class="admin-translation-base">
<small>{baseKind(item, lang)}</small>
<span title={baseValue(item, lang)}>{baseValue(item, lang) || "-"}</span>
@@ -457,7 +458,7 @@
<div class="admin-translations-toolbar">
<label class="admin-translations-search">
<Search size={15} />
<input
<Input
bind:value={search}
class="input"
type="text"
@@ -502,7 +503,7 @@
addLanguage();
}}
>
<input
<Input
bind:value={newLanguageCode}
class="input"
type="text"
@@ -1,6 +1,6 @@
<script>
import { Label, Separator, Tabs } from "$components/ui/primitives.js";
import { Checkbox } from "$components/ui/index.js";
import { Checkbox, Input, Textarea } from "$components/ui/index.js";
import Dialog from "$components/ui/dialog.svelte";
import {
AdminBadge,
@@ -632,7 +632,7 @@
<Label.Root class="admin-field-label admin-extend-field">
<span>{at("user_label_extend", {}, "Продлить подписку")}</span>
<div class="admin-extend-control">
<input
<Input
class="input"
type="number"
min="1"
@@ -662,7 +662,7 @@
<span>{at("user_premium_override_bonus", {}, "Доп. премиум-трафик, GB")}</span
>
<small>{at("user_premium_override_bonus_hint", {}, "")}</small>
<input
<Input
class="input"
type="number"
min="0"
@@ -741,7 +741,7 @@
>{at("user_regular_override_bonus", {}, "Доп. основной трафик, GB")}</span
>
<small>{at("user_regular_override_bonus_hint", {}, "")}</small>
<input
<Input
class="input"
type="number"
min="0"
@@ -837,7 +837,7 @@
<Label.Root class="admin-field-label admin-extend-field">
<span>{at("user_traffic_grant_gb", {}, "ГБ к выдаче")}</span>
<div class="admin-extend-control">
<input
<Input
class="input"
type="number"
min="0"
@@ -869,12 +869,12 @@
"Поддерживается HTML-разметка Telegram"
)}</small
>
<textarea
<Textarea
class="admin-textarea"
rows="3"
placeholder={at("user_placeholder_msg", {}, "Текст сообщения")}
bind:value={$usersStore.userMessageDraft}
></textarea>
/>
</Label.Root>
<div class="admin-message-actions">
<AdminButton
@@ -1,4 +1,5 @@
<script>
import { Input } from "$components/ui/index.js";
import { Label } from "$components/ui/primitives.js";
import {
AdminBadge,
@@ -107,7 +108,7 @@
<div class="admin-toolbar admin-toolbar-users">
<div class="admin-toolbar-search">
<input
<Input
type="search"
class="input"
placeholder={at("users_search_placeholder", {}, "ID, @username или email")}
@@ -0,0 +1,20 @@
<script>
import { cn } from "$lib/utils.js";
export let value = "#000000";
export let disabled = false;
export let ariaLabel = "";
let className = "";
export { className as class };
</script>
<input
bind:value
class={cn("ui-color-input", className)}
type="color"
{disabled}
aria-label={ariaLabel}
on:input
on:change
{...$$restProps}
/>
@@ -0,0 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
export let element = null;
export let accept = undefined;
export let disabled = false;
let className = "";
export { className as class };
</script>
<input
bind:this={element}
class={cn("ui-file-input", className)}
type="file"
{accept}
{disabled}
on:change
{...$$restProps}
/>
+3
View File
@@ -2,11 +2,14 @@ export { default as AttentionDot } from "./attention-dot.svelte";
export { default as Badge } from "./badge.svelte";
export { default as Button } from "./button.svelte";
export { default as Checkbox } from "./checkbox.svelte";
export { default as ColorInput } from "./color-input.svelte";
export { default as Dialog } from "./dialog.svelte";
export { default as FileInput } from "./file-input.svelte";
export { default as Input } from "./input.svelte";
export { default as LegacyCard } from "./card.svelte";
export { default as RadioGroup } from "./radio-group.svelte";
export { default as RadioGroupItem } from "./radio-group-item.svelte";
export { default as RangeInput } from "./range-input.svelte";
export { default as Skeleton } from "./skeleton.svelte";
export { default as Spinner } from "./spinner.svelte";
export { default as ScrollArea } from "./scroll-area.svelte";
+11 -1
View File
@@ -1 +1,11 @@
export { Accordion, Label, Select, Separator, Switch, Tabs, Tooltip } from "bits-ui";
export {
Accordion,
Label,
ScrollArea,
Select,
Separator,
Slider,
Switch,
Tabs,
Tooltip,
} from "bits-ui";
@@ -0,0 +1,97 @@
<script>
import { cn } from "$lib/utils.js";
import { Slider } from "./primitives.js";
export let value = 0;
export let min = undefined;
export let max = undefined;
export let step = undefined;
export let disabled = false;
export let ariaLabel = "";
export let onValueChange = () => {};
export let onValueCommit = () => {};
let className = "";
export { className as class };
$: sliderValue = Number(value ?? min ?? 0);
$: sliderMin = min === undefined ? undefined : Number(min);
$: sliderMax = max === undefined ? undefined : Number(max);
$: sliderStep = step === undefined ? undefined : Number(step);
function handleValueChange(next) {
value = next;
onValueChange(next);
}
</script>
<Slider.Root
class={cn("ui-range-input", className)}
type="single"
value={sliderValue}
min={sliderMin}
max={sliderMax}
step={sliderStep}
{disabled}
onValueChange={handleValueChange}
{onValueCommit}
{...$$restProps}
>
<Slider.Range class="ui-range-input__range" />
<Slider.Thumb class="ui-range-input__thumb" aria-label={ariaLabel} />
</Slider.Root>
<style>
:global(.ui-range-input) {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 18px;
touch-action: none;
user-select: none;
}
:global(.ui-range-input::before) {
content: "";
position: absolute;
right: 0;
left: 0;
height: 5px;
border-radius: 999px;
background: color-mix(in srgb, var(--admin-border-strong, var(--border)) 70%, transparent);
}
:global(.ui-range-input__range) {
position: absolute;
height: 5px;
border-radius: 999px;
background: var(--accent);
}
:global(.ui-range-input__thumb) {
display: block;
width: 16px;
height: 16px;
border: 2px solid var(--accent);
border-radius: 999px;
background: var(--admin-surface, var(--panel));
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.24);
outline: none;
transition:
box-shadow 0.14s ease,
transform 0.14s ease;
}
:global(.ui-range-input__thumb:hover) {
transform: scale(1.05);
}
:global(.ui-range-input__thumb:focus-visible) {
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 24%, transparent);
}
:global(.ui-range-input[data-disabled]) {
opacity: 0.5;
cursor: not-allowed;
}
</style>
@@ -1,15 +1,86 @@
<script>
import { cn } from "$lib/utils.js";
import { ScrollArea as ScrollAreaPrimitive } from "./primitives.js";
export let maxHeight = "100%";
export let element = null;
export let type = "auto";
let className = "";
export { className as class };
</script>
<div
bind:this={element}
class={`scroll-area ${className}`.trim()}
<ScrollAreaPrimitive.Root
class={cn("scroll-area scroll-area--mono", className)}
style={`max-height:${maxHeight};`}
{type}
{...$$restProps}
>
<slot />
</div>
<ScrollAreaPrimitive.Viewport bind:ref={element} class="scroll-area__viewport">
<slot />
</ScrollAreaPrimitive.Viewport>
<ScrollAreaPrimitive.Scrollbar class="scroll-area__scrollbar" orientation="vertical">
<ScrollAreaPrimitive.Thumb class="scroll-area__thumb" />
</ScrollAreaPrimitive.Scrollbar>
<ScrollAreaPrimitive.Scrollbar
class="scroll-area__scrollbar scroll-area__scrollbar--horizontal"
orientation="horizontal"
>
<ScrollAreaPrimitive.Thumb class="scroll-area__thumb" />
</ScrollAreaPrimitive.Scrollbar>
<ScrollAreaPrimitive.Corner class="scroll-area__corner" />
</ScrollAreaPrimitive.Root>
<style>
:global(.scroll-area) {
position: relative;
overflow: hidden;
}
:global(.scroll-area__viewport) {
width: 100%;
height: 100%;
max-height: inherit;
border-radius: inherit;
}
:global(.scroll-area__scrollbar) {
z-index: 2;
display: flex;
touch-action: none;
user-select: none;
padding: 2px;
transition: background 0.14s ease;
}
:global(.scroll-area__scrollbar[data-orientation="vertical"]) {
width: 10px;
}
:global(.scroll-area__scrollbar[data-orientation="horizontal"]) {
flex-direction: column;
height: 10px;
}
:global(.scroll-area__thumb) {
position: relative;
flex: 1;
border-radius: 999px;
background: color-mix(
in srgb,
var(--admin-muted, var(--muted)) 34%,
var(--admin-border, var(--border))
);
}
:global(.scroll-area__thumb:hover) {
background: color-mix(
in srgb,
var(--admin-muted, var(--muted)) 48%,
var(--admin-border, var(--border))
);
}
:global(.scroll-area__corner) {
background: transparent;
}
</style>
@@ -2,6 +2,7 @@
import { ArrowLeft, RefreshCw } from "$components/ui/icons.js";
import Button from "$components/ui/button.svelte";
import { Input } from "$components/ui/index.js";
import { StatusMessage } from "$components/patterns/webapp/index.js";
export let code = "";
@@ -29,7 +30,7 @@
</header>
<div class="otp-wrap">
<label class="otp-input-wrap">
<input
<Input
bind:value={code}
inputmode="numeric"
autocomplete="one-time-code"
@@ -4,7 +4,7 @@
import { Check, ChevronsUpDown, LifeBuoy, MessageSquarePlus } from "$components/ui/icons.js";
import Button from "$components/ui/button.svelte";
import Card from "$components/ui/card.svelte";
import { Skeleton } from "$components/ui/index.js";
import { Input, Skeleton, Textarea } from "$components/ui/index.js";
import { TicketCard } from "$components/patterns/webapp/index.js";
import { Select, Tabs } from "$components/ui/primitives.js";
import {
@@ -169,7 +169,7 @@
<div class="support-create-panel-inner" in:fade={{ duration: 140 }}>
<label class="support-field">
<span>{t("wa_support_subject")}</span>
<input
<Input
class="input"
bind:value={subject}
maxlength={maxSubjectLength}
@@ -255,13 +255,13 @@
<label class="support-field">
<span>{t("wa_support_message")}</span>
<textarea
<Textarea
class="textarea support-message-input"
bind:value={body}
maxlength={maxBodyLength}
rows="5"
placeholder={t("wa_support_message_placeholder")}
></textarea>
/>
<small>{body.length}/{maxBodyLength}</small>
</label>