Browse Source
- Added new components: Header, Sidebar, ExportView, ImportView, EventsView, ComposeView, RecoveryView, SprocketView, and SearchResultsView to enhance the application's functionality and user experience. - Updated App.svelte to integrate the new views and improve the overall layout. - Refactored existing components for better organization and maintainability. - Adjusted CSS styles for improved visual consistency across the application. - Incremented version number to v0.19.3 to reflect the latest changes and additions.main
16 changed files with 3177 additions and 1949 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,129 @@ |
|||||||
|
<script> |
||||||
|
export let composeEventJson = ""; |
||||||
|
|
||||||
|
import { createEventDispatcher } from "svelte"; |
||||||
|
const dispatch = createEventDispatcher(); |
||||||
|
|
||||||
|
function reformatJson() { |
||||||
|
dispatch("reformatJson"); |
||||||
|
} |
||||||
|
|
||||||
|
function signEvent() { |
||||||
|
dispatch("signEvent"); |
||||||
|
} |
||||||
|
|
||||||
|
function publishEvent() { |
||||||
|
dispatch("publishEvent"); |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<div class="compose-view"> |
||||||
|
<div class="compose-header"> |
||||||
|
<button class="compose-btn reformat-btn" on:click={reformatJson} |
||||||
|
>Reformat</button |
||||||
|
> |
||||||
|
<button class="compose-btn sign-btn" on:click={signEvent}>Sign</button> |
||||||
|
<button class="compose-btn publish-btn" on:click={publishEvent} |
||||||
|
>Publish</button |
||||||
|
> |
||||||
|
</div> |
||||||
|
<div class="compose-editor"> |
||||||
|
<textarea |
||||||
|
bind:value={composeEventJson} |
||||||
|
class="compose-textarea" |
||||||
|
placeholder="Enter your Nostr event JSON here..." |
||||||
|
spellcheck="false" |
||||||
|
></textarea> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<style> |
||||||
|
.compose-view { |
||||||
|
position: fixed; |
||||||
|
top: 3em; |
||||||
|
left: 200px; |
||||||
|
right: 0; |
||||||
|
bottom: 0; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
background: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
.compose-header { |
||||||
|
display: flex; |
||||||
|
gap: 0.5em; |
||||||
|
padding: 0.5em; |
||||||
|
background: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
.compose-btn { |
||||||
|
padding: 0.5em 1em; |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
border-radius: 0.25rem; |
||||||
|
background: var(--button-bg); |
||||||
|
color: var(--button-text); |
||||||
|
cursor: pointer; |
||||||
|
font-size: 0.9rem; |
||||||
|
transition: background-color 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
.compose-btn:hover { |
||||||
|
background: var(--button-hover-bg); |
||||||
|
} |
||||||
|
|
||||||
|
.reformat-btn { |
||||||
|
background: var(--info); |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.reformat-btn:hover { |
||||||
|
background: var(--info); |
||||||
|
filter: brightness(0.9); |
||||||
|
} |
||||||
|
|
||||||
|
.sign-btn { |
||||||
|
background: var(--warning); |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.sign-btn:hover { |
||||||
|
background: var(--warning); |
||||||
|
filter: brightness(0.9); |
||||||
|
} |
||||||
|
|
||||||
|
.publish-btn { |
||||||
|
background: var(--success); |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.publish-btn:hover { |
||||||
|
background: var(--success); |
||||||
|
filter: brightness(0.9); |
||||||
|
} |
||||||
|
|
||||||
|
.compose-editor { |
||||||
|
flex: 1; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.compose-textarea { |
||||||
|
flex: 1; |
||||||
|
width: 100%; |
||||||
|
padding: 1em; |
||||||
|
border-radius: 0.5em; |
||||||
|
background: var(--input-bg); |
||||||
|
color: var(--input-text-color); |
||||||
|
font-family: monospace; |
||||||
|
font-size: 0.9em; |
||||||
|
line-height: 1.4; |
||||||
|
resize: vertical; |
||||||
|
outline: none; |
||||||
|
} |
||||||
|
|
||||||
|
.compose-textarea:focus { |
||||||
|
border-color: var(--accent-color); |
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,117 @@ |
|||||||
|
<script> |
||||||
|
export let isLoggedIn = false; |
||||||
|
export let currentEffectiveRole = ""; |
||||||
|
|
||||||
|
import { createEventDispatcher } from "svelte"; |
||||||
|
const dispatch = createEventDispatcher(); |
||||||
|
|
||||||
|
function exportMyEvents() { |
||||||
|
dispatch("exportMyEvents"); |
||||||
|
} |
||||||
|
|
||||||
|
function exportAllEvents() { |
||||||
|
dispatch("exportAllEvents"); |
||||||
|
} |
||||||
|
|
||||||
|
function openLoginModal() { |
||||||
|
dispatch("openLoginModal"); |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
{#if isLoggedIn} |
||||||
|
<div class="export-section"> |
||||||
|
<h3>Export My Events</h3> |
||||||
|
<p>Download your personal events as a JSONL file.</p> |
||||||
|
<button class="export-btn" on:click={exportMyEvents}> |
||||||
|
📤 Export My Events |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
{#if currentEffectiveRole === "admin" || currentEffectiveRole === "owner"} |
||||||
|
<div class="export-section"> |
||||||
|
<h3>Export All Events</h3> |
||||||
|
<p> |
||||||
|
Download the complete database as a JSONL file. This includes |
||||||
|
all events from all users. |
||||||
|
</p> |
||||||
|
<button class="export-btn" on:click={exportAllEvents}> |
||||||
|
📤 Export All Events |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
{:else} |
||||||
|
<div class="login-prompt"> |
||||||
|
<p>Please log in to access export functionality.</p> |
||||||
|
<button class="login-btn" on:click={openLoginModal}>Log In</button> |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
|
||||||
|
<style> |
||||||
|
.export-section { |
||||||
|
border-radius: 8px; |
||||||
|
padding: 1em; |
||||||
|
margin: 1em; |
||||||
|
width: 32em; |
||||||
|
background-color: var(--card-bg); |
||||||
|
} |
||||||
|
|
||||||
|
.export-section h3 { |
||||||
|
margin: 0 0 1rem 0; |
||||||
|
color: var(--text-color); |
||||||
|
font-size: 1.2rem; |
||||||
|
font-weight: 600; |
||||||
|
} |
||||||
|
|
||||||
|
.export-section p { |
||||||
|
margin: 0 0 1.5rem 0; |
||||||
|
color: var(--text-color); |
||||||
|
opacity: 0.8; |
||||||
|
line-height: 1.4; |
||||||
|
} |
||||||
|
|
||||||
|
.export-btn { |
||||||
|
background-color: var(--primary); |
||||||
|
color: var(--text-color); |
||||||
|
border: none; |
||||||
|
padding: 0.75em 1.5em; |
||||||
|
border-radius: 0.5em; |
||||||
|
cursor: pointer; |
||||||
|
font-weight: bold; |
||||||
|
font-size: 0.9em; |
||||||
|
transition: background-color 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
.export-btn:hover { |
||||||
|
background-color: var(--accent-hover-color); |
||||||
|
} |
||||||
|
|
||||||
|
.login-prompt { |
||||||
|
text-align: center; |
||||||
|
padding: 2em; |
||||||
|
background-color: var(--card-bg); |
||||||
|
border-radius: 8px; |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
width: 32em; |
||||||
|
} |
||||||
|
|
||||||
|
.login-prompt p { |
||||||
|
margin: 0 0 1.5rem 0; |
||||||
|
color: var(--text-color); |
||||||
|
font-size: 1.1rem; |
||||||
|
} |
||||||
|
|
||||||
|
.login-btn { |
||||||
|
background-color: var(--primary); |
||||||
|
color: var(--text-color); |
||||||
|
border: none; |
||||||
|
padding: 0.75em 1.5em; |
||||||
|
border-radius: 4px; |
||||||
|
cursor: pointer; |
||||||
|
font-weight: bold; |
||||||
|
font-size: 0.9em; |
||||||
|
transition: background-color 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
.login-btn:hover { |
||||||
|
background-color: var(--accent-hover-color); |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,157 @@ |
|||||||
|
<script> |
||||||
|
export let isLoggedIn = false; |
||||||
|
export let currentEffectiveRole = ""; |
||||||
|
export let selectedFile = null; |
||||||
|
|
||||||
|
import { createEventDispatcher } from "svelte"; |
||||||
|
const dispatch = createEventDispatcher(); |
||||||
|
|
||||||
|
function handleFileSelect(event) { |
||||||
|
dispatch("fileSelect", event); |
||||||
|
} |
||||||
|
|
||||||
|
function importEvents() { |
||||||
|
dispatch("importEvents"); |
||||||
|
} |
||||||
|
|
||||||
|
function openLoginModal() { |
||||||
|
dispatch("openLoginModal"); |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<div class="import-section"> |
||||||
|
{#if isLoggedIn && (currentEffectiveRole === "admin" || currentEffectiveRole === "owner")} |
||||||
|
<h3>Import Events</h3> |
||||||
|
<p>Upload a JSONL file to import events into the database.</p> |
||||||
|
<div class="recovery-controls-card"> |
||||||
|
<input |
||||||
|
type="file" |
||||||
|
id="import-file" |
||||||
|
accept=".jsonl,.txt" |
||||||
|
on:change={handleFileSelect} |
||||||
|
/> |
||||||
|
<button |
||||||
|
class="import-btn" |
||||||
|
on:click={importEvents} |
||||||
|
disabled={!selectedFile} |
||||||
|
> |
||||||
|
Import Events |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
{:else if isLoggedIn} |
||||||
|
<div class="permission-denied"> |
||||||
|
<h3 class="recovery-header">Import Events</h3> |
||||||
|
<p class="recovery-description"> |
||||||
|
❌ Admin or owner permission required for import functionality. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
{:else} |
||||||
|
<div class="login-prompt"> |
||||||
|
<h3 class="recovery-header">Import Events</h3> |
||||||
|
<p class="recovery-description"> |
||||||
|
Please log in to access import functionality. |
||||||
|
</p> |
||||||
|
<button class="login-btn" on:click={openLoginModal}>Log In</button> |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
|
||||||
|
<style> |
||||||
|
.import-section { |
||||||
|
background: transparent; |
||||||
|
padding: 1em; |
||||||
|
border-radius: 8px; |
||||||
|
margin-bottom: 1.5rem; |
||||||
|
width: 32em; |
||||||
|
} |
||||||
|
|
||||||
|
.import-section h3 { |
||||||
|
margin: 0 0 1rem 0; |
||||||
|
color: var(--text-color); |
||||||
|
font-size: 1.2rem; |
||||||
|
font-weight: 600; |
||||||
|
} |
||||||
|
|
||||||
|
.import-section p { |
||||||
|
margin: 0 0 1.5rem 0; |
||||||
|
color: var(--text-color); |
||||||
|
opacity: 0.8; |
||||||
|
line-height: 1.4; |
||||||
|
} |
||||||
|
|
||||||
|
.recovery-controls-card { |
||||||
|
background-color: var(--card-bg); |
||||||
|
padding: 1em; |
||||||
|
border: 0; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
border-radius: 0.5em; |
||||||
|
gap: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
#import-file { |
||||||
|
padding: 0.5em; |
||||||
|
border: 0; |
||||||
|
background: var(--input-bg); |
||||||
|
color: var(--input-text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.import-btn { |
||||||
|
background-color: var(--primary); |
||||||
|
color: var(--text-color); |
||||||
|
border-radius: 0.5em; |
||||||
|
padding: 0.75em 1.5em; |
||||||
|
border: 0; |
||||||
|
cursor: pointer; |
||||||
|
font-weight: bold; |
||||||
|
font-size: 0.9em; |
||||||
|
transition: background-color 0.2s; |
||||||
|
align-self: flex-start; |
||||||
|
} |
||||||
|
|
||||||
|
.import-btn:hover:not(:disabled) { |
||||||
|
background-color: var(--accent-hover-color); |
||||||
|
} |
||||||
|
|
||||||
|
.import-btn:disabled { |
||||||
|
background-color: var(--secondary); |
||||||
|
cursor: not-allowed; |
||||||
|
} |
||||||
|
|
||||||
|
.permission-denied, |
||||||
|
.login-prompt { |
||||||
|
text-align: center; |
||||||
|
padding: 2em; |
||||||
|
background-color: var(--card-bg); |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.recovery-header { |
||||||
|
margin: 0 0 1rem 0; |
||||||
|
color: var(--text-color); |
||||||
|
font-size: 1.2rem; |
||||||
|
font-weight: 600; |
||||||
|
} |
||||||
|
|
||||||
|
.recovery-description { |
||||||
|
margin: 0 0 1.5rem 0; |
||||||
|
color: var(--text-color); |
||||||
|
line-height: 1.4; |
||||||
|
} |
||||||
|
|
||||||
|
.login-btn { |
||||||
|
background-color: var(--primary); |
||||||
|
color: var(--text-color); |
||||||
|
border: none; |
||||||
|
padding: 0.75em 1.5em; |
||||||
|
border: 0; |
||||||
|
cursor: pointer; |
||||||
|
font-weight: bold; |
||||||
|
font-size: 0.9em; |
||||||
|
transition: background-color 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
.login-btn:hover { |
||||||
|
background-color: var(--accent-hover-color); |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,358 @@ |
|||||||
|
<script> |
||||||
|
export let recoverySelectedKind = null; |
||||||
|
export let recoveryCustomKind = ""; |
||||||
|
export let isLoadingRecovery = false; |
||||||
|
export let recoveryEvents = []; |
||||||
|
export let recoveryHasMore = false; |
||||||
|
|
||||||
|
import { createEventDispatcher } from "svelte"; |
||||||
|
const dispatch = createEventDispatcher(); |
||||||
|
|
||||||
|
const replaceableKinds = [ |
||||||
|
{ value: 0, label: "Profile (0)" }, |
||||||
|
{ value: 3, label: "Contacts (3)" }, |
||||||
|
{ value: 10000, label: "Mute List (10000)" }, |
||||||
|
{ value: 10001, label: "Pin List (10001)" }, |
||||||
|
{ value: 10002, label: "Relay List (10002)" }, |
||||||
|
{ value: 30000, label: "Categorized People (30000)" }, |
||||||
|
{ value: 30001, label: "Categorized Bookmarks (30001)" }, |
||||||
|
{ value: 30008, label: "Profile Badges (30008)" }, |
||||||
|
{ value: 30009, label: "Badge Definition (30009)" }, |
||||||
|
{ value: 30017, label: "Create or update a stall (30017)" }, |
||||||
|
{ value: 30018, label: "Create or update a product (30018)" }, |
||||||
|
{ value: 30023, label: "Long-form Content (30023)" }, |
||||||
|
{ value: 30024, label: "Draft Long-form Content (30024)" }, |
||||||
|
{ value: 30078, label: "Application-specific Data (30078)" }, |
||||||
|
{ value: 30311, label: "Live Event (30311)" }, |
||||||
|
{ value: 30315, label: "User Statuses (30315)" }, |
||||||
|
{ value: 30402, label: "Classified Listing (30402)" }, |
||||||
|
{ value: 30403, label: "Draft Classified Listing (30403)" }, |
||||||
|
{ value: 31922, label: "Date-Based Calendar Event (31922)" }, |
||||||
|
{ value: 31923, label: "Time-Based Calendar Event (31923)" }, |
||||||
|
{ value: 31924, label: "Calendar (31924)" }, |
||||||
|
{ value: 31925, label: "Calendar Event RSVP (31925)" }, |
||||||
|
{ value: 31989, label: "Handler recommendation (31989)" }, |
||||||
|
{ value: 31990, label: "Handler information (31990)" }, |
||||||
|
{ value: 34550, label: "Community Definition (34550)" }, |
||||||
|
]; |
||||||
|
|
||||||
|
function selectRecoveryKind() { |
||||||
|
dispatch("selectRecoveryKind"); |
||||||
|
} |
||||||
|
|
||||||
|
function handleCustomKindInput() { |
||||||
|
dispatch("handleCustomKindInput"); |
||||||
|
} |
||||||
|
|
||||||
|
function loadRecoveryEvents() { |
||||||
|
dispatch("loadRecoveryEvents"); |
||||||
|
} |
||||||
|
|
||||||
|
function repostEventToAll(event) { |
||||||
|
dispatch("repostEventToAll", event); |
||||||
|
} |
||||||
|
|
||||||
|
function repostEvent(event) { |
||||||
|
dispatch("repostEvent", event); |
||||||
|
} |
||||||
|
|
||||||
|
function copyEventToClipboard(event, e) { |
||||||
|
dispatch("copyEventToClipboard", { event, e }); |
||||||
|
} |
||||||
|
|
||||||
|
function isCurrentVersion(event) { |
||||||
|
// This logic would need to be passed from parent or implemented here |
||||||
|
// For now, just return false for old versions |
||||||
|
return false; |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<div class="recovery-tab"> |
||||||
|
<div> |
||||||
|
<h3>Event Recovery</h3> |
||||||
|
<p>Search and recover old versions of replaceable events</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="recovery-controls-card"> |
||||||
|
<div class="recovery-controls"> |
||||||
|
<div class="kind-selector"> |
||||||
|
<label for="recovery-kind">Select Event Kind:</label> |
||||||
|
<select |
||||||
|
id="recovery-kind" |
||||||
|
bind:value={recoverySelectedKind} |
||||||
|
on:change={selectRecoveryKind} |
||||||
|
> |
||||||
|
<option value={null}>Choose a replaceable kind...</option> |
||||||
|
{#each replaceableKinds as kind} |
||||||
|
<option value={kind.value}>{kind.label}</option> |
||||||
|
{/each} |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="custom-kind-input"> |
||||||
|
<label for="custom-kind">Or enter custom kind number:</label> |
||||||
|
<input |
||||||
|
id="custom-kind" |
||||||
|
type="number" |
||||||
|
bind:value={recoveryCustomKind} |
||||||
|
on:input={handleCustomKindInput} |
||||||
|
placeholder="e.g., 10001" |
||||||
|
min="0" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{#if (recoverySelectedKind !== null && recoverySelectedKind !== undefined && recoverySelectedKind >= 0) || (recoveryCustomKind !== "" && parseInt(recoveryCustomKind) >= 0)} |
||||||
|
<div class="recovery-results"> |
||||||
|
{#if isLoadingRecovery} |
||||||
|
<div class="loading">Loading events...</div> |
||||||
|
{:else if recoveryEvents.length === 0} |
||||||
|
<div class="no-events">No events found for this kind</div> |
||||||
|
{:else} |
||||||
|
<div class="events-list"> |
||||||
|
{#each recoveryEvents as event} |
||||||
|
{@const isCurrent = isCurrentVersion(event)} |
||||||
|
<div class="event-item" class:old-version={!isCurrent}> |
||||||
|
<div class="event-header"> |
||||||
|
<div class="event-header-left"> |
||||||
|
<span class="event-kind"> |
||||||
|
{#if isCurrent} |
||||||
|
Current Version{/if}</span |
||||||
|
> |
||||||
|
<span class="event-timestamp"> |
||||||
|
{new Date( |
||||||
|
event.created_at * 1000, |
||||||
|
).toLocaleString()} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="event-header-actions"> |
||||||
|
{#if !isCurrent} |
||||||
|
<button |
||||||
|
class="repost-all-button" |
||||||
|
on:click={() => |
||||||
|
repostEventToAll(event)} |
||||||
|
> |
||||||
|
🌐 Repost to All |
||||||
|
</button> |
||||||
|
<button |
||||||
|
class="repost-button" |
||||||
|
on:click={() => repostEvent(event)} |
||||||
|
> |
||||||
|
🔄 Repost |
||||||
|
</button> |
||||||
|
{/if} |
||||||
|
<button |
||||||
|
class="copy-json-btn" |
||||||
|
on:click|stopPropagation={(e) => |
||||||
|
copyEventToClipboard(event, e)} |
||||||
|
> |
||||||
|
📋 Copy JSON |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="event-content"> |
||||||
|
<pre class="event-json">{JSON.stringify( |
||||||
|
event, |
||||||
|
null, |
||||||
|
2, |
||||||
|
)}</pre> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{/each} |
||||||
|
</div> |
||||||
|
|
||||||
|
{#if recoveryHasMore} |
||||||
|
<button |
||||||
|
class="load-more" |
||||||
|
on:click={loadRecoveryEvents} |
||||||
|
disabled={isLoadingRecovery} |
||||||
|
> |
||||||
|
Load More Events |
||||||
|
</button> |
||||||
|
{/if} |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
|
||||||
|
<style> |
||||||
|
.recovery-tab { |
||||||
|
width: 100%; |
||||||
|
max-width: 1200px; |
||||||
|
margin: 0; |
||||||
|
padding: 20px; |
||||||
|
background: var(--header-bg); |
||||||
|
color: var(--text-color); |
||||||
|
border-radius: 8px; |
||||||
|
} |
||||||
|
|
||||||
|
.recovery-tab h3 { |
||||||
|
margin: 0 0 0.5rem 0; |
||||||
|
color: var(--text-color); |
||||||
|
font-size: 1.5rem; |
||||||
|
font-weight: 600; |
||||||
|
} |
||||||
|
|
||||||
|
.recovery-tab p { |
||||||
|
margin: 0 0 1.5rem 0; |
||||||
|
color: var(--text-color); |
||||||
|
opacity: 0.8; |
||||||
|
line-height: 1.4; |
||||||
|
} |
||||||
|
|
||||||
|
.recovery-controls-card { |
||||||
|
background-color: var(--card-bg); |
||||||
|
border-radius: 0.5em; |
||||||
|
padding: 1em; |
||||||
|
margin-bottom: 1.5rem; |
||||||
|
} |
||||||
|
|
||||||
|
.recovery-controls { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
gap: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.kind-selector, |
||||||
|
.custom-kind-input { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
gap: 0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
.kind-selector label, |
||||||
|
.custom-kind-input label { |
||||||
|
font-weight: 600; |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
|
||||||
|
.kind-selector select, |
||||||
|
.custom-kind-input input { |
||||||
|
padding: 0.5em; |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
border-radius: 4px; |
||||||
|
background: var(--input-bg); |
||||||
|
color: var(--input-text-color); |
||||||
|
font-size: 0.9em; |
||||||
|
} |
||||||
|
|
||||||
|
.recovery-results { |
||||||
|
margin-top: 1.5rem; |
||||||
|
} |
||||||
|
|
||||||
|
.loading, |
||||||
|
.no-events { |
||||||
|
text-align: center; |
||||||
|
padding: 2em; |
||||||
|
color: var(--text-color); |
||||||
|
opacity: 0.7; |
||||||
|
} |
||||||
|
|
||||||
|
.events-list { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
gap: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.event-item { |
||||||
|
background: var(--card-bg); |
||||||
|
border: 1px solid var(--border-color); |
||||||
|
border-radius: 8px; |
||||||
|
padding: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.event-item.old-version { |
||||||
|
border-color: var(--warning); |
||||||
|
background: var(--warning-bg); |
||||||
|
} |
||||||
|
|
||||||
|
.event-header { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
margin-bottom: 1em; |
||||||
|
flex-wrap: wrap; |
||||||
|
gap: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.event-header-left { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
gap: 0.25em; |
||||||
|
} |
||||||
|
|
||||||
|
.event-kind { |
||||||
|
font-weight: 600; |
||||||
|
color: var(--primary); |
||||||
|
} |
||||||
|
|
||||||
|
.event-timestamp { |
||||||
|
font-size: 0.9em; |
||||||
|
color: var(--text-color); |
||||||
|
opacity: 0.7; |
||||||
|
} |
||||||
|
|
||||||
|
.event-header-actions { |
||||||
|
display: flex; |
||||||
|
gap: 0.5em; |
||||||
|
flex-wrap: wrap; |
||||||
|
} |
||||||
|
|
||||||
|
.repost-all-button, |
||||||
|
.repost-button, |
||||||
|
.copy-json-btn { |
||||||
|
background: var(--accent-color); |
||||||
|
color: var(--accent-hover-color); |
||||||
|
border: none; |
||||||
|
padding: 0.5em; |
||||||
|
border-radius: 0.5em; |
||||||
|
cursor: pointer; |
||||||
|
font-size: 0.8em; |
||||||
|
transition: background-color 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
.repost-all-button:hover, |
||||||
|
.repost-button:hover, |
||||||
|
.copy-json-btn:hover { |
||||||
|
background: var(--accent-hover-color); |
||||||
|
} |
||||||
|
|
||||||
|
.event-content { |
||||||
|
margin-top: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.event-json { |
||||||
|
background: var(--code-bg); |
||||||
|
padding: 1em; |
||||||
|
border: 0; |
||||||
|
font-size: 0.8em; |
||||||
|
line-height: 1.4; |
||||||
|
overflow-x: auto; |
||||||
|
margin: 0; |
||||||
|
color: var(--code-text); |
||||||
|
} |
||||||
|
|
||||||
|
.load-more { |
||||||
|
width: 100%; |
||||||
|
padding: 12px; |
||||||
|
background: var(--primary); |
||||||
|
color: var(--text-color); |
||||||
|
border: none; |
||||||
|
border-radius: 4px; |
||||||
|
cursor: pointer; |
||||||
|
font-size: 1em; |
||||||
|
margin-top: 20px; |
||||||
|
transition: background 0.2s ease; |
||||||
|
} |
||||||
|
|
||||||
|
.load-more:hover:not(:disabled) { |
||||||
|
background: var(--accent-hover-color); |
||||||
|
} |
||||||
|
|
||||||
|
.load-more:disabled { |
||||||
|
opacity: 0.6; |
||||||
|
cursor: not-allowed; |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,121 @@ |
|||||||
|
<script> |
||||||
|
export let isDarkTheme = false; |
||||||
|
export let tabs = []; |
||||||
|
export let selectedTab = ""; |
||||||
|
|
||||||
|
import { createEventDispatcher } from "svelte"; |
||||||
|
const dispatch = createEventDispatcher(); |
||||||
|
|
||||||
|
function selectTab(tabId) { |
||||||
|
dispatch("selectTab", tabId); |
||||||
|
} |
||||||
|
|
||||||
|
function closeSearchTab(tabId) { |
||||||
|
dispatch("closeSearchTab", tabId); |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<aside class="sidebar" class:dark-theme={isDarkTheme}> |
||||||
|
<div class="sidebar-content"> |
||||||
|
<div class="tabs"> |
||||||
|
{#each tabs as tab} |
||||||
|
<button |
||||||
|
class="tab" |
||||||
|
class:active={selectedTab === tab.id} |
||||||
|
on:click={() => selectTab(tab.id)} |
||||||
|
> |
||||||
|
<span class="tab-icon">{tab.icon}</span> |
||||||
|
<span class="tab-label">{tab.label}</span> |
||||||
|
{#if tab.isSearchTab} |
||||||
|
<span |
||||||
|
class="tab-close-icon" |
||||||
|
on:click|stopPropagation={() => |
||||||
|
closeSearchTab(tab.id)} |
||||||
|
on:keydown={(e) => |
||||||
|
e.key === "Enter" && closeSearchTab(tab.id)} |
||||||
|
role="button" |
||||||
|
tabindex="0">✕</span |
||||||
|
> |
||||||
|
{/if} |
||||||
|
</button> |
||||||
|
{/each} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</aside> |
||||||
|
|
||||||
|
<style> |
||||||
|
.sidebar { |
||||||
|
position: fixed; |
||||||
|
left: 0; |
||||||
|
top: 2.5em; |
||||||
|
width: 200px; |
||||||
|
bottom: 0; |
||||||
|
background: var(--sidebar-bg); |
||||||
|
overflow-y: auto; |
||||||
|
z-index: 100; |
||||||
|
} |
||||||
|
|
||||||
|
.sidebar-content { |
||||||
|
padding: 0; |
||||||
|
background: var(--sidebar-bg); |
||||||
|
} |
||||||
|
|
||||||
|
.tabs { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.tab { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
padding: 0.75em; |
||||||
|
background: transparent; |
||||||
|
color: var(--text-color); |
||||||
|
border: none; |
||||||
|
cursor: pointer; |
||||||
|
transition: background-color 0.2s; |
||||||
|
gap: 0.75rem; |
||||||
|
text-align: left; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.tab:hover { |
||||||
|
background-color: var(--bg-color); |
||||||
|
} |
||||||
|
|
||||||
|
.tab.active { |
||||||
|
background-color: var(--bg-color); |
||||||
|
} |
||||||
|
|
||||||
|
.tab-icon { |
||||||
|
font-size: 1.2em; |
||||||
|
flex-shrink: 0; |
||||||
|
width: 1.5em; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.tab-label { |
||||||
|
font-size: 0.9em; |
||||||
|
font-weight: 500; |
||||||
|
white-space: nowrap; |
||||||
|
overflow: hidden; |
||||||
|
text-overflow: ellipsis; |
||||||
|
flex: 1; |
||||||
|
} |
||||||
|
|
||||||
|
.tab-close-icon { |
||||||
|
cursor: pointer; |
||||||
|
transition: opacity 0.2s; |
||||||
|
font-size: 0.8em; |
||||||
|
margin-left: auto; |
||||||
|
padding: 0.25rem; |
||||||
|
flex-shrink: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.tab-close-icon:hover { |
||||||
|
opacity: 0.7; |
||||||
|
background-color: var(--warning); |
||||||
|
color: var(--text-color); |
||||||
|
} |
||||||
|
</style> |
||||||
Loading…
Reference in new issue