fernwell 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/dist/combobox.d.ts +28 -0
- package/dist/fernwell.cjs +4 -0
- package/dist/fernwell.cjs.map +7 -0
- package/dist/fernwell.css +1548 -0
- package/dist/fernwell.esm.js +4 -0
- package/dist/fernwell.esm.js.map +7 -0
- package/dist/fernwell.iife.js +5 -0
- package/dist/fernwell.iife.js.map +7 -0
- package/dist/fernwell.min.css +2 -0
- package/dist/fernwell.tokens.css +105 -0
- package/dist/iife.d.ts +2 -0
- package/dist/index.d.ts +36 -0
- package/dist/loading.d.ts +5 -0
- package/dist/modal.d.ts +13 -0
- package/dist/nav.d.ts +3 -0
- package/dist/theme.d.ts +52 -0
- package/dist/toast.d.ts +14 -0
- package/dist/tokens.json +199 -0
- package/package.json +61 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Changed (breaking)
|
|
8
|
+
- **Colour tokens renamed from the default palette to semantic roles**, so a consumer never has to rename a token to retheme it. `src/tokens/tokens.json` and every component now speak in roles; the shipped values are unchanged, just renamed:
|
|
9
|
+
- `--fw-ink` → `--fw-text`, `--fw-ink-soft` → `--fw-text-soft`, `--fw-ink-fixed` → `--fw-on-primary`
|
|
10
|
+
- `--fw-cloud` → `--fw-bg`, `--fw-cloud-dim` → `--fw-bg-dim`
|
|
11
|
+
- `--fw-marigold` → `--fw-primary`, `--fw-marigold-lt` → `--fw-primary-soft`
|
|
12
|
+
- `--fw-marigold-dk` → `--fw-primary-hover` (button hover) or `--fw-warning` (pending tag/dot/toast — see below)
|
|
13
|
+
- `--fw-plum` → `--fw-secondary`, `--fw-plum-dk` → `--fw-secondary-hover`, `--fw-plum-bg` → `--fw-secondary-bg`
|
|
14
|
+
- `--fw-meadow` → `--fw-success`, `--fw-meadow-bg` → `--fw-success-bg`
|
|
15
|
+
- `--fw-coral` → `--fw-danger`, `--fw-coral-bg` → `--fw-danger-bg`
|
|
16
|
+
- `--fw-sky` → `--fw-info`, `--fw-sky-bg` → `--fw-info-bg`
|
|
17
|
+
- `--fw-on-brand` / `-soft` / `-fill` / `-line` → `--fw-on-secondary` / `-soft` / `-fill` / `-line`
|
|
18
|
+
- `--fw-pending-bg` → `--fw-warning-bg`
|
|
19
|
+
- `--fw-shadow-marigold` → `--fw-shadow-primary`, `--fw-shadow-meadow` → `--fw-shadow-success`
|
|
20
|
+
- `.fw-tag-plum` → `.fw-tag-secondary`
|
|
21
|
+
- `--fw-marigold-dk` split into two roles because it did two jobs under one name (button hover *and* the pending/waiting accent) — a consumer retheming Primary to blue no longer accidentally turns pending tags blue too.
|
|
22
|
+
- `src/tokens/tokens.json` schema changed: `color.light` / `color.dark` (and `shadow.light` / `shadow.dark`) collapsed into one entry per token, `{ value }` (same both themes) or `{ light, dark? }`. A value can reference another token in the same group with `{name}`.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **Derived tokens**: `--fw-primary-hover`, `--fw-primary-soft`, `--fw-secondary-hover`, `--fw-secondary-bg`, `--fw-success-bg`, `--fw-warning-bg`, `--fw-danger-bg`, `--fw-info-bg`, `--fw-on-secondary-soft/-fill/-line`, `--fw-focus-ring` and `--fw-shadow-primary/-success` are now computed from their seed colour with `color-mix()` instead of being separately hand-tuned hex/rgba values — override the seed (e.g. `--fw-primary`) and every derived token follows, in both themes, without needing to be redeclared.
|
|
26
|
+
- New `--fw-warning` / `--fw-warning-bg` tokens (the old `marigold-dk` pending accent, now independent of Primary).
|
|
27
|
+
- Generated `src/css/tokens.css` now wraps its rules in `@layer fw-tokens`, so a plain (unlayered) consumer override always wins over Fernwell's own `[data-theme="dark"]` re-declaration, regardless of selector specificity or source order — previously a `:root` override had to be repeated for `[data-theme="dark"]` or it was clobbered on theme toggle.
|
|
28
|
+
- `[data-theme="light"]` now also sets the light token block, so a light island inside a dark page (or vice versa) works.
|
|
29
|
+
- Runtime theming API on `theme`: `setTokens(tokens, { scope })`, `resetTokens(scope?)`, `getToken(name, el?)` — override tokens from JS (per-tenant branding, a user-picked accent), optionally scoped to a subtree or split by theme with `{ light, dark }`. Renders into one `<style data-fw-tokens>` that wins over both the `@layer fw-tokens` defaults and any static override.
|
|
30
|
+
- `docs/index.html`: new **Theming** section with a live palette editor (colour pickers for every seed role, wired to `setTokens`/`resetTokens`, with a live CSS preview) and a rewritten Color section showing role names alongside their default palette nicknames.
|
|
31
|
+
- Token lint (`npm run lint`) now fails on any retired pre-rename token or class name (`--fw-marigold`, `--fw-ink`, `.fw-tag-plum`, …), to guard against regressions.
|
|
32
|
+
|
|
33
|
+
## [0.1.0] — 2026-09-14
|
|
34
|
+
|
|
35
|
+
First standalone release. Extracted from the HunnyDo marketing site and app, where the system previously lived as two hand-synced copies.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- Design tokens as `--fw-*` custom properties (light + `[data-theme="dark"]`), generated from `src/tokens/tokens.json`, also shipped as `fernwell/tokens.json`.
|
|
39
|
+
- Components: button, icon button, card, tag/pill, eyebrow, field, checkbox/radio/toggle, combobox, alert, progress, spinner, nav, stat, empty state, modal, drawer, toast, list row, disclosure, pagination, layout helpers, opt-in `.fw-root` page defaults.
|
|
40
|
+
- JS enhancers (ESM, CJS, IIFE): `theme`, `combobox`, `modal` (modals + drawers), `nav`, `loading`, `toast`, and an idempotent `init()`.
|
|
41
|
+
- `docs/index.html` reference page.
|
|
42
|
+
- Token lint (`npm run lint`) that rejects raw colours, unprefixed variables and off-scale pixel values in component CSS.
|
|
43
|
+
|
|
44
|
+
### Changed (vs. the marketing/app copies)
|
|
45
|
+
- All tokens renamed with the `--fw-` prefix; `--white` → `--fw-surface`.
|
|
46
|
+
- All classes renamed with the `fw-` prefix; state classes are `is-*` (`.open` → `.is-open`, `.error` → `.is-error`, `.active` → `.is-active`).
|
|
47
|
+
- Checkbox checkmark is drawn with a CSS mask — no inline SVG needed in the markup.
|
|
48
|
+
- Modal and drawer are driven by `data-fw-open-modal` / `data-fw-open-drawer` and share one enhancer.
|
|
49
|
+
- New tokens: `--fw-focus-ring`, `--fw-border-w`, `--fw-motion-*`, `--fw-on-brand-*`, `--fw-overlay*`, `--fw-marigold-lt`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jacob Knaack
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# Fernwell
|
|
2
|
+
|
|
3
|
+
The design system behind [HunnyDo](https://hunnydo.cloud) — design tokens, CSS components, and a handful of small vanilla-JS enhancers. Framework-agnostic: works with a `<script>` tag, an npm bundler, or as plain CSS. No build step required on your side.
|
|
4
|
+
|
|
5
|
+
Warm, rounded, and legible — built to feel approachable to a first-time consumer trial and sturdy enough for the team running their business through it every day.
|
|
6
|
+
|
|
7
|
+
**Reference page:** [`docs/index.html`](docs/index.html) — every component, live, in both themes.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
### 1. `<script>` tag / CDN — no build step
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<link rel="stylesheet" href="https://unpkg.com/fernwell/dist/fernwell.min.css">
|
|
15
|
+
<script src="https://unpkg.com/fernwell/dist/fernwell.iife.js"></script>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This exposes `window.Fernwell` and calls `Fernwell.init()` on `DOMContentLoaded`. Add `data-fw-no-init` to the script tag to wire things up yourself.
|
|
19
|
+
|
|
20
|
+
### 2. npm + bundler
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install fernwell
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import 'fernwell/css'; // or 'fernwell/css/min'
|
|
28
|
+
import { init, toast, theme } from 'fernwell';
|
|
29
|
+
|
|
30
|
+
init(); // wires every data-fw-* hook on the page
|
|
31
|
+
toast.show('Invoice sent');
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
ESM and CommonJS are both provided; types are included.
|
|
35
|
+
|
|
36
|
+
### 3. CSS only
|
|
37
|
+
|
|
38
|
+
```css
|
|
39
|
+
@import 'fernwell/css'; /* tokens + base + every component */
|
|
40
|
+
@import 'fernwell/tokens.css'; /* just the custom properties */
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Fonts
|
|
44
|
+
|
|
45
|
+
Fernwell doesn't bundle fonts. Add the three faces (or self-host them) — the tokens carry system fallbacks either way:
|
|
46
|
+
|
|
47
|
+
```html
|
|
48
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
49
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
50
|
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700;800&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@500;600&display=swap" rel="stylesheet">
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Page defaults (optional)
|
|
54
|
+
|
|
55
|
+
Components never depend on global styles. If you *want* the system's page defaults — Background colour, Text colour, Inter body, display-font headings with zero margins — add `fw-root` to `<body>`:
|
|
56
|
+
|
|
57
|
+
```html
|
|
58
|
+
<body class="fw-root">
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The resets inside `.fw-root` use `:where()`, so any rule of yours wins.
|
|
62
|
+
|
|
63
|
+
## Dark theme
|
|
64
|
+
|
|
65
|
+
Set `data-theme="dark"` on `<html>` (or any ancestor) and every token flips. A `data-theme="light"` ancestor re-asserts light inside a dark page (a light card in a dark app, say). To remember the choice and honour the OS preference, drop a toggle anywhere:
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<button class="fw-btn fw-btn-ghost" data-fw-theme-toggle
|
|
69
|
+
data-fw-theme-label-light="🌙 Dark" data-fw-theme-label-dark="☀️ Light">🌙 Dark</button>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
To avoid a flash of the wrong theme, inline this in `<head>` before the stylesheet:
|
|
73
|
+
|
|
74
|
+
```html
|
|
75
|
+
<script>document.documentElement.dataset.theme=localStorage.getItem('fw-theme')||(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light')</script>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Dark mode is not a straight inversion: Primary stays the same hex in both themes by default, neutrals go to a near-black that keeps a trace of Secondary (never pure black), status colours lift a step lighter, and tinted backgrounds become low-opacity washes.
|
|
79
|
+
|
|
80
|
+
## Tokens
|
|
81
|
+
|
|
82
|
+
Every value in the system is a `--fw-*` custom property, named by **role** (`primary`, `success`, `text`…), not by the default palette (Marigold, Meadow, Ink…) — so retheming never means renaming. The machine-readable source is `fernwell/tokens.json`; a value can reference another token in the same group with `{name}`, resolved to `var(--fw-name)` when the CSS is generated.
|
|
83
|
+
|
|
84
|
+
### Seeds — override these
|
|
85
|
+
|
|
86
|
+
Declared literally per theme (or once, for the handful that don't flip). Overriding a seed reflows every derived token below it, in both themes:
|
|
87
|
+
|
|
88
|
+
```css
|
|
89
|
+
:root { --fw-primary: #2266ff; --fw-r-lg: 20px; }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Token | Light | Dark | Role |
|
|
93
|
+
| --- | --- | --- | --- |
|
|
94
|
+
| `--fw-text` | `#1F2430` | `#F2F1EA` | primary text |
|
|
95
|
+
| `--fw-text-soft` | `#565D6E` | `#9CA1B0` | secondary text — never pure gray |
|
|
96
|
+
| `--fw-bg` | `#F5F6F1` | `#14161C` | page background |
|
|
97
|
+
| `--fw-bg-dim` | `#EAEBE4` | `#2B2F3B` | dividers, input borders |
|
|
98
|
+
| `--fw-surface` | `#FFFFFF` | `#1D2029` | cards, panels |
|
|
99
|
+
| `--fw-primary` | `#FFC145` (Marigold) | same | primary brand / CTA |
|
|
100
|
+
| `--fw-secondary` | `#4C3A73` (Plum) | `#6B54A3` | secondary brand, nav |
|
|
101
|
+
| `--fw-success` | `#3FA672` (Meadow) | `#55D194` | success |
|
|
102
|
+
| `--fw-warning` | `#E8A521` | `#FFD166` | pending / waiting — independent of Primary |
|
|
103
|
+
| `--fw-danger` | `#E85B4F` (Coral) | `#FF8478` | error / danger |
|
|
104
|
+
| `--fw-info` | `#3E8FD0` (Sky) | `#6BB3E8` | info |
|
|
105
|
+
| `--fw-on-primary` | `#1F2430` | same | text on Primary surfaces; never flips |
|
|
106
|
+
| `--fw-on-secondary` | `#FFFFFF` | same | text on Secondary / brand surfaces (nav, footer, banners) |
|
|
107
|
+
| `--fw-overlay` / `--fw-overlay-soft` | text-tinted alphas | black alphas | modal / drawer backdrops |
|
|
108
|
+
| `--fw-shadow-sm` / `-md` / `-lg` | text-tinted | black | elevation |
|
|
109
|
+
| `--fw-font-display` / `-body` / `-mono` | Plus Jakarta Sans / Inter / IBM Plex Mono | | headlines / UI / numbers |
|
|
110
|
+
| `--fw-sp-1` … `--fw-sp-9` | `4 8 12 16 24 32 48 64 96` px | | 8pt spacing scale |
|
|
111
|
+
| `--fw-r-sm` / `-md` / `-lg` / `-xl` / `-full` | `10 16 24 32 999` px | | inputs / buttons / cards / modals / pills |
|
|
112
|
+
| `--fw-border-w` | `1.5px` | | the one border weight |
|
|
113
|
+
| `--fw-motion-fast` / `-base` / `-ease` | `120ms` / `180ms` / `ease` | | controls / panels |
|
|
114
|
+
|
|
115
|
+
### Derived — follow automatically
|
|
116
|
+
|
|
117
|
+
Computed from the seeds above with `color-mix()`. Never override these directly — override the seed they're built from instead:
|
|
118
|
+
|
|
119
|
+
| Token | Formula | Role |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| `--fw-primary-hover` | mix of Primary + Text | button hover / active — darkens in light, brightens in dark |
|
|
122
|
+
| `--fw-primary-soft` | mix of Primary + white | highlight (empty-state blob gradient) |
|
|
123
|
+
| `--fw-secondary-hover` | mix of Secondary + Text | secondary button hover |
|
|
124
|
+
| `--fw-secondary-bg` / `-success-bg` / `-warning-bg` / `-danger-bg` / `-info-bg` | mix of the role colour + Surface (light) / transparent (dark) | tag / alert / toast tints — opaque pastel in light, low-opacity wash in dark |
|
|
125
|
+
| `--fw-on-secondary-soft` / `-fill` / `-line` | Secondary-text alphas | secondary text / control fill / border on Secondary surfaces |
|
|
126
|
+
| `--fw-focus-ring` | `0 0 0 4px` mix of Primary + transparent | box-shadow focus ring |
|
|
127
|
+
| `--fw-shadow-primary` / `-success` | mix of the role colour + transparent | CTA / success glow |
|
|
128
|
+
|
|
129
|
+
## Theming
|
|
130
|
+
|
|
131
|
+
Override any **seed** token and every derived token follows automatically, in both themes — no need to fork the CSS or hand-tune the tokens it feeds.
|
|
132
|
+
|
|
133
|
+
**CSS — static.** Redeclare a seed after the stylesheet; it wins over `[data-theme="dark"]` too, so you don't need to repeat it per theme:
|
|
134
|
+
|
|
135
|
+
```css
|
|
136
|
+
:root { --fw-primary: #2266ff; }
|
|
137
|
+
/* only if you want a *different* value in dark: */
|
|
138
|
+
[data-theme="dark"] { --fw-primary: #5c9dff; }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This works because Fernwell's own token declarations live in `@layer fw-tokens` — an unlayered rule (yours, unless you also use `@layer`) always beats a layered one, regardless of selector specificity or source order. If your app declares its own CSS layers, list them (e.g. `@layer reset, base, components;`) before importing Fernwell's CSS so `fw-tokens` isn't implicitly nested inside one of them.
|
|
142
|
+
|
|
143
|
+
**JS — runtime.** For per-tenant branding, a user-picked accent, or a live preview:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
import { theme } from 'fernwell';
|
|
147
|
+
|
|
148
|
+
theme.setTokens({ primary: '#2266ff' }); // both themes
|
|
149
|
+
theme.setTokens({ light: { primary: '#2266ff' }, dark: { primary: '#5c9dff' } }); // per theme
|
|
150
|
+
theme.setTokens({ secondary: '#a33' }, { scope: '.tenant-acme' }); // one subtree
|
|
151
|
+
theme.getToken('primary'); // live resolved value, e.g. '#2266ff'
|
|
152
|
+
theme.resetTokens(); // clear every runtime override
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Runtime overrides render into one `<style data-fw-tokens>` appended to `<head>` and win over both the `@layer fw-tokens` defaults and any static override, so `setTokens` always has the last word.
|
|
156
|
+
|
|
157
|
+
**Browser support.** `color-mix()` and `@layer` need Chrome 111+, Safari 16.2+, Firefox 113+. There's no polyfill shipped — on an older browser the seed tokens still apply, but derived tokens fall back to whatever the browser does with an unsupported `color-mix()` value (typically the property's initial value), so hovers/tints/focus rings may go missing rather than mis-colour.
|
|
158
|
+
|
|
159
|
+
## Components
|
|
160
|
+
|
|
161
|
+
All class names are prefixed `fw-`; state classes are `is-*`. Each stylesheet in `src/css/components/` opens with the markup it expects.
|
|
162
|
+
|
|
163
|
+
| Component | Classes |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| Button | `.fw-btn` + `.fw-btn-primary` `-secondary` `-ghost` `-danger` `-lg`; `.fw-btn-loading`; `.fw-icon-btn` |
|
|
166
|
+
| Card | `.fw-card` |
|
|
167
|
+
| Tag | `.fw-tag` + `-live` `-pending` `-error` `-info` `-secondary`; `.fw-tag-pill` (compact, untilted) |
|
|
168
|
+
| Eyebrow | `.fw-eyebrow` |
|
|
169
|
+
| Field | `.fw-field` (`.fw-field-full`), `.fw-label`, `.fw-input`, `.fw-hint`, `.fw-error-msg`, `.fw-required`; `.is-error`, `.is-disabled` |
|
|
170
|
+
| Selection | `.fw-checkbox` / `.fw-radio` (`<input>` + `.fw-box`), `.fw-toggle` (`<input>` + `.fw-track > .fw-thumb`), `.fw-option-group` (`-inline`) |
|
|
171
|
+
| Combobox | `.fw-combobox` wrapper, `input[data-fw-combobox][list]` + `<datalist>` |
|
|
172
|
+
| Alert | `.fw-alert` + `-success` `-error` `-pending` `-info` `-with-action`; `.fw-alert-dot` `-title` `-body` |
|
|
173
|
+
| Progress | `.fw-progress` (`-sm`, `.is-indeterminate`, `.is-success`) > `.fw-progress-fill`; `.fw-progress-label` |
|
|
174
|
+
| Spinner | `.fw-spinner` + `-sm` `-md` `-lg` (colour from `currentColor`) |
|
|
175
|
+
| Nav | `.fw-nav` (`-sticky`), `-brand`, `-dot` / `-hex`, `-links`, `-right`, `-avatar`, `-btn`, `-toggle`, `-dropdown` (`-menu`) |
|
|
176
|
+
| Stat | `.fw-stat-label` `-value` (`-unit`) `-delta` (`.is-up` / `.is-down`) |
|
|
177
|
+
| Empty state | `.fw-empty`, `-blob`, `-title`, `-body` |
|
|
178
|
+
| Modal | `.fw-modal-overlay` > `.fw-modal` (`-wide`); `-head`, `-title`, `-sub`, `-foot` |
|
|
179
|
+
| Drawer | `.fw-drawer-overlay` + `.fw-drawer`; `-head`, `-eyebrow`, `-title`, `-body`, `-foot` |
|
|
180
|
+
| Toast | `.fw-toast` + `-error` `-info` `-pending` (created by `toast.show`) |
|
|
181
|
+
| List row | `.fw-list-row` (`.is-locked`), `-main`, `-title`, `-meta`; `.fw-sub-list` |
|
|
182
|
+
| Disclosure | `details.fw-disclosure`, `-chevron`, `-count`, `-body` |
|
|
183
|
+
| Pagination | `.fw-pagination`, `.fw-pag-info`, `.fw-pag-controls`, `.fw-pag-label`, `.fw-btn-page` |
|
|
184
|
+
| Layout | `.fw-wrap`, `.fw-row`, `.fw-grid-2`, `.fw-sr-only` |
|
|
185
|
+
|
|
186
|
+
## JavaScript
|
|
187
|
+
|
|
188
|
+
Everything is progressive enhancement over plain HTML. `init(root?)` is idempotent — call it again after injecting markup (HTMX, Turbo, fetch-and-insert) and only new elements get bound.
|
|
189
|
+
|
|
190
|
+
| Hook | What it does |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| `[data-fw-theme-toggle]` | flips `data-theme`, persists to `localStorage['fw-theme']`, sets `aria-pressed` |
|
|
193
|
+
| `input[data-fw-combobox]` | typeahead over the `<datalist>` in `list=` (↑ ↓ Enter Esc, `aria-activedescendant`) |
|
|
194
|
+
| `[data-fw-open-modal="id"]` / `[data-fw-close-modal]` | modal with backdrop-click (drag-safe), Escape, focus return, scroll lock |
|
|
195
|
+
| `[data-fw-open-drawer="id"]` / `[data-fw-close-drawer]` / `[data-fw-drawer-overlay="id"]` | same for drawers |
|
|
196
|
+
| `[data-fw-nav-toggle][aria-controls]` | dropdown open/close, outside-click and Escape |
|
|
197
|
+
| `form[data-fw-loading]` | submit button gets a spinner + `data-fw-loading-text` label on submit |
|
|
198
|
+
|
|
199
|
+
Programmatic API:
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
import { theme, combobox, modal, nav, loading, toast } from 'fernwell';
|
|
203
|
+
|
|
204
|
+
theme.get();
|
|
205
|
+
theme.set('dark');
|
|
206
|
+
theme.toggle();
|
|
207
|
+
theme.setTokens({ primary: '#2266ff' }); theme.resetTokens(); theme.getToken('primary'); // see Theming
|
|
208
|
+
|
|
209
|
+
const cb = combobox(inputEl,
|
|
210
|
+
{
|
|
211
|
+
options: ['Seattle', 'Portland'],
|
|
212
|
+
onSelect(v) {}
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
cb.setOptions([...]);
|
|
216
|
+
cb.destroy();
|
|
217
|
+
modal.open('confirm');
|
|
218
|
+
modal.close('confirm'); // works for drawers too
|
|
219
|
+
loading.setButtonLoading(btn, 'Sending…');
|
|
220
|
+
loading.resetButtonLoading(btn);
|
|
221
|
+
toast.show('That card was declined', { variant: 'error', duration: 6000 });
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Events: `fw:themechange` on `document` (detail: `'light' | 'dark'`), `fw:open` / `fw:close` bubbling from the modal or drawer.
|
|
225
|
+
|
|
226
|
+
## Principles
|
|
227
|
+
|
|
228
|
+
**Colour.** Primary (Marigold by default) carries the energy — one action per view, never decoration. Secondary (Plum) grounds navigation and headers. Text and Background do 90% of the work. Status colours (Success/Meadow, Warning, Danger/Coral, Info/Sky) each pair a saturated foreground with a soft tint so status is never confusable with a CTA. All of it is retheme-able — see [Theming](#theming).
|
|
229
|
+
|
|
230
|
+
**Type.** Plus Jakarta Sans for anything that should feel like a voice; Inter for anything read quickly and often; Plex Mono for numbers and IDs.
|
|
231
|
+
|
|
232
|
+
**Shape.** One rounding scale: the bigger the surface, the softer the corner. Inputs 10 · buttons 16 · cards 24 · modals 32.
|
|
233
|
+
|
|
234
|
+
**Motion.** 120ms ease and a 1px lift on controls, no bounce. 180ms fade + rise for panels, no scale-up. A ~300ms checkmark draw-in is the single expressive exception. `prefers-reduced-motion` drops every transform to an opacity fade.
|
|
235
|
+
|
|
236
|
+
**Voice.** Buttons name the action ("Send invoice", not "Submit"). Errors say what happened and what to do next, without apologising. Empty states are invitations. Confirmations echo the verb on the button.
|
|
237
|
+
|
|
238
|
+
**Icons.** Outline only, 16–20px box, 1.3–1.4px stroke, round caps and joins, `currentColor`. No fills — except a checkmark on a completed item.
|
|
239
|
+
|
|
240
|
+
## Development
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
npm install
|
|
244
|
+
npm run build # tokens.json → tokens.css, lint, esbuild → dist/, tsc → .d.ts
|
|
245
|
+
npm run lint # fails on raw hex / rgb / unprefixed vars / off-scale px in component CSS
|
|
246
|
+
npm run docs # serve docs/ (self-contained: build copies dist/ into docs/dist/)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`src/tokens/tokens.json` is the only place a value is defined; `src/css/tokens.css` is generated from it.
|
|
250
|
+
|
|
251
|
+
## Versioning
|
|
252
|
+
|
|
253
|
+
Semantic versioning, strictly: **major** for any rename or removal of a token, class, data attribute, or JS export; **minor** for additions; **patch** for fixes. Fernwell is `0.x` until HunnyDo's app and marketing site both consume it — expect breaking changes in minors until `1.0.0`.
|
|
254
|
+
|
|
255
|
+
**Deprecation policy (from 1.0):** a token or class slated for removal keeps working for at least one minor release, with a `/* @deprecated */` note in the CSS and an entry in the changelog naming the replacement. It's removed in the next major.
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
MIT
|
|
260
|
+
|
|
261
|
+
## Docs deployment
|
|
262
|
+
|
|
263
|
+
`docs/` is published to GitHub Pages by [`.github/workflows/deploy-docs.yml`](.github/workflows/deploy-docs.yml) on every push to `main`. Because `docs/dist/` is generated (and gitignored), the workflow runs `npm run build` first and uploads the resulting `docs/` folder as the Pages artifact — so the deployed page always matches the built package.
|
|
264
|
+
|
|
265
|
+
One-time setup in the GitHub repo: **Settings → Pages → Source → "GitHub Actions"** (not "Deploy from a branch").
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combobox / typeahead — progressive enhancement over an <input> + <datalist>.
|
|
3
|
+
*
|
|
4
|
+
* <div class="fw-combobox">
|
|
5
|
+
* <input class="fw-input" data-fw-combobox list="cities" placeholder="Start typing…">
|
|
6
|
+
* <datalist id="cities"><option value="Seattle"></option>…</datalist>
|
|
7
|
+
* </div>
|
|
8
|
+
*
|
|
9
|
+
* Or programmatically: combobox(input, { options: ['Seattle', …], onSelect })
|
|
10
|
+
*/
|
|
11
|
+
export interface ComboboxOptions {
|
|
12
|
+
/** Static option list. Defaults to the <datalist> the input's `list` attribute points at. */
|
|
13
|
+
options?: string[];
|
|
14
|
+
/** Filter function; default is case-insensitive substring match. */
|
|
15
|
+
filter?: (option: string, query: string) => boolean;
|
|
16
|
+
/** Text shown when nothing matches. */
|
|
17
|
+
emptyText?: string;
|
|
18
|
+
onSelect?: (value: string) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface ComboboxInstance {
|
|
21
|
+
open(): void;
|
|
22
|
+
close(): void;
|
|
23
|
+
setOptions(options: string[]): void;
|
|
24
|
+
destroy(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function combobox(input: HTMLInputElement, opts?: ComboboxOptions): ComboboxInstance;
|
|
27
|
+
/** Enhance every `[data-fw-combobox]` input under `root`. */
|
|
28
|
+
export declare function init(root?: ParentNode): void;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/*! fernwell v0.1.0 | MIT | https://github.com/JacobKnaack/fernwell */
|
|
2
|
+
"use strict";var q=Object.defineProperty;var Oe=Object.getOwnPropertyDescriptor;var Se=Object.getOwnPropertyNames;var Be=Object.prototype.hasOwnProperty;var m=(e,t)=>{for(var n in t)q(e,n,{get:t[n],enumerable:!0})},Ce=(e,t,n,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of Se(t))!Be.call(e,s)&&s!==n&&q(e,s,{get:()=>t[s],enumerable:!(o=Oe(t,s))||o.enumerable});return e};var Ne=e=>Ce(q({},"__esModule",{value:!0}),e);var Ye={};m(Ye,{combobox:()=>A,default:()=>Ue,init:()=>Me,loading:()=>C,modal:()=>O,nav:()=>B,theme:()=>H,toast:()=>N,version:()=>ye});module.exports=Ne(Ye);var H={};m(H,{get:()=>P,getToken:()=>De,init:()=>D,resetTokens:()=>Pe,set:()=>M,setTokens:()=>je,toggle:()=>fe});var I="fw-theme",$e="fw:themechange",qe="--fw-",ce="data-fw-tokens";function j(){try{return window.localStorage}catch{return null}}function P(){var t;let e=document.documentElement.getAttribute("data-theme");return e==="dark"||e==="light"?e:(t=window.matchMedia)!=null&&t.call(window,"(prefers-color-scheme: dark)").matches?"dark":"light"}function M(e,{persist:t=!0}={}){var n;document.documentElement.setAttribute("data-theme",e),t&&((n=j())==null||n.setItem(I,e)),document.querySelectorAll("[data-fw-theme-toggle]").forEach(o=>me(o,e)),document.dispatchEvent(new CustomEvent($e,{detail:e}))}function fe(){let e=P()==="dark"?"light":"dark";return M(e),e}function me(e,t){e.setAttribute("aria-pressed",String(t==="dark"));let n=t==="dark"?e.dataset.fwThemeLabelDark:e.dataset.fwThemeLabelLight;n!==void 0&&(e.textContent=n)}function D(e=document){var o,s,i,a;let t=(o=j())==null?void 0:o.getItem(I),n=t==="dark"||t==="light"?t:P();M(n,{persist:!1}),e.querySelectorAll("[data-fw-theme-toggle]").forEach(u=>{u.dataset.fwBound||(u.dataset.fwBound="1",me(u,n),u.addEventListener("click",()=>fe()))}),(a=(s=window.matchMedia)==null?void 0:(i=s.call(window,"(prefers-color-scheme: dark)")).addEventListener)==null||a.call(i,"change",u=>{var w;(w=j())!=null&&w.getItem(I)||M(u.matches?"dark":"light",{persist:!1})})}var L=new Map;function pe(e){return e.startsWith("--")?e:`${qe}${e}`}function Ie(e){return Object.values(e).some(t=>t!==null&&typeof t=="object")}function de(e){return Object.entries(e).map(([t,n])=>`${pe(t)}:${n};`).join("")}function ue(e){let t=document.head.querySelector(`style[${ce}]`);if(t||!e)return t;let n=document.createElement("style");return n.setAttribute(ce,""),document.head.appendChild(n),n}function ve(){var t;if(L.size===0){(t=ue(!1))==null||t.remove();return}let e=[];L.forEach(({light:n,dark:o},s)=>{if(Object.keys(n).length&&e.push(`${s}{${de(n)}}`),Object.keys(o).length){let i=s===":root"?'[data-theme="dark"]':`[data-theme="dark"] ${s}, ${s}[data-theme="dark"]`;e.push(`${i}{${de(o)}}`)}}),ue(!0).textContent=e.join(`
|
|
3
|
+
`)}function je(e,t={}){var s,i;let n=(s=t.scope)!=null?s:":root",o=(i=L.get(n))!=null?i:{light:{},dark:{}};Ie(e)?(Object.assign(o.light,e.light),Object.assign(o.dark,e.dark)):(Object.assign(o.light,e),Object.assign(o.dark,e)),L.set(n,o),ve()}function Pe(e){e?L.delete(e):L.clear(),ve()}function De(e,t=document.documentElement){return getComputedStyle(t).getPropertyValue(pe(e)).trim()}var A={};m(A,{combobox:()=>Ee,init:()=>F});var Fe=0;function x(e){return e.replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t])}function Ee(e,t={}){var re,ie,ae,le;let n=(re=t.filter)!=null?re:((r,l)=>r.toLowerCase().includes(l)),o=(ie=t.emptyText)!=null?ie:"No matches",s=(ae=t.options)!=null?ae:Ke(e),i=[],a=-1,u=!1,w=e.getAttribute("list");w&&(e.dataset.fwList=w,e.removeAttribute("list"));let J=(le=e.closest(".fw-combobox"))!=null?le:Ve(e),c=document.createElement("ul"),$=`fw-combobox-${++Fe}`;c.className="fw-combobox-list",c.id=$,c.setAttribute("role","listbox"),J.appendChild(c),e.setAttribute("role","combobox"),e.setAttribute("aria-autocomplete","list"),e.setAttribute("aria-expanded","false"),e.setAttribute("aria-controls",$),e.autocomplete="off";function He(r){i=r,a=-1;let l=e.value.trim().toLowerCase();if(r.length===0){c.innerHTML=`<li class="fw-combobox-empty">${x(o)}</li>`;return}c.innerHTML=r.map((f,b)=>{let g=l?f.toLowerCase().indexOf(l):-1,Ae=g>-1?`${x(f.slice(0,g))}<b>${x(f.slice(g,g+l.length))}</b>${x(f.slice(g+l.length))}`:x(f);return`<li class="fw-combobox-option" role="option" id="${$}-${b}" data-index="${b}" aria-selected="false">${Ae}</li>`}).join("")}function h(){let r=e.value.trim().toLowerCase();He(r?s.filter(l=>n(l,r)):s),c.classList.add("is-open"),e.setAttribute("aria-expanded","true")}function y(){c.classList.remove("is-open"),e.setAttribute("aria-expanded","false"),e.removeAttribute("aria-activedescendant"),a=-1}function Q(r){let l=c.querySelectorAll(".fw-combobox-option");l.forEach(b=>{b.classList.remove("is-active"),b.setAttribute("aria-selected","false")});let f=l[r];f&&(f.classList.add("is-active"),f.setAttribute("aria-selected","true"),f.scrollIntoView({block:"nearest"}),e.setAttribute("aria-activedescendant",f.id)),a=r}function Z(r){var l;e.value=r,y(),u=!0,e.focus(),e.dispatchEvent(new Event("input",{bubbles:!0})),e.dispatchEvent(new Event("change",{bubbles:!0})),u=!1,(l=t.onSelect)==null||l.call(t,r)}let ee=()=>{u||h()},te=()=>{u||h()},ne=r=>{r.key==="ArrowDown"?(r.preventDefault(),c.classList.contains("is-open")||h(),Q(Math.min(a+1,i.length-1))):r.key==="ArrowUp"?(r.preventDefault(),Q(Math.max(a-1,0))):r.key==="Enter"?a>-1&&i[a]!==void 0&&(r.preventDefault(),Z(i[a])):r.key==="Escape"&&y()},oe=r=>{let l=r.target.closest(".fw-combobox-option");l&&(r.preventDefault(),Z(i[Number(l.dataset.index)]))},se=r=>{J.contains(r.target)||y()};return e.addEventListener("input",ee),e.addEventListener("focus",te),e.addEventListener("keydown",ne),c.addEventListener("mousedown",oe),document.addEventListener("click",se),{open:h,close:y,setOptions(r){s=r,c.classList.contains("is-open")&&h()},destroy(){e.removeEventListener("input",ee),e.removeEventListener("focus",te),e.removeEventListener("keydown",ne),c.removeEventListener("mousedown",oe),document.removeEventListener("click",se),c.remove(),e.dataset.fwList&&e.setAttribute("list",e.dataset.fwList),delete e.dataset.fwBound}}}function Ke(e){var o;let t=(o=e.getAttribute("list"))!=null?o:e.dataset.fwList,n=t?document.getElementById(t):null;return n?Array.from(n.querySelectorAll("option")).map(s=>s.value||s.textContent||""):[]}function Ve(e){var n;let t=document.createElement("div");return t.className="fw-combobox",(n=e.parentNode)==null||n.insertBefore(t,e),t.appendChild(e),t}function F(e=document){e.querySelectorAll("input[data-fw-combobox]").forEach(t=>{t.dataset.fwBound||(t.dataset.fwBound="1",Ee(t))})}var O={};m(O,{bindOutsideClickToClose:()=>V,close:()=>p,init:()=>z,open:()=>W,toggle:()=>_e});var v="is-open",Te="fw-scroll-lock",E=[],K=new WeakMap,Re='a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';function R(e){return typeof e=="string"?document.getElementById(e):e}function _(e){if(e.classList.contains("fw-modal-overlay"))return e;let t=e.id?document.querySelector(`[data-fw-drawer-overlay="${e.id}"]`):null;if(t)return t;let n=e.previousElementSibling;return n instanceof HTMLElement&&n.classList.contains("fw-drawer-overlay")?n:null}function W(e,t=(n=>(n=document.activeElement)!=null?n:null)()){var i;let o=R(e);if(!o||o.classList.contains(v))return;o.classList.add(v),(i=_(o))==null||i.classList.add(v),K.set(o,t),E.push(o),document.body.classList.add(Te),o.dispatchEvent(new CustomEvent("fw:open",{bubbles:!0}));let s=o.querySelector(Re);s?s.focus({preventScroll:!0}):(o.hasAttribute("tabindex")||o.setAttribute("tabindex","-1"),o.focus({preventScroll:!0}))}function p(e){var o,s,i;let t=R(e);if(!t||!t.classList.contains(v))return;t.classList.remove(v),(o=_(t))==null||o.classList.remove(v);let n=E.indexOf(t);n>-1&&E.splice(n,1),E.length===0&&document.body.classList.remove(Te),t.dispatchEvent(new CustomEvent("fw:close",{bubbles:!0})),(i=(s=K.get(t))==null?void 0:s.focus)==null||i.call(s,{preventScroll:!0}),K.delete(t)}function _e(e){let t=R(e);t&&(t.classList.contains(v)?p:W)(t)}function V(e,t){let n=!1;e.addEventListener("mousedown",o=>{n=o.target===e}),e.addEventListener("click",o=>{o.target===e&&n&&t()})}var Le=!1;function z(e=document){e.querySelectorAll("[data-fw-open-modal], [data-fw-open-drawer]").forEach(t=>{var o,s;if(t.dataset.fwBound)return;t.dataset.fwBound="1";let n=(s=(o=t.dataset.fwOpenModal)!=null?o:t.dataset.fwOpenDrawer)!=null?s:"";t.addEventListener("click",()=>W(n,t))}),e.querySelectorAll(".fw-modal-overlay").forEach(t=>{t.dataset.fwBound||(t.dataset.fwBound="1",V(t,()=>p(t)),t.querySelectorAll("[data-fw-close-modal]").forEach(n=>n.addEventListener("click",()=>p(t))))}),e.querySelectorAll(".fw-drawer").forEach(t=>{if(t.dataset.fwBound)return;t.dataset.fwBound="1";let n=_(t);n&&V(n,()=>p(t)),t.querySelectorAll("[data-fw-close-drawer]").forEach(o=>o.addEventListener("click",()=>p(t)))}),Le||(Le=!0,document.addEventListener("keydown",t=>{t.key==="Escape"&&E.length&&p(E[E.length-1])}))}var B={};m(B,{close:()=>S,init:()=>Y,open:()=>we});var G="is-open";function we(e){let t=U(e);t&&(t.classList.add(G),e.setAttribute("aria-expanded","true"))}function S(e){let t=U(e);t&&(t.classList.remove(G),e.setAttribute("aria-expanded","false"))}function U(e){let t=e.getAttribute("aria-controls");return t?document.getElementById(t):null}function Y(e=document){e.querySelectorAll("[data-fw-nav-toggle]").forEach(t=>{var s,i;if(t.dataset.fwBound)return;t.dataset.fwBound="1";let n=U(t);if(!n)return;let o=(i=(s=t.closest(".fw-nav"))!=null?s:t.parentElement)!=null?i:t;t.addEventListener("click",a=>{a.stopPropagation(),(n.classList.contains(G)?S:we)(t)}),document.addEventListener("click",a=>{!o.contains(a.target)&&!n.contains(a.target)&&S(t)}),document.addEventListener("keydown",a=>{a.key==="Escape"&&S(t)})})}var C={};m(C,{init:()=>X,resetAll:()=>xe,resetButtonLoading:()=>ge,setButtonLoading:()=>be});var k="fw-btn-loading";function be(e,t){var o,s;if(!e||e.classList.contains(k))return;e.dataset.fwOriginalHtml=e.innerHTML,e.disabled=!0,e.setAttribute("aria-busy","true"),e.classList.add(k);let n=(s=(o=t!=null?t:e.dataset.fwLoadingText)!=null?o:e.textContent)!=null?s:"";e.innerHTML='<span class="fw-spinner fw-spinner-sm" aria-hidden="true"></span>',e.appendChild(document.createTextNode(n))}function ge(e){!e||!e.classList.contains(k)||(e.disabled=!1,e.removeAttribute("aria-busy"),e.classList.remove(k),e.dataset.fwOriginalHtml!==void 0&&(e.innerHTML=e.dataset.fwOriginalHtml,delete e.dataset.fwOriginalHtml))}function xe(e=document){e.querySelectorAll(`.${k}`).forEach(ge)}var he=!1;function X(e=document){e.querySelectorAll("form[data-fw-loading]").forEach(t=>{t.dataset.fwBound||(t.dataset.fwBound="1",t.addEventListener("submit",()=>{let n=t.querySelector('button[type="submit"], button:not([type])');be(n)}))}),he||(he=!0,window.addEventListener("pageshow",t=>{t.persisted&&xe()}))}var N={};m(N,{hide:()=>ke,show:()=>ze});var d=null,T=null;function We(){return d&&d.isConnected||(d=document.createElement("div"),d.className="fw-toast",d.setAttribute("role","status"),d.setAttribute("aria-live","polite"),d.innerHTML='<span class="fw-toast-dot" aria-hidden="true"></span><span class="fw-toast-text"></span>',document.body.appendChild(d)),d}function ze(e,{variant:t="success",duration:n=4e3}={}){let o=We();return o.className=`fw-toast${t==="success"?"":` fw-toast-${t}`}`,o.querySelector(".fw-toast-text").textContent=e,T&&clearTimeout(T),o.classList.remove("is-show"),o.offsetWidth,o.classList.add("is-show"),n>0&&(T=setTimeout(ke,n)),o}function ke(){d==null||d.classList.remove("is-show"),T&&clearTimeout(T),T=null}var ye="0.1.0";function Me(e=document){D(e),F(e),z(e),Y(e),X(e)}var Ge={version:ye,init:Me,theme:H,combobox:A,modal:O,nav:B,loading:C,toast:N},Ue=Ge;
|
|
4
|
+
//# sourceMappingURL=fernwell.cjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/js/index.ts", "../src/js/theme.ts", "../src/js/combobox.ts", "../src/js/modal.ts", "../src/js/nav.ts", "../src/js/loading.ts", "../src/js/toast.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Fernwell \u2014 vanilla JS enhancers for the Fernwell design system.\n *\n * ESM / CJS: import { init, theme, toast } from 'fernwell'; init();\n * <script>: <script src=\"https://unpkg.com/fernwell/dist/fernwell.iife.js\"></script>\n * \u2192 window.Fernwell, auto-initialised on DOMContentLoaded\n * (add data-fw-no-init to the <script> tag to opt out).\n */\nimport * as theme from './theme';\nimport * as combobox from './combobox';\nimport * as modal from './modal';\nimport * as nav from './nav';\nimport * as loading from './loading';\nimport * as toast from './toast';\n\nexport { theme, combobox, modal, nav, loading, toast };\nexport type { Theme, TokenOverrides, TokenThemeOverrides } from './theme';\nexport type { ComboboxOptions, ComboboxInstance } from './combobox';\nexport type { ToastVariant, ToastOptions } from './toast';\n\nexport const version = \"0.1.0\";\n\n/**\n * Wire every `data-fw-*` hook under `root`. Safe to call more than once \u2014\n * already-bound elements are skipped \u2014 so call it again after injecting\n * markup (e.g. after an HTMX / Turbo swap).\n */\nexport function init(root: ParentNode = document): void {\n theme.init(root);\n combobox.init(root);\n modal.init(root);\n nav.init(root);\n loading.init(root);\n}\n\nconst Fernwell = { version, init, theme, combobox, modal, nav, loading, toast };\nexport default Fernwell;\n", "/**\n * Theme \u2014 toggles `data-theme=\"dark\"` on <html>, remembers the choice, and\n * falls back to the OS preference. Also carries the runtime token API\n * (`setTokens` / `resetTokens` / `getToken`) for retheming Fernwell's colour\n * tokens on the fly \u2014 per-tenant branding, a user-picked accent, a live\n * preview \u2014 without touching a stylesheet.\n *\n * <button class=\"fw-btn fw-btn-ghost\" data-fw-theme-toggle\n * data-fw-theme-label-dark=\"\u2600\uFE0F Light\" data-fw-theme-label-light=\"\uD83C\uDF19 Dark\">\uD83C\uDF19 Dark</button>\n *\n * To avoid a flash of the wrong theme on load, inline this before your CSS:\n * <script>document.documentElement.dataset.theme=localStorage.getItem('fw-theme')||(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light')</script>\n */\nexport type Theme = 'light' | 'dark';\n\nconst STORAGE_KEY = 'fw-theme';\nconst EVENT = 'fw:themechange';\nconst TOKEN_PREFIX = '--fw-';\nconst TOKEN_STYLE_ATTR = 'data-fw-tokens';\n\nfunction store(): Storage | null {\n try {\n return window.localStorage;\n } catch {\n return null;\n }\n}\n\n/** The theme currently applied to <html>, or the system preference if none is set. */\nexport function get(): Theme {\n const attr = document.documentElement.getAttribute('data-theme');\n if (attr === 'dark' || attr === 'light') return attr;\n return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n}\n\n/** Apply a theme, persist it, and update every toggle button on the page. */\nexport function set(theme: Theme, { persist = true }: { persist?: boolean } = {}): void {\n document.documentElement.setAttribute('data-theme', theme);\n if (persist) store()?.setItem(STORAGE_KEY, theme);\n document.querySelectorAll<HTMLElement>('[data-fw-theme-toggle]').forEach((btn) => syncToggle(btn, theme));\n document.dispatchEvent(new CustomEvent<Theme>(EVENT, { detail: theme }));\n}\n\nexport function toggle(): Theme {\n const next: Theme = get() === 'dark' ? 'light' : 'dark';\n set(next);\n return next;\n}\n\nfunction syncToggle(btn: HTMLElement, theme: Theme): void {\n btn.setAttribute('aria-pressed', String(theme === 'dark'));\n const label = theme === 'dark' ? btn.dataset.fwThemeLabelDark : btn.dataset.fwThemeLabelLight;\n if (label !== undefined) btn.textContent = label;\n}\n\n/** Apply the stored / system theme and wire every `[data-fw-theme-toggle]` under `root`. */\nexport function init(root: ParentNode = document): void {\n const stored = store()?.getItem(STORAGE_KEY);\n const current: Theme = stored === 'dark' || stored === 'light' ? stored : get();\n set(current, { persist: false });\n\n root.querySelectorAll<HTMLElement>('[data-fw-theme-toggle]').forEach((btn) => {\n if (btn.dataset.fwBound) return;\n btn.dataset.fwBound = '1';\n syncToggle(btn, current);\n btn.addEventListener('click', () => toggle());\n });\n\n // Follow the OS only while the user hasn't chosen explicitly.\n window.matchMedia?.('(prefers-color-scheme: dark)').addEventListener?.('change', (e) => {\n if (!store()?.getItem(STORAGE_KEY)) set(e.matches ? 'dark' : 'light', { persist: false });\n });\n}\n\n/* ---------------------------------------------------------------------- *\n * Runtime tokens \u2014 override any --fw-* custom property from JS, in both\n * themes or per-theme, scoped to :root or to any selector. Overrides render\n * into one <style data-fw-tokens> appended to <head>: being unlayered and\n * last, it wins over Fernwell's own @layer fw-tokens *and* any earlier\n * static override in the consumer's stylesheet.\n * ---------------------------------------------------------------------- */\n\n/** Token name \u2192 CSS value. Bare names (`primary`) and full names (`--fw-primary`) both work. */\nexport type TokenOverrides = Record<string, string>;\n\n/** Split form: set different values per theme instead of both at once. */\nexport interface TokenThemeOverrides {\n light?: TokenOverrides;\n dark?: TokenOverrides;\n}\n\ninterface ScopeTokens {\n light: TokenOverrides;\n dark: TokenOverrides;\n}\n\nconst tokenScopes = new Map<string, ScopeTokens>();\n\nfunction normalizeTokenName(name: string): string {\n return name.startsWith('--') ? name : `${TOKEN_PREFIX}${name}`;\n}\n\nfunction isThemeSplit(tokens: TokenOverrides | TokenThemeOverrides): tokens is TokenThemeOverrides {\n // A token's own value is always a CSS string \u2014 so any object-valued entry\n // means this is the { light, dark } split form, not a flat token map.\n return Object.values(tokens).some((v) => v !== null && typeof v === 'object');\n}\n\nfunction declBlock(tokens: TokenOverrides): string {\n return Object.entries(tokens)\n .map(([name, value]) => `${normalizeTokenName(name)}:${value};`)\n .join('');\n}\n\nfunction tokenStyleEl(create: boolean): HTMLStyleElement | null {\n const existing = document.head.querySelector<HTMLStyleElement>(`style[${TOKEN_STYLE_ATTR}]`);\n if (existing || !create) return existing;\n const el = document.createElement('style');\n el.setAttribute(TOKEN_STYLE_ATTR, '');\n document.head.appendChild(el);\n return el;\n}\n\nfunction renderTokens(): void {\n if (tokenScopes.size === 0) {\n tokenStyleEl(false)?.remove();\n return;\n }\n const rules: string[] = [];\n tokenScopes.forEach(({ light, dark }, scope) => {\n if (Object.keys(light).length) rules.push(`${scope}{${declBlock(light)}}`);\n if (Object.keys(dark).length) {\n const darkSelector =\n scope === ':root' ? '[data-theme=\"dark\"]' : `[data-theme=\"dark\"] ${scope}, ${scope}[data-theme=\"dark\"]`;\n rules.push(`${darkSelector}{${declBlock(dark)}}`);\n }\n });\n tokenStyleEl(true)!.textContent = rules.join('\\n');\n}\n\n/**\n * Override Fernwell design tokens at runtime \u2014 every derived token (hovers,\n * tints, focus ring, glows) follows automatically, in both themes:\n *\n * theme.setTokens({ primary: '#2266ff' });\n *\n * Pass `{ light, dark }` to set different values per theme, and `scope` to\n * theme a subtree instead of the whole page:\n *\n * theme.setTokens({ light: { primary: '#2266ff' }, dark: { primary: '#5c9dff' } });\n * theme.setTokens({ secondary: '#a33' }, { scope: '.tenant-acme' });\n *\n * Calls accumulate (merge into whatever overrides are already set for that\n * scope) \u2014 call `resetTokens()` to clear them.\n */\nexport function setTokens(tokens: TokenOverrides | TokenThemeOverrides, opts: { scope?: string } = {}): void {\n const scope = opts.scope ?? ':root';\n const entry = tokenScopes.get(scope) ?? { light: {}, dark: {} };\n if (isThemeSplit(tokens)) {\n Object.assign(entry.light, tokens.light);\n Object.assign(entry.dark, tokens.dark);\n } else {\n Object.assign(entry.light, tokens);\n Object.assign(entry.dark, tokens);\n }\n tokenScopes.set(scope, entry);\n renderTokens();\n}\n\n/** Clear runtime token overrides \u2014 for one `scope`, or every scope if omitted. */\nexport function resetTokens(scope?: string): void {\n if (scope) tokenScopes.delete(scope);\n else tokenScopes.clear();\n renderTokens();\n}\n\n/** The live, resolved value of a token (e.g. `getToken('primary')` \u2192 `'#ffc145'`), including runtime overrides. */\nexport function getToken(name: string, el: Element = document.documentElement): string {\n return getComputedStyle(el).getPropertyValue(normalizeTokenName(name)).trim();\n}\n", "/**\n * Combobox / typeahead \u2014 progressive enhancement over an <input> + <datalist>.\n *\n * <div class=\"fw-combobox\">\n * <input class=\"fw-input\" data-fw-combobox list=\"cities\" placeholder=\"Start typing\u2026\">\n * <datalist id=\"cities\"><option value=\"Seattle\"></option>\u2026</datalist>\n * </div>\n *\n * Or programmatically: combobox(input, { options: ['Seattle', \u2026], onSelect })\n */\nexport interface ComboboxOptions {\n /** Static option list. Defaults to the <datalist> the input's `list` attribute points at. */\n options?: string[];\n /** Filter function; default is case-insensitive substring match. */\n filter?: (option: string, query: string) => boolean;\n /** Text shown when nothing matches. */\n emptyText?: string;\n onSelect?: (value: string) => void;\n}\n\nexport interface ComboboxInstance {\n open(): void;\n close(): void;\n setOptions(options: string[]): void;\n destroy(): void;\n}\n\nlet uid = 0;\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' })[c] as string);\n}\n\nexport function combobox(input: HTMLInputElement, opts: ComboboxOptions = {}): ComboboxInstance {\n const filter = opts.filter ?? ((o, q) => o.toLowerCase().includes(q));\n const emptyText = opts.emptyText ?? 'No matches';\n let options = opts.options ?? readDatalist(input);\n let current: string[] = [];\n let activeIndex = -1;\n let selecting = false;\n\n // The native datalist would double up with our list \u2014 detach it but keep\n // the options readable for setOptions()/no-JS fallbacks.\n const listId = input.getAttribute('list');\n if (listId) {\n input.dataset.fwList = listId;\n input.removeAttribute('list');\n }\n\n const wrap = input.closest('.fw-combobox') ?? wrapInput(input);\n const list = document.createElement('ul');\n const id = `fw-combobox-${++uid}`;\n list.className = 'fw-combobox-list';\n list.id = id;\n list.setAttribute('role', 'listbox');\n wrap.appendChild(list);\n\n input.setAttribute('role', 'combobox');\n input.setAttribute('aria-autocomplete', 'list');\n input.setAttribute('aria-expanded', 'false');\n input.setAttribute('aria-controls', id);\n input.autocomplete = 'off';\n\n function render(matches: string[]): void {\n current = matches;\n activeIndex = -1;\n const q = input.value.trim().toLowerCase();\n if (matches.length === 0) {\n list.innerHTML = `<li class=\"fw-combobox-empty\">${escapeHtml(emptyText)}</li>`;\n return;\n }\n list.innerHTML = matches\n .map((opt, i) => {\n const idx = q ? opt.toLowerCase().indexOf(q) : -1;\n const label =\n idx > -1\n ? `${escapeHtml(opt.slice(0, idx))}<b>${escapeHtml(opt.slice(idx, idx + q.length))}</b>${escapeHtml(opt.slice(idx + q.length))}`\n : escapeHtml(opt);\n return `<li class=\"fw-combobox-option\" role=\"option\" id=\"${id}-${i}\" data-index=\"${i}\" aria-selected=\"false\">${label}</li>`;\n })\n .join('');\n }\n\n function open(): void {\n const q = input.value.trim().toLowerCase();\n render(q ? options.filter((o) => filter(o, q)) : options);\n list.classList.add('is-open');\n input.setAttribute('aria-expanded', 'true');\n }\n\n function close(): void {\n list.classList.remove('is-open');\n input.setAttribute('aria-expanded', 'false');\n input.removeAttribute('aria-activedescendant');\n activeIndex = -1;\n }\n\n function highlight(i: number): void {\n const items = list.querySelectorAll<HTMLElement>('.fw-combobox-option');\n items.forEach((el) => {\n el.classList.remove('is-active');\n el.setAttribute('aria-selected', 'false');\n });\n const el = items[i];\n if (el) {\n el.classList.add('is-active');\n el.setAttribute('aria-selected', 'true');\n el.scrollIntoView({ block: 'nearest' });\n input.setAttribute('aria-activedescendant', el.id);\n }\n activeIndex = i;\n }\n\n function select(value: string): void {\n input.value = value;\n close();\n // The synthetic events below would re-open the list via onInput/onFocus.\n selecting = true;\n input.focus();\n input.dispatchEvent(new Event('input', { bubbles: true }));\n input.dispatchEvent(new Event('change', { bubbles: true }));\n selecting = false;\n opts.onSelect?.(value);\n }\n\n const onInput = () => {\n if (!selecting) open();\n };\n const onFocus = () => {\n if (!selecting) open();\n };\n const onKeydown = (e: KeyboardEvent) => {\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n if (!list.classList.contains('is-open')) open();\n highlight(Math.min(activeIndex + 1, current.length - 1));\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n highlight(Math.max(activeIndex - 1, 0));\n } else if (e.key === 'Enter') {\n if (activeIndex > -1 && current[activeIndex] !== undefined) {\n e.preventDefault();\n select(current[activeIndex]);\n }\n } else if (e.key === 'Escape') {\n close();\n }\n };\n const onListMousedown = (e: MouseEvent) => {\n const opt = (e.target as HTMLElement).closest<HTMLElement>('.fw-combobox-option');\n if (opt) {\n e.preventDefault(); // keep focus on the input\n select(current[Number(opt.dataset.index)]);\n }\n };\n const onDocClick = (e: MouseEvent) => {\n if (!wrap.contains(e.target as Node)) close();\n };\n\n input.addEventListener('input', onInput);\n input.addEventListener('focus', onFocus);\n input.addEventListener('keydown', onKeydown);\n list.addEventListener('mousedown', onListMousedown);\n document.addEventListener('click', onDocClick);\n\n return {\n open,\n close,\n setOptions(next) {\n options = next;\n if (list.classList.contains('is-open')) open();\n },\n destroy() {\n input.removeEventListener('input', onInput);\n input.removeEventListener('focus', onFocus);\n input.removeEventListener('keydown', onKeydown);\n list.removeEventListener('mousedown', onListMousedown);\n document.removeEventListener('click', onDocClick);\n list.remove();\n if (input.dataset.fwList) input.setAttribute('list', input.dataset.fwList);\n delete input.dataset.fwBound;\n },\n };\n}\n\nfunction readDatalist(input: HTMLInputElement): string[] {\n const id = input.getAttribute('list') ?? input.dataset.fwList;\n const dl = id ? document.getElementById(id) : null;\n return dl ? Array.from(dl.querySelectorAll('option')).map((o) => o.value || o.textContent || '') : [];\n}\n\nfunction wrapInput(input: HTMLInputElement): HTMLElement {\n const wrap = document.createElement('div');\n wrap.className = 'fw-combobox';\n input.parentNode?.insertBefore(wrap, input);\n wrap.appendChild(input);\n return wrap;\n}\n\n/** Enhance every `[data-fw-combobox]` input under `root`. */\nexport function init(root: ParentNode = document): void {\n root.querySelectorAll<HTMLInputElement>('input[data-fw-combobox]').forEach((input) => {\n if (input.dataset.fwBound) return;\n input.dataset.fwBound = '1';\n combobox(input);\n });\n}\n", "/**\n * Modal & drawer overlays.\n *\n * <button data-fw-open-modal=\"confirm\">\u2026</button>\n * <div class=\"fw-modal-overlay\" id=\"confirm\" role=\"dialog\" aria-modal=\"true\"><div class=\"fw-modal\">\u2026 <button data-fw-close-modal>Cancel</button></div></div>\n *\n * <button data-fw-open-drawer=\"details\">\u2026</button>\n * <div class=\"fw-drawer-overlay\" data-fw-drawer-overlay=\"details\"></div>\n * <aside class=\"fw-drawer\" id=\"details\" role=\"dialog\" aria-modal=\"true\">\u2026 <button data-fw-close-drawer>\u00D7</button></aside>\n *\n * Closes on backdrop click (guarded \u2014 a text-selection drag that ends on the\n * backdrop does NOT close it), on Escape, and returns focus to the opener.\n */\nconst OPEN = 'is-open';\nconst LOCK = 'fw-scroll-lock';\nconst openStack: HTMLElement[] = [];\nconst openers = new WeakMap<HTMLElement, HTMLElement | null>();\n\nconst FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\nfunction resolve(target: string | HTMLElement): HTMLElement | null {\n return typeof target === 'string' ? document.getElementById(target) : target;\n}\n\n/** The backdrop element paired with a drawer (via `data-fw-drawer-overlay=\"<id>\"`), if any. */\nfunction backdropFor(el: HTMLElement): HTMLElement | null {\n if (el.classList.contains('fw-modal-overlay')) return el;\n const explicit = el.id ? document.querySelector<HTMLElement>(`[data-fw-drawer-overlay=\"${el.id}\"]`) : null;\n if (explicit) return explicit;\n const prev = el.previousElementSibling;\n return prev instanceof HTMLElement && prev.classList.contains('fw-drawer-overlay') ? prev : null;\n}\n\nexport function open(target: string | HTMLElement, opener: HTMLElement | null = (document.activeElement as HTMLElement) ?? null): void {\n const el = resolve(target);\n if (!el || el.classList.contains(OPEN)) return;\n el.classList.add(OPEN);\n backdropFor(el)?.classList.add(OPEN);\n openers.set(el, opener);\n openStack.push(el);\n document.body.classList.add(LOCK);\n el.dispatchEvent(new CustomEvent('fw:open', { bubbles: true }));\n // Focus the first control inside; fall back to the panel itself.\n const first = el.querySelector<HTMLElement>(FOCUSABLE);\n if (first) first.focus({ preventScroll: true });\n else {\n if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '-1');\n el.focus({ preventScroll: true });\n }\n}\n\nexport function close(target: string | HTMLElement): void {\n const el = resolve(target);\n if (!el || !el.classList.contains(OPEN)) return;\n el.classList.remove(OPEN);\n backdropFor(el)?.classList.remove(OPEN);\n const i = openStack.indexOf(el);\n if (i > -1) openStack.splice(i, 1);\n if (openStack.length === 0) document.body.classList.remove(LOCK);\n el.dispatchEvent(new CustomEvent('fw:close', { bubbles: true }));\n openers.get(el)?.focus?.({ preventScroll: true });\n openers.delete(el);\n}\n\nexport function toggle(target: string | HTMLElement): void {\n const el = resolve(target);\n if (el) (el.classList.contains(OPEN) ? close : open)(el);\n}\n\n/**\n * Click-outside-to-close, guarded against a classic false positive: a\n * browser's `click` fires on the nearest common ancestor of mousedown and\n * mouseup. A drag that starts inside the panel and ends on the backdrop has\n * the overlay as that ancestor, so a naive `e.target === overlay` check would\n * close the modal mid-selection. Close only when BOTH landed on the backdrop.\n */\nexport function bindOutsideClickToClose(backdrop: HTMLElement, onClose: () => void): void {\n let downOnBackdrop = false;\n backdrop.addEventListener('mousedown', (e) => {\n downOnBackdrop = e.target === backdrop;\n });\n backdrop.addEventListener('click', (e) => {\n if (e.target === backdrop && downOnBackdrop) onClose();\n });\n}\n\nlet keyBound = false;\n\n/** Wire every trigger, overlay and close button under `root`. */\nexport function init(root: ParentNode = document): void {\n root.querySelectorAll<HTMLElement>('[data-fw-open-modal], [data-fw-open-drawer]').forEach((btn) => {\n if (btn.dataset.fwBound) return;\n btn.dataset.fwBound = '1';\n const id = btn.dataset.fwOpenModal ?? btn.dataset.fwOpenDrawer ?? '';\n btn.addEventListener('click', () => open(id, btn));\n });\n\n root.querySelectorAll<HTMLElement>('.fw-modal-overlay').forEach((overlay) => {\n if (overlay.dataset.fwBound) return;\n overlay.dataset.fwBound = '1';\n bindOutsideClickToClose(overlay, () => close(overlay));\n overlay.querySelectorAll<HTMLElement>('[data-fw-close-modal]').forEach((btn) =>\n btn.addEventListener('click', () => close(overlay)),\n );\n });\n\n root.querySelectorAll<HTMLElement>('.fw-drawer').forEach((drawer) => {\n if (drawer.dataset.fwBound) return;\n drawer.dataset.fwBound = '1';\n const backdrop = backdropFor(drawer);\n if (backdrop) bindOutsideClickToClose(backdrop, () => close(drawer));\n drawer.querySelectorAll<HTMLElement>('[data-fw-close-drawer]').forEach((btn) =>\n btn.addEventListener('click', () => close(drawer)),\n );\n });\n\n if (!keyBound) {\n keyBound = true;\n document.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && openStack.length) close(openStack[openStack.length - 1]);\n });\n }\n}\n", "/**\n * Nav dropdown \u2014 the hamburger toggle in `.fw-nav` (and any other\n * button-controls-panel pair).\n *\n * <button data-fw-nav-toggle aria-controls=\"menu\" aria-expanded=\"false\">\u2026</button>\n * <div class=\"fw-nav-dropdown\" id=\"menu\">\u2026</div>\n *\n * Closes on outside click and Escape.\n */\nconst OPEN = 'is-open';\n\nexport function open(toggle: HTMLElement): void {\n const panel = panelFor(toggle);\n if (!panel) return;\n panel.classList.add(OPEN);\n toggle.setAttribute('aria-expanded', 'true');\n}\n\nexport function close(toggle: HTMLElement): void {\n const panel = panelFor(toggle);\n if (!panel) return;\n panel.classList.remove(OPEN);\n toggle.setAttribute('aria-expanded', 'false');\n}\n\nfunction panelFor(toggle: HTMLElement): HTMLElement | null {\n const id = toggle.getAttribute('aria-controls');\n return id ? document.getElementById(id) : null;\n}\n\nexport function init(root: ParentNode = document): void {\n root.querySelectorAll<HTMLElement>('[data-fw-nav-toggle]').forEach((toggle) => {\n if (toggle.dataset.fwBound) return;\n toggle.dataset.fwBound = '1';\n const panel = panelFor(toggle);\n if (!panel) return;\n const container = toggle.closest<HTMLElement>('.fw-nav') ?? toggle.parentElement ?? toggle;\n\n toggle.addEventListener('click', (e) => {\n e.stopPropagation();\n (panel.classList.contains(OPEN) ? close : open)(toggle);\n });\n document.addEventListener('click', (e) => {\n if (!container.contains(e.target as Node) && !panel.contains(e.target as Node)) close(toggle);\n });\n document.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') close(toggle);\n });\n });\n}\n", "/**\n * Loading states.\n *\n * loading.setButtonLoading(btn, 'Sending\u2026'); // disables, swaps in a spinner\n * loading.resetButtonLoading(btn); // restores original markup\n *\n * Or declaratively: any <form data-fw-loading> puts its submit button into the\n * loading state on submit (label from `data-fw-loading-text` on the button).\n */\nconst LOADING = 'fw-btn-loading';\n\nexport function setButtonLoading(btn: HTMLButtonElement | null, loadingText?: string): void {\n if (!btn || btn.classList.contains(LOADING)) return;\n btn.dataset.fwOriginalHtml = btn.innerHTML;\n btn.disabled = true;\n btn.setAttribute('aria-busy', 'true');\n btn.classList.add(LOADING);\n const label = loadingText ?? btn.dataset.fwLoadingText ?? btn.textContent ?? '';\n btn.innerHTML = '<span class=\"fw-spinner fw-spinner-sm\" aria-hidden=\"true\"></span>';\n btn.appendChild(document.createTextNode(label));\n}\n\nexport function resetButtonLoading(btn: HTMLButtonElement | null): void {\n if (!btn || !btn.classList.contains(LOADING)) return;\n btn.disabled = false;\n btn.removeAttribute('aria-busy');\n btn.classList.remove(LOADING);\n // Restore markup, not just text, so buttons with an icon keep it.\n if (btn.dataset.fwOriginalHtml !== undefined) {\n btn.innerHTML = btn.dataset.fwOriginalHtml;\n delete btn.dataset.fwOriginalHtml;\n }\n}\n\n/** Reset every loading button \u2014 e.g. after a bfcache restore. */\nexport function resetAll(root: ParentNode = document): void {\n root.querySelectorAll<HTMLButtonElement>(`.${LOADING}`).forEach(resetButtonLoading);\n}\n\nlet pageshowBound = false;\n\nexport function init(root: ParentNode = document): void {\n root.querySelectorAll<HTMLFormElement>('form[data-fw-loading]').forEach((form) => {\n if (form.dataset.fwBound) return;\n form.dataset.fwBound = '1';\n form.addEventListener('submit', () => {\n const btn = form.querySelector<HTMLButtonElement>('button[type=\"submit\"], button:not([type])');\n setButtonLoading(btn);\n });\n });\n\n // Browsers restore pages from the back-forward cache without re-running JS,\n // so a spinner painted right before navigating away would come back stuck.\n if (!pageshowBound) {\n pageshowBound = true;\n window.addEventListener('pageshow', (e) => {\n if (e.persisted) resetAll();\n });\n }\n}\n", "/**\n * Toast \u2014 a single, reused, bottom-right notice.\n *\n * toast.show('Invoice sent');\n * toast.show('That card was declined', { variant: 'error', duration: 6000 });\n */\nexport type ToastVariant = 'success' | 'error' | 'info' | 'pending';\n\nexport interface ToastOptions {\n variant?: ToastVariant;\n /** ms before it hides; 0 keeps it until `hide()` or the next `show()`. */\n duration?: number;\n}\n\nlet el: HTMLElement | null = null;\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\nfunction ensure(): HTMLElement {\n if (el && el.isConnected) return el;\n el = document.createElement('div');\n el.className = 'fw-toast';\n el.setAttribute('role', 'status');\n el.setAttribute('aria-live', 'polite');\n el.innerHTML = '<span class=\"fw-toast-dot\" aria-hidden=\"true\"></span><span class=\"fw-toast-text\"></span>';\n document.body.appendChild(el);\n return el;\n}\n\nexport function show(message: string, { variant = 'success', duration = 4000 }: ToastOptions = {}): HTMLElement {\n const node = ensure();\n node.className = `fw-toast${variant === 'success' ? '' : ` fw-toast-${variant}`}`;\n (node.querySelector('.fw-toast-text') as HTMLElement).textContent = message;\n if (timer) clearTimeout(timer);\n // Re-trigger the transition even if it's already showing.\n node.classList.remove('is-show');\n void node.offsetWidth;\n node.classList.add('is-show');\n if (duration > 0) timer = setTimeout(hide, duration);\n return node;\n}\n\nexport function hide(): void {\n el?.classList.remove('is-show');\n if (timer) clearTimeout(timer);\n timer = null;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["index_exports", "__export", "combobox_exports", "index_default", "init", "loading_exports", "modal_exports", "nav_exports", "theme_exports", "toast_exports", "version", "__toCommonJS", "theme_exports", "__export", "get", "getToken", "init", "resetTokens", "set", "setTokens", "toggle", "STORAGE_KEY", "EVENT", "TOKEN_PREFIX", "TOKEN_STYLE_ATTR", "store", "_a", "attr", "theme", "persist", "btn", "syncToggle", "next", "label", "root", "_b", "_c", "_d", "stored", "current", "e", "tokenScopes", "normalizeTokenName", "name", "isThemeSplit", "tokens", "v", "declBlock", "value", "tokenStyleEl", "create", "existing", "el", "renderTokens", "rules", "light", "dark", "scope", "darkSelector", "opts", "entry", "combobox_exports", "__export", "combobox", "init", "uid", "escapeHtml", "s", "c", "input", "opts", "_a", "_b", "_c", "_d", "filter", "o", "q", "emptyText", "options", "readDatalist", "current", "activeIndex", "selecting", "listId", "wrap", "wrapInput", "list", "id", "render", "matches", "opt", "i", "idx", "label", "open", "close", "highlight", "items", "el", "select", "value", "onInput", "onFocus", "onKeydown", "e", "onListMousedown", "onDocClick", "next", "dl", "root", "modal_exports", "__export", "bindOutsideClickToClose", "close", "init", "open", "toggle", "OPEN", "LOCK", "openStack", "openers", "FOCUSABLE", "resolve", "target", "backdropFor", "el", "explicit", "prev", "opener", "_a", "first", "_b", "_c", "i", "backdrop", "onClose", "downOnBackdrop", "e", "keyBound", "root", "btn", "id", "overlay", "drawer", "nav_exports", "__export", "close", "init", "open", "OPEN", "toggle", "panel", "panelFor", "id", "root", "_a", "_b", "container", "e", "loading_exports", "__export", "init", "resetAll", "resetButtonLoading", "setButtonLoading", "LOADING", "btn", "loadingText", "_a", "_b", "label", "root", "pageshowBound", "form", "e", "toast_exports", "__export", "hide", "show", "el", "timer", "ensure", "message", "variant", "duration", "node", "version", "init", "root", "Fernwell", "theme_exports", "combobox_exports", "modal_exports", "nav_exports", "loading_exports", "toast_exports", "index_default"]
|
|
7
|
+
}
|