1
0

refactor to use CSS grid

This commit is contained in:
Nicola Clark 2024-10-12 23:47:09 -05:00
parent 8ac0f6b931
commit 8e32989785
Signed by: nicola
GPG Key ID: 3E1710E7FF08956C
16 changed files with 155 additions and 199 deletions

View File

@ -1,3 +1,3 @@
{
"cSpell.words": ["resumarkdown", "spacebar", "testid"]
"cSpell.words": ["resumarkdown", "spacebar", "tablist", "testid"]
}

View File

@ -12,7 +12,7 @@ body {
}
h1 {
font-weight: @weight-bold;
font-weight: @font-w-bold;
margin: 0;
padding: 0;
}

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -1,3 +0,0 @@
import { writable } from 'svelte/store';
export const navHeight = writable(-1);

View File

@ -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>

View File

@ -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;

View File

@ -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);
}
});

View File

@ -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 }) => {

View File

@ -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 }) => {