chore: improve backup admin controls
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
AdminEmptyState,
|
AdminEmptyState,
|
||||||
AdminSelect,
|
AdminSelect,
|
||||||
} from "$components/patterns/admin/index.js";
|
} from "$components/patterns/admin/index.js";
|
||||||
|
import { Checkbox } from "$components/ui/index.js";
|
||||||
import { Switch } from "$components/ui/primitives.js";
|
import { Switch } from "$components/ui/primitives.js";
|
||||||
import { getContext, onDestroy, onMount } from "svelte";
|
import { getContext, onDestroy, onMount } from "svelte";
|
||||||
|
|
||||||
@@ -343,9 +344,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAdminTheme(event, theme) {
|
function toggleAdminTheme(theme, checked) {
|
||||||
event.stopPropagation();
|
themesStore.toggleAdminUse(theme.key, checked);
|
||||||
themesStore.toggleAdminUse(theme.key, event.currentTarget.checked);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setThemeAccent(theme, value) {
|
function setThemeAccent(theme, value) {
|
||||||
@@ -675,11 +675,11 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="admin-theme-card-option">
|
<label class="admin-theme-card-option">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
checked={theme.use_in_admin !== false}
|
checked={theme.use_in_admin !== false}
|
||||||
disabled={themesSaving}
|
disabled={themesSaving}
|
||||||
onchange={(event) => toggleAdminTheme(event, theme)}
|
ariaLabel={at("themes_use_in_admin", {}, "Use in admin")}
|
||||||
|
onCheckedChange={(checked) => toggleAdminTheme(theme, checked)}
|
||||||
/>
|
/>
|
||||||
<span>{at("themes_use_in_admin", {}, "Использовать в админке")}</span>
|
<span>{at("themes_use_in_admin", {}, "Использовать в админке")}</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -855,10 +855,6 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-theme-card-option input[type="checkbox"] {
|
|
||||||
accent-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-theme-grid {
|
.admin-theme-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
AdminBadge,
|
AdminBadge,
|
||||||
AdminButton,
|
AdminButton,
|
||||||
AdminEmptyState,
|
AdminEmptyState,
|
||||||
|
AdminPagination,
|
||||||
AdminTable,
|
AdminTable,
|
||||||
AdminTableSkeleton,
|
AdminTableSkeleton,
|
||||||
} from "$components/patterns/admin/index.js";
|
} from "$components/patterns/admin/index.js";
|
||||||
|
import { Checkbox, RadioGroup, RadioGroupItem } from "$components/ui/index.js";
|
||||||
import {
|
import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Database,
|
Database,
|
||||||
@@ -16,15 +18,18 @@
|
|||||||
TriangleAlert,
|
TriangleAlert,
|
||||||
Upload,
|
Upload,
|
||||||
} from "$components/ui/icons.js";
|
} from "$components/ui/icons.js";
|
||||||
|
import { Tooltip } from "$components/ui/primitives.js";
|
||||||
|
|
||||||
export let at = (key) => key;
|
export let at = (key) => key;
|
||||||
export let fmtDate = (value) => value;
|
export let fmtDate = (value) => value;
|
||||||
|
|
||||||
|
const BACKUPS_PAGE_SIZE = 10;
|
||||||
const backupsStore = getContext("backupsStore");
|
const backupsStore = getContext("backupsStore");
|
||||||
|
|
||||||
let selectedName = "";
|
let selectedName = "";
|
||||||
let restoreDatabase = true;
|
let restoreDatabase = true;
|
||||||
let restoreCompose = false;
|
let restoreCompose = false;
|
||||||
|
let backupsPage = 0;
|
||||||
let fileInput = null;
|
let fileInput = null;
|
||||||
|
|
||||||
$: ({
|
$: ({
|
||||||
@@ -36,9 +41,18 @@
|
|||||||
backupsRestoring,
|
backupsRestoring,
|
||||||
lastRestore,
|
lastRestore,
|
||||||
} = $backupsStore);
|
} = $backupsStore);
|
||||||
$: if (!selectedName && archives?.length) selectedName = archives[0].name;
|
$: totalArchives = archives?.length || 0;
|
||||||
|
$: backupsPageCount = Math.max(1, Math.ceil(totalArchives / BACKUPS_PAGE_SIZE));
|
||||||
|
$: if (backupsPage > backupsPageCount - 1) backupsPage = backupsPageCount - 1;
|
||||||
|
$: backupsPageStart = backupsPage * BACKUPS_PAGE_SIZE;
|
||||||
|
$: pagedArchives = (archives || []).slice(backupsPageStart, backupsPageStart + BACKUPS_PAGE_SIZE);
|
||||||
|
$: if (!selectedName && archives?.length) {
|
||||||
|
selectedName = archives[0].name;
|
||||||
|
backupsPage = 0;
|
||||||
|
}
|
||||||
$: if (selectedName && archives?.length && !archives.some((item) => item.name === selectedName)) {
|
$: if (selectedName && archives?.length && !archives.some((item) => item.name === selectedName)) {
|
||||||
selectedName = archives[0].name;
|
selectedName = archives[0].name;
|
||||||
|
backupsPage = 0;
|
||||||
}
|
}
|
||||||
$: selectedArchive = (archives || []).find((item) => item.name === selectedName) || null;
|
$: selectedArchive = (archives || []).find((item) => item.name === selectedName) || null;
|
||||||
$: if (selectedArchive && restoreDatabase && !selectedArchive.has_database)
|
$: if (selectedArchive && restoreDatabase && !selectedArchive.has_database)
|
||||||
@@ -82,17 +96,45 @@
|
|||||||
return parts.join(" + ");
|
return parts.join(" + ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectArchive(name) {
|
||||||
|
selectedName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusArchivePage(name) {
|
||||||
|
const index = (archives || []).findIndex((item) => item.name === name);
|
||||||
|
if (index >= 0) backupsPage = Math.floor(index / BACKUPS_PAGE_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function warningsText(warnings) {
|
||||||
|
return (warnings || []).filter(Boolean).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function paginationMeta() {
|
||||||
|
const end = Math.min(backupsPageStart + pagedArchives.length, totalArchives);
|
||||||
|
return at(
|
||||||
|
"backups_pagination_meta",
|
||||||
|
{ from: backupsPageStart + 1, to: end, total: totalArchives },
|
||||||
|
`${backupsPageStart + 1}-${end} / ${totalArchives}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadSelectedFile(event) {
|
async function uploadSelectedFile(event) {
|
||||||
const file = event?.currentTarget?.files?.[0];
|
const file = event?.currentTarget?.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const archive = await backupsStore.uploadArchive(file);
|
const archive = await backupsStore.uploadArchive(file);
|
||||||
if (archive?.name) selectedName = archive.name;
|
if (archive?.name) {
|
||||||
|
selectedName = archive.name;
|
||||||
|
focusArchivePage(archive.name);
|
||||||
|
}
|
||||||
event.currentTarget.value = "";
|
event.currentTarget.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createManualBackup() {
|
async function createManualBackup() {
|
||||||
const archive = await backupsStore.createBackup();
|
const archive = await backupsStore.createBackup();
|
||||||
if (archive?.name) selectedName = archive.name;
|
if (archive?.name) {
|
||||||
|
selectedName = archive.name;
|
||||||
|
focusArchivePage(archive.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreSelected() {
|
async function restoreSelected() {
|
||||||
@@ -167,19 +209,19 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="admin-card-body backups-restore-body">
|
<div class="admin-card-body backups-restore-body">
|
||||||
<label class="backups-check" class:is-disabled={!selectedArchive?.has_database}>
|
<label class="backups-check" class:is-disabled={!selectedArchive?.has_database}>
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
bind:checked={restoreDatabase}
|
bind:checked={restoreDatabase}
|
||||||
disabled={!selectedArchive?.has_database || backupsRestoring}
|
disabled={!selectedArchive?.has_database || backupsRestoring}
|
||||||
|
ariaLabel={at("backups_target_database", {}, "БД")}
|
||||||
/>
|
/>
|
||||||
<Database size={16} />
|
<Database size={16} />
|
||||||
<span>{at("backups_target_database", {}, "БД")}</span>
|
<span>{at("backups_target_database", {}, "БД")}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="backups-check" class:is-disabled={!selectedArchive?.has_compose}>
|
<label class="backups-check" class:is-disabled={!selectedArchive?.has_compose}>
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
bind:checked={restoreCompose}
|
bind:checked={restoreCompose}
|
||||||
disabled={!selectedArchive?.has_compose || backupsRestoring}
|
disabled={!selectedArchive?.has_compose || backupsRestoring}
|
||||||
|
ariaLabel={at("backups_target_compose", {}, "compose-папка")}
|
||||||
/>
|
/>
|
||||||
<Server size={16} />
|
<Server size={16} />
|
||||||
<span>{at("backups_target_compose", {}, "compose-папка")}</span>
|
<span>{at("backups_target_compose", {}, "compose-папка")}</span>
|
||||||
@@ -214,68 +256,95 @@
|
|||||||
<span class="admin-muted">{at("backups_empty", {}, "Архивов пока нет")}</span>
|
<span class="admin-muted">{at("backups_empty", {}, "Архивов пока нет")}</span>
|
||||||
</AdminEmptyState>
|
</AdminEmptyState>
|
||||||
{:else}
|
{:else}
|
||||||
<AdminTable class="backups-table">
|
<RadioGroup
|
||||||
<thead>
|
class="backups-archive-radio-group"
|
||||||
<tr>
|
name="backup-archive"
|
||||||
<th aria-label={at("select", {}, "Выбрать")}></th>
|
value={selectedName}
|
||||||
<th>{at("backups_col_archive", {}, "Архив")}</th>
|
onValueChange={selectArchive}
|
||||||
<th>{at("backups_col_created", {}, "Создан")}</th>
|
>
|
||||||
<th>{at("backups_col_size", {}, "Размер")}</th>
|
<AdminTable class="backups-table">
|
||||||
<th>{at("backups_col_contents", {}, "Состав")}</th>
|
<thead>
|
||||||
<th>{at("backups_col_warnings", {}, "Предупреждения")}</th>
|
<tr>
|
||||||
</tr>
|
<th aria-label={at("select", {}, "Выбрать")}></th>
|
||||||
</thead>
|
<th>{at("backups_col_archive", {}, "Архив")}</th>
|
||||||
<tbody>
|
<th>{at("backups_col_created", {}, "Создан")}</th>
|
||||||
{#each archives as archive (archive.name)}
|
<th>{at("backups_col_size", {}, "Размер")}</th>
|
||||||
<tr class:is-selected={archive.name === selectedName}>
|
<th>{at("backups_col_contents", {}, "Состав")}</th>
|
||||||
<td data-label={at("select", {}, "Выбрать")}>
|
<th>{at("backups_col_warnings", {}, "Предупреждения")}</th>
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="backup-archive"
|
|
||||||
value={archive.name}
|
|
||||||
checked={archive.name === selectedName}
|
|
||||||
on:change={() => (selectedName = archive.name)}
|
|
||||||
aria-label={archive.name}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
class="admin-cell-wrap backups-name"
|
|
||||||
data-label={at("backups_col_archive", {}, "Архив")}
|
|
||||||
>
|
|
||||||
{archive.name}
|
|
||||||
</td>
|
|
||||||
<td data-label={at("backups_col_created", {}, "Создан")}
|
|
||||||
>{fmtDate(archiveDate(archive))}</td
|
|
||||||
>
|
|
||||||
<td data-label={at("backups_col_size", {}, "Размер")}
|
|
||||||
>{formatSize(archive.size_bytes)}</td
|
|
||||||
>
|
|
||||||
<td data-label={at("backups_col_contents", {}, "Состав")}>
|
|
||||||
<span class="backups-badges">
|
|
||||||
{#if archive.has_database}
|
|
||||||
<AdminBadge variant="success">{at("backups_badge_db", {}, "БД")}</AdminBadge>
|
|
||||||
{/if}
|
|
||||||
{#if archive.has_compose}
|
|
||||||
<AdminBadge variant="muted">
|
|
||||||
{at("backups_badge_compose", {}, "Compose")}
|
|
||||||
</AdminBadge>
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td data-label={at("backups_col_warnings", {}, "Предупреждения")}>
|
|
||||||
{#if archive.warnings?.length}
|
|
||||||
<AdminBadge variant="warning">
|
|
||||||
<TriangleAlert size={12} />
|
|
||||||
{archive.warnings.length}
|
|
||||||
</AdminBadge>
|
|
||||||
{:else}
|
|
||||||
<span class="admin-muted">-</span>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</AdminTable>
|
{#each pagedArchives as archive (archive.name)}
|
||||||
|
<tr class:is-selected={archive.name === selectedName}>
|
||||||
|
<td data-label={at("select", {}, "Выбрать")}>
|
||||||
|
<RadioGroupItem
|
||||||
|
value={archive.name}
|
||||||
|
ariaLabel={archive.name}
|
||||||
|
class="backups-radio"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="admin-cell-wrap backups-name"
|
||||||
|
data-label={at("backups_col_archive", {}, "Архив")}
|
||||||
|
>
|
||||||
|
{archive.name}
|
||||||
|
</td>
|
||||||
|
<td data-label={at("backups_col_created", {}, "Создан")}
|
||||||
|
>{fmtDate(archiveDate(archive))}</td
|
||||||
|
>
|
||||||
|
<td data-label={at("backups_col_size", {}, "Размер")}
|
||||||
|
>{formatSize(archive.size_bytes)}</td
|
||||||
|
>
|
||||||
|
<td data-label={at("backups_col_contents", {}, "Состав")}>
|
||||||
|
<span class="backups-badges">
|
||||||
|
{#if archive.has_database}
|
||||||
|
<AdminBadge variant="success">{at("backups_badge_db", {}, "БД")}</AdminBadge>
|
||||||
|
{/if}
|
||||||
|
{#if archive.has_compose}
|
||||||
|
<AdminBadge variant="muted">
|
||||||
|
{at("backups_badge_compose", {}, "Compose")}
|
||||||
|
</AdminBadge>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td data-label={at("backups_col_warnings", {}, "Предупреждения")}>
|
||||||
|
{#if archive.warnings?.length}
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger
|
||||||
|
class="backups-warning-trigger"
|
||||||
|
aria-label={warningsText(archive.warnings)}
|
||||||
|
>
|
||||||
|
<TriangleAlert size={12} />
|
||||||
|
{archive.warnings.length}
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content class="backups-warning-tooltip" side="top" align="end">
|
||||||
|
{#each archive.warnings as warning, index}
|
||||||
|
<p>{index + 1}. {warning}</p>
|
||||||
|
{/each}
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
{:else}
|
||||||
|
<span class="admin-muted">-</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</AdminTable>
|
||||||
|
</RadioGroup>
|
||||||
|
{#if totalArchives > BACKUPS_PAGE_SIZE}
|
||||||
|
<AdminPagination
|
||||||
|
meta={paginationMeta()}
|
||||||
|
prevLabel={at("pagination_prev", {}, "Назад")}
|
||||||
|
nextLabel={at("pagination_next", {}, "Далее")}
|
||||||
|
prevDisabled={backupsPage <= 0}
|
||||||
|
nextDisabled={backupsPage >= backupsPageCount - 1}
|
||||||
|
onPrev={() => (backupsPage = Math.max(0, backupsPage - 1))}
|
||||||
|
onNext={() => (backupsPage = Math.min(backupsPageCount - 1, backupsPage + 1))}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,12 +404,6 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backups-check input {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backups-check.is-disabled {
|
.backups-check.is-disabled {
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
}
|
}
|
||||||
@@ -362,6 +425,56 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.backups-archive-radio-group.ui-radio-group) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.backups-radio.ui-radio-item) {
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.backups-warning-trigger) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border: 1px solid var(--warning-border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--warning-soft);
|
||||||
|
color: var(--warning-text);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: help;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.backups-warning-trigger:focus-visible) {
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--warning) 22%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.backups-warning-tooltip) {
|
||||||
|
z-index: 120;
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
max-width: min(440px, calc(100vw - 32px));
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--admin-surface);
|
||||||
|
color: var(--admin-text);
|
||||||
|
box-shadow: var(--shadow-popover);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.backups-warning-tooltip) p {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.backups-restore-body {
|
.backups-restore-body {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Label, Separator, Tabs } from "$components/ui/primitives.js";
|
import { Label, Separator, Tabs } from "$components/ui/primitives.js";
|
||||||
|
import { Checkbox } from "$components/ui/index.js";
|
||||||
import Dialog from "$components/ui/dialog.svelte";
|
import Dialog from "$components/ui/dialog.svelte";
|
||||||
import {
|
import {
|
||||||
AdminBadge,
|
AdminBadge,
|
||||||
@@ -680,8 +681,7 @@
|
|||||||
<div class="admin-user-action-sheet-footer admin-override-card-footer">
|
<div class="admin-user-action-sheet-footer admin-override-card-footer">
|
||||||
<div class="admin-override-card-toolbar">
|
<div class="admin-override-card-toolbar">
|
||||||
<label class="admin-override-unlimited-label">
|
<label class="admin-override-unlimited-label">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
bind:checked={$usersStore.premiumUnlimitedDraft}
|
bind:checked={$usersStore.premiumUnlimitedDraft}
|
||||||
aria-label={at("user_override_unlimited_short", {}, "Безлимит")}
|
aria-label={at("user_override_unlimited_short", {}, "Безлимит")}
|
||||||
/>
|
/>
|
||||||
@@ -760,8 +760,7 @@
|
|||||||
<div class="admin-user-action-sheet-footer admin-override-card-footer">
|
<div class="admin-user-action-sheet-footer admin-override-card-footer">
|
||||||
<div class="admin-override-card-toolbar">
|
<div class="admin-override-card-toolbar">
|
||||||
<label class="admin-override-unlimited-label">
|
<label class="admin-override-unlimited-label">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
bind:checked={$usersStore.regularUnlimitedDraft}
|
bind:checked={$usersStore.regularUnlimitedDraft}
|
||||||
aria-label={at("user_override_unlimited_short", {}, "Безлимит")}
|
aria-label={at("user_override_unlimited_short", {}, "Безлимит")}
|
||||||
/>
|
/>
|
||||||
@@ -1110,11 +1109,6 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
}
|
}
|
||||||
.admin-override-unlimited-label input[type="checkbox"] {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
@media (max-width: 520px) {
|
@media (max-width: 520px) {
|
||||||
.admin-override-card-toolbar {
|
.admin-override-card-toolbar {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<script>
|
||||||
|
import { Checkbox as CheckboxPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import { Check } from "./icons.js";
|
||||||
|
|
||||||
|
export let checked = false;
|
||||||
|
export let disabled = false;
|
||||||
|
export let indeterminate = false;
|
||||||
|
export let ariaLabel = "";
|
||||||
|
export let onCheckedChange = () => {};
|
||||||
|
let className = "";
|
||||||
|
export { className as class };
|
||||||
|
|
||||||
|
function handleCheckedChange(next) {
|
||||||
|
checked = Boolean(next);
|
||||||
|
onCheckedChange(checked);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
class={cn("ui-checkbox", className)}
|
||||||
|
{checked}
|
||||||
|
{disabled}
|
||||||
|
{indeterminate}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
onCheckedChange={handleCheckedChange}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
{#if checked || indeterminate}
|
||||||
|
<Check size={13} strokeWidth={3} aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.ui-checkbox) {
|
||||||
|
appearance: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex: 0 0 18px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--admin-border-strong, var(--border));
|
||||||
|
border-radius: 4px;
|
||||||
|
background: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--admin-bg, var(--panel)) 84%,
|
||||||
|
var(--admin-surface-2, transparent)
|
||||||
|
);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition:
|
||||||
|
background 0.14s ease,
|
||||||
|
border-color 0.14s ease,
|
||||||
|
box-shadow 0.14s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ui-checkbox:hover) {
|
||||||
|
border-color: var(--admin-ring, var(--accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ui-checkbox[data-state="checked"]),
|
||||||
|
:global(.ui-checkbox[data-state="indeterminate"]) {
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 82%, transparent);
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ui-checkbox:focus-visible) {
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 24%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ui-checkbox:disabled) {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
export { default as AttentionDot } from "./attention-dot.svelte";
|
export { default as AttentionDot } from "./attention-dot.svelte";
|
||||||
export { default as Badge } from "./badge.svelte";
|
export { default as Badge } from "./badge.svelte";
|
||||||
export { default as Button } from "./button.svelte";
|
export { default as Button } from "./button.svelte";
|
||||||
|
export { default as Checkbox } from "./checkbox.svelte";
|
||||||
export { default as Dialog } from "./dialog.svelte";
|
export { default as Dialog } from "./dialog.svelte";
|
||||||
export { default as Input } from "./input.svelte";
|
export { default as Input } from "./input.svelte";
|
||||||
export { default as LegacyCard } from "./card.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 Skeleton } from "./skeleton.svelte";
|
export { default as Skeleton } from "./skeleton.svelte";
|
||||||
export { default as Spinner } from "./spinner.svelte";
|
export { default as Spinner } from "./spinner.svelte";
|
||||||
export { default as ScrollArea } from "./scroll-area.svelte";
|
export { default as ScrollArea } from "./scroll-area.svelte";
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script>
|
||||||
|
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
export let value = "";
|
||||||
|
export let disabled = false;
|
||||||
|
export let ariaLabel = "";
|
||||||
|
let className = "";
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
class={cn("ui-radio-item", className)}
|
||||||
|
{value}
|
||||||
|
{disabled}
|
||||||
|
aria-label={ariaLabel || value}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<span class="ui-radio-indicator" aria-hidden="true"></span>
|
||||||
|
<slot />
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.ui-radio-item) {
|
||||||
|
appearance: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex: 0 0 18px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--admin-border-strong, var(--border));
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--admin-bg, var(--panel)) 84%,
|
||||||
|
var(--admin-surface-2, transparent)
|
||||||
|
);
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition:
|
||||||
|
border-color 0.14s ease,
|
||||||
|
box-shadow 0.14s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ui-radio-item:hover) {
|
||||||
|
border-color: var(--admin-ring, var(--accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ui-radio-item:focus-visible) {
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 24%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ui-radio-item:disabled) {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-radio-indicator {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accent);
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.6);
|
||||||
|
transition:
|
||||||
|
opacity 0.14s ease,
|
||||||
|
transform 0.14s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ui-radio-item[data-state="checked"]) {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ui-radio-item[data-state="checked"]) .ui-radio-indicator {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script>
|
||||||
|
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
export let value = "";
|
||||||
|
export let orientation = "vertical";
|
||||||
|
export let name = "";
|
||||||
|
export let disabled = false;
|
||||||
|
export let onValueChange = () => {};
|
||||||
|
let className = "";
|
||||||
|
export { className as class };
|
||||||
|
|
||||||
|
function handleValueChange(next) {
|
||||||
|
value = next;
|
||||||
|
onValueChange(next);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
class={cn("ui-radio-group", className)}
|
||||||
|
{value}
|
||||||
|
{orientation}
|
||||||
|
{name}
|
||||||
|
{disabled}
|
||||||
|
onValueChange={handleValueChange}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</RadioGroupPrimitive.Root>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.ui-radio-group) {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user