Compare commits

...

38 Commits

Author SHA1 Message Date
Cory LaViska
4e9223953e remove unused controller 2024-03-21 15:10:24 -04:00
Cory LaViska
cf9044d826 remove unused controller 2024-03-21 15:10:16 -04:00
Cory LaViska
252d710175 remove slot detection from card, dialog, drawer 2024-03-21 14:47:29 -04:00
Cory LaViska
58d14ed1d2 remove slot detection from buttons 2024-03-21 13:55:40 -04:00
Cory LaViska
879e912a5c remove slot detection from breadcrumb items 2024-03-21 13:55:31 -04:00
Cory LaViska
c69e98cd7f remove slot detection from alert 2024-03-21 13:55:22 -04:00
Konnor Rogers
8e5e039af8 Rename Layout to Page (#65)
* working on layouts

* fix buildS

* first layout converted to sportawesome

* working on playgrounds

* continued work on layouts

* working on astro layout

* light-pen 3

* rename layout to page

* prettier

* add vercel.json

* add vercel.json

* add vercel.json

* add vercel.json

* add vercel.json

* add sandbox-settings

* add sandbox-settings

* add sandbox-settings

* maybe now?

* remove sandbox-settings

* remove vercel.json

* rename to pagE

* fix grid for navigation

* prettier

* fix stuff

* fix search

* prettier
2024-03-14 12:47:28 -04:00
Cory LaViska
2e725a2d93 prettier 2024-03-12 16:02:31 -04:00
Cory LaViska
c139865635 fix padding 2024-03-12 15:59:22 -04:00
Cory LaViska
f59c544fbe update content 2024-03-12 15:06:28 -04:00
Cory LaViska
28bdcae2c6 fix layout to be more font awesomey 2024-03-12 15:00:58 -04:00
konnorrogers
b1530d0773 Fix dev server infinite reload 2024-03-12 14:16:11 -04:00
Konnor Rogers
5feee64425 Font Awesome theme 👀 (#64)
* font awesome site theme

* separate font awesome.css

* prettier

* remove image borders

* fix search and prism

* select first item

* 30ms'

* fix double render

* fix for turbo loading

* fix preview

* prettier

* fix pagefind

* prettier
2024-03-12 13:45:33 -04:00
Konnor Rogers
9647259b5f fix the test suite (#63)
* fix the test suite

* prettier

* fix the test suite

* prettier
2024-03-11 12:23:25 -04:00
Cory LaViska
84e276ae10 Merge pull request #32 from shoelace-style/prepare
Backport 1868
2024-03-06 07:56:34 -05:00
Cory LaViska
4718c3d815 move to prepare 2024-03-06 07:56:24 -05:00
Cory LaViska
aa1bfb0885 Merge branch 'next' into prepare 2024-03-06 07:54:52 -05:00
Cory LaViska
acf2055768 Merge pull request #31 from shoelace-style/efficient-style-imports
Backport 1861
2024-03-06 07:54:16 -05:00
Cory LaViska
ef4d2fac40 Merge branch 'next' into efficient-style-imports 2024-03-06 07:53:52 -05:00
Cory LaViska
b53c1d940a backport 1880 2024-02-21 13:33:00 -05:00
Cory LaViska
edd62490f8 backport 1839 2024-02-20 15:01:35 -05:00
Cory LaViska
96a381d3a3 backport 1879 2024-02-20 14:18:48 -05:00
Cory LaViska
a2a72de2cf backport 1788 2024-02-20 14:13:23 -05:00
Cory LaViska
9a51e69320 backport 1878 2024-02-20 14:00:49 -05:00
Cory LaViska
07be57847d backport 1874 2024-02-20 13:48:57 -05:00
Cory LaViska
0095ca5fe7 backport 1877 2024-02-20 12:53:38 -05:00
Cory LaViska
a1b1d594aa backport 1868 2024-02-12 12:50:55 -05:00
Cory LaViska
531a2f1634 backport 1861 2024-02-09 11:03:15 -05:00
Cory LaViska
cb1c423aea backport 1862 2024-02-09 10:05:14 -05:00
Cory LaViska
93306c99ce backport 1800, 1860 2024-02-09 09:58:13 -05:00
Cory LaViska
5f8c69064c backport 1852 2024-02-09 09:46:46 -05:00
Cory LaViska
f51a09ddf0 backport 1840 2024-02-08 12:46:08 -05:00
Konnor Rogers
92533c0297 Convert to Starlight (#22)
* first pass at starlight

* converting to starlight

* working on converting to starlight

* working on data

* watch custom-elements.json

* turn on pagefind

* add component meta data

* fix renderings / overrides.

* fix mdx logo

* continue starlight work

* building site

* get global styles + reloads working

* themer fixes

* adding additional headings

* working on dynamic content

* have TableOfContents.astro push to TOC

* working on code stuff

* remove code preview

* deploy

* add patch package

* patch in build

* patch in build

* remove {% raw %} calls

* convert to starlight...complete

* prettier

* update lockfile

* merge main

* fix index.mdx

* prettier'

* fix small things

* docs updates

* add dark mode shortcut

* prettier

* prettier

* prettier

* remove pagefind from public

* add twitteR

* prettier

* fix tests

* prettier
2024-02-05 11:02:14 -05:00
Cory LaViska
94558e6ea5 Merge pull request #19 from shoelace-style/backport-menu-item-loading
backport `<sl-menu-item loading>`
2024-01-31 16:13:59 -05:00
lindsaym-fa
bab673fbdc optional class for alternating table row colors 2024-01-31 15:27:39 -05:00
lindsaym-fa
7b20f9c87a deprecate global toggle-size properties 2024-01-30 16:06:09 -05:00
lindsaym-fa
ddbd91ad89 update radio with custom properties, svg circle 2024-01-30 15:45:27 -05:00
Cory LaViska
349aa45d2b backport 2023-12-06 17:18:12 -05:00
341 changed files with 19501 additions and 23737 deletions

View File

@@ -35,7 +35,7 @@ This Code of Conduct applies within all project spaces, and it also applies when
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at cory@abeautifulsite.net. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@fontawesome.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
github: [claviska]

2
.github/SECURITY.md vendored
View File

@@ -2,6 +2,6 @@
We take security issues in Web Awesome very seriously and appreciate your efforts to disclose your findings responsibly.
To report a security issue, email [cory@fontawesome.com](mailto:cory@abeautifulsite.net) and include "WEB AWESOME SECURITY" in the subject line.
To report a security issue, email [support@fontawesome.com](mailto:support@fontawesome.com) and include "WEB AWESOME SECURITY" in the subject line.
We'll respond as soon as possible and keep you updated throughout the process.

2
.gitignore vendored
View File

@@ -5,6 +5,8 @@ package.json
package-lock.json
dist
docs/assets/images/sprite.svg
docs/public/pagefind
node_modules
src/react
cdn
.astro

View File

@@ -1,4 +1,5 @@
*.hbs
*.mdx
.cache
.github
cspell.json

View File

@@ -1,7 +1,5 @@
# Web Awesome
A forward-thinking library of web components.
- Works with all frameworks 🧩
- Works with CDNs 🚛
- Fully customizable with CSS 🎨

View File

@@ -21,7 +21,6 @@
"cdndir",
"chatbubble",
"checkmark",
"claviska",
"Clippy",
"codebases",
"codepen",
@@ -86,7 +85,6 @@
"Kool",
"labelledby",
"Laravel",
"LaViska",
"linkify",
"listbox",
"listitem",
@@ -173,6 +171,7 @@
"valpha",
"valuenow",
"valuetext",
"viewports",
"Vuejs",
"WCAG",
"webawesome",

4
docs/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
docs/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@@ -1,349 +0,0 @@
{% extends "default.njk" %}
{# Find the component based on the `tag` front matter #}
{% set component = getComponent('wa-' + page.fileSlug) %}
{% block content %}
{# Determine the badge variant #}
{% if component.status == 'stable' %}
{% set badgeVariant = 'brand' %}
{% elseif component.status == 'experimental' %}
{% set badgeVariant = 'warning' %}
{% elseif component.status == 'planned' %}
{% set badgeVariant = 'neutral' %}
{% elseif component.status == 'deprecated' %}
{% set badgeVariant = 'danger' %}
{% else %}
{% set badgeVariant = 'neutral' %}
{% endif %}
{# Header #}
<header class="component-header">
<h1>{{ component.name | classNameToComponentName }}</h1>
<div class="component-header__tag">
<code>&lt;{{ component.tagName }}&gt; | {{ component.name }}</code>
</div>
<div class="component-header__info">
<wa-badge variant="neutral" pill>
Since {{component.since or '?' }}
</wa-badge>
<wa-badge variant="{{ badgeVariant }}" pill style="text-transform: capitalize;">
{{ component.status }}
</wa-badge>
</div>
</header>
<p class="component-summary">
{% if component.summary %}
{{ component.summary | markdownInline | safe }}
{% endif %}
</p>
{# Markdown content #}
{{ content | safe }}
{# Importing #}
<h2>Importing</h2>
<p>
If you're using the autoloader or the traditional loader, you can ignore this section. Otherwise, feel free to use
any of the following snippets to <a href="/getting-started/installation#cherry-picking">cherry pick</a> this component.
</p>
<wa-tab-group>
<wa-tab slot="nav" panel="script">Script</wa-tab>
<wa-tab slot="nav" panel="import">Import</wa-tab>
<wa-tab slot="nav" panel="bundler">Bundler</wa-tab>
<wa-tab slot="nav" panel="react">React</wa-tab>
<wa-tab-panel name="script">
<p>
To import this component from <a href="https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace">the CDN</a>
using a script tag:
</p>
<pre><code class="language-html">&lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{{ meta.version }}/{{ meta.cdndir }}/{{ component.path }}&quot;&gt;&lt;/script&gt;</code></pre>
</wa-tab-panel>
<wa-tab-panel name="import">
<p>
To import this component from <a href="https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace">the CDN</a>
using a JavaScript import:
</p>
<pre><code class="language-js">import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{{ meta.version }}/{{ meta.cdndir }}/{{ component.path }}';</code></pre>
</wa-tab-panel>
<wa-tab-panel name="bundler">
<p>
To import this component using <a href="{{ rootUrl('/getting-started/installation#bundling') }}">a bundler</a>:
</p>
<pre><code class="language-js">import '@shoelace-style/shoelace/{{ meta.npmdir }}/{{ component.path }}';</code></pre>
</wa-tab-panel>
<wa-tab-panel name="react">
<p>
To import this component as a <a href="/frameworks/react">React component</a>:
</p>
<pre><code class="language-js">import {{ component.name }} from '@shoelace-style/shoelace/{{ meta.npmdir }}/react/{{ component.tagNameWithoutPrefix }}';</code></pre>
</wa-tab-panel>
</wa-tab-group>
{# Slots #}
{% if component.slots.length %}
<h2>Slots</h2>
<table>
<thead>
<tr>
<th class="table-name">Name</th>
<th class="table-description">Description</th>
</tr>
</thead>
<tbody>
{% for slot in component.slots %}
<tr>
<td class="nowrap">
{% if slot.name %}
<code>{{ slot.name }}</code>
{% else %}
(default)
{% endif %}
</td>
<td>{{ slot.description | markdownInline | safe }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#slots') }}">using slots</a>.</em></p>
{% endif %}
{# Properties #}
{% if component.properties.length %}
<h2>Properties</h2>
<table>
<thead>
<tr>
<th class="table-name">Name</th>
<th class="table-description">Description</th>
<th class="table-reflects">Reflects</th>
<th class="table-type">Type</th>
<th class="table-default">Default</th>
</tr>
</thead>
<tbody>
{% for prop in component.properties %}
<tr>
<td>
<code class="nowrap">{{ prop.name }}</code>
{% if prop.attribute | length > 0 %}
{% if prop.attribute != prop.name %}
<br>
<wa-tooltip content="This attribute is different from its property">
<small>
<code class="nowrap">
{{ prop.attribute }}
</code>
</small>
</wa-tooltip>
{% endif %}
{% endif %}
</td>
<td>
{{ prop.description | markdownInline | safe }}
</td>
<td style="text-align: center;">
{% if prop.reflects %}
<wa-icon label="yes" name="check" variant="solid"></wa-icon>
{% endif %}
</td>
<td>
{% if prop.type.text %}
<code>{{ prop.type.text | trimPipes | markdownInline | safe }}</code>
{% else %}
-
{% endif %}
</td>
<td>
{% if prop.default %}
<code>{{ prop.default | markdownInline | safe }}</code>
{% else %}
-
{% endif %}
</td>
</tr>
{% endfor %}
<tr>
<td class="nowrap"><code>updateComplete</code></td>
<td>
A read-only promise that resolves when the component has
<a href="/getting-started/usage?#component-rendering-and-updating">finished updating</a>.
</td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#attributes-and-properties') }}">attributes and properties</a>.</em></p>
{% endif %}
{# Events #}
{% if component.events.length %}
<h2>Events</h2>
<table>
<thead>
<tr>
<th class="table-name" data-flavor="html">Name</th>
<th class="table-name" data-flavor="react">React Event</th>
<th class="table-description">Description</th>
<th class="table-event-detail">Event Detail</th>
</tr>
</thead>
<tbody>
{% for event in component.events %}
<tr>
<td data-flavor="html"><code class="nowrap">{{ event.name }}</code></td>
<td data-flavor="react"><code class="nowrap">{{ event.reactName }}</code></td>
<td>{{ event.description | markdownInline | safe }}</td>
<td>
{% if event.type.text %}
<code>{{ event.type.text | trimPipes }}</code>
{% else %}
-
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#events') }}">events</a>.</em></p>
{% endif %}
{# Methods #}
{% if component.methods.length %}
<h2>Methods</h2>
<table>
<thead>
<tr>
<th class="table-name">Name</th>
<th class="table-description">Description</th>
<th class="table-arguments">Arguments</th>
</tr>
</thead>
<tbody>
{% for method in component.methods %}
<tr>
<td class="nowrap"><code>{{ method.name }}()</code></td>
<td>{{ method.description | markdownInline | safe }}</td>
<td>
{% if method.parameters.length %}
<code>
{% for param in method.parameters %}
{{ param.name }}: {{ param.type.text | trimPipes }}{% if not loop.last %},{% endif %}
{% endfor %}
</code>
{% else %}
-
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#methods') }}">methods</a>.</em></p>
{% endif %}
{# Custom Properties #}
{% if component.cssProperties.length %}
<h2>Custom Properties</h2>
<table>
<thead>
<tr>
<th class="table-name">Name</th>
<th class="table-description">Description</th>
<th class="table-default">Default</th>
</tr>
</thead>
<tbody>
{% for cssProperty in component.cssProperties %}
<tr>
<td class="nowrap"><code>{{ cssProperty.name }}</code></td>
<td>{{ cssProperty.description | markdownInline | safe }}</td>
<td>{{ cssProperty.default }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#custom-properties') }}">customizing CSS custom properties</a>.</em></p>
{% endif %}
{# CSS Parts #}
{% if component.cssParts.length %}
<h2>Parts</h2>
<table>
<thead>
<tr>
<th class="table-name">Name</th>
<th class="table-description">Description</th>
</tr>
</thead>
<tbody>
{% for cssPart in component.cssParts %}
<tr>
<td class="nowrap"><code>{{ cssPart.name }}</code></td>
<td>{{ cssPart.description | markdownInline | safe }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing/#css-parts') }}">customizing CSS parts</a>.</em></p>
{% endif %}
{# Animations #}
{% if component.animations.length %}
<h2>Animations</h2>
<table>
<thead>
<tr>
<th class="table-name">Name</th>
<th class="table-description">Description</th>
</tr>
</thead>
<tbody>
{% for animation in component.animations %}
<tr>
<td class="nowrap"><code>{{ animation.name }}</code></td>
<td>{{ animation.description | markdownInline | safe }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing#animations') }}">customizing animations</a>.</em></p>
{% endif %}
{# Dependencies #}
{% if component.dependencies.length %}
<h2>Dependencies</h2>
<p>This component automatically imports the following dependencies.</p>
<ul>
{% for dependency in component.dependencies %}
<li><code>&lt;{{ dependency }}&gt;</code></li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@@ -1,129 +0,0 @@
<!DOCTYPE html>
<html
lang="en"
data-layout="{{ layout }}"
data-wa-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">
{# Stylesheets #}
<link rel="stylesheet" href="{{ assetUrl('styles/docs.css') }}" />
<link rel="stylesheet" href="{{ assetUrl('styles/code-previews.css') }}" />
<link rel="stylesheet" href="{{ assetUrl('styles/search.css') }}" />
{# Favicons #}
<link rel="icon" href="{{ assetUrl('images/favicon.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) }}" />
{# Web Awesome #}
<link rel="stylesheet" href="/dist/themes/applied.css" />
<link rel="stylesheet" href="/dist/themes/forms.css" />
<link id="theme-stylesheet" rel="stylesheet" href="/dist/themes/default.css" />
<script type="module" src="/dist/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('wa-theme-default-dark', theme === 'dark' || (theme === 'auto' && prefersDark));
})();
</script>
{# Web Fonts #}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Assistant:wght@200..800&family=Inter:wght@100..900&family=Lora:wght@400..700&family=Mulish:wght@200..1000&family=Noto+Sans+Display:wght@100..900&family=Noto+Sans+Mono:wght@100..900&family=Noto+Sans:wght@100..900&family=Noto+Serif:wght@100..900&family=Open+Sans:wght@300..800&family=Playfair+Display:wght@400..900&family=Playfair:opsz,wght@5..1200,300;5..1200,400;5..1200,500;5..1200,600&family=Quicksand:wght@300..700&family=Roboto+Flex:opsz,wght@8..144,300;8..144,400;8..144,500;8..144,600&family=Roboto+Mono:wght@300..700&family=Roboto+Serif:opsz,wght@8..144,300;8..144,400;8..144,500;8..144,600&family=Roboto+Slab:wght@300..700&display=swap" rel="stylesheet">
{# Turbo + Scroll positioning #}
<script src="{{ assetUrl('scripts/turbo.js') }}" type="module"></script>
<script src="{{ assetUrl('scripts/docs.js') }}" defer></script>
<script src="{{ assetUrl('scripts/code-previews.js') }}" defer></script>
<script src="{{ assetUrl('scripts/lunr.js') }}" defer></script>
<script src="{{ assetUrl('scripts/search.js') }}" defer></script>
</head>
<body>
<a id="skip-to-main" class="wa-visually-hidden" href="#main-content" data-smooth-link="false">
Skip to main content
</a>
{# Menu toggle #}
<button id="menu-toggle" type="button" aria-label="Menu">
<svg width="148" height="148" viewBox="0 0 148 148" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="18" fill="none" fill-rule="evenodd" stroke-linecap="round">
<path d="M9.5 125.5h129M9.5 74.5h129M9.5 23.5h129"></path>
</g>
</svg>
</button>
<aside id="sidebar" data-preserve-scroll>
<header>
<a href="/">
{% include 'logo.njk' %}
</a>
<div class="sidebar-version">
{{ meta.version }}
</div>
</header>
<div class="sidebar-buttons">
<wa-button size="small" class="repo-button repo-button--github" href="https://github.com/shoelace-style/shoelace" target="_blank">
<wa-icon slot="prefix" name="github" family="brands"></wa-icon> Code
</wa-button>
<wa-button size="small" class="repo-button repo-button--star" href="https://github.com/shoelace-style/shoelace/stargazers" target="_blank">
<wa-icon slot="prefix" name="star" variant="solid"></wa-icon> Star
</wa-button>
<wa-button size="small" class="repo-button repo-button--twitter" href="https://twitter.com/shoelace_style" target="_blank">
<wa-icon slot="prefix" name="twitter" family="brands"></wa-icon> Follow
</wa-button>
</div>
<button class="search-box" type="button" title="Press / to search" aria-label="Search" data-plugin="search">
<wa-icon name="search"></wa-icon>
<span>Search</span>
</button>
<nav>
{% include 'sidebar.njk' %}
</nav>
</aside>
{# Content #}
<main>
<a id="main-content"></a>
<article id="content" class="content{% if toc %} content--with-toc{% endif %}">
{% if toc %}
<div class="content__toc">
<ul>
<li class="top"><a href="#">{{ meta.title }}</a></li>
</ul>
</div>
{% endif %}
<div class="content__body">
{% block content %}
{{ content | safe }}
{% endblock %}
</div>
</article>
</main>
</body>
</html>

View File

@@ -1,85 +0,0 @@
<ul>
<li>
<h2>Experimental</h2>
<ul>
<li><a href="/experimental/themer">Themer</a></li>
<li><a href="/experimental/style-guide">Style Guide</a></li>
<li><a href="/experimental/form-validation">Form Validation Styles</a></li>
<li style="margin-top: .5rem;"><wa-switch id="theme-toggle">Dark mode</wa-switch></li>
<script type="module">
// Temporary dark toggle
const toggle = document.getElementById('theme-toggle');
toggle.checked = document.documentElement.classList.contains('wa-theme-default-dark');
toggle.addEventListener('wa-change', () => {
document.documentElement.classList.toggle('wa-theme-default-dark');
localStorage.setItem('theme', toggle.checked ? 'dark' : 'light');
});
</script>
<li><a href="/experimental/sandbox">Sandbox</a></li>
</ul>
</li>
<li>
<h2>Getting Started</h2>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/getting-started/installation">Installation</a></li>
<li><a href="/getting-started/usage">Usage</a></li>
<li><a href="/getting-started/themes">Themes</a></li>
<li><a href="/getting-started/customizing">Customizing</a></li>
<li><a href="/getting-started/form-controls">Form Controls</a></li>
<li><a href="/getting-started/localization">Localization</a></li>
</ul>
</li>
<li>
<h2>Frameworks</h2>
<ul>
<li><a href="/frameworks/react">React</a></li>
<li><a href="/frameworks/vue">Vue</a></li>
<li><a href="/frameworks/angular">Angular</a></li>
</ul>
</li>
<li>
<h2>Resources</h2>
<ul>
<li><a href="/resources/community">Community</a></li>
<li><a href="https://github.com/shoelace-style/shoelace/discussions">Help &amp; Support</a></li>
<li><a href="/resources/accessibility">Accessibility</a></li>
<li><a href="/resources/contributing">Contributing</a></li>
<li><a href="/resources/changelog">Changelog</a></li>
</ul>
</li>
<li>
<h2>Components</h2>
<ul>
{% for component in meta.components %}
<li>
<a href="/components/{{ component.tagName | removeWaPrefix }}">
{{ component.name | classNameToComponentName }}
</a>
</li>
{% endfor %}
</ul>
</li>
<li>
<h2>Design Tokens</h2>
<ul>
<li><a href="/tokens/typography">Typography</a></li>
<li><a href="/tokens/color">Color</a></li>
<li><a href="/tokens/spacing">Spacing</a></li>
<li><a href="/tokens/borders">Borders</a></li>
<li><a href="/tokens/shadows">Shadows</a></li>
<li><a href="/tokens/transition">Transition</a></li>
<li><a href="/tokens/z-index">Z-index</a></li>
<li><a href="/tokens/more">More Tokens</a></li>
</ul>
</li>
<li>
<h2>Tutorials</h2>
<ul>
<li><a href="/tutorials/integrating-with-laravel">Integrating with Laravel</a></li>
<li><a href="/tutorials/integrating-with-nextjs">Integrating with NextJS</a></li>
<li><a href="/tutorials/integrating-with-rails">Integrating with Rails</a></li>
</ul>
</li>
</ul>

View File

@@ -1,35 +0,0 @@
function normalizePathname(pathname) {
// Remove /index.html
if (pathname.endsWith('/index.html')) {
pathname = pathname.replace(/\/index\.html/, '');
}
// Remove trailing slashes
return pathname.replace(/\/$/, '');
}
/**
* Adds a class name to links that are currently active.
*/
module.exports = function (doc, options) {
options = {
className: 'active-link', // the class to add to active links
pathname: undefined, // the current pathname to compare
within: 'body', // element containing the target links
...options
};
const within = doc.querySelector(options.within);
if (!within) {
return doc;
}
within.querySelectorAll('a').forEach(link => {
if (normalizePathname(options.pathname) === normalizePathname(link.pathname)) {
link.classList.add(options.className);
}
});
return doc;
};

View File

@@ -1,64 +0,0 @@
const { createSlug } = require('./strings.cjs');
/**
* Turns headings into clickable, deep linkable anchors. The provided doc should be a document object provided by JSDOM.
* The same document will be returned with the appropriate DOM manipulations.
*/
module.exports = function (doc, options) {
options = {
levels: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], // the headings to convert
className: 'anchor-heading', // the class name to add
within: 'body', // the element containing the target headings
...options
};
const within = doc.querySelector(options.within);
if (!within) {
return doc;
}
within.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => {
const hasAnchor = heading.querySelector('a');
const anchor = doc.createElement('a');
let id = heading.textContent ?? '';
let suffix = 0;
// Skip heading levels we don't care about
if (!options.levels?.includes(heading.tagName.toLowerCase())) {
return;
}
// Convert dots to underscores
id = id.replace(/\./g, '_');
// Turn it into a slug
id = createSlug(id);
// Make sure it starts with a letter
if (!/^[a-z]/i.test(id)) {
id = `id_${id}`;
}
// Make sure the id is unique
const originalId = id;
while (doc.getElementById(id) !== null) {
id = `${originalId}-${++suffix}`;
}
if (hasAnchor || !id) return;
heading.setAttribute('id', id);
anchor.setAttribute('href', `#${encodeURIComponent(id)}`);
anchor.setAttribute('aria-label', `Direct link to "${heading.textContent}"`);
if (options.className) {
heading.classList.add(options.className);
}
// Append the anchor
heading.append(anchor);
});
return doc;
};

View File

@@ -1,138 +0,0 @@
let count = 1;
function escapeHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/**
* Turns code fields with the :preview suffix into interactive code previews.
*/
module.exports = function (doc, options) {
options = {
within: 'body', // the element containing the code fields to convert
...options
};
const within = doc.querySelector(options.within);
if (!within) {
return doc;
}
within.querySelectorAll('[class*=":preview"]').forEach(code => {
const pre = code.closest('pre');
if (!pre) {
return;
}
const adjacentPre = pre.nextElementSibling?.tagName.toLowerCase() === 'pre' ? pre.nextElementSibling : null;
const reactCode = adjacentPre?.querySelector('code[class$="react"]');
const sourceGroupId = `code-preview-source-group-${count}`;
const isExpanded = code.getAttribute('class').includes(':expanded');
const noCodePen = code.getAttribute('class').includes(':no-codepen');
count++;
const htmlButton = `
<button type="button"
title="Show HTML code"
class="code-preview__button code-preview__button--html"
>
HTML
</button>
`;
const reactButton = `
<button type="button" title="Show React code" class="code-preview__button code-preview__button--react">
React
</button>
`;
const codePenButton = `
<button type="button" class="code-preview__button code-preview__button--codepen" title="Edit on CodePen">
<svg
width="138"
height="26"
viewBox="0 0 138 26"
fill="none"
stroke="currentColor"
stroke-width="2.3"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M80 6h-9v14h9 M114 6h-9 v14h9 M111 13h-6 M77 13h-6 M122 20V6l11 14V6 M22 16.7L33 24l11-7.3V9.3L33 2L22 9.3V16.7z M44 16.7L33 9.3l-11 7.4 M22 9.3l11 7.3 l11-7.3 M33 2v7.3 M33 16.7V24 M88 14h6c2.2 0 4-1.8 4-4s-1.8-4-4-4h-6v14 M15 8c-1.3-1.3-3-2-5-2c-4 0-7 3-7 7s3 7 7 7 c2 0 3.7-0.8 5-2 M64 13c0 4-3 7-7 7h-5V6h5C61 6 64 9 64 13z" />
</svg>
</button>
`;
const codePreview = `
<div class="code-preview ${isExpanded ? 'code-preview--expanded' : ''}">
<div class="code-preview__preview">
${code.textContent}
<div class="code-preview__resizer">
<wa-icon name="grip-vertical" variant="solid"></wa-icon>
</div>
</div>
<div class="code-preview__source-group" id="${sourceGroupId}">
<div class="code-preview__source code-preview__source--html" ${reactCode ? 'data-flavor="html"' : ''}>
<pre><code class="language-html">${escapeHtml(code.textContent)}</code></pre>
</div>
${
reactCode
? `
<div class="code-preview__source code-preview__source--react" data-flavor="react">
<pre><code class="language-jsx">${escapeHtml(reactCode.textContent)}</code></pre>
</div>
`
: ''
}
</div>
<div class="code-preview__buttons">
<button
type="button"
class="code-preview__button code-preview__toggle"
aria-expanded="${isExpanded ? 'true' : 'false'}"
aria-controls="${sourceGroupId}"
>
Source
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
${reactCode ? ` ${htmlButton} ${reactButton} ` : ''}
${noCodePen ? '' : codePenButton}
</div>
</div>
`;
pre.insertAdjacentHTML('afterend', codePreview);
pre.remove();
if (adjacentPre) {
adjacentPre.remove();
}
});
// Wrap code preview scripts in anonymous functions so they don't run in the global scope
doc.querySelectorAll('.code-preview__preview script').forEach(script => {
if (script.type === 'module') {
// Modules are already scoped
script.textContent = script.innerHTML;
} else {
// Wrap non-modules in an anonymous function so they don't run in the global scope
script.textContent = `(() => { ${script.innerHTML} })();`;
}
});
return doc;
};

View File

@@ -1,23 +0,0 @@
let codeBlockId = 0;
/**
* Adds copy code buttons to code fields. The provided doc should be a document object provided by JSDOM. The same
* document will be returned with the appropriate DOM manipulations.
*/
module.exports = function (doc) {
doc.querySelectorAll('pre > code').forEach(code => {
const pre = code.closest('pre');
const button = doc.createElement('wa-copy-button');
if (!code.id) {
code.id = `code-block-${++codeBlockId}`;
}
button.classList.add('copy-code-button');
button.setAttribute('from', code.id);
pre.append(button);
});
return doc;
};

View File

@@ -1,41 +0,0 @@
const { isExternalLink } = require('./strings.cjs');
/**
* Transforms external links to make them safer and optionally add a target. The provided doc should be a document
* object provided by JSDOM. The same document will be returned with the appropriate DOM manipulations.
*/
module.exports = function (doc, options) {
options = {
className: 'external-link', // the class name to add to links
noopener: true, // sets rel="noopener"
noreferrer: true, // sets rel="noreferrer"
ignore: () => false, // callback function to filter links that should be ignored
within: 'body', // element that contains the target links
target: '', // sets the target attribute
...options
};
const within = doc.querySelector(options.within);
if (within) {
within.querySelectorAll('a').forEach(link => {
if (isExternalLink(link) && !options.ignore(link)) {
link.classList.add(options.className);
const rel = [];
if (options.noopener) rel.push('noopener');
if (options.noreferrer) rel.push('noreferrer');
if (rel.length) {
link.setAttribute('rel', rel.join(' '));
}
if (options.target) {
link.setAttribute('target', options.target);
}
}
});
}
return doc;
};

View File

@@ -1,63 +0,0 @@
const Prism = require('prismjs');
const PrismLoader = require('prismjs/components/index.js');
PrismLoader('diff');
PrismLoader.silent = true;
/** Highlights a code string. */
function highlight(code, language) {
const alias = language.replace(/^diff-/, '');
const isDiff = /^diff-/i.test(language);
// Auto-load the target language
if (!Prism.languages[alias]) {
PrismLoader(alias);
if (!Prism.languages[alias]) {
throw new Error(`Unsupported language for code highlighting: "${language}"`);
}
}
// Register diff-* languages to use the diff grammar
if (isDiff) {
Prism.languages[language] = Prism.languages.diff;
}
return Prism.highlight(code, Prism.languages[language], language);
}
/**
* Highlights all code fields that have a language parameter. If the language has a colon in its name, the first chunk
* will be the language used and additional chunks will be applied as classes to the `<pre>`. For example, a code field
* tagged with "html:preview" will be rendered as `<pre class="language-html preview">`.
*
* The provided doc should be a document object provided by JSDOM. The same document will be returned with the
* appropriate DOM manipulations.
*/
module.exports = function (doc) {
doc.querySelectorAll('pre > code[class]').forEach(code => {
// Look for class="language-*" and split colons into separate classes
code.classList.forEach(className => {
if (className.startsWith('language-')) {
//
// We use certain suffixes to indicate code previews, expanded states, etc. The class might look something like
// this:
//
// class="language-html:preview:expanded"
//
// The language will always come first, so we need to drop the "language-" prefix and everything after the first
// color to get the highlighter language.
//
const language = className.replace(/^language-/, '').split(':')[0];
try {
code.innerHTML = highlight(code.textContent ?? '', language);
} catch (err) {
// Language not found, skip it
}
}
});
});
return doc;
};

View File

@@ -1,75 +0,0 @@
const MarkdownIt = require('markdown-it');
const markdownItContainer = require('markdown-it-container');
const markdownItIns = require('markdown-it-ins');
const markdownItKbd = require('markdown-it-kbd');
const markdownItMark = require('markdown-it-mark');
const markdownItReplaceIt = require('markdown-it-replace-it');
const markdown = MarkdownIt({
html: true,
xhtmlOut: false,
breaks: false,
langPrefix: 'language-',
linkify: false,
typographer: false
});
// Third-party plugins
markdown.use(markdownItContainer);
markdown.use(markdownItIns);
markdown.use(markdownItKbd);
markdown.use(markdownItMark);
markdown.use(markdownItReplaceIt);
// Callouts
['tip', 'warning', 'danger'].forEach(type => {
const variant = type === 'tip' ? 'brand' : type;
let icon = 'circle-info';
if (type === 'warning') icon = 'triangle-exclamation';
if (type === 'danger') icon = 'circle-exclamation';
markdown.use(markdownItContainer, type, {
render: function (tokens, idx) {
if (tokens[idx].nesting === 1) {
return `
<wa-alert class="callout" variant="${variant}" open>
<wa-icon slot="icon" name="${icon}" variant="regular"></wa-icon>
`;
}
return '</wa-alert>\n';
}
});
});
// Asides
markdown.use(markdownItContainer, 'aside', {
render: function (tokens, idx) {
if (tokens[idx].nesting === 1) {
return `<aside>`;
}
return '</aside>\n';
}
});
// Details
markdown.use(markdownItContainer, 'details', {
validate: params => params.trim().match(/^details\s+(.*)$/),
render: (tokens, idx) => {
const m = tokens[idx].info.trim().match(/^details\s+(.*)$/);
if (tokens[idx].nesting === 1) {
return `<details>\n<summary><span>${markdown.utils.escapeHtml(m[1])}</span></summary>\n`;
}
return '</details>\n';
}
});
// Replace [#1234] with a link to GitHub issues
markdownItReplaceIt.replacements.push({
name: 'github-issues',
re: /\[#([0-9]+)\]/gs,
sub: '<a href="https://github.com/shoelace-style/shoelace/issues/$1">#$1</a>',
html: true,
default: true
});
module.exports = markdown;

View File

@@ -1,26 +0,0 @@
const { format } = require('prettier');
/** Formats markup using prettier. */
module.exports = function (content, options) {
options = {
arrowParens: 'avoid',
bracketSpacing: true,
htmlWhitespaceSensitivity: 'css',
insertPragma: false,
bracketSameLine: false,
jsxSingleQuote: false,
parser: 'html',
printWidth: 120,
proseWrap: 'preserve',
quoteProps: 'as-needed',
requirePragma: false,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'none',
useTabs: false,
...options
};
return format(content, options);
};

View File

@@ -1,24 +0,0 @@
/**
* @typedef {object} Replacement
* @property {string | RegExp} pattern
* @property {string} replacement
*/
/**
* @typedef {Array<Replacement>} Replacements
*/
/**
* @param {Document} content
* @param {Replacements} replacements
*/
module.exports = function (content, replacements) {
/** This seems trivial, but by assigning to a string first, THEN using innerHTML after iterating over every replacement, we reduce the calculations of JSDOM. At the time of writing benchmarks show a reduction from 9seconds to 3 seconds by doing so. */
let html = content.body.innerHTML;
replacements.forEach(replacement => {
html = html.replaceAll(replacement.pattern, replacement.replacement);
});
content.body.innerHTML = html;
};

View File

@@ -1,21 +0,0 @@
/**
* Turns headings into clickable, deep linkable anchors. The provided doc should be a document object provided by JSDOM.
* The same document will be returned with the appropriate DOM manipulations.
*/
module.exports = function (doc, options) {
const tables = [...doc.querySelectorAll('table')];
options = {
className: 'table-scroll', // the class name to add to the table's container
...options
};
tables.forEach(table => {
const div = doc.createElement('div');
div.classList.add(options.className);
table.insertAdjacentElement('beforebegin', div);
div.append(table);
});
return doc;
};

View File

@@ -1,16 +0,0 @@
const slugify = require('slugify');
/** Creates a slug from an arbitrary string of text. */
module.exports.createSlug = function (text) {
return slugify(String(text), {
remove: /[^\w|\s]/g,
lower: true
});
};
/** Determines whether or not a link is external. */
module.exports.isExternalLink = function (link) {
// We use the "internal" hostname when initializing JSDOM so we know that those are local links
if (!link.hostname || link.hostname === 'internal') return false;
return true;
};

View File

@@ -1,42 +0,0 @@
/**
* Generates an in-page table of contents based on headings.
*/
module.exports = function (doc, options) {
options = {
levels: ['h2'], // headings to include (they must have an id)
container: 'nav', // the container to append links to
listItem: true, // if true, links will be wrapped in <li>
within: 'body', // the element containing the headings to summarize
...options
};
const container = doc.querySelector(options.container);
const within = doc.querySelector(options.within);
const headingSelector = options.levels.map(h => `${h}[id]`).join(', ');
if (!container || !within) {
return doc;
}
within.querySelectorAll(headingSelector).forEach(heading => {
const listItem = doc.createElement('li');
const link = doc.createElement('a');
const level = heading.tagName.slice(1);
link.href = `#${heading.id}`;
link.textContent = heading.textContent;
if (options.listItem) {
// List item + link
listItem.setAttribute('data-level', level);
listItem.append(link);
container.append(listItem);
} else {
// Link only
link.setAttribute('data-level', level);
container.append(link);
}
});
return doc;
};

View File

@@ -1,23 +0,0 @@
const smartquotes = require('smartquotes');
smartquotes.replacements.push([/---/g, '\u2014']); // em dash
smartquotes.replacements.push([/--/g, '\u2013']); // en dash
smartquotes.replacements.push([/\.\.\./g, '\u2026']); // ellipsis
smartquotes.replacements.push([/\(c\)/gi, '\u00A9']); // copyright
smartquotes.replacements.push([/\(r\)/gi, '\u00AE']); // registered trademark
smartquotes.replacements.push([/\?!/g, '\u2048']); // ?!
smartquotes.replacements.push([/!!/g, '\u203C']); // !!
smartquotes.replacements.push([/\?\?/g, '\u2047']); // ??
smartquotes.replacements.push([/([0-9]\s?)-(\s?[0-9])/g, '$1\u2013$2']); // number ranges use en dash
/**
* Improves typography by adding smart quotes and similar corrections within the specified element(s).
*
* The provided doc should be a document object provided by JSDOM. The same document will be returned with the
* appropriate DOM manipulations.
*/
module.exports = function (doc, selector = 'body') {
const elements = [...doc.querySelectorAll(selector)];
elements.forEach(el => smartquotes.element(el));
return doc;
};

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.63 3.625C11.63 4.27911 11.2435 4.84296 10.6865 5.10064L14 8L17.2622 7.34755C17.0968 7.10642 17 6.81452 17 6.5C17 5.67157 17.6716 5 18.5 5C19.3284 5 20 5.67157 20 6.5C20 7.31157 19.3555 7.9726 18.5504 7.99917L15.0307 15.8207C14.7077 16.5384 13.9939 17 13.2068 17H6.79317C6.00615 17 5.29229 16.5384 4.96933 15.8207L1.44963 7.99917C0.64452 7.9726 0 7.31157 0 6.5C0 5.67157 0.671573 5 1.5 5C2.32843 5 3 5.67157 3 6.5C3 6.81452 2.9032 7.10642 2.73777 7.34755L6 8L9.31702 5.09761C8.76346 4.83855 8.38 4.27656 8.38 3.625C8.38 2.72754 9.10754 2 10.005 2C10.9025 2 11.63 2.72754 11.63 3.625Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 722 B

View File

@@ -1,206 +0,0 @@
//
// Sidebar
//
// When the sidebar is hidden, we apply the inert attribute to prevent focus from reaching it. Due to the many states
// the sidebar can have (e.g. static, hidden, expanded), we test for visibility by checking to see if it's placed
// offscreen or not. Then, on resize/transition we make sure to update the attribute accordingly.
//
(() => {
function getSidebar() {
return document.getElementById('sidebar');
}
function isSidebarOpen() {
return document.documentElement.classList.contains('sidebar-open');
}
function isSidebarVisible() {
return getSidebar().getBoundingClientRect().x >= 0;
}
function toggleSidebar(force) {
const isOpen = typeof force === 'boolean' ? force : !isSidebarOpen();
return document.documentElement.classList.toggle('sidebar-open', isOpen);
}
function updateInert() {
getSidebar().inert = !isSidebarVisible();
}
// Toggle the menu
document.addEventListener('click', event => {
const menuToggle = event.target.closest('#menu-toggle');
if (!menuToggle) return;
toggleSidebar();
});
// Update the sidebar's inert state when the window resizes and when the sidebar transitions
window.addEventListener('resize', () => toggleSidebar(false));
document.addEventListener('transitionend', event => {
const sidebar = event.target.closest('#sidebar');
if (!sidebar) return;
updateInert();
});
// Close when a menu item is selected on mobile
document.addEventListener('click', event => {
const sidebar = event.target.closest('#sidebar');
const link = event.target.closest('a');
if (!sidebar || !link) return;
if (isSidebarOpen()) {
toggleSidebar();
}
});
// Close when open and escape is pressed
document.addEventListener('keydown', event => {
if (event.key === 'Escape' && isSidebarOpen()) {
event.stopImmediatePropagation();
toggleSidebar();
}
});
// Close when clicking outside of the sidebar
document.addEventListener('mousedown', event => {
if (isSidebarOpen() & !event.target?.closest('#sidebar, #menu-toggle')) {
event.stopImmediatePropagation();
toggleSidebar();
}
});
updateInert();
})();
//
// Open details when printing
//
(() => {
const detailsOpenOnPrint = new Set();
window.addEventListener('beforeprint', () => {
detailsOpenOnPrint.clear();
document.querySelectorAll('details').forEach(details => {
if (details.open) {
detailsOpenOnPrint.add(details);
}
details.open = true;
});
});
window.addEventListener('afterprint', () => {
document.querySelectorAll('details').forEach(details => {
details.open = detailsOpenOnPrint.has(details);
});
detailsOpenOnPrint.clear();
});
})();
//
// Smooth links
//
(() => {
document.addEventListener('click', event => {
const link = event.target.closest('a');
const id = (link?.hash ?? '').substr(1);
const isFragment = link?.hasAttribute('href') && link?.getAttribute('href').startsWith('#');
if (!link || !isFragment || link.getAttribute('data-smooth-link') === 'false') {
return;
}
// Scroll to the top
if (link.hash === '') {
event.preventDefault();
window.scroll({ top: 0, behavior: 'smooth' });
history.pushState(undefined, undefined, location.pathname);
}
// Scroll to an id
if (id) {
const target = document.getElementById(id);
if (target) {
event.preventDefault();
window.scroll({ top: target.offsetTop, behavior: 'smooth' });
history.pushState(undefined, undefined, `#${id}`);
}
}
});
})();
//
// Table of Contents scrollspy
//
(() => {
// This will be stale if its not a function.
const getLinks = () => [...document.querySelectorAll('.content__toc a')];
const linkTargets = new WeakMap();
const visibleTargets = new WeakSet();
const observer = new IntersectionObserver(handleIntersect, { rootMargin: '0px 0px' });
let debounce;
function handleIntersect(entries) {
entries.forEach(entry => {
// Remember which targets are visible
if (entry.isIntersecting) {
visibleTargets.add(entry.target);
} else {
visibleTargets.delete(entry.target);
}
});
updateActiveLinks();
}
function updateActiveLinks() {
const links = getLinks();
// Find the first visible target and activate the respective link
links.find(link => {
const target = linkTargets.get(link);
if (target && visibleTargets.has(target)) {
links.forEach(el => el.classList.toggle('active', el === link));
return true;
}
return false;
});
}
// Observe link targets
function observeLinks() {
getLinks().forEach(link => {
const hash = link.hash.slice(1);
const target = hash ? document.querySelector(`.content__body #${hash}`) : null;
if (target) {
linkTargets.set(link, target);
observer.observe(target);
}
});
}
observeLinks();
document.addEventListener('turbo:load', updateActiveLinks);
document.addEventListener('turbo:load', observeLinks);
})();
//
// Show custom versions in the sidebar
//
(() => {
function updateVersion() {
const el = document.querySelector('.sidebar-version');
if (!el) return;
if (location.hostname === 'next.shoelace.style') el.textContent = 'Next';
if (location.hostname === 'localhost') el.textContent = 'Development';
}
updateVersion();
document.addEventListener('turbo:load', updateVersion);
})();

View File

@@ -1,384 +0,0 @@
(() => {
// Append the search dialog to the body
const siteSearch = document.createElement('div');
const scrollbarWidth = Math.abs(window.innerWidth - document.documentElement.clientWidth);
siteSearch.classList.add('search');
siteSearch.innerHTML = `
<div class="search__overlay"></div>
<dialog id="search-dialog" class="search__dialog">
<div class="search__content">
<div class="search__header">
<div id="search-combobox" class="search__input-wrapper">
<wa-icon name="search"></wa-icon>
<input
id="search-input"
class="search__input"
type="search"
placeholder="Search"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
enterkeyhint="go"
spellcheck="false"
maxlength="100"
role="combobox"
aria-autocomplete="list"
aria-expanded="true"
aria-controls="search-listbox"
aria-haspopup="listbox"
aria-activedescendant
>
<button type="button" class="search__clear-button" aria-label="Clear entry" tabindex="-1" hidden>
<wa-icon name="circle-xmark" variant="regular"></wa-icon>
</button>
</div>
</div>
<div class="search__body">
<ul
id="search-listbox"
class="search__results"
role="listbox"
aria-label="Search results"
></ul>
<div class="search__empty">No matching pages</div>
</div>
<footer class="search__footer">
<small><kbd><wa-icon label="Up" name="arrow-up"></wa-icon></kbd> <kbd><wa-icon label="Down" name="arrow-down"></wa-icon></kbd> Navigate</small>
<small><kbd><wa-icon label="Enter" name="arrow-turn-down-left"></wa-icon></kbd> Select</small>
<small><kbd>Esc</kbd> Close</small>
</footer>
</div>
</dialog>
`;
const overlay = siteSearch.querySelector('.search__overlay');
const dialog = siteSearch.querySelector('.search__dialog');
const input = siteSearch.querySelector('.search__input');
const clearButton = siteSearch.querySelector('.search__clear-button');
const results = siteSearch.querySelector('.search__results');
const version = document.documentElement.getAttribute('data-wa-version');
const key = `search_${version}`;
const searchDebounce = 50;
const animationDuration = 150;
let isShowing = false;
let searchTimeout;
let searchIndex;
let map;
const loadSearchIndex = new Promise(resolve => {
const cache = localStorage.getItem(key);
const wait = 'requestIdleCallback' in window ? requestIdleCallback : requestAnimationFrame;
// Cleanup older search indices (everything before this version)
try {
const items = { ...localStorage };
Object.keys(items).forEach(k => {
if (key > k) {
localStorage.removeItem(k);
}
});
} catch {
/* do nothing */
}
// Look for a cached index
try {
if (cache) {
const data = JSON.parse(cache);
searchIndex = window.lunr.Index.load(data.searchIndex);
map = data.map;
return resolve();
}
} catch {
/* do nothing */
}
// Wait until idle to fetch the index
wait(() => {
fetch('/assets/search.json')
.then(res => res.json())
.then(data => {
if (!window.lunr) {
console.error('The Lunr search client has not yet been loaded.');
}
searchIndex = window.lunr.Index.load(data.searchIndex);
map = data.map;
// Cache the search index for this version
if (version) {
try {
localStorage.setItem(key, JSON.stringify(data));
} catch (err) {
console.warn(`Unable to cache the search index: ${err}`);
}
}
resolve();
});
});
});
async function show() {
isShowing = true;
document.body.append(siteSearch);
document.body.classList.add('search-visible');
document.body.style.setProperty('--docs-search-scroll-lock-size', `${scrollbarWidth}px`);
clearButton.hidden = true;
requestAnimationFrame(() => input.focus());
updateResults();
dialog.showModal();
await Promise.all([
dialog.animate(
[
{ opacity: 0, transform: 'scale(.9)', transformOrigin: 'top' },
{ opacity: 1, transform: 'scale(1)', transformOrigin: 'top' }
],
{ duration: animationDuration }
).finished,
overlay.animate([{ opacity: 0 }, { opacity: 1 }], { duration: animationDuration }).finished
]);
dialog.addEventListener('mousedown', handleMouseDown);
dialog.addEventListener('keydown', handleKeyDown);
}
async function hide() {
isShowing = false;
await Promise.all([
dialog.animate(
[
{ opacity: 1, transform: 'scale(1)', transformOrigin: 'top' },
{ opacity: 0, transform: 'scale(.9)', transformOrigin: 'top' }
],
{ duration: animationDuration }
).finished,
overlay.animate([{ opacity: 1 }, { opacity: 0 }], { duration: animationDuration }).finished
]);
dialog.close();
input.blur(); // otherwise Safari will scroll to the bottom of the page on close
input.value = '';
document.body.classList.remove('search-visible');
document.body.style.removeProperty('--docs-search-scroll-lock-size');
siteSearch.remove();
updateResults();
dialog.removeEventListener('mousedown', handleMouseDown);
dialog.removeEventListener('keydown', handleKeyDown);
}
function handleInput() {
clearButton.hidden = input.value === '';
// Debounce search queries
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => updateResults(input.value), searchDebounce);
}
function handleClear() {
clearButton.hidden = true;
input.value = '';
input.focus();
updateResults();
}
function handleMouseDown(event) {
if (!event.target.closest('.search__content')) {
hide();
}
}
function handleKeyDown(event) {
// Close when pressing escape
if (event.key === 'Escape') {
event.preventDefault(); // prevent <dialog> from closing immediately so it can animate
event.stopImmediatePropagation();
hide();
return;
}
// Handle keyboard selections
if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) {
event.preventDefault();
const currentEl = results.querySelector('[data-selected="true"]');
const items = [...results.querySelectorAll('li')];
const index = items.indexOf(currentEl);
let nextEl;
if (items.length === 0) {
return;
}
switch (event.key) {
case 'ArrowUp':
nextEl = items[Math.max(0, index - 1)];
break;
case 'ArrowDown':
nextEl = items[Math.min(items.length - 1, index + 1)];
break;
case 'Home':
nextEl = items[0];
break;
case 'End':
nextEl = items[items.length - 1];
break;
case 'Enter':
currentEl?.querySelector('a')?.click();
break;
}
// Update the selected item
items.forEach(item => {
if (item === nextEl) {
input.setAttribute('aria-activedescendant', item.id);
item.setAttribute('data-selected', 'true');
nextEl.scrollIntoView({ block: 'nearest' });
} else {
item.setAttribute('data-selected', 'false');
}
});
}
}
async function updateResults(query = '') {
try {
await loadSearchIndex;
const hasQuery = query.length > 0;
const searchTerms = query
.split(' ')
.map((term, index, arr) => {
// Search API: https://lunrjs.com/guides/searching.html
if (index === arr.length - 1) {
// The last term is not mandatory and 1x fuzzy. We also duplicate it with a wildcard to match partial words
// as the user types.
return `${term}~1 ${term}*`;
} else {
// All other terms are mandatory and 1x fuzzy
return `+${term}~1`;
}
})
.join(' ');
const matches = hasQuery ? searchIndex.search(searchTerms) : [];
const hasResults = hasQuery && matches.length > 0;
siteSearch.classList.toggle('search--has-results', hasQuery && hasResults);
siteSearch.classList.toggle('search--no-results', hasQuery && !hasResults);
input.setAttribute('aria-activedescendant', '');
results.innerHTML = '';
matches.forEach((match, index) => {
const page = map[match.ref];
const li = document.createElement('li');
const a = document.createElement('a');
const displayTitle = page.title ?? '';
const displayDescription = page.description ?? '';
const displayUrl = page.url.replace(/^\//, '').replace(/\/$/, '');
let icon = 'file-text';
a.setAttribute('role', 'option');
a.setAttribute('id', `search-result-item-${match.ref}`);
if (page.url.includes('getting-started/')) {
icon = 'lightbulb';
}
if (page.url.includes('resources/')) {
icon = 'book';
}
if (page.url.includes('components/')) {
icon = 'puzzle-piece';
}
if (page.url.includes('tokens/')) {
icon = 'swatchbook';
}
if (page.url.includes('utilities/')) {
icon = 'wrench';
}
if (page.url.includes('tutorials/')) {
icon = 'gamepad';
}
li.classList.add('search__result');
li.setAttribute('role', 'option');
li.setAttribute('id', `search-result-item-${match.ref}`);
li.setAttribute('data-selected', index === 0 ? 'true' : 'false');
a.href = page.url;
a.innerHTML = `
<div class="search__result-icon" aria-hidden="true">
<wa-icon name="${icon}"></wa-icon>
</div>
<div class="search__result__details">
<div class="search__result-title"></div>
<div class="search__result-description"></div>
<div class="search__result-url"></div>
</div>
`;
a.querySelector('.search__result-title').textContent = displayTitle;
a.querySelector('.search__result-description').textContent = displayDescription;
a.querySelector('.search__result-url').textContent = displayUrl;
li.appendChild(a);
results.appendChild(li);
});
} catch {
// Ignore query errors as the user types
}
}
// Show the search dialog when clicking on data-plugin="search"
document.addEventListener('click', event => {
const searchButton = event.target.closest('[data-plugin="search"]');
if (searchButton) {
show();
}
});
// Show the search dialog when slash (or CMD+K) is pressed and focus is not inside a form element
document.addEventListener('keydown', event => {
if (
!isShowing &&
(event.key === '/' || (event.key === 'k' && (event.metaKey || event.ctrlKey))) &&
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
) {
event.preventDefault();
show();
}
});
// Purge cache when we press CMD+CTRL+R
document.addEventListener('keydown', event => {
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'r') {
localStorage.clear();
}
});
input.addEventListener('input', handleInput);
clearButton.addEventListener('click', handleClear);
// Close when a result is selected
results.addEventListener('click', event => {
if (event.target.closest('a')) {
hide();
}
});
// We're using Turbo, so when a user searches for something, visits a result, and presses the back button, the search
// UI will still be visible but not interactive. This removes the search UI when Turbo renders a page so they don't
// get trapped.
window.addEventListener('turbo:render', () => {
document.body.classList.remove('search-visible');
document.querySelectorAll('.search__overlay, .search__dialog').forEach(el => el.remove());
});
})();

View File

@@ -1,29 +0,0 @@
import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@7.3.0/+esm';
(() => {
if (!window.scrollPositions) {
window.scrollPositions = {};
}
function preserveScroll() {
document.querySelectorAll('[data-preserve-scroll').forEach(element => {
scrollPositions[element.id] = element.scrollTop;
});
}
function restoreScroll(event) {
document.querySelectorAll('[data-preserve-scroll').forEach(element => {
element.scrollTop = scrollPositions[element.id];
});
if (event.detail && event.detail.newBody) {
event.detail.newBody.querySelectorAll('[data-preserve-scroll').forEach(element => {
element.scrollTop = scrollPositions[element.id];
});
}
}
window.addEventListener('turbo:before-cache', preserveScroll);
window.addEventListener('turbo:before-render', restoreScroll);
window.addEventListener('turbo:render', restoreScroll);
})();

View File

@@ -1,943 +0,0 @@
:root {
--docs-background-color: var(--wa-color-surface-default);
--docs-content-max-width: 860px;
--docs-sidebar-width: 320px;
--docs-sidebar-transition-speed: 250ms;
--docs-content-toc-max-width: 260px;
--docs-content-padding: 2rem;
--docs-content-vertical-spacing: 2rem;
--docs-search-overlay-background: rgb(0 0 0 / 0.2);
--docs-skip-to-main-width: 200px;
}
/* Light theme */
:root {
color-scheme: normal;
--docs-overlay-color: hsl(240 3.8% 46.1% / 33%);
--docs-shadow-x-small: 0 1px 2px hsl(240 3.8% 46.1% / 12%);
--docs-shadow-small: 0 1px 2px hsl(240 3.8% 46.1% / 24%);
--docs-shadow-medium: 0 2px 4px hsl(240 3.8% 46.1% / 24%);
--docs-shadow-large: 0 2px 8px hsl(240 3.8% 46.1% / 24%);
--docs-shadow-x-large: 0 4px 16px hsl(240 3.8% 46.1% / 24%);
}
/* Utils */
html.wa-theme-dark .only-light,
html:not(.wa-theme-dark) .only-dark {
display: none !important;
}
.nowrap {
white-space: nowrap;
}
.visually-hidden:not(:focus-within) {
position: absolute !important;
width: 1px !important;
height: 1px !important;
clip: rect(0 0 0 0) !important;
clip-path: inset(50%) !important;
border: none !important;
overflow: hidden !important;
white-space: nowrap !important;
padding: 0 !important;
}
@media screen and (max-width: 900px) {
:root {
--docs-content-padding: 1rem;
}
}
html {
height: 100%;
box-sizing: border-box;
line-height: var(--wa-font-line-height-regular);
padding: 0;
margin: 0;
}
body {
height: 100%;
padding: 0;
margin: 0;
overflow-x: hidden;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
}
/* Common elements */
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 3rem 0 1.5rem 0;
}
h1:first-of-type {
margin-top: 1rem;
}
.badges img {
border-radius: var(--wa-corners-s);
}
.callout img {
width: 100%;
margin-left: 0;
margin-right: 0;
}
/* Color matching logos */
svg.logo {
color: var(--wa-color-brand-text-on-surface);
}
/* Anchor headings */
.anchor-heading {
position: relative;
color: inherit;
text-decoration: none;
}
.anchor-heading a {
text-decoration: none;
color: inherit;
}
.anchor-heading a::after {
content: '#';
color: color-mix(in oklab, var(--wa-color-text-link) 100%, var(--wa-color-mix-hover));
margin-inline: 0.5rem;
opacity: 0;
transition: 100ms opacity;
}
.anchor-heading:hover a::after,
.anchor-heading:focus-within a::after {
opacity: 1;
}
/* External links */
.external-link__icon {
width: 0.75em;
height: 0.75em;
vertical-align: 0;
margin-left: 0.25em;
margin-right: 0.125em;
}
/* Tables */
table th p:first-child,
table td p:first-child {
margin-top: 0;
}
table th p:last-child,
table td p:last-child {
margin-bottom: 0;
}
.table-scroll {
max-width: 100%;
overflow-x: auto;
}
.table-scroll code {
white-space: nowrap;
}
th.table-name,
th.table-event-detail {
min-width: 15ch;
}
th.table-description {
min-width: 50ch !important;
max-width: 70ch;
}
/* Code blocks */
pre:not(:last-child) {
margin-bottom: 1.5rem;
}
pre {
position: relative;
}
pre > code {
display: block;
background: none !important;
border-radius: 0;
hyphens: none;
tab-size: 2;
white-space: pre;
padding: 1rem;
margin: -1rem;
overflow: auto;
}
pre .token.comment {
color: var(--wa-color-base-40);
}
pre .token.prolog,
pre .token.doctype,
pre .token.cdata,
pre .token.operator,
pre .token.punctuation {
color: var(--wa-color-base-40);
}
.namespace {
opacity: 0.7;
}
pre .token.property,
pre .token.keyword,
pre .token.tag,
pre .token.url {
color: var(--wa-color-brand-text-on-fill);
}
pre .token.symbol,
pre .token.deleted {
color: var(--wa-color-danger-text-on-fill);
}
pre .token.boolean,
pre .token.constant,
pre .token.selector,
pre .token.attr-name,
pre .token.string,
pre .token.char,
pre .token.builtin,
pre .token.inserted {
color: var(--wa-color-success-text-on-fill);
}
pre .token.atrule,
pre .token.attr-value,
pre .token.number,
pre .token.variable {
color: var(--wa-color-warning-text-on-fill);
}
pre .token.function,
pre .token.class-name,
pre .token.regex {
color: var(--wa-color-danger-text-on-fill);
}
pre .token.important {
color: var(--wa-color-danger-text-on-fill);
}
pre .token.important,
pre .token.bold {
font-weight: bold;
}
pre .token.italic {
font-style: italic;
}
/* Copy code button */
.copy-code-button {
position: absolute;
top: 0;
right: 0;
white-space: normal;
color: var(--wa-color-neutral-text-on-fill-alt);
transition:
150ms opacity,
150ms scale;
}
.copy-code-button::part(button) {
background-color: var(--wa-color-neutral-fill-subtle);
border-radius: 0 var(--wa-corners-s) 0 var(--wa-corners-s);
padding: 0.75rem;
}
.copy-code-button::part(button):hover {
background-color: color-mix(in oklab, var(--wa-color-neutral-fill-subtle), var(--wa-color-mix-hover));
}
.copy-code-button::part(button):active {
background-color: color-mix(in oklab, var(--wa-color-neutral-fill-subtle), var(--wa-color-mix-active));
}
pre .copy-code-button {
opacity: 0;
scale: 0.75;
}
pre:hover .copy-code-button,
.copy-code-button:focus-within {
opacity: 1;
scale: 1;
}
/* Callouts */
.callout {
margin-bottom: var(--docs-content-vertical-spacing);
}
.callout > :first-child {
margin-top: 0;
}
.callout > :last-child {
margin-bottom: 0;
}
.callout a {
color: inherit;
}
.callout p {
margin-top: 0;
}
/* Aside */
.content aside {
float: right;
min-width: 300px;
max-width: 50%;
background: var(--wa-color-surface-lowered);
border-radius: var(--wa-corners-s);
padding: var(--wa-space-m);
margin-left: var(--wa-space-m);
}
.content aside > :first-child {
margin-top: 0;
}
.content aside > :last-child {
margin-bottom: 0;
}
@media screen and (max-width: 600px) {
.content aside {
float: none;
width: calc(100% + (var(--docs-content-padding) * 2));
max-width: none;
margin: var(--docs-content-vertical-spacing) calc(-1 * var(--docs-content-padding));
}
}
/* Sidebar */
#sidebar {
position: fixed;
flex: 0;
top: 0;
left: 0;
bottom: 0;
z-index: 20;
width: var(--docs-sidebar-width);
background-color: var(--docs-background-color);
border-right: solid var(--wa-border-width-s) var(--wa-color-surface-border);
border-radius: 0;
padding: 2rem;
margin: 0;
overflow: auto;
scrollbar-width: thin;
transition: var(--docs-sidebar-transition-speed) translate ease-in-out;
}
#sidebar::-webkit-scrollbar {
width: 4px;
}
#sidebar::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 9999px;
}
#sidebar:hover::-webkit-scrollbar-thumb {
background: var(--wa-color-base-60);
}
#sidebar:hover::-webkit-scrollbar-track {
background: var(--wa-color-base-95);
}
#sidebar > header {
margin-bottom: 1.5rem;
}
#sidebar > header h1 {
margin: 0;
}
#sidebar > header svg {
margin-bottom: var(--wa-space-s);
}
#sidebar > header a {
display: block;
}
#sidebar nav a {
text-decoration: none;
}
#sidebar nav h2 {
font-size: var(--wa-font-size-m);
font-weight: var(--wa-font-weight-medium);
border-bottom: solid var(--wa-border-width-s) var(--wa-color-surface-border);
margin: var(--wa-space-xl) 0 var(--wa-space-xs) 0;
}
#sidebar ul {
padding: 0;
margin: 0;
}
#sidebar ul li {
list-style: none;
padding: 0;
margin: 0.125rem 0.5rem;
}
#sidebar ul ul ul {
margin-left: 0.75rem;
}
#sidebar ul li a {
line-height: 1.33;
color: inherit;
display: inline-block;
padding: 0;
}
#sidebar ul li a:not(.active-link):hover {
color: var(--wa-color-text-link);
}
#sidebar nav .active-link {
color: var(--wa-color-text-link);
border-bottom: dashed 1px var(--wa-color-text-link);
}
#sidebar > header img {
display: block;
width: 100%;
height: auto;
margin: 0 auto;
}
@media screen and (max-width: 900px) {
#sidebar {
translate: -100%;
}
.sidebar-open #sidebar {
translate: 0;
}
}
.sidebar-version {
font-size: var(--wa-font-size-s);
color: var(--wa-color-text-quiet);
text-align: right;
margin-top: calc(-1 * var(--wa-space-s));
margin-bottom: calc(-1 * var(--wa-space-s));
}
.sidebar-buttons {
display: flex;
justify-content: space-between;
}
/* Main content */
main {
position: relative;
padding: var(--docs-content-vertical-spacing) var(--docs-content-padding)
calc(var(--docs-content-vertical-spacing) * 2) var(--docs-content-padding);
margin-left: var(--docs-sidebar-width);
}
.sidebar-open .content {
margin-left: 0;
}
.content__body > :last-child {
margin-bottom: 0;
}
@media screen and (max-width: 900px) {
main {
margin-left: 0;
}
}
/* Component layouts */
.content {
display: grid;
grid-template-columns: 100%;
gap: 2rem;
position: relative;
max-width: var(--docs-content-max-width);
margin: 0 auto;
}
.content--with-toc {
/* There's a 2rem gap, so we need to remove it from the column */
grid-template-columns: calc(75% - 2rem) min(25%, var(--docs-content-toc-max-width));
max-width: calc(var(--docs-content-max-width) + var(--docs-content-toc-max-width));
}
.content__body {
order: 1;
width: 100%;
}
.content:not(.content--with-toc) .content__toc {
display: none;
}
.content__toc {
order: 2;
display: flex;
flex-direction: column;
margin-top: 0;
}
.content__toc ul {
position: sticky;
top: 5rem;
max-height: calc(100vh - 6rem);
font-size: var(--wa-font-size-s);
line-height: 1.33;
border-left: var(--wa-border-style) var(--wa-border-width-s) var(--wa-color-surface-border);
list-style: none;
padding: 1rem 0;
margin: 0;
padding-left: 1rem;
overflow-y: auto;
}
.content__toc li {
padding: 0 0 0 0.5rem;
margin: 0;
}
.content__toc li[data-level='3'] {
margin-left: 1rem;
}
/* We don't use them, but just in case */
.content__toc li[data-level='4'],
.content__toc li[data-level='5'],
.content__toc li[data-level='6'] {
margin-left: 2rem;
}
.content__toc li:not(:last-of-type) {
margin-bottom: 0.6rem;
}
.content__toc a {
color: var(--wa-color-text-normal);
text-decoration: none;
}
.content__toc a:hover {
color: var(--wa-color-text-link);
}
.content__toc a.active {
color: var(--wa-color-brand-text-on-surface);
border-bottom: dashed 1px var(--wa-color-brand-text-on-surface);
}
.content__toc .top a {
font-weight: var(--wa-font-weight-medium);
color: var(--wa-color-text-quiet);
}
@media screen and (max-width: 1024px) {
.content {
grid-template-columns: 100%;
gap: 0;
}
.content__toc {
position: relative;
order: 1;
}
.content__toc ul {
display: flex;
justify-content: start;
gap: 1rem 1.5rem;
position: static;
border: none;
border-bottom: solid 1px var(--wa-color-surface-border);
border-radius: 0;
padding: 1rem 1.5rem 1rem 0.5rem; /* extra right padding to hide the fade effect */
margin-top: 1rem;
overflow-x: auto;
}
.content__toc ul::after {
content: '';
position: absolute;
top: 0;
bottom: 1rem; /* don't cover the scrollbar */
right: 0;
width: 2rem;
background: linear-gradient(90deg, rgba(0 0 0 / 0) 0%, var(--wa-color-surface-default) 100%);
}
.content__toc li {
white-space: nowrap;
}
.content__toc li:not(:last-of-type) {
margin-bottom: 0;
}
.content__toc [data-level]:not([data-level='2']) {
display: none;
}
.content__body {
order: 2;
}
}
/* Menu toggle */
#menu-toggle {
display: none;
position: fixed;
z-index: 30;
top: 0.25rem;
left: 0.25rem;
height: auto;
width: auto;
color: var(--wa-color-neutral-spot);
border: none;
border-radius: 50%;
background: var(--wa-color-surface-default);
padding: 0.5rem;
margin: 0;
cursor: pointer;
transition: 250ms rotate ease;
}
@media screen and (max-width: 900px) {
#menu-toggle {
display: flex;
}
}
#menu-toggle svg {
width: 1.25rem;
height: 1.25rem;
}
html.sidebar-open #menu-toggle {
rotate: 180deg;
}
/* Skip to main content */
#skip-to-main {
position: fixed;
top: 0.25rem;
left: calc(50% - var(--docs-skip-to-main-width) / 2);
z-index: 100;
width: var(--docs-skip-to-main-width);
text-align: center;
text-decoration: none;
border-radius: 9999px;
background: var(--wa-color-surface-default);
color: var(--wa-color-text-normal);
padding: 0.5rem;
}
/* Print styles */
@media print {
a:not(.anchor-heading)[href]::after {
content: ' (' attr(href) ')';
}
details,
pre {
border: solid var(--wa-border-width-s) var(--wa-color-surface-border);
}
details summary {
list-style: none;
}
details summary span {
padding-left: 0;
}
details summary::marker,
details summary::-webkit-details-marker {
display: none;
}
.component-page__navigation,
.copy-code-button,
.code-preview__buttons,
.code-preview__resizer {
display: none !important;
}
.flavor-html .code-preview__source--html,
.flavor-react .code-preview__source--react {
display: block !important;
}
.flavor-html .code-preview__source--html > pre,
.flavor-react .code-preview__source--react > pre {
border: none;
}
.code-preview__source-group {
border-bottom: solid 1px var(--wa-border-width-s);
border-bottom-left-radius: var(--wa-corners-s);
border-bottom-right-radius: var(--wa-corners-s);
}
#sidebar {
display: none;
}
#content {
margin-left: 0;
}
#menu-toggle,
#icon-toolbar,
.external-link__icon {
display: none;
}
}
/* Splash */
.splash {
display: flex;
padding-top: 2rem;
}
.splash-start {
min-width: 440px;
}
.splash li img {
width: 1em;
height: 1em;
vertical-align: -2px;
}
.splash svg {
margin-block-end: var(--wa-space-m);
}
.splash-end {
display: flex;
align-items: flex-end;
width: auto;
padding-left: 1rem;
}
.splash-image {
width: 100%;
height: auto;
}
.splash-start h1:first-of-type {
font-size: var(--wa-font-size-l);
font-weight: var(--wa-font-weight-normal);
margin: 0 0 0.5rem 0;
}
@media screen and (max-width: 1280px) {
.splash {
display: block;
}
.splash-start {
min-width: 0;
padding-bottom: 1rem;
}
.splash-end {
padding: 0;
}
.splash-image {
display: block;
max-width: 400px;
}
/* Shields */
.splash + p {
margin-top: 2rem;
}
}
/* Component headers */
.component-header h1 {
margin-bottom: 0;
}
.component-header__tag {
margin-top: -0.5rem;
margin-bottom: 0.5rem;
}
.component-header__tag code {
background: none;
color: var(--wa-color-text-quiet);
font-size: var(--wa-font-size-l);
padding: 0;
margin: 0;
}
.component-header__info {
margin-bottom: var(--wa-space-2xl);
}
.component-summary {
font-size: var(--wa-font-size-l);
line-height: 1.6;
margin: 2rem 0;
}
/* Repo buttons */
.sidebar-buttons {
display: flex;
gap: 0.125rem;
justify-content: space-between;
}
.sidebar-buttons .repo-button {
flex: 1 1 auto;
}
.repo-button wa-icon {
color: var(--wa-color-neutral-text-on-spot);
}
@media screen and (max-width: 400px) {
:not(.sidebar-buttons) > .repo-button {
width: 100%;
margin-bottom: 1rem;
}
}
body[data-page^='/tokens/'] .table-wrapper td:first-child,
body[data-page^='/tokens/'] .table-wrapper td:first-child code {
white-space: nowrap;
}
/* Border demos */
.border-demo {
height: 60px;
border-left: solid 1px var(--wa-color-brand-spot);
}
.corner-demo {
width: 3rem;
height: 3rem;
background: var(--wa-color-brand-spot);
}
/* Transition demo */
.transition-demo {
position: relative;
background: var(--wa-color-neutral-fill-subtle);
width: 8rem;
height: 2rem;
}
.transition-demo:after {
content: '';
position: absolute;
background-color: var(--wa-color-brand-spot);
top: 0;
left: 0;
width: 0;
height: 100%;
transition-duration: inherit;
transition-property: width;
}
.transition-demo:hover:after {
width: 100%;
}
/* Spacing demo */
.spacing-demo {
background: var(--wa-color-brand-spot);
}
/* Shadow demo */
.shadow-demo {
background: transparent;
border-radius: 3px;
width: 4rem;
height: 4rem;
margin: 1rem;
}
/* Color palettes */
.color-palette {
display: grid;
grid-template-columns: 200px repeat(11, 1fr);
gap: var(--wa-space-m) 1px;
margin: var(--wa-space-2xl) 0;
}
.color-palette__name {
font-weight: var(--wa-font-weight-medium);
grid-template-columns: repeat(11, 1fr);
}
.color-palette__name code {
background: none;
font-size: var(--wa-font-size-s);
}
.color-palette__example {
font-size: var(--wa-font-size-s);
text-align: center;
}
.color-palette__swatch {
height: 3rem;
border-radius: var(--wa-corners-xs);
}
.color-palette__swatch--border {
box-shadow: inset 0 0 0 1px var(--wa-color-surface-border);
}
@media screen and (max-width: 1200px) {
.color-palette {
grid-template-columns: repeat(6, 1fr);
}
.color-palette__name {
grid-column-start: span 6;
}
}
.docs-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 2rem;
}

147
docs/astro.config.mjs Normal file
View File

@@ -0,0 +1,147 @@
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import * as url from 'node:url';
import * as path from 'node:path';
// const __filename = url.fileURLToPath(import.meta.url);
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
import FullReload from 'vite-plugin-full-reload';
import { customElementsManifest } from './src/js/cem.js';
import { RemarkPluginFindAndReplace } from 'remark-plugin-find-and-replace';
import rehypeExternalLinks from 'rehype-external-links';
import remarkCodeHighlighter from './src/plugins/prism';
import GithubAutolink from './src/plugins/github-autolink.ts';
const version = customElementsManifest().package.version;
const cdndir = 'cdn';
const npmdir = 'dist';
function remarkFrontmatterPlugin() {
// All remark and rehype plugins return a separate function
return function (tree, file) {
const frontmatter = file.data.astro.frontmatter;
frontmatter.npmdir = npmdir;
frontmatter.cdndir = cdndir;
frontmatter.version = version;
};
}
// https://astro.build/config
export default defineConfig({
server: {
open: true,
port: 4000,
host: true
},
vite: {
server: {
watch: {
ignored: ['./public/pagefind/**/*.*'] // HERE
}
},
plugins: [
FullReload([
path.relative(__dirname, '../dist/custom-elements.json')
// path.relative(__dirname, './public/**/*.*')
])
]
},
outDir: '../_site',
site: 'https://shoelace.style',
markdown: {
syntaxHighlight: 'prism',
remarkPlugins: [
remarkFrontmatterPlugin,
RemarkPluginFindAndReplace({
replacements: [
{ pattern: '%VERSION%', replacement: version },
{ pattern: '%CDNDIR%', replacement: cdndir },
{ pattern: '%NPMDIR%', replacement: npmdir }
]
}),
GithubAutolink,
remarkCodeHighlighter
],
rehypePlugins: [
() =>
rehypeExternalLinks({
rel: ['nofollow', 'noopener', 'noreferrer'],
target: ['_blank'],
properties: {
class: 'external-link'
}
})
]
},
integrations: [
starlight({
expressiveCode: false,
title: 'Web Awesome',
social: {
github: 'https://github.com/shoelace-style/shoelace',
twitter: 'https://twitter.com/shoelace_style'
},
sidebar: [
{
label: 'Experimental',
autogenerate: { directory: 'experimental' }
},
{
label: 'Getting Started',
autogenerate: { directory: 'getting-started' }
},
{
label: 'Frameworks',
autogenerate: { directory: 'frameworks' }
},
{
label: 'Resources',
autogenerate: { directory: 'resources' },
items: [
{
label: 'Community',
link: '/resources/community'
},
{
label: 'Help & Support',
link: 'https://github.com/shoelace-style/shoelace/discussions'
},
{
label: 'Accessibility',
link: '/resources/accessibility'
},
{
label: 'Contributing',
link: '/resources/contributing'
},
{
label: 'Changelog',
link: '/resources/changelog'
}
]
},
{
label: 'Components',
autogenerate: { directory: 'components' }
},
{
label: 'Design Tokens',
autogenerate: { directory: 'tokens' }
},
{
label: 'Tutorials',
autogenerate: { directory: 'tutorials' }
}
],
// Component overrides
components: {
// Override the default `Head` component.
Head: './src/components/overrides/Head.astro',
TableOfContents: './src/components/overrides/TableOfContents.astro',
Search: './src/components/overrides/Search.astro'
}
})
]
});

View File

@@ -1,246 +0,0 @@
/* eslint-disable no-invalid-this */
const fs = require('fs');
const path = require('path');
const lunr = require('lunr');
const { capitalCase } = require('change-case');
const { JSDOM } = require('jsdom');
const { customElementsManifest, getAllComponents } = require('./_utilities/cem.cjs');
const webAwesomeFlavoredMarkdown = require('./_utilities/markdown.cjs');
const activeLinks = require('./_utilities/active-links.cjs');
const anchorHeadings = require('./_utilities/anchor-headings.cjs');
const codePreviews = require('./_utilities/code-previews.cjs');
const copyCodeButtons = require('./_utilities/copy-code-buttons.cjs');
const externalLinks = require('./_utilities/external-links.cjs');
const highlightCodeBlocks = require('./_utilities/highlight-code.cjs');
const tableOfContents = require('./_utilities/table-of-contents.cjs');
const prettier = require('./_utilities/prettier.cjs');
const scrollingTables = require('./_utilities/scrolling-tables.cjs');
const typography = require('./_utilities/typography.cjs');
const replacer = require('./_utilities/replacer.cjs');
const assetsDir = 'assets';
const cdndir = 'cdn';
const npmdir = 'dist';
const allComponents = getAllComponents();
let hasBuiltSearchIndex = false;
module.exports = function (eleventyConfig) {
//
// Global data
//
eleventyConfig.addGlobalData('baseUrl', 'https://shoelace.style/'); // the production URL
eleventyConfig.addGlobalData('layout', 'default'); // make 'default' the default layout
eleventyConfig.addGlobalData('toc', true); // enable the table of contents
eleventyConfig.addGlobalData('meta', {
title: 'Web Awesome',
description: 'A forward-thinking library of web components.',
image: 'images/og-image.png',
version: customElementsManifest.package.version,
components: allComponents,
cdndir,
npmdir
});
//
// Layout aliases
//
eleventyConfig.addLayoutAlias('default', 'default.njk');
//
// Copy assets
//
eleventyConfig.addPassthroughCopy(assetsDir);
eleventyConfig.setServerPassthroughCopyBehavior('passthrough'); // emulates passthrough copy during --serve
//
// Functions
//
// Generates a URL relative to the site's root
eleventyConfig.addNunjucksGlobal('rootUrl', (value = '', absolute = false) => {
value = path.join('/', value);
return absolute ? new URL(value, eleventyConfig.globalData.baseUrl).toString() : value;
});
// Generates a URL relative to the site's asset directory
eleventyConfig.addNunjucksGlobal('assetUrl', (value = '', absolute = false) => {
value = path.join(`/${assetsDir}`, value);
return absolute ? new URL(value, eleventyConfig.globalData.baseUrl).toString() : value;
});
// Fetches a specific component's metadata
eleventyConfig.addNunjucksGlobal('getComponent', tagName => {
const component = allComponents.find(c => c.tagName === tagName);
if (!component) {
throw new Error(
`Unable to find a component called "${tagName}". Make sure the file name is the same as the component's tag ` +
`name (minus the wa- prefix).`
);
}
return component;
});
//
// Custom markdown syntaxes
//
eleventyConfig.setLibrary('md', webAwesomeFlavoredMarkdown);
//
// Filters
//
eleventyConfig.addFilter('markdown', content => {
return webAwesomeFlavoredMarkdown.render(content);
});
eleventyConfig.addFilter('markdownInline', content => {
return webAwesomeFlavoredMarkdown.renderInline(content);
});
// Trims whitespace and pipes from the start and end of a string. Useful for CEM types, which can be pipe-delimited.
// With Prettier 3, this means a leading pipe will exist if the line wraps.
eleventyConfig.addFilter('trimPipes', content => {
return typeof content === 'string' ? content.replace(/^(\s|\|)/g, '').replace(/(\s|\|)$/g, '') : content;
});
eleventyConfig.addFilter('classNameToComponentName', className => {
let name = capitalCase(className.replace(/^Wa/, ''));
if (name === 'Qr Code') name = 'QR Code'; // manual override
return name;
});
eleventyConfig.addFilter('removeWaPrefix', tagName => {
return tagName.replace(/^wa-/, '');
});
//
// Transforms
//
eleventyConfig.addTransform('html-transform', function (content) {
// Parse the template and get a Document object
const doc = new JSDOM(content, {
// We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily
// identify which ones are internal and which ones are external.
url: `https://internal/`
}).window.document;
// DOM transforms
activeLinks(doc, { pathname: this.page.url });
anchorHeadings(doc, {
within: '#content .content__body',
levels: ['h2', 'h3', 'h4', 'h5']
});
tableOfContents(doc, {
levels: ['h2', 'h3'],
container: '#content .content__toc > ul',
within: '#content .content__body'
});
codePreviews(doc);
externalLinks(doc, { target: '_blank' });
highlightCodeBlocks(doc);
scrollingTables(doc);
copyCodeButtons(doc); // must be after codePreviews + highlightCodeBlocks
typography(doc, '#content');
replacer(doc, [
{ pattern: '%VERSION%', replacement: customElementsManifest.package.version },
{ pattern: '%CDNDIR%', replacement: cdndir },
{ pattern: '%NPMDIR%', replacement: npmdir }
]);
// Serialize the Document object to an HTML string and prepend the doctype
content = `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
// String transforms
content = prettier(content);
return content;
});
//
// Build a search index
//
eleventyConfig.on('eleventy.after', ({ results }) => {
// We only want to build the search index on the first run so all pages get indexed.
if (hasBuiltSearchIndex) {
return;
}
const map = {};
const searchIndexFilename = path.join(eleventyConfig.dir.output, assetsDir, 'search.json');
const lunrInput = path.resolve('../node_modules/lunr/lunr.min.js');
const lunrOutput = path.join(eleventyConfig.dir.output, assetsDir, 'scripts/lunr.js');
const searchIndex = lunr(function () {
// The search index uses these field names extensively, so shortening them can save some serious bytes. The
// initial index file went from 468 KB => 401 KB by using single-character names!
this.ref('id'); // id
this.field('t', { boost: 50 }); // title
this.field('h', { boost: 25 }); // headings
this.field('c'); // content
results.forEach((result, index) => {
const url = path
.join('/', path.relative(eleventyConfig.dir.output, result.outputPath))
.replace(/\\/g, '/') // convert backslashes to forward slashes
.replace(/\/index.html$/, '/'); // convert trailing /index.html to /
const doc = new JSDOM(result.content, {
// We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily
// identify which ones are internal and which ones are external.
url: `https://internal/`
}).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();
// 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 };
});
});
// Copy the Lunr search client and write the index
fs.mkdirSync(path.dirname(lunrOutput), { recursive: true });
fs.copyFileSync(lunrInput, lunrOutput);
fs.writeFileSync(searchIndexFilename, JSON.stringify({ searchIndex, map }), 'utf-8');
hasBuiltSearchIndex = true;
});
//
// Send a signal to stdout that let's the build know we've reached this point
//
eleventyConfig.on('eleventy.after', () => {
console.log('[eleventy.after]');
});
//
// Dev server options (see https://www.11ty.dev/docs/dev-server/#options)
//
eleventyConfig.setServerOptions({
domDiff: false, // disable dom diffing so custom elements don't break on reload,
port: 4000, // if port 4000 is taken, 11ty will use the next one available
watch: ['cdn/**/*'] // additional files to watch that will trigger server updates (array of paths or globs)
});
//
// 11ty config
//
return {
dir: {
input: 'pages',
output: '../_site',
includes: '../_includes' // resolved relative to the input dir
},
markdownTemplateEngine: 'njk', // use Nunjucks instead of Liquid for markdown files
templateEngineOverride: ['njk'] // just Nunjucks and then markdown
};
};

View File

@@ -1,19 +0,0 @@
---
meta:
title: Page Not Found
description: "The page you were looking for couldn't be found."
permalink: 404.html
toc: false
---
<div style="text-align: center;">
# Page Not Found
![A UFO takes one of the little worker monsters](/assets/images/undraw-taken.svg)
The page you were looking for couldn't be found.
Press [[/]] to search, or [head back to the homepage](/).
</div>

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View File

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 175 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -0,0 +1,125 @@
---
---
<script>
// double check we're marking links external
;(() => {
document.querySelectorAll("a[href^='http']").forEach((el) => {
const anchor = el as HTMLAnchorElement
if (anchor.hostname !== window.location.hostname) {
anchor.setAttribute("rel", "nofollow noreferrer noopener")
anchor.setAttribute("target", "_blank")
anchor.classList.add("external-link")
}
})
})()
// Add anchor headings
;(() => {
// This should probably be a proper plugin, but it requires adding the anchor heading directly in "ComponentLayout.astro" and
// writing a RemarkPlugin.
function updateAnchorHeadings () {
document.querySelectorAll(":is(h1,h2,h3,h4,h5,h6)[id]").forEach((headingEl) => {
if (headingEl.querySelector("a")) return
headingEl.classList.add("anchor-heading")
const anchor = document.createElement("a")
const visuallyHidden = document.createElement("wa-visually-hidden")
visuallyHidden.innerText = `Direct link to ${headingEl.textContent}`
anchor.setAttribute("href", `#${encodeURIComponent(headingEl.id)}`)
anchor.append(visuallyHidden)
headingEl.append(anchor)
})
}
document.addEventListener("turbo:load", updateAnchorHeadings)
})()
;(() => {
function setTurboFalse () {
// We don't control this markup, but we can programmatically add turbo false. The reason is it borks layouts otherwise.
document.querySelectorAll("a[href*='/experimental/themer']").forEach((el) => {
el.setAttribute("data-turbo", "false")
})
}
setTurboFalse()
document.addEventListener("turbo:load", setTurboFalse)
})()
//
// Open details when printing
//
;(() => {
const detailsOpenOnPrint = new Set();
window.addEventListener('beforeprint', () => {
detailsOpenOnPrint.clear();
document.querySelectorAll('details').forEach(details => {
if (details.open) {
detailsOpenOnPrint.add(details);
}
details.open = true;
});
});
window.addEventListener('afterprint', () => {
document.querySelectorAll('details').forEach(details => {
details.open = detailsOpenOnPrint.has(details);
});
detailsOpenOnPrint.clear();
});
})();
//
// Smooth links
//
;(() => {
document.addEventListener('click', event => {
const link = event.target.closest('a');
const id = (link?.hash ?? '').substr(1);
const isFragment = link?.hasAttribute('href') && link?.getAttribute('href').startsWith('#');
if (!link || !isFragment || link.getAttribute('data-smooth-link') === 'false') {
return;
}
// Scroll to the top
if (link.hash === '') {
event.preventDefault();
window.scroll({ top: 0, behavior: 'smooth' });
history.pushState(undefined, undefined, location.pathname);
}
// Scroll to an id
if (id) {
const target = document.getElementById(id);
if (target) {
event.preventDefault();
window.scroll({ top: target.offsetTop, behavior: 'smooth' });
history.pushState(undefined, undefined, `#${id}`);
}
}
});
})();
//
// Show custom versions in the sidebar
//
;(() => {
function updateVersion() {
const el = document.querySelector('.sidebar-version');
if (!el) return;
if (location.hostname === 'next.shoelace.style') el.textContent = 'Next';
if (location.hostname === 'localhost') el.textContent = 'Development';
}
updateVersion();
document.addEventListener('turbo:load', updateVersion);
})();
</script>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -0,0 +1,69 @@
<style>
.layout-widget {
position: fixed;
z-index: 9999;
background-color: white;
bottom: 4rem;
left: 4rem;
}
.layout-widget:not(:defined) {
display: none;
}
</style>
<wa-dropdown id="js-layout-widget" class="layout-widget" stay-open-on-select>
<wa-button slot="trigger" caret>Dropdown</wa-button>
<wa-menu></wa-menu>
</wa-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("wa-menu-item"), {
type: "checkbox",
textContent: `${type} ${slot}`
})
menuItem.setAttribute("value", `${type}-${slot}`)
return menuItem
}
document.querySelectorAll("wa-page > [slot]").forEach((el) => {
const slot = el.getAttribute("slot");
docFrag.append(makeMenuItem("toggle", slot), makeMenuItem("overflow", slot), document.createElement("wa-divider"))
})
docFrag.append(makeMenuItem("toggle", "main"), makeMenuItem("overflow", "main"))
layoutWidget.querySelector("wa-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(`wa-page > [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("wa-select", handleSelect);
</script>

View File

@@ -0,0 +1,35 @@
---
import PageLayout from "../layouts/PageLayout.astro"
import Themer from "./Themer.astro"
const { html, css, js, title, description } = Astro.props
---
<script>
import "light-pen"
</script>
<PageLayout frontmatter={{
description,
title
}}>
<div style="display:grid; grid-template-columns: minmax(0, 300px) minmax(0, 1fr);">
<Themer />
<light-pen style="height: 100%;" resize-position="30" preserve-whitespace>
<script type="text/plain" slot="html" set:html={
`<link id="theme-stylesheet" type="text/css" href="/dist/themes/default.css" rel="stylesheet">
<link id="applied-stylesheet" type="text/css" href="/dist/themes/applied.css" rel="stylesheet">
<script type="module" defer>import "/dist/webawesome.js"&lt;/script>
${html}`}>
</script>
<script type="text/plain" slot="css" set:html={`${css}`}>
</script>
<script type="text/plain" slot="js" set:html={''}>
</script>
</light-pen>
</div>
</PageLayout>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
---
---
<script>
import * as Turbo from '@hotwired/turbo';
;(() => {
if (!window.scrollPositions) {
window.scrollPositions = {};
}
const preservedScrollQuery = '[id][data-preserve-scroll], [id="starlight__sidebar"]'
function preserveScroll() {
document.querySelectorAll(preservedScrollQuery).forEach(element => {
scrollPositions[element.id] = element.scrollTop;
});
}
function restoreScroll(event) {
document.querySelectorAll(preservedScrollQuery).forEach(element => {
element.scrollTop = scrollPositions[element.id];
});
if (event.detail && event.detail.newBody) {
event.detail.newBody.querySelectorAll(preservedScrollQuery).forEach(element => {
element.scrollTop = scrollPositions[element.id];
});
}
}
window.addEventListener('turbo:before-cache', preserveScroll);
window.addEventListener('turbo:before-render', restoreScroll);
window.addEventListener('turbo:render', restoreScroll);
})();
</script>

View File

@@ -0,0 +1,57 @@
---
import type { Props } from '@astrojs/starlight/props';
import { default as AstroHead } from '@astrojs/starlight/components/Head.astro';
import Turbo from "../Turbo.astro"
import Docs from "../Docs.astro"
import Search from "../Search.astro"
import '../../styles/global.css'
import '../../styles/syntax-highlight.css'
import '../../styles/code-previews.css'
import '../../styles/font-awesome.css'
import { customElementsManifest } from '../../js/cem';
const version = customElementsManifest().package.version
---
<script>
import "../../../../dist/webawesome.js"
import "../../js/code-previews.js"
import "../../js/theme.js"
</script>
<style is:inline>
/* Override starlight selector for auto injecting margin since it causes a lot of issues. */
body .sl-markdown-content :not(a, strong, em, del, span, input, code, h1, h2, h3, h4, h5, h6) + :not(a, strong, em, del, span, input, code, h1, h2, h3, h4, h5, h6, :where(.not-content *)) {
margin-top: 0;
}
/** Applied styles overrides for docs. */
body details {
background-color: transparent;
margin-block-end: 0;
}
body details summary::before {
display: none;
}
</style>
<>
<Turbo />
<Docs />
{/* <Search /> */}
<meta name="wa-version" content={version} />
<meta name="turbo-cache-control" content="no-preview">
<link id="theme-stylesheet" rel="stylesheet" href="/dist/themes/default.css" />
<link rel="stylesheet" href="/dist/themes/applied.css" />
<link rel="stylesheet" href="/dist/themes/forms.css" />
<AstroHead {...Astro.props}>
<slot />
</AstroHead>
</>

View File

@@ -0,0 +1,570 @@
---
import '@pagefind/default-ui/css/ui.css';
import { Icon } from '@astrojs/starlight/components';
import type { Props } from '@astrojs/starlight/props';
const { labels } = Astro.props;
const pagefindTranslations = {
placeholder: labels['search.label'],
...Object.fromEntries(
Object.entries(labels)
.filter(([key]) => key.startsWith('pagefind.'))
.map(([key, value]) => [key.replace('pagefind.', ''), value])
),
};
---
<site-search
data-translations={JSON.stringify(pagefindTranslations)}
data-strip-trailing-slash={false}
>
<button data-open-modal disabled>
{
/* The span is `aria-hidden` because it is not shown on small screens. Instead, the icon label is used for accessibility purposes. */
}
<Icon name="magnifier" label={labels['search.label']} />
<span class="sl-hidden md:sl-block" aria-hidden="true">{labels['search.label']}</span>
<Icon
name="forward-slash"
class="sl-hidden md:sl-block"
label={labels['search.shortcutLabel']}
/>
</button>
<dialog style="padding:0" aria-label={labels['search.label']}>
<div class="dialog-frame sl-flex">
{
/* TODO: Make the layout of this button flexible to accommodate different word lengths. Currently hard-coded for English: “Cancel” */
}
<button data-close-modal class="sl-flex md:sl-hidden">
{labels['search.cancelLabel']}
</button>
{
<div class="search-container">
<div id="starlight__search" />
</div>
}
</div>
</dialog>
</site-search>
<script>
class SiteSearch extends HTMLElement {
connectedCallback() {
// super.connectedCallback();
const openBtn = () => this.querySelector<HTMLButtonElement>('button[data-open-modal]')!;
const closeBtn = () => this.querySelector<HTMLButtonElement>('button[data-close-modal]')!;
const dialog = () => this.querySelector('dialog')!;
const dialogFrame = () => this.querySelector('.dialog-frame')!;
/** Close the modal if a user clicks on a link or outside of the modal. */
const onClick = (event: MouseEvent) => {
const isLink = 'href' in (event.target || {});
if (
isLink ||
(document.body.contains(event.target as Node) &&
!dialogFrame().contains(event.target as Node))
) {
closeModal();
}
};
const openModal = (event?: MouseEvent) => {
dialog().showModal();
document.body.toggleAttribute('data-search-modal-open', true);
this.querySelector('input')?.focus();
event?.stopPropagation();
window.addEventListener('click', onClick);
};
const closeModal = () => dialog().close();
openBtn().addEventListener('click', openModal);
openBtn().disabled = false;
closeBtn().addEventListener('click', closeModal);
dialog().addEventListener('close', () => {
document.body.toggleAttribute('data-search-modal-open', false);
window.removeEventListener('click', onClick);
});
// Listen for `/` and `cmd + k` keyboard shortcuts.
window.addEventListener('keydown', (e) => {
const isInput =
document.activeElement instanceof HTMLElement &&
(['input', 'select', 'textarea'].includes(document.activeElement.tagName.toLowerCase()) ||
document.activeElement.isContentEditable);
if (e.metaKey === true && e.key === 'k') {
dialog().open ? closeModal() : openModal();
e.preventDefault();
} else if (e.key === '/' && !dialog().open && !isInput) {
openModal();
e.preventDefault();
}
});
let translations = {};
try {
translations = JSON.parse(this.dataset.translations || '{}');
} catch {}
const shouldStrip = this.dataset.stripTrailingSlash !== undefined;
const stripTrailingSlash = (path: string) => path.replace(/(.)\/(#.*)?$/, '$1$2');
const formatURL = shouldStrip ? stripTrailingSlash : (path: string) => path;
document.querySelector("#starlight__search").innerHTML = ""
window.addEventListener('turbo:load', () => {
document.querySelector("#starlight__search").innerHTML = ""
const onIdle = window.requestIdleCallback || ((cb) => setTimeout(cb, 1));
onIdle(async () => {
if (import.meta.env.DEV) {
// Generate a fake search in dev by calling a JSON endpoint that generates the search.
await fetch(import.meta.env.BASE_URL.replace(/\/$/, '') + '/pagefind-dev.json')
}
// @ts-expect-error — Missing types for @pagefind/default-ui package.
const { PagefindUI } = await import('@pagefind/default-ui');
document.querySelector("#starlight__search").innerHTML = ""
new PagefindUI({
element: '#starlight__search',
baseUrl: import.meta.env.BASE_URL,
bundlePath: import.meta.env.BASE_URL.replace(/\/$/, '') + '/pagefind/',
showImages: false,
translations,
showSubResults: true,
processResult: (result: { url: string; sub_results: Array<{ url: string }> }) => {
result.url = formatURL(result.url);
result.sub_results = result.sub_results.map((sub_result) => {
sub_result.url = formatURL(sub_result.url);
return sub_result;
});
},
});
});
});
let links: (HTMLAnchorElement | HTMLButtonElement)[] = []
function findSelectionIndex () {
let index = links.findIndex((link) => link.getAttribute("aria-selected") === "true")
return index
}
function selectPrevious () {
selectInDirection("backward")
}
function selectInDirection (direction: "forward" | "backward") {
const index = findSelectionIndex()
const step = direction === "forward" ? 1 : -1
const isOutOfRange = !(index + step < links.length && index + step >= 0)
if (isOutOfRange) {
return
}
const prevLink = links[index]
if (prevLink) {
prevLink.removeAttribute("aria-selected")
let prevResult = prevLink.closest(".pagefind-ui__result-nested, .pagefind-ui__button")
if (!prevResult) {
prevResult = prevLink.closest(".pagefind-ui__result-title")
}
if (prevResult) {
prevResult.classList.remove("is-active")
}
}
const nextLink = links[index + step]
nextLink.setAttribute("aria-selected", "true")
nextLink.scrollIntoView({ block: "center" })
let nextResult = nextLink.closest(".pagefind-ui__result-nested, .pagefind-ui__button")
if (!nextResult) {
nextResult = nextLink.closest(".pagefind-ui__result-title")
}
if (nextResult) {
nextResult.classList.add("is-active")
}
}
function selectNext () {
selectInDirection("forward")
}
function handleKeyDown(e) {
if (e.key === "Enter") {
e.preventDefault()
const currentItem = links[findSelectionIndex()]
// When we're on the page find UI button, we want to jump to the previous result before showing more results.
if (currentItem.tagName === "BUTTON") {
selectPrevious()
}
currentItem.click()
}
if (e.key === "ArrowUp") {
e.preventDefault()
selectPrevious()
return
}
if (e.key === "ArrowDown") {
e.preventDefault()
selectNext()
return
}
}
const observerOptions = {
childList: true,
subtree: true,
};
const search = document.querySelector("#starlight__search")?.closest("dialog")
function debounce(this: any, func: (...args: any[]) => unknown, timeout = 300){
let timer: ReturnType<typeof setTimeout>;
return (...args: any[]) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
const setFirstSelection = debounce(() => {
if (links.length > 0 && links[0].tagName !== "BUTTON" && findSelectionIndex() === -1) {
selectNext()
}
}, 30)
if (search) {
search.addEventListener("keydown", handleKeyDown)
const resultObserver = new MutationObserver(() => {
links = Array.from(document.querySelectorAll(".pagefind-ui__result-link"))
.concat(Array.from(document.querySelectorAll(".pagefind-ui__button")))
for (const link of links) {
if (!link.hasAttribute("aria-selected")) {
link.removeAttribute("aria-selected")
}
link.setAttribute("tabindex", "-1")
}
setFirstSelection()
})
resultObserver.observe(search, observerOptions)
}
}
}
customElements.define('site-search', SiteSearch);
// :is(.pagefind-ui__result-title, .pagefind-ui__result-nested) .pagefind-ui__result-link {}
</script>
<style>
site-search {
display: contents;
}
button[data-open-modal] {
display: flex;
align-items: center;
gap: 0.5rem;
border: 0;
background-color: transparent;
color: var(--sl-color-gray-1);
cursor: pointer;
height: 2.5rem;
font-size: var(--sl-text-xl);
}
@media (min-width: 50rem) {
button[data-open-modal] {
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.5rem;
padding-inline-start: 0.75rem;
padding-inline-end: 1rem;
background-color: var(--sl-color-black);
color: var(--sl-color-gray-2);
font-size: var(--sl-text-sm);
width: 100%;
max-width: 22rem;
}
button[data-open-modal]:hover {
border-color: var(--sl-color-gray-2);
color: var(--sl-color-white);
}
button[data-open-modal] > :last-child {
margin-inline-start: auto;
}
}
dialog {
margin: 0;
background-color: var(--sl-color-gray-6);
border: 1px solid var(--sl-color-gray-5);
width: 100%;
max-width: 100%;
height: 100%;
max-height: 100%;
box-shadow: var(--sl-shadow-lg);
}
dialog[open] {
display: grid;
}
dialog::backdrop {
background-color: var(--sl-color-backdrop-overlay);
-webkit-backdrop-filter: blur(0.25rem);
backdrop-filter: blur(0.25rem);
}
.dialog-frame {
flex-direction: column;
gap: 1rem;
padding: 1rem;
}
button[data-close-modal] {
position: absolute;
z-index: 1;
align-items: center;
align-self: flex-end;
height: calc(64px * var(--pagefind-ui-scale));
padding: 0.25rem;
border: 0;
background: transparent;
cursor: pointer;
color: var(--sl-color-text-accent);
}
#starlight__search {
--pagefind-ui-primary: var(--sl-color-accent-light);
--pagefind-ui-text: var(--sl-color-gray-2);
--pagefind-ui-font: var(--__sl-font);
--pagefind-ui-background: var(--sl-color-black);
--pagefind-ui-border: var(--sl-color-gray-5);
--pagefind-ui-border-width: 1px;
--sl-search-cancel-space: 5rem;
}
@media (min-width: 50rem) {
#starlight__search {
--sl-search-cancel-space: 0px;
}
dialog {
margin: 4rem auto auto;
border-radius: 0.5rem;
width: 90%;
max-width: 40rem;
height: max-content;
min-height: 15rem;
max-height: calc(100% - 8rem);
}
.dialog-frame {
padding: 1.5rem;
}
}
</style>
<style is:global>
[data-search-modal-open] {
overflow: hidden;
}
#starlight__search {
--sl-search-result-spacing: calc(1.25rem * var(--pagefind-ui-scale));
--sl-search-result-pad-inline-start: calc(3.75rem * var(--pagefind-ui-scale));
--sl-search-result-pad-inline-end: calc(1.25rem * var(--pagefind-ui-scale));
--sl-search-result-pad-block: calc(0.9375rem * var(--pagefind-ui-scale));
--sl-search-result-nested-pad-block: calc(0.625rem * var(--pagefind-ui-scale));
--sl-search-corners: calc(0.3125rem * var(--pagefind-ui-scale));
--sl-search-page-icon-size: calc(1.875rem * var(--pagefind-ui-scale));
--sl-search-page-icon-inline-start: calc(
(var(--sl-search-result-pad-inline-start) - var(--sl-search-page-icon-size)) / 2
);
--sl-search-tree-diagram-size: calc(2.5rem * var(--pagefind-ui-scale));
--sl-search-tree-diagram-inline-start: calc(
(var(--sl-search-result-pad-inline-start) - var(--sl-search-tree-diagram-size)) / 2
);
}
#starlight__search .pagefind-ui__form::before {
--pagefind-ui-text: var(--sl-color-gray-1);
opacity: 1;
}
#starlight__search .pagefind-ui__search-input {
color: var(--sl-color-white);
font-weight: 400;
width: calc(100% - var(--sl-search-cancel-space));
}
#starlight__search input:focus {
--pagefind-ui-border: var(--sl-color-accent);
}
#starlight__search .pagefind-ui__search-clear {
inset-inline-end: var(--sl-search-cancel-space);
width: calc(60px * var(--pagefind-ui-scale));
padding: 0;
background-color: transparent;
overflow: hidden;
}
#starlight__search .pagefind-ui__search-clear:focus {
outline: 1px solid var(--sl-color-accent);
}
#starlight__search .pagefind-ui__search-clear::before {
content: '';
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m13.41 12 6.3-6.29a1 1 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1 1 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 .33 1.64 1 1 0 0 0 1.09-.22l6.29-6.3 6.29 6.3a1 1 0 0 0 1.64-.33 1 1 0 0 0-.22-1.09L13.41 12Z'/%3E%3C/svg%3E")
center / 50% no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m13.41 12 6.3-6.29a1 1 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1 1 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 .33 1.64 1 1 0 0 0 1.09-.22l6.29-6.3 6.29 6.3a1 1 0 0 0 1.64-.33 1 1 0 0 0-.22-1.09L13.41 12Z'/%3E%3C/svg%3E")
center / 50% no-repeat;
background-color: var(--sl-color-text-accent);
display: block;
width: 100%;
height: 100%;
}
#starlight__search .pagefind-ui__results-area {
overflow: auto;
max-height: 70vh;
padding: 8px;
}
#starlight__search .pagefind-ui__results > * + * {
margin-top: var(--sl-search-result-spacing);
}
#starlight__search .pagefind-ui__result {
border: 0;
padding: 0;
}
#starlight__search .pagefind-ui__result-nested {
position: relative;
padding: var(--sl-search-result-nested-pad-block) var(--sl-search-result-pad-inline-end);
padding-inline-start: var(--sl-search-result-pad-inline-start);
}
#starlight__search .pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)),
#starlight__search .pagefind-ui__result-nested {
position: relative;
background-color: var(--sl-color-black);
}
#starlight__search .pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):hover,
#starlight__search
.pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):is(.is-active, :focus-within),
#starlight__search .pagefind-ui__result-nested:hover,
#starlight__search .pagefind-ui__result-nested:is(.is-active, :focus-within),
#starlight__search .pagefind-ui__button.is-active {
outline: 1px solid var(--sl-color-accent-high);
}
#starlight__search
.pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):is(.is-active, :focus-within),
#starlight__search .pagefind-ui__result-nested:is(.is-active, :focus-within),
#starlight__search .pagefind-ui__button.is-active {
background-color: var(--sl-color-accent-low);
}
#starlight__search .pagefind-ui__result-thumb,
#starlight__search .pagefind-ui__result-inner {
margin-top: 0;
}
#starlight__search .pagefind-ui__result-inner > :first-child {
border-radius: var(--sl-search-corners) var(--sl-search-corners) 0 0;
}
#starlight__search .pagefind-ui__result-inner > :last-child {
border-radius: 0 0 var(--sl-search-corners) var(--sl-search-corners);
}
#starlight__search .pagefind-ui__result-inner > .pagefind-ui__result-title {
padding: var(--sl-search-result-pad-block) var(--sl-search-result-pad-inline-end);
padding-inline-start: var(--sl-search-result-pad-inline-start);
}
#starlight__search .pagefind-ui__result-inner > .pagefind-ui__result-title::before {
content: '';
position: absolute;
inset-block: 0;
inset-inline-start: var(--sl-search-page-icon-inline-start);
width: var(--sl-search-page-icon-size);
background: var(--sl-color-gray-3);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24'%3E%3Cpath d='M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3V8l-6-6a1 1 0 0 0-1 0H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V9Zm-6-4 3 3h-2a1 1 0 0 1-1-1V5Zm4 14a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z'/%3E%3C/svg%3E")
center no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24'%3E%3Cpath d='M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3V8l-6-6a1 1 0 0 0-1 0H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V9Zm-6-4 3 3h-2a1 1 0 0 1-1-1V5Zm4 14a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z'/%3E%3C/svg%3E")
center no-repeat;
}
#starlight__search .pagefind-ui__result-inner {
align-items: stretch;
gap: 1px;
}
#starlight__search .pagefind-ui__result-link {
position: unset;
--pagefind-ui-text: var(--sl-color-white);
font-weight: 600;
}
#starlight__search .pagefind-ui__result-link:hover {
text-decoration: none;
}
#starlight__search .pagefind-ui__result-nested .pagefind-ui__result-link::before {
content: unset;
}
#starlight__search .pagefind-ui__result-nested::before {
content: '';
position: absolute;
inset-block: 0;
inset-inline-start: var(--sl-search-tree-diagram-inline-start);
width: var(--sl-search-tree-diagram-size);
background: var(--sl-color-gray-4);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' viewBox='0 0 16 1000' preserveAspectRatio='xMinYMin slice'%3E%3Cpath d='M8 0v1000m6-988H8'/%3E%3C/svg%3E")
0% 0% / 100% no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' viewBox='0 0 16 1000' preserveAspectRatio='xMinYMin slice'%3E%3Cpath d='M8 0v1000m6-988H8'/%3E%3C/svg%3E")
0% 0% / 100% no-repeat;
}
#starlight__search .pagefind-ui__result-nested:last-child::before {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' viewBox='0 0 16 16'%3E%3Cpath d='M8 0v12m6 0H8'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' viewBox='0 0 16 16'%3E%3Cpath d='M8 0v12m6 0H8'/%3E%3C/svg%3E");
}
/* Flip page and tree icons around the vertical axis when in an RTL layout. */
[dir='rtl'] .pagefind-ui__result-title::before,
[dir='rtl'] .pagefind-ui__result-nested::before {
transform: matrix(-1, 0, 0, 1, 0, 0);
}
#starlight__search .pagefind-ui__result-link::after {
content: '';
position: absolute;
inset: 0;
}
#starlight__search .pagefind-ui__result-excerpt {
font-size: calc(1rem * var(--pagefind-ui-scale));
overflow-wrap: anywhere;
}
#starlight__search mark {
color: var(--sl-color-gray-2);
background-color: transparent;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,63 @@
---
import { default as StarlightTOC } from '@astrojs/starlight/components/TableOfContents.astro';
import type { Props } from "@astrojs/starlight/props"
const { labels, toc, slug, entry, id } = Astro.props;
import { getComponentFromFileName } from "../../js/cem.js"
if (slug.includes("components/")) {
const component = getComponentFromFileName(slug + ".md")
const {
hasSlots,
hasProperties,
hasEvents,
hasMethods,
hasCssProperties,
hasCssParts,
hasAnimations,
hasDependencies,
} = component
toc.items.push({
text: "Importing",
slug: "importing",
depth: 0,
children: []
})
const strToText = {
CssProperties: "Custom Properties",
CssParts: "Parts",
}
const strToId = {
CssProperties: "custom-properties",
CssParts: "parts",
}
;[
"Slots",
"Properties",
"Events",
"Methods",
"CssProperties",
"CssParts",
"Animations",
"Dependencies",
].forEach((str) => {
if (component[`has${str}`]) {
toc.items.push({
text: strToText[str] || str,
slug: strToId[str] || str.toLowerCase(),
depth: 0,
children: []
})
}
})
}
---
<StarlightTOC {...Astro.props} />

View File

@@ -0,0 +1,16 @@
import { defineCollection, z } from 'astro:content';
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({
schema: docsSchema({
extend: z.object({
// Add a new field to the schema.
category: z
.enum(['components', 'experimental', 'frameworks', 'getting-started', 'resources', 'tokens', 'tutorials'])
.optional()
})
})
}),
i18n: defineCollection({ type: 'data', schema: i18nSchema() })
};

View File

@@ -0,0 +1,28 @@
---
title: Page Not Found
description: "The page you were looking for couldn't be found."
tableOfContents: false
pageTitle: false
---
<div style="text-align: center;">
# Page Not Found
![A UFO takes one of the little worker monsters](/assets/images/undraw-taken.svg)
The page you were looking for couldn't be found.
Press <kbd>/</kbd> to search, or [head back to the homepage](/).
</div>
<style>
.content-panel + .content-panel {
border: none;
}
.content-panel:first-of-type {
display: none;
}
</style>

View File

@@ -1,8 +1,7 @@
---
meta:
title: Alert
description: Alerts are used to display important messages inline or as toast notifications.
layout: component
title: Alert
description: Alerts are used to display important messages inline or as toast notifications.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview
@@ -10,6 +9,15 @@ layout: component
<wa-icon slot="icon" name="circle-info" variant="regular"></wa-icon>
This is a standard alert. You can customize its content and even the icon.
</wa-alert>
<wa-alert open>
<wa-icon slot="icon" name="circle-info" variant="regular"></wa-icon>
<div style="border: solid 2px tomato; height: 2rem;"></div>
</wa-alert>
<wa-alert open>
<div style="border: solid 2px tomato; height: 2rem;"></div>
</wa-alert>
```
```jsx:react
@@ -24,7 +32,7 @@ const App = () => (
);
```
:::warning
:::caution
Alerts will not be visible if the `open` attribute is not present.
:::

View File

@@ -1,8 +1,7 @@
---
meta:
title: Animated Image
description: A component for displaying animated GIFs and WEBPs that play and pause on interaction.
layout: component
title: Animated Image
description: A component for displaying animated GIFs and WEBPs that play and pause on interaction.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview
@@ -61,8 +60,6 @@ To set a custom size, apply a width and/or height to the host element.
</wa-animated-image>
```
{% raw %}
```jsx:react
import WaAnimatedImage from '@shoelace-style/shoelace/dist/react/animated-image';
@@ -75,8 +72,6 @@ const App = () => (
);
```
{% endraw %}
### Customizing the Control Box
You can change the appearance and location of the control box by targeting the `control-box` part in your styles.

View File

@@ -1,8 +1,7 @@
---
meta:
title: Animation
description: Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes.
layout: component
title: Animation
description: Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes.
layout: ../../../layouts/ComponentLayout.astro
---
To animate an element, wrap it in `<wa-animation>` and set an animation `name`. The animation will not start until you add the `play` attribute. Refer to the [properties table](#properties) for a list of all animation options.
@@ -80,7 +79,8 @@ This example demonstrates all of the baked-in animations and easings. Animations
<div class="controls">
<wa-select label="Animation" value="bounce"></wa-select>
<wa-select label="Easing" value="linear"></wa-select>
<wa-input label="Playback Rate" type="number" min="0" max="2" step=".25" value="1"></wa-input>
<wa-input label="Playback Rate" type="number" min="0" max="2" step=".25" value="1">
</wa-input>
</div>
</div>
@@ -134,6 +134,10 @@ This example demonstrates all of the baked-in animations and easings. Animations
</style>
```
```jsx:react
```
### Using Intersection Observer
Use an [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to control the animation when an element enters or exits the viewport. For example, scroll the box below in and out of your screen. The animation stops when the box exits the viewport and restarts each time it enters the viewport.

View File

@@ -1,8 +1,7 @@
---
meta:
title: Avatar
description: Avatars are used to represent a person or object.
layout: component
title: Avatar
description: Avatars are used to represent a person or object.
layout: ../../../layouts/ComponentLayout.astro
---
By default, a generic icon will be shown. You can personalize avatars by adding custom icons, initials, and images. You should always provide a `label` for assistive devices.

View File

@@ -1,8 +1,7 @@
---
meta:
title: Badge
description: Badges are used to draw attention and display statuses or counts.
layout: component
title: Badge
description: Badges are used to draw attention and display statuses or counts.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview
@@ -154,8 +153,6 @@ One of the most common use cases for badges is attaching them to buttons. To mak
</wa-button>
```
{% raw %}
```jsx:react
import WaBadge from '@shoelace-style/shoelace/dist/react/badge';
import WaButton from '@shoelace-style/shoelace/dist/react/button';
@@ -184,8 +181,6 @@ const App = () => (
);
```
{% endraw %}
### With Menu Items
When including badges in menu items, use the `suffix` slot to make sure they're aligned correctly.
@@ -198,8 +193,6 @@ When including badges in menu items, use the `suffix` slot to make sure they're
</wa-menu>
```
{% raw %}
```jsx:react
import WaBadge from '@shoelace-style/shoelace/dist/react/badge';
import WaButton from '@shoelace-style/shoelace/dist/react/button';
@@ -227,5 +220,3 @@ const App = () => (
</WaMenu>
);
```
{% endraw %}

View File

@@ -1,8 +1,7 @@
---
meta:
title: Breadcrumb Item
description: Breadcrumb Items are used inside breadcrumbs to represent different links.
layout: component
title: Breadcrumb Item
description: Breadcrumb Items are used inside breadcrumbs to represent different links.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview

View File

@@ -1,8 +1,7 @@
---
meta:
title: Breadcrumb
description: Breadcrumbs provide a group of links so users can easily navigate a website's hierarchy.
layout: component
title: Breadcrumb
description: Breadcrumbs provide a group of links so users can easily navigate a website's hierarchy.
layout: ../../../layouts/ComponentLayout.astro
---
Breadcrumbs are usually placed before a page's main content with the current page shown last to indicate the user's position in the navigation.

View File

@@ -1,8 +1,7 @@
---
meta:
title: Button Group
description: Button groups can be used to group related buttons into sections.
layout: component
title: Button Group
description: Button groups can be used to group related buttons into sections.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview

View File

@@ -1,8 +1,7 @@
---
meta:
title: Button
description: Buttons represent actions that are available to the user.
layout: component
title: Button
description: Buttons represent actions that are available to the user.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview
@@ -201,8 +200,6 @@ As expected, buttons can be given a custom width by setting the `width` attribut
<wa-button size="large" style="width: 100%;">Large</wa-button>
```
{% raw %}
```jsx:react
import WaButton from '@shoelace-style/shoelace/dist/react/button';
@@ -221,8 +218,6 @@ const App = () => (
);
```
{% endraw %}
### Prefix and Suffix Icons
Use the `prefix` and `suffix` slots to add icons.

View File

@@ -1,12 +1,11 @@
---
meta:
title: Card
description: Cards can be used to group related subjects in a container.
layout: component
title: Card
description: Cards can be used to group related subjects in a container.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview
<wa-card class="card-overview">
<wa-card with-image with-footer class="card-overview">
<img
slot="image"
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
@@ -63,7 +62,7 @@ const css = `
const App = () => (
<>
<WaCard className="card-overview">
<WaCard with-image with-footer className="card-overview">
<img
slot="image"
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
@@ -130,7 +129,7 @@ const App = () => (
Headers can be used to display titles and more.
```html:preview
<wa-card class="card-header">
<wa-card with-header class="card-header">
<div slot="header">
Header Title
<wa-icon-button name="gear" variant="solid" label="Settings"></wa-icon-button>
@@ -186,7 +185,7 @@ const css = `
const App = () => (
<>
<WaCard className="card-header">
<WaCard with-header className="card-header">
<div slot="header">
Header Title
<WaIconButton name="gear" variant="solid"></WaIconButton>
@@ -204,7 +203,7 @@ const App = () => (
Footers can be used to display actions, summaries, or other relevant content.
```html:preview
<wa-card class="card-footer">
<wa-card with-footer class="card-footer">
This card has a footer. You can put all sorts of things in it!
<div slot="footer">
@@ -245,7 +244,7 @@ const css = `
const App = () => (
<>
<WaCard className="card-footer">
<WaCard with-footer className="card-footer">
This card has a footer. You can put all sorts of things in it!
<div slot="footer">
<WaRating></WaRating>
@@ -265,7 +264,7 @@ const App = () => (
Cards accept an `image` slot. The image is displayed atop the card and stretches to fit.
```html:preview
<wa-card class="card-image">
<wa-card with-image class="card-image">
<img
slot="image"
src="https://images.unsplash.com/photo-1547191783-94d5f8f6d8b1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=80"
@@ -292,7 +291,7 @@ const css = `
const App = () => (
<>
<WaCard className="card-image">
<WaCard with-image className="card-image">
<img
slot="image"
src="https://images.unsplash.com/photo-1547191783-94d5f8f6d8b1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=80"

View File

@@ -1,8 +1,7 @@
---
meta:
title: Carousel Item
description: A carousel item represent a slide within a carousel.
layout: component
title: Carousel Item
description: A carousel item represent a slide within a carousel.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview

View File

@@ -1,8 +1,7 @@
---
meta:
title: Carousel
description: Carousels display an arbitrary number of content slides along a horizontal or vertical axis.
layout: component
title: Carousel
description: Carousels display an arbitrary number of content slides along a horizontal or vertical axis.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview
@@ -527,8 +526,6 @@ The `slides-per-page` attribute makes it possible to display multiple slides at
</wa-carousel>
```
{% raw %}
```jsx:react
import WaCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import WaCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
@@ -545,8 +542,6 @@ const App = () => (
);
```
{% endraw %}
### Adding and Removing Slides
The content of the carousel can be changed by adding or removing carousel items. The carousel will update itself automatically.
@@ -619,8 +614,6 @@ The content of the carousel can be changed by adding or removing carousel items.
</script>
```
{% raw %}
```jsx:react
import { useState } from 'react';
import WaCarousel from '@shoelace-style/shoelace/dist/react/carousel';
@@ -680,8 +673,6 @@ const App = () => {
};
```
{% endraw %}
### Vertical Scrolling
Setting the `orientation` attribute to `vertical` will render the carousel in a vertical layout. If the content of your slides vary in height, you will need to set amn explicit `height` or `max-height` on the carousel using CSS.
@@ -859,8 +850,6 @@ Use the `--aspect-ratio` custom property to customize the size of the carousel's
</script>
```
{% raw %}
```jsx:react
import { useState } from 'react';
import WaCarousel from '@shoelace-style/shoelace/dist/react/carousel';
@@ -926,8 +915,6 @@ const App = () => {
};
```
{% endraw %}
### Scroll Hint
Use the `--scroll-hint` custom property to add inline padding in horizontal carousels and block padding in vertical carousels. This will make the closest slides slightly visible, hinting that there are more items in the carousel.
@@ -967,8 +954,6 @@ Use the `--scroll-hint` custom property to add inline padding in horizontal caro
</wa-carousel>
```
{% raw %}
```jsx:react
import { useState } from 'react';
import WaCarousel from '@shoelace-style/shoelace/dist/react/carousel';
@@ -1014,8 +999,6 @@ const App = () => (
);
```
{% endraw %}
### Gallery Example
The carousel has a robust API that makes it possible to extend and customize. This example syncs the active slide with a set of thumbnails, effectively creating a gallery-style carousel.

View File

@@ -1,8 +1,7 @@
---
meta:
title: Checkbox
description: Checkboxes allow the user to toggle an option on or off.
layout: component
title: Checkbox
description: Checkboxes allow the user to toggle an option on or off.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview
@@ -89,6 +88,18 @@ const App = () => (
);
```
### Help Text
Add descriptive help text to a switch with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
```html:preview
<wa-checkbox help-text="What should the user know about the checkbox?">Label</wa-checkbox>
```
````jsx:react
import WaCheckbox from '@shoelace-style/shoelace/dist/react/checkbox';
const App = () => <WaCheckbox help-text="What should the user know about the switch?">Label</WaCheckbox>;
### Custom Validity
Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string.
@@ -123,9 +134,7 @@ Use the `setCustomValidity()` method to set a custom validation message. This wi
});
});
</script>
```
{% raw %}
````
```jsx:react
import { useEffect, useRef } from 'react';
@@ -162,5 +171,3 @@ const App = () => {
);
};
```
{% endraw %}

View File

@@ -1,8 +1,7 @@
---
meta:
title: Color Picker
description: Color pickers allow the user to select a color.
layout: component
title: Color Picker
description: Color pickers allow the user to select a color.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview

View File

@@ -1,8 +1,7 @@
---
meta:
title: Copy Button
description: Copies data to the clipboard when the user clicks the button.
layout: component
title: Copy Button
description: Copies data to the clipboard when the user clicks the button.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview

View File

@@ -1,8 +1,7 @@
---
meta:
title: Details
description: Details show a brief summary and expand to show additional content.
layout: component
title: Details
description: Details show a brief summary and expand to show additional content.
layout: ../../../layouts/ComponentLayout.astro
---
<!-- cspell:dictionaries lorem-ipsum -->

View File

@@ -1,14 +1,13 @@
---
meta:
title: Dialog
description: 'Dialogs, sometimes called "modals", appear above the page and require the user''s immediate attention.'
layout: component
title: Dialog
description: 'Dialogs, sometimes called "modals", appear above the page and require the user''s immediate attention.'
layout: ../../../layouts/ComponentLayout.astro
---
<!-- cspell:dictionaries lorem-ipsum -->
```html:preview
<wa-dialog label="Dialog" class="dialog-overview">
<wa-dialog label="Dialog" with-header with-footer class="dialog-overview">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand">Close</wa-button>
</wa-dialog>
@@ -35,7 +34,7 @@ const App = () => {
return (
<>
<WaDialog label="Dialog" open={open} onWaAfterHide={() => setOpen(false)}>
<WaDialog label="Dialog" with-header with-footer open={open} onWaAfterHide={() => setOpen(false)}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<WaButton slot="footer" variant="brand" onClick={() => setOpen(false)}>
Close
@@ -55,7 +54,7 @@ const App = () => {
Use the `--width` custom property to set the dialog's width.
```html:preview
<wa-dialog label="Dialog" class="dialog-width" style="--width: 50vw;">
<wa-dialog label="Dialog" with-header with-footer class="dialog-width" style="--width: 50vw;">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand">Close</wa-button>
</wa-dialog>
@@ -72,8 +71,6 @@ Use the `--width` custom property to set the dialog's width.
</script>
```
{% raw %}
```jsx:react
import { useState } from 'react';
import WaButton from '@shoelace-style/shoelace/dist/react/button';
@@ -84,7 +81,7 @@ const App = () => {
return (
<>
<WaDialog label="Dialog" open={open} style={{ '--width': '50vw' }} onWaAfterHide={() => setOpen(false)}>
<WaDialog label="Dialog" with-header with-footer open={open} style={{ '--width': '50vw' }} onWaAfterHide={() => setOpen(false)}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<WaButton slot="footer" variant="brand" onClick={() => setOpen(false)}>
Close
@@ -97,14 +94,12 @@ const App = () => {
};
```
{% endraw %}
### Scrolling
By design, a dialog's height will never exceed that of the viewport. As such, dialogs will not scroll with the page ensuring the header and footer are always accessible to the user.
```html:preview
<wa-dialog label="Dialog" class="dialog-scrolling">
<wa-dialog label="Dialog" with-header with-footer class="dialog-scrolling">
<div style="height: 150vh; border: dashed 2px var(--wa-color-surface-border); padding: 0 1rem;">
<p>Scroll down and give it a try! 👇</p>
</div>
@@ -123,8 +118,6 @@ By design, a dialog's height will never exceed that of the viewport. As such, di
</script>
```
{% raw %}
```jsx:react
import { useState } from 'react';
import WaButton from '@shoelace-style/shoelace/dist/react/button';
@@ -135,7 +128,7 @@ const App = () => {
return (
<>
<WaDialog label="Dialog" open={open} onWaAfterHide={() => setOpen(false)}>
<WaDialog label="Dialog" with-header with-footer open={open} onWaAfterHide={() => setOpen(false)}>
<div
style={{
height: '150vh',
@@ -157,14 +150,12 @@ const App = () => {
};
```
{% endraw %}
### Header Actions
The header shows a functional close button by default. You can use the `header-actions` slot to add additional [icon buttons](/components/icon-button) if needed.
```html:preview
<wa-dialog label="Dialog" class="dialog-header-actions">
<wa-dialog label="Dialog" with-header with-footer class="dialog-header-actions">
<wa-icon-button class="new-window" slot="header-actions" name="arrow-up-right-from-square" variant="solid"></wa-icon-button>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand">Close</wa-button>
@@ -195,7 +186,7 @@ const App = () => {
return (
<>
<WaDialog label="Dialog" open={open} onWaAfterHide={() => setOpen(false)}>
<WaDialog label="Dialog" with-header with-footer open={open} onWaAfterHide={() => setOpen(false)}>
<WaIconButton
class="new-window"
slot="header-actions"
@@ -223,7 +214,7 @@ To keep the dialog open in such cases, you can cancel the `wa-request-close` eve
You can use `event.detail.source` to determine what triggered the request to close. This example prevents the dialog from closing when the overlay is clicked, but allows the close button or [[Escape]] to dismiss it.
```html:preview
<wa-dialog label="Dialog" class="dialog-deny-close">
<wa-dialog label="Dialog" with-header with-footer class="dialog-deny-close">
This dialog will not close when you click on the overlay.
<wa-button slot="footer" variant="brand">Close</wa-button>
</wa-dialog>
@@ -264,7 +255,7 @@ const App = () => {
return (
<>
<WaDialog label="Dialog" open={open} onWaRequestClose={handleRequestClose} onWaAfterHide={() => setOpen(false)}>
<WaDialog label="Dialog" with-header with-footer open={open} onWaRequestClose={handleRequestClose} onWaAfterHide={() => setOpen(false)}>
This dialog will not close when you click on the overlay.
<WaButton slot="footer" variant="brand" onClick={() => setOpen(false)}>
Close
@@ -282,7 +273,7 @@ const App = () => {
By default, the dialog's panel will gain focus when opened. This allows a subsequent tab press to focus on the first tabbable element in the dialog. If you want a different element to have focus, add the `autofocus` attribute to it as shown below.
```html:preview
<wa-dialog label="Dialog" class="dialog-focus">
<wa-dialog label="Dialog" with-header with-footer class="dialog-focus">
<wa-input autofocus placeholder="I will have focus when the dialog is opened"></wa-input>
<wa-button slot="footer" variant="brand">Close</wa-button>
</wa-dialog>
@@ -311,7 +302,7 @@ const App = () => {
return (
<>
<WaDialog label="Dialog" open={open} onWaAfterHide={() => setOpen(false)}>
<WaDialog label="Dialog" with-header with-footer open={open} onWaAfterHide={() => setOpen(false)}>
<WaInput autofocus placeholder="I will have focus when the dialog is opened" />
<WaButton slot="footer" variant="brand" onClick={() => setOpen(false)}>
Close

View File

@@ -1,8 +1,7 @@
---
meta:
title: Divider
description: Dividers are used to visually separate or group elements.
layout: component
title: Divider
description: Dividers are used to visually separate or group elements.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview
@@ -25,16 +24,12 @@ Use the `--width` custom property to change the width of the divider.
<wa-divider style="--width: 4px;"></wa-divider>
```
{% raw %}
```jsx:react
import WaDivider from '@shoelace-style/shoelace/dist/react/divider';
const App = () => <WaDivider style={{ '--width': '4px' }} />;
```
{% endraw %}
### Color
Use the `--color` custom property to change the color of the divider.
@@ -43,16 +38,12 @@ Use the `--color` custom property to change the color of the divider.
<wa-divider style="--color: tomato;"></wa-divider>
```
{% raw %}
```jsx:react
import WaDivider from '@shoelace-style/shoelace/dist/react/divider';
const App = () => <WaDivider style={{ '--color': 'tomato' }} />;
```
{% endraw %}
### Spacing
Use the `--spacing` custom property to change the amount of space between the divider and it's neighboring elements.
@@ -65,22 +56,6 @@ Use the `--spacing` custom property to change the amount of space between the di
</div>
```
{% raw %}
```jsx:react
import WaDivider from '@shoelace-style/shoelace/dist/react/divider';
const App = () => (
<>
Above
<WaDivider style={{ '--spacing': '2rem' }} />
Below
</>
);
```
{% endraw %}
### Vertical
Add the `vertical` attribute to draw the divider in a vertical orientation. The divider will span the full height of its container. Vertical dividers work especially well inside of a flex container.
@@ -95,8 +70,6 @@ Add the `vertical` attribute to draw the divider in a vertical orientation. The
</div>
```
{% raw %}
```jsx:react
import WaDivider from '@shoelace-style/shoelace/dist/react/divider';
@@ -117,8 +90,6 @@ const App = () => (
);
```
{% endraw %}
### Menu Dividers
Use dividers in [menus](/components/menu) to visually group menu items.
@@ -135,8 +106,6 @@ Use dividers in [menus](/components/menu) to visually group menu items.
</wa-menu>
```
{% raw %}
```jsx:react
import WaDivider from '@shoelace-style/shoelace/dist/react/divider';
import WaMenu from '@shoelace-style/shoelace/dist/react/menu';
@@ -154,5 +123,3 @@ const App = () => (
</WaMenu>
);
```
{% endraw %}

View File

@@ -1,14 +1,13 @@
---
meta:
title: Drawer
description: Drawers slide in from a container to expose additional options and information.
layout: component
title: Drawer
description: Drawers slide in from a container to expose additional options and information.
layout: ../../../layouts/ComponentLayout.astro
---
<!-- cspell:dictionaries lorem-ipsum -->
```html:preview
<wa-drawer label="Drawer" class="drawer-overview">
<wa-drawer label="Drawer" with-header with-footer class="drawer-overview">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand">Close</wa-button>
</wa-drawer>
@@ -35,7 +34,7 @@ const App = () => {
return (
<>
<WaDrawer label="Drawer" open={open} onWaAfterHide={() => setOpen(false)}>
<WaDrawer label="Drawer" with-header with-footer open={open} onWaAfterHide={() => setOpen(false)}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<WaButton slot="footer" variant="brand" onClick={() => setOpen(false)}>
Close
@@ -55,7 +54,7 @@ const App = () => {
By default, drawers slide in from the end. To make the drawer slide in from the start, set the `placement` attribute to `start`.
```html:preview
<wa-drawer label="Drawer" placement="start" class="drawer-placement-start">
<wa-drawer label="Drawer" placement="start" with-header with-footer class="drawer-placement-start">
This drawer slides in from the start.
<wa-button slot="footer" variant="brand">Close</wa-button>
</wa-drawer>
@@ -82,7 +81,7 @@ const App = () => {
return (
<>
<WaDrawer label="Drawer" placement="start" open={open} onWaAfterHide={() => setOpen(false)}>
<WaDrawer label="Drawer" placement="start" with-header with-footer open={open} onWaAfterHide={() => setOpen(false)}>
This drawer slides in from the start.
<WaButton slot="footer" variant="brand" onClick={() => setOpen(false)}>
Close
@@ -100,7 +99,7 @@ const App = () => {
To make the drawer slide in from the top, set the `placement` attribute to `top`.
```html:preview
<wa-drawer label="Drawer" placement="top" class="drawer-placement-top">
<wa-drawer label="Drawer" placement="top" with-header with-footer class="drawer-placement-top">
This drawer slides in from the top.
<wa-button slot="footer" variant="brand">Close</wa-button>
</wa-drawer>
@@ -127,7 +126,7 @@ const App = () => {
return (
<>
<WaDrawer label="Drawer" placement="top" open={open} onWaAfterHide={() => setOpen(false)}>
<WaDrawer label="Drawer" placement="top" with-header with-footer open={open} onWaAfterHide={() => setOpen(false)}>
This drawer slides in from the top.
<WaButton slot="footer" variant="brand" onClick={() => setOpen(false)}>
Close
@@ -145,7 +144,7 @@ const App = () => {
To make the drawer slide in from the bottom, set the `placement` attribute to `bottom`.
```html:preview
<wa-drawer label="Drawer" placement="bottom" class="drawer-placement-bottom">
<wa-drawer label="Drawer" placement="bottom" with-header with-footer class="drawer-placement-bottom">
This drawer slides in from the bottom.
<wa-button slot="footer" variant="brand">Close</wa-button>
</wa-drawer>
@@ -172,7 +171,7 @@ const App = () => {
return (
<>
<WaDrawer label="Drawer" placement="bottom" open={open} onWaAfterHide={() => setOpen(false)}>
<WaDrawer label="Drawer" placement="bottom" with-header with-footer open={open} onWaAfterHide={() => setOpen(false)}>
This drawer slides in from the bottom.
<WaButton slot="footer" variant="brand" onClick={() => setOpen(false)}>
Close
@@ -185,88 +184,12 @@ const App = () => {
};
```
### Contained to an Element
By default, drawers slide out of their [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport. To make a drawer slide out of a parent element, add the `contained` attribute to the drawer and apply `position: relative` to its parent.
Unlike normal drawers, contained drawers are not modal. This means they do not show an overlay, they do not trap focus, and they are not dismissible with [[Escape]]. This is intentional to allow users to interact with elements outside of the drawer.
```html:preview
<div
style="position: relative; border: solid 2px var(--wa-color-surface-border); height: 300px; padding: 1rem; margin-bottom: 1rem;"
>
The drawer will be contained to this box. This content won't shift or be affected in any way when the drawer opens.
<wa-drawer label="Drawer" contained class="drawer-contained" style="--size: 50%;">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand">Close</wa-button>
</wa-drawer>
</div>
<wa-button>Toggle Drawer</wa-button>
<script>
const drawer = document.querySelector('.drawer-contained');
const openButton = drawer.parentElement.nextElementSibling;
const closeButton = drawer.querySelector('wa-button[variant="brand"]');
openButton.addEventListener('click', () => (drawer.open = !drawer.open));
closeButton.addEventListener('click', () => drawer.hide());
</script>
```
{% raw %}
```jsx:react
import { useState } from 'react';
import WaButton from '@shoelace-style/shoelace/dist/react/button';
import WaDrawer from '@shoelace-style/shoelace/dist/react/drawer';
const App = () => {
const [open, setOpen] = useState(false);
return (
<>
<div
style={{
position: 'relative',
border: 'solid 2px var(--wa-color-surface-border)',
height: '300px',
padding: '1rem',
marginBottom: '1rem'
}}
>
The drawer will be contained to this box. This content won't shift or be affected in any way when the drawer
opens.
<WaDrawer
label="Drawer"
contained
no-modal
open={open}
onWaAfterHide={() => setOpen(false)}
style={{ '--size': '50%' }}
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<WaButton slot="footer" variant="brand" onClick={() => setOpen(false)}>
Close
</WaButton>
</WaDrawer>
</div>
<WaButton onClick={() => setOpen(true)}>Open Drawer</WaButton>
</>
);
};
```
{% endraw %}
### Custom Size
Use the `--size` custom property to set the drawer's size. This will be applied to the drawer's width or height depending on its `placement`.
```html:preview
<wa-drawer label="Drawer" class="drawer-custom-size" style="--size: 50vw;">
<wa-drawer label="Drawer" with-header with-footer class="drawer-custom-size" style="--size: 50vw;">
This drawer is always 50% of the viewport.
<wa-button slot="footer" variant="brand">Close</wa-button>
</wa-drawer>
@@ -283,8 +206,6 @@ Use the `--size` custom property to set the drawer's size. This will be applied
</script>
```
{% raw %}
```jsx:react
import { useState } from 'react';
import WaButton from '@shoelace-style/shoelace/dist/react/button';
@@ -295,7 +216,7 @@ const App = () => {
return (
<>
<WaDrawer label="Drawer" open={open} onWaAfterHide={() => setOpen(false)} style={{ '--size': '50vw' }}>
<WaDrawer label="Drawer" with-header with-footer open={open} onWaAfterHide={() => setOpen(false)} style={{ '--size': '50vw' }}>
This drawer is always 50% of the viewport.
<WaButton slot="footer" variant="brand" onClick={() => setOpen(false)}>
Close
@@ -308,14 +229,12 @@ const App = () => {
};
```
{% endraw %}
### Scrolling
By design, a drawer's height will never exceed 100% of its container. As such, drawers will not scroll with the page to ensure the header and footer are always accessible to the user.
```html:preview
<wa-drawer label="Drawer" class="drawer-scrolling">
<wa-drawer label="Drawer" with-header with-footer class="drawer-scrolling">
<div style="height: 150vh; border: dashed 2px var(--wa-color-surface-border); padding: 0 1rem;">
<p>Scroll down and give it a try! 👇</p>
</div>
@@ -334,8 +253,6 @@ By design, a drawer's height will never exceed 100% of its container. As such, d
</script>
```
{% raw %}
```jsx:react
import { useState } from 'react';
import WaButton from '@shoelace-style/shoelace/dist/react/button';
@@ -346,7 +263,7 @@ const App = () => {
return (
<>
<WaDrawer label="Drawer" open={open} onWaAfterHide={() => setOpen(false)}>
<WaDrawer label="Drawer" with-header with-footer open={open} onWaAfterHide={() => setOpen(false)}>
<div
style={{
height: '150vh',
@@ -367,14 +284,12 @@ const App = () => {
};
```
{% endraw %}
### Header Actions
The header shows a functional close button by default. You can use the `header-actions` slot to add additional [icon buttons](/components/icon-button) if needed.
```html:preview
<wa-drawer label="Drawer" class="drawer-header-actions">
<wa-drawer label="Drawer" with-header with-footer class="drawer-header-actions">
<wa-icon-button class="new-window" slot="header-actions" name="arrow-up-right-from-square" variant="solid"></wa-icon-button>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand">Close</wa-button>
@@ -405,7 +320,7 @@ const App = () => {
return (
<>
<WaDrawer label="Drawer" open={open} onWaAfterHide={() => setOpen(false)}>
<WaDrawer label="Drawer" with-header with-footer open={open} onWaAfterHide={() => setOpen(false)}>
<WaIconButton slot="header-actions" name="arrow-up-right-from-square" onClick={() => window.open(location.href)} />
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<WaButton slot="footer" variant="brand" onClick={() => setOpen(false)}>
@@ -428,7 +343,7 @@ To keep the drawer open in such cases, you can cancel the `wa-request-close` eve
You can use `event.detail.source` to determine what triggered the request to close. This example prevents the drawer from closing when the overlay is clicked, but allows the close button or [[Escape]] to dismiss it.
```html:preview
<wa-drawer label="Drawer" class="drawer-deny-close">
<wa-drawer label="Drawer" with-header with-footer class="drawer-deny-close">
This drawer will not close when you click on the overlay.
<wa-button slot="footer" variant="brand">Close</wa-button>
</wa-drawer>
@@ -469,7 +384,7 @@ const App = () => {
return (
<>
<WaDrawer label="Drawer" open={open} onWaRequestClose={handleRequestClose} onWaAfterHide={() => setOpen(false)}>
<WaDrawer label="Drawer" with-header with-footer open={open} onWaRequestClose={handleRequestClose} onWaAfterHide={() => setOpen(false)}>
This drawer will not close when you click on the overlay.
<WaButton slot="footer" variant="brand" onClick={() => setOpen(false)}>
Save &amp; Close
@@ -487,7 +402,7 @@ const App = () => {
By default, the drawer's panel will gain focus when opened. This allows a subsequent tab press to focus on the first tabbable element in the drawer. If you want a different element to have focus, add the `autofocus` attribute to it as shown below.
```html:preview
<wa-drawer label="Drawer" class="drawer-focus">
<wa-drawer label="Drawer" with-header with-footer class="drawer-focus">
<wa-input autofocus placeholder="I will have focus when the drawer is opened"></wa-input>
<wa-button slot="footer" variant="brand">Close</wa-button>
</wa-drawer>
@@ -516,7 +431,7 @@ const App = () => {
return (
<>
<WaDrawer label="Drawer" open={open} onWaAfterHide={() => setOpen(false)}>
<WaDrawer label="Drawer" with-header with-footer open={open} onWaAfterHide={() => setOpen(false)}>
<WaInput autofocus placeholder="I will have focus when the drawer is opened" />
<WaButton slot="footer" variant="brand" onClick={() => setOpen(false)}>
Close

View File

@@ -1,8 +1,7 @@
---
meta:
title: Dropdown
description: 'Dropdowns expose additional content that "drops down" in a panel.'
layout: component
title: Dropdown
description: 'Dropdowns expose additional content that "drops down" in a panel.'
layout: ../../../layouts/ComponentLayout.astro
---
Dropdowns consist of a trigger and a panel. By default, activating the trigger will expose the panel and interacting outside of the panel will close it.
@@ -396,7 +395,7 @@ const App = () => (
);
```
:::warning
:::caution
As a UX best practice, avoid using more than one level of submenu when possible.
:::

View File

@@ -1,8 +1,7 @@
---
meta:
title: Format Bytes
description: Formats a number as a human readable bytes value.
layout: component
title: Format Bytes
description: Formats a number as a human readable bytes value.
layout: ../../../layouts/ComponentLayout.astro
---
```html:preview
@@ -20,8 +19,6 @@ layout: component
</script>
```
{% raw %}
```jsx:react
import { useState } from 'react';
import WaButton from '@shoelace-style/shoelace/dist/react/button';
@@ -48,8 +45,6 @@ const App = () => {
};
```
{% endraw %}
## Examples
### Formatting Bytes

View File

@@ -1,8 +1,7 @@
---
meta:
title: Format Date
description: Formats a date/time using the specified locale and options.
layout: component
title: Format Date
description: Formats a date/time using the specified locale and options.
layout: ../../../layouts/ComponentLayout.astro
---
Localization is handled by the browser's [`Intl.DateTimeFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat). No language packs are required.

View File

@@ -1,8 +1,7 @@
---
meta:
title: Format Number
description: Formats a number using the specified locale and options.
layout: component
title: Format Number
description: Formats a number using the specified locale and options.
layout: ../../../layouts/ComponentLayout.astro
---
Localization is handled by the browser's [`Intl.NumberFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat). No language packs are required.
@@ -23,8 +22,6 @@ Localization is handled by the browser's [`Intl.NumberFormat` API](https://devel
</script>
```
{% raw %}
```jsx:react
import { useState } from 'react';
import WaFormatNumber from '@shoelace-style/shoelace/dist/react/format-number';
@@ -50,8 +47,6 @@ const App = () => {
};
```
{% endraw %}
## Examples
### Percentages

View File

@@ -1,8 +1,7 @@
---
meta:
title: Icon Button
description: Icons buttons are simple, icon-only buttons that can be used for actions and in toolbars.
layout: component
title: Icon Button
description: Icons buttons are simple, icon-only buttons that can be used for actions and in toolbars.
layout: ../../../layouts/ComponentLayout.astro
---
For a full list of icons that come bundled with Web Awesome, refer to the [icon component](/components/icon).
@@ -29,8 +28,6 @@ Icon buttons inherit their parent element's `font-size`.
<wa-icon-button name="pen-to-square" variant="solid" label="Edit" style="font-size: 2.5rem;"></wa-icon-button>
```
{% raw %}
```jsx:react
import WaIconButton from '@shoelace-style/shoelace/dist/react/icon-button';
@@ -43,8 +40,6 @@ const App = () => (
);
```
{% endraw %}
### Colors
Icon buttons are designed to have a uniform appearance, so their color is not inherited. However, you can still customize them by styling the `base` part.

View File

@@ -1,8 +1,7 @@
---
meta:
title: Icon
description: Icons are symbols that can be used to represent various options within an application.
layout: component
title: Icon
description: Icons are symbols that can be used to represent various options within an application.
layout: ../../../layouts/ComponentLayout.astro
---
Web Awesome comes bundled with over 1,500 icons courtesy of the [Bootstrap Icons](https://icons.getbootstrap.com/) project. These icons are part of the `default` icon library. If you prefer, you can register [custom icon libraries](#icon-libraries) as well.
@@ -55,8 +54,6 @@ Icons inherit their color from the current text color. Thus, you can set the `co
</div>
```
{% raw %}
```jsx:react
import WaIcon from '@shoelace-style/shoelace/dist/react/icon';
@@ -90,8 +87,6 @@ const App = () => (
);
```
{% endraw %}
### Sizing
Icons are sized relative to the current font size. To change their size, set the `font-size` property on the icon itself or on a parent element as shown below.
@@ -117,8 +112,6 @@ Icons are sized relative to the current font size. To change their size, set the
</div>
```
{% raw %}
```jsx:react
import WaIcon from '@shoelace-style/shoelace/dist/react/icon';
@@ -144,8 +137,6 @@ const App = () => (
);
```
{% endraw %}
### Labels
For non-decorative icons, use the `label` attribute to announce it to assistive devices.
@@ -168,16 +159,12 @@ Custom icons can be loaded individually with the `src` attribute. Only SVGs on a
<wa-icon src="https://shoelace.style/assets/images/shoe.svg" style="font-size: 8rem;"></wa-icon>
```
{% raw %}
```jsx:react
import WaIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => <WaIcon src="https://shoelace.style/assets/images/shoe.svg" style={{ fontSize: '8rem' }}></WaIcon>;
```
{% endraw %}
## Icon Libraries
You can register additional icons to use with the `<wa-icon>` component through icon libraries. Icon files can exist locally or on a CORS-enabled endpoint (e.g. a CDN). There is no limit to how many icon libraries you can register and there is no cost associated with registering them, as individual icons are only requested when they're used.
@@ -630,9 +617,7 @@ As always, make sure to benchmark these changes. When using HTTP/2, it may in fa
:::danger
When using sprite sheets, the `wa-load` and `wa-error` events will not fire.
:::
:::danger
For security reasons, browsers may apply the same-origin policy on `<use>` elements located in the `<wa-icon>` shadow DOM and may refuse to load a cross-origin URL. There is currently no defined way to set a cross-origin policy for `<use>` elements. For this reason, sprite sheets should only be used if you're self-hosting them.
:::

View File

@@ -1,8 +1,7 @@
---
meta:
title: Image Comparer
description: Compare visual differences between similar photos with a sliding panel.
layout: component
title: Image Comparer
description: Compare visual differences between similar photos with a sliding panel.
layout: ../../../layouts/ComponentLayout.astro
---
For best results, use images that share the same dimensions. The slider can be controlled by dragging or pressing the left and right arrow keys. (Tip: press shift + arrows to move the slider in larger intervals, or home + end to jump to the beginning or end.)

View File

@@ -1,8 +1,7 @@
---
meta:
title: Include
description: Includes give you the power to embed external HTML files into the page.
layout: component
title: Include
description: Includes give you the power to embed external HTML files into the page.
layout: ../../../layouts/ComponentLayout.astro
---
Included files are asynchronously requested using `window.fetch()`. Requests are cached, so the same file can be included multiple times, but only one request will be made.

Some files were not shown because too many files have changed in this diff Show More