Compare commits

...

94 Commits
v3.0.0 ... next

Author SHA1 Message Date
Brian Talbot
6e73f329e0 Sync Up Layout Blocks with Page Component Slots (#1712)
* revising + adding Page component-based blocks to base.njk

* skip base classes (no tag names)

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2026-01-15 16:01:34 -05:00
Konnor Rogers
23df0591a6 Konnorrogers/composed path submenu (#1956)
* Search event composed path for submenu item

* composed path submenu fixes

* update changelog

* prettier

* fix if tag name doesn't exist

* prettier

* fix llms.js

* prettier

* add comment

* fix broken tests

* prettier

---------

Co-authored-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
2026-01-15 13:40:29 -05:00
Cory LaViska
40ba153499 prettier 2026-01-15 13:08:36 -05:00
Cory LaViska
5200b95f79 update changelog (#1955) 2026-01-15 13:02:47 -05:00
Konnor Rogers
ac955f7ca9 fix exports for utilities and events to be accessible to bundlers and actually built. (#1816)
* fix exports

* add changelog entry

* Update build.js
2026-01-15 13:02:38 -05:00
S. Ali Mihandoost
1c4b7c7410 chore(translation): update persian translation (#1923) 2026-01-15 13:01:14 -05:00
Konnor Rogers
8144ba3d69 porting shoelace framework docs to web awesome (and CEM goodies) (#1895)
* porting shoelace docs to web awesome

* update cem versions

* update framework docs

* update docs to remove the helper.

* Update packages/webawesome/custom-elements-manifest.js

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2026-01-15 12:58:41 -05:00
Cory LaViska
093ed3944b Add and enable CEM plugin for llms.txt (#1894)
* add and enable cem plugin for llms.txt

* add docs and update sidebra
2026-01-15 12:55:39 -05:00
Cory LaViska
6a03c17b34 add parts to circles; fixes #1863 (#1954) 2026-01-15 12:37:56 -05:00
Cory LaViska
71fce016cf update changelog (#1953) 2026-01-15 12:25:49 -05:00
Wendelin
c8ddc2c1c0 Fix tree-item initial RTL check (#1798)
* Fix tree-item initial RTL check

* Update packages/webawesome/src/components/tree-item/tree-item.ts

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2026-01-15 12:23:41 -05:00
Alan Chambers
64a9374aa9 wa-dialog docs - clarify prevent dialog closing documentation (#1831)
Updated the dialog documentation to match newer default behaviour (non light-dismiss) and to describe the example more accurately.
2026-01-15 12:05:40 -05:00
Cory LaViska
67785f64cb update changelog (#1952) 2026-01-15 11:48:26 -05:00
Leon Vogt
39ab883802 Set default cursor for empty <wa-tree-item> expand buttons (#1936) 2026-01-15 11:46:08 -05:00
Lindsay M
08ef969db3 Add missing layout utility descriptions (#1946) 2026-01-15 11:31:29 -05:00
Konnor Rogers
72c4fb7af7 update plop templates (#1940) 2026-01-15 11:14:06 -05:00
Cory LaViska
827d70b2f5 Fix popover and dropdown bugs (#1913)
* fix popover bug; closes #1911

* fixes #1911

* reword guard
2026-01-15 09:53:54 -05:00
Lindsay M
46aa1429a7 Apply flexbox by default with wa-align-items-* and wa-justify-content-* utils (#1943)
* add `display: flex` with 0 specificity to utils

* add changelog
2026-01-13 15:44:37 -05:00
Brian Talbot
4b68a741bd abstracting layout docs example styling (#1935) 2026-01-13 15:31:37 -05:00
Cory LaViska
bd58f7f0a1 Divider docs (#1945)
* don't you know me

* clarify and add note
2026-01-13 15:08:25 -05:00
Alex Rao
e8f366835c Update tooltip.ts (#1928)
This removes the second period in the docs, which gets reflected on the website
2026-01-13 14:52:17 -05:00
Cory LaViska
70df6fea1f fix drag bug; closes #1905 (#1908) 2026-01-13 14:12:29 -05:00
Lindsay M
d76bef5dbb Unlist unreleased utilities from the docs (#1938) 2026-01-13 11:17:36 -05:00
Lindsay M
f8f54d543a Add justify-content utility classes (#1930)
* add `justify-content` utilities

* add docs

* fix up Align Items docs

* add Justify Content to sidebar

* add changelog

* add PR to changelog

* drop specificity of `justify-content` in layout utilities
2026-01-12 16:53:00 -05:00
Lindsay M
5b0e3ed1e8 Add missing .wa-gap-4xl utility class (#1931)
* add missing `.wa-gap-4xl`

* update the docs

* update changelog

* add PR to changelog
2026-01-12 16:35:01 -05:00
Brian Talbot
09ace44c17 A Glow Up for Dialogs That Pop Up (#1927)
* adding visual divider + padding to wa-dialog footers

* adding `wa-button`-focused `.shush` to `utils.css`
2026-01-09 14:52:23 -05:00
Lindsay M
f3de45803f Ensure consistent input heights in flex/grid containers (#1915)
* apply flex rules to `form-control` parts

* prevent input heights from growing in flex containers

* change approach: ignore `form-control` wrapper for styling
2025-12-31 11:35:37 -05:00
Steve Matney
ede1f0725d Ensuring styles apply for input labels when using 'slot="label"' (#1757)
* Ensuring styles apply for input labels when using 'slot="label"'

* Using 'has-slotted' class for codebase consistency

* Fleshing out 'has-slotted' label class to relevant form elements

* Ensuring label line height uses the correct token; removing unrelated PR changes

* Using 'has-label' class instead of 'has-slotted' for labels; applying it with all label content
2025-12-30 12:13:28 -05:00
Kelsey Jackson
56b1196265 got it working, but may revert to use on repo (#1888) 2025-12-19 10:54:45 -06:00
Cory LaViska
7e5f18ea97 update changelog (#1889) 2025-12-18 14:25:22 -05:00
Brian Talbot
c748721f12 adding event tracking to site search (#1872) 2025-12-17 15:22:18 -05:00
konnorrogers
6e5a720f51 update lockfiles and prettierignore 2025-12-16 13:42:13 -05:00
konnorrogers
f739121aaf updae lockfile 2025-12-16 13:30:16 -05:00
konnorrogers
5fb3625ee5 fix script 2025-12-16 13:22:32 -05:00
konnorrogers
da206a8787 update versions 2025-12-16 13:21:46 -05:00
konnorrogers
2b69fa74a5 Bump package.json version 2025-12-16 13:21:23 -05:00
konnorrogers
d1b6628d59 update package lock 2025-12-16 11:48:56 -05:00
konnorrogers
4dca4185e7 Bump changelog 2025-12-16 11:48:29 -05:00
Konnor Rogers
1e6d468959 add changelog entry (#1877) 2025-12-16 11:46:01 -05:00
Cory LaViska
00b8150f3e ensure min/max/step are numbers; fixes #1823 (#1832)
Co-authored-by: konnorrogers <konnor5456@gmail.com>
2025-12-16 11:06:27 -05:00
Konnor Rogers
3d395653fc Bug fix: el.form = 'foo' no longer sets the el.form property (#1815)
* Bug fix: el.form = 'foo' no longer sets the el.form property

* add changelog entry

* add changelog entry

* prettier

* fix form

* skip style attr for button

* prettier
2025-12-16 10:59:40 -05:00
Konnor Rogers
b3844ef77f fixing deploy scripts, and updating root version (#1857)
* fixing deploy scripts

* update root version script

* prettier
2025-12-16 10:29:26 -05:00
Lindsay M
a50648c6e8 Update changelog with leftover items (#1874)
* add leftover logs

* remove video component log
2025-12-15 17:49:56 -05:00
Cory LaViska
b6b82fb0ac Revert "updated sidebar (#1873)" (#1875)
This reverts commit fa073e8d21.
2025-12-15 15:59:38 -05:00
Kelsey Jackson
fa073e8d21 updated sidebar (#1873)
* updated sidebar

* added video item

* fixed formatting

* update

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2025-12-15 15:01:32 -05:00
Cory LaViska
e0f6ff11ec Combobox (supporting contributions in free) (#1838)
* hold the mustard! let's make it yellower

* words

* update sidebar

* update changelog

* fix icon cropping

* add combobox support

* preserve user-selected order

* add quick pro flag

* move import to the top

* fix custom tag example
2025-12-11 13:04:42 -05:00
Cory LaViska
895fb304e0 Revert .css => .styles.ts (#1865)
* revert .css => .styles.ts; fixes #1812

* fix native styles and vh util

* remove layers in component CSS

* add layer
2025-12-11 11:32:51 -05:00
Brian Talbot
079c09b9a3 Space Junk Analytics: Remove Plausible Code + Details (#1801)
* removing plausible-based code

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-10 09:38:46 -05:00
Brian Talbot
fa6ccc1ee2 adding cookie consent dialog to base layout (#1840) 2025-12-09 12:10:24 -05:00
David Enke
579e86fc49 fix(wa-button): add missing css transition property (#1850)
fixes #1844
2025-12-08 13:49:22 -05:00
Cory LaViska
2baebca230 remove old and unsupported example (#1842) 2025-12-08 13:37:00 -05:00
Patrick McDougle
c0ced13f06 Add Intersection Observer component to the Utilities Category (#1837) 2025-12-02 11:29:04 -05:00
Cory LaViska
8ae90ef2b2 add missing icon dep (#1828) 2025-11-25 16:35:31 -05:00
Konnor Rogers
5fb6faf527 update webawesome simulation to be closer to the app. (#1789)
* update webawesome simulation to be closer to the app.

* prettier
2025-11-25 12:09:56 -05:00
Cory LaViska
fa558eeda0 update npm-check-updates and set a cooldown (#1820) 2025-11-24 15:58:08 -05:00
Brian Talbot
ed3cb6d782 marking planned pro components in sidebar (#1819) 2025-11-24 12:31:15 -05:00
Lindsay M
09c8f02f79 Use tokens for <wa-dropdown-item> transitions (#1693)
* use tokens for `<wa-dropdown-item>` transitions

* add changelog
2025-11-19 17:50:47 -05:00
Lindsay M
d6fe9f7819 Define and import cascade layers (#1793)
* define cascade layers

* invoke cascade layers in kitchen sink styles

* invoke cascade layers in utilities and native styles

* invoke cascade layers in palettes + consolidate shared palette rules

* invoke cascade layers in themes

* add changelog

* prettier demands whitespace
2025-11-19 17:43:55 -05:00
Cory LaViska
200b02f023 fix scroller styles; closes #1724 (#1785) 2025-11-19 07:34:22 -05:00
Cory LaViska
1429095eed Fix SVG fill in <wa-icon> (#1784)
* fix svg fill; closes #1733

* remove fill
2025-11-19 07:33:56 -05:00
Cory LaViska
9706964511 Fix spinner when <wa-tree-item> is lazy loaded (#1786)
* fix spinner when lazy; closes #1678

* remove redundant entry
2025-11-18 15:31:51 -05:00
Cory LaViska
bb0a961784 remove offending property; fixes #1614 (#1792) 2025-11-18 15:29:57 -05:00
Jesse Jurman
e2f2a8a0d3 Fix: Allow wa-hide to be cancelled in WaDropdown (#1774)
* Fix: Allow wa-hide to be cancelled in WaDropdown

* add tests around open attribute / property

* update changelog
2025-11-18 14:07:53 -05:00
Christian Oliff
86293cc3e1 Remove width property from select component (#1736)
Deleted the duplicate 'width: 100%' style from the select component (already defined on line 56). Also fixed a typo in a comment in the docs CSS. You should setup Stylelint to prevent these issues :-)
2025-11-18 10:03:29 -05:00
Fred
8fb521d9ef Adding a reflect property option to the name property (#1716) 2025-11-17 22:15:18 +00:00
Kelsey Jackson
2491ca45ac Fix appearance="filled-outlined" in <wa-card> (#1694)
* upated filled-outlined attribute for cards

* updated selector

* updated changelog

* updated heading

* updated css

* remove final `~=` selector (thanks @Copilot)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Lindsay M <126139086+lindsaym-fa@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-17 15:12:23 -05:00
Cory LaViska
411d385d65 improve icon perf; closes #1729 (#1783) 2025-11-17 14:49:45 -05:00
Dimitri
634828d89a fix(wa-icon): add waitUntilFirstUpdate: true option to the setIcon() watch decorator (#1738) 2025-11-17 14:49:32 -05:00
Cory LaViska
17fd3d238c fix typo; closes #1713 2025-11-17 14:38:40 -05:00
Konnor Rogers
ab0f2a1411 add manifest json file copying (#1780)
* add manifest json file copying

* update comment

* prettier
2025-11-17 11:31:26 -05:00
Cory LaViska
4069360242 fix card slot detection (#1779) 2025-11-17 09:51:11 -05:00
Owarie
8c51c5b933 fix: re-add page metadata (#1776) 2025-11-17 09:47:14 -05:00
Brian Talbot
5a6ce1c4d3 Gating Launch Sale-Based Banner and Dialog (#1769)
* gating wa-launch banner and dialog based on coupon state

* addressing PR feedback

* Update packages/webawesome/docs/_includes/_dialog-wa-launch.njk

Co-authored-by: Konnor Rogers <konnor5456@gmail.com>

---------

Co-authored-by: Konnor Rogers <konnor5456@gmail.com>
2025-11-13 11:46:55 -05:00
Brian Talbot
e695b54dcd Revert Custom Last Modified Utility (#1750)
* reverting custom `getLastModifiedISO` utility

* using 11ty's `git Last Modified` on docs/resources/changelog

* dumbing down "Last updated" date to be static
2025-11-13 11:25:29 -05:00
Brian Talbot
9a3fc59e6a forcing brand color on launch sale dialog (#1764)
* to override any docs-based brand
2025-11-12 15:21:28 -05:00
Brian Talbot
dfaa959951 Add Launch Sale Dialog (#1739)
* adding site-wide dialog to base.njk

* adding logic to control dialog rendering

* adding sale-based site-wide dialog

* overriding docs brand props for `#dialog-site`

* adding plausible event tracking to sale-based site-wide dialog
2025-11-12 14:46:04 -05:00
Brian Talbot
9d6ded49b9 Fix Broken Docs Links (#1740)
* fixing broken links in docs/resources/contributing.md

* removing broken links in docs/resources/changelog.md

* fixing broken link in docs/components/button-group.md

* fixing broken link in docs/components/comparison.md

* fixing broken link in docs/components/divider.md

* fixing broken link in docs/components/radio-group.md

* fixing broken link in docs/utilities/frame.md

* fixing broken link in docs/utilities/gap.md

* fixing broken link in docs/utilities/rounding.md

* Apply suggestions from code review

Co-authored-by: Lindsay M <126139086+lindsaym-fa@users.noreply.github.com>

* addressing PR feedback

---------

Co-authored-by: Lindsay M <126139086+lindsaym-fa@users.noreply.github.com>
2025-11-07 13:35:09 -05:00
Brian Talbot
3451255454 Docs: Add Free Badge (#1696)
* adding Free Badge macro

* using semantic color value for Pro Badge's `color`

* removing unncessary `border-color` from Pro Badge's custom CSS

* adding `data-pro-badge` attr to Pro Badge

* using freeBadge and proBadge macros in themes.njk
2025-11-06 13:12:57 -05:00
Brian Talbot
20628a98eb 🧼 Anchor Link Clean Up (#1732)
* using wa-icon in generated anchors

* moving anchor-heading styling from docs.css to utils.css

* improving anchor-heading styling

* adding subtle transition
* using --wa-space-3xs custom property for padding
2025-11-06 11:13:54 -05:00
Brian Talbot
3d4b3ada27 Allow Setting data-no-anchor at the View-Level (#1718)
* adding option to set 11ty's `data-no-anchor` at view-level

* moving all `base.njk` initializations to before `<html>`

* updating anchor-headings.js transformer logic
2025-11-05 14:06:56 -05:00
Lindsay M
0bead172fe Tweak <wa-radio appearance="button"> focus styles (#1695) 2025-11-05 13:31:40 -05:00
Christian Oliff
9ca3e6f39c Fix typo in 'Github' heading to 'GitHub' (#1720) 2025-11-05 07:25:13 -05:00
Brian Talbot
e2c7286ccc Control 11ty-Generated Headings' Display via Front Matter (#1715)
* controlling 11ty generated headings' display via frontmatter

* addressing PR feedback

* addressing additional PR feedback
2025-11-04 14:10:59 -05:00
Cory LaViska
46812e08c5 fix slider on touch; fixes #1703 (#1709) 2025-11-03 09:43:21 -05:00
Brian Talbot
0f601df74a removing page title suffixes from search results (#1697)
* Use <title> for search indexing but strip trailing site suffix at
build time
* centralize title logic with shared composePageTitle() and
stripSiteTitleSuffix() helpers
* introduce single source of truth for site name and separators
* keep client search rendering simple; no post-processing in the browser
* preserve existing page/OG title behavior while improving search
relevance
2025-10-31 14:03:39 -04:00
Cory LaViska
eb2410ff88 fix links (#1691) 2025-10-30 11:29:30 -04:00
Cory LaViska
479fff4b61 remove unsupported property; closes #1677 (#1689) 2025-10-30 11:06:47 -04:00
Brian Talbot
9c7b341889 adding planned stretch goal-based components to sidebar.njk (#1682) 2025-10-30 10:41:50 -04:00
Kelsey Jackson
d09973b571 updated sidebar (#1687)
* updated sidebar

* Update packages/webawesome/docs/_includes/sidebar.njk

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fixed pr

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-30 09:22:39 -05:00
Cory LaViska
a03da137e3 nest tree item (#1686) 2025-10-30 09:37:19 -04:00
Brian Talbot
d8b0447151 Docs: Adding CSS Selector Info to Components (#1679)
* adding CSS selector info to component docs

* adding Copy Button to custom states' CSS selectors in component docs
2025-10-29 17:25:50 -04:00
konnorrogers
6f0bd4e872 update versions.txt 2025-10-28 12:19:06 -04:00
konnorrogers
55973a7eff update package-lock 2025-10-28 12:15:46 -04:00
konnorrogers
e9aa72cc87 Bump package.json version 2025-10-28 12:14:39 -04:00
249 changed files with 9038 additions and 6161 deletions

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@ yarn.lock
_bundle_
/packages/webawesome-pro
/packages/webawesome-app
custom-elements.json

View File

@@ -20,4 +20,5 @@ packages/**/*/src/react/index.ts
node_modules
packages/**/*/_site
packages/**/*/_bundle_
packages/webawesome/docs/assets/scripts/prism-downloaded.js

View File

@@ -1,2 +1,2 @@
3.0.0-beta.5
3.0.0-beta.6
3.0.0
3.1.0

View File

@@ -70,6 +70,7 @@
"exportparts",
"fetchpriority",
"fieldsets",
"flexbox",
"focusin",
"focusout",
"fontawesome",
@@ -105,6 +106,7 @@
"keydown",
"keyframes",
"keymaker",
"Kickstarter",
"Konnor",
"Kool",
"labelledby",
@@ -113,10 +115,14 @@
"listbox",
"listitem",
"litelement",
"llm",
"llms",
"llmstxt",
"longform",
"lowercasing",
"Lucide",
"maxlength",
"mdash",
"Menlo",
"menuitemcheckbox",
"menuitemradio",
@@ -130,6 +136,7 @@
"mouseout",
"mouseup",
"multiselectable",
"nbsp",
"nextjs",
"nocheck",
"noindex",
@@ -148,6 +155,7 @@
"ParamagicDev",
"peta",
"petabit",
"pointercancel",
"Preact",
"preconnect",
"prerendered",
@@ -179,6 +187,7 @@
"shadowrootmode",
"Shortcode",
"Shortcodes",
"signup",
"sitedir",
"slotchange",
"smartquotes",
@@ -200,6 +209,8 @@
"thead",
"Themer",
"tinycolor",
"touchcancel",
"touchend",
"transitionend",
"treeitem",
"treeshaking",
@@ -233,8 +244,6 @@
"src/translations/!(en).ts",
"**/*.min.js"
],
"ignoreRegExpList": [
"(^|[^a-z])sl[a-z]*(^|[^a-z])"
],
"ignoreRegExpList": ["(^|[^a-z])sl[a-z]*(^|[^a-z])"],
"useGitignore": true
}

860
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@webawesome/monorepo",
"private": true,
"description": "A forward-thinking library of web components.",
"version": "3.0.0-alpha.13",
"version": "3.1.0",
"homepage": "https://webawesome.com/",
"author": "Web Awesome",
"license": "MIT",
@@ -13,20 +13,24 @@
"scripts": {
"check-updates": "npx npm-check-updates --interactive --format group",
"start": "cd packages/webawesome && npm run start",
"start:pro": "cd packages/webawesome-pro && npm run start"
"build": "cd packages/webawesome && npm run build",
"start:pro": "cd packages/webawesome-pro && npm run start",
"build:pro": "cd packages/webawesome-pro && npm run build"
},
"engines": {
"node": ">=14.17.0"
},
"devDependencies": {
"@11ty/eleventy": "3.0.0",
"@custom-elements-manifest/analyzer": "^0.10.4",
"@custom-elements-manifest/analyzer": "^0.11.0",
"@lit-labs/eleventy-plugin-lit": "^1.0.3",
"@lit-labs/testing": "^0.2.5",
"@lit/react": "^1.0.8",
"@open-wc/testing": "^3.2.0",
"@types/mocha": "^10.0.10",
"@types/react": "^18.2.28",
"@wc-toolkit/cem-inheritance": "^1.2.2",
"@wc-toolkit/type-parser": "^1.2.0",
"@web/dev-server-esbuild": "^0.3.6",
"@web/test-runner": "^0.19.0",
"@web/test-runner-commands": "^0.9.0",
@@ -39,9 +43,10 @@
"command-line-args": "^5.2.1",
"comment-parser": "^1.4.1",
"cspell": "^6.18.1",
"custom-element-jet-brains-integration": "^1.6.2",
"custom-element-vs-code-integration": "^1.4.1",
"custom-element-vuejs-integration": "^1.3.3",
"custom-element-jet-brains-integration": "^1.7.0",
"custom-element-svelte-integration": "^1.2.0",
"custom-element-vs-code-integration": "^1.5.0",
"custom-element-vuejs-integration": "^1.4.0",
"del": "^7.1.0",
"download": "^8.0.0",
"esbuild": "0.23.1",

View File

@@ -1,12 +1,17 @@
import { cemInheritancePlugin } from '@wc-toolkit/cem-inheritance';
import { cemValidatorPlugin } from '@wc-toolkit/cem-validator';
import { jsxTypesPlugin } from '@wc-toolkit/jsx-types';
import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration';
import { customElementVsCodePlugin } from 'custom-element-vs-code-integration';
// import { customElementVuejsPlugin } from 'custom-element-vuejs-integration';
import { getTsProgram, typeParserPlugin } from '@wc-toolkit/type-parser';
import { parse } from 'comment-parser';
import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration';
import { customElementSveltePlugin } from 'custom-element-svelte-integration';
import { customElementVsCodePlugin } from 'custom-element-vs-code-integration';
import { customElementVuejsPlugin } from 'custom-element-vuejs-integration';
import fs from 'fs';
import * as path from 'node:path';
import { pascalCase } from 'pascal-case';
import * as url from 'url';
import { llmsTxtPlugin } from './scripts/llms.js';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const packageData = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
@@ -22,11 +27,20 @@ function replace(string, terms) {
}
export default {
globs: ['src/components/**/*.ts'],
// `src/components/**/*.ts` will ignore src/internal breaking inheritance chains.
globs: ['src/**/*.ts'],
exclude: ['**/*.styles.ts', '**/*.test.ts'],
litelement: true,
dependencies: true,
outdir,
// Give the plugin access to the TypeScript type checker
overrideModuleCreation({ ts, globs }) {
const program = getTsProgram(ts, globs, 'tsconfig.json');
return program.getSourceFiles().filter(sf => globs.find(glob => sf.fileName.includes(glob)));
},
plugins: [
typeParserPlugin(),
// Append package data
{
name: 'wa-package-data',
@@ -35,6 +49,11 @@ export default {
},
},
cemInheritancePlugin({
fileName: 'custom-elements.json',
outdir,
}),
// Parse custom jsDoc tags
{
name: 'wa-custom-tags',
@@ -115,7 +134,6 @@ export default {
}
},
},
{
name: 'wa-translate-module-paths',
packageLinkPhase({ customElementsManifest }) {
@@ -152,7 +170,6 @@ export default {
});
},
},
// Generate custom VS Code data
customElementVsCodePlugin({
outdir,
@@ -167,7 +184,7 @@ export default {
// Generate custom JetBrains data
customElementJetBrainsPlugin({
outdir: './dist-cdn',
outdir,
excludeCss: true,
packageJson: false,
referencesTemplate: (_, tag) => {
@@ -188,13 +205,27 @@ export default {
},
}),
// Generate llms.txt
llmsTxtPlugin({
outdir,
docsDir: path.join(__dirname, 'docs'),
baseUrl: 'https://webawesome.com',
}),
//
// TODO - figure out why this broke when events were updated
//
// customElementVuejsPlugin({
// outdir: './dist/types/vue',
// fileName: 'index.d.ts',
// componentTypePath: (_, tag) => `../../components/${tag.replace('wa-', '')}/${tag.replace('wa-', '')}.js`
// })
customElementVuejsPlugin({
outdir: './dist-cdn/types/vue',
fileName: 'index.d.ts',
componentTypePath: (_, tag) => `../../components/${tag.replace('wa-', '')}/${tag.replace('wa-', '')}.js`,
}),
customElementSveltePlugin({
outdir: './dist-cdn/types/svelte',
fileName: 'index.d.ts',
}),
// cemValidatorPlugin({
// cemFileName: "./dist-cdn/custom-elements.json"
// }),
],
};

View File

@@ -1,6 +1,5 @@
import { nanoid } from 'nanoid';
import { parse as HTMLParse } from 'node-html-parser';
import { execFileSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { anchorHeadingsTransformer } from './_transformers/anchor-headings.js';
@@ -21,7 +20,6 @@ import { replaceTextPlugin } from './_plugins/replace-text.js';
import { searchPlugin } from './_plugins/search.js';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const isDev = process.argv.includes('--develop');
const ignoreGit = process.env.ELEVENTY_IGNORE_GIT === 'true';
const passThroughExtensions = ['js', 'css', 'png', 'svg', 'jpg', 'mp4'];
async function getPackageData() {
@@ -60,15 +58,6 @@ export default async function (eleventyConfig) {
if (updateComponentData) {
allComponents = getComponents();
}
// Invalidate last-modified cache for changed content files during watch
if (Array.isArray(changedFiles)) {
for (const file of changedFiles) {
if (/\.(md|njk|html)$/i.test(file)) {
lastModCache.delete(file);
}
}
}
});
/**
@@ -102,6 +91,31 @@ export default async function (eleventyConfig) {
image: 'https://webawesome.com/assets/images/open-graph/default.png',
};
// Title composition/stripping config - single source of truth
const SITE_NAME = siteMetadata.name;
const SITE_TITLE_SEPARATORS = ['|'];
// Helper to escape user-provided strings for safe use inside RegExp sources
const escapeRegExp = string => (string + '').replace(/[.*+?^${}()|[\\]\\]/g, '\\$&');
// Precompute a reusable regex to strip a trailing site name suffix from titles, e.g. " | Web Awesome"
// Supports configured separators and flexible whitespace. This keeps search titles clean and improves Lunr scoring
const siteNameEscapedForRegex = escapeRegExp(SITE_NAME);
const separatorsEscaped = SITE_TITLE_SEPARATORS.map(s => escapeRegExp(s)).join('');
const siteTitleSuffixPattern = new RegExp(`\\s*[${separatorsEscaped}]\\s*${siteNameEscapedForRegex}$`);
// Helper to remove the site suffix from a page title. Keep this in sync with how page titles
// are composed (see eleventyComputed.pageTitle) so search indexing stays consistent
const stripSiteTitleSuffix = title => (title || '').replace(siteTitleSuffixPattern, '');
// Helper to compose a full page title with site suffix when appropriate
// Uses the same separator set as the stripping logic for consistency
const composePageTitle = baseTitle => {
const title = baseTitle || SITE_NAME;
const preferredSeparator = SITE_TITLE_SEPARATORS[0] || '|';
return title !== SITE_NAME ? `${title} ${preferredSeparator} ${SITE_NAME}` : title;
};
eleventyConfig.addGlobalData('siteMetadata', siteMetadata);
// Template filters - {{ content | filter }}
@@ -110,88 +124,12 @@ export default async function (eleventyConfig) {
eleventyConfig.addFilter('stripExtension', string => path.parse(string + '').name);
eleventyConfig.addFilter('stripPrefix', content => content.replace(/^wa-/, ''));
eleventyConfig.addFilter('uniqueId', (_value, length = 8) => nanoid(length));
// Returns last modified date as ISO 8601 (UTC, Z-suffixed)
// Fallback order: front matter override -> Git last commit date -> filesystem mtime -> now
// Caching: in-memory per inputPath during one build/dev session
// Override: pass a Date or string: {{ page.inputPath | gitLastModifiedISO(lastUpdated) }}
const lastModCache = new Map();
let repoRoot = null; // lazily resolved; null => not resolved, undefined => failed
function getLastModifiedISO(inputPath, overrideDate) {
if (overrideDate instanceof Date) {
return overrideDate.toISOString();
}
if (typeof overrideDate === 'string' && overrideDate) {
const parsed = new Date(overrideDate);
if (!isNaN(parsed.getTime())) return parsed.toISOString();
}
if (!inputPath) return new Date().toISOString();
if (lastModCache.has(inputPath)) return lastModCache.get(inputPath);
// Try Git (ISO via %cI). Use a repo-root-relative path for portability.
if (!ignoreGit) {
try {
if (repoRoot === null) {
try {
repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
stdio: ['ignore', 'pipe', 'ignore'],
cwd: __dirname,
})
.toString()
.trim();
} catch (_) {
repoRoot = undefined;
}
}
const gitPath = repoRoot ? path.relative(repoRoot, inputPath) : inputPath;
const args = ['log', '-1', '--format=%cI', '--follow', '--', gitPath];
const result = execFileSync('git', args, {
stdio: ['ignore', 'pipe', 'ignore'],
cwd: repoRoot || path.dirname(inputPath),
})
.toString()
.trim();
if (result) {
const iso = new Date(result).toISOString();
lastModCache.set(inputPath, iso);
return iso;
}
} catch (_) {
// continue to fs fallback
}
}
// Fallback to filesystem mtime
try {
const stats = fs.statSync(inputPath);
const iso = new Date(stats.mtime).toISOString();
lastModCache.set(inputPath, iso);
return iso;
} catch (_) {
const now = new Date().toISOString();
lastModCache.set(inputPath, now);
return now;
}
}
eleventyConfig.addFilter('gitLastModifiedISO', function (inputPath, overrideDate) {
return getLastModifiedISO(inputPath, overrideDate);
});
// Attach lastUpdatedISO to page data so templates can use {{ lastUpdatedISO }} directly
eleventyConfig.addGlobalData('eleventyComputed', {
lastUpdatedISO: data => getLastModifiedISO(data.page?.inputPath, data.lastUpdated),
// Page title with smart + default site name formatting
pageTitle: data => {
const title = data.title || siteMetadata.name;
return title !== siteMetadata.name ? `${title} | ${siteMetadata.name}` : title;
},
pageTitle: data => composePageTitle(data.title),
// Open Graph title with smart + default site name formatting
ogTitle: data => {
const ogTitle = data.ogTitle || data.title || siteMetadata.name;
return ogTitle !== siteMetadata.name ? `${ogTitle} | ${siteMetadata.name}` : ogTitle;
},
ogTitle: data => composePageTitle(data.ogTitle || data.title),
ogDescription: data => data.ogDescription || data.description,
ogImage: data => data.ogImage || siteMetadata.image,
ogUrl: data => {
@@ -201,6 +139,7 @@ export default async function (eleventyConfig) {
},
ogType: data => data.ogType || 'website',
});
// 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 be present when the line wraps.
eleventyConfig.addFilter('trimPipes', content => {
@@ -354,6 +293,11 @@ export default async function (eleventyConfig) {
searchPlugin({
filename: '',
selectorsToIgnore: ['code.example'],
// Use <title> but strip a trailing site name suffix for cleaner search results
getTitle: doc => {
const raw = doc.querySelector('title')?.textContent ?? '';
return stripSiteTitleSuffix(raw);
},
getContent: doc => doc.querySelector('#content')?.textContent ?? '',
}),
);
@@ -369,18 +313,19 @@ export default async function (eleventyConfig) {
// This needs to happen in "eleventy.after" otherwise incremental builds never update.
eleventyConfig.on('eleventy.after', function () {
let assetsDir = path.join(process.env.BASE_DIR || 'docs', 'assets');
const baseDir = process.env.BASE_DIR || 'docs';
let assetsDir = path.join(baseDir, 'assets');
const siteAssetsDir = path.join(eleventyConfig.directories.output, 'assets');
fs.cpSync(assetsDir, siteAssetsDir, { recursive: true });
// Passthrough copy for manifest.json (PWA manifest file)
fs.cpSync(path.join(baseDir, 'manifest.json'), path.join(eleventyConfig.directories.output, 'manifest.json'));
});
for (let glob of passThrough) {
eleventyConfig.addPassthroughCopy(glob);
}
// Passthrough copy for manifest.json (PWA manifest file)
eleventyConfig.addPassthroughCopy('manifest.json');
// // SSR plugin
// if (!isDev) {
// //

View File

@@ -1,5 +1,5 @@
{% raw %}
{%- if not currentUser.hasPro -%}
{%- if req.stripe.discount.active and not currentUser.hasPro -%}
<div slot="banner" class="banner-wa-launch wa-dark">
<div class="banner-content wa-split">
<div class="wa-cluster wa-gap-s">

View File

@@ -0,0 +1,77 @@
{% raw %}
{%- if req.stripe.discount.active and not currentUser.hasPro -%}
<wa-dialog id="dialog-site" light-dismiss without-header>
<div class="background-wa-pattern" style="justify-content: center; margin-inline: calc(var(--spacing) * -1); margin-block-start: calc(var(--spacing) * -1); margin-block-end: var(--spacing); background: linear-gradient(to bottom, var(--wa-color-brand), var(--wa-color-brand-50)); color: var(--wa-color-brand-on-loud); padding: var(--wa-space-3xl) var(--spacing); --background-pattern-opacity: 0.2; --background-pattern-image: url('/assets/images/bg-wa-pattern.svg');">
<div class="wa-stack wa-align-items-center" style="text-align: center;">
<wa-icon name="party-horn" family="duotone" variant="solid" style="font-size: var(--wa-font-size-3xl); --secondary-color: var(--wa-color-brand-40); --secondary-opacity: 1.0;"></wa-icon>
<h2 class="wa-heading-2xl brand-font">Get a lifetime discount on Web Awesome Pro!</h2>
</div>
</div>
<div class="wa-stack wa-gap-l">
<p>Celebrate our official launch with a 20% discount on a Web Awesome Pro plan&hellip;<span class="appearance-underlined variant-drawn" style="--underline-color: var(--wa-color-brand);">for life</span>! But hurry, this lifetime discount is only available for a limited time.</p>
<div class="wa-split">
<wa-button type="button" appearance="plain" data-dialog="close">Maybe Later</wa-button>
<wa-button variant="neutral" appearance="accent" href="/purchase" class="brand-font">
<wa-icon slot="start" variant="regular" name="rocket-launch"></wa-icon>
Get Pro + Save 20%
</wa-button>
</div>
</div>
</wa-dialog>
<script type="module">
(function() {
const SITE_DIALOG_DISMISSED_KEY = 'dialog-wa-launch-sale-dismissed';
// Early exit if user has dismissed the dialog
try {
if (localStorage.getItem(SITE_DIALOG_DISMISSED_KEY) === 'true') {
return;
}
} catch (e) {
// localStorage may be disabled or unavailable
return;
}
// Wait for DOM and dialog element
const dialog = document.getElementById('dialog-site');
if (!dialog) {
return;
}
// Initialize dialog functionality
let initCalled = false;
const initDialog = () => {
// Prevent double initialization
if (initCalled) {
return;
}
initCalled = true;
// Save dismissal state when dialog is hidden
dialog.addEventListener('wa-hide', (event) => {
// Save dismissal state to localStorage
try {
localStorage.setItem(SITE_DIALOG_DISMISSED_KEY, 'true');
} catch (e) {
// localStorage may be disabled or unavailable
}
}, { once: true });
// Show dialog after a short delay to ensure page is loaded
setTimeout(() => {
dialog.open = true;
}, 500);
};
customElements.whenDefined("wa-dialog").then(() => {
initDialog()
})
})();
</script>
{%- endif -%}
{% endraw %}

View File

@@ -1,5 +1,9 @@
<!DOCTYPE html>
<html lang="en" data-fa-kit-code="38c11e3f20" data-version="{{ package.version }}" class="wa-cloak">
{% if hasAnchors == undefined %}{% set hasAnchors = true %}{% endif %}
{% if hasBanner == undefined %}{% set hasBanner = true %}{% endif %}
{% if hasSiteDialog == undefined %}{% set hasSiteDialog = true %}{% endif %}
{% if hasGeneratedTitle == undefined %}{% set hasGeneratedTitle = true %}{% endif %}
<html lang="en" data-fa-kit-code="38c11e3f20" data-version="{{ package.version }}" class="wa-cloak"{% if hasAnchors == false %} data-no-anchor{% endif %}>
<head>
{% include 'head.njk' %}
<meta name="theme-color" content="#f36944">
@@ -13,7 +17,6 @@
<script type="module" src="/assets/scripts/color-scheme.js"></script>
<script type="module" src="/assets/scripts/theme.js"></script>
{% if hasSidebar %}<script type="module" src="/assets/scripts/sidebar.js"></script>{% endif %}
<script defer data-domain="webawesome.com" src="https://plausible.io/js/script.js"></script>
{% block head %}
<link rel="stylesheet" href="/assets/styles/docs.css" />
@@ -27,9 +30,6 @@
</script>
</head>
<body class="layout-{{ layout | stripExtension }} page-{{ pageClass or page.fileSlug or 'home' }}{{ ' page-wide' if wide }}">
{% if hasBanner == undefined %}
{% set hasBanner = true %}
{% endif %}
{% set defaultWaPageAttributes = defaultWaPageAttributes or { view: 'desktop', 'disable-navigation-toggle': true, 'mobile-breakpoint': 1180, 'disable-sticky': 'banner' } %}
{% set waPageAttributes = waPageAttributes or {} %}
@@ -41,72 +41,82 @@
{% endif %}
{% endfor %}
>
{% block pageBanner %}
{% if hasBanner %}
{#- WA Launch Banner -#}
{% include "_banner-wa-launch.njk" ignore missing %}
{% endif %}
{% endblock %}
{# wa-page-based Skip to Content #}
{% block pageSkipToContent %}{% endblock %}
{% block pageHeader %}
<header slot="header" class="wa-split">
{# Nav toggle #}
<wa-button appearance="plain" size="small" data-toggle-nav>
<wa-icon name="bars" label="Toggle navigation" class="icon-default icon-embiggen"></wa-icon>
<wa-icon name="burger" aria-hidden="true" class="icon-hover icon-embiggen"></wa-icon>
</wa-button>
{# wa-page-based Banner #}
{% block pageBanner %}
{% if hasBanner %}
{#- WA Launch Banner -#}
{% include "_banner-wa-launch.njk" ignore missing %}
{% endif %}
{% endblock %}
{# Logo - Desktop #}
<a class="brand-logo wa-desktop-only" href="/" aria-label="Web Awesome">
{% include "logo.njk" %}
</a>
{# wa-page-based Subheader #}
{% block pageSubheader %}{% endblock %}
{#- Logo - mobile branding -#}
<a href="/" class="brand-logo wa-mobile-only" aria-label="Web Awesome">
{# Logo - Mobile #}
{% include "logo-simple.njk" %}
</a>
{# wa-page-based Header #}
{% block pageHeader %}
<header slot="header" class="wa-split">
{# Nav toggle #}
<wa-button appearance="plain" size="small" data-toggle-nav>
<wa-icon name="bars" label="Toggle navigation" class="icon-default icon-embiggen"></wa-icon>
<wa-icon name="burger" aria-hidden="true" class="icon-hover icon-embiggen"></wa-icon>
</wa-button>
<div id="docs-toolbar" class="wa-cluster gap-s">
<div class="wa-desktop-only wa-cluster wa-gap-2xs">
{% include "theme-selector.njk" %}
{% include "color-scheme-selector.njk" %}
{% include "github-icon-buttons.njk" %}
</div>
{#- Login -#}
{% include "login-or-avatar.njk" ignore missing %}
</div>
</header>
{% endblock %}
{# Logo - Desktop #}
<a href="/" class="brand-logo wa-desktop-only" aria-label="Web Awesome">{% include "logo.njk" %}</a>
{# Sidebar #}
{% if hasSidebar %}
{# Mobile selectors #}
<div class="wa-mobile-only" slot="navigation-header">
<div class="wa-cluster wa-gap-s">
<a class="brand-logo" href="/" aria-label="Web Awesome">{% include "logo-simple.njk" %}</a>
<div class="wa-cluster wa-gap-2xs" style="flex-wrap: nowrap;">
{# Logo - Mobile #}
<a href="/" class="brand-logo wa-mobile-only" aria-label="Web Awesome">{% include "logo-simple.njk" %}</a>
<div id="docs-toolbar" class="wa-cluster gap-s">
<div class="wa-desktop-only wa-cluster wa-gap-2xs">
{% include "theme-selector.njk" %}
{% include "color-scheme-selector.njk" %}
{% include "github-icon-buttons.njk" %}
</div>
{#- Login -#}
{% include "login-or-avatar.njk" ignore missing %}
</div>
</div>
<div slot="navigation" id="sidebar" class="docs-aside" data-remember-scroll>
{% include "sidebar.njk" %}
</div>
{% endif %}
</header>
{% endblock %}
{# Outline #}
{% if hasOutline %}
<aside slot="aside" id="outline" class="docs-aside">
<nav id="outline-standard" class="outline-links">
<h2><a href="#content">{{ title }}</a></h2>
</nav>
</aside>
{% endif %}
{# wa-page-based Navigation Header #}
{% block pageNavigationHeader %}
{# Sidebar - Mobile Selectors #}
{% if hasSidebar %}
<div class="wa-mobile-only" slot="navigation-header">
<div class="wa-cluster wa-gap-s">
<a class="brand-logo" href="/" aria-label="Web Awesome">{% include "logo-simple.njk" %}</a>
<div class="wa-cluster wa-gap-2xs" style="flex-wrap: nowrap;">
{% include "theme-selector.njk" %}
{% include "color-scheme-selector.njk" %}
{% include "github-icon-buttons.njk" %}
</div>
</div>
</div>
{% endif %}
{% endblock %}
{# Main #}
{# wa-page-based Navigation #}
{% block pageNavigation %}
{# Sidebar - Navigation #}
{% if hasSidebar %}
<div slot="navigation" id="sidebar" class="docs-aside" data-remember-scroll>
{% include "sidebar.njk" %}
</div>
{% endif %}
{% endblock %}
{# wa-page-based Navigation Footer #}
{% block pageNavigationFooter %}{% endblock %}
{# wa-page-based Main Header #}
{% block pageMainHeader %}{% endblock %}
{# wa-page-based Main Content (default) #}
<main id="content">
{# Expandable outline #}
{% if hasOutline %}
@@ -117,10 +127,15 @@
</nav>
{% endif %}
<div id="flashes">{% server "flashes" %}</div>
{# Flashes #}
{% block flashes %}
<div id="flashes">{% server "flashes" %}</div>
{% endblock %}
{% block header %}
<h1 class="title">{{ title }}</h1>
{% if hasGeneratedTitle %}
<h1 class="title">{{ title }}</h1>
{% endif %}
{% endblock %}
{% block beforeContent %}{% endblock %}
@@ -132,10 +147,33 @@
{% block afterContent %}{% endblock %}
</main>
{# wa-page-based Main Footer #}
{% block pageMainFooter %}{% endblock %}
{# wa-page-based Aside #}
{% block pageAside %}
{# Outline #}
{% if hasOutline %}
<aside slot="aside" id="outline" class="docs-aside">
<nav id="outline-standard" class="outline-links">
<h2><a href="#content">{{ title }}</a></h2>
</nav>
</aside>
{% endif %}
{% endblock %}
{# wa-page-based Footer #}
{% block pageFooter %}{% endblock %}
{% include 'search.njk' %}
{# Footer #}
{% block pageFooter %}{% endblock %}
{#- Site-Wide Dialog -#}
{% if hasSiteDialog %}
{% include "_dialog-wa-launch.njk" ignore missing %}
{% endif %}
{#- Cookie Consent Dialog -#}
{% include "cookie-consent.njk" ignore missing %}
</wa-page>
</body>

View File

@@ -0,0 +1,7 @@
{% macro freeBadge(params) %}
{% set description = params.description or "This feature is available in the free version of Web Awesome" %}
{% set badgeId = params.id or ("free-badge-" + ("" | uniqueId(8))) %}
<wa-badge appearance="filled" variant="neutral" pill class="free" id="{{ badgeId }}" data-free-badge>Free</wa-badge>
<wa-tooltip for="{{ badgeId }}">{{ description }}</wa-tooltip>
{% endmacro %}

View File

@@ -1,6 +1,6 @@
{% macro proBadge(params) %}
{% set description = params.description or "This requires access to Web Awesome Pro" %}
{% set badgeId = params.id or ("pro-badge-" + ("" | uniqueId(8))) %}
<wa-badge appearance="accent" pill class="pro" id="{{ badgeId }}">Pro</wa-badge>
<wa-badge appearance="accent" pill class="pro" id="{{ badgeId }}" data-pro-badge>Pro</wa-badge>
<wa-tooltip for="{{ badgeId }}">{{ description }}</wa-tooltip>
{% endmacro %}

View File

@@ -29,8 +29,12 @@
<li><a href="/docs/resources/contributing">Contributing</a></li>
<li><a href="/docs/resources/changelog">Changelog</a></li>
<li><a href="/docs/resources/visual-tests">Visual Tests</a></li>
<li>
<a class="wa-cluster wa-gap-xs" href="/docs/resources/llms">
LLMs
<wa-icon name="flask" aria-hidden="true" class="icon-shrink"></wa-icon>
</a>
</li>
</ul>
<!-- Components -->
@@ -78,10 +82,18 @@
</li>
</ul>
</li>
<li><span class="is-planned wa-split">Charts <a href="https://github.com/shoelace-style/webawesome/issues/1073" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a></span></li>
<li><span class="is-planned wa-split">Charts <span><a href="https://github.com/shoelace-style/webawesome/issues/1073" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a>{{ proBadge({ description: "This will require access to Web Awesome Pro" }) }}</span></span></li>
<li><a href="/docs/components/checkbox/">Checkbox</a></li>
<li><a href="/docs/components/color-picker/">Color Picker</a></li>
<li><span class="is-planned wa-split">Combobox <a href="https://github.com/shoelace-style/webawesome/issues/1074" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a></span></li>
<li>
<span class="wa-split">
<span>
<a href="/docs/components/combobox">Combobox</a>
<wa-icon name="flask" aria-hidden="true" class="icon-shrink"></wa-icon>
</span>
{{ proBadge() }}
</span>
</li>
<li><a href="/docs/components/comparison/">Comparison</a></li>
<li>
<a class="wa-cluster wa-gap-xs" href="/docs/components/copy-button/">
@@ -89,8 +101,8 @@
<wa-icon name="flask" aria-hidden="true" class="icon-shrink"></wa-icon>
</a>
</li>
<li><span class="is-planned wa-split">Data Grid <a href="https://github.com/shoelace-style/webawesome/issues/1072" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a></span></li>
<li><span class="is-planned wa-split">Datepicker <a href="https://github.com/shoelace-style/webawesome/issues/1075" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a></span></li>
<li><span class="is-planned wa-split">Data Grid <span><a href="https://github.com/shoelace-style/webawesome/issues/1072" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a>{{ proBadge({ description: "This will require access to Web Awesome Pro" }) }}</span></span></li>
<li><span class="is-planned wa-split">Date Picker <span><a href="https://github.com/shoelace-style/webawesome/issues/1075" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a>{{ proBadge({ description: "This will require access to Web Awesome Pro" }) }}</span></span></li>
<li><a href="/docs/components/details/">Details</a></li>
<li><a href="/docs/components/dialog/">Dialog</a></li>
<li><a href="/docs/components/divider/">Divider</a></li>
@@ -101,6 +113,7 @@
<li><a href="/docs/components/dropdown-item">Dropdown Item</a></li>
</ul>
</li>
<li><span class="is-planned wa-split">File Input <span><a href="https://github.com/shoelace-style/webawesome/issues/1240" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a>{{ proBadge({ description: "This will require access to Web Awesome Pro" }) }}</span></span></li>
<li><a href="/docs/components/format-bytes/">Format Bytes</a></li>
<li><a href="/docs/components/format-date/">Format Date</a></li>
<li><a href="/docs/components/format-number/">Format Number</a></li>
@@ -109,6 +122,7 @@
<li><a href="/docs/components/input/">Input</a></li>
<li><a href="/docs/components/intersection-observer">Intersection Observer</a></li>
<li><a href="/docs/components/mutation-observer/">Mutation Observer</a></li>
<li><span class="is-planned wa-split">Number Input <a href="https://github.com/shoelace-style/webawesome/issues/1688" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a></span></li>
<li><a href="/docs/components/popover/">Popover</a></li>
<li><a href="/docs/components/popup/">Popup</a></li>
<li><a href="/docs/components/progress-bar/">Progress Bar</a></li>
@@ -144,9 +158,15 @@
</li>
<li><a href="/docs/components/tag/">Tag</a></li>
<li><a href="/docs/components/textarea/">Textarea</a></li>
<li><span class="is-planned wa-split">Toast <span><a href="https://github.com/shoelace-style/webawesome/issues/105" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a>{{ proBadge( { description: "This will require access to Web Awesome Pro" }) }}</span></span></li>
<li><a href="/docs/components/tooltip/">Tooltip</a></li>
<li><a href="/docs/components/tree/">Tree</a></li>
<li><a href="/docs/components/tree-item/">Tree Item</a></li>
<li>
<a href="/docs/components/tree/">Tree</a>
<ul>
<li><a href="/docs/components/tree-item/">Tree Item</a></li>
</ul>
</li>
<li><span class="is-planned wa-split">Video <span><a href="https://github.com/shoelace-style/webawesome/issues/985" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a>{{ proBadge( { description: "This will require access to Web Awesome Pro" }) }}</span></span></li>
<li><a href="/docs/components/zoomable-frame">Zoomable Frame</a></li>
{# PLOP_NEW_COMPONENT_PLACEHOLDER #}
</ul>
@@ -180,6 +200,8 @@
</h2>
<ul>
<li><a href="/docs/utilities/align-items/">Align Items</a></li>
<!-- Pending 3.2.0 release -->
<!-- <li><a href="/docs/utilities/justify-content/">Justify Content</a></li> -->
<li><a href="/docs/utilities/gap/">Gap</a></li>
<li><a href="/docs/utilities/cluster/">Cluster</a></li>
<li><a href="/docs/utilities/flank/">Flank</a></li>
@@ -365,9 +387,6 @@
<li>
<a href="/docs/patterns/layouts/ecommerce/">Ecommerce</a>
</li>
<li>
<a href="/docs/patterns/layouts/app/">App</a>
</li>
<li>
<a href="/docs/patterns/layouts/blog/">Blog</a>
</li>

View File

@@ -8,10 +8,13 @@
<wa-badge variant="neutral">Since {{ component.since }}</wa-badge>
<wa-badge
{% if component.status == 'stable' %}variant="brand"{% endif %}
{% if component.status == 'experimental' %}variant="warning"{% endif %}
{% if component.status == 'experimental' %}variant="warning" appearance="filled"{% endif %}
>
{{ component.status }}
</wa-badge>
{% if isProComponent %}
<wa-badge class="pro">Pro</wa-badge>
{% endif %}
</div>
<p class="component-summary">
{{ component.summary | inlineMarkdown | safe }}
@@ -20,6 +23,37 @@
{# Component API #}
{% block afterContent %}
{# Importing #}
<h2>Importing</h2>
<p>
Autoloading components via <a href="/docs/#using-a-project">projects</a> is the recommended way to import components. If you prefer to do it manually, use one of the following code snippets.
</p>
{% set componentName = component.tagName | stripPrefix %}
{% set componentPath = ["components/", componentName, "/", componentName, ".js"] | join("") %}
<wa-tab-group label="How would you like to import this component?">
<wa-tab panel="cdn">CDN</wa-tab>
<wa-tab panel="npm">npm</wa-tab>
<wa-tab panel="react">React</wa-tab>
<wa-tab-panel name="cdn">
<p>
Let your project code do the work! <a href="/signup">Sign up for free</a> to use a project with your very own CDN &mdash; it's the fastest and easiest way to use Web Awesome.
</p>
</wa-tab-panel>
<wa-tab-panel name="npm">
<p>
To manually import this component from NPM, use the following code.
</p>
<pre><code class="language-js">import '@awesome.me/webawesome/dist/{{ componentPath }}';</code></pre>
</wa-tab-panel>
<wa-tab-panel name="react">
<p>
To manually import this component from React, use the following code.
</p>
<pre><code class="language-js">import {{ component.name }} from '@awesome.me/webawesome/dist/react/{{ componentName }}';</code></pre>
</wa-tab-panel>
</wa-tab-group>
{# Slots #}
{% if component.slots.length %}
<h2>Slots</h2>
@@ -211,7 +245,12 @@
<tr>
<td class="table-name"><code>{{ state.name }}</code></td>
<td class="table-description">{{ state.description | inlineMarkdown | safe }}</td>
<td class="table-selector"><code>:state({{ state.name }})</code></td>
<td class="table-selector">
<span class="wa-cluster wa-gap-3xs">
<code>:state({{ state.name }})</code>
<wa-copy-button value=":state({{ state.name }})"></wa-copy-button>
</span>
</td>
</tr>
{% endfor %}
</tbody>
@@ -230,6 +269,7 @@
<tr>
<th class="table-name">Name</th>
<th class="table-description">Description</th>
<th class="table-selector">CSS selector</th>
</tr>
</thead>
<tbody>
@@ -237,6 +277,12 @@
<tr>
<td class="table-name"><code>{{ cssPart.name }}</code></td>
<td class="table-description">{{ cssPart.description | inlineMarkdown | safe }}</td>
<td class="table-selector">
<span class="wa-cluster wa-gap-3xs">
<code>::part({{ cssPart.name }})</code>
<wa-copy-button value="::part({{ cssPart.name }})"></wa-copy-button>
</span>
</td>
</tr>
{% endfor %}
</tbody>
@@ -258,38 +304,6 @@
</ul>
{% endif %}
{# Importing #}
<h2>Importing</h2>
<p>
Autoloading components via <a href="/docs/#using-a-project">projects</a> is the recommended way to import components. If you prefer to do it manually, use one of the following code snippets.
</p>
{% set componentName = component.tagName | stripPrefix %}
{% set componentPath = ["components/", componentName, "/", componentName, ".js"] | join("") %}
<wa-tab-group label="How would you like to import this component?">
<wa-tab panel="cdn">CDN</wa-tab>
<wa-tab panel="npm">npm</wa-tab>
<wa-tab panel="react">React</wa-tab>
<wa-tab-panel name="cdn">
<p>
Let your project code do the work! <a href="/signup">Sign up for free</a> to use a project with your very own CDN &mdash; it's the fastest and easiest way to use Web Awesome.
</p>
</wa-tab-panel>
<wa-tab-panel name="npm">
<p>
To manually import this component from NPM, use the following code.
</p>
<pre><code class="language-js">import '@awesome.me/webawesome/dist/{{ componentPath }}';</code></pre>
</wa-tab-panel>
<wa-tab-panel name="react">
<p>
To manually import this component from React, use the following code.
</p>
<pre><code class="language-js">import {{ component.name }} from '@awesome.me/webawesome/dist/react/{{ componentName }}';</code></pre>
</wa-tab-panel>
</wa-tab-group>
<wa-divider></wa-divider>
<div class="component-help">

View File

@@ -35,9 +35,23 @@ export function anchorHeadingsTransformer(options = {}) {
return doc;
}
// Look for headings
let selector = `:is(${options.headingSelector}):not([data-no-anchor], [data-no-anchor] *)`;
// Check if the document or container has data-no-anchor (view-level)
const hasNoAnchorOnDocument = doc.querySelector('html')?.hasAttribute('data-no-anchor') || false;
const hasNoAnchorOnContainer = container.closest('[data-no-anchor]') !== null;
// If view-level data-no-anchor is set, skip processing all headings
if (hasNoAnchorOnDocument || hasNoAnchorOnContainer) {
return doc;
}
// Look for headings (selector excludes headings with data-no-anchor attribute)
let selector = `:is(${options.headingSelector}):not([data-no-anchor])`;
container.querySelectorAll(selector).forEach(heading => {
// Skip if heading is a descendant of an element with data-no-anchor
// (selector already excludes headings with the attribute directly)
if (heading.closest('[data-no-anchor]') !== null) {
return;
}
const hasAnchor = heading.querySelector('a');
const existingId = heading.getAttribute('id');
const clone = parse(heading.outerHTML);
@@ -65,7 +79,7 @@ export function anchorHeadingsTransformer(options = {}) {
const anchor = parse(`
<a href="#${encodeURIComponent(id)}">
<span class="wa-visually-hidden"></span>
<span aria-hidden="true">#</span>
<wa-icon variant="regular" name="hashtag" class="icon-shrink"></wa-icon>
</a>
`);
anchor.querySelector('.wa-visually-hidden').textContent = options.anchorLabel;

View File

@@ -11,6 +11,7 @@ export function getComponents() {
const manifest = JSON.parse(readFileSync(join(distDir, 'custom-elements.json'), 'utf-8'));
const components = [];
const sortByName = (a, b) => (a.name || '').localeCompare(b.name || '');
manifest.modules?.forEach(module => {
module.declarations?.forEach(declaration => {
if (declaration.customElement) {
@@ -18,7 +19,16 @@ export function getComponents() {
declaration.path = module.path.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js');
// Remove private members and those that lack a description
const members = declaration.members?.filter(member => member.description && member.privacy !== 'private');
const slots = declaration.slots?.sort(sortByName);
const events = declaration.events?.sort(sortByName);
const cssProperties = declaration.cssProperties?.sort(sortByName);
const cssParts = declaration.cssParts?.sort(sortByName);
const cssStates = declaration.cssStates?.sort(sortByName);
const dependencies = declaration.dependencies?.sort((a, b) => a.localeCompare(b));
const members = declaration.members
?.filter(member => member.description && member.privacy !== 'private')
?.sort(sortByName);
const methods = members?.filter(prop => prop.kind === 'method' && prop.privacy !== 'private');
const properties = members?.filter(prop => {
// Look for a corresponding attribute
@@ -31,6 +41,12 @@ export function getComponents() {
});
components.push({
...declaration,
slots,
events,
cssProperties,
cssStates,
cssParts,
dependencies,
methods,
properties,
});

View File

@@ -1,10 +1,20 @@
import * as path from 'node:path';
import nunjucks from 'nunjucks';
const baseDir = process.env.BASE_DIR || 'docs';
const views = [path.join(baseDir), path.join(baseDir, '_layouts'), path.join(baseDir, '_includes')];
const nunjucksEnv = new nunjucks.Environment(new nunjucks.FileSystemLoader(views), {
autoescape: true,
noCache: process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test',
});
/**
* This function simulates what a server would do running "on top" of eleventy.
*/
export function SimulateWebAwesomeApp(str) {
return nunjucks.renderString(str, {
return nunjucksEnv.renderString(str, {
// Stub the server EJS shortcodes.
currentUser: {
hasPro: false,

View File

@@ -1,12 +1,24 @@
// Search data
const version = document.documentElement.getAttribute('data-version') || '';
const res = await Promise.all([import('https://cdn.jsdelivr.net/npm/lunr/+esm'), fetch(`/search.json?v=${version}`)]);
const res = await Promise.all([
import('https://cdn.jsdelivr.net/npm/lunr/+esm'),
fetch(`/search.json?v=${version}`),
import('/assets/scripts/track.js').catch(() => null),
]);
const lunr = res[0].default;
const searchData = await res[1].json();
const searchIndex = lunr.Index.load(searchData.searchIndex);
const map = searchData.map;
const searchDebounce = 200;
const queryTrackDelay = 1000;
let searchTimeout;
let queryTrackTimeout;
let lastTrackedQuery = '';
let resultSelected = false;
// Optional event tracking - works standalone if track.js isn't available
const trackModule = res[2];
const trackEvent = trackModule?.trackEvent || window.trackEvent || (() => {});
// We're using Turbo, so references to these elements aren't guaranteed to remain intact
function getElements() {
@@ -17,6 +29,24 @@ function getElements() {
};
}
function trackQuerySubmit(query, resultSelectedValue) {
if (!query || query.length === 0) return;
const { results } = getElements();
if (!results) return;
const matches = results.querySelectorAll('li').length;
const truncatedQuery = query.length > 500 ? query.substring(0, 500) : query;
trackEvent('navigation:search_query_submit', {
query: truncatedQuery,
query_length: query.length,
result_count: matches,
has_results: matches > 0,
result_selected: resultSelectedValue,
});
}
// Show the search dialog when slash (or CMD+K) is pressed and focus is not inside a form element
document.addEventListener('keydown', event => {
if (
@@ -42,40 +72,98 @@ document.addEventListener('click', event => {
function show() {
const { dialog, input, results } = getElements();
if (!dialog || !input || !results) return;
const wasAlreadyOpen = dialog.open;
// Remove existing listeners before adding to prevent duplicates
input.removeEventListener('input', handleInput);
results.removeEventListener('click', handleSelection);
dialog.removeEventListener('keydown', handleKeyDown);
dialog.removeEventListener('wa-hide', handleClose);
resultSelected = false;
lastTrackedQuery = '';
input.addEventListener('input', handleInput);
results.addEventListener('click', handleSelection);
dialog.addEventListener('keydown', handleKeyDown);
dialog.addEventListener('wa-hide', handleClose);
dialog.open = true;
if (!wasAlreadyOpen) {
trackEvent('navigation:search_dialog_open');
}
}
function hide() {
function cleanup() {
const { dialog, input, results } = getElements();
if (!dialog || !input || !results) return;
clearTimeout(searchTimeout);
clearTimeout(queryTrackTimeout);
input.removeEventListener('input', handleInput);
results.removeEventListener('click', handleSelection);
dialog.removeEventListener('keydown', handleKeyDown);
dialog.removeEventListener('wa-hide', handleClose);
dialog.open = false;
// Reset state to prevent leakage between dialog sessions
resultSelected = false;
lastTrackedQuery = '';
}
function handleClose() {
const { input } = getElements();
async function handleClose() {
const { dialog, input } = getElements();
if (!dialog || !input) return;
clearTimeout(queryTrackTimeout);
queryTrackTimeout = null;
dialog.removeEventListener('wa-hide', handleClose);
if (!resultSelected) {
const query = input.value.trim();
if (query.length > 0 && query !== lastTrackedQuery) {
trackQuerySubmit(query, false);
lastTrackedQuery = query;
}
}
input.value = '';
updateResults();
try {
await updateResults();
} catch (error) {
// Silently handle errors - UI cleanup should continue
}
cleanup();
trackEvent('navigation:search_dialog_close');
}
function handleInput() {
const { input } = getElements();
if (!input) return;
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => updateResults(input.value), searchDebounce);
clearTimeout(queryTrackTimeout);
const query = input.value.trim();
if (query.length === 0) {
lastTrackedQuery = '';
}
searchTimeout = setTimeout(async () => {
await updateResults(query);
if (query.length > 0 && query !== lastTrackedQuery) {
queryTrackTimeout = setTimeout(() => {
const { input: currentInput, results } = getElements();
if (!currentInput || resultSelected) return;
const currentQuery = currentInput.value.trim();
if (currentQuery === query && currentQuery !== lastTrackedQuery) {
trackQuerySubmit(currentQuery, false);
lastTrackedQuery = currentQuery;
}
}, queryTrackDelay);
}
}, searchDebounce);
}
function handleKeyDown(event) {
const { input, results } = getElements();
if (!input || !results) return;
// Handle keyboard selections
if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) {
@@ -104,7 +192,12 @@ function handleKeyDown(event) {
nextEl = items[items.length - 1];
break;
case 'Enter':
currentEl?.querySelector('a')?.click();
if (currentEl) {
const link = currentEl.querySelector('a');
if (link) {
selectResult(link, 'keyboard_enter');
}
}
break;
}
@@ -121,27 +214,62 @@ function handleKeyDown(event) {
}
}
function selectResult(link, selectionMethod) {
const { input, results } = getElements();
if (!input || !link) return;
// Clear pending query tracking timeout to prevent duplicate events
clearTimeout(queryTrackTimeout);
queryTrackTimeout = null;
resultSelected = true; // Set immediately so timeout callback (if executing) sees it
const query = input.value.trim();
if (!link.dataset.searchResultIndex) return;
const resultIndex = parseInt(link.dataset.searchResultIndex, 10);
if (isNaN(resultIndex) || resultIndex < 1) return;
const resultUrl = link.dataset.searchResultUrl || link.getAttribute('href');
if (!resultUrl) return;
lastTrackedQuery = query;
trackQuerySubmit(query, true);
trackEvent('navigation:search_result_click', {
query,
result_index: resultIndex,
result_url: resultUrl,
selection_method: selectionMethod,
});
const { dialog } = getElements();
if (dialog) {
dialog.removeEventListener('wa-hide', handleClose);
cleanup();
trackEvent('navigation:search_dialog_close');
dialog.open = false;
}
if (window.Turbo) {
Turbo.visit(resultUrl);
} else {
location.href = resultUrl;
}
}
function handleSelection(event) {
const link = event.target.closest('a');
if (link) {
event.preventDefault();
hide();
if (window.Turbo) {
Turbo.visit(link.href);
} else {
location.href = link.href;
}
selectResult(link, 'mouse_click');
}
}
// Queries the search index and updates the results
async function updateResults(query = '') {
const { dialog, input, results } = getElements();
if (!dialog || !input || !results) return;
try {
const hasQuery = query.length > 0;
const trimmedQuery = query.trim();
const hasQuery = trimmedQuery.length > 0;
let matches = [];
if (hasQuery) {
@@ -149,13 +277,13 @@ async function updateResults(query = '') {
const seenRefs = new Set();
// Start with a standard search to get the best "exact match" result
searchIndex.search(`${query}`).forEach(match => {
searchIndex.search(`${trimmedQuery}`).forEach(match => {
matches.push(match);
seenRefs.add(match.ref);
});
// Add wildcard matches if not already included
searchIndex.search(`${query}*`).forEach(match => {
searchIndex.search(`${trimmedQuery}*`).forEach(match => {
if (!seenRefs.has(match.ref)) {
matches.push(match);
seenRefs.add(match.ref);
@@ -163,11 +291,10 @@ async function updateResults(query = '') {
});
// Add fuzzy search matches last
const fuzzyTokens = query
const fuzzyTokens = trimmedQuery
.split(' ')
.map(term => `${term}~1`)
.join(' ');
searchIndex.search(fuzzyTokens).forEach(match => {
if (!seenRefs.has(match.ref)) {
matches.push(match);
@@ -180,12 +307,12 @@ async function updateResults(query = '') {
dialog.classList.toggle('has-results', hasQuery && hasResults);
dialog.classList.toggle('no-results', hasQuery && !hasResults);
input.setAttribute('aria-activedescendant', '');
results.innerHTML = '';
matches.forEach((match, index) => {
const page = map[match.ref];
if (!page || !page.url) return;
const li = document.createElement('li');
const a = document.createElement('a');
const displayTitle = page.title ?? '';
@@ -197,12 +324,10 @@ async function updateResults(query = '') {
li.setAttribute('role', 'option');
li.setAttribute('id', `search-result-item-${match.ref}`);
li.setAttribute('data-selected', index === 0 ? 'true' : 'false');
if (page.url === '/') icon = 'home';
if (page.url.startsWith('/docs/utilities/native')) icon = 'code';
if (page.url.startsWith('/docs/components')) icon = 'puzzle-piece';
if (page.url.startsWith('/docs/theme') || page.url.startsWith('/docs/restyle')) icon = 'palette';
a.href = page.url;
a.innerHTML = `
<div class="site-search-result-icon" aria-hidden="true">
@@ -218,6 +343,9 @@ async function updateResults(query = '') {
a.querySelector('.site-search-result-description').textContent = displayDescription;
a.querySelector('.site-search-result-url').textContent = displayUrl;
// Use 1-based indexing for analytics
a.dataset.searchResultIndex = (index + 1).toString();
a.dataset.searchResultUrl = page.url;
li.appendChild(a);
results.appendChild(li);
});

View File

@@ -10,6 +10,13 @@
:root {
--wa-brand-orange: #f36944;
--wa-brand-grey: #30323b;
/* layout-based example style aspects */
--layout-example-border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
--layout-example-border-radius: var(--wa-border-radius-l);
--layout-example-padding: var(--wa-space-s);
--layout-example-element-background: var(--wa-color-indigo-60);
--layout-example-element-border-radius: var(--wa-border-radius-m);
}
.wa-dark .only-light,
@@ -38,7 +45,7 @@ wa-page > [slot='banner'] {
}
&.banner-wa-launch {
/* custom brand colors carrried over from theme-site for the banner */
/* custom brand colors carried over from theme-site for the banner */
--wa-color-brand-95: #fef0ec;
--wa-color-brand-90: #fce0d8;
--wa-color-brand-80: #f8bcac;
@@ -64,6 +71,24 @@ wa-page > [slot='banner'] {
}
}
/* Site-Wide Dialog */
#dialog-site {
/* custom brand colors carrried over from theme-site for the banner */
--wa-color-brand-95: #fef0ec;
--wa-color-brand-90: #fce0d8;
--wa-color-brand-80: #f8bcac;
--wa-color-brand-70: #fa9378;
--wa-color-brand-60: #f46a45;
--wa-color-brand-50: #cb4b27;
--wa-color-brand-40: #9d371a;
--wa-color-brand-30: #7c2a13;
--wa-color-brand-20: #5d1d0b;
--wa-color-brand-10: #3b0f05;
--wa-color-brand-05: #270802;
--wa-color-brand: var(--wa-color-brand-60);
--wa-color-brand-on: var(--wa-color-brand-10);
}
/* Header */
wa-page::part(header) {
background-color: var(--wa-color-surface-default);
@@ -351,26 +376,6 @@ h1.title {
}
}
/* Anchor headings */
.anchor-heading a {
visibility: hidden;
text-decoration: none;
}
@media (hover: hover) {
.anchor-heading:hover a {
visibility: visible;
padding: 0 0.125em;
}
}
@media print {
/* Show URLs for printed links */
a:not(.anchor-heading)[href]::after {
content: ' (' attr(href) ')';
}
}
/* Callouts */
.callout {
display: flex;
@@ -665,6 +670,10 @@ wa-scroller:has(.component-table) {
min-width: var(--line-length-xs);
}
.table-selector .wa-cluster {
flex-wrap: nowrap;
}
.table-reflect {
text-align: center;
}

View File

@@ -5,7 +5,6 @@
border-radius: var(--wa-border-radius-l);
padding: 0;
margin: 0 auto;
overflow: hidden;
&::part(dialog) {
margin-block-start: 10vh;

View File

@@ -70,9 +70,8 @@
/* #region shared UI */
/* pro badge */
wa-badge.pro {
color: white;
color: var(--wa-color-brand-on-loud);
background-color: var(--wa-brand-orange);
border-color: var(--wa-brand-orange);
+ wa-tooltip {
font-size: var(--wa-font-size-xs);
@@ -80,6 +79,14 @@
}
}
/* free badge */
wa-badge.free {
+ wa-tooltip {
font-size: var(--wa-font-size-xs);
--max-width: unset;
}
}
/* planned badge */
wa-badge.planned {
background-color: var(--wa-color-neutral-fill-quiet);
@@ -123,6 +130,35 @@
}
}
/* dialogs */
wa-dialog:has([slot='footer']) [slot='footer'] {
border-block-start: var(--wa-border-width-s) solid var(--wa-color-surface-border);
flex-grow: 1; /* make footer contents span entire width of dialog */
padding-block-start: var(--wa-space-l);
}
/* anchor headings */
.anchor-heading a {
opacity: 0;
visibility: hidden;
text-decoration: none;
transition: opacity var(--wa-transition-normal) var(--wa-transition-easing);
}
@media (hover: hover) {
.anchor-heading:hover a {
opacity: 1;
visibility: visible;
padding: var(--wa-space-3xs);
}
}
@media print {
/* show URLs for printed links */
a:not(.anchor-heading)[href]::after {
content: ' (' attr(href) ')';
}
}
/* #endregion */
/* #region funsies + cosmetics */
@@ -221,7 +257,6 @@
z-index: 0;
}
}
/* #endregion */
/* buttons with icon toggle on hover */
wa-button .icon-hover {
@@ -233,6 +268,13 @@
wa-button:hover .icon-hover {
display: inline-flex;
}
/* buttons that are "shushed" (visually muted) by default, but have their full presentation otherwise */
wa-button.shush {
&:not(:hover):not(active)::part(base) {
color: var(--wa-color-text-quiet);
}
}
/* #endregion */
/* #region resets */

View File

@@ -40,60 +40,6 @@ Set the `orientation` attribute to `vertical` to make a vertical button group.
</wa-button-group>
```
### Theme Buttons
Theme buttons are supported through the button group's `variant` attribute.
```html {.example}
<wa-button-group label="Alignment" variant="brand">
<wa-button>Left</wa-button>
<wa-button>Center</wa-button>
<wa-button>Right</wa-button>
</wa-button-group>
<br /><br />
<wa-button-group label="Alignment" variant="success">
<wa-button>Left</wa-button>
<wa-button>Center</wa-button>
<wa-button>Right</wa-button>
</wa-button-group>
<br /><br />
<wa-button-group label="Alignment">
<wa-button>Left</wa-button>
<wa-button>Center</wa-button>
<wa-button>Right</wa-button>
</wa-button-group>
<br /><br />
<wa-button-group label="Alignment" variant="warning">
<wa-button>Left</wa-button>
<wa-button>Center</wa-button>
<wa-button>Right</wa-button>
</wa-button-group>
<br /><br />
<wa-button-group label="Alignment" variant="danger">
<wa-button>Left</wa-button>
<wa-button>Center</wa-button>
<wa-button>Right</wa-button>
</wa-button-group>
```
You can still use the buttons own `variant` attribute to override the inherited variant.
```html {.example}
<wa-button-group label="Alignment" variant="brand">
<wa-button>Left</wa-button>
<wa-button>Center</wa-button>
<wa-button variant="neutral">Right</wa-button>
</wa-button-group>
```
### Pill Buttons
Pill buttons are supported through the button's `pill` attribute.
@@ -141,7 +87,7 @@ Dropdowns can be placed into button groups.
### Split Buttons
Create a split button using a button and a dropdown. Use a [visually hidden](/docs/components/visually-hidden) label to ensure the dropdown is accessible to users with assistive devices.
Create a split button using a button and a dropdown. Use a [visually hidden](/docs/utilities/visually-hidden) label to ensure the dropdown is accessible to users with assistive devices.
```html {.example}
<wa-button-group label="Example Button Group">

View File

@@ -5,7 +5,7 @@ layout: component
category: Imagery
---
This is especially useful for comparing images, but can be used for comparing any type of content (for an example of using it to compare entire UIs, check out our [theme pages](/docs/themes/default/)).
This is especially useful for comparing images, but can be used for comparing any type of content (for an example of using it to compare entire UIs, check out our [theme page](/docs/themes)).
For best results, use content that shares 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

@@ -178,11 +178,11 @@ If you want the dialog to close when the user clicks on the overlay, add the `li
### Preventing the Dialog from Closing
By default, dialogs will close when the user clicks the close button, clicks the overlay, or presses the [[Escape]] key. In most cases, the default behavior is the best behavior in terms of UX. However, there are situations where this may be undesirable, such as when data loss will occur.
By default, dialogs will close when the user clicks the close button or presses the [[Escape]] key. In most cases, the default behavior is the best behavior in terms of UX. However, there are situations where this may be undesirable, such as when data loss will occur.
To keep the dialog open in such cases, you can cancel the `wa-hide` event. When canceled, the dialog will remain open and pulse briefly to draw the user's attention to it.
You can use `event.detail.source` to determine which element 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.
You can use `event.detail.source` to determine which element triggered the request to close. This example prevents the dialog from closing unless a specific button is clicked.
```html {.example}
<wa-dialog label="Dialog" class="dialog-deny-close">

View File

@@ -41,7 +41,7 @@ Use the `--spacing` custom property to change the amount of space between the di
### Orientation
The default orientation for dividers is `horizontal`. Set `orientation` attribute to `vertical` to draw a vertical divider. The divider will span the full height of its container.
The default orientation for dividers is `horizontal`. Set `orientation` attribute to `vertical` to draw a vertical divider. The divider will span the full height of its [Flexbox](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Flexbox) or [CSS Grid](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/grid) container.
```html {.example}
<div style="display: flex; align-items: center;">
@@ -53,9 +53,13 @@ The default orientation for dividers is `horizontal`. Set `orientation` attribut
</div>
```
### Menu Dividers
:::info
If your container isn't Flexbox or CSS Grid, you may need to set an explicit height for the divider.
:::
Use dividers in [menus](/docs/components/menu) to visually group menu items.
### Dropdown Dividers
Use dividers in [dropdowns](/docs/components/dropdown) to visually group dropdown items.
```html {.example}
<wa-dropdown style="max-width: 200px;">

View File

@@ -2,6 +2,7 @@
title: Intersection Observer
description: Tracks immediate child elements and fires events as they move in and out of view.
layout: component
category: Utilities
---
This component leverages the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) to track when its direct children enter or leave a designated root element. The `wa-intersect` event fires whenever elements cross the visibility threshold.

View File

@@ -102,7 +102,7 @@ The default orientation for radio items is `vertical`. Set the `orientation` to
### Sizing Options
The size of [Radios](/docs/components/radio) will be determined by the Radio Group's `size` attribute.
The size of radios will be determined by the Radio Group's `size` attribute.
```html {.example}
<wa-radio-group label="Select an option" size="medium" value="medium" onchange="this.size = this.value">
@@ -113,7 +113,7 @@ The size of [Radios](/docs/components/radio) will be determined by the Radio Gro
```
:::info
[Radios](/docs/components/radio) and [Radio Buttons](/docs/components/radio-button) also have a `size` attribute,
[Radios](/docs/components/radio) also have a `size` attribute,
which will override the inherited size when used.
:::

View File

@@ -285,9 +285,10 @@ Remember that custom tags are rendered in a shadow root. To style them, you can
const name = option.querySelector('wa-icon[slot="start"]').name;
// You can return a string, a Lit Template, or an HTMLElement here
// Important: include data-value so the tag can be removed properly!
return `
<wa-tag with-remove>
<wa-icon name="${name}" style="padding-inline-end: .5rem;"></wa-icon>
<wa-tag with-remove data-value="${option.value}">
<wa-icon name="${name}"></wa-icon>
${option.label}
</wa-tag>
`;
@@ -299,6 +300,10 @@ Remember that custom tags are rendered in a shadow root. To style them, you can
Be sure you trust the content you are outputting! Passing unsanitized user input to `getTag()` can result in XSS vulnerabilities.
:::
:::info
When using custom tags with `with-remove`, you must include the `data-value` attribute set to the option's value. This allows the select to identify which option to deselect when the tag's remove button is clicked.
:::
### Lazy loading options
Lazy loading options works similarly to native `<select>` elements. The select component handles various scenarios intelligently:

View File

@@ -0,0 +1,101 @@
---
title: Angular
description: Tips for using Web Awesome in your Angular app.
layout: page-outline
---
# Angular
Angular [plays nice](https://custom-elements-everywhere.com/#angular) with custom elements, so you can use Web Awesome in your Angular apps with ease.
## Installation
### Download the npm package
To add Web Awesome to your Angular app, install the package from npm.
```bash
npm install @awesome.me/webawesome
```
### Update the Angular Configuration
Next, [include a theme](/getting-started/themes). In this example, we'll import the light theme.
Its also important to load the components by using a `<script>` tag into the index.html file. However, the Angular way to do it is by adding a script configurations into your angular.json file as follows:
```json
"architect": {
"build": {
...
"options": {
...
"styles": [
"src/styles.scss",
"@awesome.me/webawesome/dist/styles/webawesome.css"
]
...
}
}
}
```
## Configuration
Then make sure to apply the custom elements schema as shown below.
```js
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}
```
## Reference Web Awesome components in your Angular component code
```js
// need to have both or Angular will tree shake the component out.
import type { WaDrawer } from '@awesome.me/webawesome/dist/components/drawer/drawer.js';
import "@awesome.me/webawesome/dist/components/drawer/drawer.js";
@Component({
selector: 'app-drawer-example',
template: '<div id="page"><button (click)="showDrawer()">Show drawer</button><wa-drawer #drawer label="Drawer" class="drawer-focus" style="--size: 50vw"><p>Drawer content</p></wa-drawer></div>'
})
export class DrawerExampleComponent implements OnInit {
// use @ViewChild to get a reference to the #drawer element within component template
@ViewChild('drawer')
drawer?: ElementRef<WaDrawer>;
...
constructor(...) {
}
ngOnInit() {
}
...
showDrawer() {
// use nativeElement to access Web Awesome components
this.drawer?.nativeElement.show();
}
}
```
Now you can start using Web Awesome components in your app!
:::tip
Are you using Web Awesome with Angular? [Help us improve this page!](https://github.com/shoelace-style/webawesome/blob/next/docs/frameworks/angular.md)
:::

View File

@@ -0,0 +1,193 @@
---
title: React
description: Tips for using Web Awesome in your React app.
layout: page-outline
---
# React
Web Awesome offers a React version of every component to provide an idiomatic experience for React users. You can easily toggle between HTML and React examples throughout the documentation.
## Installation
To add Web Awesome to your React app, install the package from npm.
```bash
npm install @awesome.me/webawesome
```
Next, import the Web Awesome stylesheet, import the components you need, and then start using Web Awesome!
```jsx
// App.jsx (React 19, using native custom elements)
import '@awesome.me/webawesome/dist/styles/webawesome.css';
import '@awesome.me/webawesome/dist/components/button/button.js';
export default function App () {
return <wa-button>I'm a button!</wa-button>
}
```
Now you can start using components!
### Preact
Preact users facing type errors using components may benefit from setting "paths" in their tsconfig.json so that react types will instead resolve to preact/compat as described in [Preact's typescript documentation](https://preactjs.com/guide/v10/typescript/#typescript-preactcompat-configuration).
## Usage
### Importing Components
Every Web Awesome component is available to import as a React component. Note that we're importing the `<WaButton>` _React component_ instead of the `<wa-button>` _custom element_ in the example below.
```jsx
import WaButton from '@awesome.me/webawesome/react/button/index.js';
const MyComponent = () => <WaButton variant="primary">Click me</WaButton>;
export default MyComponent;
```
#### Notes about tree shaking
Previously, it was recommended to import from a single entrypoint like so:
```jsx
import { WaButton } from '@awesome.me/webawesome/dist/react';
```
However, tree-shaking extra Web Awesome components proved to be a challenge. As a result, we now recommend cherry-picking components you want to use, rather than importing from a single entrypoint.
```diff
- import { WaButton } from '@awesome.me/webawesome/dist/react';
+ import WaButton from '@awesome.me/webawesome/dist/react/button/index.js';
```
You can find a copy + paste import for each component in the "importing" section of its documentation.
### Event Handling
Many Web Awesome components emit [native events](https://developer.mozilla.org/en-US/docs/Web/API/Event). For example, the [input component](/components/input) emits the `input` event when it receives input. In React, you can listen for the event using `onInput`.
Here's how you can bind the input's value to a state variable.
```jsx
import { useState } from 'react';
import WaInput from '@awesome.me/webawesome/dist/react/input/index.js';
function MyComponent() {
const [value, setValue] = useState('');
return <>
<WaInput value={value} onInput={event => setValue(event.target.value)} />;
<WaInput defaultValue={"Foo"} /> {/* This is an "uncontrolled input" */}
</>
}
export default MyComponent;
```
If you're using TypeScript, it's important to note that `event.target` will be a reference to the underlying custom element. You can use `(event.target as any).value` as a quick fix, or you can strongly type the event target as shown below.
```tsx
import { useState } from 'react';
import WaInput from '@awesome.me/webawesome/dist/react/input/index.js';
import type WaInputElement from '@awesome.me/webawesome/dist/components/input/input.js';
function MyComponent() {
const [value, setValue] = useState('');
return <WaInput value={value} onInput={event => setValue((event.target as WaInputElement).value)} />;
}
export default MyComponent;
```
You can also import the event type for use in your callbacks, shown below.
```tsx
import { useCallback, useState } from 'react';
import WaInput, { type WaInputEvent } from '@awesome.me/webawesome/dist/react/input/index.js';
import type WaInputElement from '@awesome.me/webawesome/dist/components/input/input.js';
function MyComponent() {
const [value, setValue] = useState('');
const onInput = useCallback((event: WaInputEvent) => {
setValue(event.detail);
}, []);
return <WaInput value={value} onInput={event => setValue((event.target as WaInputElement).value)} />;
}
export default MyComponent;
```
## Testing with Jest
Testing with web components can be challenging if your test environment runs in a Node environment (i.e. it doesn't run in a real browser). Fortunately, [Jest](https://jestjs.io/) has made a number of strides to support web components and provide additional browser APIs. However, it's still not a complete replication of a browser environment.
Here are some tips that will help smooth things over if you're having trouble with Jest + Web Awesome.
:::tip
If you're looking for a fast, modern testing alternative, consider [Web Test Runner](https://modern-web.dev/docs/test-runner/overview/).
:::
### Upgrade Jest
Jest underwent a major revamp and received support for web components in [version 26.5.0](https://github.com/facebook/jest/blob/main/CHANGELOG.md#2650) when it introduced [JSDOM 16.2.0](https://github.com/jsdom/jsdom/blob/master/Changelog.md#1620). This release also included a number of mocks for built-in browser functions such as `MutationObserver`, `document.createRange`, and others.
If you're using [Create React App](https://reactjs.org/docs/create-a-new-react-app.html#create-react-app), you can update `react-scripts` which will also update Jest.
```
npm install react-scripts@latest
```
### Mock Missing APIs
Some components use `window.matchMedia`, but this function isn't supported by JSDOM so you'll need to mock it yourself.
In `src/setupTests.js`, add the following.
```js
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn()
}))
});
```
For more details, refer to Jest's [manual mocking](https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom) documentation.
### Transform ES Modules
ES Modules are a [well-supported browser standard](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/). This is how Web Awesome is distributed, but most React apps expect CommonJS. As a result, you'll probably run into the following error.
```
Error: Unable to import outside of a module
```
To fix this, add the following to your `package.json` which tells the transpiler to process Web Awesome modules.
```js
{
"jest": {
"transformIgnorePatterns": ["node_modules/(?!(@awesome.me|lit|@lit-labs))"]
}
}
```
These instructions are for apps created via Create React App. If you're using Jest directly, you can add `transformIgnorePatterns` directly into `jest.config.js`.
For more details, refer to Jest's [`transformIgnorePatterns` customization](https://jestjs.io/docs/tutorial-react-native#transformignorepatterns-customization) documentation.
:::tip
Are you using Web Awesome with React? [Help us improve this page!](https://github.com/shoelace-style/webawesome/blob/next/docs/frameworks/react.md)
:::

View File

@@ -0,0 +1,90 @@
---
title: Svelte
description: Tips for using Web Awesome in your Svelte app.
layout: page-outline
---
# Svelte
Svelte [plays nice](https://custom-elements-everywhere.com/#svelte) with custom elements, so you can use Web Awesome in your Svelte apps with ease.
## Installation
To add Web Awesome to your Svelte app, install the package from npm.
```bash
npm install @awesome.me/webawesome
```
Next, import the Web Awesome stylesheet, import the components you need, and then start using Web Awesome!
```jsx
// main.js or main.ts
import '@awesome.me/webawesome/dist/styles/webawesome.css';
import '@awesome.me/webawesome/dist/components/button/button.js';
```
## Usage
### QR code generator example
```jsx
<h1>Live editing</h1>
<wa-input label="Message" value={message} oninput={event => message = event.target.value}></wa-input>
<wa-alert open>
<wa-icon slot="icon" name="info-circle"></wa-icon>
{message}
</wa-alert>
<script>
import '@awesome.me/webawesome/dist/components/alert/alert.js'
import '@awesome.me/webawesome/dist/components/input/input.js';
let message = $state('')
</script>
```
### Two-way Binding
One caveat is there's currently Svelte only supports `bind:value` directive in `<input>`, `<textarea>` and `<select>`, but you can still achieve two-way binding manually.
```jsx
// ❌ These do not work
<wa-input bind:value="name"></wa-input>
<wa-select bind:value="job">
<wa-option value="designer">Designer</wa-option>
<wa-option value="developer">Developer</wa-option>
</wa-select>
// ✅ These are a bit longer, but work
<wa-input value={name} oninput={event => name = event.target.value}></wa-input>
<wa-select value={job} oninput={event => job = event.target.value}>
<wa-option value="designer">Designer</wa-option>
<wa-option value="developer">Developer</wa-option>
</wa-select>
```
:::tip
Are you using Web Awesome with Svelte? [Help us improve this page!](https://github.com/shoelace-style/webawesome/blob/next/docs/frameworks/svelte.md)
:::
### Slots
Slots in Web Awesome/web components are functionally the same as basic slots in Svelte. Slots can be assigned to elements using the `slot` attribute followed by the name of the slot it is being assigned to.
Here is an example:
```jsx
<wa-drawer label="Drawer" placement="start" class="drawer-placement-start" bind:open={drawerIsOpen}>
This drawer slides in from the start.
<div slot="footer">
<wa-button variant="primary" onclick={() => (drawerIsOpen = false)}>
Close
</wa-button>
</div>
</wa-drawer>
```

View File

@@ -0,0 +1,102 @@
---
title: Vue (version 2)
description: Tips for using Web Awesome in your Vue 2 app.
layout: page-outline
---
# Vue (version 2)
Vue [plays nice](https://custom-elements-everywhere.com/#vue) with custom elements, so you can use Web Awesome in your Vue apps with ease.
:::tip
These instructions are for Vue 2. If you're using Vue 3 or above, please see the [Vue 3 instructions](/frameworks/vue).
:::
## Installation
To add Web Awesome to your Vue app, install the package from npm.
```bash
npm install @awesome.me/webawesome
```
Next, import the Web Awesome stylesheet, import the components you need, and then start using Web Awesome!
```jsx
// main.js or main.ts
import '@awesome.me/webawesome/dist/styles/webawesome.css';
import '@awesome.me/webawesome/dist/components/button/button.js';
```
## Configuration
You'll need to tell Vue to ignore Web Awesome components. This is pretty easy because they all start with `wa-`.
```js
import Vue from 'vue';
import App from './App.vue';
Vue.config.ignoredElements = [/wa-/];
const app = new Vue({
render: h => h(App)
});
app.$mount('#app');
```
Now you can start using Web Awesome components in your app!
## Usage
### Binding Complex Data
When binding complex data such as objects and arrays, use the `.prop` modifier to make Vue bind them as a property instead of an attribute.
```html
<wa-color-picker :swatches.prop="mySwatches" />
```
### Two-way Binding
One caveat is there's currently [no support for v-model on custom elements](https://github.com/vuejs/vue/issues/7830), but you can still achieve two-way binding manually.
```html
<!-- ❌ This doesn't work -->
<wa-input v-model="name"></wa-input>
<!-- ✅ This works, but it's a bit longer -->
<wa-input :value="name" @input="name = $event.target.value"></wa-input>
```
If that's too verbose for your liking, you can use a custom directive instead. [This utility](https://www.npmjs.com/package/@shoelace-style/vue-sl-model) adds a custom directive that will work just like `v-model` but for Web Awesome components. To install it, use this command.
```bash
npm install @shoelace-style/vue-sl-model@1
```
Next, import the directive and enable it like this.
```js
import Vue from 'vue';
import Web AwesomeModelDirective from '@shoelace-style/vue-sl-model';
import App from './App.vue';
Vue.use(Web AwesomeModelDirective);
Vue.config.ignoredElements = [/wa-/];
const app = new Vue({
render: h => h(App)
});
app.$mount('#app');
```
Now you can use the `v-sl-model` directive to keep your data in sync!
```html
<wa-input v-sl-model="name"></wa-input>
```
:::tip
Are you using Web Awesome with Vue 2? [Help us improve this page!](https://github.com/shoelace-style/shoelace/blob/next/docs/frameworks/vue-2.md)
:::

View File

@@ -0,0 +1,119 @@
---
title: Vue
description: Tips for using Web Awesome in your Vue 3 app.
layout: page-outline
---
# Vue
Vue [plays nice](https://custom-elements-everywhere.com/#vue) with custom elements, so you can use Web Awesome in your Vue apps with ease.
:::tip
These instructions are for Vue 3 and above. If you're using Vue 2, please see the [Vue 2 instructions](/frameworks/vue-2).
:::
## Installation
To add Web Awesome to your Vue app, install the package from npm.
```bash
npm install @awesome.me/webawesome
```
Next, import the Web Awesome stylesheet, import the components you need, and then start using Web Awesome!
```jsx
// main.js or main.ts
import '@awesome.me/webawesome/dist/styles/webawesome.css';
import '@awesome.me/webawesome/dist/components/button/button.js';
```
## Configuration
If you haven't configured your Vue.js project to work with custom elements/web components, follow [the instructions here](https://vuejs.org/guide/extras/web-components.html#using-custom-elements-in-vue) based on your project type to ensure your project will not throw an error when it encounters a custom element.
Now you can start using Web Awesome components in your app!
## Types
Once you have configured your application for custom elements, you should be able to use Shoelace in your application without it causing any errors. Unfortunately, this doesn't register the custom elements to behave like components built using Vue. To provide autocomplete information and type safety for your components, you can import the Shoelace Vue types into your `tsconfig.json` to get better integration in your standard Vue and JSX templates.
```json
{
"compilerOptions": {
"types": ["@awesome.me/webawesome/dist/types/vue/index.d.ts"]
}
}
```
## Usage
### QR code generator example
```html
<template>
<div class="container">
<h1>QR code generator</h1>
<wa-input maxlength="255" clearable label="Value" v-model="qrCode"></wa-input>
<wa-qr-code :value="qrCode"></wa-qr-code>
</div>
</template>
<script setup>
import { ref } from 'vue';
import '@awesome.me/webawesome/dist/components/qr-code/qr-code.js';
import '@awesome.me/webawesome/dist/components/input/input.js';
const qrCode = ref();
</script>
<style>
.container {
max-width: 400px;
margin: 0 auto;
}
</style>
```
### Binding Complex Data
When binding complex data such as objects and arrays, use the `.prop` modifier to make Vue bind them as a property instead of an attribute.
```html
<wa-color-picker :swatches.prop="mySwatches" />
```
### Two-way Binding
One caveat is there's currently [varying levels of support for v-model on custom elements](https://github.com/vuejs/vue/issues/7830), but you can still achieve two-way binding manually.
```html
<!-- ❌ This doesn't work -->
<wa-input v-model="name"></wa-input>
<!-- ✅ This works, but it's a bit longer -->
<wa-input :value="name" @input="name = $event.target.value"></wa-input>
```
<!-- Remove this until we can test it works, and if there is interest. Vue3 kinda clobbered it all up
If that's too verbose for your liking, you can use a custom directive instead. [This utility](https://www.npmjs.com/package/@shoelace-style/vue-wa-model) adds a custom directive that will work just like `v-model` but for Web Awesome components. -->
### Slots
Slots in Web Awesome / web components are functionally the same as basic slots in Vue. Slots can be assigned to elements using the `slot` attribute followed by the name of the slot it is being assigned to.
Here is an example:
```html
<wa-drawer label="Drawer" placement="start" class="drawer-placement-start" :open="drawerIsOpen">
This drawer slides in from the start.
<div slot="footer">
<wa-button variant="primary" @click=" drawerIsOpen = false">Close</wa-button>
</div>
</wa-drawer>
```
:::tip
Are you using Web Awesome with Vue? [Help us improve this page!](https://github.com/shoelace-style/webawesome/blob/next/docs/frameworks/vue.md)
:::

View File

@@ -1,21 +1,71 @@
---
title: Changelog
dateLastUpdated: 2025-11-07
description: Changes to each version of the project are documented here.
layout: page-outline
---
<p class="wa-caption-s">Last updated: <wa-format-date month="long" day="numeric" year="numeric" date="{{ lastUpdatedISO }}"></wa-format-date></p>
<p class="wa-caption-s">Last updated: <wa-format-date month="long" day="numeric" year="numeric" date="{{ dateLastUpdated }}"></wa-format-date></p>
Web Awesome follows [Semantic Versioning](https://semver.org/). Breaking changes in components with the <wa-badge variant="brand">Stable</wa-badge> badge will not be accepted until the next major version. As such, all contributions must consider the project's roadmap and take this into consideration. Features that are deemed no longer necessary will be deprecated but not removed.
Components with the <wa-badge variant="warning">Experimental</wa-badge> badge should not be used in production. They are made available as release candidates for development and testing purposes. As such, changes to experimental components will not be subject to semantic versioning.
## Next
- Added types for Vue and Svelte generated by CEM [pr:]
- Added llms.txt to assist AI agents with using Web Awesome [discuss:1100]
- Added `justify-content` CSS utilities [pr:1930]
- Added missing `.wa-gap-4xl` utility class [pr:1931]
- Added `pointercancel` and `touchcancel` event handling to draggable elements to prevent drags from getting stuck
- Added `wa-justify-content-*` utility classes [pr:1930]
- Added missing `wa-gap-4xl` utility class [pr:1931]
- Added `track` and `indicator` CSS parts to `<wa-progress-ring>` [pr:1863]
- [Docs]: component APIs like slots, state, methods, etc, are now alphabetized [pr:1895]
- [Docs]: component APIs now properly check their inheritance chain [pr:1895]
- [Docs]: Included framework specific documentation for Svelte, Vue, and Angular. [pr:1895]
- Fixed a bug in `<wa-dropdown>` where submenu detection would not work in shadow dom. [pr:]
- Fixed a bug in `<wa-combobox>` that prevented the listbox from opening when options were preselected [issue:1883]
- Fixed a bug in `<wa-combobox>` that prevented the listbox from opening when options were preselected [issue:1883]
- Fixed a bug in `<wa-popup>` and `<wa-dropdown-item>` that caused an error when removing a popup while it was opening [issue:1910]
- Fixed a bug in `<wa-popup>` and `<wa-dropdown>` that caused errors when shadow DOM queries returned null [issue:1911]
- Fixed a bug in `<wa-combobox>` that prevented the listbox from opening when options were preselected [issue:1883]
- Fixed a bug in draggable elements that caused a TypeError on `touchend` events when `event.touches` was empty
- Fixed a bug in `<wa-tree-item>` that caused the cursor to show a pointer when no expand icon was present [pr:1936]
- Fixed a bug in `<wa-tree-item>` that caused the chevron to render the wrong direction in RTL [pr:1798]
- Improved the Persian translation [#1923]
- Modified `wa-align-items-*` utility classes to apply `display: flex` by default [pr:1943]
## 3.1.0
- Added `<wa-combobox>` as an experimental pro component [issue:1074]
- Added version 2.0.0 of the [official Web Awesome Figma Design Kit](/docs/resources/figma)
- Added npm support for Web Awesome Pro
- Added `layers.css` to define cascade layer order and updated palettes, themes, native styles, and utilities to import the new rule for more fail-safe modularity [pr:1793]
- [PRO]: Fixed a few sizing bugs in `<wa-page>` and `slot="footer"` no longer will always "overflow" the container.
- Fixed a bug in `<wa-slider>` that caused some touch devices to end up with the incorrect value [issue:1703]
- Fixed a bug in `<wa-card>` that prevented some slots from being detected correctly [discuss:1450]
- Fixed a z-index bug in `<wa-scroller>` styles [issue:1724]
- Fixed a bug in `<wa-icon>` that caused some icon libraries to render with the incorrect SVG fill [issue:1733]
- Fixed a bug in `<wa-tree-item>` that caused the spinner to not show when lazy loading [issue:1678]
- Fixed a bug in `<wa-dropdown>` that caused the browser to hang when cancelling the `wa-hide` event [issue:1483]
- Fixed a bug in `<wa-dropdown-item>` that prevented the icon dependency from being imported [issue:1825]
- Fixed a bug in `<wa-select>` that prevented clicks on the tag's remove button from removing options in multiple mode
- Fixed a bug in `<wa-select>` that caused tags to appear in alphabetical order instead of selection order when using `multiple`
- Improved performance of `<wa-icon>` so initial rendering occurs faster, especially with multiple icons on the page [issue:1729]
- Improved `<wa-slider>` to not throw an error when string values are passed to the `min`, `max`, and `step` properties [issue:1823]
- Fixed a bug in Web Awesome form controls that caused `<wa-input form="foo">` to set the form property to equal `"foo"` instead of returning an `HTMLFormElement` breaking platform expectations. [pr:1815]
- Fixed a bug in `<wa-button>` causing it to not copy over attributes for form submissions. [pr:1815]
- Fixed a bug where the build script was not building `/dist/(utilities|events).js` [pr:1816]
- Improved performance of all components by fixing how CSS is imported and reused [issue:1812]
- Modified the default `transition` styles of `<wa-dropdown-item>` to use design tokens [pr:1693]
## 3.0.0
- 🚨 BREAKING: Changed `appearance="filled outlined"` to `appearance="filled-outlined"` in the following elements [issue:1127]
- `<wa-badge>`
- `<wa-button>`
- `<wa-callout>`
- `<wa-card>`
- `<wa-details>`
- `<wa-input>`
- `<wa-select>`
@@ -245,7 +295,7 @@ Many of these changes and improvements were the direct result of feedback from u
- 🚨 BREAKING: Renamed `<image-comparer>` to `<wa-comparison>` and improved compatibility for non-image content
- 🚨 BREAKING: Added slot detection to `<wa-dialog>` and `<wa-drawer>` so you don't need to specify `with-header` and `with-footer`; headers are on by default now, but you can use the `without-header` attribute to turn them off
- 🚨 BREAKING: Renamed the `image` slot to `media` for a more appropriate naming convention
- Added [a theme builder](/docs/themes/edit/) to create your own themes
- Added Theme Builder to create your own themes
- Added a new Blog & News pattern category
- Added a new free component: `<wa-scroller>` (#1 of 14 per stretch goals)
- Added support for Duotone Thin, Light, and Regular styles and the Sharp Duotone family of styles to `<wa-icon>`
@@ -262,7 +312,7 @@ Many of these changes and improvements were the direct result of feedback from u
### Enhancements {data-no-outline}
- Added `appearance` to [`<wa-details>`](/docs/components/details) and [`<wa-card>`](/docs/components/card) and support for the [appearance utilities](/docs/utilities/appearance/) in the [`<details>` native styles](/docs/utilities/native/details).
- Added `appearance` to [`<wa-details>`](/docs/components/details) and [`<wa-card>`](/docs/components/card) and support for the appearance utilities in the [`<details>` native styles](/docs/utilities/native/#details).
- Added an `orange` scale to all color palettes
- Added the [`.wa-cloak` utility](/docs/utilities/fouce) to prevent FOUCE
- Added the [`allDefined()` utility](/docs/usage/#all-defined) for awaiting component registration
@@ -287,7 +337,7 @@ Many of these changes and improvements were the direct result of feedback from u
- Revert `<wa-dialog>` structure and CSS to fix clipped content in dialogs (WA-A #123) and light dismiss in iOS Safari (WA-A #201)
- Fixed a bug in `<wa-color-picker>` that prevented light dismiss from working when clicking immediately above the color picker dropdown
- Fixed a bug in `<wa-progress>` that prevented Safari from animation progress changes
- Fixed the missing indeterminate icon in [native checkbox styles](/docs/utilities/native/checkbox)
- Fixed the missing indeterminate icon in [native checkbox styles](/docs/utilities/native/#form-controls)
- Fixed a bug in `<wa-radio>` where elements would stack instead of display inline
- Docs fixes:
- Fixed the search dialog's styles so it doesn't jump around as you search
@@ -434,13 +484,13 @@ Many of these changes and improvements were the direct result of feedback from u
### Native styles {data-no-outline}
- Added native styles for
[buttons](/docs/utilities/native/button),
[input fields](/docs/utilities/native/input),
[dialogs](/docs/utilities/native/dialog),
[details](/docs/utilities/native/details),
[tables](/docs/utilities/native/table),
[lists](/docs/utilities/native/lists),
and most [content elements](/docs/utilities/native/content).
[buttons](/docs/utilities/native/#buttons),
[input fields](/docs/utilities/native/#form-controls),
[dialogs](/docs/utilities/native/#dialog),
[details](/docs/utilities/native/#details),
[tables](/docs/utilities/native/#tables),
[lists](/docs/utilities/native/#lists),
and most [content elements](/docs/utilities/native/#typography).
### Style utilities {data-no-outline}
@@ -448,7 +498,7 @@ Many of these changes and improvements were the direct result of feedback from u
- Added [appearance utilities](/docs/utilities/appearance/)
- Added [size utilities](/docs/utilities/size/)
- Added [layout utilities](/docs/layout/#utilities)
- Added [`.wa-visually hidden`](/docs/utilities/a11y/#visually-hidden) utility
- Added [`.wa-visually hidden`](/docs/utilities/visually-hidden) utility
- Added [`<wa-page>`](/docs/components/page/#styles) native styles and utilities
### Components {data-no-outline}

View File

@@ -164,7 +164,7 @@ This section can be a lot to digest in one sitting, so don't feel like you need
Web Awesome is built with accessibility in mind. Creating generic components that are fully accessible to users with varying capabilities across a multitude of circumstances is a daunting challenge. Oftentimes, the solution to an a11y problem is not written in black and white and, therefore, we may not get it right the first time around. There are, however, guidelines we can follow in our effort to make Web Awesome an accessible foundation from which applications and websites can be built.
We take this commitment seriously, so please ensure your contributions have this goal in mind. If you need help with anything a11y-related, please [reach out to the community](/resources/community) for assistance. If you discover an accessibility concern within the library, please file a bug on the [issue tracker](https://github.com/shoelace-style/webawesome/issues).
We take this commitment seriously, so please ensure your contributions have this goal in mind. If you need help with anything a11y-related, please reach out on the [community chat](https://discord.gg/mg8f26C) for assistance. If you discover an accessibility concern within the library, please file a bug on the [issue tracker](https://github.com/shoelace-style/webawesome/issues).
It's important to remember that, although accessibility starts with foundational components, it doesn't end with them. It everyone's responsibility to encourage best practices and ensure we're providing an optimal experience for all of our users.
@@ -364,7 +364,7 @@ Form controls should support submission and validation through the following con
### System Icons
Avoid inlining SVG icons inside of templates. If a component requires an icon, make sure `<wa-icon>` is a dependency of the component and use the [system library](/components/icon#customizing-the-system-library):
Avoid inlining SVG icons inside of templates. If a component requires an icon, make sure `<wa-icon>` is a dependency of the component and use the [system library](/docs/components/icon#customizing-the-system-library):
```html
<wa-icon library="system" name="..." variant="..."></wa-icon>

View File

@@ -0,0 +1,86 @@
---
title: LLMs
description: Web Awesome provides an llms.txt file to help AI assistants understand and work with our components.
layout: page-outline
---
The [llms.txt specification](https://llmstxt.org/) is a proposed standard for providing information to large language models (LLMs) in a format they can easily consume. It's like a robots.txt, but instead of telling search engines how to crawl your site, it helps AI assistants understand your project.
Web Awesome publishes an `llms.txt` file that provides AI tools with structured information about our components, including their APIs, properties, events, methods, slots, and CSS custom properties.
:::warning
This feature is experimental! The llms.txt format and its contents may change as we refine the output based on feedback and evolving AI capabilities.
:::
## Why Use It?
When working with AI coding assistants like Claude, ChatGPT, Copilot, or Cursor, you can reference the llms.txt file to give the AI context about Web Awesome components. This can lead to more accurate code suggestions and fewer hallucinations when the AI generates Web Awesome code.
## Accessing the File
The llms.txt file is available in every Web Awesome build at:
```
/dist/llms.txt
/dist-cdn/llms.txt
```
You can also find it in your `node_modules` directory if you've installed Web Awesome via npm:
```
node_modules/@awesome.me/webawesome/dist/llms.txt
```
## How to Use It
How you reference the file depends on which AI tool you're using.
### Claude Projects
If you're using [Claude Projects](https://www.anthropic.com/news/projects), you can add the llms.txt URL to your project knowledge. Claude will use this context when helping you write Web Awesome code.
### Cursor
In [Cursor](https://cursor.sh/), you can add the file to your project's documentation sources via **Cursor Settings > Features > Docs**. You can also reference the file directly in chat using `@Docs` after adding it, or paste the content into the chat context.
### VS Code + Copilot
GitHub Copilot in VS Code doesn't have a built-in way to reference external documentation files, but you can:
1. Copy the llms.txt file into your project's root directory
2. Open it in a VS Code tab (Copilot considers open files as context)
3. Use `#file` in Copilot Chat to explicitly reference it (e.g., `#file:llms.txt how do I create a dialog?`)
### VS Code + Claude Code
If you're using the [Claude Code extension](https://marketplace.visualstudio.com/items?itemName=anthropics.claude-code), you can reference the file directly by path:
```
@node_modules/@awesome.me/webawesome/dist/llms.txt
```
Or simply ask Claude to read it — Claude Code can access files in your project directly.
### Other AI Tools
Most AI coding assistants allow you to provide context through URLs, file uploads, or direct pasting. Check your tool's documentation for the best way to include external references.
## What's Included
The llms.txt file contains:
- An overview of Web Awesome and its capabilities
- Links to documentation sections
- A complete list of all components with descriptions
- Detailed API reference for each component including:
- Slots
- Properties and their types
- Methods and their signatures
- Events
- CSS custom properties
- CSS parts
- CSS states
## Feedback
Since this is experimental, we'd love to hear how it works for you! If you find issues with the generated content or have suggestions for improvement, please [open an issue on GitHub](https://github.com/shoelace-style/webawesome/issues).

View File

@@ -11,7 +11,7 @@ layout: page
<div class="wa-stack wa-gap-xl">
<div class="wa-stack wa-align-items-start">
<div class="wa-stack wa-gap-s">
<h2 class="anchor-heading wa-cluster wa-gap-xs" data-no-anchor><wa-icon name="github" family="brands" style="font-size: 2ch;"></wa-icon>Github</h2>
<h2 class="anchor-heading wa-cluster wa-gap-xs" data-no-anchor><wa-icon name="github" family="brands" style="font-size: 2ch;"></wa-icon>GitHub</h2>
<span class="wa-heading-m">Feature requests & bugs</span>
<p class="wa-caption-l">Notice a bug or have an idea? Open an issue on GitHub so we can triage, track, and ship fixes.</p>
</div>

View File

@@ -4,6 +4,9 @@ description: Style (and restyle) your website at will with any of Web Awesome's
layout: page
---
{% from "pro-badge.njk" import proBadge %}
{% from "free-badge.njk" import freeBadge %}
<h1>{{ title }}</h1>
<p>Themes are collections of <a href="/docs/tokens">design tokens</a> that give a cohesive look and feel to the entire Web Awesome library. Style and restyle your website at will by loading any pre-built theme.</p>
@@ -64,8 +67,9 @@ to create a project with any one of these themes.
<header class="wa-stack">
<div class="wa-cluster">
<h2 data-theme-name="name">Theme</h2>
<wa-badge data-free-badge appearance="outlined" variant="neutral" hidden>FREE</wa-badge>
<wa-badge data-pro-badge appearance="accent" hidden>PRO</wa-badge>
{{ freeBadge({ id: "free-badge", description: "This theme is available in the free version of Web Awesome." }) }}
{{ proBadge({ id: "pro-badge", description: "This theme requires access to Web Awesome Pro." }) }}
</div>
<p data-theme-description>Description</p>
</header>
@@ -162,8 +166,12 @@ Then apply the following classes to the `<html>` element:
const beforeFrame = document.querySelector('wa-zoomable-frame[slot="before"]');
const nameElement = document.querySelector('[data-theme-name]');
const descriptionElement = document.querySelector('[data-theme-description]');
const freeBadge = document.querySelector('[data-free-badge]');
const proBadge = document.querySelector('[data-pro-badge]');
const freeBadge = document.getElementById('free-badge');
const proBadge = document.getElementById('pro-badge');
// Hide both badges initially
if (freeBadge) freeBadge.hidden = true;
if (proBadge) proBadge.hidden = true;
function updateFrames(selectedValue, title, description, isPro, palette, brand) {
// Update theme classes on both frames

View File

@@ -1,35 +1,37 @@
---
title: Align Items
description: Align items utilities set the gap property of flex and grid containers, like other Web Awesome layout utilities.
description: Align items utilities align items within flex and grid containers on the cross axis.
layout: docs
tags: layoutUtilities
---
<style>
.preview-wrapper {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
border: var(--layout-example-border);
border-radius: var(--wa-border-radius-m);
min-block-size: 3em;
min-inline-size: 5em;
padding: var(--wa-space-2xs);
}
.preview-block {
aspect-ratio: 1 / 1;
background-color: var(--wa-color-neutral-fill-loud);
background-color: var(--layout-example-element-background);
border-radius: var(--wa-border-radius-s);
min-block-size: 1em;
}
</style>
Web Awesome includes classes to set the `align-items` property of flex and grid containers. They can be used alongside other Web Awesome layout utilities, like [cluster](/docs/utilities/cluster) and [stack](/docs/utilities/stack), to align children in container on the container's cross axis.
Web Awesome includes classes to set the `align-items` property of flex and grid containers. Use them alongside other Web Awesome layout utilities, like [cluster](/docs/utilities/cluster) and [stack](/docs/utilities/stack), to align items in a container on the container's [cross axis](#whats-the-cross-axis).
| Class Name | `align-items` Value | Preview |
| ------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `wa-align-items-baseline` | `baseline` | <div class="wa-cluster wa-align-items-baseline preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-center` | `center` | <div class="wa-cluster wa-align-items-center preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-end` | `flex-end` | <div class="wa-cluster wa-align-items-end preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-start` | `flex-start` | <div class="wa-cluster wa-align-items-start preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-stretch` | `stretch` | <div class="wa-cluster wa-align-items-stretch preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| Class Name | `align-items` Value | Preview |
| ------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wa-align-items-baseline` | `baseline` | <div class="wa-cluster wa-gap-2xs wa-align-items-baseline preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-center` | `center` | <div class="wa-cluster wa-gap-2xs wa-align-items-center preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-end` | `flex-end` | <div class="wa-cluster wa-gap-2xs wa-align-items-end preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-start` | `flex-start` | <div class="wa-cluster wa-gap-2xs wa-align-items-start preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-stretch` | `stretch` | <div class="wa-cluster wa-gap-2xs wa-align-items-stretch preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
## What's a Cross Axis?
## What's the Cross Axis?
The cross axis runs perpendicular to a flex container's content direction. For containers where `flex-direction` is `row` and content flows in the inline direction, the cross axis runs in the block direction. For containers where `flex-direction` is `column` and content flows in the block direction, the cross axis runs in the inline direction.
The cross axis runs perpendicular to a container's content direction. For containers where `flex-direction` is `row` and content flows in the inline direction, the cross axis runs in the block direction. For containers where `flex-direction` is `column` and content flows in the block direction, the cross axis runs in the inline direction.

View File

@@ -7,19 +7,21 @@ tags: layoutUtilities
<style>
:is(.wa-flank, .wa-grid, .wa-stack) > [class*='wa-cluster']:has(div:empty) {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
border-radius: var(--wa-border-radius-l);
padding: var(--wa-space-s);
border: var(--layout-example-border);
border-radius: var(--layout-example-border-radius);
padding: var(--layout-example-padding);
}
[class*='wa-cluster'] div:empty {
background-color: var(--wa-color-indigo-60);
border-radius: var(--wa-border-radius-m);
background-color: var(--layout-example-element-background);
border-radius: var(--layout-example-element-border-radius);
min-block-size: 4rem;
min-inline-size: 4rem;
}
</style>
{{ description }}
```html {.example}
<div class="wa-cluster">
<div></div>
@@ -84,7 +86,7 @@ Clusters are great for inline lists and aligning items of varying sizes.
## Align Items
By default, items are centered in the block direction of the `wa-cluster` container. You can add any of the following [`wa-align-items-*`](/docs/style-utilities/align-items) classes to an element with `wa-cluster` to specify how items are aligned in the block direction:
By default, items are centered in the block direction of the `wa-cluster` container. You can add any of the following [`wa-align-items-*`](/docs/utilities/align-items) classes to an element with `wa-cluster` to specify how items are aligned in the block direction:
- `wa-align-items-start`
- `wa-align-items-end`
@@ -119,7 +121,7 @@ By default, items are centered in the block direction of the `wa-cluster` contai
## Gap
By default, the gap between cluster items uses `--wa-space-m` from your theme. You can add any of the following [`wa-gap-*`](/docs/style-utilities/gap) classes to an element with `wa-cluster` to specify the gap between items:
By default, the gap between cluster items uses `--wa-space-m` from your theme. You can add any of the following [`wa-gap-*`](/docs/utilities/gap) classes to an element with `wa-cluster` to specify the gap between items:
- `wa-gap-0`
- `wa-gap-3xs`

View File

@@ -7,20 +7,20 @@ tags: layoutUtilities
<style>
:is(.wa-flank, .wa-grid, .wa-stack) > [class*='wa-flank']:has(div:empty) {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
border-radius: var(--wa-border-radius-l);
padding: var(--wa-space-s);
border: var(--layout-example-border);
border-radius: var(--layout-example-border-radius);
padding: var(--layout-example-padding);
}
[class*='wa-flank'] div:empty {
background-color: var(--wa-color-indigo-60);
border-radius: var(--wa-border-radius-m);
background-color: var(--layout-example-element-background);
border-radius: var(--layout-example-element-border-radius);
min-block-size: 4rem;
min-inline-size: 4rem;
}
</style>
When space is limited, the items wrap.
{{ description }} When space is limited, the items wrap.
```html {.example}
<div class="wa-flank">
@@ -137,7 +137,7 @@ The main content fills the remaining inline space of the container. By default,
## Align Items
By default, items are centered in the block direction of the `wa-flank` container. You can add any of the following [`wa-align-items-*`](/docs/style-utilities/align-items) classes to an element with `wa-flank` to specify how items are aligned in the block direction:
By default, items are centered in the block direction of the `wa-flank` container. You can add any of the following [`wa-align-items-*`](/docs/utilities/align-items) classes to an element with `wa-flank` to specify how items are aligned in the block direction:
- `wa-align-items-start`
- `wa-align-items-end`
@@ -168,7 +168,7 @@ By default, items are centered in the block direction of the `wa-flank` containe
## Gap
By default, the gap between flank items uses `--wa-space-m` from your theme. You can add any of the following [`wa-gap-*`](/docs/style-utilities/gap) classes to an element with `wa-flank` to specify the gap between items:
By default, the gap between flank items uses `--wa-space-m` from your theme. You can add any of the following [`wa-gap-*`](/docs/utilities/gap) classes to an element with `wa-flank` to specify the gap between items:
- `wa-gap-0`
- `wa-gap-3xs`

View File

@@ -22,7 +22,7 @@ As soon as all elements are registered _or_ after two seconds have elapsed, the
:::details Are you using Turbo in your app?
If you're using [Turbo](https://turbo.hotwired.dev/) to serve a multi-page application (MPA) as a single page application (SPA), you might notice FOUCE when navigating from page to page. This is because Turbo renders the new page's content before the autoloader has a change to register new components.
If you're using [Turbo](https://turbo.hotwired.dev/) to serve a multi-page application (MPA) as a single page application (SPA), you might notice FOUCE when navigating from page to page. This is because Turbo renders the new page's content before the autoloader has a chance to register new components.
The following function acts as a middleware to ensure components are registered _before_ the page shows, eliminating FOUCE for page-to-page navigation with Turbo.

View File

@@ -7,18 +7,20 @@ tags: layoutUtilities
<style>
[class*='wa-frame']:has(div:empty) {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
padding: var(--wa-space-s);
border: var(--layout-example-border);
padding: var(--layout-example-padding);
}
[class*='wa-frame'] div:empty {
background-color: var(--wa-color-indigo-60);
border-radius: var(--wa-border-radius-m);
background-color: var(--layout-example-element-background);
border-radius: var(--layout-example-element-border-radius);
min-block-size: 4rem;
min-inline-size: 4rem;
}
</style>
{{ description }}
```html {.example}
<div class="wa-frame" style="max-inline-size: 20rem;">
<div></div>
@@ -139,7 +141,7 @@ Frames have a square aspect ratio by default. You can append `:square` (1 / 1),
## Border Radius
Frames have a square border radius by default. You can add any of the following [`wa-border-radius-*`](/docs/style-utilities/border-radius) classes to an element with `wa-frame` to specify the border radius:
Frames have a square border radius by default. You can add any of the following [`wa-border-radius-*`](/docs/utilities/rounding) classes to an element with `wa-frame` to specify the border radius:
- `wa-border-radius-s`
- `wa-border-radius-m`

View File

@@ -6,28 +6,37 @@ tags: layoutUtilities
---
<style>
.preview-wrapper {
border: var(--layout-example-border);
border-radius: var(--wa-border-radius-m);
min-block-size: 3em;
min-inline-size: 5em;
padding: var(--wa-space-2xs);
}
.preview-block {
aspect-ratio: 1 / 1;
background-color: var(--wa-color-neutral-fill-loud);
background-color: var(--layout-example-element-background);
border-radius: var(--wa-border-radius-s);
min-block-size: 1.5em;
min-block-size: 1em;
}
</style>
Web Awesome includes classes to set the `gap` property of flex and grid containers. They can be used alongside other Web Awesome layout utilities, like [cluster](/docs/layout/cluster) and [stack](/docs/layout/stack), to change the space between items.
Or even by themselves — all gap properties also set `display: flex` with a specificity of 0 so that it can be trivially overridden.
Web Awesome includes classes to set the `gap` property of flex and grid containers. Use them alone to create a flex container with a gap, or use them alongside other Web Awesome layout utilities, like [cluster](/docs/utilities/cluster) and [stack](/docs/utilities/stack), to change the space between items.
Besides `wa-gap-0`, which sets `gap` to zero, each class corresponds to one of the [`--wa-space-*`](/docs/tokens/space) tokens in your theme.
| Class Name | `gap` Value | Preview |
| ------------ | ---------------- | ----------------------------------------------------------------------------------------------------------- |
| `wa-gap-0` | `0` | <div class="wa-cluster wa-gap-0"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-3xs` | `--wa-space-3xs` | <div class="wa-cluster wa-gap-3xs"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-2xs` | `--wa-space-2xs` | <div class="wa-cluster wa-gap-2xs"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-xs` | `--wa-space-xs` | <div class="wa-cluster wa-gap-xs"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-s` | `--wa-space-s` | <div class="wa-cluster wa-gap-s"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-m` | `--wa-space-m` | <div class="wa-cluster wa-gap-m"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-l` | `--wa-space-l` | <div class="wa-cluster wa-gap-l"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-xl` | `--wa-space-xl` | <div class="wa-cluster wa-gap-xl"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-2xl` | `--wa-space-2xl` | <div class="wa-cluster wa-gap-2xl"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-3xl` | `--wa-space-3xl` | <div class="wa-cluster wa-gap-3xl"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-0` | `0` | <div class="preview-wrapper wa-cluster wa-gap-0"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-3xs` | `--wa-space-3xs` | <div class="preview-wrapper wa-cluster wa-gap-3xs"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-2xs` | `--wa-space-2xs` | <div class="preview-wrapper wa-cluster wa-gap-2xs"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-xs` | `--wa-space-xs` | <div class="preview-wrapper wa-cluster wa-gap-xs"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-s` | `--wa-space-s` | <div class="preview-wrapper wa-cluster wa-gap-s"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-m` | `--wa-space-m` | <div class="preview-wrapper wa-cluster wa-gap-m"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-l` | `--wa-space-l` | <div class="preview-wrapper wa-cluster wa-gap-l"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-xl` | `--wa-space-xl` | <div class="preview-wrapper wa-cluster wa-gap-xl"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-2xl` | `--wa-space-2xl` | <div class="preview-wrapper wa-cluster wa-gap-2xl"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-3xl` | `--wa-space-3xl` | <div class="preview-wrapper wa-cluster wa-gap-3xl"><div class="preview-block"></div><div class="preview-block"></div></div> |
<!-- Pending 3.2.0 release -->
<!-- | `wa-gap-4xl` | `--wa-space-4xl` | <div class="preview-wrapper wa-cluster wa-gap-4xl"><div class="preview-block"></div><div class="preview-block"></div></div> | -->

View File

@@ -7,19 +7,21 @@ tags: layoutUtilities
<style>
:is(.wa-flank, .wa-grid, .wa-stack) > [class*='wa-grid']:has(div:empty) {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
border-radius: var(--wa-border-radius-l);
padding: var(--wa-space-s);
border: var(--layout-example-border);
border-radius: var(--layout-example-border-radius);
padding: var(--layout-example-padding);
}
[class*='wa-grid'] div:empty {
background-color: var(--wa-color-indigo-60);
border-radius: var(--wa-border-radius-m);
background-color: var(--layout-example-element-background);
border-radius: var(--layout-example-element-border-radius);
min-block-size: 4rem;
min-inline-size: 4rem;
}
</style>
{{ description }}
```html {.example}
<div class="wa-grid">
<div></div>
@@ -169,7 +171,7 @@ By default, grid items will wrap when the grid's column size is less than `20ch`
## Gap
By default, the gap between grid items uses `--wa-space-m` from your theme. You can add any of the following [`wa-gap-*`](/docs/style-utilities/gap) classes to an element with `wa-grid` to specify the gap between items:
By default, the gap between grid items uses `--wa-space-m` from your theme. You can add any of the following [`wa-gap-*`](/docs/utilities/gap) classes to an element with `wa-grid` to specify the gap between items:
- `wa-gap-0`
- `wa-gap-3xs`

View File

@@ -0,0 +1,40 @@
---
title: Justify Content
description: Justify content utilities determine how space is distributed between items in flex and grid containers.
layout: docs
tags: layoutUtilities
unpublished: true
unlisted: true
---
<style>
.preview-wrapper {
border: var(--layout-example-border);
border-radius: var(--wa-border-radius-m);
min-block-size: 3em;
min-inline-size: 5em;
padding: var(--wa-space-2xs);
}
.preview-block {
aspect-ratio: 1 / 1;
background-color: var(--layout-example-element-background);
border-radius: var(--wa-border-radius-s);
min-block-size: 1em;
}
</style>
Web Awesome includes classes to set the `justify-content` property of flex and grid containers. Use them alongside other Web Awesome layout utilities, like [cluster](/docs/utilities/cluster) and [stack](/docs/utilities/stack), to distribute space between items along the container's [main axis](#whats-the-main-axis).
| Class Name | `justify-content` Value | Preview |
| ---------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `wa-justify-content-start` | `flex-start` | <div class="wa-cluster wa-gap-2xs wa-justify-content-start preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-justify-content-end` | `flex-end` | <div class="wa-cluster wa-gap-2xs wa-justify-content-end preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-justify-content-center` | `center` | <div class="wa-cluster wa-gap-2xs wa-justify-content-center preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-justify-content-space-around` | `space-around` | <div class="wa-cluster wa-gap-2xs wa-justify-content-space-around preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-justify-content-space-between` | `space-between` | <div class="wa-cluster wa-gap-2xs wa-justify-content-space-between preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-justify-content-space-evenly` | `space-evenly` | <div class="wa-cluster wa-gap-2xs wa-justify-content-space-evenly preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
## What's the Main Axis?
The main axis runs parallel to a container's content direction. For grid containers and flex containers where `flex-direction` is `row`, the main axis runs in the inline direction. For containers where `flex-direction` is `column`, the main axis runs in the block direction.

View File

@@ -12,7 +12,7 @@ tags: styleUtilities
}
</style>
Web Awesome includes classes to set an element's `border-radius` property. They can be used alongside Web Awesome layout utilities, like [frame](/docs/layout/frame), to round all corners of an element.
Web Awesome includes classes to set an element's `border-radius` property. They can be used alongside Web Awesome layout utilities, like [frame](/docs/utilities/frame), to round all corners of an element.
Each class corresponds to one of the [`--wa-border-radius-*`](/docs/tokens/borders/#radius) tokens in your theme.

View File

@@ -7,19 +7,21 @@ tags: layoutUtilities
<style>
:is(.wa-flank, .wa-grid, .wa-stack) > [class*='wa-split']:has(div:empty) {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
border-radius: var(--wa-border-radius-l);
padding: var(--wa-space-s);
border: var(--layout-example-border);
border-radius: var(--layout-example-border-radius);
padding: var(--layout-example-padding);
}
[class*='wa-split'] div:empty {
background-color: var(--wa-color-indigo-60);
border-radius: var(--wa-border-radius-m);
background-color: var(--layout-example-element-background);
border-radius: var(--layout-example-element-border-radius);
min-block-size: 4rem;
min-inline-size: 4rem;
}
</style>
{{ description }}
```html {.example}
<div class="wa-split">
<div></div>
@@ -106,7 +108,7 @@ Items can be split across a row or a column by appending `:row` or `:column` to
## Align Items
By default, items are centered on the cross axis of the `wa-split` container. You can add any of the following [`wa-align-items-*`](/docs/style-utilities/align-items) classes to an element with `wa-split` to specify how items are aligned:
By default, items are centered on the cross axis of the `wa-split` container. You can add any of the following [`wa-align-items-*`](/docs/utilities/align-items) classes to an element with `wa-split` to specify how items are aligned:
- `wa-align-items-start`
- `wa-align-items-end`
@@ -139,7 +141,7 @@ These modifiers specify how items are aligned in the block direction for `wa-spl
## Gap
A split's gap determines how close items can be before they wrap. By default, the gap between split items uses `--wa-space-m` from your theme. You can add any of the following [`wa-gap-*`](/docs/style-utilities/gap) classes to an element with `wa-split` to specify the gap between items:
A split's gap determines how close items can be before they wrap. By default, the gap between split items uses `--wa-space-m` from your theme. You can add any of the following [`wa-gap-*`](/docs/utilities/gap) classes to an element with `wa-split` to specify the gap between items:
- `wa-gap-0`
- `wa-gap-3xs`

View File

@@ -7,19 +7,21 @@ tags: layoutUtilities
<style>
:is(.wa-flank, .wa-grid, .wa-stack) > [class*='wa-stack']:has(div:empty) {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
border-radius: var(--wa-border-radius-l);
padding: var(--wa-space-s);
border: var(--layout-example-border);
border-radius: var(--layout-example-border-radius);
padding: var(--layout-example-padding);
}
[class*='wa-stack'] div:empty {
background-color: var(--wa-color-indigo-60);
border-radius: var(--wa-border-radius-m);
background-color: var(--layout-example-element-background);
border-radius: var(--layout-example-element-border-radius);
min-block-size: 4rem;
min-inline-size: 4rem;
}
</style>
{{ description }}
```html {.example}
<div class="wa-stack">
<div></div>
@@ -62,7 +64,7 @@ Stacks are well suited for forms, text, and ensuring consistent spacing between
## Align Items
By default, items stretch to fill the inline size of the `wa-stack` container. You can add any of the following [`wa-align-items-*`](/docs/style-utilities/align-items) classes to an element with `wa-stack` to specify how items are aligned in the inline direction:
By default, items stretch to fill the inline size of the `wa-stack` container. You can add any of the following [`wa-align-items-*`](/docs/utilities/align-items) classes to an element with `wa-stack` to specify how items are aligned in the inline direction:
- `wa-align-items-start`
- `wa-align-items-end`
@@ -92,7 +94,7 @@ By default, items stretch to fill the inline size of the `wa-stack` container. Y
## Gap
By default, the gap between stack items uses `--wa-space-m` from your theme. You can add any of the following [`wa-gap-*`](/docs/style-utilities/gap) classes to an element with `wa-stack` to specify the gap between items:
By default, the gap between stack items uses `--wa-space-m` from your theme. You can add any of the following [`wa-gap-*`](/docs/utilities/gap) classes to an element with `wa-stack` to specify the gap between items:
- `wa-gap-0`
- `wa-gap-3xs`

View File

@@ -4,11 +4,11 @@
"access": "public"
},
"description": "A forward-thinking library of web components.",
"version": "3.0.0-beta.6",
"version": "3.1.0",
"homepage": "https://webawesome.com/",
"author": "Web Awesome",
"license": "MIT",
"customElements": "dist/custom-elements.json",
"customElements": "dist-cdn/custom-elements.json",
"web-types": "./dist/web-types.json",
"type": "module",
"types": "dist/webawesome.d.ts",
@@ -30,12 +30,19 @@
"./dist/react/*": "./dist/react/*",
"./dist/translations": "./dist/translations",
"./dist/translations/*": "./dist/translations/*",
"./dist/utilities": "./dist/utilities",
"./dist/utilities/*": "./dist/utilities/*",
"./dist/events": "./dist/events",
"./dist/events/*": "./dist/events/*",
"./package.json": "./package.json"
},
"files": [
"README.md",
"dist",
"dist-cdn"
"dist-cdn",
"dist/custom-elements.json",
"./dist/custom-elements.json",
"custom-elements.json"
],
"keywords": [
"web components",
@@ -64,10 +71,10 @@
"spellcheck": "cspell \"**/*.{js,ts,json,html,css,md}\" --no-progress --config=\"../../cspell.json\"",
"verify": "npm run prettier && npm run build && npm run test",
"prepublishOnly": "npm run verify",
"check-updates": "npx npm-check-updates --interactive --format group",
"check-updates": "npm-check-updates --cooldown 7 --interactive --format group",
"print-version": "echo $npm_package_version",
"tag-version": "git tag -a \"v$(npm run print-version | tail -n1)\" -m \"tag v$(npm run print-version | tail -n1)\"",
"postversion": "npm run tag-version"
"postversion": "node ./scripts/update-root-version.js"
},
"engines": {
"node": ">=14.17.0"
@@ -89,8 +96,11 @@
]
},
"devDependencies": {
"@wc-toolkit/cem-validator": "^1.0.3",
"@wc-toolkit/jsx-types": "^1.3.0",
"eleventy-plugin-git-commit-date": "^0.1.3",
"esbuild": "^0.25.11"
"esbuild": "^0.25.11",
"gray-matter": "^4.0.3",
"npm-check-updates": "^19.1.2"
}
}

View File

@@ -215,6 +215,11 @@ export async function build(options = {}) {
...(await globby(posix.join(rootDir, 'src/components/**/!(*.(style|test)).ts'))),
// Translations
...(await globby(posix.join(rootDir, 'src/translations/**/*.ts'))),
// Utilities
...(await globby(posix.join(rootDir, 'src/utilities/**/*.ts'))),
// Events
...(await globby(posix.join(rootDir, 'src/events/**/*.ts'))),
// TODO: Should `src/internal` be included?
// React wrappers
...(await globby(posix.join(rootDir, 'src/react/**/*.ts'))),
],
@@ -230,9 +235,6 @@ export async function build(options = {}) {
js: `/*! Copyright ${currentYear} Fonticons, Inc. - https://webawesome.com/license */`,
},
plugins: [replace({ __WEBAWESOME_VERSION__: await getVersion() })],
loader: {
'.css': 'text',
},
};
const unbundledConfig = {
@@ -438,6 +440,7 @@ export async function build(options = {}) {
}
// copy everything to unbundled before we generate bundles.
// this may cause watcher events to break. if things are broken with file watching, comment this out.
await copy(getCdnDir(), getDistDir(), { overwrite: true });
await regenerateBundle();

View File

@@ -0,0 +1,268 @@
import fs from 'fs';
import matter from 'gray-matter';
import path from 'path';
import { fileURLToPath } from 'url';
import { getAllComponents } from './shared.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/** Removes newlines from text to keep llms.txt formatting clean. */
function removeNewlines(str) {
return str ? str.replace(/\n/g, ' ').trim() : '';
}
/** Loads front-matter from all component markdown files. */
function loadAllFrontMatter(components, docsDir) {
const cache = new Map();
for (const component of components) {
if (!component.tagName) {
continue;
}
const componentName = component.tagName.replace(/^wa-/, '');
const mdPath = path.join(docsDir, 'docs/components', `${componentName}.md`);
if (fs.existsSync(mdPath)) {
try {
const content = fs.readFileSync(mdPath, 'utf-8');
const { data } = matter(content);
cache.set(component.tagName, data);
} catch {
// Skip if parsing fails
}
}
}
return cache;
}
/** Generates the API reference section for a single component. */
function generateComponentApiSection(component, frontMatterCache, baseUrl) {
const lines = [];
if (!component.tagName) {
return lines;
}
const frontMatter = frontMatterCache.get(component.tagName);
const componentSlug = component.tagName.replace(/^wa-/, '');
const description = removeNewlines(frontMatter?.description || component.summary || '');
lines.push(`#### \`<${component.tagName}>\``);
lines.push('');
lines.push(`**Description:** ${description || 'No description available.'}`);
lines.push('');
lines.push(`**Documentation:** ${baseUrl}/docs/components/${componentSlug}`);
lines.push('');
// Slots
if (component.slots?.length > 0) {
lines.push('**Slots:**');
lines.push('');
for (const slot of component.slots) {
const slotName = slot.name || '(default)';
lines.push(`- \`${slotName}\`: ${removeNewlines(slot.description) || 'No description.'}`);
}
lines.push('');
}
// Properties
const properties =
component.members?.filter(m => m.kind === 'field' && m.privacy !== 'private' && m.description) || [];
if (properties.length > 0) {
lines.push('**Properties:**');
lines.push('');
for (const prop of properties) {
// Find corresponding attribute if any
const attr = component.attributes?.find(a => a.fieldName === prop.name);
const attrNote = attr && attr.name !== prop.name ? ` (attribute: \`${attr.name}\`)` : '';
const typeStr = prop.type?.text ? `Type: \`${removeNewlines(prop.type.text)}\`` : '';
const defaultStr = prop.default ? `Default: \`${prop.default}\`` : '';
const meta = [typeStr, defaultStr].filter(Boolean).join(', ');
lines.push(
`- \`${prop.name}\`${attrNote}: ${removeNewlines(prop.description) || 'No description.'}${meta ? ` (${meta})` : ''}`,
);
}
lines.push('');
}
// Methods
const methods = component.members?.filter(m => m.kind === 'method' && m.privacy !== 'private' && m.description) || [];
if (methods.length > 0) {
lines.push('**Methods:**');
lines.push('');
for (const method of methods) {
const params = method.parameters?.length
? `(${method.parameters.map(p => `${p.name}: ${removeNewlines(p.type?.text) || 'unknown'}`).join(', ')})`
: '()';
lines.push(`- \`${method.name}${params}\`: ${removeNewlines(method.description) || 'No description.'}`);
}
lines.push('');
}
// Events
const events = component.events?.filter(e => e.name) || [];
if (events.length > 0) {
lines.push('**Events:**');
lines.push('');
for (const event of events) {
lines.push(`- \`${event.name}\`: ${removeNewlines(event.description) || 'No description.'}`);
}
lines.push('');
}
// CSS Custom Properties
if (component.cssProperties?.length > 0) {
lines.push('**CSS Custom Properties:**');
lines.push('');
for (const prop of component.cssProperties) {
const defaultStr = prop.default ? ` (Default: \`${prop.default}\`)` : '';
lines.push(`- \`${prop.name}\`: ${removeNewlines(prop.description) || 'No description.'}${defaultStr}`);
}
lines.push('');
}
// CSS Parts
if (component.cssParts?.length > 0) {
lines.push('**CSS Parts:**');
lines.push('');
for (const part of component.cssParts) {
lines.push(`- \`${part.name}\`: ${removeNewlines(part.description) || 'No description.'}`);
}
lines.push('');
}
// CSS States
if (component.cssStates?.length > 0) {
lines.push('**CSS States:**');
lines.push('');
for (const state of component.cssStates) {
lines.push(`- \`${state.name}\`: ${removeNewlines(state.description) || 'No description.'}`);
}
lines.push('');
}
return lines;
}
/**
* Generates the complete llms.txt content.
*/
function generateLlmsTxt({ components, packageData, frontMatterCache, baseUrl }) {
// Account for base "abstract elements" that don't have a tagName.
components = components.filter(c => c.tagName);
const lines = [];
// H1 Title (required by llmstxt.org spec)
lines.push('# Web Awesome');
lines.push('');
// Blockquote summary
lines.push(`> ${packageData.description} Version ${packageData.version}.`);
lines.push('');
// Overview section
lines.push(
`
Web Awesome provides a comprehensive set of customizable, accessible web components for building modern
web applications. All components use shadow DOM and are framework-agnostic, working with vanilla JavaScript
or any framework including React, Vue, Angular, and Svelte.
Form controls are form-associated custom elements that work with native form validation and the
Constraint Validation API.
Font Awesome is the default icon library, so \`<wa-icon name="...">\` values should reference Font Awesome
icon names.
`.trim(),
);
lines.push('');
//
// Documentation
//
lines.push('## Documentation');
lines.push('');
lines.push(`For comprehensive documentation, visit ${baseUrl}/docs/`);
lines.push('');
lines.push(`- [Getting Started](${baseUrl}/docs/getting-started): Installation and setup guide`);
lines.push(`- [Components Overview](${baseUrl}/docs/components): Complete component reference`);
lines.push(`- [Theming](${baseUrl}/docs/theming): Customization and design tokens`);
lines.push(`- [Form Controls](${baseUrl}/docs/form-controls): Form integration and validation`);
lines.push('');
//
// Components
//
lines.push('## Components');
lines.push('');
const sortedComponentsList = components.filter(c => c.tagName).sort((a, b) => a.tagName.localeCompare(b.tagName));
for (const component of sortedComponentsList) {
const frontMatter = frontMatterCache.get(component.tagName);
const description = removeNewlines(frontMatter?.description || component.summary || '');
const componentSlug = component.tagName.replace(/^wa-/, '');
const title = frontMatter?.title || componentSlug;
lines.push(
`- [${title}](${baseUrl}/docs/components/${componentSlug}): ${description || 'No description available.'}`,
);
}
lines.push('');
//
// Optional
//
lines.push('## Optional');
lines.push('');
lines.push(
`The following is a quick reference describing every component's API. For comprehensive documentation, refer to the component documentation using the URLs provided above.`,
);
lines.push('');
// Sort components alphabetically by tag name for the API reference
const sortedComponents = components.filter(c => c.tagName).sort((a, b) => a.tagName.localeCompare(b.tagName));
for (const component of sortedComponents) {
lines.push(...generateComponentApiSection(component, frontMatterCache, baseUrl));
}
return lines.join('\n').trim();
}
/**
* A CEM plugin that generates an llms.txt file following the llmstxt.org specification.
*/
export function llmsTxtPlugin(options = {}) {
const {
outdir = 'dist-cdn',
docsDir = path.resolve(__dirname, '../docs'),
baseUrl = 'https://webawesome.com',
} = options;
return {
name: 'wa-llms-txt',
packageLinkPhase({ customElementsManifest }) {
const components = getAllComponents(customElementsManifest);
const packageData = customElementsManifest.package || {};
const frontMatterCache = loadAllFrontMatter(components, docsDir);
const llmsTxt = generateLlmsTxt({
components,
packageData,
frontMatterCache,
baseUrl,
});
// Write to the output directory
const outputPath = path.join(outdir, 'llms.txt');
fs.writeFileSync(outputPath, llmsTxt, 'utf-8');
},
};
}
export default llmsTxtPlugin;

View File

@@ -22,6 +22,9 @@ const components = getAllComponents(metadata);
const index = [];
for await (const component of components) {
if (!component.tagName) {
continue;
}
const tagWithoutPrefix = component.tagName.replace(/^wa-/, '');
const componentDir = path.join(reactDir, tagWithoutPrefix);
const componentFile = path.join(componentDir, 'index.ts');

View File

@@ -37,7 +37,7 @@ export default function (plop) {
},
{
type: 'add',
path: '../../src/components/{{ tagWithoutPrefix tag }}/{{ tagWithoutPrefix tag }}.css',
path: '../../src/components/{{ tagWithoutPrefix tag }}/{{ tagWithoutPrefix tag }}.styles.ts',
templateFile: 'templates/component/styles.hbs',
},
{

View File

@@ -2,7 +2,7 @@ import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import styles from './{{ tagWithoutPrefix tag }}.css';
import styles from './{{ tagWithoutPrefix tag }}.styles.js';
/**
* @summary Short summary of the component's intended use.
@@ -21,7 +21,7 @@ import styles from './{{ tagWithoutPrefix tag }}.css';
*/
@customElement("{{ tag }}")
export default class {{ properCase tag }} extends WebAwesomeElement {
static css = styles;
static css = [styles];
/** An example attribute. */
@property() attr = 'example';

View File

@@ -1,3 +1,7 @@
:host {
display: block;
import { css } from "lit";
export default css`
:host {
display: block;
}
`

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env node
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as url from 'url';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const monorepoRoot = path.resolve(__dirname, '..', '..', '..');
const rootPackageJSONFile = path.join(monorepoRoot, 'package.json');
const webawesomePackageJSONFile = path.join(path.resolve(__dirname, '..'), 'package.json');
const rootPackageJSON = JSON.parse(fs.readFileSync(rootPackageJSONFile, { encoding: 'utf8' }));
const webawesomePackageJSON = JSON.parse(fs.readFileSync(webawesomePackageJSONFile, { encoding: 'utf8' }));
const currentVersion = webawesomePackageJSON.version;
rootPackageJSON.version = currentVersion;
fs.writeFileSync(rootPackageJSONFile, JSON.stringify(rootPackageJSON, null, 2));
const versionsFile = path.join(monorepoRoot, 'VERSIONS.txt');
const versions = fs.readFileSync(versionsFile, { encoding: 'utf8' }).split(/\r?\n/);
// TODO: Make this smart and understand semver and "insert" in the correct spot instead of appending.
if (!versions.includes(currentVersion)) {
fs.appendFileSync(versionsFile, webawesomePackageJSON.version);
}

View File

@@ -1,65 +0,0 @@
:host {
--control-box-size: 3rem;
--icon-size: calc(var(--control-box-size) * 0.625);
display: inline-flex;
position: relative;
cursor: pointer;
}
img {
display: block;
width: 100%;
height: 100%;
}
img[aria-hidden='true'] {
display: none;
}
.control-box {
display: flex;
position: absolute;
align-items: center;
justify-content: center;
top: calc(50% - var(--control-box-size) / 2);
right: calc(50% - var(--control-box-size) / 2);
width: var(--control-box-size);
height: var(--control-box-size);
font-size: calc(var(--icon-size) * 0.75);
background: none;
border: solid var(--wa-border-width-s) currentColor;
background-color: rgb(0 0 0 / 50%);
border-radius: var(--wa-border-radius-circle);
color: white;
pointer-events: none;
transition: opacity var(--wa-transition-normal) var(--wa-transition-easing);
}
@media (hover: hover) {
:host([play]:hover) .control-box {
opacity: 1;
}
}
:where(:host([play]:not(:hover))) .control-box {
opacity: 0;
}
:host([play]) slot[name='play-icon'],
:host(:not([play])) slot[name='pause-icon'] {
display: none;
}
/* Show control box on keyboard focus */
.animated-image {
&:focus {
outline: none;
}
&:focus-visible .control-box {
opacity: 1;
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
}

View File

@@ -0,0 +1,69 @@
import { css } from 'lit';
export default css`
:host {
--control-box-size: 3rem;
--icon-size: calc(var(--control-box-size) * 0.625);
display: inline-flex;
position: relative;
cursor: pointer;
}
img {
display: block;
width: 100%;
height: 100%;
}
img[aria-hidden='true'] {
display: none;
}
.control-box {
display: flex;
position: absolute;
align-items: center;
justify-content: center;
top: calc(50% - var(--control-box-size) / 2);
right: calc(50% - var(--control-box-size) / 2);
width: var(--control-box-size);
height: var(--control-box-size);
font-size: calc(var(--icon-size) * 0.75);
background: none;
border: solid var(--wa-border-width-s) currentColor;
background-color: rgb(0 0 0 / 50%);
border-radius: var(--wa-border-radius-circle);
color: white;
pointer-events: none;
transition: opacity var(--wa-transition-normal) var(--wa-transition-easing);
}
@media (hover: hover) {
:host([play]:hover) .control-box {
opacity: 1;
}
}
:where(:host([play]:not(:hover))) .control-box {
opacity: 0;
}
:host([play]) slot[name='play-icon'],
:host(:not([play])) slot[name='pause-icon'] {
display: none;
}
/* Show control box on keyboard focus */
.animated-image {
&:focus {
outline: none;
}
&:focus-visible .control-box {
opacity: 1;
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
}
`;

View File

@@ -6,7 +6,7 @@ import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import { LocalizeController } from '../../utilities/localize.js';
import '../icon/icon.js';
import styles from './animated-image.css';
import styles from './animated-image.styles.js';
/**
* @summary A component for displaying animated GIFs and WEBPs that play and pause on interaction.

View File

@@ -1,3 +0,0 @@
:host {
display: contents;
}

View File

@@ -0,0 +1,7 @@
import { css } from 'lit';
export default css`
:host {
display: contents;
}
`;

View File

@@ -5,7 +5,7 @@ import { WaFinishEvent } from '../../events/finish.js';
import { WaStartEvent } from '../../events/start.js';
import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import styles from './animation.css';
import styles from './animation.styles.js';
import { animations } from './animations.js';
/**

View File

@@ -1,53 +0,0 @@
:host {
--size: 3rem;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
width: var(--size);
height: var(--size);
color: var(--wa-color-neutral-on-normal);
font: inherit;
font-size: calc(var(--size) * 0.4);
vertical-align: middle;
background-color: var(--wa-color-neutral-fill-normal);
border-radius: var(--wa-border-radius-circle);
user-select: none;
-webkit-user-select: none;
}
:host([shape='square']) {
border-radius: 0;
}
:host([shape='rounded']) {
border-radius: var(--wa-border-radius-m);
}
.icon {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.initials {
line-height: 1;
text-transform: uppercase;
}
.image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
overflow: hidden;
border-radius: inherit;
}

View File

@@ -0,0 +1,57 @@
import { css } from 'lit';
export default css`
:host {
--size: 3rem;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
width: var(--size);
height: var(--size);
color: var(--wa-color-neutral-on-normal);
font: inherit;
font-size: calc(var(--size) * 0.4);
vertical-align: middle;
background-color: var(--wa-color-neutral-fill-normal);
border-radius: var(--wa-border-radius-circle);
user-select: none;
-webkit-user-select: none;
}
:host([shape='square']) {
border-radius: 0;
}
:host([shape='rounded']) {
border-radius: var(--wa-border-radius-m);
}
.icon {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.initials {
line-height: 1;
text-transform: uppercase;
}
.image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
overflow: hidden;
border-radius: inherit;
}
`;

View File

@@ -4,7 +4,7 @@ import { WaErrorEvent } from '../../events/error.js';
import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import '../icon/icon.js';
import styles from './avatar.css';
import styles from './avatar.styles.js';
/**
* @summary Avatars are used to represent a person or object.

View File

@@ -1,104 +0,0 @@
:host {
--pulse-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.375em 0.625em;
color: var(--wa-color-on-loud, var(--wa-color-brand-on-loud));
font-size: max(var(--wa-font-size-2xs), 0.75em);
font-weight: var(--wa-font-weight-semibold);
line-height: 1;
white-space: nowrap;
background-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));
border-color: transparent;
border-radius: var(--wa-border-radius-s);
border-style: var(--wa-border-style);
border-width: var(--wa-border-width-s);
user-select: none;
-webkit-user-select: none;
cursor: inherit;
}
/* Appearance modifiers */
:host([appearance='outlined']) {
--pulse-color: var(--wa-color-border-loud, var(--wa-color-brand-border-loud));
color: var(--wa-color-on-quiet, var(--wa-color-brand-on-quiet));
background-color: transparent;
border-color: var(--wa-color-border-loud, var(--wa-color-brand-border-loud));
}
:host([appearance='filled']) {
--pulse-color: var(--wa-color-fill-normal, var(--wa-color-brand-fill-normal));
color: var(--wa-color-on-normal, var(--wa-color-brand-on-normal));
background-color: var(--wa-color-fill-normal, var(--wa-color-brand-fill-normal));
border-color: transparent;
}
:host([appearance='filled-outlined']) {
--pulse-color: var(--wa-color-border-normal, var(--wa-color-brand-border-normal));
color: var(--wa-color-on-normal, var(--wa-color-brand-on-normal));
background-color: var(--wa-color-fill-normal, var(--wa-color-brand-fill-normal));
border-color: var(--wa-color-border-normal, var(--wa-color-brand-border-normal));
}
:host([appearance='accent']) {
--pulse-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));
color: var(--wa-color-on-loud, var(--wa-color-brand-on-loud));
background-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));
border-color: transparent;
}
/* Pill modifier */
:host([pill]) {
border-radius: var(--wa-border-radius-pill);
}
/* Pulse attention */
:host([attention='pulse']) {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 var(--pulse-color);
}
70% {
box-shadow: 0 0 0 0.5rem transparent;
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
/* Bounce attention */
:host([attention='bounce']) {
animation: bounce 1s cubic-bezier(0.28, 0.84, 0.42, 1) infinite;
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-5px);
}
60% {
transform: translateY(-2px);
}
}
::slotted(wa-icon) {
margin-inline-end: var(--wa-space-2xs, 0.25em);
opacity: 90%;
line-height: 1;
height: 0.85em;
}

View File

@@ -0,0 +1,108 @@
import { css } from 'lit';
export default css`
:host {
--pulse-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.375em 0.625em;
color: var(--wa-color-on-loud, var(--wa-color-brand-on-loud));
font-size: max(var(--wa-font-size-2xs), 0.75em);
font-weight: var(--wa-font-weight-semibold);
line-height: 1;
white-space: nowrap;
background-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));
border-color: transparent;
border-radius: var(--wa-border-radius-s);
border-style: var(--wa-border-style);
border-width: var(--wa-border-width-s);
user-select: none;
-webkit-user-select: none;
cursor: inherit;
}
/* Appearance modifiers */
:host([appearance='outlined']) {
--pulse-color: var(--wa-color-border-loud, var(--wa-color-brand-border-loud));
color: var(--wa-color-on-quiet, var(--wa-color-brand-on-quiet));
background-color: transparent;
border-color: var(--wa-color-border-loud, var(--wa-color-brand-border-loud));
}
:host([appearance='filled']) {
--pulse-color: var(--wa-color-fill-normal, var(--wa-color-brand-fill-normal));
color: var(--wa-color-on-normal, var(--wa-color-brand-on-normal));
background-color: var(--wa-color-fill-normal, var(--wa-color-brand-fill-normal));
border-color: transparent;
}
:host([appearance='filled-outlined']) {
--pulse-color: var(--wa-color-border-normal, var(--wa-color-brand-border-normal));
color: var(--wa-color-on-normal, var(--wa-color-brand-on-normal));
background-color: var(--wa-color-fill-normal, var(--wa-color-brand-fill-normal));
border-color: var(--wa-color-border-normal, var(--wa-color-brand-border-normal));
}
:host([appearance='accent']) {
--pulse-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));
color: var(--wa-color-on-loud, var(--wa-color-brand-on-loud));
background-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));
border-color: transparent;
}
/* Pill modifier */
:host([pill]) {
border-radius: var(--wa-border-radius-pill);
}
/* Pulse attention */
:host([attention='pulse']) {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 var(--pulse-color);
}
70% {
box-shadow: 0 0 0 0.5rem transparent;
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
/* Bounce attention */
:host([attention='bounce']) {
animation: bounce 1s cubic-bezier(0.28, 0.84, 0.42, 1) infinite;
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-5px);
}
60% {
transform: translateY(-2px);
}
}
::slotted(wa-icon) {
margin-inline-end: var(--wa-space-2xs, 0.25em);
opacity: 90%;
line-height: 1;
height: 0.85em;
}
`;

View File

@@ -1,8 +1,8 @@
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import variantStyles from '../../styles/utilities/variants.css';
import styles from './badge.css';
import variantStyles from '../../styles/component/variants.styles.js';
import styles from './badge.styles.js';
/**
* @summary Badges are used to draw attention and display statuses or counts.

View File

@@ -1,81 +0,0 @@
:host {
color: var(--wa-color-text-link);
display: inline-flex;
align-items: center;
font: inherit;
font-weight: var(--wa-font-weight-action);
line-height: var(--wa-line-height-normal);
white-space: nowrap;
}
:host(:last-of-type) {
color: var(--wa-color-text-quiet);
}
.label {
display: inline-block;
font: inherit;
text-decoration: none;
color: currentColor;
background: none;
border: none;
border-radius: var(--wa-border-radius-m);
padding: 0;
margin: 0;
cursor: pointer;
transition: color var(--wa-transition-normal) var(--wa-transition-easing);
}
@media (hover: hover) {
:host(:not(:last-of-type)) .label:hover {
color: color-mix(in oklab, currentColor, var(--wa-color-mix-hover));
}
}
:host(:not(:last-of-type)) .label:active {
color: color-mix(in oklab, currentColor, var(--wa-color-mix-active));
}
.label:focus {
outline: none;
}
.label:focus-visible {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
.start,
.end {
display: none;
flex: 0 0 auto;
display: flex;
align-items: center;
}
.start,
.end {
display: inline-flex;
color: var(--wa-color-text-quiet);
}
::slotted([slot='start']) {
margin-inline-end: var(--wa-space-s);
}
::slotted([slot='end']) {
margin-inline-start: var(--wa-space-s);
}
:host(:last-of-type) .separator {
display: none;
}
.separator {
color: var(--wa-color-text-quiet);
display: inline-flex;
align-items: center;
margin: 0 var(--wa-space-s);
user-select: none;
-webkit-user-select: none;
}

View File

@@ -0,0 +1,85 @@
import { css } from 'lit';
export default css`
:host {
color: var(--wa-color-text-link);
display: inline-flex;
align-items: center;
font: inherit;
font-weight: var(--wa-font-weight-action);
line-height: var(--wa-line-height-normal);
white-space: nowrap;
}
:host(:last-of-type) {
color: var(--wa-color-text-quiet);
}
.label {
display: inline-block;
font: inherit;
text-decoration: none;
color: currentColor;
background: none;
border: none;
border-radius: var(--wa-border-radius-m);
padding: 0;
margin: 0;
cursor: pointer;
transition: color var(--wa-transition-normal) var(--wa-transition-easing);
}
@media (hover: hover) {
:host(:not(:last-of-type)) .label:hover {
color: color-mix(in oklab, currentColor, var(--wa-color-mix-hover));
}
}
:host(:not(:last-of-type)) .label:active {
color: color-mix(in oklab, currentColor, var(--wa-color-mix-active));
}
.label:focus {
outline: none;
}
.label:focus-visible {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
.start,
.end {
display: none;
flex: 0 0 auto;
display: flex;
align-items: center;
}
.start,
.end {
display: inline-flex;
color: var(--wa-color-text-quiet);
}
::slotted([slot='start']) {
margin-inline-end: var(--wa-space-s);
}
::slotted([slot='end']) {
margin-inline-start: var(--wa-space-s);
}
:host(:last-of-type) .separator {
display: none;
}
.separator {
color: var(--wa-color-text-quiet);
display: inline-flex;
align-items: center;
margin: 0 var(--wa-space-s);
user-select: none;
-webkit-user-select: none;
}
`;

View File

@@ -3,7 +3,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import styles from './breadcrumb-item.css';
import styles from './breadcrumb-item.styles.js';
/**
* @summary Breadcrumb Items are used inside breadcrumbs to represent different links.

View File

@@ -1,5 +0,0 @@
.breadcrumb {
display: flex;
align-items: center;
flex-wrap: wrap;
}

View File

@@ -0,0 +1,9 @@
import { css } from 'lit';
export default css`
.breadcrumb {
display: flex;
align-items: center;
flex-wrap: wrap;
}
`;

View File

@@ -4,7 +4,7 @@ import WebAwesomeElement from '../../internal/webawesome-element.js';
import { LocalizeController } from '../../utilities/localize.js';
import type WaBreadcrumbItem from '../breadcrumb-item/breadcrumb-item.js';
import '../icon/icon.js';
import styles from './breadcrumb.css';
import styles from './breadcrumb.styles.js';
/**
* @summary Breadcrumbs provide a group of links so users can easily navigate a website's hierarchy.

View File

@@ -1,44 +0,0 @@
:host {
display: inline-flex;
}
.button-group {
display: flex;
position: relative;
isolation: isolate;
flex-wrap: wrap;
gap: 1px;
@media (hover: hover) {
> :hover,
&::slotted(:hover) {
z-index: 1;
}
}
/* Focus and checked are always on top */
> :focus,
&::slotted(:focus),
> [aria-checked='true'],
&::slotted([aria-checked='true']),
> [checked],
&::slotted([checked]) {
z-index: 2 !important;
}
}
:host([orientation='vertical']) .button-group {
flex-direction: column;
}
/* Button groups with at least one outlined button will not have a gap and instead have borders overlap */
.button-group.has-outlined {
gap: 0;
&:not([aria-orientation='vertical']):not(.button-group-vertical)::slotted(:not(:first-child)) {
margin-inline-start: calc(-1 * var(--border-width));
}
&:is([aria-orientation='vertical'], .button-group-vertical)::slotted(:not(:first-child)) {
margin-block-start: calc(-1 * var(--border-width));
}
}

View File

@@ -0,0 +1,48 @@
import { css } from 'lit';
export default css`
:host {
display: inline-flex;
}
.button-group {
display: flex;
position: relative;
isolation: isolate;
flex-wrap: wrap;
gap: 1px;
@media (hover: hover) {
> :hover,
&::slotted(:hover) {
z-index: 1;
}
}
/* Focus and checked are always on top */
> :focus,
&::slotted(:focus),
> [aria-checked='true'],
&::slotted([aria-checked='true']),
> [checked],
&::slotted([checked]) {
z-index: 2 !important;
}
}
:host([orientation='vertical']) .button-group {
flex-direction: column;
}
/* Button groups with at least one outlined button will not have a gap and instead have borders overlap */
.button-group.has-outlined {
gap: 0;
&:not([aria-orientation='vertical']):not(.button-group-vertical)::slotted(:not(:first-child)) {
margin-inline-start: calc(-1 * var(--border-width));
}
&:is([aria-orientation='vertical'], .button-group-vertical)::slotted(:not(:first-child)) {
margin-block-start: calc(-1 * var(--border-width));
}
}
`;

View File

@@ -3,9 +3,8 @@ import { html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import variantStyles from '../../styles/utilities/variants.css';
import type WaButton from '../button/button.js';
import styles from './button-group.css';
import styles from './button-group.styles.js';
/**
* @summary Button groups can be used to group related buttons into sections.
@@ -19,7 +18,7 @@ import styles from './button-group.css';
*/
@customElement('wa-button-group')
export default class WaButtonGroup extends WebAwesomeElement {
static css = [variantStyles, styles];
static css = [styles];
@query('slot') defaultSlot: HTMLSlotElement;
@@ -35,9 +34,6 @@ export default class WaButtonGroup extends WebAwesomeElement {
/** The button group's orientation. */
@property({ reflect: true }) orientation: 'horizontal' | 'vertical' = 'horizontal';
/** The button group's theme variant. Defaults to `neutral` if not within another element with a variant. */
@property({ reflect: true }) variant: 'neutral' | 'brand' | 'success' | 'warning' | 'danger' = 'neutral';
updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);

View File

@@ -1,372 +0,0 @@
@layer wa-component {
:host {
display: inline-block;
/* Workaround because Chrome doesn't like :host(:has()) below
* https://issues.chromium.org/issues/40062355
* Firefox doesn't like this nested rule, so both are needed */
&:has(wa-badge) {
position: relative;
}
}
/* Apply relative positioning only when needed to position wa-badge
* This avoids creating a new stacking context for every button */
:host(:has(wa-badge)) {
position: relative;
}
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
user-select: none;
-webkit-user-select: none;
white-space: nowrap;
vertical-align: middle;
transition-property: background, border, box-shadow, color;
transition-duration: var(--wa-transition-fast);
transition-timing-function: var(--wa-transition-easing);
cursor: pointer;
padding: 0 var(--wa-form-control-padding-inline);
font-family: inherit;
font-size: inherit;
font-weight: var(--wa-font-weight-action);
line-height: calc(var(--wa-form-control-height) - var(--border-width) * 2);
height: var(--wa-form-control-height);
width: 100%;
background-color: var(--wa-color-fill-loud, var(--wa-color-neutral-fill-loud));
border-color: transparent;
color: var(--wa-color-on-loud, var(--wa-color-neutral-on-loud));
border-radius: var(--wa-form-control-border-radius);
border-style: var(--wa-border-style);
border-width: var(--wa-border-width-s);
}
/* Appearance modifiers */
:host([appearance='plain']) {
.button {
color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));
background-color: transparent;
border-color: transparent;
}
@media (hover: hover) {
.button:not(.disabled):not(.loading):hover {
color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));
background-color: var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet));
}
}
.button:not(.disabled):not(.loading):active {
color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));
background-color: color-mix(
in oklab,
var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet)),
var(--wa-color-mix-active)
);
}
}
:host([appearance='outlined']) {
.button {
color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));
background-color: transparent;
border-color: var(--wa-color-border-loud, var(--wa-color-neutral-border-loud));
}
@media (hover: hover) {
.button:not(.disabled):not(.loading):hover {
color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));
background-color: var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet));
}
}
.button:not(.disabled):not(.loading):active {
color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));
background-color: color-mix(
in oklab,
var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet)),
var(--wa-color-mix-active)
);
}
}
:host([appearance='filled']) {
.button {
color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
background-color: var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal));
border-color: transparent;
}
@media (hover: hover) {
.button:not(.disabled):not(.loading):hover {
color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
background-color: color-mix(
in oklab,
var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal)),
var(--wa-color-mix-hover)
);
}
}
.button:not(.disabled):not(.loading):active {
color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
background-color: color-mix(
in oklab,
var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal)),
var(--wa-color-mix-active)
);
}
}
:host([appearance='filled-outlined']) {
.button {
color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
background-color: var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal));
border-color: var(--wa-color-border-normal, var(--wa-color-neutral-border-normal));
}
@media (hover: hover) {
.button:not(.disabled):not(.loading):hover {
color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
background-color: color-mix(
in oklab,
var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal)),
var(--wa-color-mix-hover)
);
}
}
.button:not(.disabled):not(.loading):active {
color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
background-color: color-mix(
in oklab,
var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal)),
var(--wa-color-mix-active)
);
}
}
:host([appearance='accent']) {
.button {
color: var(--wa-color-on-loud, var(--wa-color-neutral-on-loud));
background-color: var(--wa-color-fill-loud, var(--wa-color-neutral-fill-loud));
border-color: transparent;
}
@media (hover: hover) {
.button:not(.disabled):not(.loading):hover {
background-color: color-mix(
in oklab,
var(--wa-color-fill-loud, var(--wa-color-neutral-fill-loud)),
var(--wa-color-mix-hover)
);
}
}
.button:not(.disabled):not(.loading):active {
background-color: color-mix(
in oklab,
var(--wa-color-fill-loud, var(--wa-color-neutral-fill-loud)),
var(--wa-color-mix-active)
);
}
}
/* Focus states */
.button:focus {
outline: none;
}
.button:focus-visible {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
/* Disabled state */
.button.disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* When disabled, prevent mouse events from bubbling up from children */
.button.disabled * {
pointer-events: none;
}
/* Keep it last so Safari doesn't stop parsing this block */
.button::-moz-focus-inner {
border: 0;
}
/* Icon buttons */
.button.is-icon-button {
outline-offset: 2px;
width: var(--wa-form-control-height);
aspect-ratio: 1;
}
.button.is-icon-button:has(wa-icon) {
width: auto;
}
/* Pill modifier */
:host([pill]) .button {
border-radius: var(--wa-border-radius-pill);
}
/*
* Label
*/
.start,
.end {
flex: 0 0 auto;
display: flex;
align-items: center;
pointer-events: none;
}
.label {
display: inline-block;
}
.is-icon-button .label {
display: flex;
}
.label::slotted(wa-icon) {
align-self: center;
}
/*
* Caret modifier
*/
wa-icon[part='caret'] {
display: flex;
align-self: center;
align-items: center;
&::part(svg) {
width: 0.875em;
height: 0.875em;
}
.button:has(&) .end {
display: none;
}
}
/*
* Loading modifier
*/
.loading {
position: relative;
cursor: wait;
.start,
.label,
.end,
.caret {
visibility: hidden;
}
wa-spinner {
--indicator-color: currentColor;
--track-color: color-mix(in oklab, currentColor, transparent 90%);
position: absolute;
font-size: 1em;
height: 1em;
width: 1em;
top: calc(50% - 0.5em);
left: calc(50% - 0.5em);
}
}
/*
* Badges
*/
.button ::slotted(wa-badge) {
border-color: var(--wa-color-surface-default);
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
translate: 50% -50%;
pointer-events: none;
}
:host(:dir(rtl)) ::slotted(wa-badge) {
translate: -50% -50%;
}
/*
* Button spacing
*/
slot[name='start']::slotted(*) {
margin-inline-end: 0.75em;
}
slot[name='end']::slotted(*),
.button:not(.visually-hidden-label) [part='caret'] {
margin-inline-start: 0.75em;
}
/*
* Button group border radius modifications
*/
/* Remove border radius from all grouped buttons by default */
:host(.wa-button-group__button) .button {
border-radius: 0;
}
/* Horizontal orientation */
:host(.wa-button-group__horizontal.wa-button-group__button-first) .button {
border-start-start-radius: var(--wa-form-control-border-radius);
border-end-start-radius: var(--wa-form-control-border-radius);
}
:host(.wa-button-group__horizontal.wa-button-group__button-last) .button {
border-start-end-radius: var(--wa-form-control-border-radius);
border-end-end-radius: var(--wa-form-control-border-radius);
}
/* Vertical orientation */
:host(.wa-button-group__vertical) {
flex: 1 1 auto;
}
:host(.wa-button-group__vertical) .button {
width: 100%;
justify-content: start;
}
:host(.wa-button-group__vertical.wa-button-group__button-first) .button {
border-start-start-radius: var(--wa-form-control-border-radius);
border-start-end-radius: var(--wa-form-control-border-radius);
}
:host(.wa-button-group__vertical.wa-button-group__button-last) .button {
border-end-start-radius: var(--wa-form-control-border-radius);
border-end-end-radius: var(--wa-form-control-border-radius);
}
/* Handle pill modifier for button groups */
:host([pill].wa-button-group__horizontal.wa-button-group__button-first) .button {
border-start-start-radius: var(--wa-border-radius-pill);
border-end-start-radius: var(--wa-border-radius-pill);
}
:host([pill].wa-button-group__horizontal.wa-button-group__button-last) .button {
border-start-end-radius: var(--wa-border-radius-pill);
border-end-end-radius: var(--wa-border-radius-pill);
}
:host([pill].wa-button-group__vertical.wa-button-group__button-first) .button {
border-start-start-radius: var(--wa-border-radius-pill);
border-start-end-radius: var(--wa-border-radius-pill);
}
:host([pill].wa-button-group__vertical.wa-button-group__button-last) .button {
border-end-start-radius: var(--wa-border-radius-pill);
border-end-end-radius: var(--wa-border-radius-pill);
}

View File

@@ -0,0 +1,376 @@
import { css } from 'lit';
export default css`
@layer wa-component {
:host {
display: inline-block;
/* Workaround because Chrome doesn't like :host(:has()) below
* https://issues.chromium.org/issues/40062355
* Firefox doesn't like this nested rule, so both are needed */
&:has(wa-badge) {
position: relative;
}
}
/* Apply relative positioning only when needed to position wa-badge
* This avoids creating a new stacking context for every button */
:host(:has(wa-badge)) {
position: relative;
}
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
user-select: none;
-webkit-user-select: none;
white-space: nowrap;
vertical-align: middle;
transition-property: background, border, box-shadow, color, opacity;
transition-duration: var(--wa-transition-fast);
transition-timing-function: var(--wa-transition-easing);
cursor: pointer;
padding: 0 var(--wa-form-control-padding-inline);
font-family: inherit;
font-size: inherit;
font-weight: var(--wa-font-weight-action);
line-height: calc(var(--wa-form-control-height) - var(--border-width) * 2);
height: var(--wa-form-control-height);
width: 100%;
background-color: var(--wa-color-fill-loud, var(--wa-color-neutral-fill-loud));
border-color: transparent;
color: var(--wa-color-on-loud, var(--wa-color-neutral-on-loud));
border-radius: var(--wa-form-control-border-radius);
border-style: var(--wa-border-style);
border-width: var(--wa-border-width-s);
}
/* Appearance modifiers */
:host([appearance='plain']) {
.button {
color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));
background-color: transparent;
border-color: transparent;
}
@media (hover: hover) {
.button:not(.disabled):not(.loading):hover {
color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));
background-color: var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet));
}
}
.button:not(.disabled):not(.loading):active {
color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));
background-color: color-mix(
in oklab,
var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet)),
var(--wa-color-mix-active)
);
}
}
:host([appearance='outlined']) {
.button {
color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));
background-color: transparent;
border-color: var(--wa-color-border-loud, var(--wa-color-neutral-border-loud));
}
@media (hover: hover) {
.button:not(.disabled):not(.loading):hover {
color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));
background-color: var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet));
}
}
.button:not(.disabled):not(.loading):active {
color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));
background-color: color-mix(
in oklab,
var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet)),
var(--wa-color-mix-active)
);
}
}
:host([appearance='filled']) {
.button {
color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
background-color: var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal));
border-color: transparent;
}
@media (hover: hover) {
.button:not(.disabled):not(.loading):hover {
color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
background-color: color-mix(
in oklab,
var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal)),
var(--wa-color-mix-hover)
);
}
}
.button:not(.disabled):not(.loading):active {
color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
background-color: color-mix(
in oklab,
var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal)),
var(--wa-color-mix-active)
);
}
}
:host([appearance='filled-outlined']) {
.button {
color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
background-color: var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal));
border-color: var(--wa-color-border-normal, var(--wa-color-neutral-border-normal));
}
@media (hover: hover) {
.button:not(.disabled):not(.loading):hover {
color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
background-color: color-mix(
in oklab,
var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal)),
var(--wa-color-mix-hover)
);
}
}
.button:not(.disabled):not(.loading):active {
color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
background-color: color-mix(
in oklab,
var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal)),
var(--wa-color-mix-active)
);
}
}
:host([appearance='accent']) {
.button {
color: var(--wa-color-on-loud, var(--wa-color-neutral-on-loud));
background-color: var(--wa-color-fill-loud, var(--wa-color-neutral-fill-loud));
border-color: transparent;
}
@media (hover: hover) {
.button:not(.disabled):not(.loading):hover {
background-color: color-mix(
in oklab,
var(--wa-color-fill-loud, var(--wa-color-neutral-fill-loud)),
var(--wa-color-mix-hover)
);
}
}
.button:not(.disabled):not(.loading):active {
background-color: color-mix(
in oklab,
var(--wa-color-fill-loud, var(--wa-color-neutral-fill-loud)),
var(--wa-color-mix-active)
);
}
}
/* Focus states */
.button:focus {
outline: none;
}
.button:focus-visible {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
/* Disabled state */
.button.disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* When disabled, prevent mouse events from bubbling up from children */
.button.disabled * {
pointer-events: none;
}
/* Keep it last so Safari doesn't stop parsing this block */
.button::-moz-focus-inner {
border: 0;
}
/* Icon buttons */
.button.is-icon-button {
outline-offset: 2px;
width: var(--wa-form-control-height);
aspect-ratio: 1;
}
.button.is-icon-button:has(wa-icon) {
width: auto;
}
/* Pill modifier */
:host([pill]) .button {
border-radius: var(--wa-border-radius-pill);
}
/*
* Label
*/
.start,
.end {
flex: 0 0 auto;
display: flex;
align-items: center;
pointer-events: none;
}
.label {
display: inline-block;
}
.is-icon-button .label {
display: flex;
}
.label::slotted(wa-icon) {
align-self: center;
}
/*
* Caret modifier
*/
wa-icon[part='caret'] {
display: flex;
align-self: center;
align-items: center;
&::part(svg) {
width: 0.875em;
height: 0.875em;
}
.button:has(&) .end {
display: none;
}
}
/*
* Loading modifier
*/
.loading {
position: relative;
cursor: wait;
.start,
.label,
.end,
.caret {
visibility: hidden;
}
wa-spinner {
--indicator-color: currentColor;
--track-color: color-mix(in oklab, currentColor, transparent 90%);
position: absolute;
font-size: 1em;
height: 1em;
width: 1em;
top: calc(50% - 0.5em);
left: calc(50% - 0.5em);
}
}
/*
* Badges
*/
.button ::slotted(wa-badge) {
border-color: var(--wa-color-surface-default);
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
translate: 50% -50%;
pointer-events: none;
}
:host(:dir(rtl)) ::slotted(wa-badge) {
translate: -50% -50%;
}
/*
* Button spacing
*/
slot[name='start']::slotted(*) {
margin-inline-end: 0.75em;
}
slot[name='end']::slotted(*),
.button:not(.visually-hidden-label) [part='caret'] {
margin-inline-start: 0.75em;
}
/*
* Button group border radius modifications
*/
/* Remove border radius from all grouped buttons by default */
:host(.wa-button-group__button) .button {
border-radius: 0;
}
/* Horizontal orientation */
:host(.wa-button-group__horizontal.wa-button-group__button-first) .button {
border-start-start-radius: var(--wa-form-control-border-radius);
border-end-start-radius: var(--wa-form-control-border-radius);
}
:host(.wa-button-group__horizontal.wa-button-group__button-last) .button {
border-start-end-radius: var(--wa-form-control-border-radius);
border-end-end-radius: var(--wa-form-control-border-radius);
}
/* Vertical orientation */
:host(.wa-button-group__vertical) {
flex: 1 1 auto;
}
:host(.wa-button-group__vertical) .button {
width: 100%;
justify-content: start;
}
:host(.wa-button-group__vertical.wa-button-group__button-first) .button {
border-start-start-radius: var(--wa-form-control-border-radius);
border-start-end-radius: var(--wa-form-control-border-radius);
}
:host(.wa-button-group__vertical.wa-button-group__button-last) .button {
border-end-start-radius: var(--wa-form-control-border-radius);
border-end-end-radius: var(--wa-form-control-border-radius);
}
/* Handle pill modifier for button groups */
:host([pill].wa-button-group__horizontal.wa-button-group__button-first) .button {
border-start-start-radius: var(--wa-border-radius-pill);
border-end-start-radius: var(--wa-border-radius-pill);
}
:host([pill].wa-button-group__horizontal.wa-button-group__button-last) .button {
border-start-end-radius: var(--wa-border-radius-pill);
border-end-end-radius: var(--wa-border-radius-pill);
}
:host([pill].wa-button-group__vertical.wa-button-group__button-first) .button {
border-start-start-radius: var(--wa-border-radius-pill);
border-start-end-radius: var(--wa-border-radius-pill);
}
:host([pill].wa-button-group__vertical.wa-button-group__button-last) .button {
border-end-start-radius: var(--wa-border-radius-pill);
border-end-end-radius: var(--wa-border-radius-pill);
}
`;

View File

@@ -7,13 +7,13 @@ import { HasSlotController } from '../../internal/slot.js';
import { MirrorValidator } from '../../internal/validators/mirror-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import sizeStyles from '../../styles/utilities/size.css';
import variantStyles from '../../styles/utilities/variants.css';
import sizeStyles from '../../styles/component/size.styles.js';
import variantStyles from '../../styles/component/variants.styles.js';
import { LocalizeController } from '../../utilities/localize.js';
import '../icon/icon.js';
import type WaIcon from '../icon/icon.js';
import '../spinner/spinner.js';
import styles from './button.css';
import styles from './button.styles.js';
/**
* @summary Buttons represent actions that are available to the user.
@@ -115,7 +115,6 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
* The "form owner" to associate the button with. If omitted, the closest containing form will be used instead. The
* value of this attribute must be an id of a form in the same document or shadow root as the button.
*/
@property({ reflect: true }) form: string | null = null;
/** Used to override the form owner's `action` attribute. */
@property({ attribute: 'formaction' }) formAction: string;
@@ -135,24 +134,27 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
private constructLightDOMButton() {
const button = document.createElement('button');
for (const attribute of this.attributes) {
if (attribute.name === 'style') {
// Skip style attributes as they *shouldn't* be necessary
continue;
}
button.setAttribute(attribute.name, attribute.value);
}
button.type = this.type;
button.style.position = 'absolute';
button.style.width = '0';
button.style.height = '0';
button.style.clipPath = 'inset(50%)';
button.style.overflow = 'hidden';
button.style.whiteSpace = 'nowrap';
button.style.position = 'absolute !important';
button.style.width = '0 !important';
button.style.height = '0 !important';
button.style.clipPath = 'inset(50%) !important';
button.style.overflow = 'hidden !important';
button.style.whiteSpace = 'nowrap !important';
if (this.name) {
button.name = this.name;
}
button.value = this.value || '';
['form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget'].forEach(attr => {
if (this.hasAttribute(attr)) {
button.setAttribute(attr, this.getAttribute(attr)!);
}
});
return button;
}

View File

@@ -1,60 +0,0 @@
:host {
display: flex;
position: relative;
align-items: stretch;
border-radius: var(--wa-panel-border-radius);
background-color: var(--wa-color-fill-quiet, var(--wa-color-brand-fill-quiet));
border-color: var(--wa-color-border-quiet, var(--wa-color-brand-border-quiet));
border-style: var(--wa-panel-border-style);
border-width: var(--wa-panel-border-width);
color: var(--wa-color-text-normal);
padding: 1em;
}
/* Appearance modifiers */
:host([appearance~='plain']) {
background-color: transparent;
border-color: transparent;
}
:host([appearance~='outlined']) {
background-color: transparent;
border-color: var(--wa-color-border-loud, var(--wa-color-brand-border-loud));
}
:host([appearance~='filled']) {
background-color: var(--wa-color-fill-quiet, var(--wa-color-brand-fill-quiet));
border-color: transparent;
}
:host([appearance~='filled-outlined']) {
border-color: var(--wa-color-border-quiet, var(--wa-color-brand-border-quiet));
}
:host([appearance~='accent']) {
color: var(--wa-color-on-loud, var(--wa-color-brand-on-loud));
background-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));
border-color: transparent;
[part~='icon'] {
color: currentColor;
}
}
[part~='icon'] {
flex: 0 0 auto;
display: flex;
align-items: center;
color: var(--wa-color-on-quiet);
font-size: 1.25em;
}
::slotted([slot='icon']) {
margin-inline-end: var(--wa-form-control-padding-inline);
}
[part~='message'] {
flex: 1 1 auto;
display: block;
overflow: hidden;
}

View File

@@ -0,0 +1,64 @@
import { css } from 'lit';
export default css`
:host {
display: flex;
position: relative;
align-items: stretch;
border-radius: var(--wa-panel-border-radius);
background-color: var(--wa-color-fill-quiet, var(--wa-color-brand-fill-quiet));
border-color: var(--wa-color-border-quiet, var(--wa-color-brand-border-quiet));
border-style: var(--wa-panel-border-style);
border-width: var(--wa-panel-border-width);
color: var(--wa-color-text-normal);
padding: 1em;
}
/* Appearance modifiers */
:host([appearance~='plain']) {
background-color: transparent;
border-color: transparent;
}
:host([appearance~='outlined']) {
background-color: transparent;
border-color: var(--wa-color-border-loud, var(--wa-color-brand-border-loud));
}
:host([appearance~='filled']) {
background-color: var(--wa-color-fill-quiet, var(--wa-color-brand-fill-quiet));
border-color: transparent;
}
:host([appearance~='filled-outlined']) {
border-color: var(--wa-color-border-quiet, var(--wa-color-brand-border-quiet));
}
:host([appearance~='accent']) {
color: var(--wa-color-on-loud, var(--wa-color-brand-on-loud));
background-color: var(--wa-color-fill-loud, var(--wa-color-brand-fill-loud));
border-color: transparent;
[part~='icon'] {
color: currentColor;
}
}
[part~='icon'] {
flex: 0 0 auto;
display: flex;
align-items: center;
color: var(--wa-color-on-quiet);
font-size: 1.25em;
}
::slotted([slot='icon']) {
margin-inline-end: var(--wa-form-control-padding-inline);
}
[part~='message'] {
flex: 1 1 auto;
display: block;
overflow: hidden;
}
`;

View File

@@ -1,9 +1,9 @@
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import sizeStyles from '../../styles/utilities/size.css';
import variantStyles from '../../styles/utilities/variants.css';
import styles from './callout.css';
import sizeStyles from '../../styles/component/size.styles.js';
import variantStyles from '../../styles/component/variants.styles.js';
import styles from './callout.styles.js';
/**
* @summary Callouts are used to display important messages inline.

View File

@@ -1,140 +0,0 @@
:host {
--spacing: var(--wa-space-l);
/* Internal calculated properties */
--inner-border-radius: calc(var(--wa-panel-border-radius) - var(--wa-panel-border-width));
display: flex;
flex-direction: column;
background-color: var(--wa-color-surface-default);
border-color: var(--wa-color-surface-border);
border-radius: var(--wa-panel-border-radius);
border-style: var(--wa-panel-border-style);
box-shadow: var(--wa-shadow-s);
border-width: var(--wa-panel-border-width);
color: var(--wa-color-text-normal);
}
/* Appearance modifiers */
:host([appearance~='plain']) {
background-color: transparent;
border-color: transparent;
box-shadow: none;
}
:host([appearance~='outlined']) {
background-color: var(--wa-color-surface-default);
border-color: var(--wa-color-surface-border);
}
:host([appearance~='filled']) {
background-color: var(--wa-color-neutral-fill-quiet);
border-color: transparent;
}
:host([appearance~='filled'][appearance~='outlined']) {
border-color: var(--wa-color-neutral-border-quiet);
}
:host([appearance~='accent']) {
color: var(--wa-color-neutral-on-loud);
background-color: var(--wa-color-neutral-fill-loud);
border-color: transparent;
}
/* Take care of top and bottom radii */
.media,
:host(:not([with-media])) .header,
:host(:not([with-media], [with-header])) .body {
border-start-start-radius: var(--inner-border-radius);
border-start-end-radius: var(--inner-border-radius);
}
:host(:not([with-footer])) .body,
.footer {
border-end-start-radius: var(--inner-border-radius);
border-end-end-radius: var(--inner-border-radius);
}
.media {
display: flex;
overflow: hidden;
&::slotted(*) {
display: block;
width: 100%;
border-radius: 0 !important;
}
}
/* Round all corners for plain appearance */
:host([appearance='plain']) .media {
border-radius: var(--inner-border-radius);
&::slotted(*) {
border-radius: inherit !important;
}
}
.header {
display: block;
border-block-end-style: inherit;
border-block-end-color: var(--wa-color-surface-border);
border-block-end-width: var(--wa-panel-border-width);
padding: calc(var(--spacing) / 2) var(--spacing);
}
.body {
display: block;
padding: var(--spacing);
}
.footer {
display: block;
border-block-start-style: inherit;
border-block-start-color: var(--wa-color-surface-border);
border-block-start-width: var(--wa-panel-border-width);
padding: var(--spacing);
}
/* Push slots to sides when the action slots renders */
.has-actions {
display: flex;
align-items: center;
justify-content: space-between;
}
:host(:not([with-header])) .header,
:host(:not([with-footer])) .footer,
:host(:not([with-media])) .media {
display: none;
}
/* Orientation Styles */
:host([orientation='horizontal']) {
flex-direction: row;
.media {
border-start-start-radius: var(--inner-border-radius);
border-end-start-radius: var(--inner-border-radius);
border-start-end-radius: 0;
&::slotted(*) {
block-size: 100%;
inline-size: 100%;
object-fit: cover;
}
}
}
:host([orientation='horizontal']) ::slotted([slot='body']) {
display: block;
height: 100%;
margin: 0;
}
:host([orientation='horizontal']) ::slotted([slot='actions']) {
display: flex;
align-items: center;
padding: var(--spacing);
}

View File

@@ -0,0 +1,145 @@
import { css } from 'lit';
export default css`
:host {
--spacing: var(--wa-space-l);
/* Internal calculated properties */
--inner-border-radius: calc(var(--wa-panel-border-radius) - var(--wa-panel-border-width));
display: flex;
flex-direction: column;
background-color: var(--wa-color-surface-default);
border-color: var(--wa-color-surface-border);
border-radius: var(--wa-panel-border-radius);
border-style: var(--wa-panel-border-style);
box-shadow: var(--wa-shadow-s);
border-width: var(--wa-panel-border-width);
color: var(--wa-color-text-normal);
}
/* Appearance modifiers */
:host([appearance='plain']) {
background-color: transparent;
border-color: transparent;
box-shadow: none;
}
:host([appearance='outlined']) {
background-color: var(--wa-color-surface-default);
border-color: var(--wa-color-surface-border);
}
:host([appearance='filled']) {
background-color: var(--wa-color-neutral-fill-quiet);
border-color: transparent;
}
:host([appearance='filled-outlined']) {
background-color: var(--wa-color-neutral-fill-quiet);
border-color: var(--wa-color-surface-border);
}
:host([appearance='accent']) {
color: var(--wa-color-neutral-on-loud);
background-color: var(--wa-color-neutral-fill-loud);
border-color: transparent;
}
/* Take care of top and bottom radii */
.media,
:host(:not([with-media])) .header,
:host(:not([with-media], [with-header])) .body {
border-start-start-radius: var(--inner-border-radius);
border-start-end-radius: var(--inner-border-radius);
}
:host(:not([with-footer])) .body,
.footer {
border-end-start-radius: var(--inner-border-radius);
border-end-end-radius: var(--inner-border-radius);
}
.media {
display: flex;
overflow: hidden;
&::slotted(*) {
display: block;
width: 100%;
border-radius: 0 !important;
}
}
/* Round all corners for plain appearance */
:host([appearance='plain']) .media {
border-radius: var(--inner-border-radius);
&::slotted(*) {
border-radius: inherit !important;
}
}
.header {
display: block;
border-block-end-style: inherit;
border-block-end-color: var(--wa-color-surface-border);
border-block-end-width: var(--wa-panel-border-width);
padding: calc(var(--spacing) / 2) var(--spacing);
}
.body {
display: block;
padding: var(--spacing);
}
.footer {
display: block;
border-block-start-style: inherit;
border-block-start-color: var(--wa-color-surface-border);
border-block-start-width: var(--wa-panel-border-width);
padding: var(--spacing);
}
/* Push slots to sides when the action slots renders */
.has-actions {
display: flex;
align-items: center;
justify-content: space-between;
}
:host(:not([with-header])) .header,
:host(:not([with-footer])) .footer,
:host(:not([with-media])) .media {
display: none;
}
/* Orientation Styles */
:host([orientation='horizontal']) {
flex-direction: row;
.media {
border-start-start-radius: var(--inner-border-radius);
border-end-start-radius: var(--inner-border-radius);
border-start-end-radius: 0;
&::slotted(*) {
block-size: 100%;
inline-size: 100%;
object-fit: cover;
}
}
}
:host([orientation='horizontal']) ::slotted([slot='body']) {
display: block;
height: 100%;
margin: 0;
}
:host([orientation='horizontal']) ::slotted([slot='actions']) {
display: flex;
align-items: center;
padding: var(--spacing);
}
`;

View File

@@ -2,8 +2,8 @@ import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { HasSlotController } from '../../internal/slot.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import sizeStyles from '../../styles/utilities/size.css';
import styles from './card.css';
import sizeStyles from '../../styles/component/size.styles.js';
import styles from './card.styles.js';
/**
* @summary Cards can be used to group related subjects in a container.
@@ -30,11 +30,19 @@ import styles from './card.css';
export default class WaCard extends WebAwesomeElement {
static css = [sizeStyles, styles];
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'media');
private readonly hasSlotController = new HasSlotController(
this,
'footer',
'header',
'media',
'header-actions',
'footer-actions',
'actions',
);
/** The card's visual appearance. */
@property({ reflect: true })
appearance: 'accent' | 'filled' | 'outlined' | 'plain' = 'outlined';
appearance: 'accent' | 'filled' | 'outlined' | 'filled-outlined' | 'plain' = 'outlined';
/** Renders the card with a header. Only needed for SSR, otherwise is automatically added. */
@property({ attribute: 'with-header', type: Boolean, reflect: true }) withHeader = false;

View File

@@ -1,19 +0,0 @@
:host {
--aspect-ratio: inherit;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
width: 100%;
max-height: 100%;
aspect-ratio: var(--aspect-ratio);
scroll-snap-align: start;
scroll-snap-stop: always;
}
::slotted(img) {
width: 100% !important;
height: 100% !important;
object-fit: cover;
}

View File

@@ -0,0 +1,23 @@
import { css } from 'lit';
export default css`
:host {
--aspect-ratio: inherit;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
width: 100%;
max-height: 100%;
aspect-ratio: var(--aspect-ratio);
scroll-snap-align: start;
scroll-snap-stop: always;
}
::slotted(img) {
width: 100% !important;
height: 100% !important;
object-fit: cover;
}
`;

View File

@@ -1,7 +1,7 @@
import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import styles from './carousel-item.css';
import styles from './carousel-item.styles.js';
/**
* @summary A carousel item represent a slide within a carousel.

View File

@@ -1,154 +0,0 @@
:host {
--aspect-ratio: 16 / 9;
--scroll-hint: 0px;
--slide-gap: var(--wa-space-m, 1rem); /* fallback value is necessary */
display: flex;
}
.carousel {
display: grid;
grid-template-columns: min-content 1fr min-content;
grid-template-rows: 1fr min-content;
grid-template-areas:
'. slides .'
'. pagination .';
gap: var(--wa-space-m);
align-items: center;
min-height: 100%;
min-width: 100%;
position: relative;
}
.pagination {
grid-area: pagination;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--wa-space-s);
}
.slides {
grid-area: slides;
display: grid;
height: 100%;
width: 100%;
align-items: center;
justify-items: center;
overflow: auto;
overscroll-behavior-x: contain;
scrollbar-width: none;
aspect-ratio: calc(var(--aspect-ratio) * var(--slides-per-page));
border-radius: var(--wa-border-radius-m);
--slide-size: calc((100% - (var(--slides-per-page) - 1) * var(--slide-gap)) / var(--slides-per-page));
}
@media (prefers-reduced-motion) {
:where(.slides) {
scroll-behavior: auto;
}
}
.slides-horizontal {
grid-auto-flow: column;
grid-auto-columns: var(--slide-size);
grid-auto-rows: 100%;
column-gap: var(--slide-gap);
scroll-snap-type: x mandatory;
scroll-padding-inline: var(--scroll-hint);
padding-inline: var(--scroll-hint);
overflow-y: hidden;
}
.slides-vertical {
grid-auto-flow: row;
grid-auto-columns: 100%;
grid-auto-rows: var(--slide-size);
row-gap: var(--slide-gap);
scroll-snap-type: y mandatory;
scroll-padding-block: var(--scroll-hint);
padding-block: var(--scroll-hint);
overflow-x: hidden;
}
.slides-dragging,
.slides-dropping {
scroll-snap-type: unset;
}
:host([vertical]) ::slotted(wa-carousel-item) {
height: 100%;
}
.slides::-webkit-scrollbar {
display: none;
}
.navigation {
grid-area: navigation;
display: contents;
font-size: var(--wa-font-size-l);
}
.navigation-button {
flex: 0 0 auto;
display: flex;
align-items: center;
background: none;
border: none;
border-radius: var(--wa-border-radius-m);
font-size: inherit;
color: var(--wa-color-text-quiet);
padding: var(--wa-space-xs);
cursor: pointer;
transition: var(--wa-transition-normal) color;
appearance: none;
}
.navigation-button-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.navigation-button-disabled::part(base) {
pointer-events: none;
}
.navigation-button-previous {
grid-column: 1;
grid-row: 1;
}
.navigation-button-next {
grid-column: 3;
grid-row: 1;
}
.pagination-item {
display: block;
cursor: pointer;
background: none;
border: 0;
border-radius: var(--wa-border-radius-circle);
width: var(--wa-space-s);
height: var(--wa-space-s);
background-color: var(--wa-color-neutral-fill-normal);
padding: 0;
margin: 0;
transition: transform var(--wa-transition-slow);
}
.pagination-item-active {
background-color: var(--wa-form-control-activated-color);
transform: scale(1.25);
}
/* Focus styles */
.slides:focus-visible,
.navigation-button:focus-visible,
.pagination-item:focus-visible {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}

View File

@@ -0,0 +1,158 @@
import { css } from 'lit';
export default css`
:host {
--aspect-ratio: 16 / 9;
--scroll-hint: 0px;
--slide-gap: var(--wa-space-m, 1rem); /* fallback value is necessary */
display: flex;
}
.carousel {
display: grid;
grid-template-columns: min-content 1fr min-content;
grid-template-rows: 1fr min-content;
grid-template-areas:
'. slides .'
'. pagination .';
gap: var(--wa-space-m);
align-items: center;
min-height: 100%;
min-width: 100%;
position: relative;
}
.pagination {
grid-area: pagination;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--wa-space-s);
}
.slides {
grid-area: slides;
display: grid;
height: 100%;
width: 100%;
align-items: center;
justify-items: center;
overflow: auto;
overscroll-behavior-x: contain;
scrollbar-width: none;
aspect-ratio: calc(var(--aspect-ratio) * var(--slides-per-page));
border-radius: var(--wa-border-radius-m);
--slide-size: calc((100% - (var(--slides-per-page) - 1) * var(--slide-gap)) / var(--slides-per-page));
}
@media (prefers-reduced-motion) {
:where(.slides) {
scroll-behavior: auto;
}
}
.slides-horizontal {
grid-auto-flow: column;
grid-auto-columns: var(--slide-size);
grid-auto-rows: 100%;
column-gap: var(--slide-gap);
scroll-snap-type: x mandatory;
scroll-padding-inline: var(--scroll-hint);
padding-inline: var(--scroll-hint);
overflow-y: hidden;
}
.slides-vertical {
grid-auto-flow: row;
grid-auto-columns: 100%;
grid-auto-rows: var(--slide-size);
row-gap: var(--slide-gap);
scroll-snap-type: y mandatory;
scroll-padding-block: var(--scroll-hint);
padding-block: var(--scroll-hint);
overflow-x: hidden;
}
.slides-dragging,
.slides-dropping {
scroll-snap-type: unset;
}
:host([vertical]) ::slotted(wa-carousel-item) {
height: 100%;
}
.slides::-webkit-scrollbar {
display: none;
}
.navigation {
grid-area: navigation;
display: contents;
font-size: var(--wa-font-size-l);
}
.navigation-button {
flex: 0 0 auto;
display: flex;
align-items: center;
background: none;
border: none;
border-radius: var(--wa-border-radius-m);
font-size: inherit;
color: var(--wa-color-text-quiet);
padding: var(--wa-space-xs);
cursor: pointer;
transition: var(--wa-transition-normal) color;
appearance: none;
}
.navigation-button-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.navigation-button-disabled::part(base) {
pointer-events: none;
}
.navigation-button-previous {
grid-column: 1;
grid-row: 1;
}
.navigation-button-next {
grid-column: 3;
grid-row: 1;
}
.pagination-item {
display: block;
cursor: pointer;
background: none;
border: 0;
border-radius: var(--wa-border-radius-circle);
width: var(--wa-space-s);
height: var(--wa-space-s);
background-color: var(--wa-color-neutral-fill-normal);
padding: 0;
margin: 0;
transition: transform var(--wa-transition-slow);
}
.pagination-item-active {
background-color: var(--wa-form-control-activated-color);
transform: scale(1.25);
}
/* Focus styles */
.slides:focus-visible,
.navigation-button:focus-visible,
.pagination-item:focus-visible {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
`;

View File

@@ -16,7 +16,7 @@ import { LocalizeController } from '../../utilities/localize.js';
import type WaCarouselItem from '../carousel-item/carousel-item.js';
import '../icon/icon.js';
import { AutoplayController } from './autoplay-controller.js';
import styles from './carousel.css';
import styles from './carousel.styles.js';
/**
* @summary Carousels display an arbitrary number of content slides along a horizontal or vertical axis.

View File

@@ -1,100 +0,0 @@
:host {
--checked-icon-color: var(--wa-color-brand-on-loud);
--checked-icon-scale: 0.8;
display: inline-flex;
color: var(--wa-form-control-value-color);
font-family: inherit;
font-weight: var(--wa-form-control-value-font-weight);
line-height: var(--wa-form-control-value-line-height);
user-select: none;
-webkit-user-select: none;
}
[part~='control'] {
display: inline-flex;
flex: 0 0 auto;
position: relative;
align-items: center;
justify-content: center;
width: var(--wa-form-control-toggle-size);
height: var(--wa-form-control-toggle-size);
border-color: var(--wa-form-control-border-color);
border-radius: min(
calc(var(--wa-form-control-toggle-size) * 0.375),
var(--wa-border-radius-s)
); /* min prevents entirely circular checkbox */
border-style: var(--wa-border-style);
border-width: var(--wa-form-control-border-width);
background-color: var(--wa-form-control-background-color);
transition:
background var(--wa-transition-normal),
border-color var(--wa-transition-fast),
box-shadow var(--wa-transition-fast),
color var(--wa-transition-fast);
transition-timing-function: var(--wa-transition-easing);
margin-inline-end: 0.5em;
}
[part~='base'] {
display: flex;
align-items: flex-start;
position: relative;
color: currentColor;
vertical-align: middle;
cursor: pointer;
}
[part~='label'] {
display: inline;
}
/* Checked */
[part~='control']:has(:checked, :indeterminate) {
color: var(--checked-icon-color);
border-color: var(--wa-form-control-activated-color);
background-color: var(--wa-form-control-activated-color);
}
/* Focus */
[part~='control']:has(> input:focus-visible:not(:disabled)) {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
/* Disabled */
:host [part~='base']:has(input:disabled) {
opacity: 0.5;
cursor: not-allowed;
}
input {
position: absolute;
padding: 0;
margin: 0;
height: 100%;
width: 100%;
opacity: 0;
pointer-events: none;
}
[part~='icon'] {
display: flex;
scale: var(--checked-icon-scale);
/* Without this, Safari renders the icon slightly to the left */
&::part(svg) {
translate: 0.0009765625em;
}
input:not(:checked, :indeterminate) + & {
visibility: hidden;
}
}
:host([required]) [part~='label']::after {
content: var(--wa-form-control-required-content);
color: var(--wa-form-control-required-content-color);
margin-inline-start: var(--wa-form-control-required-content-offset);
}

View File

@@ -0,0 +1,104 @@
import { css } from 'lit';
export default css`
:host {
--checked-icon-color: var(--wa-color-brand-on-loud);
--checked-icon-scale: 0.8;
display: inline-flex;
color: var(--wa-form-control-value-color);
font-family: inherit;
font-weight: var(--wa-form-control-value-font-weight);
line-height: var(--wa-form-control-value-line-height);
user-select: none;
-webkit-user-select: none;
}
[part~='control'] {
display: inline-flex;
flex: 0 0 auto;
position: relative;
align-items: center;
justify-content: center;
width: var(--wa-form-control-toggle-size);
height: var(--wa-form-control-toggle-size);
border-color: var(--wa-form-control-border-color);
border-radius: min(
calc(var(--wa-form-control-toggle-size) * 0.375),
var(--wa-border-radius-s)
); /* min prevents entirely circular checkbox */
border-style: var(--wa-border-style);
border-width: var(--wa-form-control-border-width);
background-color: var(--wa-form-control-background-color);
transition:
background var(--wa-transition-normal),
border-color var(--wa-transition-fast),
box-shadow var(--wa-transition-fast),
color var(--wa-transition-fast);
transition-timing-function: var(--wa-transition-easing);
margin-inline-end: 0.5em;
}
[part~='base'] {
display: flex;
align-items: flex-start;
position: relative;
color: currentColor;
vertical-align: middle;
cursor: pointer;
}
[part~='label'] {
display: inline;
}
/* Checked */
[part~='control']:has(:checked, :indeterminate) {
color: var(--checked-icon-color);
border-color: var(--wa-form-control-activated-color);
background-color: var(--wa-form-control-activated-color);
}
/* Focus */
[part~='control']:has(> input:focus-visible:not(:disabled)) {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
/* Disabled */
:host [part~='base']:has(input:disabled) {
opacity: 0.5;
cursor: not-allowed;
}
input {
position: absolute;
padding: 0;
margin: 0;
height: 100%;
width: 100%;
opacity: 0;
pointer-events: none;
}
[part~='icon'] {
display: flex;
scale: var(--checked-icon-scale);
/* Without this, Safari renders the icon slightly to the left */
&::part(svg) {
translate: 0.0009765625em;
}
input:not(:checked, :indeterminate) + & {
visibility: hidden;
}
}
:host([required]) [part~='label']::after {
content: var(--wa-form-control-required-content);
color: var(--wa-form-control-required-content-color);
margin-inline-start: var(--wa-form-control-required-content-offset);
}
`;

View File

@@ -8,10 +8,10 @@ import { HasSlotController } from '../../internal/slot.js';
import { RequiredValidator } from '../../internal/validators/required-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import formControlStyles from '../../styles/component/form-control.css';
import sizeStyles from '../../styles/utilities/size.css';
import formControlStyles from '../../styles/component/form-control.styles.js';
import sizeStyles from '../../styles/component/size.styles.js';
import '../icon/icon.js';
import styles from './checkbox.css';
import styles from './checkbox.styles.js';
/**
* @summary Checkboxes allow the user to toggle an option on or off.
@@ -108,13 +108,6 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
@property({ type: Boolean, reflect: true, attribute: 'checked' }) defaultChecked: boolean =
this.hasAttribute('checked');
/**
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
* the same document or shadow root for this to work.
*/
@property({ reflect: true }) form = null;
/** Makes the checkbox a required field. */
@property({ type: Boolean, reflect: true }) required = false;

View File

@@ -1,341 +0,0 @@
:host {
--grid-width: 17em;
--grid-height: 12em;
--grid-handle-size: 1.25em;
--slider-height: 1em;
--slider-handle-size: calc(var(--slider-height) + 0.25em);
}
.color-picker {
background-color: var(--wa-color-surface-raised);
border-radius: var(--wa-border-radius-m);
border-style: var(--wa-border-style);
border-width: var(--wa-border-width-s);
border-color: var(--wa-color-surface-border);
box-shadow: var(--wa-shadow-m);
color: var(--color);
font: inherit;
font-size: inherit;
user-select: none;
width: var(--grid-width);
-webkit-user-select: none;
}
.grid {
position: relative;
height: var(--grid-height);
background-image:
linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%),
linear-gradient(to right, #fff 0%, rgba(255, 255, 255, 0) 100%);
border-top-left-radius: calc(var(--wa-border-radius-m) - var(--wa-border-width-s));
border-top-right-radius: calc(var(--wa-border-radius-m) - var(--wa-border-width-s));
cursor: crosshair;
forced-color-adjust: none;
}
.grid-handle {
position: absolute;
width: var(--grid-handle-size);
height: var(--grid-handle-size);
border-radius: var(--wa-border-radius-circle);
box-shadow: 0 0 0 0.0625rem rgba(0, 0, 0, 0.2);
border: solid 0.125rem white;
margin-top: calc(var(--grid-handle-size) / -2);
margin-left: calc(var(--grid-handle-size) / -2);
transition: scale var(--wa-transition-normal) var(--wa-transition-easing);
}
.grid-handle-dragging {
cursor: none;
scale: 1.5;
}
.grid-handle:focus-visible {
outline: var(--wa-focus-ring);
}
.controls {
padding: 0.75em;
display: flex;
align-items: center;
}
.sliders {
flex: 1 1 auto;
}
.slider {
position: relative;
height: var(--slider-height);
border-radius: var(--wa-border-radius-s);
box-shadow: inset 0 0 0 0.0625rem rgba(0, 0, 0, 0.2);
forced-color-adjust: none;
}
.slider:not(:last-of-type) {
margin-bottom: 0.75em;
}
.slider-handle {
position: absolute;
top: calc(50% - var(--slider-handle-size) / 2);
width: var(--slider-handle-size);
height: var(--slider-handle-size);
border-radius: var(--wa-border-radius-circle);
border: solid 0.125rem white;
box-shadow: 0 0 0 0.0625rem rgba(0, 0, 0, 0.2);
margin-left: calc(var(--slider-handle-size) / -2);
}
.slider-handle:focus-visible {
outline: var(--wa-focus-ring);
}
.hue {
background-image: linear-gradient(
to right,
rgb(255, 0, 0) 0%,
rgb(255, 255, 0) 17%,
rgb(0, 255, 0) 33%,
rgb(0, 255, 255) 50%,
rgb(0, 0, 255) 67%,
rgb(255, 0, 255) 83%,
rgb(255, 0, 0) 100%
);
}
.alpha .alpha-gradient {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
}
.preview {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
width: 3em;
height: 3em;
border: none;
border-radius: var(--wa-border-radius-circle);
background: none;
font-size: inherit;
margin-inline-start: 0.75em;
cursor: copy;
forced-color-adjust: none;
}
.preview:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
box-shadow: inset 0 0 0 0.0625rem rgba(0, 0, 0, 0.2);
/* We use a custom property in lieu of currentColor because of https://bugs.webkit.org/show_bug.cgi?id=216780 */
background-color: var(--preview-color);
}
.preview:focus-visible {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
.preview-color {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: solid 0.0625rem rgba(0, 0, 0, 0.125);
}
.preview-color-copied {
animation: pulse 850ms;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 var(--wa-color-brand-fill-loud);
}
70% {
box-shadow: 0 0 0 0.5rem transparent;
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
.user-input {
display: flex;
align-items: center;
padding: 0 0.75em 0.75em 0.75em;
}
.user-input wa-input {
min-width: 0; /* fix input width in Safari */
flex: 1 1 auto;
&::part(form-control-label) {
/* Visually hidden */
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;
}
}
.user-input wa-button-group {
margin-inline-start: 0.75em;
&::part(base) {
flex-wrap: nowrap;
}
}
.user-input wa-button:first-of-type {
min-width: 3em;
max-width: 3em;
}
.swatches {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(1.5em, 100%), 1fr));
grid-gap: 0.5em;
justify-items: center;
border-block-start: var(--wa-form-control-border-style) var(--wa-form-control-border-width)
var(--wa-color-surface-border);
padding: 0.5em;
forced-color-adjust: none;
}
.swatch {
position: relative;
aspect-ratio: 1 / 1;
width: 100%;
border-radius: var(--wa-border-radius-s);
}
.swatch .swatch-color {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: solid 0.0625rem rgba(0, 0, 0, 0.125);
border-radius: inherit;
cursor: pointer;
}
.swatch:focus-visible {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
.transparent-bg {
background-image:
linear-gradient(45deg, var(--wa-color-neutral-fill-normal) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--wa-color-neutral-fill-normal) 75%),
linear-gradient(45deg, transparent 75%, var(--wa-color-neutral-fill-normal) 75%),
linear-gradient(45deg, var(--wa-color-neutral-fill-normal) 25%, transparent 25%);
background-size: 0.5rem 0.5rem;
background-position:
0 0,
0 0,
-0.25rem -0.25rem,
0.25rem 0.25rem;
}
:host([disabled]) {
opacity: 0.5;
cursor: not-allowed;
.grid,
.grid-handle,
.slider,
.slider-handle,
.preview,
.swatch,
.swatch-color {
pointer-events: none;
}
}
/*
* Color dropdown
*/
.color-dropdown {
display: contents;
}
.color-dropdown::part(panel) {
max-height: none;
background-color: var(--wa-color-surface-raised);
border: var(--wa-border-style) var(--wa-border-width-s) var(--wa-color-surface-border);
border-radius: var(--wa-border-radius-m);
overflow: visible;
}
.trigger {
display: block;
position: relative;
background-color: transparent;
border: none;
cursor: pointer;
font-size: inherit;
forced-color-adjust: none;
width: var(--wa-form-control-height);
height: var(--wa-form-control-height);
border-radius: var(--wa-form-control-border-radius);
}
.trigger:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
background-color: currentColor;
box-shadow:
inset 0 0 0 var(--wa-form-control-border-width) var(--wa-form-control-border-color),
inset 0 0 0 calc(var(--wa-form-control-border-width) * 3) var(--wa-color-surface-default);
}
.trigger-empty:before {
background-color: transparent;
}
.trigger:focus-visible {
outline: none;
}
.trigger:focus-visible:not(.trigger:disabled) {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
:host([disabled]) :is(.label, .trigger) {
opacity: 0.5;
cursor: not-allowed;
}
.form-control.form-control-has-label .label {
cursor: pointer;
display: inline-block;
}

View File

@@ -0,0 +1,345 @@
import { css } from 'lit';
export default css`
:host {
--grid-width: 17em;
--grid-height: 12em;
--grid-handle-size: 1.25em;
--slider-height: 1em;
--slider-handle-size: calc(var(--slider-height) + 0.25em);
}
.color-picker {
background-color: var(--wa-color-surface-raised);
border-radius: var(--wa-border-radius-m);
border-style: var(--wa-border-style);
border-width: var(--wa-border-width-s);
border-color: var(--wa-color-surface-border);
box-shadow: var(--wa-shadow-m);
color: var(--color);
font: inherit;
font-size: inherit;
user-select: none;
width: var(--grid-width);
-webkit-user-select: none;
}
.grid {
position: relative;
height: var(--grid-height);
background-image:
linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%),
linear-gradient(to right, #fff 0%, rgba(255, 255, 255, 0) 100%);
border-top-left-radius: calc(var(--wa-border-radius-m) - var(--wa-border-width-s));
border-top-right-radius: calc(var(--wa-border-radius-m) - var(--wa-border-width-s));
cursor: crosshair;
forced-color-adjust: none;
}
.grid-handle {
position: absolute;
width: var(--grid-handle-size);
height: var(--grid-handle-size);
border-radius: var(--wa-border-radius-circle);
box-shadow: 0 0 0 0.0625rem rgba(0, 0, 0, 0.2);
border: solid 0.125rem white;
margin-top: calc(var(--grid-handle-size) / -2);
margin-left: calc(var(--grid-handle-size) / -2);
transition: scale var(--wa-transition-normal) var(--wa-transition-easing);
}
.grid-handle-dragging {
cursor: none;
scale: 1.5;
}
.grid-handle:focus-visible {
outline: var(--wa-focus-ring);
}
.controls {
padding: 0.75em;
display: flex;
align-items: center;
}
.sliders {
flex: 1 1 auto;
}
.slider {
position: relative;
height: var(--slider-height);
border-radius: var(--wa-border-radius-s);
box-shadow: inset 0 0 0 0.0625rem rgba(0, 0, 0, 0.2);
forced-color-adjust: none;
}
.slider:not(:last-of-type) {
margin-bottom: 0.75em;
}
.slider-handle {
position: absolute;
top: calc(50% - var(--slider-handle-size) / 2);
width: var(--slider-handle-size);
height: var(--slider-handle-size);
border-radius: var(--wa-border-radius-circle);
border: solid 0.125rem white;
box-shadow: 0 0 0 0.0625rem rgba(0, 0, 0, 0.2);
margin-left: calc(var(--slider-handle-size) / -2);
}
.slider-handle:focus-visible {
outline: var(--wa-focus-ring);
}
.hue {
background-image: linear-gradient(
to right,
rgb(255, 0, 0) 0%,
rgb(255, 255, 0) 17%,
rgb(0, 255, 0) 33%,
rgb(0, 255, 255) 50%,
rgb(0, 0, 255) 67%,
rgb(255, 0, 255) 83%,
rgb(255, 0, 0) 100%
);
}
.alpha .alpha-gradient {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
}
.preview {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
width: 3em;
height: 3em;
border: none;
border-radius: var(--wa-border-radius-circle);
background: none;
font-size: inherit;
margin-inline-start: 0.75em;
cursor: copy;
forced-color-adjust: none;
}
.preview:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
box-shadow: inset 0 0 0 0.0625rem rgba(0, 0, 0, 0.2);
/* We use a custom property in lieu of currentColor because of https://bugs.webkit.org/show_bug.cgi?id=216780 */
background-color: var(--preview-color);
}
.preview:focus-visible {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
.preview-color {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: solid 0.0625rem rgba(0, 0, 0, 0.125);
}
.preview-color-copied {
animation: pulse 850ms;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 var(--wa-color-brand-fill-loud);
}
70% {
box-shadow: 0 0 0 0.5rem transparent;
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
.user-input {
display: flex;
align-items: center;
padding: 0 0.75em 0.75em 0.75em;
}
.user-input wa-input {
min-width: 0; /* fix input width in Safari */
flex: 1 1 auto;
&::part(form-control-label) {
/* Visually hidden */
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;
}
}
.user-input wa-button-group {
margin-inline-start: 0.75em;
&::part(base) {
flex-wrap: nowrap;
}
}
.user-input wa-button:first-of-type {
min-width: 3em;
max-width: 3em;
}
.swatches {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(1.5em, 100%), 1fr));
grid-gap: 0.5em;
justify-items: center;
border-block-start: var(--wa-form-control-border-style) var(--wa-form-control-border-width)
var(--wa-color-surface-border);
padding: 0.5em;
forced-color-adjust: none;
}
.swatch {
position: relative;
aspect-ratio: 1 / 1;
width: 100%;
border-radius: var(--wa-border-radius-s);
}
.swatch .swatch-color {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: solid 0.0625rem rgba(0, 0, 0, 0.125);
border-radius: inherit;
cursor: pointer;
}
.swatch:focus-visible {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
.transparent-bg {
background-image:
linear-gradient(45deg, var(--wa-color-neutral-fill-normal) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--wa-color-neutral-fill-normal) 75%),
linear-gradient(45deg, transparent 75%, var(--wa-color-neutral-fill-normal) 75%),
linear-gradient(45deg, var(--wa-color-neutral-fill-normal) 25%, transparent 25%);
background-size: 0.5rem 0.5rem;
background-position:
0 0,
0 0,
-0.25rem -0.25rem,
0.25rem 0.25rem;
}
:host([disabled]) {
opacity: 0.5;
cursor: not-allowed;
.grid,
.grid-handle,
.slider,
.slider-handle,
.preview,
.swatch,
.swatch-color {
pointer-events: none;
}
}
/*
* Color dropdown
*/
.color-dropdown {
display: contents;
}
.color-dropdown::part(panel) {
max-height: none;
background-color: var(--wa-color-surface-raised);
border: var(--wa-border-style) var(--wa-border-width-s) var(--wa-color-surface-border);
border-radius: var(--wa-border-radius-m);
overflow: visible;
}
.trigger {
display: block;
position: relative;
background-color: transparent;
border: none;
cursor: pointer;
font-size: inherit;
forced-color-adjust: none;
width: var(--wa-form-control-height);
height: var(--wa-form-control-height);
border-radius: var(--wa-form-control-border-radius);
}
.trigger:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
background-color: currentColor;
box-shadow:
inset 0 0 0 var(--wa-form-control-border-width) var(--wa-form-control-border-color),
inset 0 0 0 calc(var(--wa-form-control-border-width) * 3) var(--wa-color-surface-default);
}
.trigger-empty:before {
background-color: transparent;
}
.trigger:focus-visible {
outline: none;
}
.trigger:focus-visible:not(.trigger:disabled) {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
:host([disabled]) :is(.label, .trigger) {
opacity: 0.5;
cursor: not-allowed;
}
.form-control.form-control-has-label .label {
cursor: pointer;
display: inline-block;
}
`;

View File

@@ -14,9 +14,9 @@ import { HasSlotController } from '../../internal/slot.js';
import { RequiredValidator } from '../../internal/validators/required-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import formControlStyles from '../../styles/component/form-control.css';
import sizeStyles from '../../styles/utilities/size.css';
import visuallyHidden from '../../styles/utilities/visually-hidden.css';
import formControlStyles from '../../styles/component/form-control.styles.js';
import sizeStyles from '../../styles/component/size.styles.js';
import visuallyHidden from '../../styles/component/visually-hidden.styles.js';
import { LocalizeController } from '../../utilities/localize.js';
import '../button-group/button-group.js';
import '../button/button.js';
@@ -25,7 +25,7 @@ import '../input/input.js';
import type WaInput from '../input/input.js';
import '../popup/popup.js';
import type WaPopup from '../popup/popup.js';
import styles from './color-picker.css';
import styles from './color-picker.styles.js';
interface EyeDropperConstructor {
new (): EyeDropperInterface;
@@ -220,13 +220,6 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
*/
@property() swatches: string | string[] = '';
/**
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
* the same document or shadow root for this to work.
*/
@property({ reflect: true }) form = null;
/** Makes the color picker a required field. */
@property({ type: Boolean, reflect: true }) required = false;
@@ -1284,7 +1277,14 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
})}
part="trigger-container form-control"
>
<div part="form-control-label" class="label" id="form-control-label">
<div
part="form-control-label"
class=${classMap({
label: true,
'has-label': hasLabel,
})}
id="form-control-label"
>
<slot name="label">${this.label}</slot>
</div>

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