Merge pull request #1 from bogdancornescu/bc_1

Add URL content type support and enhance snippet functionality
This commit is contained in:
Bogdan
2026-06-17 11:50:16 +03:00
committed by GitHub
18 changed files with 260 additions and 55 deletions
+4 -4
View File
@@ -47,15 +47,15 @@ Global hotkey (`Ctrl+Shift+Space` by default) opens a focused floating window
FTS5-powered search across all snippet titles and content. Results update as you type.
### Auto Content Detection
highlight.js inspects content on save and automatically classifies it as `code`, `cli`, or `text`, plus detects the specific language (Rust, Python, bash, SQL, etc.) and creates system tags. You can override the type manually.
highlight.js inspects content on save and automatically classifies it as `code`, `cli`, `text`, or `url`, plus detects the specific language (Rust, Python, bash, SQL, etc.) and creates system tags. URL detection triggers on single-line `http`/`https`/`ftp` content. You can override the type manually.
### Three-Panel Layout
**Left panel** — timeline navigation, tag browser with inline filter, new snippet button, settings access.
**Left panel** — timeline navigation, content type filter (Code / CLI / Text / URL chips), tag browser with inline filter scoped to the active type, new snippet button, settings access.
**Center panel** — full-text search bar, snippet list with type icons and 2-line previews, tag chips (user tags and auto-detected language tags are visually distinct).
**Center panel** — full-text search bar, snippet list with type icons, copy count badge, and 2-line previews; tag chips (user tags and auto-detected language tags are visually distinct). Snippets are ordered by most-copied first, then by recency.
**Right panel** — snippet content, directly editable in place. Explicit save keeps accidental edits from destroying data.
**Right panel** — snippet content, directly editable in place. Explicit save keeps accidental edits from destroying data. A **Copy** button next to the content label copies to clipboard and increments the copy counter.
### Tag System
Two tag sources coexist on each snippet:
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 181 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 648 KiB

+2
View File
@@ -1,2 +1,4 @@
packages:
- '.'
allowBuilds:
esbuild: true
+10 -8
View File
@@ -25,7 +25,7 @@ fn load_tags(conn: &Connection, snippet_id: i64) -> Result<Vec<Tag>, AppError> {
fn load_snippet(conn: &Connection, id: i64) -> Result<Snippet, AppError> {
let mut snippet = conn.query_row(
"SELECT id, title, content, content_type, created_at, updated_at \
"SELECT id, title, content, content_type, copy_count, created_at, updated_at \
FROM snippet WHERE id = ?1",
[id],
|r| {
@@ -35,8 +35,9 @@ fn load_snippet(conn: &Connection, id: i64) -> Result<Snippet, AppError> {
title: r.get(1)?,
content: r.get(2)?,
content_type: ct.parse::<ContentType>().unwrap_or(ContentType::Text),
created_at: r.get(4)?,
updated_at: r.get(5)?,
copy_count: r.get(4)?,
created_at: r.get(5)?,
updated_at: r.get(6)?,
tags: vec![],
})
},
@@ -71,7 +72,7 @@ fn load_snippets_for_ids(conn: &Connection, ids: &[i64]) -> Result<Vec<Snippet>,
.collect::<Vec<_>>()
.join(", ");
let snippet_sql = format!(
"SELECT id, title, content, content_type, created_at, updated_at \
"SELECT id, title, content, content_type, copy_count, created_at, updated_at \
FROM snippet WHERE id IN ({placeholders})"
);
let mut stmt = conn.prepare(&snippet_sql)?;
@@ -83,8 +84,9 @@ fn load_snippets_for_ids(conn: &Connection, ids: &[i64]) -> Result<Vec<Snippet>,
title: r.get(1)?,
content: r.get(2)?,
content_type: ct.parse::<ContentType>().unwrap_or(ContentType::Text),
created_at: r.get(4)?,
updated_at: r.get(5)?,
copy_count: r.get(4)?,
created_at: r.get(5)?,
updated_at: r.get(6)?,
tags: vec![],
})
})?
@@ -119,7 +121,7 @@ fn db_list_snippets(
let ids: Vec<i64> = match tag_filter {
None => {
let mut stmt = conn
.prepare("SELECT id FROM snippet ORDER BY created_at DESC, id DESC")?;
.prepare("SELECT id FROM snippet ORDER BY copy_count DESC, created_at DESC, id DESC")?;
let rows = stmt.query_map([], |r| r.get(0))?
.collect::<Result<Vec<_>, _>>()?;
rows
@@ -130,7 +132,7 @@ fn db_list_snippets(
JOIN snippet_tag st ON st.snippet_id = s.id \
JOIN tag t ON t.id = st.tag_id \
WHERE t.name = ?1 AND st.source != 'suppressed' \
ORDER BY s.created_at DESC, s.id DESC",
ORDER BY s.copy_count DESC, s.created_at DESC, s.id DESC",
)?;
let rows = stmt.query_map([tag], |r| r.get(0))?
.collect::<Result<Vec<_>, _>>()?;
+4
View File
@@ -6,6 +6,7 @@ pub enum ContentType {
Code,
Cli,
Text,
Url,
}
impl std::fmt::Display for ContentType {
@@ -14,6 +15,7 @@ impl std::fmt::Display for ContentType {
ContentType::Code => write!(f, "code"),
ContentType::Cli => write!(f, "cli"),
ContentType::Text => write!(f, "text"),
ContentType::Url => write!(f, "url"),
}
}
}
@@ -25,6 +27,7 @@ impl std::str::FromStr for ContentType {
"code" => Ok(ContentType::Code),
"cli" => Ok(ContentType::Cli),
"text" => Ok(ContentType::Text),
"url" => Ok(ContentType::Url),
other => Err(format!("unknown content_type: {other}")),
}
}
@@ -36,6 +39,7 @@ pub struct Snippet {
pub title: String,
pub content: String,
pub content_type: ContentType,
pub copy_count: i64,
pub created_at: String,
pub updated_at: String,
pub tags: Vec<Tag>,
+2 -2
View File
@@ -13,8 +13,8 @@
"windows": [
{
"title": "Faro",
"width": 1100,
"height": 720,
"width": 1300,
"height": 900,
"minWidth": 800,
"minHeight": 520,
"resizable": true
+3
View File
@@ -43,6 +43,7 @@
/* semantic */
--success: #c3e88d;
--danger: #ef4444;
--link: #60a5fa;
/* syntax */
--syntax-keyword: #c792ea;
@@ -95,6 +96,7 @@
--accent-border: #6d5dfc;
--success: #16a34a;
--danger: #dc2626;
--link: #2563eb;
--syntax-keyword: #2563eb;
--syntax-type: #0891b2;
--syntax-func: #7c3aed;
@@ -133,6 +135,7 @@
--accent-border: #88c0d0;
--success: #a3be8c;
--danger: #bf616a;
--link: #5e81ac;
--syntax-keyword: #81a1c1;
--syntax-type: #8fbcbb;
--syntax-func: #88c0d0;
+16 -3
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import { Code2, Terminal, FileText, Copy, Check } from 'lucide-svelte';
import { Code2, Terminal, FileText, Link, Copy, Check } from 'lucide-svelte';
import type { Snippet } from '$lib/types';
import { copyToClipboard, recordCopy } from '$lib/api';
import { copyToClipboard } from '$lib/api';
import { snippets as store } from '$lib/stores/snippets.svelte';
import { highlightPreviewHtml } from '$lib/highlight';
let { snippet, selected = false, onselect }: {
@@ -32,7 +33,7 @@
await copyToClipboard(snippet.content);
copied = true;
setTimeout(() => { copied = false; }, 1500);
recordCopy(snippet.id).catch(() => {});
store.recordCopy(snippet.id);
} catch { /* clipboard unavailable */ }
}
</script>
@@ -52,11 +53,16 @@
<Code2 size={13} strokeWidth={2} />
{:else if snippet.content_type === 'cli'}
<Terminal size={13} strokeWidth={2} />
{:else if snippet.content_type === 'url'}
<Link size={13} strokeWidth={2} />
{:else}
<FileText size={13} strokeWidth={2} />
{/if}
</span>
<span class="title">{snippet.title || 'Untitled'}</span>
{#if snippet.copy_count > 0}
<span class="copy-count">{snippet.copy_count}</span>
{/if}
<button class="copy-btn" onclick={copyContent}>
{#if copied}
<Check size={12} strokeWidth={2.5} />
@@ -113,6 +119,7 @@
.type-icon.code { color: var(--accent); }
.type-icon.cli { color: var(--success); }
.type-icon.text { color: var(--text-muted); }
.type-icon.url { color: var(--link); }
.title {
flex: 1;
font-size: 14px;
@@ -121,6 +128,12 @@
overflow: hidden;
text-overflow: ellipsis;
}
.copy-count {
flex-shrink: 0;
font-size: 11px;
color: var(--text-faint);
font-variant-numeric: tabular-nums;
}
.copy-btn {
flex-shrink: 0;
display: flex;
+52 -1
View File
@@ -3,6 +3,9 @@
import { detectSnippet } from '$lib/highlight';
import CodeEditor from './CodeEditor.svelte';
import { LANGUAGE_OPTIONS } from '$lib/languages';
import { Copy, Check } from 'lucide-svelte';
import { copyToClipboard } from '$lib/api';
import { snippets as store } from '$lib/stores/snippets.svelte';
let { snippet = null, mode, onSave, onCancel, onDelete, dirty = $bindable(false) }: {
snippet?: Snippet | null;
@@ -21,6 +24,16 @@
let draftSuppressedTags = $state<string[]>([]);
let typeManuallySet = $state(false);
let systemTagsManuallySet = $state(false);
let detailCopied = $state(false);
async function handleDetailCopy() {
try {
await copyToClipboard(draftContent);
detailCopied = true;
setTimeout(() => { detailCopied = false; }, 1500);
if (snippet) store.recordCopy(snippet.id);
} catch { /* clipboard unavailable */ }
}
// Re-initialize draft whenever mode or snippet changes.
$effect(() => {
@@ -145,7 +158,7 @@
<label for="type-buttons">Type</label>
<div class="type-row">
<div class="type-buttons" id="type-buttons" role="group">
{#each (['code', 'cli', 'text'] as ContentType[]) as t (t)}
{#each (['code', 'cli', 'text', 'url'] as ContentType[]) as t (t)}
<button
class="type-btn"
class:active={draftType === t}
@@ -170,6 +183,20 @@
</div>
<div class="field grow">
<div class="content-label-row">
<label>Content</label>
{#if mode === 'editing' && snippet}
<button class="content-copy-btn" onclick={handleDetailCopy}>
{#if detailCopied}
<Check size={12} strokeWidth={2.5} />
<span>Copied</span>
{:else}
<Copy size={12} strokeWidth={2} />
<span>Copy</span>
{/if}
</button>
{/if}
</div>
<CodeEditor
bind:value={draftContent}
contentType={draftType}
@@ -268,6 +295,30 @@
.title-input:hover:not(:focus) { border-bottom-color: var(--border); }
.title-input:focus { border-bottom-color: var(--accent-border); }
.title-input::placeholder { color: var(--text-faint); font-weight: 400; }
.content-label-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.content-copy-btn {
display: flex;
align-items: center;
gap: 0.25rem;
background: none;
border: 1px solid var(--border);
border-radius: var(--radius-chip);
color: var(--text-muted);
font-size: 11px;
font-family: var(--font-body);
padding: 0.15rem 0.5rem;
cursor: pointer;
transition: background 0.1s, color 0.1s, border-color 0.1s;
}
.content-copy-btn:hover {
background: var(--bg-surface-hover);
color: var(--text);
border-color: var(--border-strong);
}
.system-tags {
display: flex;
flex-wrap: wrap;
+17 -5
View File
@@ -1,14 +1,15 @@
<script lang="ts">
import SnippetCard from '$lib/components/SnippetCard.svelte';
import SearchBar from '$lib/components/SearchBar.svelte';
import type { Snippet, TimePeriod } from '$lib/types';
import type { Snippet, TimePeriod, ContentType } from '$lib/types';
let { snippets, selectedId, loading = false, searchQuery, tagFilter = null, timePeriodFilter = null, onSelect, onSearch, searchInputEl = $bindable() }: {
let { snippets, selectedId, loading = false, searchQuery, tagFilter = null, contentTypeFilter = null, timePeriodFilter = null, onSelect, onSearch, searchInputEl = $bindable() }: {
snippets: Snippet[];
selectedId: number | null;
loading?: boolean;
searchQuery: string;
tagFilter?: string | null;
contentTypeFilter?: ContentType | null;
timePeriodFilter?: TimePeriod | null;
onSelect: (id: number) => void;
onSearch: (q: string) => void;
@@ -23,14 +24,25 @@
'older': 'older than two weeks',
};
const CT_LABELS: Record<ContentType, string> = {
code: 'code',
cli: 'CLI',
text: 'text',
url: 'URL',
};
const emptyMessage = $derived(
searchQuery
? 'No snippets match your search.'
: timePeriodFilter
? `No snippets from ${PERIOD_LABELS[timePeriodFilter]}.`
: tagFilter
? `No snippets tagged "${tagFilter}".`
: 'No snippets yet — create one with the button on the left.'
: tagFilter && contentTypeFilter
? `No ${CT_LABELS[contentTypeFilter]} snippets tagged "${tagFilter}".`
: tagFilter
? `No snippets tagged "${tagFilter}".`
: contentTypeFilter
? `No ${CT_LABELS[contentTypeFilter]} snippets.`
: 'No snippets yet — create one with the button on the left.'
);
</script>
+79 -23
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { Settings, Plus, Clock, Tag as TagIcon, Search } from 'lucide-svelte';
import type { Tag, TimePeriod } from '$lib/types';
import { Settings, Plus, Clock, Tag as TagIcon, Search, Code2, Terminal, FileText, Link, Layers } from 'lucide-svelte';
import type { Tag, TimePeriod, ContentType } from '$lib/types';
const TIMELINE_ITEMS: { label: string; value: TimePeriod }[] = [
{ label: 'Today', value: 'today' },
@@ -10,10 +10,19 @@
{ label: 'Older', value: 'older' },
];
const CONTENT_TYPES: { label: string; value: ContentType; Icon: typeof Code2 }[] = [
{ label: 'Code', value: 'code', Icon: Code2 },
{ label: 'CLI', value: 'cli', Icon: Terminal },
{ label: 'Text', value: 'text', Icon: FileText },
{ label: 'Url', value: 'url', Icon: Link },
];
let {
tags,
activeFilter = null,
contentTypeFilter = null,
onFilterChange,
onContentTypeChange = () => {},
onNewSnippet,
onOpenSettings = () => {},
timePeriodFilter = null,
@@ -21,7 +30,9 @@
}: {
tags: Tag[];
activeFilter?: string | null;
contentTypeFilter?: ContentType | null;
onFilterChange: (tag: string | null) => void;
onContentTypeChange?: (ct: ContentType | null) => void;
onNewSnippet: () => void;
onOpenSettings?: () => void;
timePeriodFilter?: TimePeriod | null;
@@ -34,14 +45,13 @@
? tags.filter(t => t.name.toLowerCase().includes(tagSearch.toLowerCase()))
: tags
);
function toggleContentType(ct: ContentType) {
onContentTypeChange(contentTypeFilter === ct ? null : ct);
}
</script>
<aside class="left-panel">
<!-- <div class="panel-header">
<img src="/logo.svg" alt="Faro Logo" width="64" height="64" />
<span class="brand">Faro</span>
</div> -->
<div class="panel-body">
<!-- Timeline section -->
<div class="section">
@@ -57,6 +67,26 @@
{/each}
</div>
<div class="divider-strong"></div>
<!-- Content Type section -->
<div class="section">
<div class="section-label"><Layers size={10} strokeWidth={2.5} />Content Type</div>
<div class="ct-row">
{#each CONTENT_TYPES as ct (ct.value)}
<button
class="ct-chip"
class:active={contentTypeFilter === ct.value}
onclick={() => toggleContentType(ct.value)}
title={ct.label}
>
<ct.Icon size={12} strokeWidth={2} />
<span>{ct.label}</span>
</button>
{/each}
</div>
</div>
<div class="divider"></div>
<!-- Tags section -->
@@ -75,8 +105,8 @@
{#if !tagSearch.trim()}
<button
class="nav-item"
class:active={activeFilter === null && timePeriodFilter === null}
onclick={() => { onFilterChange(null); onTimePeriodChange(null); }}
class:active={activeFilter === null && timePeriodFilter === null && contentTypeFilter === null}
onclick={() => { onFilterChange(null); onTimePeriodChange(null); onContentTypeChange(null); }}
>
All snippets
</button>
@@ -117,20 +147,6 @@
border-right: 1px solid var(--border);
background: var(--bg-sidebar);
}
/* .panel-header {
padding: 1rem 1rem 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
display: flex;
flex-direction: column;
align-items: center;
}
.brand {
font-size: 28px;
font-weight: 600;
color: var(--accent);
letter-spacing: 0.06em;
} */
.panel-body {
flex: 1;
min-height: 0;
@@ -162,6 +178,46 @@
background: var(--border);
margin: 0.25rem 0;
}
.divider-strong {
height: 1px;
background: var(--border-strong, var(--border));
margin: 0.5rem 0;
opacity: 0.6;
box-shadow: 0 1px 0 var(--border);
}
/* Content type chip row */
.ct-row {
display: flex;
gap: 0.3rem;
padding: 0.1rem 1rem 0.35rem;
}
.ct-chip {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
padding: 0.35rem 0.2rem;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-chip);
color: var(--text-muted);
font-size: 11px;
font-family: var(--font-body);
cursor: pointer;
transition: background 0.1s, border-color 0.1s, color 0.1s;
}
.ct-chip:hover {
background: var(--bg-surface-hover);
border-color: var(--border-strong);
color: var(--text);
}
.ct-chip.active {
background: var(--accent-tint);
border-color: var(--accent-border);
color: var(--accent);
}
.tag-search-wrap {
position: relative;
display: flex;
+17
View File
@@ -2,6 +2,19 @@ import hljs from 'highlight.js/lib/common';
import type { ContentType } from './types';
import { SUBSET } from './languages';
// ── URL recognition ──────────────────────────────────────────────────────────
const URL_PROTOCOLS = new Set(['http:', 'https:', 'ftp:']);
function looksLikeUrl(content: string): boolean {
if (content.includes('\n')) return false;
try {
return URL_PROTOCOLS.has(new URL(content).protocol);
} catch {
return false;
}
}
// ── CLI recognition ──────────────────────────────────────────────────────────
const SHELL_COMMANDS = new Set([
@@ -65,6 +78,10 @@ export function detectSnippet(
const trimmed = content.trim();
if (!trimmed) return { content_type: 'text', language: null };
if (looksLikeUrl(trimmed)) {
return { content_type: 'url', language: null };
}
if (looksLikeCli(trimmed)) {
return { content_type: 'cli', language: null };
}
+40 -4
View File
@@ -1,11 +1,12 @@
import type { Snippet, Tag, CreateSnippetInput, UpdateSnippetInput, TimePeriod } from '$lib/types';
import { listSnippets, createSnippet, updateSnippet, deleteSnippet, listTags, searchSnippets, listSnippetsByPeriod } from '$lib/api';
import type { Snippet, Tag, CreateSnippetInput, UpdateSnippetInput, TimePeriod, ContentType } from '$lib/types';
import { listSnippets, createSnippet, updateSnippet, deleteSnippet, listTags, searchSnippets, listSnippetsByPeriod, recordCopy as apiRecordCopy } from '$lib/api';
class SnippetStore {
snippets = $state<Snippet[]>([]);
selectedId = $state<number | null>(null);
tags = $state<Tag[]>([]);
tagFilter = $state<string | null>(null);
contentTypeFilter = $state<ContentType | null>(null);
searchQuery = $state<string>('');
timePeriodFilter = $state<TimePeriod | null>(null);
loading = $state(false);
@@ -15,7 +16,7 @@ class SnippetStore {
this.loading = true;
this.error = null;
try {
const [list, tagList] = await Promise.all([
const [rawList, tagList] = await Promise.all([
this.searchQuery
? searchSnippets(this.searchQuery)
: this.timePeriodFilter
@@ -23,7 +24,9 @@ class SnippetStore {
: listSnippets(this.tagFilter ?? undefined),
listTags(),
]);
this.snippets = list;
this.snippets = this.contentTypeFilter
? rawList.filter(s => s.content_type === this.contentTypeFilter)
: rawList;
this.tags = tagList;
} catch (e) {
this.error = String(e);
@@ -68,6 +71,28 @@ class SnippetStore {
}
}
recordCopy(id: number): void {
const idx = this.snippets.findIndex(s => s.id === id);
if (idx >= 0) this.snippets[idx].copy_count += 1;
apiRecordCopy(id).catch(() => {});
}
get visibleTags(): Tag[] {
if (!this.contentTypeFilter) return this.tags;
const map = new Map<number, Tag & { count: number }>();
for (const snippet of this.snippets) {
for (const tag of snippet.tags) {
const entry = map.get(tag.id);
if (entry) {
entry.count++;
} else {
map.set(tag.id, { ...tag, count: 1 });
}
}
}
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
}
select(id: number | null): void {
this.selectedId = id;
}
@@ -76,12 +101,22 @@ class SnippetStore {
this.tagFilter = tag;
this.searchQuery = '';
this.timePeriodFilter = null;
// contentTypeFilter composes with tagFilter — don't clear it
void this.load();
}
setContentTypeFilter(ct: ContentType | null): void {
this.contentTypeFilter = ct;
this.timePeriodFilter = null;
this.searchQuery = '';
// tagFilter composes with contentTypeFilter — don't clear it
void this.load();
}
setSearch(query: string): void {
this.searchQuery = query;
this.tagFilter = null;
this.contentTypeFilter = null;
this.timePeriodFilter = null;
void this.load();
}
@@ -89,6 +124,7 @@ class SnippetStore {
setTimePeriodFilter(period: TimePeriod | null): void {
this.timePeriodFilter = period;
this.tagFilter = null;
this.contentTypeFilter = null;
this.searchQuery = '';
void this.load();
}
+2 -1
View File
@@ -1,4 +1,4 @@
export type ContentType = 'code' | 'cli' | 'text';
export type ContentType = 'code' | 'cli' | 'text' | 'url';
export interface Tag {
id: number;
@@ -15,6 +15,7 @@ export interface Snippet {
title: string;
content: string;
content_type: ContentType;
copy_count: number;
created_at: string;
updated_at: string;
tags: Tag[];
+11 -3
View File
@@ -7,7 +7,7 @@
import CenterPanel from '$lib/components/layout/CenterPanel.svelte';
import RightPanel from '$lib/components/layout/RightPanel.svelte';
import SettingsModal from '$lib/components/SettingsModal.svelte';
import type { CreateSnippetInput, UpdateSnippetInput, TimePeriod } from '$lib/types';
import type { CreateSnippetInput, UpdateSnippetInput, TimePeriod, ContentType } from '$lib/types';
let mode = $state<'idle' | 'creating' | 'editing'>('idle');
let formDirty = $state(false);
@@ -118,8 +118,9 @@
<div class="app-layout">
<LeftPanel
tags={store.tags}
tags={store.visibleTags}
activeFilter={store.tagFilter}
contentTypeFilter={store.contentTypeFilter}
timePeriodFilter={store.timePeriodFilter}
onFilterChange={(tag) => {
if (formDirty && !confirm('You have unsaved changes. Discard them?')) return;
@@ -127,6 +128,12 @@
store.select(null);
mode = 'idle';
}}
onContentTypeChange={(ct: ContentType | null) => {
if (formDirty && !confirm('You have unsaved changes. Discard them?')) return;
store.setContentTypeFilter(ct);
store.select(null);
mode = 'idle';
}}
onTimePeriodChange={(period: TimePeriod | null) => {
if (formDirty && !confirm('You have unsaved changes. Discard them?')) return;
store.setTimePeriodFilter(period);
@@ -142,6 +149,7 @@
loading={store.loading}
searchQuery={store.searchQuery}
tagFilter={store.tagFilter}
contentTypeFilter={store.contentTypeFilter}
timePeriodFilter={store.timePeriodFilter}
onSelect={handleSelectSnippet}
onSearch={(q) => {
@@ -170,7 +178,7 @@
<style>
.app-layout {
display: grid;
grid-template-columns: 220px 1fr 360px;
grid-template-columns: 270px 1fr 360px;
grid-template-rows: 100vh;
height: 100vh;
overflow: hidden;
+1 -1
View File
@@ -97,7 +97,7 @@
<div class="field">
<label for="type-buttons">Type</label>
<div class="type-buttons" id="type-buttons" role="group">
{#each (['code', 'cli', 'text'] as ContentType[]) as t (t)}
{#each (['code', 'cli', 'text', 'url'] as ContentType[]) as t (t)}
<button
class="type-btn"
class:active={contentType === t}