Extract ToggleButton component

Signed-off-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
Rūdolfs Ošiņš
2022-08-31 10:40:31 +02:00
committed by Alexis Sellier
parent c5eddde23f
commit caac53a8f6
9 changed files with 220 additions and 232 deletions
+68
View File
@@ -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>
+1
View File
@@ -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">&middot;</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}
+52 -18
View File
@@ -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}
+43 -52
View File
@@ -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>
+50 -15
View File
@@ -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>
+5 -1
View File
@@ -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 {