feat: update - use design system components
This commit is contained in:
@@ -1,21 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Button from './ui/Button.svelte';
|
||||||
import { downloadBlob, encode } from '$lib/core/io';
|
import { downloadBlob, encode } from '$lib/core/io';
|
||||||
import type { PixelImage } from '$lib/core/types';
|
import type { PixelImage } from '$lib/core/types';
|
||||||
import type { OutputFormat } from '$lib/registry';
|
import type { OutputFormat } from '$lib/registry';
|
||||||
|
|
||||||
let {
|
interface Props {
|
||||||
image,
|
|
||||||
format,
|
|
||||||
baseName,
|
|
||||||
params,
|
|
||||||
onError
|
|
||||||
}: {
|
|
||||||
image: PixelImage | null;
|
image: PixelImage | null;
|
||||||
format?: OutputFormat;
|
format?: OutputFormat;
|
||||||
baseName: string;
|
baseName: string;
|
||||||
params: Record<string, unknown>;
|
params: Record<string, unknown>;
|
||||||
onError?: (e: unknown) => void;
|
onError?: (e: unknown) => void;
|
||||||
} = $props();
|
}
|
||||||
|
|
||||||
|
let { image, format, baseName, params, onError }: Props = $props();
|
||||||
|
|
||||||
let busy = $state(false);
|
let busy = $state(false);
|
||||||
|
|
||||||
@@ -36,6 +33,13 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button class="primary" onclick={download} disabled={!image || !format || busy}>
|
<Button
|
||||||
{busy ? 'Готовим файл…' : `Скачать .${format?.ext ?? 'png'}`}
|
variant="primary"
|
||||||
</button>
|
fullWidth
|
||||||
|
disabled={!image || !format}
|
||||||
|
{busy}
|
||||||
|
busyText="Готовим файл…"
|
||||||
|
onclick={download}
|
||||||
|
>
|
||||||
|
Скачать .{format?.ext ?? 'png'}
|
||||||
|
</Button>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="dropzone"
|
class="dropzone panel"
|
||||||
class:dragging
|
class:dragging
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -81,16 +81,14 @@
|
|||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
min-height: 14rem;
|
min-height: 14rem;
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
border: 2px dashed var(--border);
|
border-style: dashed;
|
||||||
border-radius: var(--radius-m);
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition:
|
transition:
|
||||||
border-color 120ms ease,
|
border-color var(--transition-fast),
|
||||||
background 120ms ease;
|
background var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropzone:hover,
|
.dropzone:hover,
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ImageInfo } from '$lib/core/analyze';
|
import type { ImageInfo } from '$lib/core/analyze';
|
||||||
|
|
||||||
let { info }: { info: ImageInfo | null } = $props();
|
interface Props {
|
||||||
|
info: ImageInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { info }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if info}
|
{#if info}
|
||||||
<dl>
|
<dl>
|
||||||
<div>
|
<div class="panel">
|
||||||
<dt>Размеры</dt>
|
<dt>Размеры</dt>
|
||||||
<dd>{info.width} × {info.height} px</dd>
|
<dd>{info.width} × {info.height} px</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="panel">
|
||||||
<dt>Альфа-канал</dt>
|
<dt>Альфа-канал</dt>
|
||||||
<dd>{info.hasAlpha ? 'есть — есть полупрозрачные пиксели' : 'нет'}</dd>
|
<dd>{info.hasAlpha ? 'есть — есть полупрозрачные пиксели' : 'нет'}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="panel">
|
||||||
<dt>Уникальных цветов (RGBA)</dt>
|
<dt>Уникальных цветов (RGBA)</dt>
|
||||||
<dd>{info.colorCount.toLocaleString('ru-RU')}</dd>
|
<dd>{info.colorCount.toLocaleString('ru-RU')}</dd>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,9 +37,6 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
padding: var(--space-2) var(--space-3);
|
padding: var(--space-2) var(--space-3);
|
||||||
background: var(--surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-s);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dt {
|
dt {
|
||||||
|
|||||||
@@ -1,89 +1,39 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import CheckboxField from './ui/CheckboxField.svelte';
|
||||||
|
import ColorField from './ui/ColorField.svelte';
|
||||||
|
import SelectField from './ui/SelectField.svelte';
|
||||||
|
import TextField from './ui/TextField.svelte';
|
||||||
import type { ParamDef } from '$lib/registry';
|
import type { ParamDef } from '$lib/registry';
|
||||||
|
|
||||||
let {
|
interface Props {
|
||||||
params,
|
|
||||||
values = $bindable()
|
|
||||||
}: {
|
|
||||||
params: ParamDef[];
|
params: ParamDef[];
|
||||||
values: Record<string, any>;
|
values: Record<string, any>;
|
||||||
} = $props();
|
}
|
||||||
|
|
||||||
|
let { params, values = $bindable() }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each params as param (param.id)}
|
{#each params as param (param.id)}
|
||||||
<div class="field">
|
{#if param.type === 'checkbox'}
|
||||||
{#if param.type === 'checkbox'}
|
<CheckboxField id={param.id} label={param.label} bind:checked={values[param.id]} />
|
||||||
<label class="checkbox">
|
{:else if param.type === 'number'}
|
||||||
<input type="checkbox" bind:checked={values[param.id]} />
|
<TextField
|
||||||
{param.label}
|
id={param.id}
|
||||||
</label>
|
label={param.label}
|
||||||
{:else}
|
type="number"
|
||||||
<label for={param.id}>{param.label}</label>
|
min={param.min}
|
||||||
{#if param.type === 'number'}
|
max={param.max}
|
||||||
<input
|
step={param.step}
|
||||||
id={param.id}
|
bind:value={values[param.id]}
|
||||||
type="number"
|
/>
|
||||||
min={param.min}
|
{:else if param.type === 'select'}
|
||||||
max={param.max}
|
<SelectField
|
||||||
step={param.step}
|
id={param.id}
|
||||||
bind:value={values[param.id]}
|
label={param.label}
|
||||||
/>
|
options={param.options}
|
||||||
{:else if param.type === 'select'}
|
bind:value={values[param.id]}
|
||||||
<select id={param.id} bind:value={values[param.id]}>
|
/>
|
||||||
{#each param.options as option (option.value)}
|
{:else if param.type === 'color'}
|
||||||
<option value={option.value}>{option.label}</option>
|
<ColorField id={param.id} label={param.label} bind:value={values[param.id]} />
|
||||||
{/each}
|
{/if}
|
||||||
</select>
|
|
||||||
{:else if param.type === 'color'}
|
|
||||||
<input id={param.id} type="color" bind:value={values[param.id]} />
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<style>
|
|
||||||
.field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-1);
|
|
||||||
margin-bottom: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
label,
|
|
||||||
.checkbox {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
select {
|
|
||||||
padding: var(--space-1) var(--space-2);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-s);
|
|
||||||
background: var(--surface);
|
|
||||||
max-width: 16rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='number'] {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='color'] {
|
|
||||||
height: 2.4rem;
|
|
||||||
padding: 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
color: var(--text);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox input {
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
import { decodeFile } from '$lib/core/io';
|
import { decodeFile } from '$lib/core/io';
|
||||||
import type { PixelImage } from '$lib/core/types';
|
import type { PixelImage } from '$lib/core/types';
|
||||||
import { defaultParams, outputOf, sanitizeParams, type ToolEntry } from '$lib/registry';
|
import { defaultParams, outputOf, sanitizeParams, type ToolEntry } from '$lib/registry';
|
||||||
|
import Button from './ui/Button.svelte';
|
||||||
import DownloadButton from './DownloadButton.svelte';
|
import DownloadButton from './DownloadButton.svelte';
|
||||||
import DropZone from './DropZone.svelte';
|
import DropZone from './DropZone.svelte';
|
||||||
|
import EmptyState from './ui/EmptyState.svelte';
|
||||||
import InfoPanel from './InfoPanel.svelte';
|
import InfoPanel from './InfoPanel.svelte';
|
||||||
import ParamForm from './ParamForm.svelte';
|
import ParamForm from './ParamForm.svelte';
|
||||||
import Preview from './Preview.svelte';
|
import Preview from './Preview.svelte';
|
||||||
@@ -75,10 +77,10 @@
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h1>{tool.title}</h1>
|
<h1>{tool.title}</h1>
|
||||||
<p class="description">{tool.description}</p>
|
<p class="description text-muted">{tool.description}</p>
|
||||||
|
|
||||||
{#if errorText}
|
{#if errorText}
|
||||||
<div class="error" role="alert">{errorText}</div>
|
<div class="error-banner" role="alert">{errorText}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !source}
|
{#if !source}
|
||||||
@@ -86,28 +88,37 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<div class="result-col">
|
<div class="result-col">
|
||||||
<h2>{isInfo ? 'Изображение' : 'Результат'}</h2>
|
<h2 class="heading-section">{isInfo ? 'Изображение' : 'Результат'}</h2>
|
||||||
{#if status === 'processing' && !result}
|
{#if status === 'processing' && !result}
|
||||||
<p class="hint">Обработка…</p>
|
<EmptyState title="Обработка…" hint="Изображение обрабатывается, это займёт немного времени" />
|
||||||
{:else}
|
{:else}
|
||||||
<Preview image={isInfo ? source : result} />
|
<Preview image={isInfo ? source : result} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if isInfo && info}
|
{#if isInfo && info}
|
||||||
<InfoPanel {info} />
|
<InfoPanel {info} />
|
||||||
{/if}
|
{/if}
|
||||||
<button class="secondary" onclick={reset}>Загрузить другое изображение</button>
|
<div class="reset-row">
|
||||||
|
<Button variant="secondary" onclick={reset}>Загрузить другое изображение</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !isInfo}
|
{#if !isInfo}
|
||||||
<div class="params-col">
|
<div class="params-col">
|
||||||
<h2>Параметры</h2>
|
<h2 class="heading-section">Параметры</h2>
|
||||||
{#if tool.params.length > 0}
|
{#if tool.params.length > 0}
|
||||||
<ParamForm params={tool.params} bind:values />
|
<ParamForm params={tool.params} bind:values />
|
||||||
<button class="primary" onclick={apply} disabled={status === 'processing'}>
|
<Button
|
||||||
{status === 'processing' ? 'Обработка…' : 'Применить'}
|
onclick={apply}
|
||||||
</button>
|
busy={status === 'processing'}
|
||||||
|
busyText="Обработка…"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
Применить
|
||||||
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="hint">У этого инструмента нет параметров — результат уже готов.</p>
|
<p class="hint text-caption text-muted">
|
||||||
|
У этого инструмента нет параметров — результат уже готов.
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="download-row">
|
<div class="download-row">
|
||||||
<DownloadButton
|
<DownloadButton
|
||||||
@@ -130,19 +141,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
color: var(--text-muted);
|
|
||||||
max-width: 48rem;
|
max-width: 48rem;
|
||||||
margin-bottom: var(--space-4);
|
margin-bottom: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error-banner {
|
||||||
padding: var(--space-2) var(--space-3);
|
|
||||||
margin-bottom: var(--space-3);
|
margin-bottom: var(--space-3);
|
||||||
border: 1px solid #e5484d;
|
|
||||||
border-radius: var(--radius-s);
|
|
||||||
background: color-mix(in srgb, #e5484d 8%, var(--surface));
|
|
||||||
color: #b3261e;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout {
|
.layout {
|
||||||
@@ -161,33 +165,19 @@
|
|||||||
.result-col h2,
|
.result-col h2,
|
||||||
.params-col h2 {
|
.params-col h2 {
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-col button.secondary {
|
.reset-row {
|
||||||
margin-top: var(--space-3);
|
margin-top: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.params-col button.primary {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-row {
|
.download-row {
|
||||||
margin-top: var(--space-4);
|
margin-top: var(--space-4);
|
||||||
padding-top: var(--space-3);
|
padding-top: var(--space-3);
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-row :global(button) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-bottom: var(--space-3);
|
margin-bottom: var(--space-3);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
busy?: boolean;
|
busy?: boolean;
|
||||||
busyText?: string;
|
busyText?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
onclick?: () => void;
|
onclick?: () => void;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
}
|
}
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
busy = false,
|
busy = false,
|
||||||
busyText = '',
|
busyText = '',
|
||||||
|
fullWidth = false,
|
||||||
onclick,
|
onclick,
|
||||||
children
|
children
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
@@ -24,7 +26,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
{type}
|
{type}
|
||||||
class={variant}
|
class={fullWidth ? `${variant} fullwidth` : variant}
|
||||||
aria-busy={busy}
|
aria-busy={busy}
|
||||||
disabled={disabled || busy}
|
disabled={disabled || busy}
|
||||||
onclick={onclick}
|
onclick={onclick}
|
||||||
@@ -74,4 +76,8 @@
|
|||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fullwidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -25,7 +25,9 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>Все операции выполняются локально в вашем браузере — файлы никуда не отправляются.</p>
|
<p class="text-caption text-muted">
|
||||||
|
Все операции выполняются локально в вашем браузере — файлы никуда не отправляются.
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -66,7 +68,7 @@
|
|||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.9rem;
|
font-size: var(--text-s);
|
||||||
padding: var(--space-1) var(--space-2);
|
padding: var(--space-1) var(--space-2);
|
||||||
border-radius: var(--radius-s);
|
border-radius: var(--radius-s);
|
||||||
}
|
}
|
||||||
@@ -88,8 +90,6 @@
|
|||||||
footer {
|
footer {
|
||||||
padding: var(--space-3) var(--space-4);
|
padding: var(--space-3) var(--space-4);
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1>easy-png-tools</h1>
|
<h1>easy-png-tools</h1>
|
||||||
<p class="lead">
|
<p class="lead text-muted">
|
||||||
Набор утилит для работы с PNG. Все операции выполняются локально в браузере — файлы никуда не
|
Набор утилит для работы с PNG. Все операции выполняются локально в браузере — файлы никуда не
|
||||||
отправляются.
|
отправляются.
|
||||||
</p>
|
</p>
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
{@const categoryTools = TOOLS.filter((tool) => tool.category === category.id)}
|
{@const categoryTools = TOOLS.filter((tool) => tool.category === category.id)}
|
||||||
{#if categoryTools.length > 0}
|
{#if categoryTools.length > 0}
|
||||||
<section id={category.id} class="category" aria-labelledby="{category.id}-heading">
|
<section id={category.id} class="category" aria-labelledby="{category.id}-heading">
|
||||||
<h2 id="{category.id}-heading">{category.label}</h2>
|
<h2 id="{category.id}-heading" class="heading-section">{category.label}</h2>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{#each categoryTools as tool (tool.id)}
|
{#each categoryTools as tool (tool.id)}
|
||||||
<a class="card" href="/tools/{tool.id}">
|
<a class="card panel" href="/tools/{tool.id}">
|
||||||
<span class="card-title">{tool.title}</span>
|
<span class="card-title">{tool.title}</span>
|
||||||
<span class="card-desc">{tool.description}</span>
|
<span class="card-desc text-caption text-muted">{tool.description}</span>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +40,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lead {
|
.lead {
|
||||||
color: var(--text-muted);
|
|
||||||
max-width: 48rem;
|
max-width: 48rem;
|
||||||
margin-bottom: var(--space-5);
|
margin-bottom: var(--space-5);
|
||||||
}
|
}
|
||||||
@@ -51,10 +50,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
margin-bottom: var(--space-3);
|
margin-bottom: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,27 +64,23 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
padding: var(--space-3) var(--space-3) var(--space-4);
|
padding: var(--space-3) var(--space-3) var(--space-4);
|
||||||
background: var(--surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-m);
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
transition:
|
transition:
|
||||||
border-color 120ms ease,
|
border-color var(--transition-fast),
|
||||||
box-shadow 120ms ease,
|
box-shadow var(--transition-fast),
|
||||||
transform 120ms ease;
|
transform var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 4px 14px rgb(37 99 235 / 12%);
|
box-shadow: var(--shadow-card);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.95rem;
|
font-size: var(--text-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover .card-title {
|
.card:hover .card-title {
|
||||||
@@ -97,8 +88,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-desc {
|
.card-desc {
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
line-clamp: 3;
|
line-clamp: 3;
|
||||||
|
|||||||
Reference in New Issue
Block a user