sveltekit-prototype: Add separator component and setup storybook (#54674)

This commit adds a simple separator component to be used for resizing
panels (e.g. sidebars) and is used on file tree pages. The way the 
separator works is quite simple atm: It computes its position in
relation
to the size of the parent element and saves that value in the provided
store.
The parent component can use this value however it sees fit.
The current value is persisted in local storage.
I assume that this functionality will be expanded or changed in the
future
but it's a start.

I also setup storybook 7, following the default setup instructions. I
left everybody as default except for deleting the example stories it
came with. We can always change the structure later.
Initially I wasn't able to run `pnpm run storybook`, it was throwing an
error about not being able to import `ts-dedent`. Adding it to the root
`package.json` file seems to resolve this issue.

Repo page: 


https://github.com/sourcegraph/sourcegraph/assets/179026/fb6ba082-8959-47ed-b2ae-8e1aa351b64c

Storybook: 

![2023-07-06_14-34](https://github.com/sourcegraph/sourcegraph/assets/179026/558fdbfc-cbc6-45c3-973b-92d1931f1395)



## Test plan

Open repo page and use separator.

Run storybooks with `pnpm run storybooks`

Run existing storybooks in the workspace roots. It's not impacted by
adding a new version.
This commit is contained in:
Felix Kling 2023-07-06 20:06:51 +02:00 committed by GitHub
parent faabc3a35b
commit 53e7816010
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 2091 additions and 154 deletions

View File

@ -1,13 +1,19 @@
const baseConfig = require('../../.eslintrc')
module.exports = {
root: true,
extends: '../../.eslintrc.js',
extends: ['../../.eslintrc.js', 'plugin:storybook/recommended'],
parserOptions: {
...baseConfig.parserOptions,
project: [__dirname + '/tsconfig.json', __dirname + '/src/**/tsconfig.json'],
},
plugins: [...baseConfig.plugins, 'svelte3'],
overrides: [...baseConfig.overrides, { files: ['*.svelte'], processor: 'svelte3/svelte3' }],
overrides: [
...baseConfig.overrides,
{
files: ['*.svelte'],
processor: 'svelte3/svelte3',
},
],
settings: {
...baseConfig.settings,
'svelte3/typescript': () => require('typescript'),

View File

@ -0,0 +1,13 @@
/** @type { import('@storybook/sveltekit').StorybookConfig } */
const config = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: {
name: '@storybook/sveltekit',
options: {},
},
docs: {
autodocs: 'tag',
},
}
export default config

View File

@ -0,0 +1,14 @@
/** @type { import('@storybook/svelte').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
}
export default preview

View File

@ -1,45 +1,58 @@
{
"name": "@sourcegraph/web-sveltekit",
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"dev:dotcom": "vite dev --mode=dotcom",
"dev:oss": "vite dev --mode=oss",
"build": "vite build",
"preview": "vite preview",
"test": "playwright test",
"sync": "svelte-kit sync",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint .",
"format": "prettier --plugin-search-dir . --write .",
"generate": "pnpm -w generate"
},
"devDependencies": {
"@playwright/test": "1.25.0",
"@sveltejs/adapter-auto": "^2.1.0",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.20.4",
"@types/cookie": "^0.5.1",
"@types/prismjs": "^1.26.0",
"eslint-plugin-svelte3": "^4.0.0",
"prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.0.0",
"svelte-check": "^3.4.3",
"tslib": "2.1.0",
"vite": "^4.3.9"
},
"type": "module",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@remix-run/router": "~1.3.2",
"@sourcegraph/branded": "workspace:*",
"@sourcegraph/common": "workspace:*",
"@sourcegraph/http-client": "workspace:*",
"@sourcegraph/shared": "workspace:*",
"@sourcegraph/web": "workspace:*",
"@sourcegraph/wildcard": "workspace:*",
"lodash-es": "^4.17.21",
"prismjs": "^1.29.0"
}
"name": "@sourcegraph/web-sveltekit",
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"dev:dotcom": "vite dev --mode=dotcom",
"dev:oss": "vite dev --mode=oss",
"build": "vite build",
"preview": "vite preview",
"test": "playwright test",
"sync": "svelte-kit sync",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint .",
"format": "prettier --plugin-search-dir . --write .",
"generate": "pnpm -w generate",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"devDependencies": {
"@playwright/test": "1.25.0",
"@storybook/addon-essentials": "^7.0.26",
"@storybook/addon-interactions": "^7.0.26",
"@storybook/addon-links": "^7.0.26",
"@storybook/blocks": "^7.0.26",
"@storybook/svelte": "^7.0.26",
"@storybook/sveltekit": "^7.0.26",
"@storybook/testing-library": "^0.0.14-next.2",
"@sveltejs/adapter-auto": "^2.1.0",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.20.4",
"@types/cookie": "^0.5.1",
"@types/prismjs": "^1.26.0",
"eslint-plugin-storybook": "^0.6.12",
"eslint-plugin-svelte3": "^4.0.0",
"prettier-plugin-svelte": "^2.10.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"storybook": "^7.0.26",
"svelte": "^4.0.0",
"svelte-check": "^3.4.3",
"tslib": "2.1.0",
"vite": "^4.3.9"
},
"type": "module",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@remix-run/router": "~1.3.2",
"@sourcegraph/branded": "workspace:*",
"@sourcegraph/common": "workspace:*",
"@sourcegraph/http-client": "workspace:*",
"@sourcegraph/shared": "workspace:*",
"@sourcegraph/web": "workspace:*",
"@sourcegraph/wildcard": "workspace:*",
"lodash-es": "^4.17.21",
"prismjs": "^1.29.0"
}
}

View File

@ -0,0 +1,102 @@
<script lang="ts" context="module">
import { derived, type Writable } from 'svelte/store'
import { createLocalWritable } from '$lib/stores'
const dividerStore = createLocalWritable<Record<string, number>>('dividers', {})
export function getSeparatorPosition(name: string, defaultValue: number): Writable<number> {
const { subscribe } = derived(dividerStore, dividers => dividers[name] ?? defaultValue)
return {
subscribe,
set(value) {
dividerStore.update(dividers => ({ ...dividers, [name]: value }))
},
update(updater) {
dividerStore.update(dividers => ({ ...dividers, [name]: updater(dividers[name]) }))
},
}
}
</script>
<script lang="ts">
/**
* Store to write current position (0-1) to.
*/
export let currentPosition: Writable<number>
let divider: HTMLElement | null = null
let offset = 0
let dragging = false
function onMouseMove(event: MouseEvent) {
event.preventDefault()
if (divider?.parentElement) {
let width = (event.x - offset) / divider.parentElement.clientWidth
if (width < 0) {
width = 0
} else if (width > 1) {
width = 1
}
$currentPosition = width
}
}
function endResize() {
dragging = false
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', endResize)
}
function startResize(event: MouseEvent) {
event.preventDefault()
if (divider?.parentElement) {
dragging = true
offset = divider.parentElement.getBoundingClientRect().x + divider.clientWidth
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', endResize)
}
}
</script>
<!-- TODO: implement keyboard handlers. See https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/ -->
<div
bind:this={divider}
role="separator"
tabindex="0"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={$currentPosition}
class:dragging
on:mousedown={startResize}
>
<!-- spacer is used to increase the interactable surface-->
<div class="spacer" />
</div>
<style lang="scss">
div[role='separator'] {
flex-shrink: 0;
position: relative;
width: 1px;
margin: 0 5px;
background-color: var(--border-color);
cursor: col-resize;
.spacer {
position: absolute;
top: 0;
bottom: 0;
left: -5px;
margin-left: -50%;
width: 10px;
}
&.dragging {
background-color: var(--oc-blue-3);
margin: 0 4px;
width: 3px;
}
}
</style>

View File

@ -1,5 +1,5 @@
import { getContext } from 'svelte'
import { readable, type Readable } from 'svelte/store'
import { readable, writable, type Readable, type Writable } from 'svelte/store'
import type { GraphQLClient } from '$lib/http-client'
import type { SettingsCascade, AuthenticatedUser, TemporarySettingsStorage } from '$lib/shared'
@ -53,3 +53,30 @@ export const graphqlClient = readable<GraphQLClient | null>(null, set => {
// eslint-disable-next-line no-void
void getWebGraphQLClient().then(client => set(client))
})
/**
* This store syncs the provided value with localStorage. Values must be JSON (de)seralizable.
*/
export function createLocalWritable<T>(localStorageKey: string, defaultValue: T): Writable<T> {
const { subscribe, set, update } = writable(defaultValue, set => {
const existingValue = localStorage.getItem(localStorageKey)
if (existingValue) {
set(JSON.parse(existingValue))
}
})
return {
subscribe,
set: value => {
set(value)
localStorage.setItem(localStorageKey, JSON.stringify(value))
},
update: fn => {
update(value => {
const newValue = fn(value)
localStorage.setItem(localStorageKey, JSON.stringify(newValue))
return newValue
})
},
}
}

View File

@ -5,6 +5,7 @@
import Icon from '$lib/Icon.svelte'
import FileTree from '$lib/repo/FileTree.svelte'
import { asStore } from '$lib/utils'
import Separator, { getSeparatorPosition } from '$lib/Separator.svelte'
import type { PageData } from './$types'
@ -15,11 +16,14 @@
}
$: treeOrError = asStore(data.treeEntries.deferred)
let showSidebar = true
const sidebarSize = getSeparatorPosition('repo-sidebar', 0.2)
$: sidebarWidth = showSidebar ? `max(200px, ${$sidebarSize * 100}%)` : undefined
</script>
<section>
<div class="sidebar" class:open={showSidebar}>
<div class="sidebar" class:open={showSidebar} style:min-width={sidebarWidth} style:max-width={sidebarWidth}>
{#if showSidebar && !$treeOrError.loading && $treeOrError.data}
<FileTree
activeEntry={$page.params.path ? last($page.params.path.split('/')) : ''}
@ -39,6 +43,9 @@
>
{/if}
</div>
{#if showSidebar}
<Separator currentPosition={sidebarSize} />
{/if}
<div class="content">
<slot />
</div>

View File

@ -0,0 +1,18 @@
import type { Meta, StoryObj } from '@storybook/svelte'
import Separator from '$lib/Separator.svelte'
import SeparatorExample from './SeparatorExample.svelte'
const meta: Meta<typeof SeparatorExample> = {
component: Separator,
}
export default meta
type Story = StoryObj<typeof meta>
export const SplitPane: Story = {
render: () => ({
Component: SeparatorExample,
}),
}

View File

@ -0,0 +1,38 @@
<script lang="ts">
import '../routes/styles.scss'
import Separator, { getSeparatorPosition } from '$lib/Separator.svelte'
const currentPosition = getSeparatorPosition('separator-example', 0.5)
$: width = `${$currentPosition * 100}%`
</script>
<section>
<div class="left" style:min-width={width} style:max-width={width}>Left content</div>
<Separator {currentPosition} />
<div class="right">Right content</div>
</section>
<style lang="scss">
section {
// Doesn't look like border-color is set for whatever reason, making the separator invisible
--border-color: black;
display: flex;
height: 90vh;
}
div {
display: flex;
align-items: center;
justify-content: center;
}
.left {
background-color: lightblue;
}
.right {
flex: 1;
background-color: lightgray;
}
</style>

View File

@ -330,6 +330,7 @@
"term-size": "^2.2.0",
"terser-webpack-plugin": "^5.3.6",
"text-table": "^0.2.0",
"ts-dedent": "^2.2.0",
"ts-loader": "^9.4.2",
"ts-node": "^10.7.0",
"typed-scss-modules": "^4.1.1",

File diff suppressed because it is too large Load Diff