Compare commits

...
11 Commits
26 changed files with 3816 additions and 574 deletions
+72
View File
@@ -0,0 +1,72 @@
name: Announce release on Discord
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: Release tag to announce
required: true
type: string
permissions:
contents: read
jobs:
announce:
runs-on: ubuntu-latest
steps:
- name: Send Discord announcement
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
RELEASE_TAG_INPUT: ${{ inputs.tag }}
REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
test -n "$DISCORD_WEBHOOK_URL"
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
test -n "$RELEASE_TAG_INPUT"
curl --fail-with-body \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"$GITHUB_API_URL/repos/$REPO/releases/tags/$RELEASE_TAG_INPUT" \
> release.json
else
jq '.release' "$GITHUB_EVENT_PATH" > release.json
fi
if [ "$(jq -r '.prerelease' release.json)" = "true" ]; then
echo "Skipping prerelease."
exit 0
fi
jq -n \
--arg repo "$REPO" \
--arg name "$(jq -r '.name // ""' release.json)" \
--arg tag "$(jq -r '.tag_name' release.json)" \
--arg url "$(jq -r '.html_url' release.json)" \
--arg body "$(jq -r '.body // ""' release.json)" \
'($body | if length > 1500 then .[:1500] + "...\n\n[Read more](" + $url + ")" else . end) as $description |
{
username: "Basecoat Releases",
allowed_mentions: { parse: [] },
embeds: [{
title: ($repo + " " + $tag),
url: $url,
description: ((if $name != "" then "**" + $name + "**\n\n" else "" end) + $description),
color: 5793266
}]
}' > payload.json
curl --fail-with-body \
--retry 3 \
--retry-all-errors \
--retry-delay 2 \
-H "Content-Type: application/json" \
-X POST \
-d @payload.json \
"$DISCORD_WEBHOOK_URL"
+4
View File
@@ -1,6 +1,10 @@
.*
!.gitignore
!.gitattributes
!.github/
!.github/workflows/
!.github/workflows/*.yml
!.github/workflows/*.yaml
_site/
node_modules/
+1
View File
@@ -48,6 +48,7 @@
## JavaScript
- JavaScript is behavior only; do not use JS to paper over CSS architecture issues.
- Register JS components with `window.basecoat.register()` and make initialization idempotent with `data-*-initialized` flags.
- Component init functions must return before attaching listeners or mutating DOM when the root already has its initialized flag.
- Keep generated DOM minimal and aligned with documented HTML.
- Prefer native browser APIs and events.
- Keep scroll management scoped to component containers. Do not use `scrollIntoView()` when it can scroll the page unexpectedly.
+20
View File
@@ -2,6 +2,26 @@
## Unreleased
## [1.0.2] - 2026-07-06
### Added
- Added `data-size="sm"` support to Alert Dialog for the compact upstream size.
### Changed
- Removed global font smoothing from Basecoat's package CSS so `antialiased` remains an application-level choice.
## [1.0.1] - 2026-06-28
### Added
- Added explicit `force: true` support to `window.basecoat.init()` and `window.basecoat.initAll()` for rehydrating restored DOM after framework or navigation cache restores.
### Fixed
- Reinitialized Basecoat components after HTMX history restores in the docs site.
## [1.0.0] - 2026-06-27
### Breaking Changes
+1
View File
@@ -92,6 +92,7 @@ These entrypoints are generated by `scripts/generate-css-entrypoints.js` and by
- `window.basecoat.init(name)` initializes uninitialized components for one registered component.
- `window.basecoat.initAll()` initializes all uninitialized registered components.
- `window.basecoat.init(name, { force: true })` and `window.basecoat.initAll({ force: true })` destroy and reinitialize matching components. Use this when a framework or navigation library restores previously initialized DOM from a cache. Force mode resets transient component state such as open menus, focus state, and active interactions.
- `window.basecoat.refresh(element)` asks an initialized component to rescan dynamic children when it supports refresh.
- Components use internal destroy hooks to clean up listeners when initialized roots are removed from the DOM.
+800 -70
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "basecoat-css",
"version": "1.0.0",
"version": "1.0.2",
"description": "Tailwind CSS for Basecoat components",
"author": {
"name": "hunvreus",
@@ -123,12 +123,12 @@
"scripts": {
"build": "node ./scripts/build.js",
"prepack": "node ./scripts/build.js",
"docs:build": "npm run build && npm --prefix site run build",
"docs:build": "npm run build && npm --prefix site clean-install --progress=false && npm --prefix site run build",
"docs:dev": "npm --prefix site run dev",
"docs:preview": "npm run docs:build && npm --prefix site run preview",
"workers:dev": "npm run build && npm --prefix site run workers:dev",
"workers:deploy": "npm run build && npm --prefix site run workers:deploy",
"workers:preview": "npm run build && npm --prefix site run workers:preview"
"workers:dev": "npm run build && npm --prefix site clean-install --progress=false && npm --prefix site run workers:dev",
"workers:deploy": "npm run build && npm --prefix site clean-install --progress=false && npm --prefix site run workers:deploy",
"workers:preview": "npm run build && npm --prefix site clean-install --progress=false && npm --prefix site run workers:preview"
},
"devDependencies": {
"@tailwindcss/cli": "^4.1.17",
+1
View File
@@ -31,6 +31,7 @@ export default defineConfig({
style: 'vega',
css: false,
bodyAttrs: {
class: 'antialiased',
'hx-boost': 'true',
'hx-target': '#content',
'hx-select': '#content',
+2764 -485
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -15,7 +15,7 @@
"htmx.org": "^2.0.10",
"lucide-static": "^1.17.0",
"nunjucks": "^3.2.4",
"reallysimpledocs": "^1.0.6"
"reallysimpledocs": "^1.0.12"
},
"devDependencies": {
"astro": "^6.4.6",
+26
View File
@@ -159,7 +159,33 @@ const styleUrls = {
}
};
const clearSerializedRuntimeState = (root) => {
for (const element of root.querySelectorAll?.("*") || []) {
delete element.dataset.basecoatComponent;
for (const attribute of Array.from(element.attributes)) {
if (/^data-[a-z0-9-]+-initialized$/.test(attribute.name)) {
element.removeAttribute(attribute.name);
}
}
}
};
const cleanHistoryItem = (event) => {
const item = event.detail?.item;
if (typeof item?.content !== "string") return;
const template = document.createElement("template");
template.innerHTML = item.content;
clearSerializedRuntimeState(template.content);
item.content = template.innerHTML;
};
document.addEventListener("DOMContentLoaded", syncSidebar);
document.addEventListener("htmx:historyItemCreated", cleanHistoryItem);
document.addEventListener("htmx:historyRestore", () => {
window.basecoat?.initAll?.({ force: true });
syncSidebar();
});
document.addEventListener("htmx:afterSettle", () => {
window.basecoat?.initAll?.();
syncSidebar();
+22 -1
View File
@@ -74,7 +74,7 @@ Alert Dialog uses a native `<dialog class="alert-dialog">` element. Omit the clo
<dd>Trigger button. Basecoat uses the native <code>HTMLDialogElement.showModal()</code> method.</dd>
<dt><code>&lt;dialog class="alert-dialog" id="&#123; DIALOG_ID &#125;"&gt;</code></dt>
<dd>
Native modal dialog. Add <code>aria-labelledby</code> and <code>aria-describedby</code> when title and description are present.
Native modal dialog. Add <code>aria-labelledby</code> and <code>aria-describedby</code> when title and description are present. Use <code>data-size="sm"</code> for the compact upstream size.
<dl>
<dt><code>&lt;div&gt;</code></dt>
<dd>
@@ -164,6 +164,27 @@ Use a destructive action button when the alert dialog confirms a destructive ope
</dialog>
</Preview>
### Small
Use `data-size="sm"` for a compact alert dialog. The small size keeps media, title, and description stacked and centers the action buttons in a two-column footer.
<Preview>
<button type="button" onclick="document.getElementById('alert-dialog-small').showModal()" class="btn" data-variant="outline">Invite Members</button>
<dialog id="alert-dialog-small" class="alert-dialog" data-size="sm" aria-labelledby="alert-dialog-small-title" aria-describedby="alert-dialog-small-description">
<div>
<header>
<figure><svg class="lucide lucide-users" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><path d="M16 3.128a4 4 0 0 1 0 7.744" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><circle cx="9" cy="7" r="4" /></svg></figure>
<h2 id="alert-dialog-small-title">Invite team members?</h2>
<p id="alert-dialog-small-description">Send an invitation link to your selected teammates.</p>
</header>
<footer>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Invite</button>
</footer>
</div>
</dialog>
</Preview>
### RTL
Alert Dialog supports document direction. Set `dir="rtl"` on the dialog or a parent element.
+8
View File
@@ -243,6 +243,14 @@ Chart uses a separate helper and is not included in `basecoat-css/all`.
Each component page shows the minimal scripts or imports required for that component.
### JavaScript lifecycle
Basecoat initializes registered components on page load and when new DOM is inserted.
Use `window.basecoat.initAll()` after manually inserting HTML that contains Basecoat components. It only initializes components that are not already initialized.
Use `window.basecoat.initAll({ force: true })` when a framework or navigation library restores previously initialized DOM from a cache, such as an HTMX history restore. Force mode destroys existing component instances before reinitializing them, so it also resets transient state such as open menus, focus state, and active interactions.
## Compatibility
Basecoat 1.0 introduces new style packs and a smaller public API based on root classes plus semantic HTML and documented attributes. If you need pre-1.0 class aliases while migrating, load the compatibility stylesheet after your Basecoat stylesheet.
+2 -3
View File
@@ -1,6 +1,3 @@
import "../../../src/js/basecoat.js";
import "../../../src/js/accordion.js";
const initPriceRange = () => {
const input = document.getElementById("price-range");
const output = document.getElementById("price-range-output");
@@ -104,3 +101,5 @@ if (document.readyState === "loading") {
} else {
initDemos();
}
document.addEventListener("htmx:afterSettle", initDemos);
+1 -1
View File
@@ -131,6 +131,6 @@
@apply scroll-smooth;
}
body {
@apply bg-background text-foreground overscroll-none antialiased;
@apply bg-background text-foreground overscroll-none;
}
}
+20
View File
@@ -77,5 +77,25 @@
> * > footer {
@apply flex flex-col-reverse sm:flex-row sm:justify-end;
}
&:is([data-size='sm']) > * > header,
> [data-size='sm'] > header {
@apply sm:place-items-center sm:text-center;
}
&:is([data-size='sm']) > * > header:has(> figure),
> [data-size='sm'] > header:has(> figure) {
@apply sm:grid-rows-[auto_auto_1fr];
}
&:is([data-size='sm']) > * > header:has(> figure) > :is(h2, p),
> [data-size='sm'] > header:has(> figure) > :is(h2, p) {
@apply sm:col-start-auto;
}
&:is([data-size='sm']) > * > footer,
> [data-size='sm'] > footer {
@apply grid grid-cols-2 sm:grid sm:grid-cols-2;
}
}
}
+5
View File
@@ -540,6 +540,11 @@
@apply bg-popover text-popover-foreground ring-foreground/5 dark:ring-foreground/10 max-w-xs gap-6 rounded-4xl p-6 shadow-xl ring-1 duration-100 sm:max-w-md;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/5 dark:ring-foreground/10 max-w-[calc(100%-2rem)] gap-6 rounded-4xl p-6 text-sm shadow-xl ring-1 duration-100 sm:max-w-md;
}
+5
View File
@@ -520,6 +520,11 @@
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-xs gap-4 rounded-none p-4 ring-1 duration-100 sm:max-w-sm;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-[calc(100%-2rem)] gap-4 rounded-none p-4 text-xs/relaxed ring-1 duration-100 sm:max-w-sm;
}
+5
View File
@@ -532,6 +532,11 @@
@apply bg-popover text-popover-foreground ring-foreground/5 max-w-xs gap-6 rounded-4xl p-6 ring-1 duration-100 sm:max-w-md;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/5 max-w-[calc(100%-2rem)] gap-6 rounded-4xl p-6 text-sm ring-1 duration-100 sm:max-w-md;
}
+5
View File
@@ -536,6 +536,11 @@
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-xs gap-3 rounded-xl p-4 ring-1 duration-100 sm:max-w-sm;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply max-w-64 sm:max-w-64;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-xs/relaxed ring-1 duration-100 sm:max-w-sm;
}
+5
View File
@@ -528,6 +528,11 @@
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-xs gap-4 rounded-xl p-4 ring-1 duration-100 sm:max-w-sm;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm;
}
+5
View File
@@ -540,6 +540,11 @@
@apply bg-popover text-popover-foreground ring-foreground/5 dark:ring-foreground/10 max-w-xs gap-6 rounded-3xl p-6 shadow-xl ring-1 duration-100 sm:max-w-md;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/5 dark:ring-foreground/10 max-w-[calc(100%-2rem)] gap-6 rounded-3xl p-6 text-sm shadow-xl ring-1 duration-100 sm:max-w-md;
}
+5
View File
@@ -544,6 +544,11 @@
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-xs gap-6 rounded-none p-6 shadow-md ring-1 duration-100 sm:max-w-md;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-[calc(100%-2rem)] gap-6 rounded-none p-6 text-sm shadow-md ring-1 duration-100 sm:max-w-md;
}
+5
View File
@@ -528,6 +528,11 @@
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-xs gap-6 rounded-xl p-6 ring-1 duration-100 sm:max-w-lg;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-[calc(100%-2rem)] gap-6 rounded-xl p-6 text-sm ring-1 duration-100 sm:max-w-md;
}
+26 -6
View File
@@ -63,9 +63,24 @@
node.querySelectorAll('[data-basecoat-component]').forEach(destroyComponent);
};
const initAllComponents = () => {
const uniqueElements = (elements) => Array.from(new Set(elements));
const getComponentElements = (componentName, selector, force = false) => {
const elements = Array.from(document.querySelectorAll(selector));
if (force) {
elements.push(...document.querySelectorAll(`[data-basecoat-component="${componentName}"]`));
}
return uniqueElements(elements);
};
const initAllComponents = (options = {}) => {
const force = options.force === true;
Object.entries(componentRegistry).forEach(([name, { selector }]) => {
document.querySelectorAll(selector).forEach(element => initComponent(element, name));
getComponentElements(name, selector, force).forEach((element) => {
const wasComponent = element.dataset?.basecoatComponent === name;
if (force) destroyComponent(element);
if (wasComponent || element.matches(selector)) initComponent(element, name);
});
});
};
@@ -111,18 +126,23 @@
observer = null;
};
const initRegisteredComponent = (componentName) => {
const initRegisteredComponent = (componentName, options = {}) => {
const component = componentRegistry[componentName];
if (!component) {
console.warn(`Component '${componentName}' not found in registry`);
return;
}
document.querySelectorAll(component.selector).forEach(element => initComponent(element, componentName));
const force = options.force === true;
getComponentElements(componentName, component.selector, force).forEach((element) => {
const wasComponent = element.dataset?.basecoatComponent === componentName;
if (force) destroyComponent(element);
if (wasComponent || element.matches(component.selector)) initComponent(element, componentName);
});
};
const initAllRegisteredComponents = () => {
initAllComponents();
const initAllRegisteredComponents = (options = {}) => {
initAllComponents(options);
};
const setTheme = (mode) => {
+2 -2
View File
@@ -1,6 +1,6 @@
(() => {
const initSidebar = (sidebarComponent) => {
if (sidebarComponent.dataset.sidebarInitialized) return;
if (sidebarComponent.dataset.sidebarInitialized && typeof sidebarComponent.toggle === 'function') return;
const initialOpen = sidebarComponent.dataset.initialOpen !== 'false';
const initialMobileOpen = sidebarComponent.dataset.initialMobileOpen === 'true';
@@ -60,6 +60,6 @@
};
if (window.basecoat) {
window.basecoat.register('sidebar', '.sidebar:not([data-sidebar-initialized])', initSidebar);
window.basecoat.register('sidebar', '.sidebar', initSidebar);
}
})();