Compare commits

..

4 Commits

Author SHA1 Message Date
Cory LaViska
0daaedea68 fix pro+ icon family/variants; closes #1951 2026-01-16 11:31:25 -05:00
Cory LaViska
e04124c280 update changelog 2026-01-16 11:31:03 -05:00
Cory LaViska
c1f237c204 update example 2026-01-16 11:30:58 -05:00
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
6 changed files with 162 additions and 196 deletions

View File

@@ -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,7 +127,10 @@
</nav>
{% endif %}
<div id="flashes">{% server "flashes" %}</div>
{# Flashes #}
{% block flashes %}
<div id="flashes">{% server "flashes" %}</div>
{% endblock %}
{% block header %}
{% if hasGeneratedTitle %}
@@ -134,6 +147,24 @@
{% 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' %}
{#- Site-Wide Dialog -#}
@@ -143,9 +174,6 @@
{#- Cookie Consent Dialog -#}
{% include "cookie-consent.njk" ignore missing %}
{# Footer #}
{% block pageFooter %}{% endblock %}
</wa-page>
</body>

View File

@@ -396,6 +396,20 @@ If you're a [Font Awesome Pro+ customer](https://fontawesome.com/), you have acc
</div>
</div>
<div class="wa-flank" style="--flank-size: 10ch;">
<a href="https://fontawesome.com/icons/packs/utility" target="_blank">Utility</a>
<div class="wa-cluster" style="font-size: 1.5em;">
<wa-icon family="utility" variant="semibold" name="house"></wa-icon>
<wa-icon
family="utility-duo"
variant="semibold"
name="house"
style="--secondary-color: skyblue; --secondary-opacity: 0.8;"
></wa-icon>
<wa-icon family="utility-fill" variant="semibold" name="house"></wa-icon>
</div>
</div>
<div class="wa-flank" style="--flank-size: 10ch;">
<a href="https://fontawesome.com/icons/packs/whiteboard" target="_blank">Whiteboard</a>
<div class="wa-cluster" style="font-size: 32px;">

View File

@@ -24,6 +24,7 @@ Components with the <wa-badge variant="warning">Experimental</wa-badge> badge sh
- [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-icon>` to support Font Awesome Pro+ icon families that include qualifiers (e.g., `family="jelly-duo"` now works correctly instead of requiring `family="jelly" variant="duo-regular"`) and updated Font Awesome to 7.1.0
- 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]
@@ -542,4 +543,4 @@ Many of these changes and improvements were the direct result of feedback from u
</details>
Did we miss something? [Let us know!](https://github.com/shoelace-style/webawesome/discussions)
Did we miss something? [Let us know!](https://github.com/shoelace-style/webawesome/discussions)

View File

@@ -200,7 +200,7 @@ icon names.
lines.push('## Components');
lines.push('');
const sortedComponentsList = [...components].sort((a, b) => a.tagName.localeCompare(b.tagName));
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);
@@ -225,7 +225,7 @@ icon names.
lines.push('');
// Sort components alphabetically by tag name for the API reference
const sortedComponents = [...components].sort((a, b) => a.tagName.localeCompare(b.tagName));
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));

View File

@@ -21,80 +21,6 @@ const components = getAllComponents(metadata);
const index = [];
/**
* Generate JSDoc comment for a property
*/
function generatePropJsDoc(description) {
if (!description) return '';
// Escape any */ in the description to avoid breaking the JSDoc comment
const escaped = description.replace(/\*\//g, '*\\/');
return `/** ${escaped} */\n `;
}
/**
* Get public properties from component members that have an attribute
* (these are the props that can be set via JSX)
*/
function getPublicProps(component, internalProps = []) {
const props = [];
const seenProps = new Set();
// Get properties from members that have attributes (these are the public reactive properties)
for (const member of component.members || []) {
if (member.kind === 'field' && member.attribute && !member.privacy) {
// Skip internal properties
if (internalProps.includes(member.name)) continue;
if (seenProps.has(member.name)) continue;
seenProps.add(member.name);
props.push({
name: member.name,
type: member.type?.text || 'any',
description: member.description || '',
});
}
}
return props;
}
/**
* Convert a TypeScript type from the CEM to a valid prop type.
* Returns the type to use in the interface, and whether it should use Component reference.
*/
function normalizeType(typeText, propName) {
if (!typeText) return { type: 'any', useComponentRef: false };
// Simple/primitive types that don't need Component reference
const simpleTypePatterns = [
/^(string|number|boolean|null|undefined)$/,
/^'[^']*'(\s*\|\s*'[^']*')*$/, // String literal unions like 'small' | 'medium' | 'large'
/^(string|number|boolean)\s*\|\s*(string|number|boolean|null|undefined)/, // Simple unions
];
for (const pattern of simpleTypePatterns) {
if (pattern.test(typeText)) {
return { type: typeText, useComponentRef: false };
}
}
// Complex types (containing Element, custom types, etc.) should use Component reference
// to ensure all types are properly resolved
const complexTypeIndicators = ['Element', 'VirtualElement', 'HTMLElement', '[]', '()', '=>', 'Record', 'Map', 'Set'];
if (complexTypeIndicators.some(indicator => typeText.includes(indicator))) {
return { type: `Component['${propName}']`, useComponentRef: true };
}
// Default: use the type directly
return { type: typeText, useComponentRef: false };
}
// Properties that conflict with React.HTMLAttributes and need to be omitted
const CONFLICTING_HTML_PROPS = ['defaultValue', 'color', 'size', 'value', 'checked', 'disabled', 'type', 'name', 'title'];
// Internal properties that shouldn't be exposed in React props
const INTERNAL_PROPS = ['didSSR', 'form', 'internals', 'shadowRoot', 'assignedSlot'];
for await (const component of components) {
if (!component.tagName) {
continue;
@@ -121,51 +47,6 @@ for await (const component of components) {
const jsDoc = component.jsDoc || '';
// Generate explicit props interface for better IDE support
const publicProps = getPublicProps(component, INTERNAL_PROPS);
const propsInterfaceName = `${component.name}Props`;
// Generate prop definitions with JSDoc comments
const propDefinitions = publicProps
.map(prop => {
const jsDocComment = generatePropJsDoc(prop.description);
const { type } = normalizeType(prop.type, prop.name);
return `${jsDocComment}'${prop.name}'?: ${type};`;
})
.join('\n ');
// Generate event handler prop definitions
const eventPropDefinitions = eventsToWrap
.map(event => {
const description = component.events?.find(e => e.name === event.name)?.description || '';
const jsDocComment = generatePropJsDoc(description);
return `${jsDocComment}${event.reactName}?: (event: ${event.eventName}) => void;`;
})
.join('\n ');
// Combine props and events into the interface
const allPropDefinitions = [propDefinitions, eventPropDefinitions].filter(Boolean).join('\n ');
// Find which conflicting props this component has
const componentConflictingProps = publicProps
.filter(prop => CONFLICTING_HTML_PROPS.includes(prop.name))
.map(prop => `'${prop.name}'`);
// Generate the base type with omitted conflicting props
const baseType = componentConflictingProps.length > 0
? `Omit<React.HTMLAttributes<Component>, ${componentConflictingProps.join(' | ')}>`
: `React.HTMLAttributes<Component>`;
const propsInterface = `
/**
* Props for the ${component.name} component.
* This interface provides explicit typing for better IDE support and documentation.
*/
export interface ${propsInterfaceName} extends ${baseType} {
${allPropDefinitions}
}
`;
const source = await prettier.format(
`
import * as React from 'react';
@@ -178,8 +59,6 @@ export interface ${propsInterfaceName} extends ${baseType} {
const tagName = '${component.tagName}'
${propsInterface}
${jsDoc}
const reactWrapper = createComponent({
tagName,
@@ -198,7 +77,7 @@ export interface ${propsInterfaceName} extends ${baseType} {
}),
);
index.push(`export { default as ${component.name}, type ${propsInterfaceName} } from './${tagWithoutPrefix}/index.js';`);
index.push(`export { default as ${component.name} } from './${tagWithoutPrefix}/index.js';`);
fs.writeFileSync(componentFile, source, 'utf8');
}

View File

@@ -1,7 +1,7 @@
import { getKitCode } from '../../utilities/base-path.js';
import type { IconLibrary } from './library.js';
const FA_VERSION = '7.0.1';
const FA_VERSION = '7.1.0';
function getIconUrl(name: string, family: string, variant: string) {
const kitCode = getKitCode();
@@ -9,10 +9,16 @@ function getIconUrl(name: string, family: string, variant: string) {
let folder = 'solid';
// Notdog (Pro+)
// Correct usage: family="notdog" or family="notdog-duo", variant="solid"
if (family === 'notdog') {
if (variant === 'solid') folder = 'solid';
if (variant === 'duo-solid') folder = 'duo-solid';
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/notdog-${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`;
// NOTE: variant="duo-solid" is deprecated, use family="notdog-duo" variant="solid" instead
if (variant === 'solid') folder = 'notdog-solid';
if (variant === 'duo-solid') folder = 'notdog-duo-solid';
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`;
}
if (family === 'notdog-duo') {
folder = 'notdog-duo-solid';
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`;
}
// Chisel (Pro+)
@@ -26,18 +32,35 @@ function getIconUrl(name: string, family: string, variant: string) {
}
// Jelly (Pro+)
// Correct usage: family="jelly", family="jelly-duo", or family="jelly-fill", variant="regular"
if (family === 'jelly') {
if (variant === 'regular') folder = 'regular';
if (variant === 'duo-regular') folder = 'duo-regular';
if (variant === 'fill-regular') folder = 'fill-regular';
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/jelly-${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`;
// NOTE: variant="duo-regular" and variant="fill-regular" are deprecated
// Use family="jelly-duo" variant="regular" or family="jelly-fill" variant="regular" instead
if (variant === 'regular') folder = 'jelly-regular';
if (variant === 'duo-regular') folder = 'jelly-duo-regular';
if (variant === 'fill-regular') folder = 'jelly-fill-regular';
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`;
}
if (family === 'jelly-duo') {
folder = 'jelly-duo-regular';
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`;
}
if (family === 'jelly-fill') {
folder = 'jelly-fill-regular';
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`;
}
// Slab (Pro+)
// Correct usage: family="slab" or family="slab-press", variant="regular"
if (family === 'slab') {
if (variant === 'solid' || variant === 'regular') folder = 'regular';
if (variant === 'press-regular') folder = 'press-regular';
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/slab-${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`;
// NOTE: variant="press-regular" is deprecated, use family="slab-press" variant="regular" instead
if (variant === 'solid' || variant === 'regular') folder = 'slab-regular';
if (variant === 'press-regular') folder = 'slab-press-regular';
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`;
}
if (family === 'slab-press') {
folder = 'slab-press-regular';
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`;
}
// Thumbprint (Pro+)
@@ -50,6 +73,21 @@ function getIconUrl(name: string, family: string, variant: string) {
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/whiteboard-semibold/${name}.svg?token=${encodeURIComponent(kitCode)}`;
}
// Utility (Pro+)
// Correct usage: family="utility", family="utility-duo", or family="utility-fill", variant="semibold"
if (family === 'utility') {
folder = 'utility-semibold';
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`;
}
if (family === 'utility-duo') {
folder = 'utility-duo-semibold';
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`;
}
if (family === 'utility-fill') {
folder = 'utility-fill-semibold';
return `https://ka-p.fontawesome.com/releases/v${FA_VERSION}/svgs/${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`;
}
// Classic
if (family === 'classic') {
if (variant === 'thin') folder = 'thin';
@@ -108,10 +146,16 @@ const library: IconLibrary = {
family === 'duotone' ||
// Sharp duotone
family === 'sharp-duotone' ||
// Notdog duo-solid
// Notdog duo (correct usage: family="notdog-duo")
family === 'notdog-duo' ||
// NOTE: family="notdog" variant="duo-solid" is deprecated
(family === 'notdog' && variant === 'duo-solid') ||
// Jelly duo-regular
// Jelly duo (correct usage: family="jelly-duo")
family === 'jelly-duo' ||
// NOTE: family="jelly" variant="duo-regular" is deprecated
(family === 'jelly' && variant === 'duo-regular') ||
// Utility duo (correct usage: family="utility-duo")
family === 'utility-duo' ||
// Thumbprint
family === 'thumbprint'
) {