Repository: shoelace-style/shoelace
Branch: next
Commit: 4fc0fc332ccc
Files: 492
Total size: 2.2 MB
Directory structure:
gitextract_b6qx7r1_/
├── .editorconfig
├── .eslintignore
├── .eslintrc.cjs
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── config.yml
│ ├── SECURITY.md
│ └── workflows/
│ ├── node.js.yml
│ └── release.yml
├── .gitignore
├── .gitpod.yml
├── .husky/
│ └── pre-commit
├── .prettierignore
├── .vscode/
│ ├── extensions.json
│ └── settings.json
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── cspell.json
├── custom-elements-manifest.config.js
├── docs/
│ ├── _includes/
│ │ ├── component.njk
│ │ ├── default.njk
│ │ ├── sidebar.njk
│ │ └── wa-logo-icon.njk
│ ├── _utilities/
│ │ ├── active-links.cjs
│ │ ├── anchor-headings.cjs
│ │ ├── cem.cjs
│ │ ├── code-previews.cjs
│ │ ├── copy-code-buttons.cjs
│ │ ├── external-links.cjs
│ │ ├── highlight-code.cjs
│ │ ├── markdown.cjs
│ │ ├── prettier.cjs
│ │ ├── replacer.cjs
│ │ ├── scrolling-tables.cjs
│ │ ├── strings.cjs
│ │ ├── table-of-contents.cjs
│ │ └── typography.cjs
│ ├── assets/
│ │ ├── examples/
│ │ │ └── include.html
│ │ ├── scripts/
│ │ │ ├── code-previews.js
│ │ │ ├── docs.js
│ │ │ ├── search.js
│ │ │ └── turbo.js
│ │ └── styles/
│ │ ├── code-previews.css
│ │ ├── docs.css
│ │ └── search.css
│ ├── eleventy.config.cjs
│ └── pages/
│ ├── 404.md
│ ├── components/
│ │ ├── alert.md
│ │ ├── animated-image.md
│ │ ├── animation.md
│ │ ├── avatar.md
│ │ ├── badge.md
│ │ ├── breadcrumb-item.md
│ │ ├── breadcrumb.md
│ │ ├── button-group.md
│ │ ├── button.md
│ │ ├── card.md
│ │ ├── carousel-item.md
│ │ ├── carousel.md
│ │ ├── checkbox.md
│ │ ├── color-picker.md
│ │ ├── copy-button.md
│ │ ├── details.md
│ │ ├── dialog.md
│ │ ├── divider.md
│ │ ├── drawer.md
│ │ ├── dropdown.md
│ │ ├── format-bytes.md
│ │ ├── format-date.md
│ │ ├── format-number.md
│ │ ├── icon-button.md
│ │ ├── icon.md
│ │ ├── image-comparer.md
│ │ ├── include.md
│ │ ├── input.md
│ │ ├── menu-item.md
│ │ ├── menu-label.md
│ │ ├── menu.md
│ │ ├── mutation-observer.md
│ │ ├── option.md
│ │ ├── popup.md
│ │ ├── progress-bar.md
│ │ ├── progress-ring.md
│ │ ├── qr-code.md
│ │ ├── radio-button.md
│ │ ├── radio-group.md
│ │ ├── radio.md
│ │ ├── range.md
│ │ ├── rating.md
│ │ ├── relative-time.md
│ │ ├── resize-observer.md
│ │ ├── select.md
│ │ ├── skeleton.md
│ │ ├── spinner.md
│ │ ├── split-panel.md
│ │ ├── switch.md
│ │ ├── tab-group.md
│ │ ├── tab-panel.md
│ │ ├── tab.md
│ │ ├── tag.md
│ │ ├── textarea.md
│ │ ├── tooltip.md
│ │ ├── tree-item.md
│ │ ├── tree.md
│ │ └── visually-hidden.md
│ ├── frameworks/
│ │ ├── angular.md
│ │ ├── react.md
│ │ ├── svelte.md
│ │ ├── vue-2.md
│ │ └── vue.md
│ ├── getting-started/
│ │ ├── customizing.md
│ │ ├── form-controls.md
│ │ ├── installation.md
│ │ ├── localization.md
│ │ ├── themes.md
│ │ └── usage.md
│ ├── index.md
│ ├── resources/
│ │ ├── accessibility.md
│ │ ├── changelog.md
│ │ ├── community.md
│ │ └── contributing.md
│ ├── tokens/
│ │ ├── border-radius.md
│ │ ├── color.md
│ │ ├── elevation.md
│ │ ├── more.md
│ │ ├── spacing.md
│ │ ├── transition.md
│ │ ├── typography.md
│ │ └── z-index.md
│ └── tutorials/
│ ├── integrating-with-astro.md
│ ├── integrating-with-laravel.md
│ ├── integrating-with-nextjs.md
│ └── integrating-with-rails.md
├── lint-staged.config.js
├── package.json
├── prettier.config.js
├── scripts/
│ ├── build.js
│ ├── make-icons.js
│ ├── make-metadata.js
│ ├── make-react.js
│ ├── make-themes.js
│ ├── plop/
│ │ ├── plopfile.js
│ │ └── templates/
│ │ └── component/
│ │ ├── component.hbs
│ │ ├── define.hbs
│ │ ├── docs.hbs
│ │ ├── styles.hbs
│ │ └── tests.hbs
│ └── shared.js
├── src/
│ ├── components/
│ │ ├── alert/
│ │ │ ├── alert.component.ts
│ │ │ ├── alert.styles.ts
│ │ │ ├── alert.test.ts
│ │ │ └── alert.ts
│ │ ├── animated-image/
│ │ │ ├── animated-image.component.ts
│ │ │ ├── animated-image.styles.ts
│ │ │ ├── animated-image.test.ts
│ │ │ └── animated-image.ts
│ │ ├── animation/
│ │ │ ├── animation.component.ts
│ │ │ ├── animation.styles.ts
│ │ │ ├── animation.test.ts
│ │ │ ├── animation.ts
│ │ │ └── animations.ts
│ │ ├── avatar/
│ │ │ ├── avatar.component.ts
│ │ │ ├── avatar.styles.ts
│ │ │ ├── avatar.test.ts
│ │ │ └── avatar.ts
│ │ ├── badge/
│ │ │ ├── badge.component.ts
│ │ │ ├── badge.styles.ts
│ │ │ ├── badge.test.ts
│ │ │ └── badge.ts
│ │ ├── breadcrumb/
│ │ │ ├── breadcrumb.component.ts
│ │ │ ├── breadcrumb.styles.ts
│ │ │ ├── breadcrumb.test.ts
│ │ │ └── breadcrumb.ts
│ │ ├── breadcrumb-item/
│ │ │ ├── breadcrumb-item.component.ts
│ │ │ ├── breadcrumb-item.styles.ts
│ │ │ ├── breadcrumb-item.test.ts
│ │ │ └── breadcrumb-item.ts
│ │ ├── button/
│ │ │ ├── button.component.ts
│ │ │ ├── button.styles.ts
│ │ │ ├── button.test.ts
│ │ │ └── button.ts
│ │ ├── button-group/
│ │ │ ├── button-group.component.ts
│ │ │ ├── button-group.styles.ts
│ │ │ ├── button-group.test.ts
│ │ │ └── button-group.ts
│ │ ├── card/
│ │ │ ├── card.component.ts
│ │ │ ├── card.styles.ts
│ │ │ ├── card.test.ts
│ │ │ └── card.ts
│ │ ├── carousel/
│ │ │ ├── autoplay-controller.ts
│ │ │ ├── carousel.component.ts
│ │ │ ├── carousel.styles.ts
│ │ │ ├── carousel.test.ts
│ │ │ └── carousel.ts
│ │ ├── carousel-item/
│ │ │ ├── carousel-item.component.ts
│ │ │ ├── carousel-item.styles.ts
│ │ │ ├── carousel-item.test.ts
│ │ │ └── carousel-item.ts
│ │ ├── checkbox/
│ │ │ ├── checkbox.component.ts
│ │ │ ├── checkbox.styles.ts
│ │ │ ├── checkbox.test.ts
│ │ │ └── checkbox.ts
│ │ ├── color-picker/
│ │ │ ├── color-picker.component.ts
│ │ │ ├── color-picker.styles.ts
│ │ │ ├── color-picker.test.ts
│ │ │ └── color-picker.ts
│ │ ├── copy-button/
│ │ │ ├── copy-button.component.ts
│ │ │ ├── copy-button.styles.ts
│ │ │ ├── copy-button.test.ts
│ │ │ └── copy-button.ts
│ │ ├── details/
│ │ │ ├── details.component.ts
│ │ │ ├── details.styles.ts
│ │ │ ├── details.test.ts
│ │ │ └── details.ts
│ │ ├── dialog/
│ │ │ ├── dialog.component.ts
│ │ │ ├── dialog.styles.ts
│ │ │ ├── dialog.test.ts
│ │ │ └── dialog.ts
│ │ ├── divider/
│ │ │ ├── divider.component.ts
│ │ │ ├── divider.styles.ts
│ │ │ ├── divider.test.ts
│ │ │ └── divider.ts
│ │ ├── drawer/
│ │ │ ├── drawer.component.ts
│ │ │ ├── drawer.styles.ts
│ │ │ ├── drawer.test.ts
│ │ │ └── drawer.ts
│ │ ├── dropdown/
│ │ │ ├── dropdown.component.ts
│ │ │ ├── dropdown.styles.ts
│ │ │ ├── dropdown.test.ts
│ │ │ └── dropdown.ts
│ │ ├── format-bytes/
│ │ │ ├── format-bytes.component.ts
│ │ │ ├── format-bytes.test.ts
│ │ │ └── format-bytes.ts
│ │ ├── format-date/
│ │ │ ├── format-date.component.ts
│ │ │ ├── format-date.test.ts
│ │ │ └── format-date.ts
│ │ ├── format-number/
│ │ │ ├── format-number.component.ts
│ │ │ ├── format-number.test.ts
│ │ │ └── format-number.ts
│ │ ├── icon/
│ │ │ ├── icon.component.ts
│ │ │ ├── icon.styles.ts
│ │ │ ├── icon.test.ts
│ │ │ ├── icon.ts
│ │ │ ├── library.default.ts
│ │ │ ├── library.system.ts
│ │ │ └── library.ts
│ │ ├── icon-button/
│ │ │ ├── icon-button.component.ts
│ │ │ ├── icon-button.styles.ts
│ │ │ ├── icon-button.test.ts
│ │ │ └── icon-button.ts
│ │ ├── image-comparer/
│ │ │ ├── image-comparer.component.ts
│ │ │ ├── image-comparer.styles.ts
│ │ │ ├── image-comparer.test.ts
│ │ │ └── image-comparer.ts
│ │ ├── include/
│ │ │ ├── include.component.ts
│ │ │ ├── include.styles.ts
│ │ │ ├── include.test.ts
│ │ │ ├── include.ts
│ │ │ └── request.ts
│ │ ├── input/
│ │ │ ├── input.component.ts
│ │ │ ├── input.styles.ts
│ │ │ ├── input.test.ts
│ │ │ └── input.ts
│ │ ├── menu/
│ │ │ ├── menu.component.ts
│ │ │ ├── menu.styles.ts
│ │ │ ├── menu.test.ts
│ │ │ └── menu.ts
│ │ ├── menu-item/
│ │ │ ├── menu-item.component.ts
│ │ │ ├── menu-item.styles.ts
│ │ │ ├── menu-item.test.ts
│ │ │ ├── menu-item.ts
│ │ │ └── submenu-controller.ts
│ │ ├── menu-label/
│ │ │ ├── menu-label.component.ts
│ │ │ ├── menu-label.styles.ts
│ │ │ ├── menu-label.test.ts
│ │ │ └── menu-label.ts
│ │ ├── mutation-observer/
│ │ │ ├── mutation-observer.component.ts
│ │ │ ├── mutation-observer.styles.ts
│ │ │ ├── mutation-observer.test.ts
│ │ │ └── mutation-observer.ts
│ │ ├── option/
│ │ │ ├── option.component.ts
│ │ │ ├── option.styles.ts
│ │ │ ├── option.test.ts
│ │ │ └── option.ts
│ │ ├── popup/
│ │ │ ├── popup.component.ts
│ │ │ ├── popup.styles.ts
│ │ │ ├── popup.test.ts
│ │ │ └── popup.ts
│ │ ├── progress-bar/
│ │ │ ├── progress-bar.component.ts
│ │ │ ├── progress-bar.styles.ts
│ │ │ ├── progress-bar.test.ts
│ │ │ └── progress-bar.ts
│ │ ├── progress-ring/
│ │ │ ├── progress-ring.component.ts
│ │ │ ├── progress-ring.styles.ts
│ │ │ ├── progress-ring.test.ts
│ │ │ └── progress-ring.ts
│ │ ├── qr-code/
│ │ │ ├── qr-code.component.ts
│ │ │ ├── qr-code.styles.ts
│ │ │ ├── qr-code.test.ts
│ │ │ └── qr-code.ts
│ │ ├── radio/
│ │ │ ├── radio.component.ts
│ │ │ ├── radio.styles.ts
│ │ │ ├── radio.test.ts
│ │ │ └── radio.ts
│ │ ├── radio-button/
│ │ │ ├── radio-button.component.ts
│ │ │ ├── radio-button.styles.ts
│ │ │ ├── radio-button.test.ts
│ │ │ └── radio-button.ts
│ │ ├── radio-group/
│ │ │ ├── radio-group.component.ts
│ │ │ ├── radio-group.styles.ts
│ │ │ ├── radio-group.test.ts
│ │ │ └── radio-group.ts
│ │ ├── range/
│ │ │ ├── range.component.ts
│ │ │ ├── range.styles.ts
│ │ │ ├── range.test.ts
│ │ │ └── range.ts
│ │ ├── rating/
│ │ │ ├── rating.component.ts
│ │ │ ├── rating.styles.ts
│ │ │ ├── rating.test.ts
│ │ │ └── rating.ts
│ │ ├── relative-time/
│ │ │ ├── relative-time.component.ts
│ │ │ ├── relative-time.test.ts
│ │ │ └── relative-time.ts
│ │ ├── resize-observer/
│ │ │ ├── resize-observer.component.ts
│ │ │ ├── resize-observer.styles.ts
│ │ │ └── resize-observer.ts
│ │ ├── select/
│ │ │ ├── select.component.ts
│ │ │ ├── select.styles.ts
│ │ │ ├── select.test.ts
│ │ │ └── select.ts
│ │ ├── skeleton/
│ │ │ ├── skeleton.component.ts
│ │ │ ├── skeleton.styles.ts
│ │ │ ├── skeleton.test.ts
│ │ │ └── skeleton.ts
│ │ ├── spinner/
│ │ │ ├── spinner.component.ts
│ │ │ ├── spinner.styles.ts
│ │ │ ├── spinner.test.ts
│ │ │ └── spinner.ts
│ │ ├── split-panel/
│ │ │ ├── split-panel.component.ts
│ │ │ ├── split-panel.styles.ts
│ │ │ ├── split-panel.test.ts
│ │ │ └── split-panel.ts
│ │ ├── switch/
│ │ │ ├── switch.component.ts
│ │ │ ├── switch.styles.ts
│ │ │ ├── switch.test.ts
│ │ │ └── switch.ts
│ │ ├── tab/
│ │ │ ├── tab.component.ts
│ │ │ ├── tab.styles.ts
│ │ │ ├── tab.test.ts
│ │ │ └── tab.ts
│ │ ├── tab-group/
│ │ │ ├── tab-group.component.ts
│ │ │ ├── tab-group.styles.ts
│ │ │ ├── tab-group.test.ts
│ │ │ └── tab-group.ts
│ │ ├── tab-panel/
│ │ │ ├── tab-panel.component.ts
│ │ │ ├── tab-panel.styles.ts
│ │ │ ├── tab-panel.test.ts
│ │ │ └── tab-panel.ts
│ │ ├── tag/
│ │ │ ├── tag.component.ts
│ │ │ ├── tag.styles.ts
│ │ │ ├── tag.test.ts
│ │ │ └── tag.ts
│ │ ├── textarea/
│ │ │ ├── textarea.component.ts
│ │ │ ├── textarea.styles.ts
│ │ │ ├── textarea.test.ts
│ │ │ └── textarea.ts
│ │ ├── tooltip/
│ │ │ ├── tooltip.component.ts
│ │ │ ├── tooltip.styles.ts
│ │ │ ├── tooltip.test.ts
│ │ │ └── tooltip.ts
│ │ ├── tree/
│ │ │ ├── tree.component.ts
│ │ │ ├── tree.styles.ts
│ │ │ ├── tree.test.ts
│ │ │ └── tree.ts
│ │ ├── tree-item/
│ │ │ ├── tree-item.component.ts
│ │ │ ├── tree-item.styles.ts
│ │ │ ├── tree-item.test.ts
│ │ │ └── tree-item.ts
│ │ └── visually-hidden/
│ │ ├── visually-hidden.component.ts
│ │ ├── visually-hidden.styles.ts
│ │ ├── visually-hidden.test.ts
│ │ └── visually-hidden.ts
│ ├── declaration.d.ts
│ ├── events/
│ │ ├── events.ts
│ │ ├── sl-after-collapse.ts
│ │ ├── sl-after-expand.ts
│ │ ├── sl-after-hide.ts
│ │ ├── sl-after-show.ts
│ │ ├── sl-blur.ts
│ │ ├── sl-cancel.ts
│ │ ├── sl-change.ts
│ │ ├── sl-clear.ts
│ │ ├── sl-close.ts
│ │ ├── sl-collapse.ts
│ │ ├── sl-copy.ts
│ │ ├── sl-error.ts
│ │ ├── sl-expand.ts
│ │ ├── sl-finish.ts
│ │ ├── sl-focus.ts
│ │ ├── sl-hide.ts
│ │ ├── sl-hover.ts
│ │ ├── sl-initial-focus.ts
│ │ ├── sl-input.ts
│ │ ├── sl-invalid.ts
│ │ ├── sl-lazy-change.ts
│ │ ├── sl-lazy-load.ts
│ │ ├── sl-load.ts
│ │ ├── sl-mutation.ts
│ │ ├── sl-remove.ts
│ │ ├── sl-reposition.ts
│ │ ├── sl-request-close.ts
│ │ ├── sl-resize.ts
│ │ ├── sl-select.ts
│ │ ├── sl-selection-change.ts
│ │ ├── sl-show.ts
│ │ ├── sl-slide-change.ts
│ │ ├── sl-start.ts
│ │ ├── sl-tab-hide.ts
│ │ └── sl-tab-show.ts
│ ├── internal/
│ │ ├── active-elements.ts
│ │ ├── animate.ts
│ │ ├── closeActiveElement.ts
│ │ ├── debounce.ts
│ │ ├── default-value.ts
│ │ ├── drag.ts
│ │ ├── event.ts
│ │ ├── form.test.ts
│ │ ├── form.ts
│ │ ├── math.ts
│ │ ├── modal.ts
│ │ ├── offset.ts
│ │ ├── scroll.ts
│ │ ├── scrollend-polyfill.ts
│ │ ├── shoelace-element.test.ts
│ │ ├── shoelace-element.ts
│ │ ├── slot.ts
│ │ ├── string.ts
│ │ ├── tabbable.test.ts
│ │ ├── tabbable.ts
│ │ ├── test/
│ │ │ ├── data-testid-helpers.ts
│ │ │ ├── element-visible-overflow.ts
│ │ │ ├── form-control-base-tests.ts
│ │ │ └── wait-for-scrolling.ts
│ │ ├── test.ts
│ │ └── watch.ts
│ ├── shoelace-autoloader.ts
│ ├── shoelace.ts
│ ├── styles/
│ │ ├── component.styles.ts
│ │ └── form-control.styles.ts
│ ├── themes/
│ │ ├── _utility.css
│ │ ├── dark.css
│ │ └── light.css
│ ├── translations/
│ │ ├── ar.ts
│ │ ├── cs.ts
│ │ ├── da.ts
│ │ ├── de-ch.ts
│ │ ├── de.ts
│ │ ├── en-gb.ts
│ │ ├── en.ts
│ │ ├── es.ts
│ │ ├── fa.ts
│ │ ├── fi.ts
│ │ ├── fr.ts
│ │ ├── he.ts
│ │ ├── hr.ts
│ │ ├── hu.ts
│ │ ├── id.ts
│ │ ├── it.ts
│ │ ├── ja.ts
│ │ ├── nb.ts
│ │ ├── nl.ts
│ │ ├── nn.ts
│ │ ├── pl.ts
│ │ ├── pt.ts
│ │ ├── ru.ts
│ │ ├── sl.ts
│ │ ├── sv.ts
│ │ ├── tr.ts
│ │ ├── uk.ts
│ │ ├── zh-cn.ts
│ │ └── zh-tw.ts
│ └── utilities/
│ ├── animation-registry.ts
│ ├── animation.ts
│ ├── base-path.ts
│ ├── form.ts
│ ├── icon-library.ts
│ └── localize.ts
├── tsconfig.json
├── tsconfig.prod.json
└── web-test-runner.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
================================================
FILE: .eslintignore
================================================
.cache
docs/dist
docs/search.json
docs/**/*.min.js
dist
examples
node_modules
src/react
scripts
================================================
FILE: .eslintrc.cjs
================================================
/* eslint-env node */
module.exports = {
plugins: [
'@typescript-eslint',
'wc',
'lit',
'lit-a11y',
'chai-expect',
'chai-friendly',
'import',
'sort-imports-es6-autofix'
],
extends: [
'eslint:recommended',
'plugin:wc/recommended',
'plugin:wc/best-practice',
'plugin:lit/recommended',
'plugin:lit-a11y/recommended'
],
env: {
es2021: true,
browser: true
},
parserOptions: {
sourceType: 'module'
},
overrides: [
{
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking'
],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
project: './tsconfig.json',
tsconfigRootDir: __dirname
},
files: ['*.ts'],
rules: {
'default-param-last': 'off',
'@typescript-eslint/default-param-last': 'error',
'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': 'warn',
'no-implied-eval': 'off',
'@typescript-eslint/no-implied-eval': 'error',
'no-invalid-this': 'off',
'@typescript-eslint/no-invalid-this': 'error',
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
'no-throw-literal': 'off',
'@typescript-eslint/no-throw-literal': 'error',
'no-unused-expressions': 'off',
'@typescript-eslint/prefer-regexp-exec': 'off',
'@typescript-eslint/no-unused-expressions': 'error',
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: false
}
],
'@typescript-eslint/consistent-type-assertions': [
'warn',
{
assertionStyle: 'as',
objectLiteralTypeAssertions: 'never'
}
],
'@typescript-eslint/consistent-type-imports': 'warn',
'@typescript-eslint/no-base-to-string': 'error',
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
'@typescript-eslint/no-invalid-void-type': 'error',
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'warn',
'@typescript-eslint/no-unnecessary-condition': 'off',
'@typescript-eslint/no-unnecessary-qualifier': 'warn',
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
'@typescript-eslint/non-nullable-type-assertion-style': 'warn',
'@typescript-eslint/prefer-for-of': 'warn',
'@typescript-eslint/prefer-optional-chain': 'warn',
'@typescript-eslint/prefer-ts-expect-error': 'warn',
'@typescript-eslint/prefer-return-this-type': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'warn',
'@typescript-eslint/require-array-sort-compare': 'error',
'@typescript-eslint/unified-signatures': 'warn',
'@typescript-eslint/array-type': 'warn',
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/member-delimiter-style': 'warn',
'@typescript-eslint/method-signature-style': 'warn',
'@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/parameter-properties': 'error',
'@typescript-eslint/strict-boolean-expressions': 'off'
}
},
{
files: ['**/*.cjs'],
env: {
node: true
}
},
{
extends: ['plugin:chai-expect/recommended', 'plugin:chai-friendly/recommended'],
files: ['*.test.ts'],
rules: {
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unused-expressions': 'off'
}
}
],
rules: {
'no-template-curly-in-string': 'error',
'array-callback-return': 'error',
'comma-dangle': 'off',
'consistent-return': 'error',
curly: 'off',
'default-param-last': 'error',
eqeqeq: 'error',
'lit-a11y/click-events-have-key-events': 'off',
'no-constructor-return': 'error',
'no-empty-function': 'warn',
'no-eval': 'error',
'no-extend-native': 'error',
'no-extra-bind': 'error',
'no-floating-decimal': 'error',
'no-implicit-coercion': 'off',
'no-implicit-globals': 'error',
'no-implied-eval': 'error',
'no-invalid-this': 'error',
'no-labels': 'error',
'no-lone-blocks': 'error',
'no-new': 'error',
'no-new-func': 'error',
'no-new-wrappers': 'error',
'no-octal-escape': 'error',
'no-proto': 'error',
'no-return-assign': 'warn',
'no-script-url': 'error',
'no-self-compare': 'warn',
'no-sequences': 'warn',
'no-throw-literal': 'error',
'no-unmodified-loop-condition': 'error',
'no-unused-expressions': 'warn',
'no-useless-call': 'error',
'no-useless-concat': 'error',
'no-useless-return': 'warn',
'prefer-promise-reject-errors': 'error',
radix: 'off',
'require-await': 'error',
'wrap-iife': ['warn', 'inside'],
'no-shadow': 'error',
'no-array-constructor': 'error',
'no-bitwise': 'error',
'no-multi-assign': 'warn',
'no-new-object': 'error',
'no-useless-computed-key': 'warn',
'no-useless-rename': 'warn',
'no-var': 'error',
'prefer-const': 'warn',
'prefer-numeric-literals': 'warn',
'prefer-object-spread': 'warn',
'prefer-rest-params': 'warn',
'prefer-spread': 'warn',
'prefer-template': 'off',
'no-else-return': 'off',
'func-names': ['warn', 'never'],
'one-var': ['warn', 'never'],
'operator-assignment': 'warn',
'prefer-arrow-callback': 'warn',
'no-restricted-imports': [
'warn',
{
paths: [
{
name: '.',
message: 'Usage of local index imports is not allowed.'
},
{
name: './index',
message: 'Import from the source file instead.'
}
]
}
],
'import/extensions': [
'error',
'always',
{
ignorePackages: true,
pattern: {
js: 'always',
ts: 'never'
}
}
],
'import/no-duplicates': 'warn',
'sort-imports-es6-autofix/sort-imports-es6': [
2,
{
ignoreCase: true,
ignoreMemberSort: false,
memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single']
}
],
'wc/guard-super-call': 'off'
}
};
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## 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.
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.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4.
================================================
FILE: .github/FUNDING.yml
================================================
github: [claviska]
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug Report
about: Create a bug report to help us fix a demonstrable problem with code in the library.
title: ''
labels: bug
assignees:
---
### Describe the bug
A bug is _a demonstrable problem_ caused by code in the library. Please provide a clear and concise description of what the bug is here.
### To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
### Demo
If the bug isn't obvious, please provide a link to a CodePen or Fiddle with a minimal reproduction. Bugs that have repros get attention faster than those that don't.
Tip: use the CodePen button on any example in the docs!
### Screenshots
If applicable, add screenshots to help explain the bug.
### Browser / OS
- OS: [e.g. Mac, Windows]
- Browser: [e.g. Chrome, Firefox, Safari]
- Browser version: [e.g. 22]
### Additional information
Provide any additional information about the bug here.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
contact_links:
- name: Feature Requests
url: https://github.com/shoelace-style/shoelace/discussions/categories/ideas
about: All requests for new features should go here.
- name: Help & Support
url: https://github.com/shoelace-style/shoelace/discussions/categories/help
about: Please don't create issues for personal help requests. Instead, ask your question on the discussion forum.
================================================
FILE: .github/SECURITY.md
================================================
# Reporting Security Issues
We take security issues in Shoelace very seriously and appreciate your efforts to disclose your findings responsibly.
To report a security issue, email [cory@abeautifulsite.net](mailto:cory@abeautifulsite.net) and include "SHOELACE SECURITY" in the subject line.
We'll respond as soon as possible and keep you updated throughout the process.
================================================
FILE: .github/workflows/node.js.yml
================================================
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [next]
pull_request:
branches: [next]
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Update system packages
run: |
sudo apt-get update
sudo apt-get upgrade -y
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npx playwright install --with-deps
- run: npm ci
- run: npm run verify
================================================
FILE: .github/workflows/release.yml
================================================
# This workflow will create a GitHub release every time a tag is pushed
name: Create GitHub Release
on:
push:
tags:
- "v2.*"
- "v3.*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: "marvinpinto/action-automatic-releases@v1.2.1"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false
================================================
FILE: .gitignore
================================================
_site
.cache
.DS_Store
cdn
dist
docs/assets/images/sprite.svg
node_modules
src/react
================================================
FILE: .gitpod.yml
================================================
tasks:
- init: npm install && npm run build
command: npm run start
ports:
- port: 3001
onOpen: ignore
- port: 4000-4999
onOpen: open-preview
github:
prebuilds:
# enable for the master/default branch (defaults to true)
master: true
# enable for all branches in this repo (defaults to false)
branches: true
# enable for pull requests coming from this repo (defaults to true)
pullRequests: true
# enable for pull requests coming from forks (defaults to false)
pullRequestsFromForks: true
# add a check to pull requests (defaults to true)
addCheck: true
# add a "Review in Gitpod" button as a comment to pull requests (defaults to false)
addComment: false
# add a "Review in Gitpod" button to the pull request's description (defaults to false)
addBadge: true
# add a label once the prebuild is ready to pull requests (defaults to false)
addLabel: true
================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install lint-staged
================================================
FILE: .prettierignore
================================================
*.hbs
.cache
.github
cspell.json
dist
docs/search.json
src/components/icon/icons
src/react/index.ts
node_modules
package.json
package-lock.json
tsconfig.json
cdn
_site
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bierner.lit-html",
"bashmish.es6-string-css",
"streetsidesoftware.code-spell-checker"
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Shoelace
Before contributing, please review the contributions guidelines at:
[shoelace.style/resources/contributing](https://shoelace.style/resources/contributing)
================================================
FILE: LICENSE.md
================================================
Copyright (c) 2020 A Beautiful Site, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# Shoelace is now Web Awesome 🧡!
> [!IMPORTANT]
> **Shoelace is in maintenance mode (LTS)**. It is no longer actively being developed but remains available for use under the MIT license. Critical fixes may be released as needed; there is no fixed end date.
> For active development and new features, check out Web Awesome at [https://webawesome.com](https://webawesome.com) and [https://github.com/shoelace-style/webawesome](https://github.com/shoelace-style/webawesome).
Web Awesome has an even larger library of free web [components](https://webawesome.com/docs/components/), plus [themes](https://webawesome.com/docs/themes/), [utilities](https://webawesome.com/docs/utilities/), [patterns](https://webawesome.com/docs/patterns/), and more.
---
# Shoelace
A forward-thinking library of web components.
- Works with all frameworks 🧩
- Works with CDNs 🚛
- Fully customizable with CSS 🎨
- Includes an official dark theme 🌛
- Built with accessibility in mind ♿️
- Open source 😸
---
- Documentation: [shoelace.style](https://shoelace.style)
- Shoelace Source (Maintenance Mode - LTS): [github.com/shoelace-style/shoelace](https://github.com/shoelace-style/shoelace)
- Web Awesome Source (Active Development): [github.com/shoelace-style/webawesome](https://github.com/shoelace-style/webawesome)
---
## Shoemakers 🥾
Shoemakers, or "Shoelace developers," can use this documentation to learn how to build Shoelace from source. You will need Node >= 14.17 to build and run the project locally.
**You don't need to do any of this to use Shoelace!** This page is for people who want to contribute to the project, tinker with the source, or create a custom build of Shoelace.
If that's not what you're trying to do, the [documentation website](https://shoelace.style) is where you want to be.
### What are you using to build Shoelace?
Components are built with [LitElement](https://lit-element.polymer-project.org/), a custom elements base class that provides an intuitive API and reactive data binding. The build is a custom script with bundling powered by [esbuild](https://esbuild.github.io/).
### Forking the Repo
Start by [forking the repo](https://github.com/shoelace-style/shoelace/fork) on GitHub, then clone it locally and install dependencies.
```bash
git clone https://github.com/YOUR_GITHUB_USERNAME/shoelace
cd shoelace
npm install
```
### Developing
Once you've cloned the repo, run the following command.
```bash
npm start
```
This will spin up the dev server. After the initial build, a browser will open automatically. There is currently no hot module reloading (HMR), as browsers don't provide a way to reregister custom elements, but most changes to the source will reload the browser automatically.
### Building
To generate a production build, run the following command.
```bash
npm run build
```
### Creating New Components
To scaffold a new component, run the following command, replacing `sl-tag-name` with the desired tag name.
```bash
npm run create sl-tag-name
```
This will generate a source file, a stylesheet, and a docs page for you. When you start the dev server, you'll find the new component in the "Components" section of the sidebar.
### Contributing
Shoelace is open source under the MIT license. Bug fixes and maintenance updates may still be considered; for new features and active development, see [Web Awesome](https://webawesome.com). If you want to contribute here, please review the [contribution guidelines](CONTRIBUTING.md) first.
## License
Shoelace is available under the terms of the MIT license.
Whether you're building Shoelace or building something _with_ Shoelace — have fun creating! 🥾
================================================
FILE: cspell.json
================================================
{
"version": "0.2",
"words": [
"activedescendant",
"allowfullscreen",
"animationend",
"Animista",
"apos",
"atrule",
"autocorrect",
"autofix",
"autoload",
"autoloader",
"autoloading",
"autoplay",
"bezier",
"Bokmål",
"boxicons",
"CACHEABLE",
"callout",
"callouts",
"cdndir",
"chatbubble",
"checkmark",
"claviska",
"Clippy",
"codebases",
"codepen",
"colocated",
"colour",
"combobox",
"Commonmark",
"Composability",
"Consolas",
"contenteditable",
"copydir",
"Cotte",
"coverpage",
"crossorigin",
"crutchcorn",
"csspart",
"cssproperty",
"datetime",
"describedby",
"Docsify",
"dogfood",
"dropdowns",
"easings",
"endraw",
"enterkeyhint",
"eqeqeq",
"erroneou",
"errormessage",
"esbuild",
"exportmaps",
"exportparts",
"fieldsets",
"formaction",
"formdata",
"formenctype",
"formmethod",
"formnovalidate",
"formtarget",
"FOUC",
"FOUCE",
"fullscreen",
"gestern",
"giga",
"globby",
"Grayscale",
"haspopup",
"heroicons",
"hexa",
"Iconoir",
"Iframes",
"iife",
"inputmode",
"ionicon",
"ionicons",
"jsDelivr",
"jsfiddle",
"keydown",
"keyframes",
"Kool",
"labelledby",
"Laravel",
"LaViska",
"linkify",
"listbox",
"listitem",
"litelement",
"lowercasing",
"Lucide",
"maxlength",
"Menlo",
"menuitemcheckbox",
"menuitemradio",
"middlewares",
"minlength",
"monospace",
"mousedown",
"mousemove",
"mouseout",
"mouseup",
"multiselectable",
"nextjs",
"nocheck",
"noopener",
"noreferrer",
"novalidate",
"npmdir",
"Numberish",
"onscrollend",
"outdir",
"ParamagicDev",
"peta",
"petabit",
"prismjs",
"progressbar",
"radiogroup",
"Railsbyte",
"remixicon",
"reregister",
"resizer",
"resizers",
"retargeted",
"RETRYABLE",
"rgba",
"roadmap",
"Roboto",
"roledescription",
"Sapan",
"saturationl",
"Schilp",
"scrollbars",
"scrollend",
"scroller",
"Segoe",
"semibold",
"sitedir",
"slotchange",
"smartquotes",
"spacebar",
"stylesheet",
"Tabbable",
"tabindex",
"tabler",
"tablist",
"tabpanel",
"templating",
"tera",
"testid",
"textareas",
"textfield",
"tinycolor",
"transitionend",
"treeitem",
"treeshaking",
"Triaging",
"turbolinks",
"typeof",
"unbundles",
"unbundling",
"unicons",
"unsanitized",
"unsupportive",
"valpha",
"valuenow",
"valuetext",
"vuejs",
"WEBP",
"Webpacker",
"wordmark"
],
"ignorePaths": [
"package.json",
"package-lock.json",
"docs/assets/examples/include.html",
".vscode/**",
"src/translations/!(en).ts",
"**/*.min.js"
],
"ignoreRegExpList": [
"(^|[^a-z])sl[a-z]*(^|[^a-z])"
],
"useGitignore": true
}
================================================
FILE: custom-elements-manifest.config.js
================================================
import * as path from 'path';
import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration';
import { customElementVsCodePlugin } from 'custom-element-vs-code-integration';
import { customElementVuejsPlugin } from 'custom-element-vuejs-integration';
import { parse } from 'comment-parser';
import { pascalCase } from 'pascal-case';
import commandLineArgs from 'command-line-args';
import fs from 'fs';
const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
const { name, description, version, author, homepage, license } = packageData;
const { outdir } = commandLineArgs([
{ name: 'litelement', type: String },
{ name: 'analyze', defaultOption: true },
{ name: 'outdir', type: String }
]);
function noDash(string) {
return string.replace(/^\s?-/, '').trim();
}
function replace(string, terms) {
terms.forEach(({ from, to }) => {
string = string?.replace(from, to);
});
return string;
}
export default {
globs: ['src/components/**/*.component.ts'],
exclude: ['**/*.styles.ts', '**/*.test.ts'],
plugins: [
// Append package data
{
name: 'shoelace-package-data',
packageLinkPhase({ customElementsManifest }) {
customElementsManifest.package = { name, description, version, author, homepage, license };
}
},
// Infer tag names because we no longer use @customElement decorators.
{
name: 'shoelace-infer-tag-names',
analyzePhase({ ts, node, moduleDoc }) {
switch (node.kind) {
case ts.SyntaxKind.ClassDeclaration: {
const className = node.name.getText();
const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className);
const importPath = moduleDoc.path;
// This is kind of a best guess at components. "thing.component.ts"
if (!importPath.endsWith('.component.ts')) {
return;
}
const tagNameWithoutPrefix = path.basename(importPath, '.component.ts');
const tagName = 'sl-' + tagNameWithoutPrefix;
classDoc.tagNameWithoutPrefix = tagNameWithoutPrefix;
classDoc.tagName = tagName;
// This used to be set to true by @customElement
classDoc.customElement = true;
}
}
}
},
// Parse custom jsDoc tags
{
name: 'shoelace-custom-tags',
analyzePhase({ ts, node, moduleDoc }) {
switch (node.kind) {
case ts.SyntaxKind.ClassDeclaration: {
const className = node.name.getText();
const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className);
const customTags = ['animation', 'dependency', 'documentation', 'since', 'status', 'title'];
let customComments = '/**';
node.jsDoc?.forEach(jsDoc => {
jsDoc?.tags?.forEach(tag => {
const tagName = tag.tagName.getText();
if (customTags.includes(tagName)) {
customComments += `\n * @${tagName} ${tag.comment}`;
}
});
});
// This is what allows us to map JSDOC comments to ReactWrappers.
classDoc['jsDoc'] = node.jsDoc?.map(jsDoc => jsDoc.getFullText()).join('\n');
const parsed = parse(`${customComments}\n */`);
parsed[0].tags?.forEach(t => {
switch (t.tag) {
// Animations
case 'animation':
if (!Array.isArray(classDoc['animations'])) {
classDoc['animations'] = [];
}
classDoc['animations'].push({
name: t.name,
description: noDash(t.description)
});
break;
// Dependencies
case 'dependency':
if (!Array.isArray(classDoc['dependencies'])) {
classDoc['dependencies'] = [];
}
classDoc['dependencies'].push(t.name);
break;
// Value-only metadata tags
case 'documentation':
case 'since':
case 'status':
case 'title':
classDoc[t.tag] = t.name;
break;
// All other tags
default:
if (!Array.isArray(classDoc[t.tag])) {
classDoc[t.tag] = [];
}
classDoc[t.tag].push({
name: t.name,
description: t.description,
type: t.type || undefined
});
}
});
}
}
}
},
{
name: 'shoelace-react-event-names',
analyzePhase({ ts, node, moduleDoc }) {
switch (node.kind) {
case ts.SyntaxKind.ClassDeclaration: {
const className = node.name.getText();
const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className);
if (classDoc?.events) {
classDoc.events.forEach(event => {
event.reactName = `on${pascalCase(event.name)}`;
event.eventName = `${pascalCase(event.name)}Event`;
});
}
}
}
}
},
{
name: 'shoelace-translate-module-paths',
packageLinkPhase({ customElementsManifest }) {
customElementsManifest?.modules?.forEach(mod => {
//
// CEM paths look like this:
//
// src/components/button/button.ts
//
// But we want them to look like this:
//
// components/button/button.js
//
const terms = [
{ from: /^src\//, to: '' }, // Strip the src/ prefix
{ from: /\.component.(t|j)sx?$/, to: '.js' } // Convert .ts to .js
];
mod.path = replace(mod.path, terms);
for (const ex of mod.exports ?? []) {
ex.declaration.module = replace(ex.declaration.module, terms);
}
for (const dec of mod.declarations ?? []) {
if (dec.kind === 'class') {
for (const member of dec.members ?? []) {
if (member.inheritedFrom) {
member.inheritedFrom.module = replace(member.inheritedFrom.module, terms);
}
}
}
}
});
}
},
// Generate custom VS Code data
customElementVsCodePlugin({
outdir,
cssFileName: null,
referencesTemplate: (_, tag) => [
{
name: 'Documentation',
url: `https://shoelace.style/components/${tag.replace('sl-', '')}`
}
]
}),
customElementJetBrainsPlugin({
outdir: './dist',
excludeCss: true,
packageJson: false,
referencesTemplate: (_, tag) => {
return {
name: 'Documentation',
url: `https://shoelace.style/components/${tag.replace('sl-', '')}`
};
}
}),
customElementVuejsPlugin({
outdir: './dist/types/vue',
fileName: 'index.d.ts',
componentTypePath: (_, tag) => `../../components/${tag.replace('sl-', '')}/${tag.replace('sl-', '')}.component.js`
})
]
};
================================================
FILE: docs/_includes/component.njk
================================================
{% extends "default.njk" %}
{# Find the component based on the `tag` front matter #}
{% set component = getComponent('sl-' + page.fileSlug) %}
{% block content %}
{# Determine the badge variant #}
{% if component.status == 'stable' %}
{% set badgeVariant = 'primary' %}
{% 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 #}
{{ component.name | classNameToComponentName }}
<{{ component.tagName }}> | {{ component.name }}
{% if component.summary %} {{ component.summary | markdownInline | safe }} {% endif %}
{# Markdown content #} {{ content | safe }} {# Importing #}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 cherry pick this component.
To import this component from the CDN using a script tag:
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{{ meta.version }}/{{ meta.cdndir }}/{{ component.path }}"></script>
To import this component from the CDN using a JavaScript import:
import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{{ meta.version }}/{{ meta.cdndir }}/{{ component.path }}';
To import this component using a bundler:
import '@shoelace-style/shoelace/{{ meta.npmdir }}/{{ component.path }}';
To import this component as a React component:
import {{ component.name }} from '@shoelace-style/shoelace/{{ meta.npmdir }}/react/{{ component.tagNameWithoutPrefix }}';
| Name | Description |
|---|---|
{% if slot.name %}
{{ slot.name }}
{% else %}
(default)
{% endif %}
|
{{ slot.description | markdownInline | safe }} |
Learn more about using slots.
{% endif %} {# Properties #} {% if component.properties.length %}| Name | Description | Reflects | Type | Default |
|---|---|---|---|---|
{{ prop.name }}
{% if prop.attribute | length > 0 %}
{% if prop.attribute != prop.name %}
{{ prop.attribute }}
|
{{ prop.description | markdownInline | safe }} |
{% if prop.reflects %}
|
{% if prop.type.text %}
{{ prop.type.text | trimPipes | markdownInline | safe }}
{% else %}
-
{% endif %}
|
{% if prop.default %}
{{ prop.default | markdownInline | safe }}
{% else %}
-
{% endif %}
|
updateComplete |
A read-only promise that resolves when the component has finished updating. |
Learn more about attributes and properties.
{% endif %} {# Events #} {% if component.events.length %}| Name | React Event | Description | Event Detail |
|---|---|---|---|
{{ event.name }} |
{{ event.reactName }} |
{{ event.description | markdownInline | safe }} |
{% if event.type.text %}
{{ event.type.text | trimPipes }}
{% else %}
-
{% endif %}
|
Learn more about events.
{% endif %} {# Methods #} {% if component.methods.length %}| Name | Description | Arguments |
|---|---|---|
{{ method.name }}() |
{{ method.description | markdownInline | safe }} |
{% if method.parameters.length %}
{% for param in method.parameters %}
{{ param.name }}: {{ param.type.text | trimPipes }}{% if not loop.last %},{% endif %}
{% endfor %}
{% else %}
-
{% endif %}
|
Learn more about methods.
{% endif %} {# Custom Properties #} {% if component.cssProperties.length %}| Name | Description | Default |
|---|---|---|
{{ cssProperty.name }} |
{{ cssProperty.description | markdownInline | safe }} | {{ cssProperty.default }} |
Learn more about customizing CSS custom properties.
{% endif %} {# CSS Parts #} {% if component.cssParts.length %}| Name | Description |
|---|---|
{{ cssPart.name }} |
{{ cssPart.description | markdownInline | safe }} |
Learn more about customizing CSS parts.
{% endif %} {# Animations #} {% if component.animations.length %}| Name | Description |
|---|---|
{{ animation.name }} |
{{ animation.description | markdownInline | safe }} |
Learn more about customizing animations.
{% endif %} {# Dependencies #} {% if component.dependencies.length %}This component automatically imports the following dependencies.
<{{ dependency }}>Web Awesome has an even bigger library of free web components. Plus themes, utilities, patterns, and more!
Still open source. Now with more awesome.
But it is still available for use and may receive updates as needed.
`. For example, a code field
* tagged with "html:preview" will be rendered as ``.
*
* 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;
};
================================================
FILE: docs/_utilities/markdown.cjs
================================================
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 => {
markdown.use(markdownItContainer, type, {
render: function (tokens, idx) {
if (tokens[idx].nesting === 1) {
return ``;
}
return '\n';
}
});
});
// Asides
markdown.use(markdownItContainer, 'aside', {
render: function (tokens, idx) {
if (tokens[idx].nesting === 1) {
return `\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 `\n${markdown.utils.escapeHtml(m[1])}
\n`;
}
return '\n';
}
});
// Replace [#1234] with a link to GitHub issues
markdownItReplaceIt.replacements.push({
name: 'github-issues',
re: /\[#([0-9]+)\]/gs,
sub: '#$1',
html: true,
default: true
});
module.exports = markdown;
================================================
FILE: docs/_utilities/prettier.cjs
================================================
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);
};
================================================
FILE: docs/_utilities/replacer.cjs
================================================
/**
* @typedef {object} Replacement
* @property {string | RegExp} pattern
* @property {string} replacement
*/
/**
* @typedef {Array} Replacements
*/
/**
* @param {String} rawContent
* @param {Replacements} replacements
*/
module.exports = function (rawContent, replacements) {
let content = rawContent;
replacements.forEach(replacement => {
content = content.replaceAll(replacement.pattern, replacement.replacement);
});
return content;
};
================================================
FILE: docs/_utilities/scrolling-tables.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) {
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;
};
================================================
FILE: docs/_utilities/strings.cjs
================================================
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;
};
================================================
FILE: docs/_utilities/table-of-contents.cjs
================================================
/**
* 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
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;
};
================================================
FILE: docs/_utilities/typography.cjs
================================================
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;
};
================================================
FILE: docs/assets/examples/include.html
================================================
The content in this example was included from
a separate file. 🤯
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi. Fringilla urna porttitor rhoncus dolor purus
non enim. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Gravida in fermentum et sollicitudin.
Cursus sit amet dictum sit amet justo donec enim. Sed id semper risus in hendrerit gravida. Viverra accumsan in nisl
nisi scelerisque eu ultrices vitae. Et molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit. Nec
ullamcorper sit amet risus nullam. Et egestas quis ipsum suspendisse ultrices gravida dictum. Lorem donec massa sapien
faucibus et molestie. A cras semper auctor neque vitae.
================================================
FILE: docs/assets/scripts/code-previews.js
================================================
(() => {
function convertModuleLinks(html) {
html = html
.replace(/@shoelace-style\/shoelace/g, `https://esm.sh/@shoelace-style/shoelace@${shoelaceVersion}`)
.replace(/from 'react'/g, `from 'https://esm.sh/react@${reactVersion}'`)
.replace(/from "react"/g, `from "https://esm.sh/react@${reactVersion}"`);
return html;
}
function getAdjacentExample(name, pre) {
let currentPre = pre.nextElementSibling;
while (currentPre?.tagName.toLowerCase() === 'pre') {
if (currentPre?.getAttribute('data-lang').split(' ').includes(name)) {
return currentPre;
}
currentPre = currentPre.nextElementSibling;
}
return null;
}
function runScript(script) {
const newScript = document.createElement('script');
if (script.type === 'module') {
newScript.type = 'module';
newScript.textContent = script.innerHTML;
} else {
newScript.appendChild(document.createTextNode(`(() => { ${script.innerHTML} })();`));
}
script.parentNode.replaceChild(newScript, script);
}
function getFlavor() {
return sessionStorage.getItem('flavor') || 'html';
}
function setFlavor(newFlavor) {
flavor = ['html', 'react'].includes(newFlavor) ? newFlavor : 'html';
sessionStorage.setItem('flavor', flavor);
// Set the flavor class on the body
document.documentElement.classList.toggle('flavor-html', flavor === 'html');
document.documentElement.classList.toggle('flavor-react', flavor === 'react');
}
function syncFlavor() {
setFlavor(getFlavor());
document.querySelectorAll('.code-preview__button--html').forEach(preview => {
if (flavor === 'html') {
preview.classList.add('code-preview__button--selected');
}
});
document.querySelectorAll('.code-preview__button--react').forEach(preview => {
if (flavor === 'react') {
preview.classList.add('code-preview__button--selected');
}
});
}
const shoelaceVersion = document.documentElement.getAttribute('data-shoelace-version');
const reactVersion = '^18';
const cdndir = 'cdn';
const npmdir = 'dist';
let flavor = getFlavor();
let count = 1;
// We need the version to open
if (!shoelaceVersion) {
throw new Error('The data-shoelace-version attribute is missing from .');
}
// Sync flavor UI on page load
syncFlavor();
//
// Resizing previews
//
document.addEventListener('mousedown', handleResizerDrag);
document.addEventListener('touchstart', handleResizerDrag, { passive: true });
function handleResizerDrag(event) {
const resizer = event.target.closest('.code-preview__resizer');
const preview = event.target.closest('.code-preview__preview');
if (!resizer || !preview) return;
let startX = event.changedTouches ? event.changedTouches[0].pageX : event.clientX;
let startWidth = parseInt(document.defaultView.getComputedStyle(preview).width, 10);
event.preventDefault();
preview.classList.add('code-preview__preview--dragging');
document.documentElement.addEventListener('mousemove', dragMove);
document.documentElement.addEventListener('touchmove', dragMove);
document.documentElement.addEventListener('mouseup', dragStop);
document.documentElement.addEventListener('touchend', dragStop);
function dragMove(event) {
const width = startWidth + (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - startX;
preview.style.width = `${width}px`;
}
function dragStop() {
preview.classList.remove('code-preview__preview--dragging');
document.documentElement.removeEventListener('mousemove', dragMove);
document.documentElement.removeEventListener('touchmove', dragMove);
document.documentElement.removeEventListener('mouseup', dragStop);
document.documentElement.removeEventListener('touchend', dragStop);
}
}
//
// Toggle source mode
//
document.addEventListener('click', event => {
const button = event.target.closest('.code-preview__button');
const codeBlock = button?.closest('.code-preview');
if (button?.classList.contains('code-preview__button--html')) {
// Show HTML
setFlavor('html');
toggleSource(codeBlock, true);
} else if (button?.classList.contains('code-preview__button--react')) {
// Show React
setFlavor('react');
toggleSource(codeBlock, true);
} else if (button?.classList.contains('code-preview__toggle')) {
// Toggle source
toggleSource(codeBlock);
} else {
return;
}
// Update flavor buttons
[...document.querySelectorAll('.code-preview')].forEach(cb => {
cb.querySelector('.code-preview__button--html')?.classList.toggle(
'code-preview__button--selected',
flavor === 'html'
);
cb.querySelector('.code-preview__button--react')?.classList.toggle(
'code-preview__button--selected',
flavor === 'react'
);
});
});
function toggleSource(codeBlock, force) {
codeBlock.classList.toggle('code-preview--expanded', force);
event.target.setAttribute('aria-expanded', codeBlock.classList.contains('code-preview--expanded'));
}
//
// Open in CodePen
//
document.addEventListener('click', event => {
const button = event.target.closest('button');
if (button?.classList.contains('code-preview__button--codepen')) {
const codeBlock = button.closest('.code-preview');
const htmlExample = codeBlock.querySelector('.code-preview__source--html > pre > code')?.textContent;
const reactExample = codeBlock.querySelector('.code-preview__source--react > pre > code')?.textContent;
const isReact = flavor === 'react' && typeof reactExample === 'string';
const theme = document.documentElement.classList.contains('sl-theme-dark') ? 'dark' : 'light';
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = theme === 'dark' || (theme === 'auto' && prefersDark);
const editors = isReact ? '0010' : '1000';
let htmlTemplate = '';
let jsTemplate = '';
let cssTemplate = '';
const form = document.createElement('form');
form.action = 'https://codepen.io/pen/define';
form.method = 'POST';
form.target = '_blank';
// HTML templates
if (!isReact) {
htmlTemplate =
`\n` +
`\n${htmlExample}`;
jsTemplate = '';
}
// React templates
if (isReact) {
htmlTemplate = '';
jsTemplate =
`import React from 'https://esm.sh/react@${reactVersion}';\n` +
`import ReactDOM from 'https://esm.sh/react-dom@${reactVersion}';\n` +
`import { setBasePath } from 'https://esm.sh/@shoelace-style/shoelace@${shoelaceVersion}/${cdndir}/utilities/base-path';\n` +
`\n` +
`// Set the base path for Shoelace assets\n` +
`setBasePath('https://esm.sh/@shoelace-style/shoelace@${shoelaceVersion}/${npmdir}/')\n` +
`\n${convertModuleLinks(reactExample)}\n` +
`\n` +
`ReactDOM.render( , document.getElementById('root'));`;
}
// CSS templates
cssTemplate =
`@import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${shoelaceVersion}/${cdndir}/themes/${
isDark ? 'dark' : 'light'
}.css';\n` +
'\n' +
'body {\n' +
' font: 16px sans-serif;\n' +
' background-color: var(--sl-color-neutral-0);\n' +
' color: var(--sl-color-neutral-900);\n' +
' padding: 1rem;\n' +
'}';
// Docs: https://blog.codepen.io/documentation/prefill/
const data = {
title: '',
description: '',
tags: ['shoelace', 'web components'],
editors,
head: ``,
html_classes: `sl-theme-${isDark ? 'dark' : 'light'}`,
css_external: ``,
js_external: ``,
js_module: true,
js_pre_processor: isReact ? 'babel' : 'none',
html: htmlTemplate,
css: cssTemplate,
js: jsTemplate
};
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'data';
input.value = JSON.stringify(data);
form.append(input);
document.documentElement.append(form);
form.submit();
form.remove();
}
});
// Set the initial flavor
window.addEventListener('turbo:load', syncFlavor);
})();
================================================
FILE: docs/assets/scripts/docs.js
================================================
//
// 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();
})();
//
// Theme selector
//
(() => {
function getTheme() {
return localStorage.getItem('theme') || 'auto';
}
function isDark() {
if (theme === 'auto') {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return theme === 'dark';
}
function setTheme(newTheme) {
theme = newTheme;
localStorage.setItem('theme', theme);
// Update the UI
updateSelection();
// Toggle the dark mode class
document.documentElement.classList.toggle('sl-theme-dark', isDark());
}
function updateSelection() {
const menu = document.querySelector('#theme-selector sl-menu');
if (!menu) return;
[...menu.querySelectorAll('sl-menu-item')].map(item => (item.checked = item.getAttribute('value') === theme));
}
let theme = getTheme();
// Selection is not preserved when changing page, so update when opening dropdown
document.addEventListener('sl-show', event => {
const themeSelector = event.target.closest('#theme-selector');
if (!themeSelector) return;
updateSelection();
});
// Listen for selections
document.addEventListener('sl-select', event => {
const menu = event.target.closest('#theme-selector sl-menu');
if (!menu) return;
setTheme(event.detail.item.value);
});
// Update the theme when the preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => setTheme(theme));
// Toggle with backslash
document.addEventListener('keydown', event => {
if (
event.key === '\\' &&
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
) {
event.preventDefault();
setTheme(isDark() ? 'light' : 'dark');
}
});
// Set the initial theme and sync the UI
setTheme(theme);
})();
//
// 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);
})();
================================================
FILE: docs/assets/scripts/search.js
================================================
(() => {
// 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 = `
`;
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-shoelace-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