mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-30 01:17:37 +03:00
segment control added (#4544)
This commit is contained in:
parent
ab0c70b673
commit
6194a58f5b
110
packages/ui/src/lib/SegmentControl/Segment.svelte
Normal file
110
packages/ui/src/lib/SegmentControl/Segment.svelte
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import type { SegmentContext } from './segmentTypes';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface SegmentProps {
|
||||||
|
id: string;
|
||||||
|
onselect?: (id: string) => void;
|
||||||
|
children: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, onselect, children }: SegmentProps = $props();
|
||||||
|
|
||||||
|
const context = getContext<SegmentContext>('SegmentControl');
|
||||||
|
const index = context.setIndex();
|
||||||
|
const selectedSegmentIndex = context.selectedSegmentIndex;
|
||||||
|
|
||||||
|
let elRef = $state<HTMLButtonElement>();
|
||||||
|
let isFocused = $state(false);
|
||||||
|
let isSelected = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
elRef && isFocused && elRef.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
isSelected = $selectedSegmentIndex === index;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
context.addSegment({ index });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
bind:this={elRef}
|
||||||
|
{id}
|
||||||
|
class="segment"
|
||||||
|
role="tab"
|
||||||
|
tabindex={isSelected ? -1 : 0}
|
||||||
|
aria-selected={isSelected}
|
||||||
|
onmousedown={() => {
|
||||||
|
if (index !== $selectedSegmentIndex) {
|
||||||
|
context.setSelected({
|
||||||
|
index,
|
||||||
|
id
|
||||||
|
});
|
||||||
|
onselect && onselect(id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onkeydown={({ key }) => {
|
||||||
|
if (key === 'Enter' || key === ' ') {
|
||||||
|
if (index !== $selectedSegmentIndex) {
|
||||||
|
context.setSelected({
|
||||||
|
index,
|
||||||
|
id
|
||||||
|
});
|
||||||
|
onselect && onselect(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
><span class="text-base-12 label">
|
||||||
|
{@render children()}
|
||||||
|
</span></button
|
||||||
|
>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.segment {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
user-select: none;
|
||||||
|
padding: 0 8px;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-left-width: 1px;
|
||||||
|
|
||||||
|
color: var(--clr-text-1);
|
||||||
|
border-color: var(--clr-border-2);
|
||||||
|
background-color: var(--clr-bg-1);
|
||||||
|
height: var(--size-button);
|
||||||
|
|
||||||
|
transition: background var(--transition-fast);
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
border-top-left-radius: var(--radius-m);
|
||||||
|
border-bottom-left-radius: var(--radius-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-top-right-radius: var(--radius-m);
|
||||||
|
border-bottom-right-radius: var(--radius-m);
|
||||||
|
border-right-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not([aria-selected='true']):hover {
|
||||||
|
background-color: var(--clr-bg-1-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-selected='true'] {
|
||||||
|
background-color: var(--clr-bg-2);
|
||||||
|
color: var(--clr-text-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
53
packages/ui/src/lib/SegmentControl/SegmentControl.svelte
Normal file
53
packages/ui/src/lib/SegmentControl/SegmentControl.svelte
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { setContext } from 'svelte';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import type { SegmentContext, SegmentItem } from './segmentTypes';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface SegmentProps {
|
||||||
|
selectedIndex: number;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
onselect?: (id: string) => void;
|
||||||
|
children: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { selectedIndex, fullWidth, onselect, children }: SegmentProps = $props();
|
||||||
|
|
||||||
|
let indexesIterator = -1;
|
||||||
|
let segments: SegmentItem[] = [];
|
||||||
|
|
||||||
|
let selectedSegmentIndex = writable(selectedIndex);
|
||||||
|
|
||||||
|
const context: SegmentContext = {
|
||||||
|
selectedSegmentIndex,
|
||||||
|
setIndex: () => {
|
||||||
|
indexesIterator += 1;
|
||||||
|
return indexesIterator;
|
||||||
|
},
|
||||||
|
addSegment: ({ index }) => {
|
||||||
|
segments = [...segments, { index }];
|
||||||
|
},
|
||||||
|
setSelected: ({ index: segmentIndex, id }) => {
|
||||||
|
if (segmentIndex >= 0 && segmentIndex < segments.length) {
|
||||||
|
$selectedSegmentIndex = segmentIndex;
|
||||||
|
onselect && onselect(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setContext<SegmentContext>('SegmentControl', context);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper" class:full-width={fullWidth}>
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
11
packages/ui/src/lib/SegmentControl/segmentTypes.ts
Normal file
11
packages/ui/src/lib/SegmentControl/segmentTypes.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export interface SegmentItem {
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
export interface SegmentContext {
|
||||||
|
selectedSegmentIndex: Writable<number>;
|
||||||
|
setIndex(): number;
|
||||||
|
addSegment(segment: SegmentItem): void;
|
||||||
|
setSelected({ index, id }: { index: number; id: string }): void;
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import SegmentControl from './SegmentControl.svelte';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/svelte';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
component: SegmentControl
|
||||||
|
} satisfies Meta<SegmentControl>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const SegmentControlStory: Story = {
|
||||||
|
args: {
|
||||||
|
selectedIndex: 1,
|
||||||
|
fullWidth: false
|
||||||
|
}
|
||||||
|
};
|
24
packages/ui/src/stories/SegmentControl/SegmentControl.svelte
Normal file
24
packages/ui/src/stories/SegmentControl/SegmentControl.svelte
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Segment from '$lib/SegmentControl/Segment.svelte';
|
||||||
|
import SegmentControl from '$lib/SegmentControl/SegmentControl.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedIndex: number;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { selectedIndex, fullWidth }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SegmentControl
|
||||||
|
{selectedIndex}
|
||||||
|
{fullWidth}
|
||||||
|
onselect={(id) => {
|
||||||
|
console.log('Selected index:', id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Segment id="first">First</Segment>
|
||||||
|
<Segment id="second">Second</Segment>
|
||||||
|
<Segment id="third">Third</Segment>
|
||||||
|
<Segment id="fourth">Fourth</Segment>
|
||||||
|
</SegmentControl>
|
Loading…
Reference in New Issue
Block a user