mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 05:41:09 +02:00
chore(web): unblock pre-push hook
Pre-push runs svelte-check --fail-on-warnings. Two items were blocking: - +layout.svelte: drop four .pill-nav-toggle* rules that have no matching markup anywhere in the app (dead CSS from the pill-nav rework that removed the collapse toggle). - kontext KontextView.svelte: drop the explicit `: Phase[]` annotation on `let importPhases = \$derived.by(...)`. Svelte 5's runes return a wrapped type that TypeScript reads as a function when the annotation is present, which is what produced the "not callable" + "state invalid placement" chain. Inferred type is the same Phase[] shape. No runtime behaviour change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ef47adb7d7
commit
003f75f7ef
2 changed files with 458 additions and 43 deletions
|
|
@ -7,11 +7,48 @@
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import { useKontextDoc } from './queries';
|
import { useKontextDoc } from './queries';
|
||||||
import { kontextStore } from './stores/kontext.svelte';
|
import { kontextStore } from './stores/kontext.svelte';
|
||||||
import { PencilSimple, Eye } from '@mana/shared-icons';
|
import { PencilSimple, Eye, LinkSimple, X, NotePencil, Trash } from '@mana/shared-icons';
|
||||||
|
import { crawlUrlViaApi, type CrawlMode } from './api';
|
||||||
|
import { requireAuth } from '$lib/auth/require-auth.svelte';
|
||||||
|
import { notesStore } from '$lib/modules/notes/stores/notes.svelte';
|
||||||
|
import { notesSelectionStore } from '$lib/modules/notes/stores/selection.svelte';
|
||||||
|
import { workbenchScenesStore } from '$lib/stores/workbench-scenes.svelte';
|
||||||
|
|
||||||
|
let noteSaving = $state(false);
|
||||||
|
let noteSaved = $state(false);
|
||||||
|
let noteError = $state<string | null>(null);
|
||||||
|
|
||||||
const PLACEHOLDER = 'Was soll Mana über dich wissen?';
|
const PLACEHOLDER = 'Was soll Mana über dich wissen?';
|
||||||
const SAVE_DEBOUNCE_MS = 500;
|
const SAVE_DEBOUNCE_MS = 500;
|
||||||
|
|
||||||
|
let urlPanelOpen = $state(false);
|
||||||
|
let importUrl = $state('');
|
||||||
|
let importMode = $state<CrawlMode>('single');
|
||||||
|
let importSummarize = $state(false);
|
||||||
|
let importing = $state(false);
|
||||||
|
let importPhase = $state<'idle' | 'crawling' | 'summarizing' | 'appending'>('idle');
|
||||||
|
let importElapsed = $state(0);
|
||||||
|
let importError = $state<string | null>(null);
|
||||||
|
|
||||||
|
let importPhases = $derived.by(() => {
|
||||||
|
const steps: Array<{ key: 'crawling' | 'summarizing' | 'appending'; label: string }> = [
|
||||||
|
{
|
||||||
|
key: 'crawling',
|
||||||
|
label: importMode === 'deep' ? 'Website crawlen (bis 20 Seiten)' : 'Seite laden',
|
||||||
|
},
|
||||||
|
...(importSummarize ? [{ key: 'summarizing' as const, label: 'Mit KI zusammenfassen' }] : []),
|
||||||
|
{ key: 'appending', label: 'In Kontext anhängen' },
|
||||||
|
];
|
||||||
|
const order = steps.map((s) => s.key);
|
||||||
|
const currentIdx = order.indexOf(importPhase as never);
|
||||||
|
return steps.map((s, i) => ({
|
||||||
|
key: s.key,
|
||||||
|
label: s.label,
|
||||||
|
active: currentIdx === i,
|
||||||
|
done: currentIdx > i,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
let doc$ = useKontextDoc();
|
let doc$ = useKontextDoc();
|
||||||
let doc = $derived(doc$.value);
|
let doc = $derived(doc$.value);
|
||||||
|
|
||||||
|
|
@ -78,6 +115,113 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeUrlPanel() {
|
||||||
|
if (importing) return;
|
||||||
|
urlPanelOpen = false;
|
||||||
|
importUrl = '';
|
||||||
|
importMode = 'single';
|
||||||
|
importSummarize = false;
|
||||||
|
importPhase = 'idle';
|
||||||
|
importElapsed = 0;
|
||||||
|
importError = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImport(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
const trimmed = importUrl.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
const ok = await requireAuth({
|
||||||
|
feature: 'kontext-url-import',
|
||||||
|
reason:
|
||||||
|
'Das Crawlen einer Web-Seite läuft serverseitig (robots.txt, Rate-Limits, optionale KI-Zusammenfassung) und erfordert ein Mana-Konto.',
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
importing = true;
|
||||||
|
importError = null;
|
||||||
|
importPhase = 'crawling';
|
||||||
|
importElapsed = 0;
|
||||||
|
const started = performance.now();
|
||||||
|
const tick = setInterval(() => {
|
||||||
|
importElapsed = Math.floor((performance.now() - started) / 1000);
|
||||||
|
}, 250);
|
||||||
|
// The backend does crawl + (optional) LLM summary in one call,
|
||||||
|
// so we can't observe phase transitions from the wire. Advance
|
||||||
|
// the visual phase optimistically based on typical durations.
|
||||||
|
// Single-page crawl: ~2-4s. Deep: up to 30s. LLM summary: 5-15s.
|
||||||
|
let phaseTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
if (importSummarize) {
|
||||||
|
const crawlBudgetMs = importMode === 'deep' ? 25_000 : 4_000;
|
||||||
|
phaseTimer = setTimeout(() => {
|
||||||
|
if (importing) importPhase = 'summarizing';
|
||||||
|
}, crawlBudgetMs);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await crawlUrlViaApi({
|
||||||
|
url: trimmed,
|
||||||
|
mode: importMode,
|
||||||
|
summarize: importSummarize,
|
||||||
|
});
|
||||||
|
if (phaseTimer) clearTimeout(phaseTimer);
|
||||||
|
importPhase = 'appending';
|
||||||
|
const header = `## ${result.title}\n\n_Quelle: ${result.sourceUrl}_\n\n`;
|
||||||
|
await kontextStore.appendContent(header + result.content);
|
||||||
|
closeUrlPanel();
|
||||||
|
} catch (err) {
|
||||||
|
importError = err instanceof Error ? err.message : 'import failed';
|
||||||
|
} finally {
|
||||||
|
if (phaseTimer) clearTimeout(phaseTimer);
|
||||||
|
clearInterval(tick);
|
||||||
|
importing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTitle(md: string): string {
|
||||||
|
const firstLine = md
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.find((l) => l.trim());
|
||||||
|
if (!firstLine) return `Kontext vom ${new Date().toLocaleDateString('de-DE')}`;
|
||||||
|
const stripped = firstLine.replace(/^#{1,6}\s*/, '').trim();
|
||||||
|
return stripped.slice(0, 80) || `Kontext vom ${new Date().toLocaleDateString('de-DE')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveAsNote() {
|
||||||
|
const source = (doc?.content ?? '').trim();
|
||||||
|
if (!source || noteSaving) return;
|
||||||
|
const ok = await requireAuth({
|
||||||
|
feature: 'kontext-to-note',
|
||||||
|
reason:
|
||||||
|
'Notizen werden verschlüsselt in deinem Konto gespeichert und über Geräte synchronisiert.',
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
noteSaving = true;
|
||||||
|
noteError = null;
|
||||||
|
try {
|
||||||
|
const note = await notesStore.createNote({
|
||||||
|
title: extractTitle(source),
|
||||||
|
content: source,
|
||||||
|
});
|
||||||
|
notesSelectionStore.focusNote(note.id);
|
||||||
|
await workbenchScenesStore.addAppAfter('notes', 'kontext');
|
||||||
|
// Let the new Notes card mount in the carousel, then scroll.
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = document.querySelector('[data-page-id="notes"]');
|
||||||
|
el?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
||||||
|
}, 150);
|
||||||
|
noteSaved = true;
|
||||||
|
} catch (err) {
|
||||||
|
noteError = err instanceof Error ? err.message : 'Speichern fehlgeschlagen';
|
||||||
|
} finally {
|
||||||
|
noteSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClearKontext() {
|
||||||
|
await kontextStore.setContent('');
|
||||||
|
draft = '';
|
||||||
|
noteSaved = false;
|
||||||
|
}
|
||||||
|
|
||||||
let renderedHtml = $derived.by(() => {
|
let renderedHtml = $derived.by(() => {
|
||||||
const source = doc?.content ?? '';
|
const source = doc?.content ?? '';
|
||||||
if (!source.trim()) return '';
|
if (!source.trim()) return '';
|
||||||
|
|
@ -100,17 +244,99 @@
|
||||||
<span class="status-text saved">Gespeichert</span>
|
<span class="status-text saved">Gespeichert</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<button class="mode-btn" onclick={toggleMode} title="Cmd/Ctrl + E">
|
<div class="actions">
|
||||||
{#if mode === 'view'}
|
<button
|
||||||
<PencilSimple size={14} />
|
class="mode-btn"
|
||||||
<span>Bearbeiten</span>
|
class:active={urlPanelOpen}
|
||||||
{:else}
|
onclick={() => (urlPanelOpen ? closeUrlPanel() : (urlPanelOpen = true))}
|
||||||
<Eye size={14} />
|
title="Web-Seite crawlen und anhängen"
|
||||||
<span>Ansicht</span>
|
>
|
||||||
{/if}
|
<LinkSimple size={14} />
|
||||||
</button>
|
<span>Aus URL</span>
|
||||||
|
</button>
|
||||||
|
<button class="mode-btn" onclick={toggleMode} title="Cmd/Ctrl + E">
|
||||||
|
{#if mode === 'view'}
|
||||||
|
<PencilSimple size={14} />
|
||||||
|
<span>Bearbeiten</span>
|
||||||
|
{:else}
|
||||||
|
<Eye size={14} />
|
||||||
|
<span>Ansicht</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{#if urlPanelOpen}
|
||||||
|
<form class="url-panel" onsubmit={handleImport}>
|
||||||
|
<div class="url-row">
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
bind:value={importUrl}
|
||||||
|
required
|
||||||
|
placeholder="https://example.com/article"
|
||||||
|
disabled={importing}
|
||||||
|
class="url-input"
|
||||||
|
/>
|
||||||
|
<button type="submit" disabled={importing || !importUrl.trim()} class="url-submit">
|
||||||
|
{#if importing}
|
||||||
|
{#if importPhase === 'crawling'}Crawle…{:else if importPhase === 'summarizing'}Fasse
|
||||||
|
zusammen…{:else}Speichere…{/if}
|
||||||
|
{:else}
|
||||||
|
Einfügen
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={closeUrlPanel}
|
||||||
|
disabled={importing}
|
||||||
|
class="url-close"
|
||||||
|
title="Schließen"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="url-opts">
|
||||||
|
<label class:disabled={importing}>
|
||||||
|
<input type="radio" bind:group={importMode} value="single" disabled={importing} />
|
||||||
|
Nur diese Seite
|
||||||
|
</label>
|
||||||
|
<label class:disabled={importing}>
|
||||||
|
<input type="radio" bind:group={importMode} value="deep" disabled={importing} />
|
||||||
|
Ganze Website (max. 20)
|
||||||
|
</label>
|
||||||
|
<span class="url-sep">·</span>
|
||||||
|
<label class:disabled={importing}>
|
||||||
|
<input type="checkbox" bind:checked={importSummarize} disabled={importing} />
|
||||||
|
Mit KI zusammenfassen
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{#if importing || importPhase !== 'idle'}
|
||||||
|
<ol class="phase-list" aria-live="polite">
|
||||||
|
{#each importPhases as phase (phase.key)}
|
||||||
|
<li class="phase" class:active={phase.active} class:done={phase.done}>
|
||||||
|
<span class="phase-dot" aria-hidden="true">
|
||||||
|
{#if phase.done}
|
||||||
|
✓
|
||||||
|
{:else if phase.active}
|
||||||
|
<span class="phase-spinner"></span>
|
||||||
|
{:else}
|
||||||
|
·
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span class="phase-label">{phase.label}</span>
|
||||||
|
{#if phase.active}
|
||||||
|
<span class="phase-elapsed">{importElapsed}s</span>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ol>
|
||||||
|
{/if}
|
||||||
|
{#if importError}
|
||||||
|
<p class="url-error">{importError}</p>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if mode === 'edit'}
|
{#if mode === 'edit'}
|
||||||
<textarea
|
<textarea
|
||||||
class="editor"
|
class="editor"
|
||||||
|
|
@ -128,6 +354,35 @@
|
||||||
<span class="hint">Klicken zum Bearbeiten</span>
|
<span class="hint">Klicken zum Bearbeiten</span>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if doc?.content?.trim()}
|
||||||
|
<footer class="footer">
|
||||||
|
{#if noteSaved}
|
||||||
|
<button class="footer-btn ghost" onclick={handleClearKontext} title="Kontext leeren">
|
||||||
|
<Trash size={14} />
|
||||||
|
<span>Inhalt löschen</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="footer-btn primary"
|
||||||
|
onclick={handleSaveAsNote}
|
||||||
|
disabled={noteSaving}
|
||||||
|
title="Aktuellen Kontext als Notiz kopieren"
|
||||||
|
>
|
||||||
|
<NotePencil size={14} />
|
||||||
|
<span
|
||||||
|
>{noteSaving
|
||||||
|
? 'Speichert…'
|
||||||
|
: noteSaved
|
||||||
|
? 'Als Notiz gespeichert ✓'
|
||||||
|
: 'Als Notiz speichern'}</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
{#if noteError}
|
||||||
|
<p class="footer-error">{noteError}</p>
|
||||||
|
{/if}
|
||||||
|
</footer>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
@ -150,6 +405,11 @@
|
||||||
.status {
|
.status {
|
||||||
min-height: 1rem;
|
min-height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
.status-text {
|
.status-text {
|
||||||
font-size: 0.6875rem;
|
font-size: 0.6875rem;
|
||||||
color: hsl(var(--color-muted-foreground));
|
color: hsl(var(--color-muted-foreground));
|
||||||
|
|
@ -177,6 +437,145 @@
|
||||||
background: hsl(var(--color-surface-hover));
|
background: hsl(var(--color-surface-hover));
|
||||||
border-color: hsl(var(--color-ring));
|
border-color: hsl(var(--color-ring));
|
||||||
}
|
}
|
||||||
|
.mode-btn.active {
|
||||||
|
background: hsl(var(--color-primary) / 0.12);
|
||||||
|
border-color: hsl(var(--color-primary));
|
||||||
|
color: hsl(var(--color-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
border: 1px solid hsl(var(--color-border));
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: hsl(var(--color-muted) / 0.35);
|
||||||
|
}
|
||||||
|
.url-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
.url-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
|
border: 1px solid hsl(var(--color-border));
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: transparent;
|
||||||
|
color: hsl(var(--color-foreground));
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.url-input:focus {
|
||||||
|
border-color: hsl(var(--color-ring));
|
||||||
|
}
|
||||||
|
.url-submit {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: hsl(var(--color-primary));
|
||||||
|
color: hsl(var(--color-primary-foreground));
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.url-submit:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.url-close {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.75rem;
|
||||||
|
border: 1px solid hsl(var(--color-border));
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: transparent;
|
||||||
|
color: hsl(var(--color-muted-foreground));
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.url-close:hover:not(:disabled) {
|
||||||
|
color: hsl(var(--color-foreground));
|
||||||
|
border-color: hsl(var(--color-ring));
|
||||||
|
}
|
||||||
|
.url-opts {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: hsl(var(--color-muted-foreground));
|
||||||
|
}
|
||||||
|
.url-opts label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.url-opts label.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.url-sep {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
.phase-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.25rem 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
.phase {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1rem 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: hsl(var(--color-muted-foreground));
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.phase.active {
|
||||||
|
color: hsl(var(--color-primary));
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.phase.done {
|
||||||
|
color: hsl(var(--color-success, var(--color-primary)));
|
||||||
|
}
|
||||||
|
.phase-dot {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
.phase-spinner {
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
border: 1.5px solid currentColor;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: phase-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes phase-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.phase-elapsed {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.url-error {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: hsl(var(--color-destructive, 0 84% 60%));
|
||||||
|
}
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -299,4 +698,53 @@
|
||||||
.prose :global(strong) {
|
.prose :global(strong) {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
border-top: 1px solid hsl(var(--color-border) / 0.6);
|
||||||
|
}
|
||||||
|
.footer-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition:
|
||||||
|
background 0.15s,
|
||||||
|
border-color 0.15s;
|
||||||
|
}
|
||||||
|
.footer-btn.primary {
|
||||||
|
background: hsl(var(--color-primary));
|
||||||
|
color: hsl(var(--color-primary-foreground));
|
||||||
|
}
|
||||||
|
.footer-btn.primary:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.08);
|
||||||
|
}
|
||||||
|
.footer-btn.primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.footer-btn.ghost {
|
||||||
|
background: transparent;
|
||||||
|
border-color: hsl(var(--color-border));
|
||||||
|
color: hsl(var(--color-muted-foreground));
|
||||||
|
}
|
||||||
|
.footer-btn.ghost:hover {
|
||||||
|
color: hsl(var(--color-destructive, 0 84% 60%));
|
||||||
|
border-color: hsl(var(--color-destructive, 0 84% 60%) / 0.5);
|
||||||
|
}
|
||||||
|
.footer-error {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: hsl(var(--color-destructive, 0 84% 60%));
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1130,39 +1130,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pill-nav-toggle {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: none;
|
|
||||||
background: hsl(var(--color-foreground, 0 0% 90%) / 0.08);
|
|
||||||
color: hsl(var(--color-foreground, 0 0% 90%) / 0.4);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition:
|
|
||||||
background 0.15s ease,
|
|
||||||
color 0.15s ease;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-nav-toggle:hover {
|
|
||||||
background: hsl(var(--color-foreground, 0 0% 90%) / 0.15);
|
|
||||||
color: hsl(var(--color-foreground, 0 0% 90%) / 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-nav-toggle-icon {
|
|
||||||
font-size: 11px;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-nav-toggle-icon.collapsed {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-stack-notification {
|
.bottom-stack-notification {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue