mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-31 01:00:47 +08:00
Extract ToggleButton component
Signed-off-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
<script lang="ts" context="module">
|
||||
export interface ToggleButtonOption<T> {
|
||||
title?: string;
|
||||
count?: number;
|
||||
value: T;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" strictEvents>
|
||||
type T = $$Generic;
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { capitalize } from "@app/utils";
|
||||
|
||||
export let options: ToggleButtonOption<T>[];
|
||||
export let active: T;
|
||||
|
||||
const dispatch = createEventDispatcher<{ select: T }>();
|
||||
|
||||
function onSelect(option: ToggleButtonOption<T>) {
|
||||
if (option.count !== 0) {
|
||||
dispatch("select", option.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
user-select: none;
|
||||
}
|
||||
button {
|
||||
border-radius: var(--border-radius-small);
|
||||
color: var(--color-foreground-80);
|
||||
cursor: pointer;
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: 0.75rem;
|
||||
height: var(--button-tiny-height);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: none;
|
||||
min-width: 0;
|
||||
}
|
||||
button:hover, button.active {
|
||||
cursor: pointer;
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-foreground-background);
|
||||
}
|
||||
button[disabled], button[disabled]:hover {
|
||||
cursor: not-allowed;
|
||||
color: var(--color-foreground-80);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wrapper">
|
||||
{#each options as option}
|
||||
<button
|
||||
class="state-toggle"
|
||||
on:click={() => onSelect(option)}
|
||||
disabled={option.count === 0}
|
||||
class:active={active === option.value}>
|
||||
{#if option.count !== undefined}
|
||||
{option.count}
|
||||
{/if}
|
||||
{option.title ?? capitalize(`${option.value}`)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -27,6 +27,7 @@
|
||||
padding: 1rem;
|
||||
background: var(--color-foreground-background-subtle);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.summary {
|
||||
display: flex;
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { navigate } from "svelte-routing";
|
||||
import { groupIssues, Issue } from "@app/issue";
|
||||
import Placeholder from "@app/Placeholder.svelte";
|
||||
import { capitalize } from "@app/utils";
|
||||
|
||||
export let issues: Issue[];
|
||||
export let state = "open";
|
||||
|
||||
const { open, closed } = groupIssues(issues);
|
||||
|
||||
$: filteredIssues = state === "open" ? open : closed;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 1rem 0;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
.state-toggle {
|
||||
cursor: pointer;
|
||||
color: var(--color-foreground-80);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.state-toggle:hover {
|
||||
cursor: pointer;
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
.state-toggle[disabled], .state-toggle[disabled]:hover {
|
||||
cursor: not-allowed;
|
||||
color: var(--color-foreground-80);
|
||||
}
|
||||
.active {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
.separator {
|
||||
color: var(--color-foreground-faded);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="filter">
|
||||
<button
|
||||
class="unstyled state-toggle"
|
||||
on:click={() => navigate("?state=open") }
|
||||
disabled={open.length === 0}
|
||||
class:active={state === "open"}>
|
||||
{open.length} Open
|
||||
</button>
|
||||
<span class="separator">·</span>
|
||||
<button
|
||||
class="unstyled state-toggle"
|
||||
on:click={() => navigate("?state=closed")}
|
||||
disabled={closed.length === 0}
|
||||
class:active={state === "closed"}>
|
||||
{closed.length} Closed
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if filteredIssues.length}
|
||||
<slot {filteredIssues} />
|
||||
{:else}
|
||||
<Placeholder icon="🍣">
|
||||
<div slot="title">{capitalize(state)} issues</div>
|
||||
<div slot="body">No issues matched the current filter</div>
|
||||
</Placeholder>
|
||||
{/if}
|
||||
@@ -1,24 +1,42 @@
|
||||
<script lang="ts" context="module">
|
||||
export type State = "open" | "closed";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Project, ProjectContent } from "@app/project";
|
||||
import type { Config } from "@app/config";
|
||||
import IssueTeaser from "@app/base/projects/Issue/IssueTeaser.svelte";
|
||||
import IssueFilter from "@app/base/projects/Issue/IssueFilter.svelte";
|
||||
import type { Issue } from "@app/issue";
|
||||
import type { ToggleButtonOption } from "@app/ToggleButton.svelte";
|
||||
|
||||
import { Project, ProjectContent } from "@app/project";
|
||||
import { capitalize } from "@app/utils";
|
||||
import { groupIssues } from "@app/issue";
|
||||
import { navigate } from "svelte-routing";
|
||||
|
||||
import IssueTeaser from "@app/base/projects/Issue/IssueTeaser.svelte";
|
||||
import Placeholder from "@app/Placeholder.svelte";
|
||||
import ToggleButton from "@app/ToggleButton.svelte";
|
||||
|
||||
export let project: Project;
|
||||
export let config: Config;
|
||||
export let state: string;
|
||||
export let issues: Issue[];
|
||||
export let project: Project;
|
||||
export let state: State;
|
||||
|
||||
const navigate = (issue: string) => {
|
||||
project.navigateTo({
|
||||
content: ProjectContent.Issue,
|
||||
issue,
|
||||
patch: null,
|
||||
revision: null,
|
||||
path: null
|
||||
});
|
||||
};
|
||||
let options: ToggleButtonOption<State>[];
|
||||
const { open, closed } = groupIssues(issues);
|
||||
|
||||
$: filteredIssues = state === "open" ? open : closed;
|
||||
$: sortedIssues = filteredIssues.sort(({ timestamp: t1 }, { timestamp: t2 }) => t2 - t1);
|
||||
|
||||
$: options = [
|
||||
{
|
||||
value: "open",
|
||||
count: open.length
|
||||
},
|
||||
{
|
||||
value: "closed",
|
||||
count: closed.length
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -42,14 +60,30 @@
|
||||
</style>
|
||||
|
||||
<div class="issues">
|
||||
<IssueFilter {state} {issues} let:filteredIssues>
|
||||
{@const sortedIssues = filteredIssues.sort(({ timestamp: t1 }, { timestamp: t2 }) => t2 - t1)}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<ToggleButton {options} on:select={(e) => {navigate(`?state=${e.detail}`);}} active={state} />
|
||||
</div>
|
||||
|
||||
{#if filteredIssues.length}
|
||||
<div class="issues-list">
|
||||
{#each sortedIssues as issue}
|
||||
<div class="teaser" on:click={() => navigate(issue.id)}>
|
||||
<div class="teaser" on:click={() => {
|
||||
project.navigateTo({
|
||||
content: ProjectContent.Issue,
|
||||
issue: issue.id,
|
||||
patch: null,
|
||||
revision: null,
|
||||
path: null
|
||||
});
|
||||
}}>
|
||||
<IssueTeaser {config} {issue} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</IssueFilter>
|
||||
{:else}
|
||||
<Placeholder icon="🍣">
|
||||
<div slot="title">{capitalize(state)} issues</div>
|
||||
<div slot="body">No issues matched the current filter</div>
|
||||
</Placeholder>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { groupPatches, Patch } from "@app/patch";
|
||||
import Placeholder from "@app/Placeholder.svelte";
|
||||
import { capitalize } from "@app/utils";
|
||||
|
||||
export let patches: Patch[];
|
||||
export let state = "proposed";
|
||||
|
||||
const sortedPatches = groupPatches(patches);
|
||||
|
||||
$: filteredPatches = sortedPatches[state];
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.state-toggle {
|
||||
cursor: pointer;
|
||||
color: var(--color-foreground-80);
|
||||
border-radius: var(--border-radius-small);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.state-toggle:hover:not([disabled]), .state-toggle.active {
|
||||
cursor: pointer;
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-foreground-background);
|
||||
}
|
||||
.state-toggle[disabled], .state-toggle[disabled]:hover {
|
||||
cursor: not-allowed;
|
||||
color: var(--color-foreground-80);
|
||||
}
|
||||
.active {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="filter">
|
||||
<div
|
||||
class="unstyled state-toggle"
|
||||
on:click={() => state = "proposed"}
|
||||
disabled={sortedPatches.proposed.length === 0}
|
||||
class:active={state === "proposed"}>
|
||||
{sortedPatches.proposed.length} Proposed
|
||||
</div>
|
||||
<div
|
||||
class="unstyled state-toggle"
|
||||
on:click={() => state = "draft"}
|
||||
disabled={sortedPatches.draft.length === 0}
|
||||
class:active={state === "draft"}>
|
||||
{sortedPatches.draft.length} Draft
|
||||
</div>
|
||||
<div
|
||||
class="unstyled state-toggle"
|
||||
on:click={() => state = "archived"}
|
||||
disabled={sortedPatches.archived.length === 0}
|
||||
class:active={state === "archived"}>
|
||||
{sortedPatches.archived.length} Archived
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if filteredPatches.length}
|
||||
<slot {filteredPatches} />
|
||||
{:else}
|
||||
<Placeholder icon="🍖">
|
||||
<div slot="title">{capitalize(state)} patches</div>
|
||||
<div slot="body">No patches matched the current filter</div>
|
||||
</Placeholder>
|
||||
{/if}
|
||||
@@ -1,6 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { ToggleButtonOption } from "@app/ToggleButton.svelte";
|
||||
|
||||
import Dropdown from "@app/Dropdown.svelte";
|
||||
import Floating from "@app/Floating.svelte";
|
||||
import ToggleButton from "@app/ToggleButton.svelte";
|
||||
|
||||
import { PatchTab, Revision } from "@app/patch";
|
||||
import { formatCommit, formatTimestamp } from "@app/utils";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
@@ -27,41 +31,40 @@
|
||||
const onRevisionChange = ({ detail }: { detail: string }) => {
|
||||
dispatch("revisionChanged", detail);
|
||||
};
|
||||
|
||||
let options: ToggleButtonOption<PatchTab>[];
|
||||
$: options = [
|
||||
{
|
||||
title: "Patch",
|
||||
value: PatchTab.Timeline,
|
||||
},
|
||||
{
|
||||
title: "Changeset",
|
||||
value: PatchTab.Diff,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bar {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
.tab {
|
||||
color: var(--color-foreground-80);
|
||||
border-radius: var(--border-radius-small);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.tab:hover, .tab.active {
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-foreground-background);
|
||||
cursor: pointer;
|
||||
}
|
||||
.active {
|
||||
color: var(--color-foreground);
|
||||
cursor: default !important;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.revision-toggle {
|
||||
color: var(--color-foreground-80);
|
||||
border-radius: var(--border-radius-small);
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: var(--color-foreground-80);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: 0.75rem;
|
||||
height: var(--button-tiny-height);
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.revision-toggle:hover {
|
||||
background: none;
|
||||
background-color: var(--color-foreground-background);
|
||||
color: var(--color-foreground);
|
||||
cursor: pointer;
|
||||
}
|
||||
.revision-toggle:disabled {
|
||||
color: var(--color-foreground-faded);
|
||||
@@ -69,32 +72,20 @@
|
||||
</style>
|
||||
|
||||
<div class="bar text-small">
|
||||
<div class="tabs">
|
||||
<div
|
||||
class="tab" class:active={activeTab === PatchTab.Timeline}
|
||||
on:click={() => dispatch("switchTab", PatchTab.Timeline)}>
|
||||
Patch
|
||||
</div>
|
||||
<div
|
||||
class="tab" class:active={activeTab === PatchTab.Diff}
|
||||
on:click={() => dispatch("switchTab", PatchTab.Diff)}>
|
||||
Changeset
|
||||
</div>
|
||||
<div class="revision-toggle">
|
||||
<Floating disabled={revisions.length <= 1}>
|
||||
<button
|
||||
slot="toggle"
|
||||
class:tab={revisions.length > 1}
|
||||
class="text-small revision-toggle"
|
||||
disabled={revisions.length <= 1}>
|
||||
{formatRevisionName(revisions[revisionNumber], revisionNumber)}
|
||||
</button>
|
||||
<svelte:fragment slot="modal">
|
||||
<Dropdown
|
||||
items={revisionList} selected={revisionNumber.toString()}
|
||||
on:select={onRevisionChange} />
|
||||
</svelte:fragment>
|
||||
</Floating>
|
||||
</div>
|
||||
</div>
|
||||
<ToggleButton {options} on:select={(e) => {dispatch("switchTab", e.detail);}} active={activeTab} />
|
||||
|
||||
<Floating disabled={revisions.length <= 1}>
|
||||
<button
|
||||
slot="toggle"
|
||||
class="text-small revision-toggle"
|
||||
disabled={revisions.length <= 1}>
|
||||
{formatRevisionName(revisions[revisionNumber], revisionNumber)}
|
||||
</button>
|
||||
|
||||
<svelte:fragment slot="modal">
|
||||
<Dropdown
|
||||
items={revisionList} selected={revisionNumber.toString()}
|
||||
on:select={onRevisionChange} />
|
||||
</svelte:fragment>
|
||||
</Floating>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,41 @@
|
||||
<script lang="ts">
|
||||
type State = "proposed" | "draft" | "archived";
|
||||
|
||||
import type { Config } from "@app/config";
|
||||
import type { Patch } from "@app/patch";
|
||||
import { Project, ProjectContent } from "@app/project";
|
||||
import PatchFilter from "./Patch/PatchFilter.svelte";
|
||||
import PatchTeaser from "./Patch/PatchTeaser.svelte";
|
||||
import type { ToggleButtonOption } from "@app/ToggleButton.svelte";
|
||||
|
||||
import PatchTeaser from "./Patch/PatchTeaser.svelte";
|
||||
import Placeholder from "@app/Placeholder.svelte";
|
||||
import ToggleButton from "@app/ToggleButton.svelte";
|
||||
|
||||
import { Project, ProjectContent } from "@app/project";
|
||||
import { capitalize } from "@app/utils";
|
||||
import { groupPatches } from "@app/patch";
|
||||
|
||||
export let state: State = "proposed";
|
||||
export let config: Config;
|
||||
export let patches: Patch[];
|
||||
export let project: Project;
|
||||
|
||||
const navigate = (patch: string) => {
|
||||
project.navigateTo({
|
||||
content: ProjectContent.Patch,
|
||||
patch,
|
||||
issue: null,
|
||||
revision: null,
|
||||
path: null
|
||||
});
|
||||
};
|
||||
let options: ToggleButtonOption<State>[];
|
||||
const sortedPatches = groupPatches(patches);
|
||||
|
||||
$: filteredPatches = sortedPatches[state];
|
||||
$: options = [
|
||||
{
|
||||
value: "proposed",
|
||||
count: sortedPatches.proposed.length
|
||||
},
|
||||
{
|
||||
value: "draft",
|
||||
count: sortedPatches.draft.length
|
||||
},
|
||||
{
|
||||
value: "archived",
|
||||
count: sortedPatches.archived.length
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -41,13 +59,30 @@
|
||||
</style>
|
||||
|
||||
<div class="patches">
|
||||
<PatchFilter {patches} let:filteredPatches>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<ToggleButton {options} on:select={(e) => {state = e.detail;}} active={state} />
|
||||
</div>
|
||||
|
||||
{#if filteredPatches.length}
|
||||
<div class="patches-list">
|
||||
{#each filteredPatches as patch}
|
||||
<div class="teaser" on:click={() => navigate(patch.id)}>
|
||||
<div class="teaser" on:click={() => {
|
||||
project.navigateTo({
|
||||
content: ProjectContent.Patch,
|
||||
patch: patch.id,
|
||||
issue: null,
|
||||
revision: null,
|
||||
path: null
|
||||
});
|
||||
}}>
|
||||
<PatchTeaser {config} {patch} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</PatchFilter>
|
||||
{:else}
|
||||
<Placeholder icon="🍖">
|
||||
<div slot="title">{capitalize(state)} patches</div>
|
||||
<div slot="body">No patches matched the current filter</div>
|
||||
</Placeholder>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Config } from '@app/config';
|
||||
import type { State as IssueState } from './Issues.svelte';
|
||||
|
||||
import * as proj from '@app/project';
|
||||
import Placeholder from '@app/Placeholder.svelte';
|
||||
import Loading from '@app/Loading.svelte';
|
||||
@@ -30,6 +32,8 @@
|
||||
const parentName = project.profile ? formatProfile(project.profile.nameOrAddress, config) : null;
|
||||
let pageTitle = parentName ? `${parentName}/${project.name}` : project.name;
|
||||
|
||||
$: issueFilter = $browserStore.search?.get("state") as IssueState ?? "open";
|
||||
|
||||
const baseName = parentName
|
||||
? `${parentName}/${project.name}`
|
||||
: project.name;
|
||||
@@ -93,7 +97,7 @@
|
||||
|
||||
{#if content === proj.ProjectContent.Issues}
|
||||
<Async fetch={issue.Issue.getIssues(project.urn, project.seed.api)} let:result>
|
||||
<Issues {project} state={$browserStore.search?.get("state") || "open"} {config} issues={result} />
|
||||
<Issues {project} state={issueFilter} {config} issues={result} />
|
||||
</Async>
|
||||
{:else if content === proj.ProjectContent.Issue && $browserStore.issue}
|
||||
<Async fetch={issue.Issue.getIssue(project.urn, $browserStore.issue, project.seed.api)} let:result>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<style>
|
||||
.changeset-summary {
|
||||
padding: 1.5rem 0;
|
||||
padding-bottom: 1.5rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.changeset-summary .additions {
|
||||
|
||||
Reference in New Issue
Block a user