create layouts

This commit is contained in:
konnorrogers
2023-08-30 15:28:33 -04:00
parent af7682aaca
commit 191f7d708c
33 changed files with 1751 additions and 17 deletions

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html
lang="en"
data-layout="{{ layout }}"
data-shoelace-version="{{ meta.version }}"
>
<head>
{# Metadata #}
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="{{ meta.description }}" />
<title>{{ meta.title }}</title>
{# Opt out of Turbo caching #}
<meta name="turbo-cache-control">
{# Favicons #}
<link rel="icon" href="{{ assetUrl('images/logo.svg') }}" type="image/x-icon" />
{# Twitter Cards #}
<meta name="twitter:card" content="summary" />
<meta name="twitter:creator" content="shoelace_style" />
<meta name="twitter:image" content="{{ assetUrl(meta.image, true) }}" />
{# OpenGraph #}
<meta property="og:url" content="{{ rootUrl(page.url, true) }}" />
<meta property="og:title" content="{{ meta.title }}" />
<meta property="og:description" content="{{ meta.description }}" />
<meta property="og:image" content="{{ assetUrl(meta.image, true) }}" />
{# Shoelace #}
<link rel="stylesheet" href="/dist/themes/light.css" />
<link rel="stylesheet" href="/dist/themes/dark.css" />
<script type="module" src="/dist/shoelace-autoloader.js"></script>
{# Set the initial theme and menu states here to prevent flashing #}
<script>
(() => {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = localStorage.getItem('theme') || 'auto';
document.documentElement.classList.toggle('sl-theme-dark', theme === 'dark' || (theme === 'auto' && prefersDark));
})();
</script>
</head>
<body>
{% block content %}
{{ content | safe }}
{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,64 @@
{% include "layout-templates/reset.css.njk" %}
.grid {
font-size: 1.35rem;
text-align: center;
display: grid;
place-content: center;
padding: 1rem;
}
header {
background-color: var(--sl-color-blue-100);
}
aside {
min-width: 250px;
max-width: 250px;
height: 100%;
}
main {
background-color: var(--sl-color-green-100);
height: 100%;
}
footer {
background-color: var(--sl-color-blue-200);
}
.banner {
background-color: var(--sl-color-amber-100);
}
.header {
background-color: var(--sl-color-blue-100);
}
.banner, .header {
min-width: 100%;
height: 100%;
}
[slot=header] {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
[slot=aside] {
background-color: var(--sl-color-yellow-100);
}
[slot=menu] {
background-color: var(--sl-color-pink-100);
}
[slot=main-header] {
background-color: var(--sl-color-red-200);
padding: 1rem;
}
[slot=main-footer] {
background-color: var(--sl-color-lime-200);
}

View File

@@ -0,0 +1,22 @@
<sl-layout main-id="main-content" class="sl-theme-light">
<header slot="banner" class="grid banner">
Banner
</header>
<header slot="header" class="grid header">Header</header>
<aside class="grid" slot="menu">Menu</aside>
<header class="grid" slot="main-header">Inline header</header>
<main class="grid" id="main-content">
<div style="width: 20ch; margin: 0 auto; background-color: white;">Main</div>
</main>
<footer class="grid" slot="main-footer">Inline footer</footer>
<aside class="grid" slot="aside">Aside</aside>
<footer class="grid" slot="footer">Footer</footer>
</sl-layout>
{% include "layout-widget.njk" %}

View File

@@ -0,0 +1,241 @@
{% include "layout-templates/reset.css.njk" %}
main {
min-height: 100%;
padding: 1rem 2rem;
}
/* Layout */
sl-layout {
background-color: var(--sl-color-neutral-50);
color: var(--sl-color-neutral-800);
}
sl-layout::part(header) {
/** Because headers are sticky, this keeps text from leaking through. */
background-color: var(--sl-color-neutral-0);
}
sl-layout[view="mobile"] {
background-color: var(--sl-color-neutral-0);
--menu-width: 0px;
}
sl-layout[view="mobile"]::part(header) {
padding: 0.25rem;
border-bottom: 1px solid var(--sl-color-neutral-300);
}
sl-layout[view="mobile"]::part(navigation) {
display: none;
}
sl-layout[view="desktop"]::part(main) {
padding-top: 1rem;
}
sl-layout[view="desktop"] {
--menu-width: 250px;
}
sl-layout[view="desktop"]::part(navigation) {
padding-top: 1.9rem;
}
sl-layout[view="desktop"] > [slot="header"] {
display: none;
}
sl-layout[view="desktop"]::part(header) {
display: none;
}
sl-layout[view="desktop"] main {
background-color: var(--sl-color-neutral-0);
box-shadow: 0px 0px 3px 1px rgba(0,0,0,0.05);
border: 1px solid var(--sl-color-neutral-200);
border-top-left-radius: 8px;
}
/* Navigation / Lists */
.app-navigation {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.app-navigation__list {
margin: 0;
padding: 0;
list-style-type: "";
display: flex;
flex-direction: column;
gap: 3px;
max-height: 100%;
overflow-y: auto;
height: 100%;
padding-bottom: 4px;
}
.app-navigation__list li {
position: relative;
padding: 0px 8px;
}
/* We use a blank marker to work around a safari bug with "list-style-type: none;" */
.app-navigation__list li::marker {
content: "";
}
.app-navigation__list li.is-active sl-button {
--background-color: var(--sl-color-primary-600);
--background-color-active: var(--sl-color-primary-700);
--text-color: var(--sl-color-neutral-0);
--text-color-active: var(--sl-color-neutral-0);
}
.app-navigation__list li sl-button {
--text-color: var(--sl-color-neutral-600);
}
/* Highlights */
.highlight {
font-size: 0.85em;
padding: 0.4em 0.6em;
border-radius: 6px;
display: inline-block;
}
.highlight--success {
background-color: var(--sl-color-success-50);
color: var(--sl-color-success-700);
}
.highlight--danger {
background-color: var(--sl-color-danger-50);
color: var(--sl-color-danger-700);
}
/* Text */
.text--light {
color: var(--sl-color-neutral-600);
}
/* Cards */
.sl-card--muted::part(base) {
--border-color: transparent;
background-color: transparent;
display: grid;
height: 100%;
box-shadow: none;
}
.sl-card--muted::part(body) {
display: grid;
align-content: flex-end;
gap: var(--padding);
}
/* Buttons */
.sl-button--card {
--border-radius: 8px;
--padding: 1rem 0px;
}
.sl-button--card.sl-button--muted {
--background-color: var(--sl-color-neutral-100);
}
.sl-button--card::part(base) {
border-radius: var(--border-radius);
padding: var(--padding);
}
.sl-button--card::part(label) {
width: 100%;
}
.sl-button--muted {
--text-color: var(--sl-color-neutral-700);
--text-color-active: var(--sl-color-neutral-700);
--background-color: transparent;
--background-color-active: var(--sl-color-neutral-200);
--border-color: transparent;
--border-color-active: var(--sl-color-neutral-200);
}
.sl-button--muted::part(base) {
background-color: var(--background-color);
color: var(--text-color);
border-color: var(--border-color);
}
.sl-button--muted:is(:focus-within)::part(base) {
background-color: var(--background-color);
color: var(--text-color-active);
border-color: var(--border-color-active);
}
.sl-button--muted:is(:hover)::part(base) {
background-color: var(--background-color-active);
color: var(--text-color-active);
border-color: var(--border-color-active);
}
.sl-button--logo::part(base) {
font-size: 1.5rem;
color: var(--sl-color-neutral-700);
}
.sl-button--square::part(base) {
border-radius: 0px;
}
.sl-button--stretch {
width: 100%;
}
.sl-button--stretch::part(label) {
flex: 1 1 auto;
}
.sl-button--nav-footer {
--border-color: var(--sl-color-neutral-300);
--sl-spacing-large: 8px;
}
sl-layout[view="desktop"] .sl-button--nav-footer::part(base) {
--border-color: transparent;
border-top-color: var(--sl-color-neutral-300);
}
/* Tables */
table {
max-width: 100%;
border: none;
border-collapse: collapse;
color: inherit;
}
table tr {
border-bottom: 1px solid var(--sl-color-neutral-300);
}
table th {
font-weight: var(--sl-font-weight-semibold);
text-align: left;
padding: 0.75rem 1rem;
}
table td {
line-height: var(--sl-line-height-normal);
padding: 1rem;
}
* > table {
max-width: 100%;
overflow-x: auto;
}

View File

@@ -0,0 +1,449 @@
<sl-layout main-id="main-content" class="sl-theme-light">
<sl-button href="#" variant="text" style="padding: 0 0.4rem" class="sl-button--logo sl-button--stretch sl-button--muted" size="large" slot="navigation-header">
<sl-icon name="music-note" slot="prefix" style="font-size: 2rem;"></sl-icon>
Musicify
</sl-button>
<nav class="app-navigation" slot="navigation">
<ul class="app-navigation__list">
<li>
<sl-button class="sl-button--muted sl-button--stretch" href="#" variant="text">
<sl-icon name="search" slot="prefix"></sl-icon>
Search
</sl-button>
</li>
<li>
<sl-button class="sl-button--muted sl-button--stretch" href="#" variant="text">
<sl-icon name="bell" slot="prefix"></sl-icon>
Notifications
</sl-button>
</li>
<li><sl-divider style=""></sl-divider></li>
<li class="is-active">
<sl-button class="sl-button--muted sl-button--stretch" href="#" variant="text">
<sl-icon name="house-door" slot="prefix"></sl-icon>
Home
</sl-button>
</li>
<li>
<sl-button class="sl-button--muted sl-button--stretch" href="#" variant="text">
<sl-icon name="music-note-list" slot="prefix"></sl-icon>
Playlists
</sl-button>
</li>
<li>
<sl-button class="sl-button--muted sl-button--stretch" href="#" variant="text">
<sl-icon name="file-earmark-music" slot="prefix"></sl-icon>
Tracks
</sl-button>
</li>
<li>
<sl-button class="sl-button--muted sl-button--stretch" href="#" variant="text">
<sl-icon name="gear" slot="prefix"></sl-icon>
Settings
</sl-button>
</li>
<li style="margin-top: auto;">
<sl-button class="sl-button--muted sl-button--stretch" href="#" variant="text">
<sl-icon name="question-circle" slot="prefix"></sl-icon>
Help
</sl-button>
</li>
</ul>
</nav>
<!-- Hacky override to make padding 8px -->
<sl-button slot="navigation-footer" outline class="sl-button--square sl-button--stretch sl-button--muted sl-button--nav-footer" size="large" href="#">
<div style="display: grid; align-items: center; max-width: 100%; gap: 8px; grid-template-columns: minmax(0, auto) minmax(0, 1fr) minmax(0, auto);">
<sl-avatar shape="rounded" style="--size: 36px;"></sl-avatar>
<div style="text-overflow: ellipsis; max-width: 100%; overflow: hidden; text-align: start;">Really really really long name</div>
<sl-icon name="chevron-right"></sl-icon>
</div>
</sl-button>
<main id="main-content" class="main">
<h1 style="margin: 0.5rem 0 2rem 0;">Good Evening, Konnor Rogers</h1>
<section>
<div style="display: flex; justify-content: space-between; flex-wrap: wrap; align-items: flex-end; gap: 8px;">
<h2 style="">Overview</h2>
<sl-select value="monthly">
<sl-option value="daily">Daily</sl-option>
<sl-option value="weekly">Weekly</sl-option>
<sl-option value="monthly">Monthly</sl-option>
<sl-option value="yearly">Yearly</sl-option>
</sl-select>
</div>
<sl-divider></sl-divider>
<div style="margin-top: 1rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-auto-rows: 1fr; gap: var(--sl-spacing-large); text-align: center;">
<sl-card class="sl-card--muted" style="--padding: 8px;">
<h3 slot="header">Total listening time</h3>
<p>
<strong><sl-format-number value="35000"></sl-format-number></strong> minutes
</p>
<p>
<mark class="highlight highlight--success">
+16%
</mark>
<small class="text--light">from last month</small>
</p>
</sl-card>
<sl-card class="sl-card--muted" style="--padding: 8px;">
<h3 slot="header">Total songs played</h3>
<p>
<strong><sl-format-number value="302"></sl-format-number></strong> songs
</p>
<p>
<mark class="highlight highlight--danger">
-0.3%
</mark>
<small class="text--light">from last month</small>
</p>
</sl-card>
<sl-card class="sl-card--muted" style="--padding: 8px;">
<h3 slot="header">Average listening session</h3>
<p>
<strong><sl-format-number value="36"></sl-format-number></strong> minutes
</p>
<p>
<mark class="highlight highlight--success">
+11.4%
</mark>
<small class="text--light">from last month</small>
</p>
</sl-card>
<sl-card class="sl-card--muted" style="--padding: 8px;">
<h3 slot="header">Average track listening time</h3>
<p>
<strong><sl-format-number value="2"></sl-format-number></strong> minutes,
<strong><sl-format-number value="42"></sl-format-number></strong> seconds
</p>
<p>
<mark class="highlight highlight--success">
-6.2%
</mark>
<small class="text--light">from last month</small>
</p>
</sl-card>
</div>
</section>
<section style="margin-top: 3rem;">
<h2>Recent playlists</h2>
<div style="margin-top: 1rem; --card-width: clamp(200px, 100%, 350px); display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--card-width), 1fr)); gap: 16px;">
<sl-button variant="neutral" class="sl-button--card sl-button--muted" href="#">
<div style="display: flex; gap: 1rem;">
<img src="https://via.placeholder.com/100x100" height="100" width="100" style="align-self: center; border-radius: 8px; display: inline-block; max-width: 100%; flex: 0 1 auto;">
<article style="display: grid; align-content: center; color: var(--sl-color-neutral-700); grid-template-columns: minmax(0, 1fr); max-width: 100%; overflow: hidden; width: 100%;">
<h2 style="max-width: 100%; text-overflow: ellipsis; overflow: hidden;">
Punk Rock Anthems
</h2>
<p style="max-width: 100%; text-overflow: ellipsis; overflow: hidden;">
For when you just wanna rock out, have a good time, and feel angsty.
</p>
<sl-icon name="chevron-right" style="justify-self: flex-end;"></sl-icon>
</article>
</div>
</sl-button>
<sl-button variant="neutral" class="sl-button--card sl-button--muted" href="#">
<div style="display: flex; gap: 1rem;">
<img src="https://via.placeholder.com/100x100" height="100" width="100" style="align-self: center; border-radius: 8px; display: inline-block; max-width: 100%;">
<article style="display: grid; align-content: center; color: var(--sl-color-neutral-700); grid-template-columns: minmax(0, 1fr); max-width: 100%; overflow: hidden; width: 100%;">
<h2 style="max-width: 100%; text-overflow: ellipsis; overflow: hidden;">
Random
</h2>
<p style="max-width: 100%; text-overflow: ellipsis; overflow: hidden;">
Throw it on shuffle, and embrace the chaos.
</p>
<sl-icon name="chevron-right" style="justify-self: flex-end;"></sl-icon>
</article>
</div>
</sl-button>
<sl-button variant="neutral" class="sl-button--card sl-button--muted" href="#">
<div style="display: flex; gap: 1rem;">
<img src="https://via.placeholder.com/100x100" height="100" width="100" style="align-self: center; border-radius: 8px; display: inline-block;">
<article style="display: grid; align-content: center; color: var(--sl-color-neutral-700); grid-template-columns: minmax(0, 1fr); max-width: 100%; overflow: hidden; width: 100%;">
<h2 style="max-width: 100%; text-overflow: ellipsis; overflow: hidden;">
Classics
</h2>
<p style="max-width: 100%; text-overflow: ellipsis; overflow: hidden;">
Timeless songs that you love to relive.
</p>
<sl-icon name="chevron-right" style="justify-self: flex-end;"></sl-icon>
</article>
</div>
</sl-button>
</div>
</section>
<section style="margin-top: 3rem;">
<h2>Recent tracks</h2>
<div style="margin-top: 1rem; max-width: 100%; overflow: auto;">
<table style="width: 100%; min-width: 500px; border-spacing: 2px;">
<thead>
<tr>
<th>
Release Date
</th>
<th>
Name
</th>
<th>
Album
</th>
<th>
Artist
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<sl-format-date date="2020-07-15T09:17:00-04:00"></sl-format-date>
</td>
<td>
No Strangers to Love
</td>
<td>
You Know the Rules
</td>
<td>
Rick Barry
</td>
</tr>
<tr>
<td>
<sl-format-date date="2020-07-15T09:17:00-04:00"></sl-format-date>
</td>
<td>
No Strangers to Love
</td>
<td>
You Know the Rules
</td>
<td>
Rick Barry
</td>
</tr>
<tr>
<td>
<sl-format-date date="2020-07-15T09:17:00-04:00"></sl-format-date>
</td>
<td>
No Strangers to Love
</td>
<td>
You Know the Rules
</td>
<td>
Rick Barry
</td>
</tr>
<tr>
<td>
<sl-format-date date="2020-07-15T09:17:00-04:00"></sl-format-date>
</td>
<td>
No Strangers to Love
</td>
<td>
You Know the Rules
</td>
<td>
Rick Barry
</td>
</tr>
<tr>
<td>
<sl-format-date date="2020-07-15T09:17:00-04:00"></sl-format-date>
</td>
<td>
No Strangers to Love
</td>
<td>
You Know the Rules
</td>
<td>
Rick Barry
</td>
</tr>
<tr>
<td>
<sl-format-date date="2020-07-15T09:17:00-04:00"></sl-format-date>
</td>
<td>
No Strangers to Love
</td>
<td>
You Know the Rules
</td>
<td>
Rick Barry
</td>
</tr>
<tr>
<td>
<sl-format-date date="2020-07-15T09:17:00-04:00"></sl-format-date>
</td>
<td>
No Strangers to Love
</td>
<td>
You Know the Rules
</td>
<td>
Rick Barry
</td>
</tr>
<tr>
<td>
<sl-format-date date="2020-07-15T09:17:00-04:00"></sl-format-date>
</td>
<td>
No Strangers to Love
</td>
<td>
You Know the Rules
</td>
<td>
Rick Barry
</td>
</tr>
<tr>
<td>
<sl-format-date date="2020-07-15T09:17:00-04:00"></sl-format-date>
</td>
<td>
No Strangers to Love
</td>
<td>
You Know the Rules
</td>
<td>
Rick Barry
</td>
</tr>
<tr>
<td>
<sl-format-date date="2020-07-15T09:17:00-04:00"></sl-format-date>
</td>
<td>
No Strangers to Love
</td>
<td>
You Know the Rules
</td>
<td>
Rick Barry
</td>
</tr>
<tr>
<td>
<sl-format-date date="2020-07-15T09:17:00-04:00"></sl-format-date>
</td>
<td>
No Strangers to Love
</td>
<td>
You Know the Rules
</td>
<td>
Rick Barry
</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
</sl-layout>

View File

@@ -0,0 +1,66 @@
{% include "layout-templates/reset.css.njk" %}
.grid {
font-size: 1.35rem;
text-align: center;
display: grid;
place-content: center;
padding: 1rem;
}
header {
background-color: var(--sl-color-blue-100);
}
aside {
min-width: 250px;
max-width: 250px;
}
main {
background-color: var(--sl-color-green-100);
height: 100%;
}
footer {
background-color: var(--sl-color-blue-200);
}
.banner {
background-color: var(--sl-color-amber-100);
}
.header {
background-color: var(--sl-color-blue-100);
}
.banner, .header {
min-width: 100%;
height: 100%;
}
[slot=header] {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
[slot=aside] {
height: 100%;
background-color: var(--sl-color-yellow-100);
}
[slot=menu] {
height: 100%;
background-color: var(--sl-color-pink-100);
}
[slot=main-header] {
background-color: var(--sl-color-red-200);
padding: 1rem;
}
[slot=main-footer] {
background-color: var(--sl-color-lime-200);
}

View File

@@ -0,0 +1,11 @@
<sl-layout main-id="main-content" class="sl-theme-light">
<header class="grid" slot="header">Header</header>
<aside class="grid" slot="menu">Menu</aside>
<main class="grid" id="main-content">
Main
</main>
<aside class="grid" slot="aside">Aside</aside>
<footer class="grid" slot="footer">Footer</footer>
</sl-layout>
{% include "layout-widget.njk" %}

View File

@@ -0,0 +1 @@
{% include "layout-templates/reset.css.njk" %}

View File

@@ -0,0 +1,2 @@
<sl-layout main-id="main-content" class="sl-theme-light">
</sl-layout>

View File

@@ -0,0 +1,37 @@
/* Reset */
*, *:before, *:after {
box-sizing: border-box;
}
html, body {
min-height: 100%;
}
body {
margin: 0;
}
[hidden] {
display: none !important;
}
p {
margin: 0;
}
h1,h2,h3,h4,h5,h6 {
margin: 0;
padding: 0;
font-weight: normal;
}
/* Gives us a nice fade-in to prevent layout jank */
:not(:defined) {
opacity: 0;
}
:defined {
opacity: 1;
transition: opacity 100ms ease-in-out;
}

View File

@@ -0,0 +1,92 @@
<!-- playground-hide -->
<style>
.layout-widget {
position: fixed;
z-index: 9999;
background-color: white;
bottom: 4rem;
left: 4rem;
}
.layout-widget:not(:defined) {
display: none;
}
</style>
<sl-dropdown id="js-layout-widget" class="layout-widget" stay-open-on-select>
<sl-button slot="trigger" caret>Dropdown</sl-button>
<sl-menu></sl-menu>
</sl-dropdown>
<script type="module">
const layoutWidget = document.querySelector("#js-layout-widget")
const docFrag = new DocumentFragment()
function makeMenuItem (type, slot) {
const menuItem = Object.assign(document.createElement("sl-menu-item"), {
type: "checkbox",
textContent: `${type} ${slot}`
})
menuItem.setAttribute("value", `${type}-${slot}`)
return menuItem
}
document.querySelectorAll("sl-layout > [slot]").forEach((el) => {
const slot = el.getAttribute("slot");
docFrag.append(makeMenuItem("toggle", slot), makeMenuItem("overflow", slot), document.createElement("sl-divider"))
})
docFrag.append(makeMenuItem("toggle", "main"), makeMenuItem("overflow", "main"))
layoutWidget.querySelector("sl-menu").append(docFrag)
function capitalize(string) {
return string.split(/\s+/).map((str) => str[0].toUppercase() + str.slice(1)).join(" ")
}
function handleSelect (e) {
const item = e.detail.item
const val = item.getAttribute("value")
if (val === "footer-0") {
}
const slot = val.split("-").slice(1).join("-")
let el
if (slot === "main") {
el = document.querySelector(`main`)
} else {
el = document.querySelector(`sl-layout > [slot='${slot}']`)
}
if (val.startsWith("overflow")) {
if (item.checked) {
el.textContent = "lorem ".repeat(1_000)
return
}
el.textContent = slot
return
}
if (val.startsWith("toggle")) {
if (item.checked) {
el.setAttribute("hidden", "")
return
}
el.removeAttribute("hidden")
return
}
}
layoutWidget.addEventListener("sl-select", handleSelect);
{% if in_playground %}
&lt;/script>
{% else %}
</script>
{% endif %}
<!-- playground-hide-end -->

View File

@@ -0,0 +1,126 @@
<style>
playground-ide {
height: 100vh;
height: 100dvh;
--playground-preview-width: 75%;
max-height: 100vh;
overflow: hidden;
}
playground-ide::part(preview) {
background-color: var(--sl-color-neutral-50);
min-height: 100vh;
max-height: 100vh;
}
playground-ide::part(editor) {
overflow: auto;
height: 100vh;
max-height: calc(100vh - var(--playground-preview-height, 44px));
max-height: calc(100dvh - var(--playground-preview-height, 44px));
}
body {
margin: 0;
/* Playgrounds have a weird like 1px overflow I can't track down. */
overflow: hidden;
}
</style>
<script type="module">
import 'https://esm.run/playground-elements';
import * as prettier from "https://unpkg.com/prettier@3.0.2/standalone.mjs";
import prettierPluginBabel from "https://unpkg.com/prettier@3.0.2/plugins/babel.mjs";
import prettierPluginEstree from "https://unpkg.com/prettier@3.0.2/plugins/estree.mjs";
import prettierPluginHtml from "https://unpkg.com/prettier@3.0.2/plugins/html.mjs";
import prettierPluginPostCss from "https://unpkg.com/prettier@3.0.2/plugins/postcss.mjs";
const types = ["html", "css", "js"]
const [html,css,js] = await Promise.allSettled(types.map(async (type) => {
let innerHTML = document.querySelector(`#playground-${type}`).innerHTML
let parser = type
let plugins = {
html: [prettierPluginHtml],
css: [prettierPluginPostCss],
js: [prettierPluginHtml, prettierPluginBabel, prettierPluginEstree]
}
innerHTML = innerHTML.replaceAll("&gt;", ">")
innerHTML = innerHTML.replaceAll("&lt;", "<")
if (type === "js") {
parser = "babel"
}
const str = await prettier.format(innerHTML, {
parser,
plugins: plugins[type],
}).catch((err) => {
console.error(err)
})
return str
}))
const files = {}
types.forEach((type) => {
const filename = `index.${type}`
files[filename] = {}
if (type === "html") {
files[filename].selected = true
files[filename].content = html.value
} else if (type === "css") {
files[filename].content = css.value
} else if (type === "js") {
files[filename].content = js.value
}
})
// So we can't target the iFrame with CSS parts, so this is a hack to get full height iframe in Chrome.
await customElements.whenDefined("playground-ide").then(() => {
setTimeout(() => {
const ide = document.querySelector("playground-ide")
const iframe = ide.shadowRoot.querySelector("playground-preview").shadowRoot.querySelector("iframe")
iframe.setAttribute("style", "position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; height: 100%; width: 100%;")
ide.config = {
files
}
}, 10)
})
</script>
<script id="playground-html" type="text/plain">
<!doctype html>
<head>
<link rel="stylesheet" href="./index.css">
</head>
<body>
{% include html_file %}
&lt;script type="module" src="./index.js"&gt;&lt;/script&gt;
</body>
</script>
<script id="playground-js" type="text/plain">
import { setBasePath } from "/dist/shoelace.js"
setBasePath("/dist")
</script>
<script id="playground-css" type="text/plain">
@import "/dist/themes/light.css";
@import "/dist/themes/dark.css";
{% include css_file %}
</script>
<playground-ide line-numbers resizable sandbox-base-url={{ rootUrl("/assets", true) }}>
</playground-ide>

View File

@@ -1,8 +1,13 @@
/**
* Turns headings into clickable, deep linkable anchors. The provided doc should be a document object provided by JSDOM.
* Turns tables into scrollable tables
* The same document will be returned with the appropriate DOM manipulations.
*/
module.exports = function (doc, options) {
// We don't want to run this on layouts.
if (doc.querySelector("[data-layout='layout-example.njk']")) {
return
}
const tables = [...doc.querySelectorAll('table')];
options = {

View File

@@ -0,0 +1,20 @@
<!doctype html><html>
<head></head>
<body>
<script type="module">
/**
* If you plan to update this file, update it via unpkg version and make sure to also update playground-service-worker.js
* https://unpkg.com/browse/playground-elements@0.18.1/playground-service-worker-proxy.html
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
(async()=>{try{parent.window.console.warn("Playground sandbox is executing with the same origin as its parent.","This is a security risk.","https://github.com/google/playground-elements#sandbox-security")}catch{}const{scope:e,port:t}=await new Promise((e=>{const t=a=>{1===a.data.type&&(window.removeEventListener("message",t),e(a.data))};window.addEventListener("message",t)})),a=await navigator.serviceWorker.register(new URL("playground-service-worker.js",import.meta.url).href,{scope:e}),n=()=>{var e,t;return null!==(t=null!==(e=a.installing)&&void 0!==e?e:a.waiting)&&void 0!==t?t:a.active};let r=null;const s=async(e=!1)=>{const a=n();if(!e&&a===r)return;if(r=a,null===a)return void console.error("No playground service worker found.");if(await(async e=>{if("activated"!==e.state)return new Promise((t=>{const a=new AbortController;e.addEventListener("statechange",(()=>{"activated"===e.state&&(t(),a.abort())}),{signal:a.signal})}))})(a),a!==r)return;const{port1:s,port2:o}=new MessageChannel,i={type:3,port:s};t.postMessage(i,[s]);const d={type:2,port:o};a.postMessage(d,[o])};s(),a.addEventListener("updatefound",(()=>{s()})),navigator.serviceWorker.addEventListener("message",(e=>{e.source===r&&5===e.data.type&&s(!0)})),window.addEventListener("message",(async e=>{if(6===e.data.type){const e=n();await a.update(),n()===e&&console.error("Playground service worker update failed.")}}))})();</script>
</body>
</html>

View File

@@ -0,0 +1,25 @@
!function(){
/**
* If you plan to update this file, update it via unpkg version and make sure to also update playground-service-worker-proxy.html
* https://unpkg.com/browse/playground-elements@0.18.1/playground-service-worker.js
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const e=Symbol("Comlink.proxy"),t=Symbol("Comlink.endpoint"),n=Symbol("Comlink.releaseProxy"),s=Symbol("Comlink.thrown"),a=e=>"object"==typeof e&&null!==e||"function"==typeof e,r=new Map([["proxy",{canHandle:t=>a(t)&&t[e],serialize(e){const{port1:t,port2:n}=new MessageChannel;return i(e,t),[n,[n]]},deserialize:e=>(e.start(),l(e,[],undefined))}],["throw",{canHandle:e=>a(e)&&s in e,serialize({value:e}){let t;return t=e instanceof Error?{isError:!0,value:{message:e.message,name:e.name,stack:e.stack}}:{isError:!1,value:e},[t,[]]},deserialize(e){if(e.isError)throw Object.assign(Error(e.value.message),e.value);throw e.value}}]]);function i(t,n=self){n.addEventListener("message",(function a(r){if(!r||!r.data)return;const{id:c,type:l,path:u}=Object.assign({path:[]},r.data),h=(r.data.argumentList||[]).map(f);let g;try{const n=u.slice(0,-1).reduce(((e,t)=>e[t]),t),s=u.reduce(((e,t)=>e[t]),t);switch(l){case"GET":g=s;break;case"SET":n[u.slice(-1)[0]]=f(r.data.value),g=!0;break;case"APPLY":g=s.apply(n,h);break;case"CONSTRUCT":g=function(t){return Object.assign(t,{[e]:!0})}(new s(...h));break;case"ENDPOINT":{const{port1:e,port2:n}=new MessageChannel;i(t,n),g=function(e,t){return p.set(e,t),e}(e,[e])}break;case"RELEASE":g=void 0;break;default:return}}catch(e){g={value:e,[s]:0}}Promise.resolve(g).catch((e=>({value:e,[s]:0}))).then((e=>{const[t,s]=d(e);n.postMessage(Object.assign(Object.assign({},t),{id:c}),s),"RELEASE"===l&&(n.removeEventListener("message",a),o(n))}))})),n.start&&n.start()}function o(e){(function(e){return"MessagePort"===e.constructor.name})(e)&&e.close()}function c(e){if(e)throw Error("Proxy has been released and is not useable")}function l(e,s=[],a=function(){}){let r=!1;const i=new Proxy(a,{get(t,a){if(c(r),a===n)return()=>h(e,{type:"RELEASE",path:s.map((e=>e.toString()))}).then((()=>{o(e),r=!0}));if("then"===a){if(0===s.length)return{then:()=>i};const t=h(e,{type:"GET",path:s.map((e=>e.toString()))}).then(f);return t.then.bind(t)}return l(e,[...s,a])},set(t,n,a){c(r);const[i,o]=d(a);return h(e,{type:"SET",path:[...s,n].map((e=>e.toString())),value:i},o).then(f)},apply(n,a,i){c(r);const o=s[s.length-1];if(o===t)return h(e,{type:"ENDPOINT"}).then(f);if("bind"===o)return l(e,s.slice(0,-1));const[p,d]=u(i);return h(e,{type:"APPLY",path:s.map((e=>e.toString())),argumentList:p},d).then(f)},construct(t,n){c(r);const[a,i]=u(n);return h(e,{type:"CONSTRUCT",path:s.map((e=>e.toString())),argumentList:a},i).then(f)}});return i}function u(e){const t=e.map(d);return[t.map((e=>e[0])),(n=t.map((e=>e[1])),Array.prototype.concat.apply([],n))];var n}const p=new WeakMap;function d(e){for(const[t,n]of r)if(n.canHandle(e)){const[s,a]=n.serialize(e);return[{type:"HANDLER",name:t,value:s},a]}return[{type:"RAW",value:e},p.get(e)||[]]}function f(e){switch(e.type){case"HANDLER":return r.get(e.name).deserialize(e.value);case"RAW":return e.value}}function h(e,t,n){return new Promise((s=>{const a=[,,,,].fill(0).map((()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16))).join("-");e.addEventListener("message",(function t(n){n.data&&n.data.id&&n.data.id===a&&(e.removeEventListener("message",t),s(n.data))})),e.start&&e.start(),e.postMessage(Object.assign({id:a},t),n)}))}
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
class g{constructor(){this.settled=!1,this.promise=new Promise(((e,t)=>{this._resolve=e,this._reject=t}))}resolve(e){this.settled=!0,this._resolve(e)}reject(e){this.settled=!0,this._reject(e)}}
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/const m=new Map,v={setFileAPI(e,t){let n=m.get(t);(void 0===n||n.settled)&&(n=new g,m.set(t,n)),n.resolve(e)}};
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/self.addEventListener("fetch",(e=>{const t=e.request.url;if(t.startsWith(self.registration.scope)){const{filePath:n,sessionId:s}=(e=>{const t=self.registration.scope,n=e.substring(t.length).split("?")[0],s=n.indexOf("/");let a,r;return-1===s?console.warn("Invalid sample file URL: "+e):(a=n.slice(0,s),r=n.slice(s+1)),{sessionId:a,filePath:r}})(t);void 0!==s&&e.respondWith((async(e,t,n)=>{const s=await(async e=>{let t=m.get(e);if(void 0!==t)return t.promise;const n=await(async e=>{for(const t of await self.clients.matchAll({includeUncontrolled:!0})){const n=new URL(t.url).hash;if(new URLSearchParams(n.slice(1)).get("playground-session-id")===e)return t}})(e);if(void 0===n)return;return t=new g,m.set(e,t),n.postMessage({type:5}),t.promise})(n);if(void 0===s)return new Response("Playground project not available",{status:503});const a=await s.getFile(t);if("status"in a){const{body:e,status:t}=a;return new Response(e,{status:t})}const{content:r,contentType:i}=a,o=new Headers;return o.set("Origin-Agent-Cluster","?1"),i&&o.set("Content-Type",i),new Response(r,{headers:o})})(0,n,s))}})),self.addEventListener("activate",(e=>{e.waitUntil(self.clients.claim())})),self.addEventListener("install",(()=>{self.skipWaiting()})),self.addEventListener("message",(e=>{if(2===e.data.type){const t={type:4,version:"1dae6563"};e.data.port.postMessage(t),i(v,e.data.port)}}))}();

View File

@@ -15,7 +15,7 @@
}
function isSidebarVisible() {
return getSidebar().getBoundingClientRect().x >= 0;
return getSidebar()?.getBoundingClientRect().x >= 0;
}
function toggleSidebar(force) {
@@ -24,7 +24,11 @@
}
function updateInert() {
getSidebar().inert = !isSidebarVisible();
const sidebar = getSidebar()
if (sidebar) {
sidebar.inert = !isSidebarVisible();
}
}
// Toggle the menu

View File

@@ -28,7 +28,16 @@ module.exports = function (eleventyConfig) {
//
// Global data
//
eleventyConfig.addGlobalData('baseUrl', 'https://shoelace.style/'); // the production URL
let baseUrl = 'https://shoelace.style/'
if (process.env.VERCEL_URL) {
baseUrl = process.env.VERCEL_URL
if (!process.env.VERCEL_URL.match(/^https?/)) {
baseUrl = 'https://' + baseUrl
}
}
eleventyConfig.addGlobalData('baseUrl', baseUrl); // the production URL
eleventyConfig.addGlobalData('layout', 'default'); // make 'default' the default layout
eleventyConfig.addGlobalData('toc', true); // enable the table of contents
eleventyConfig.addGlobalData('meta', {
@@ -182,21 +191,23 @@ module.exports = function (eleventyConfig) {
}).window.document;
const content = doc.querySelector('#content');
// Get title and headings
const title = (doc.querySelector('title')?.textContent || path.basename(result.outputPath)).trim();
const headings = [...content.querySelectorAll('h1, h2, h3, h4')]
.map(heading => heading.textContent)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
if (content) {
// Get title and headings
const title = (doc.querySelector('title')?.textContent || path.basename(result.outputPath)).trim();
const headings = [...content.querySelectorAll('h1, h2, h3, h4')]
.map(heading => heading.textContent)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
// Remove code blocks and whitespace from content
[...content.querySelectorAll('code[class|=language]')].forEach(code => code.remove());
const textContent = content.textContent.replace(/\s+/g, ' ').trim();
// Remove code blocks and whitespace from content
[...content.querySelectorAll('code[class|=language]')].forEach(code => code.remove());
const textContent = content.textContent.replace(/\s+/g, ' ').trim();
// Update the index and map
this.add({ id: index, t: title, h: headings, c: textContent });
map[index] = { title, url };
// Update the index and map
this.add({ id: index, t: title, h: headings, c: textContent });
map[index] = { title, url };
}
});
});

View File

@@ -0,0 +1,22 @@
---
meta:
title: Layout
description:
layout: component
---
```html preview
<sl-layout></sl-layout>
```
## Examples
### First Example
TODO
### Second Example
TODO
[component-metadata:sl-layout]

View File

@@ -0,0 +1,8 @@
---
layout: layout-example.njk
---
{% set html_file = "layout-templates/advanced-documentation.html.njk" %}
{% set css_file = "layout-templates/advanced-documentation.css.njk" %}
{% include "playground.njk" %}

View File

@@ -0,0 +1,10 @@
---
layout: layout-example.njk
---
<style>
{% include "layout-templates/advanced-documentation.css.njk" %}
</style>
{% include "layout-templates/advanced-documentation.html.njk" %}

View File

@@ -0,0 +1,8 @@
---
layout: layout-example.njk
---
{% set html_file = "layout-templates/app.html.njk" %}
{% set css_file = "layout-templates/app.css.njk" %}
{% include "playground.njk" %}

View File

@@ -0,0 +1,9 @@
---
layout: layout-example.njk
---
<style>
{% include "layout-templates/app.css.njk" %}
</style>
{% include "layout-templates/app.html.njk" %}

View File

@@ -0,0 +1,10 @@
---
layout: layout-example.njk
---
{% set html_file = "layout-templates/documentation.html.njk" %}
{% set css_file = "layout-templates/documentation.css.njk" %}
{% set in_playground = true %}
{% include "playground.njk" %}

View File

@@ -0,0 +1,10 @@
---
layout: layout-example.njk
---
<style>
{% include "layout-templates/documentation.css.njk" %}
</style>
{% include "layout-templates/documentation.html.njk" %}

View File

@@ -0,0 +1,8 @@
---
layout: layout-example.njk
---
{% set html_file = "layout-templates/hero.html.njk" %}
{% set css_file = "layout-templates/hero.css.njk" %}
{% include "playground.njk" %}

View File

@@ -0,0 +1,9 @@
---
layout: layout-example.njk
---
<style>
{% include "layout-templates/documentation.css.njk" %}
</style>
{% include "layout-templates/documentation.html.njk" %}

View File

@@ -0,0 +1,11 @@
- [Documentation Layout](/layouts/documentation/index.html)
- [Documentation Playground](/layouts/documentation-playground/index.html)
- [Advanced Documentation Layout](/layouts/advanced-documentation/index.html)
- [Advanced Documentation Playground](/layouts/advanced-documentation-playground/index.html)
- [App Layout](/layouts/app/index.html)
- [App Playground](/layouts/app-playground/index.html)
- [Hero Layout](/layouts/hero/index.html)
- [Hero Playground](/layouts/hero-playground/index.html)

View File

@@ -249,7 +249,17 @@ if (serve) {
const bs = browserSync.create();
const port = await getPort({ port: portNumbers(4000, 4999) });
/**
* @type {import("browser-sync").Options}
*/
const browserSyncConfig = {
snippetOptions: {
// Ignore all HTML files within the templates folder
ignorePaths: [
"/assets/playground-service-worker-proxy.html",
"/assets/playground-service-worker.js",
],
},
startPath: '/',
port,
logLevel: 'silent',

View File

@@ -0,0 +1,206 @@
import SlVisuallyHidden from "../visually-hidden/visually-hidden.component.js"
import { property, query } from 'lit/decorators.js';
import { html } from 'lit';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './layout.styles.js';
import type { CSSResultGroup, PropertyValueMap } from 'lit';
import { when } from "lit/directives/when.js";
import SlDrawer from "../drawer/drawer.component.js";
import SlButton from "../button/button.component.js";
/**
* @summary Short summary of the component's intended use.
* @documentation https://shoelace.style/components/layout
* @status experimental
* @since 2.0
*
* @csspart base - The component's base wrapper.
* @cssproperty --example - An example CSS custom property.
*/
export default class SlLayout extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static dependencies = {
'sl-button': SlButton,
'sl-visually-hidden': SlVisuallyHidden,
'sl-drawer': SlDrawer
}
@property({ attribute: "main-id" }) mainId: string = ""
@property({ attribute: "view", reflect: true }) view: "mobile" | "desktop" = "mobile"
@property({ attribute: "hide-nav-button", reflect: true, type: Boolean }) hideNavButton = false
@property({ attribute: "mobile-breakpoint" }) mobileBreakpoint = 768
@property({ attribute: "navigation-placement" }) navigationPlacement: "start" | "end" = "start"
layoutResizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.contentBoxSize) {
const contentBoxSize = entry.borderBoxSize[0];
const layoutWidth = contentBoxSize.inlineSize
const oldView = this.view
if (layoutWidth >= this.mobileBreakpoint) {
this.view = "desktop"
} else {
this.view = "mobile"
}
this.requestUpdate("view", oldView)
this.style.setProperty(`--layout-width`, `${layoutWidth}px`)
}
}
});
protected update(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
if (changedProperties.has("view")) {
this.hideNavigation()
}
super.update(changedProperties)
}
headerResizeObserver = this.slotResizeObserver("header");
subHeaderResizeObserver = this.slotResizeObserver("sub-header");
bannerResizeObserver = this.slotResizeObserver("banner");
footerResizeObserver = this.slotResizeObserver("footer");
slotResizeObserver (slot: string) {
return new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.contentBoxSize) {
const contentBoxSize = entry.borderBoxSize[0];
this.style.setProperty(`--${slot}-height`, `${contentBoxSize.blockSize}px`)
}
}
})
}
@query("[part~='header']") header: HTMLElement
@query("[part~='sub-header']") subHeader: HTMLElement
@query("[part~='footer']") footer: HTMLElement
@query("[part~='banner']") banner: HTMLElement
@query("[part~='navigation-drawer']") navigationDrawer: SlDrawer
connectedCallback () {
super.connectedCallback()
this.layoutResizeObserver.observe(this)
setTimeout(() => {
this.headerResizeObserver.observe(this.header)
this.subHeaderResizeObserver.observe(this.subHeader)
this.bannerResizeObserver.observe(this.banner)
this.footerResizeObserver.observe(this.footer)
})
}
disconnectedCallback () {
super.disconnectedCallback()
this.layoutResizeObserver.unobserve(this)
this.headerResizeObserver.unobserve(this.header)
this.subHeaderResizeObserver.unobserve(this.subHeader)
this.footerResizeObserver.unobserve(this.footer)
this.bannerResizeObserver.unobserve(this.banner)
}
showNavigation () {
this.navigationDrawer?.show()
}
hideNavigation () {
this.navigationDrawer?.hide()
}
toggleNavigation () {
if (this.navigationDrawer) {
this.navigationDrawer.open = !this.navigationDrawer.open
}
}
render () {
return html`
<sl-visually-hidden class="skip-links" part="skip-links">
<slot name="skip-links">
${when(this.mainId, () => html`
<sl-button variant="primary" href=${`#${this.mainId}`} part="skip-link" class="skip-link">
Skip to main
</sl-button>
`)}
</slot>
</sl-visually-hidden>
<div class="base" part="base">
<div class="banner" part="banner">
<slot name="banner"></slot>
</div>
<div class="header" part="header">
<slot name="nav-button">
<sl-icon-button
name="list"
variant="text"
size="large"
part="nav-button"
@click=${this.showNavigation}
part="icon-button"
>
</sl-icon-button>
</slot>
<slot name="header"></slot>
</div>
<div class="sub-header" part="sub-header">
<slot name="sub-header"></slot>
</div>
<div class="body" part="body">
<div class="menu" part="menu">
<slot name="menu">
<!--
We generally expect there to be a <nav> here so we wrap it. If a user wants to override it, they
can provide a <div slot="menu"> to circumvent our prebuilt <nav>
-->
<nav name="navigation" class="navigation" part="navigation">
<slot name=${this.view === "desktop" ? "navigation-header" : "___"}></slot>
<slot name=${this.view === "desktop" ? "navigation" : "____"}></slot>
<slot name=${this.view === "desktop" ? "navigation-footer" : "___"}></slot>
</nav>
</slot>
</div>
<div class="main" part="main">
<div class="main-header" part="main-header">
<slot name="main-header"></slot>
</div>
<div class="main-content" part="main-content"><slot></slot></div>
<div class="main-footer" part="main-footer">
<slot name="main-footer"></slot>
</div>
</div>
<div class="aside" part="aside">
<slot name="aside"></slot>
</div>
</div>
<div class="footer" part="footer">
<slot name="footer"></slot>
</div>
</div>
<sl-drawer placement=${this.navigationPlacement} part="navigation-drawer" class="navigation-drawer">
<slot slot="label" name=${this.view === "mobile" ? "navigation-header" : "___"}></slot>
<slot name=${this.view === "mobile" ? "navigation" : "____"}></slot>
<slot slot="footer" name=${this.view === "mobile" ? "navigation-footer" : "___"}></slot>
</sl-drawer>
<div part="dialog" class="dialog">
<slot name="dialog"></slot>
</div>
`
}
}

View File

@@ -0,0 +1,165 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
display: block;
box-sizing: border-box;
height: 100%;
--menu-width: auto;
--main-width: 1fr;
--aside-width: auto;
--banner-height: 0px;
--header-height: 0px;
--sub-header-height: 0px;
}
[part~="base"] {
min-height: 100%;
display: grid;
grid-template-rows: repeat(3, minmax(0, auto)) minmax(0, 1fr) minmax(0, auto);
grid-template-columns: 100%;
width: 100%;
grid-template-areas:
"banner"
"header"
"sub-header"
"body"
"footer";
}
/* Grid areas */
[part~="banner"] {
grid-area: banner;
}
[part~="header"] {
grid-area: header;
}
[part~="sub-header"] {
grid-area: sub-header;
}
[part~="menu"] {
grid-area: menu;
}
[part~="body"] {
grid-area: body;
}
[part~="main"] {
grid-area: main;
}
[part~="aside"] {
grid-area: aside;
}
[part~="footer"] {
grid-area: footer;
}
/* Z-indexes */
[part~="banner"],
[part~="header"],
[part~="sub-header"] {
position: sticky;
z-index: 5;
}
[part~="banner"] {
top: 0px;
}
[part~="header"] {
top: var(--banner-height);
display: grid;
grid-template-columns: minmax(0, auto) minmax(0, 1fr);
align-items: center;
}
:host([hide-nav-button]) [part~="nav-button"] {
display: none;
}
:host([hide-nav-button]) [part~="header"] {
grid-template-columns: minmax(0, 1fr);
}
[part~="sub-header"] {
top: calc(var(--header-height) + var(--banner-height));
}
[part~="body"] {
display: grid;
height: 100%;
align-items: flex-start;
grid-template-columns: minmax(0, var(--menu-width)) minmax(0, var(--main-width)) minmax(0, var(--aside-width));
grid-template-rows: minmax(0, 1fr);
grid-template-areas:
"menu main aside";
}
[part~="main"] {
display: grid;
min-height: 100%;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, auto) minmax(0, 1fr) minmax(0, auto);
grid-template-areas:
"main-header"
"main-content"
"main-footer";
}
[part~="main-header"] {
grid-area: main-header;
}
[part~="main-content"] {
grid-area: main-content;
}
[part~="main-footer"] {
grid-area: main-footer;
}
.skip-links {
position: absolute;
z-index: 6;
padding: 0.25rem;
/* This looks silly, but without this our skip links get flagged by a11y checkers. */
background-color: var(--sl-color-neutral-0);
}
[part~="menu"],
[part~="aside"] {
position: sticky;
top: calc(var(--banner-height) + var(--header-height) + var(--sub-header-height));
z-index: 4;
height: calc(100dvh - var(--header-height) - var(--banner-height) - var(--sub-header-height));
max-height: calc(100dvh - var(--header-height) - var(--banner-height) - var(--sub-header-height));
overflow: auto;
}
[part~="menu"] {
height: 100%;
}
[part~="navigation"] {
height: 100%;
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, auto) minmax(0, 1fr) minmax(0, auto);
}
[part~="nav-button"] {
font-size: 1.5rem;
}
`;

View File

@@ -0,0 +1,10 @@
import '../../../dist/shoelace.js';
import { expect, fixture, html } from '@open-wc/testing';
describe('<sl-layout>', () => {
it('should render a component', async () => {
const el = await fixture(html` <sl-layout></sl-layout> `);
expect(el).to.exist;
});
});

View File

@@ -0,0 +1,10 @@
import SlLayout from "./layout.component.js"
export * from"./layout.component.js"
export default SlLayout
SlLayout.define("sl-layout")
declare global {
interface HTMLElementTagNameMap {
'sl-layout': SlLayout;
}
}

View File

@@ -57,6 +57,7 @@ export { default as SlTooltip } from './components/tooltip/tooltip.js';
export { default as SlTree } from './components/tree/tree.js';
export { default as SlTreeItem } from './components/tree-item/tree-item.js';
export { default as SlVisuallyHidden } from './components/visually-hidden/visually-hidden.js';
export { default as SlLayout } from './components/layout/layout.js';
/* plop:component */
// Utilities