refactor to use CSS grid
This commit is contained in:
parent
8ac0f6b931
commit
8e32989785
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -1,3 +1,3 @@
|
||||
{
|
||||
"cSpell.words": ["resumarkdown", "spacebar", "testid"]
|
||||
"cSpell.words": ["resumarkdown", "spacebar", "tablist", "testid"]
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ body {
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: @weight-bold;
|
||||
font-weight: @font-w-bold;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -15,12 +15,5 @@
|
||||
padding: @padding-lg;
|
||||
resize: none;
|
||||
width: .full-without-padding(@padding-lg-x) [];
|
||||
|
||||
@media screen and (min-width: @sizes[lg]) {
|
||||
font-size: unset;
|
||||
height: .full-without-padding(@padding-xl-y) [];
|
||||
margin: @padding-xl;
|
||||
width: .full-without-padding(@padding-xl-x) [];
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,54 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { navHeight } from '$lib/stores/layout';
|
||||
import { pane as navStore, type Pane } from '$lib/stores/nav';
|
||||
|
||||
export let pane: Pane;
|
||||
|
||||
export let testid: string | undefined = undefined;
|
||||
|
||||
// don't show pane if we're not the current pane
|
||||
let hidden: boolean;
|
||||
$: hidden = pane !== $navStore;
|
||||
|
||||
// always show preview pane on a two column layout
|
||||
let preview: boolean;
|
||||
$: preview = pane === 'preview';
|
||||
|
||||
// offset preview pane upward on desktop
|
||||
let offset: number;
|
||||
$: offset = preview ? $navHeight : 0;
|
||||
</script>
|
||||
|
||||
<div
|
||||
id={`pane-${pane}`}
|
||||
data-testid={testid}
|
||||
role="tabpanel"
|
||||
class:hidden
|
||||
class:preview
|
||||
style:--offset={`-${offset}px`}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
div {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: @sizes[lg]) {
|
||||
border-right: 2px solid @color-dark;
|
||||
width: 50%;
|
||||
|
||||
&.preview {
|
||||
border-right: unset !important;
|
||||
display: unset !important;
|
||||
height: calc(100% - var(--offset));
|
||||
transform: translateY(var(--offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
25
src/lib/components/editor.svelte
Normal file
25
src/lib/components/editor.svelte
Normal file
@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { pane as navStore, type Pane } from '$lib/stores/nav';
|
||||
|
||||
export let pane: Pane;
|
||||
|
||||
export let testid: string | undefined = undefined;
|
||||
|
||||
// don't show pane if we're not the current pane
|
||||
let hidden: boolean;
|
||||
$: hidden = pane !== $navStore;
|
||||
</script>
|
||||
|
||||
<div id={`pane-${pane}`} data-testid={testid} role="tabpanel" class:hidden>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
div {
|
||||
grid-area: editor;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,4 +1,6 @@
|
||||
<h1><slot /></h1>
|
||||
<header>
|
||||
<h1><slot /></h1>
|
||||
</header>
|
||||
|
||||
<style lang="less">
|
||||
h1 {
|
||||
@ -8,4 +10,8 @@
|
||||
padding: @padding-md;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header {
|
||||
grid-area: headline;
|
||||
}
|
||||
</style>
|
||||
|
@ -6,10 +6,6 @@
|
||||
let selected: boolean;
|
||||
$: selected = destination === $pane;
|
||||
|
||||
// we need to hide the "preview" tab on desktop
|
||||
let hiddenOnDesktop: boolean;
|
||||
$: hiddenOnDesktop = destination === 'preview';
|
||||
|
||||
function handleKey({ key }: KeyboardEvent) {
|
||||
if (key === ' ' || key === 'Enter' || key === 'Spacebar') {
|
||||
navigate();
|
||||
@ -26,7 +22,6 @@
|
||||
aria-selected={selected}
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
class:hiddenOnDesktop
|
||||
class:selected
|
||||
on:click={navigate}
|
||||
on:keyup={handleKey}
|
||||
@ -36,21 +31,14 @@
|
||||
|
||||
<style lang="less">
|
||||
li {
|
||||
box-sizing: border-box;
|
||||
font-weight: @weight-semibold;
|
||||
@separator: 1px solid @color-dark;
|
||||
|
||||
font-weight: @font-w-semibold;
|
||||
margin: 0;
|
||||
padding: @padding-md;
|
||||
text-align: center;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
@media screen and (min-width: @sizes[lg]) {
|
||||
width: 50%;
|
||||
|
||||
&.hiddenOnDesktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: darken(@color-light, 20%);
|
||||
}
|
||||
@ -61,17 +49,15 @@
|
||||
}
|
||||
|
||||
&:not(:last-of-type) {
|
||||
@separator: 1px solid @color-dark;
|
||||
|
||||
border-bottom: @separator;
|
||||
}
|
||||
|
||||
@media screen and (min-width: @sizes[lg]) {
|
||||
border-bottom: unset;
|
||||
border-right: @separator;
|
||||
flex: 1;
|
||||
|
||||
&:nth-of-type(n + 2) {
|
||||
border-right: unset;
|
||||
}
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: none;
|
||||
border-right: @separator;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,12 +18,8 @@
|
||||
border: none;
|
||||
color: @color-light;
|
||||
font-size: @font-s-md;
|
||||
font-weight: @weight-semibold;
|
||||
font-weight: @font-w-semibold;
|
||||
padding: @padding-sm;
|
||||
width: 100%;
|
||||
|
||||
@media screen and (min-width: @sizes[lg]) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,25 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
import { navHeight } from '$lib/stores/layout';
|
||||
|
||||
import NavToggle from './nav-toggle.svelte';
|
||||
|
||||
export let mobile: boolean = true;
|
||||
|
||||
let open: boolean = false;
|
||||
|
||||
// magic to offset preview pane on desktop
|
||||
let navRef: HTMLElement;
|
||||
|
||||
onMount(() => {
|
||||
$navHeight = navRef.getBoundingClientRect().height;
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav bind:this={navRef}>
|
||||
<nav>
|
||||
{#if mobile}
|
||||
<NavToggle bind:navOpen={open} />
|
||||
{/if}
|
||||
{#if open || !mobile}
|
||||
<ul role="tablist" transition:slide={{ duration: 300 }}>
|
||||
<slot />
|
||||
@ -28,6 +20,10 @@
|
||||
</nav>
|
||||
|
||||
<style lang="less">
|
||||
nav {
|
||||
grid-area: navtree;
|
||||
}
|
||||
|
||||
ul {
|
||||
@border: 2px solid @color-dark;
|
||||
|
||||
@ -38,12 +34,11 @@
|
||||
padding: 0;
|
||||
|
||||
@media screen and (min-width: @sizes[lg]) {
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
border-right: @border;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
width: 50%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
32
src/lib/components/preview.svelte
Normal file
32
src/lib/components/preview.svelte
Normal file
@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { pane } from '$lib/stores/nav';
|
||||
|
||||
export let mobile: boolean;
|
||||
|
||||
let hidden: boolean;
|
||||
$: hidden = mobile ? $pane !== 'preview' : true;
|
||||
|
||||
export let markdown: string;
|
||||
export let stylesheet: string;
|
||||
</script>
|
||||
|
||||
<main class:hidden class:mobile data-testid="preview-pane">
|
||||
<h2>markdown:</h2>
|
||||
<code>{markdown}</code>
|
||||
<h2>stylesheet:</h2>
|
||||
<code>{stylesheet}</code>
|
||||
</main>
|
||||
|
||||
<style lang="less">
|
||||
main {
|
||||
grid-area: preview;
|
||||
|
||||
&.mobile {
|
||||
grid-area: editor;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,3 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const navHeight = writable(-1);
|
@ -1,12 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import { pane } from '$lib/stores/nav.js';
|
||||
|
||||
import CodeInput from '$lib/components/code-input.svelte';
|
||||
import Content from '$lib/components/content.svelte';
|
||||
import Editor from '$lib/components/editor.svelte';
|
||||
import Headline from '$lib/components/headline.svelte';
|
||||
import NavItem from '$lib/components/nav-item.svelte';
|
||||
import NavTree from '$lib/components/nav-tree.svelte';
|
||||
import Preview from '$lib/components/preview.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
let mobile: boolean = data.mobile;
|
||||
|
||||
function checkIsDesktop() {
|
||||
// TODO: figure out how to remove hard-coded value here
|
||||
mobile = !window.matchMedia('screen and (min-width: 1280px)').matches;
|
||||
|
||||
if (!mobile && get(pane) === 'preview') {
|
||||
pane.set('content');
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
checkIsDesktop();
|
||||
|
||||
window.addEventListener('resize', checkIsDesktop);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', checkIsDesktop);
|
||||
};
|
||||
});
|
||||
|
||||
let markdown: string = '';
|
||||
let stylesheet: string = '';
|
||||
</script>
|
||||
@ -15,65 +42,40 @@
|
||||
<title>resumarkdown</title>
|
||||
</svelte:head>
|
||||
<div>
|
||||
<header>
|
||||
<Headline>resumarkdown</Headline>
|
||||
<NavTree mobile={data.mobile}>
|
||||
<NavTree {mobile}>
|
||||
<NavItem destination="content">content</NavItem>
|
||||
<NavItem destination="style">style</NavItem>
|
||||
{#if mobile}
|
||||
<NavItem destination="preview">preview</NavItem>
|
||||
{/if}
|
||||
</NavTree>
|
||||
</header>
|
||||
<main>
|
||||
<Content pane="content" testid="content-pane">
|
||||
<Editor pane="content" testid="content-pane">
|
||||
<CodeInput bind:code={markdown} />
|
||||
</Content>
|
||||
<Content pane="style" testid="style-pane">
|
||||
</Editor>
|
||||
<Editor pane="style" testid="style-pane">
|
||||
<CodeInput bind:code={stylesheet} />
|
||||
</Content>
|
||||
<Content pane="preview" testid="preview-pane">
|
||||
<dl>
|
||||
<dt>markdown:</dt>
|
||||
<dd>{markdown !== '' ? markdown : '???'}</dd>
|
||||
<dt>stylesheet:</dt>
|
||||
<dd>{stylesheet !== '' ? stylesheet : '???'}</dd>
|
||||
</dl>
|
||||
</Content>
|
||||
</main>
|
||||
</Editor>
|
||||
<Preview {mobile} {markdown} {stylesheet} />
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
// util
|
||||
.margin-v(@v) {
|
||||
margin-bottom: @v;
|
||||
margin-top: @v;
|
||||
}
|
||||
|
||||
// styles
|
||||
div {
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: start;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
'headline'
|
||||
'navtree'
|
||||
'editor';
|
||||
grid-template-rows: min-content min-content 1fr;
|
||||
height: 100%;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
dd {
|
||||
.margin-v(@padding-lg-y);
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 0;
|
||||
padding: @padding-xl;
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
|
||||
@media screen and (min-width: @sizes[lg]) {
|
||||
align-items: start;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-areas:
|
||||
'headline headline'
|
||||
'navtree preview'
|
||||
'editor preview';
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -9,8 +9,8 @@
|
||||
@fonts-sans: Rubik, Arial, sans-serif;
|
||||
@font-s-lg: 2rem;
|
||||
@font-s-md: 1.25rem;
|
||||
@weight-bold: 700;
|
||||
@weight-semibold: 600;
|
||||
@font-w-bold: 700;
|
||||
@font-w-semibold: 600;
|
||||
|
||||
// layout
|
||||
@border-r-xl: 0.75em;
|
||||
|
@ -2,42 +2,21 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
test('desktop page has nav tree', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('nav')).toBeVisible();
|
||||
await expect(page.getByRole('navigation')).toBeVisible();
|
||||
});
|
||||
|
||||
test('desktop page does not have nav toggle', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('nav button')).toBeHidden();
|
||||
await expect(page.getByRole('navigation').getByRole('button')).toBeHidden();
|
||||
});
|
||||
|
||||
test('desktop page has two-column layout', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('#pane-preview')).toBeVisible();
|
||||
await expect(page.getByTestId('content-pane')).toBeVisible();
|
||||
await expect(page.getByTestId('preview-pane')).toBeVisible();
|
||||
});
|
||||
|
||||
test('desktop page has no "preview" nav item', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('tab').filter({ hasText: 'preview' })).toBeHidden();
|
||||
});
|
||||
|
||||
test('desktop page has equal width for both columns', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const { width: contentWidth } = (await page.getByTestId('content-pane').boundingBox()) ?? {
|
||||
width: -1,
|
||||
};
|
||||
const { width: previewWidth } = (await page.getByTestId('preview-pane').boundingBox()) ?? {
|
||||
width: -2,
|
||||
};
|
||||
expect(contentWidth).toEqual(previewWidth);
|
||||
});
|
||||
|
||||
test('desktop page has equal width for nav items', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const visibleTabs = page.getByRole('tab').filter({ hasNotText: 'preview' });
|
||||
const { width: firstTabWidth } = (await visibleTabs.first().boundingBox()) ?? {
|
||||
width: -1,
|
||||
};
|
||||
for (let i = 1; i < (await visibleTabs.count()); i++) {
|
||||
await expect((await visibleTabs.nth(i).boundingBox())?.width).toEqual(firstTabWidth);
|
||||
}
|
||||
});
|
||||
|
@ -2,9 +2,7 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
test('page has headline', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const headline = page.locator('header h1');
|
||||
await expect(headline).toBeVisible();
|
||||
await expect(headline).toHaveText('resumarkdown');
|
||||
await expect(page.getByRole('banner')).toHaveText('resumarkdown');
|
||||
});
|
||||
|
||||
test('nav items work', async ({ page }) => {
|
||||
|
@ -9,25 +9,26 @@ test.use({
|
||||
|
||||
test('mobile page has nav tree hidden by default', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('nav ul')).toBeHidden();
|
||||
await expect(page.getByRole('navigation').getByRole('tablist')).toBeHidden();
|
||||
});
|
||||
|
||||
test('mobile page has nav toggle', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('nav button')).toBeVisible();
|
||||
await expect(page.getByRole('navigation').getByRole('button')).toBeVisible();
|
||||
});
|
||||
|
||||
test('nav toggle works', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.locator('nav button').click();
|
||||
await expect(page.locator('nav ul')).toBeVisible();
|
||||
await page.locator('nav button').click();
|
||||
await expect(page.locator('nav ul')).toBeHidden();
|
||||
await page.getByText('show navigation').click();
|
||||
await expect(page.getByRole('navigation').getByRole('tablist')).toBeVisible();
|
||||
await page.getByText('hide navigation').click();
|
||||
await expect(page.getByRole('navigation').getByRole('tablist')).toBeHidden();
|
||||
});
|
||||
|
||||
test('mobile page has single-column layout', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('#pane-preview')).toBeHidden();
|
||||
await expect(page.getByTestId('content-pane')).toBeVisible();
|
||||
await expect(page.getByTestId('preview-pane')).toBeHidden();
|
||||
});
|
||||
|
||||
test('mobile page has preview nav item', async ({ page }) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user