Compare commits

...

7 Commits

Author SHA1 Message Date
Cory LaViska
4668573a0e improve icon perf; closes #1729 2025-11-17 14:47:13 -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
8 changed files with 105 additions and 179 deletions

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);
}
}
}
});
/**
@@ -135,78 +124,8 @@ 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 => composePageTitle(data.title),
// Open Graph title with smart + default site name formatting
@@ -220,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 => {
@@ -393,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

@@ -1,97 +1,95 @@
<wa-dialog id="dialog-site" light-dismiss without-header>
{% 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="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-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-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" class="plausible-event-name=launch_dialog:close_button_click">Maybe Later</wa-button>
<wa-button variant="neutral" appearance="accent" href="/purchase" class="brand-font plausible-event-name=launch_dialog:pro_purchase_button_click">
<wa-icon slot="start" variant="regular" name="rocket-launch"></wa-icon>
Get Pro + Save 20%
</wa-button>
</div>
</div>
</wa-dialog>
<div class="wa-split">
<wa-button type="button" appearance="plain" data-dialog="close" class="plausible-event-name=launch_dialog:close_button_click">Maybe Later</wa-button>
<wa-button variant="neutral" appearance="accent" href="/purchase" class="brand-font plausible-event-name=launch_dialog:pro_purchase_button_click">
<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';
<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;
}
// Helper function to safely track Plausible events
const trackEvent = (eventName) => {
if (typeof plausible !== 'undefined') {
plausible(eventName);
}
};
// Initialize dialog functionality
let initCalled = false;
const initDialog = () => {
// Prevent double initialization
if (initCalled) {
return;
}
initCalled = true;
// Track when dialog is shown
dialog.addEventListener('wa-show', () => {
trackEvent('launch_dialog:view');
}, { once: true });
// Track when dialog is dismissed
dialog.addEventListener('wa-hide', (event) => {
// Track overlay click or Escape key dismissal
// Button clicks are tracked via CSS classes, so we only track non-button dismissals
if (event.detail?.source === dialog) {
trackEvent('launch_dialog:overlay_click');
}
// Save dismissal state to localStorage
// Early exit if user has dismissed the dialog
try {
localStorage.setItem(SITE_DIALOG_DISMISSED_KEY, 'true');
if (localStorage.getItem(SITE_DIALOG_DISMISSED_KEY) === 'true') {
return;
}
} catch (e) {
// localStorage may be disabled or unavailable
return;
}
}, { once: true });
// Show dialog after a short delay to ensure page is loaded
setTimeout(() => {
dialog.open = true;
}, 500);
};
// Wait for DOM and dialog element
const dialog = document.getElementById('dialog-site');
if (!dialog) {
return;
}
// Initialize when ready
if (customElements.get('wa-dialog')) {
initDialog();
} else {
document.addEventListener('wa-discovery-complete', initDialog, { once: true });
// Fallback timeout in case wa-discovery-complete doesn't fire
setTimeout(initDialog, 100);
}
})();
</script>
// Helper function to safely track Plausible events
const trackEvent = (eventName) => {
if (typeof plausible !== 'undefined') {
plausible(eventName);
}
};
// Initialize dialog functionality
let initCalled = false;
const initDialog = () => {
// Prevent double initialization
if (initCalled) {
return;
}
initCalled = true;
// Track when dialog is shown
dialog.addEventListener('wa-show', () => {
trackEvent('launch_dialog:view');
}, { once: true });
// Track when dialog is dismissed
dialog.addEventListener('wa-hide', (event) => {
// Track overlay click or Escape key dismissal
// Button clicks are tracked via CSS classes, so we only track non-button dismissals
if (event.detail?.source === dialog) {
trackEvent('launch_dialog:overlay_click');
}
// 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

@@ -139,11 +139,7 @@
{#- Site-Wide Dialog -#}
{% if hasSiteDialog %}
{% raw %}
{%- if not currentUser.hasPro -%}
{% include "_dialog-wa-launch.njk" ignore missing %}
{%- endif -%}
{% endraw %}
{% include "_dialog-wa-launch.njk" ignore missing %}
{% endif %}
{# Footer #}

View File

@@ -1,10 +1,11 @@
---
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.
@@ -13,6 +14,8 @@ Components with the <wa-badge variant="warning">Experimental</wa-badge> badge sh
## Next
- 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]
- Improved performance of `<wa-icon>` so initial rendering occurs faster, especially with multiple icons on the page [issue:1729]
## 3.0.0

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

@@ -30,7 +30,15 @@ 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 })

View File

@@ -187,7 +187,7 @@ export default class WaIcon extends WebAwesomeElement {
}
}
@watch(['family', 'name', 'library', 'variant', 'src', 'autoWidth', 'swapOpacity'])
@watch(['family', 'name', 'library', 'variant', 'src', 'autoWidth', 'swapOpacity'], { waitUntilFirstUpdate: true })
async setIcon() {
const { url, fromLibrary } = this.getIconSource();
const library = fromLibrary ? getIconLibrary(this.library) : undefined;