mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-11 04:38:18 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2ac210002 | ||
|
|
b7d33eee04 | ||
|
|
30e5bdcf3a | ||
|
|
cafffa0597 | ||
|
|
2a2c64c651 | ||
|
|
0b28009e38 | ||
|
|
d942c8b31d | ||
|
|
d826637eb9 | ||
|
|
2ad4f6a43b |
@@ -0,0 +1,33 @@
|
||||
name: Deploy site on release
|
||||
|
||||
# Cloudflare Workers Builds deploys the site from the `release` branch.
|
||||
# Publishing a GitHub release moves `release` to the released tag.
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Tag to deploy (for example 1.1.0)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
move-release-branch:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Point release branch at tag
|
||||
env:
|
||||
TAG: ${{ inputs.tag || github.event.release.tag_name }}
|
||||
run: |
|
||||
SHA=$(git rev-parse "refs/tags/$TAG^{commit}")
|
||||
git push --force origin "$SHA:refs/heads/release"
|
||||
+15
-4
@@ -1,13 +1,12 @@
|
||||
# Contributing to Basecoat
|
||||
|
||||
- Support development by sponsoring the author: https://github.com/sponsors/hunvreus
|
||||
- Submit pull requests (PRs) against the `development` branch, not `main`.
|
||||
- Submit pull requests (PRs) against the `main` branch.
|
||||
- For branches, we have:
|
||||
- `main` is our production and default branch. This is what we deploy to https://basecoatui.com and publish to https://www.npmjs.com/package/basecoat-css.
|
||||
- `development` is for staging. We will deploy it soon at https://dev.basecoatui.com.
|
||||
- New features are worked in `feature/name-of-the-feature` branches.
|
||||
- Issues are addressed in `issue/123-main-isse` branches.
|
||||
- When ready, we PR against `development`, test it and then finally merge to `main`.
|
||||
- Issues are addressed in `issue/123-main-issue` branches.
|
||||
- When ready, open a PR against `main`.
|
||||
- Keep changes focused: one feature or fix per PR.
|
||||
- Test locally before submitting.
|
||||
- Follow existing code style.
|
||||
@@ -29,3 +28,15 @@ Workers Builds settings:
|
||||
- Enable builds for non-production branches for branch preview URLs.
|
||||
|
||||
Thanks for helping!
|
||||
|
||||
## Releases and the docs site
|
||||
|
||||
The site at basecoatui.com deploys from the `release` branch, not `main`, so the docs always match the version on npm. Nobody commits to `release` directly.
|
||||
|
||||
To release:
|
||||
|
||||
1. Bump the version in `package.json`, commit, tag (for example `1.1.0`) and push.
|
||||
2. Run `npm publish`.
|
||||
3. Publish a GitHub release for the tag, for example `gh release create 1.1.0 --generate-notes`.
|
||||
|
||||
Publishing the release runs `.github/workflows/deploy-site.yml`, which points `release` at the tag. Cloudflare then builds and deploys the site. To redeploy or roll back, run that workflow manually with the tag you want live.
|
||||
|
||||
@@ -64,6 +64,37 @@ async function copyDirRecursive(src, dest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively inlines relative `@import "./...";` lines into a single CSS string.
|
||||
// Each file is inlined at most once per bundle (tracked in `seen`).
|
||||
async function inlineCssImports(filePath, seen) {
|
||||
const resolved = path.resolve(filePath);
|
||||
if (seen.has(resolved)) return '';
|
||||
seen.add(resolved);
|
||||
|
||||
const source = await fs.readFile(resolved, 'utf-8');
|
||||
const lines = source.split('\n');
|
||||
const out = [];
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^\s*@import\s+["'](\.{1,2}\/[^"']+)["']\s*;\s*$/);
|
||||
if (match) {
|
||||
const inlined = await inlineCssImports(path.join(path.dirname(resolved), match[1]), seen);
|
||||
if (inlined) out.push(inlined.replace(/\n+$/, ''));
|
||||
} else if (/^\s*@import\s+["']\.{1,2}\//.test(line)) {
|
||||
throw new Error(`Unsupported relative @import (with modifiers) in ${resolved}: ${line.trim()}`);
|
||||
} else {
|
||||
out.push(line);
|
||||
}
|
||||
}
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
// Writes an uncompiled single-file bundle for a dist CSS entrypoint.
|
||||
async function writeCssBundle(entryPath, outPath, label, version) {
|
||||
const body = await inlineCssImports(entryPath, new Set());
|
||||
const header = `/* Basecoat v${version}, ${label}, single-file source. Import after tailwindcss. */\n`;
|
||||
await fs.writeFile(outPath, `${header}${body.replace(/\n+$/, '')}\n`);
|
||||
}
|
||||
|
||||
// Main build function to prepare the package for publishing.
|
||||
async function build() {
|
||||
console.log('Starting build process...');
|
||||
@@ -155,6 +186,20 @@ async function build() {
|
||||
await copyDirRecursive(srcCssCompatDir, cssCompatDistDir);
|
||||
console.log(`Copied split CSS folders to ${cssDistDir}`);
|
||||
|
||||
// Create uncompiled single-file bundles for Tailwind users without a package manager.
|
||||
const { version } = JSON.parse(await fs.readFile(path.join(projectRoot, 'package.json'), 'utf-8'));
|
||||
for (const style of styles) {
|
||||
await writeCssBundle(
|
||||
path.join(cssDistDir, `basecoat-${style}.css`),
|
||||
path.join(cssDistDir, `basecoat-${style}.bundle.css`),
|
||||
style,
|
||||
version,
|
||||
);
|
||||
}
|
||||
await writeCssBundle(path.join(cssDistDir, 'basecoat.css'), path.join(cssDistDir, 'basecoat.bundle.css'), 'vega', version);
|
||||
await writeCssBundle(path.join(cssDistDir, 'basecoat-base.css'), path.join(cssDistDir, 'basecoat-base.bundle.css'), 'base (no style pack)', version);
|
||||
console.log(`Generated single-file CSS bundles in ${cssDistDir}`);
|
||||
|
||||
await copyDirRecursive(srcNunjucksDir, path.join(cssTemplatesDir, 'nunjucks'));
|
||||
await copyDirRecursive(srcJinjaDir, path.join(cssTemplatesDir, 'jinja'));
|
||||
console.log(`Copied template assets to ${cssTemplatesDir}`);
|
||||
|
||||
@@ -24,28 +24,28 @@ const styleUrls = {
|
||||
|
||||
<style is:inline>
|
||||
@font-face {
|
||||
font-family: "Geist Sans";
|
||||
font-family: "Geist";
|
||||
src: url("/assets/fonts/geist/geist-sans-latin-400-normal.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Geist Sans";
|
||||
font-family: "Geist";
|
||||
src: url("/assets/fonts/geist/geist-sans-latin-500-normal.woff2") format("woff2");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Geist Sans";
|
||||
font-family: "Geist";
|
||||
src: url("/assets/fonts/geist/geist-sans-latin-600-normal.woff2") format("woff2");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Geist Sans";
|
||||
font-family: "Geist";
|
||||
src: url("/assets/fonts/geist/geist-sans-latin-700-normal.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
|
||||
@@ -197,7 +197,7 @@ A simple command menu in a dialog.
|
||||
|
||||
<Preview>
|
||||
<button type="button" class="btn" data-variant="outline" onclick="document.getElementById('command-basic').showModal()">Open Menu</button>
|
||||
<dialog id="command-basic" class="command-dialog" aria-label="Command menu" onclick="if (event.target === this) this.close()">
|
||||
<dialog id="command-basic" class="command-dialog" aria-label="Command menu" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
|
||||
<div class="command">
|
||||
<header><svg 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" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
|
||||
<input type="text" id="command-basic-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="command-basic-menu" />
|
||||
@@ -218,7 +218,7 @@ A simple command menu in a dialog.
|
||||
|
||||
<Preview>
|
||||
<button type="button" class="btn" data-variant="outline" onclick="document.getElementById('command-shortcuts-dialog').showModal()">Open Menu</button>
|
||||
<dialog id="command-shortcuts-dialog" class="command-dialog" aria-label="Command menu" onclick="if (event.target === this) this.close()">
|
||||
<dialog id="command-shortcuts-dialog" class="command-dialog" aria-label="Command menu" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
|
||||
<div class="command">
|
||||
<header><svg 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" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
|
||||
<input type="text" id="command-shortcuts-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="command-shortcuts-menu" />
|
||||
@@ -248,7 +248,7 @@ A simple command menu in a dialog.
|
||||
|
||||
<Preview>
|
||||
<button type="button" class="btn" data-variant="outline" onclick="document.getElementById('command-groups-dialog').showModal()">Open Menu</button>
|
||||
<dialog id="command-groups-dialog" class="command-dialog" aria-label="Command menu" onclick="if (event.target === this) this.close()">
|
||||
<dialog id="command-groups-dialog" class="command-dialog" aria-label="Command menu" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
|
||||
<div class="command">
|
||||
<header><svg 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" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
|
||||
<input type="text" id="command-groups-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="command-groups-menu" />
|
||||
@@ -291,7 +291,7 @@ A simple command menu in a dialog.
|
||||
|
||||
<Preview>
|
||||
<button type="button" class="btn" data-variant="outline" onclick="document.getElementById('command-scrollable-dialog').showModal()">Open Menu</button>
|
||||
<dialog id="command-scrollable-dialog" class="command-dialog" aria-label="Command menu" onclick="if (event.target === this) this.close()">
|
||||
<dialog id="command-scrollable-dialog" class="command-dialog" aria-label="Command menu" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
|
||||
<div class="command">
|
||||
<header><svg 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" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
|
||||
<input type="text" id="command-scrollable-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="command-scrollable-menu" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<Preview>
|
||||
<button type="button" onclick="document.getElementById('demo-dialog-edit-profile').showModal()" class="btn" data-variant="outline">Open Dialog</button>
|
||||
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onclick="if (event.target === this) this.close()">
|
||||
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
|
||||
<div class="sm:max-w-sm">
|
||||
<header>
|
||||
<h2 id="demo-dialog-edit-profile-title">Edit profile</h2>
|
||||
@@ -63,7 +63,7 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
|
||||
|
||||
```html
|
||||
<button type="button" onclick="document.getElementById('demo-dialog-edit-profile').showModal()" class="btn" data-variant="outline">Open Dialog</button>
|
||||
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onclick="if (event.target === this) this.close()">
|
||||
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
|
||||
<div class="sm:max-w-sm">
|
||||
<header>
|
||||
<h2 id="demo-dialog-edit-profile-title">Edit profile</h2>
|
||||
@@ -134,7 +134,7 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
|
||||
|
||||
<Preview>
|
||||
<button type="button" onclick="document.getElementById('dialog-custom-close').showModal()" class="btn" data-variant="outline">Share</button>
|
||||
<dialog id="dialog-custom-close" class="dialog" aria-labelledby="dialog-custom-close-title" aria-describedby="dialog-custom-close-description" onclick="if (event.target === this) this.close()">
|
||||
<dialog id="dialog-custom-close" class="dialog" aria-labelledby="dialog-custom-close-title" aria-describedby="dialog-custom-close-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
|
||||
<div class="sm:max-w-md">
|
||||
<header>
|
||||
<h2 id="dialog-custom-close-title">Share link</h2>
|
||||
@@ -157,7 +157,7 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
|
||||
|
||||
<Preview>
|
||||
<button type="button" onclick="document.getElementById('dialog-no-close').showModal()" class="btn" data-variant="outline">No Close Button</button>
|
||||
<dialog id="dialog-no-close" class="dialog" aria-labelledby="dialog-no-close-title" aria-describedby="dialog-no-close-description" onclick="if (event.target === this) this.close()">
|
||||
<dialog id="dialog-no-close" class="dialog" aria-labelledby="dialog-no-close-title" aria-describedby="dialog-no-close-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
|
||||
<div>
|
||||
<header>
|
||||
<h2 id="dialog-no-close-title">No Close Button</h2>
|
||||
@@ -171,7 +171,7 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
|
||||
|
||||
<Preview>
|
||||
<button type="button" onclick="document.getElementById('dialog-sticky-footer').showModal()" class="btn" data-variant="outline">Sticky Footer</button>
|
||||
<dialog id="dialog-sticky-footer" class="dialog" aria-labelledby="dialog-sticky-footer-title" aria-describedby="dialog-sticky-footer-description" onclick="if (event.target === this) this.close()">
|
||||
<dialog id="dialog-sticky-footer" class="dialog" aria-labelledby="dialog-sticky-footer-title" aria-describedby="dialog-sticky-footer-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
|
||||
<div>
|
||||
<header>
|
||||
<h2 id="dialog-sticky-footer-title">Sticky Footer</h2>
|
||||
@@ -199,7 +199,7 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
|
||||
|
||||
<Preview>
|
||||
<button type="button" onclick="document.getElementById('dialog-scrollable').showModal()" class="btn" data-variant="outline">Scrollable Content</button>
|
||||
<dialog id="dialog-scrollable" class="dialog" aria-labelledby="dialog-scrollable-title" aria-describedby="dialog-scrollable-description" onclick="if (event.target === this) this.close()">
|
||||
<dialog id="dialog-scrollable" class="dialog" aria-labelledby="dialog-scrollable-title" aria-describedby="dialog-scrollable-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
|
||||
<div>
|
||||
<header>
|
||||
<h2 id="dialog-scrollable-title">Scrollable Content</h2>
|
||||
@@ -229,7 +229,7 @@ Dialog positioning and close button placement use logical properties. Set `dir="
|
||||
<Preview>
|
||||
<div dir="rtl">
|
||||
<button type="button" onclick="document.getElementById('dialog-rtl').showModal()" class="btn" data-variant="outline">فتح الحوار</button>
|
||||
<dialog id="dialog-rtl" class="dialog" aria-labelledby="dialog-rtl-title" aria-describedby="dialog-rtl-description" onclick="if (event.target === this) this.close()">
|
||||
<dialog id="dialog-rtl" class="dialog" aria-labelledby="dialog-rtl-title" aria-describedby="dialog-rtl-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
|
||||
<div class="sm:max-w-sm">
|
||||
<header>
|
||||
<h2 id="dialog-rtl-title">تعديل الملف الشخصي</h2>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
hx-get="/fragments/toast/success"
|
||||
hx-target="#toaster"
|
||||
hx-swap="beforeend"
|
||||
hx-push-url="false"
|
||||
>Toast from backend (with HTMX)</button>
|
||||
</Preview>
|
||||
|
||||
@@ -163,6 +164,10 @@ Append server-rendered toast markup to the toaster, or create one from JavaScrip
|
||||
|
||||
### Config object
|
||||
|
||||
<Callout type="warning" title="Text is escaped by default" icon="triangle-alert">
|
||||
`title`, `description`, and action labels are inserted as plain text. With `html: true` they are inserted as HTML, so never pass untrusted user input with it. `onclick` is JavaScript code and must never contain user input. `icon` is always inserted as raw HTML (it is meant for SVG markup).
|
||||
</Callout>
|
||||
|
||||
<dl>
|
||||
<dt><code>duration</code> <span class="badge" data-variant="secondary">Optional</span></dt>
|
||||
<dd>Timeout in milliseconds. Defaults to <code>3000</code>, or <code>5000</code> for error toasts. Use <code>-1</code> to keep it open.</dd>
|
||||
@@ -176,4 +181,10 @@ Append server-rendered toast markup to the toaster, or create one from JavaScrip
|
||||
<dd>Primary action. Supports <code>label</code> with either <code>href</code> or <code>onclick</code>.</dd>
|
||||
<dt><code>cancel</code> <span class="badge" data-variant="secondary">Optional</span></dt>
|
||||
<dd>Secondary action. Supports <code>label</code> and optional <code>onclick</code>.</dd>
|
||||
<dt><code>icon</code> <span class="badge" data-variant="secondary">Optional</span></dt>
|
||||
<dd>Custom icon markup, usually an SVG. Inserted as raw HTML. Defaults to the category icon.</dd>
|
||||
<dt><code>html</code> <span class="badge" data-variant="secondary">Optional</span></dt>
|
||||
<dd>Set to <code>true</code> to insert <code>title</code>, <code>description</code>, and action labels as HTML instead of plain text. Defaults to <code>false</code>. Only use it with trusted content.</dd>
|
||||
</dl>
|
||||
|
||||
The Jinja and Nunjucks `toast()` macros render on your server, so your template engine's autoescaping applies to `title`, `description`, and labels there when it is enabled. Nunjucks enables it by default. With Jinja, make sure your environment's `autoescape` setting covers the Basecoat template files (for example `.html.jinja`). `icon` is output with `safe`, and `onclick` is output as an attribute, so neither should contain user input.
|
||||
|
||||
@@ -45,12 +45,12 @@ Learn more in the [shadcn/ui theming docs](https://ui.shadcn.com/docs/theming).
|
||||
|
||||
## Fonts
|
||||
|
||||
Basecoat does not ship web font files by default. Its font tokens prefer [Geist Sans](https://fonts.google.com/specimen/Geist) and [Geist Mono](https://fonts.google.com/specimen/Geist+Mono) when those fonts are available, then fall back to the full Tailwind default sans and mono stacks.
|
||||
Basecoat does not ship web font files by default. Its font tokens prefer [Geist](https://fonts.google.com/specimen/Geist) and [Geist Mono](https://fonts.google.com/specimen/Geist+Mono) when those fonts are available, then fall back to the full Tailwind default sans and mono stacks.
|
||||
|
||||
Install the fonts with Fontsource:
|
||||
|
||||
```bash
|
||||
npm install @fontsource/geist-sans @fontsource/geist-mono
|
||||
npm install @fontsource/geist @fontsource/geist-mono
|
||||
```
|
||||
|
||||
Then import the font files. Basecoat's default font tokens already reference Geist with full system fallbacks:
|
||||
@@ -58,10 +58,10 @@ Then import the font files. Basecoat's default font tokens already reference Gei
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "basecoat-css/sera";
|
||||
@import "@fontsource/geist-sans/400.css";
|
||||
@import "@fontsource/geist-sans/500.css";
|
||||
@import "@fontsource/geist-sans/600.css";
|
||||
@import "@fontsource/geist-sans/700.css";
|
||||
@import "@fontsource/geist/400.css";
|
||||
@import "@fontsource/geist/500.css";
|
||||
@import "@fontsource/geist/600.css";
|
||||
@import "@fontsource/geist/700.css";
|
||||
@import "@fontsource/geist-mono/400.css";
|
||||
@import "@fontsource/geist-mono/500.css";
|
||||
@import "@fontsource/geist-mono/600.css";
|
||||
|
||||
@@ -64,6 +64,8 @@ Or load the Basecoat runtime once, then only the component scripts you use.
|
||||
| `dist/basecoat-{style}.cdn.min.css` | Standalone CDN stylesheet for `vega`, `nova`, `maia`, `lyra`, `mira`, `luma`, `sera`, or `rhea`. |
|
||||
| `dist/basecoat-base.cdn.min.css` | Base tokens, utilities, and component structure without a style pack. |
|
||||
| `dist/basecoat-compat.cdn.min.css` | Optional pre-1.0 compatibility aliases. Load after Basecoat. |
|
||||
| `dist/basecoat.bundle.css` | Uncompiled single-file source for use with your own Tailwind build. Alias for the Vega style. |
|
||||
| `dist/basecoat-{style}.bundle.css` | Uncompiled single-file source for use with your own Tailwind build, for `vega`, `nova`, `maia`, `lyra`, `mira`, `luma`, `sera`, or `rhea`. |
|
||||
| `dist/js/all.min.js` | All auto-initialized JavaScript components except Chart. |
|
||||
| `dist/js/basecoat.min.js` | Runtime only. Load once before individual component scripts. |
|
||||
| `dist/js/{component}.min.js` | Individual JavaScript component file. |
|
||||
@@ -129,6 +131,10 @@ Or import a named style bundle.
|
||||
@import "basecoat-css/maia";
|
||||
```
|
||||
|
||||
<Callout title="Using the Tailwind CLI without npm?" icon="info">
|
||||
Download a single-file bundle, for example <a href={`https://cdn.jsdelivr.net/npm/basecoat-css@${basecoatVersion}/dist/basecoat-maia.bundle.css`}>{`basecoat-maia.bundle.css`}</a>. Save it next to your CSS and import it after Tailwind: `@import "tailwindcss"; @import "./basecoat-maia.bundle.css";`. Use `basecoat.bundle.css` for the default Vega style.
|
||||
</Callout>
|
||||
|
||||
<Callout type="warning" title="Stylesheet order matters" icon="triangle-alert">
|
||||
Load Basecoat after any stylesheet that includes Tailwind base/preflight. Loading another full Tailwind build after Basecoat can reset borders, inputs, and other component defaults.
|
||||
</Callout>
|
||||
@@ -195,7 +201,7 @@ See [Templates](/templates) for the available macros and arguments.
|
||||
| `basecoat-css/components` | All component structure without a style pack. |
|
||||
| `basecoat-css/components/button` | Individual component CSS. Replace `button` with any component file. |
|
||||
| `basecoat-css/styles/maia` | Individual style-pack CSS. Use with `basecoat-css/base` or `basecoat-css/components`. |
|
||||
| `basecoat-css/all.css` | Full default stylesheet plus all style-pack CSS. Use only when you need every style pack available at runtime. |
|
||||
| `basecoat-css/all.css` | Alias for `basecoat-css`, the default Vega stylesheet. It does not include the other style packs. |
|
||||
| `basecoat-css/compat` | Optional pre-1.0 compatibility aliases. Load after Basecoat. |
|
||||
|
||||
### Template files
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans: "Geist Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-sans: "Geist", "Geist Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
|
||||
@@ -415,7 +415,7 @@
|
||||
|
||||
/* Combobox */
|
||||
.combobox > input[role='combobox'] {
|
||||
@apply bg-input/50 border-transparent focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-3xl border py-2 ps-3 pe-8 text-sm transition-[color,box-shadow,background-color] focus-visible:ring-3 aria-invalid:ring-3;
|
||||
@apply bg-input/50 border-transparent focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-3xl border py-2 ps-3 pe-8 text-base md:text-sm transition-[color,box-shadow,background-color] focus-visible:ring-3 aria-invalid:ring-3;
|
||||
}
|
||||
|
||||
.combobox > svg,
|
||||
|
||||
@@ -407,7 +407,7 @@
|
||||
|
||||
/* Combobox */
|
||||
.combobox > input[role='combobox'] {
|
||||
@apply border-input bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-4xl border py-2 ps-3 pe-8 text-sm transition-colors focus-visible:ring-[3px] aria-invalid:ring-[3px];
|
||||
@apply border-input bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-4xl border py-2 ps-3 pe-8 text-base md:text-sm transition-colors focus-visible:ring-[3px] aria-invalid:ring-[3px];
|
||||
}
|
||||
|
||||
.combobox > svg,
|
||||
|
||||
@@ -407,7 +407,7 @@
|
||||
|
||||
/* Combobox */
|
||||
.combobox > input[role='combobox'] {
|
||||
@apply border-input bg-input/20 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-7 rounded-md border py-1 ps-2 pe-6 text-xs/relaxed transition-colors focus-visible:ring-2 aria-invalid:ring-2;
|
||||
@apply border-input bg-input/20 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-7 rounded-md border py-1 ps-2 pe-6 text-sm md:text-xs/relaxed transition-colors focus-visible:ring-2 aria-invalid:ring-2;
|
||||
}
|
||||
|
||||
.combobox > svg,
|
||||
|
||||
@@ -403,7 +403,7 @@
|
||||
|
||||
/* Combobox */
|
||||
.combobox > input[role='combobox'] {
|
||||
@apply border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent py-1 ps-2.5 pe-8 text-sm transition-colors focus-visible:ring-3 aria-invalid:ring-3;
|
||||
@apply border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent py-1 ps-2.5 pe-8 text-base md:text-sm transition-colors focus-visible:ring-3 aria-invalid:ring-3;
|
||||
}
|
||||
|
||||
.combobox > svg,
|
||||
|
||||
@@ -415,7 +415,7 @@
|
||||
|
||||
/* Combobox */
|
||||
.combobox > input[role='combobox'] {
|
||||
@apply bg-input/50 border-transparent focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-8 rounded-2xl border py-1 ps-2.5 pe-8 text-sm transition-[color,box-shadow] duration-200 focus-visible:ring-3 aria-invalid:ring-3;
|
||||
@apply bg-input/50 border-transparent focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-8 rounded-2xl border py-1 ps-2.5 pe-8 text-base md:text-sm transition-[color,box-shadow] duration-200 focus-visible:ring-3 aria-invalid:ring-3;
|
||||
}
|
||||
|
||||
.combobox > svg,
|
||||
|
||||
@@ -419,7 +419,7 @@
|
||||
|
||||
/* Combobox */
|
||||
.combobox > input[role='combobox'] {
|
||||
@apply border-transparent border-b-input bg-transparent focus-visible:border-b-ring aria-invalid:border-b-destructive dark:aria-invalid:border-b-destructive/50 h-10 rounded-none border py-2 ps-0 pe-8 text-sm transition-[color,border-color];
|
||||
@apply border-transparent border-b-input bg-transparent focus-visible:border-b-ring aria-invalid:border-b-destructive dark:aria-invalid:border-b-destructive/50 h-10 rounded-none border py-2 ps-0 pe-8 text-base md:text-sm transition-[color,border-color];
|
||||
}
|
||||
|
||||
.combobox > svg,
|
||||
|
||||
@@ -403,7 +403,7 @@
|
||||
|
||||
/* Combobox */
|
||||
.combobox > input[role='combobox'] {
|
||||
@apply border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent py-2 ps-2.5 pe-8 text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-3 aria-invalid:ring-3;
|
||||
@apply border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent py-2 ps-2.5 pe-8 text-base md:text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-3 aria-invalid:ring-3;
|
||||
}
|
||||
|
||||
.combobox > svg,
|
||||
|
||||
+18
-7
@@ -131,6 +131,13 @@
|
||||
element.addEventListener('transitionend', () => element.remove(), { once: true });
|
||||
}
|
||||
|
||||
const escapeHtml = (value) => String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
function createToast(config) {
|
||||
const {
|
||||
category = 'info',
|
||||
@@ -140,18 +147,22 @@
|
||||
cancel,
|
||||
duration,
|
||||
icon,
|
||||
html: allowHtml = false,
|
||||
} = config;
|
||||
|
||||
// Text is escaped unless `html: true`. `icon` is always raw markup (SVG).
|
||||
const text = (value) => (allowHtml ? String(value) : escapeHtml(value));
|
||||
|
||||
const iconHtml = icon || (category && ICONS[category]) || '';
|
||||
const titleHtml = title ? `<h2>${title}</h2>` : '';
|
||||
const descriptionHtml = description ? `<p>${description}</p>` : '';
|
||||
const titleHtml = title ? `<h2>${text(title)}</h2>` : '';
|
||||
const descriptionHtml = description ? `<p>${text(description)}</p>` : '';
|
||||
const actionHtml = action?.href
|
||||
? `<a href="${action.href}" class="btn" data-toast-action>${action.label}</a>`
|
||||
? `<a href="${escapeHtml(action.href)}" class="btn" data-toast-action>${text(action.label)}</a>`
|
||||
: action?.onclick
|
||||
? `<button type="button" class="btn" data-toast-action onclick="${action.onclick}">${action.label}</button>`
|
||||
? `<button type="button" class="btn" data-toast-action onclick="${escapeHtml(action.onclick)}">${text(action.label)}</button>`
|
||||
: '';
|
||||
const cancelHtml = cancel
|
||||
? `<button type="button" class="btn h-6 text-xs px-2.5 rounded-sm" data-variant="outline" data-toast-cancel onclick="${cancel?.onclick || ''}">${cancel.label}</button>`
|
||||
? `<button type="button" class="btn h-6 text-xs px-2.5 rounded-sm" data-variant="outline" data-toast-cancel onclick="${escapeHtml(cancel?.onclick || '')}">${text(cancel.label)}</button>`
|
||||
: '';
|
||||
|
||||
const footerHtml = actionHtml || cancelHtml ? `<footer>${actionHtml}${cancelHtml}</footer>` : '';
|
||||
@@ -161,8 +172,8 @@
|
||||
class="toast"
|
||||
role="${category === 'error' ? 'alert' : 'status'}"
|
||||
aria-atomic="true"
|
||||
${category ? `data-category="${category}"` : ''}
|
||||
${duration !== undefined ? `data-duration="${duration}"` : ''}
|
||||
${category ? `data-category="${escapeHtml(category)}"` : ''}
|
||||
${duration !== undefined ? `data-duration="${escapeHtml(duration)}"` : ''}
|
||||
>
|
||||
<div class="toast-content">
|
||||
${iconHtml}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
class="command-dialog {{ dialog_attrs.class | default('') }}"
|
||||
aria-label="Command menu"
|
||||
{% if open %}open{% endif %}
|
||||
onclick="if (event.target === this) this.close()"
|
||||
onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()"
|
||||
{% for key, value in dialog_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
{% if title %}aria-labelledby="{{ id }}-title"{% endif %}
|
||||
{% if description %}aria-describedby="{{ id }}-description"{% endif %}
|
||||
{% if open %}open{% endif %}
|
||||
{% if close_on_overlay_click %}onclick="if (event.target === this) this.close()"{% endif %}
|
||||
{% if close_on_overlay_click %}onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()"{% endif %}
|
||||
{% for key, value in dialog_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
class="command-dialog {{ dialog_attrs.class }}"
|
||||
aria-label="Command menu"
|
||||
{% if open %}open{% endif %}
|
||||
onclick="if (event.target === this) this.close()"
|
||||
onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()"
|
||||
{% for key, value in dialog_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
{% if title %}aria-labelledby="{{ id }}-title"{% endif %}
|
||||
{% if description %}aria-describedby="{{ id }}-description"{% endif %}
|
||||
{% if open %}open{% endif %}
|
||||
{% if close_on_overlay_click %}onclick="if (event.target === this) this.close()"{% endif %}
|
||||
{% if close_on_overlay_click %}onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()"{% endif %}
|
||||
{% for key, value in dialog_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user