mnfst 0.5.69 → 0.5.70
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/README.md +2 -2
- package/lib/manifest.appwrite.auth.js +1 -1
- package/lib/manifest.code.css +1 -1
- package/lib/manifest.css +1 -1
- package/lib/manifest.d.ts +244 -0
- package/lib/manifest.dropdowns.js +26 -0
- package/lib/manifest.js +4 -4
- package/lib/manifest.range.css +60 -0
- package/lib/manifest.router.js +63 -3
- package/lib/manifest.schema.json +212 -0
- package/lib/manifest.tabs.js +82 -1
- package/lib/manifest.theme.css +1 -1
- package/lib/manifest.toasts.js +8 -1
- package/lib/manifest.tooltips.js +28 -0
- package/lib/manifest.utilities.js +48 -6
- package/package.json +8 -4
- package/lib/manifest.themes.js +0 -109
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Manifest is a frontend framework extending HTML for rapid, feature-rich website
|
|
|
14
14
|
|
|
15
15
|
## 💾 Setup
|
|
16
16
|
|
|
17
|
-
Get [CDN links](https://
|
|
17
|
+
Get [CDN links](https://manifestx.dev/getting-started/setup) for existing projects or try the [starter project](https://manifestx.dev/getting-started/starter-project) for new ones.
|
|
18
18
|
|
|
19
19
|
<br>
|
|
20
20
|
|
|
@@ -38,7 +38,7 @@ Get [CDN links](https://manifestjs.org/getting-started/setup) for existing proje
|
|
|
38
38
|
|
|
39
39
|
## 📚 Documentation
|
|
40
40
|
|
|
41
|
-
For full documentation visit [
|
|
41
|
+
For full documentation visit [manifestx.dev](https://manifestx.dev).
|
|
42
42
|
|
|
43
43
|
<br>
|
|
44
44
|
|
|
@@ -959,7 +959,7 @@ function initializeAuthMagic() {
|
|
|
959
959
|
return false;
|
|
960
960
|
}
|
|
961
961
|
|
|
962
|
-
// Add $auth magic method (like $locale, $
|
|
962
|
+
// Add $auth magic method (like $locale, $colors)
|
|
963
963
|
Alpine.magic('auth', () => {
|
|
964
964
|
const store = Alpine.store('auth');
|
|
965
965
|
if (!store) {
|
package/lib/manifest.code.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Manifest Code CSS
|
|
2
2
|
/* By Andrew Matlock under MIT license
|
|
3
|
-
/* https://
|
|
3
|
+
/* https://manifestx.dev/styles/code
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
@import url('https://fonts.googleapis.com/css2?family=Gabarito:wght@400..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Lexend+Exa&display=swap');
|
package/lib/manifest.css
CHANGED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest ambient type declarations.
|
|
3
|
+
* https://manifestx.dev
|
|
4
|
+
*
|
|
5
|
+
* This file gives editors (VS Code, Cursor, JetBrains) and AI coding assistants
|
|
6
|
+
* type information for Manifest's magic globals and data-source state. It is
|
|
7
|
+
* intended to live in the project root and is regenerated by `npx mnfst-types`
|
|
8
|
+
* to add project-specific augmentations to ManifestSources from manifest.json.
|
|
9
|
+
*
|
|
10
|
+
* Hand-edits below the AUGMENTATION marker will be overwritten on regeneration.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Data-source state and operators (attached to every $x.<source>)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** Appwrite-style query expression returned by helper builders. */
|
|
18
|
+
export type ManifestQueryExpr = string;
|
|
19
|
+
|
|
20
|
+
/** Appwrite-style query helpers used with `$query([...])`. */
|
|
21
|
+
export interface ManifestQuery {
|
|
22
|
+
equal(field: string, value: unknown): ManifestQueryExpr;
|
|
23
|
+
notEqual(field: string, value: unknown): ManifestQueryExpr;
|
|
24
|
+
lessThan(field: string, value: number | string): ManifestQueryExpr;
|
|
25
|
+
lessThanEqual(field: string, value: number | string): ManifestQueryExpr;
|
|
26
|
+
greaterThan(field: string, value: number | string): ManifestQueryExpr;
|
|
27
|
+
greaterThanEqual(field: string, value: number | string): ManifestQueryExpr;
|
|
28
|
+
between(field: string, start: number | string, end: number | string): ManifestQueryExpr;
|
|
29
|
+
contains(field: string, value: string | string[]): ManifestQueryExpr;
|
|
30
|
+
startsWith(field: string, value: string): ManifestQueryExpr;
|
|
31
|
+
endsWith(field: string, value: string): ManifestQueryExpr;
|
|
32
|
+
search(field: string, value: string): ManifestQueryExpr;
|
|
33
|
+
isNull(field: string): ManifestQueryExpr;
|
|
34
|
+
isNotNull(field: string): ManifestQueryExpr;
|
|
35
|
+
select(fields: string[]): ManifestQueryExpr;
|
|
36
|
+
orderAsc(field: string): ManifestQueryExpr;
|
|
37
|
+
orderDesc(field: string): ManifestQueryExpr;
|
|
38
|
+
limit(value: number): ManifestQueryExpr;
|
|
39
|
+
offset(value: number): ManifestQueryExpr;
|
|
40
|
+
cursorAfter(id: string): ManifestQueryExpr;
|
|
41
|
+
cursorBefore(id: string): ManifestQueryExpr;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** State flags exposed on every data source (array or object). */
|
|
45
|
+
export interface ManifestSourceState {
|
|
46
|
+
/** True while the source is fetching for the first time or after a mutation. */
|
|
47
|
+
readonly $loading: boolean;
|
|
48
|
+
/** Last error from a load or mutation, or null. */
|
|
49
|
+
readonly $error: Error | string | null;
|
|
50
|
+
/** True once initial load has completed (success or failure). */
|
|
51
|
+
readonly $ready: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Operators available on array-shaped sources (CSV tabular, JSON arrays, Appwrite tables). */
|
|
55
|
+
export interface ManifestArrayOps<T> {
|
|
56
|
+
/** Substring match across the given fields. Returns matching items. */
|
|
57
|
+
$search(term: string, ...fields: (keyof T & string)[]): T[];
|
|
58
|
+
/**
|
|
59
|
+
* Local sources: filter by an array of `Manifest.query.*` expressions and array methods.
|
|
60
|
+
* Appwrite sources: server-side query, returns a Promise.
|
|
61
|
+
*/
|
|
62
|
+
$query(expressions: ManifestQueryExpr[]): T[] | Promise<T[]>;
|
|
63
|
+
/** Look up a single row whose `id` matches the current route param, if any. */
|
|
64
|
+
$route(path?: string): T | undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Mutations available on Appwrite-table sources. */
|
|
68
|
+
export interface ManifestAppwriteTableOps<T> {
|
|
69
|
+
$create(values: Partial<T>): Promise<T>;
|
|
70
|
+
$update(id: string, values: Partial<T>): Promise<T>;
|
|
71
|
+
$delete(id: string): Promise<void>;
|
|
72
|
+
$duplicate(id: string, overrides?: Partial<T>): Promise<T>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Operations available on Appwrite-bucket (storage) sources. */
|
|
76
|
+
export interface ManifestAppwriteBucketOps {
|
|
77
|
+
$url(fileId: string): string;
|
|
78
|
+
$preview(fileId: string, opts?: Record<string, unknown>): string;
|
|
79
|
+
$download(fileId: string): string;
|
|
80
|
+
$openUrl(fileId: string): void;
|
|
81
|
+
$openPreview(fileId: string): void;
|
|
82
|
+
$openDownload(fileId: string): void;
|
|
83
|
+
$upload(file: File | Blob, opts?: Record<string, unknown>): Promise<{ $id: string }>;
|
|
84
|
+
$remove(fileId: string): Promise<void>;
|
|
85
|
+
$filesFor(parentId: string, fieldName?: string): unknown[];
|
|
86
|
+
$unlinkFrom(parentId: string, fileId: string): Promise<void>;
|
|
87
|
+
$removeFrom(parentId: string, fileId: string): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The shape returned by `$x.<source>` for an array-of-rows source.
|
|
92
|
+
* Behaves as a regular `T[]` (supports `map`, `filter`, `find`, etc.) plus
|
|
93
|
+
* Manifest operators and state flags.
|
|
94
|
+
*/
|
|
95
|
+
export type ManifestArraySource<T> =
|
|
96
|
+
& ReadonlyArray<T>
|
|
97
|
+
& ManifestSourceState
|
|
98
|
+
& ManifestArrayOps<T>
|
|
99
|
+
& Partial<ManifestAppwriteTableOps<T>>;
|
|
100
|
+
|
|
101
|
+
/** The shape returned by `$x.<source>` for an object-shaped source (CSV key-value, JSON object, YAML map). */
|
|
102
|
+
export type ManifestObjectSource<T> = T & ManifestSourceState;
|
|
103
|
+
|
|
104
|
+
/** The shape returned by `$x.<source>` for an Appwrite storage bucket. */
|
|
105
|
+
export type ManifestBucketSource = ManifestSourceState & ManifestAppwriteBucketOps;
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Magic globals registered by Manifest plugins
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Project-specific data sources, keyed by the names declared in `manifest.json`'s
|
|
113
|
+
* `data` property. Augmented by `npx mnfst-types` from manifest.json + sample
|
|
114
|
+
* data (declarations appended below the AUGMENTATION marker).
|
|
115
|
+
*/
|
|
116
|
+
export interface ManifestSources {
|
|
117
|
+
[sourceName: string]: ManifestArraySource<any> | ManifestObjectSource<any> | ManifestBucketSource;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** `$auth` from the Appwrite auth plugin. Loose by design — auth API is broad. */
|
|
121
|
+
export interface ManifestAuth {
|
|
122
|
+
readonly current: { $id: string; email?: string; name?: string;[k: string]: unknown } | null;
|
|
123
|
+
readonly isAuthenticated: boolean;
|
|
124
|
+
readonly isLoading: boolean;
|
|
125
|
+
readonly currentTeam: { $id: string; name: string;[k: string]: unknown } | null;
|
|
126
|
+
readonly teams: Array<{ $id: string; name: string;[k: string]: unknown }>;
|
|
127
|
+
login(email: string, password?: string): Promise<unknown>;
|
|
128
|
+
loginWithMagic(email: string): Promise<unknown>;
|
|
129
|
+
loginWithOAuth(provider: string, opts?: Record<string, unknown>): Promise<unknown>;
|
|
130
|
+
loginAsGuest(): Promise<unknown>;
|
|
131
|
+
logout(): Promise<unknown>;
|
|
132
|
+
[extra: string]: unknown;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** `$locale` from the localization plugin. */
|
|
136
|
+
export interface ManifestLocale {
|
|
137
|
+
readonly current: string;
|
|
138
|
+
readonly available: string[];
|
|
139
|
+
set(locale: string): void;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** `$toast` from the toasts plugin. */
|
|
143
|
+
export interface ManifestToast {
|
|
144
|
+
(message: string, opts?: { type?: 'info' | 'success' | 'warning' | 'error'; duration?: number }): void;
|
|
145
|
+
info(message: string, opts?: Record<string, unknown>): void;
|
|
146
|
+
success(message: string, opts?: Record<string, unknown>): void;
|
|
147
|
+
warning(message: string, opts?: Record<string, unknown>): void;
|
|
148
|
+
error(message: string, opts?: Record<string, unknown>): void;
|
|
149
|
+
dismiss(id?: string): void;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `$colors` from the themes plugin. */
|
|
153
|
+
export interface ManifestTheme {
|
|
154
|
+
readonly current: 'light' | 'dark' | 'system';
|
|
155
|
+
set(theme: 'light' | 'dark' | 'system'): void;
|
|
156
|
+
toggle(): void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** `$colors` from the colors plugin. */
|
|
160
|
+
export interface ManifestColors {
|
|
161
|
+
[name: string]: string | ManifestColors;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** `$colorpicker` from the colorpicker plugin. */
|
|
165
|
+
export interface ManifestColorpicker {
|
|
166
|
+
open(opts?: Record<string, unknown>): void;
|
|
167
|
+
close(): void;
|
|
168
|
+
[extra: string]: unknown;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** `$url` from the url-parameters plugin: read/write URL query params reactively. */
|
|
172
|
+
export interface ManifestUrl {
|
|
173
|
+
get(name: string): string | null;
|
|
174
|
+
set(name: string, value: string | null): void;
|
|
175
|
+
has(name: string): boolean;
|
|
176
|
+
[name: string]: unknown;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** `$route` magic — true if the current route matches the given pattern. */
|
|
180
|
+
export type ManifestRoute = (pattern: string) => boolean;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* `$modify('attr')` — exposed in component HTML. Returns the value of an
|
|
184
|
+
* attribute set on the parent component instance (Manifest's prop mechanism).
|
|
185
|
+
*/
|
|
186
|
+
export type ManifestModify = (attr: string) => string | undefined;
|
|
187
|
+
|
|
188
|
+
/** `$try(fn)` — run a callback and swallow any thrown error, returning undefined on failure. */
|
|
189
|
+
export type ManifestTry = <T>(fn: () => T) => T | undefined;
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Global declarations (what Alpine exposes inside `x-data`, `x-show`, etc.)
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
declare global {
|
|
196
|
+
/** Project data sources registered in manifest.json. */
|
|
197
|
+
const $x: ManifestSources;
|
|
198
|
+
/** Match the current route against a pattern (e.g. `$route('/about')`). */
|
|
199
|
+
const $route: ManifestRoute;
|
|
200
|
+
/** Read a parent component attribute (Manifest's prop mechanism). */
|
|
201
|
+
const $modify: ManifestModify;
|
|
202
|
+
/** Run a callback safely, returning undefined on error. */
|
|
203
|
+
const $try: ManifestTry;
|
|
204
|
+
/** Appwrite auth plugin (only present when the plugin is loaded). */
|
|
205
|
+
const $auth: ManifestAuth;
|
|
206
|
+
/** Localization plugin. */
|
|
207
|
+
const $locale: ManifestLocale;
|
|
208
|
+
/** Toasts plugin. */
|
|
209
|
+
const $toast: ManifestToast;
|
|
210
|
+
/** Themes plugin. */
|
|
211
|
+
const $colors: ManifestTheme;
|
|
212
|
+
/** Colors plugin. */
|
|
213
|
+
const $colors: ManifestColors;
|
|
214
|
+
/** Colorpicker plugin. */
|
|
215
|
+
const $colorpicker: ManifestColorpicker;
|
|
216
|
+
/** URL parameters plugin. */
|
|
217
|
+
const $url: ManifestUrl;
|
|
218
|
+
|
|
219
|
+
/** Window-level Manifest namespace exposed by the loader. */
|
|
220
|
+
interface Window {
|
|
221
|
+
Manifest?: {
|
|
222
|
+
loadPlugin(name: string, version?: string): Promise<unknown>;
|
|
223
|
+
loadTailwind(version?: string): Promise<unknown>;
|
|
224
|
+
getPluginUrl(name: string, version?: string): string;
|
|
225
|
+
};
|
|
226
|
+
Alpine?: unknown;
|
|
227
|
+
__manifestLoaded?: unknown;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// AUGMENTATION:start
|
|
233
|
+
// Project-specific declarations are appended below by `npx mnfst-types`.
|
|
234
|
+
// Do not hand-edit between the start/end markers — re-running the CLI
|
|
235
|
+
// regenerates this section from manifest.json + sample data.
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
// (No project augmentations yet — run `npx mnfst-types` to generate.)
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// AUGMENTATION:end
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
export { };
|
|
@@ -164,6 +164,32 @@ function initializeDropdownPlugin() {
|
|
|
164
164
|
el.style.setProperty('anchor-name', anchorName);
|
|
165
165
|
menu.style.setProperty('position-anchor', anchorName);
|
|
166
166
|
|
|
167
|
+
// ----- A11y wiring (WAI-ARIA Menu Button pattern) -----
|
|
168
|
+
// The trigger needs `aria-haspopup="menu"`, `aria-controls`, and a
|
|
169
|
+
// dynamic `aria-expanded` that follows the popover's open state.
|
|
170
|
+
// The menu element gets `role="menu"` and each list item gets
|
|
171
|
+
// `role="menuitem"` so screen readers announce the relationship.
|
|
172
|
+
//
|
|
173
|
+
// We don't apply these for `.context` dropdowns invoked by right-
|
|
174
|
+
// click — they're not button-triggered popups in the APG sense.
|
|
175
|
+
if (!modifiers.includes('context')) {
|
|
176
|
+
if (!menu.id) menu.id = 'mnfst-dropdown-' + Math.random().toString(36).slice(2, 9);
|
|
177
|
+
el.setAttribute('aria-haspopup', 'menu');
|
|
178
|
+
el.setAttribute('aria-controls', menu.id);
|
|
179
|
+
el.setAttribute('aria-expanded', menu.matches(':popover-open') ? 'true' : 'false');
|
|
180
|
+
if (!menu.hasAttribute('role')) menu.setAttribute('role', 'menu');
|
|
181
|
+
menu.querySelectorAll('li').forEach((li) => {
|
|
182
|
+
if (!li.hasAttribute('role')) li.setAttribute('role', 'menuitem');
|
|
183
|
+
});
|
|
184
|
+
// Keep aria-expanded in sync with the popover's state.
|
|
185
|
+
if (!menu.__mnfstAriaToggleBound) {
|
|
186
|
+
menu.__mnfstAriaToggleBound = true;
|
|
187
|
+
menu.addEventListener('toggle', (e) => {
|
|
188
|
+
el.setAttribute('aria-expanded', e.newState === 'open' ? 'true' : 'false');
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
167
193
|
// Set up hover functionality after menu is ready
|
|
168
194
|
if (modifiers.includes('hover')) {
|
|
169
195
|
const handleShowPopover = () => {
|
package/lib/manifest.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Manifest JS
|
|
2
2
|
/* By Andrew Matlock under MIT license
|
|
3
|
-
/* https://
|
|
3
|
+
/* https://manifestx.dev
|
|
4
4
|
/*
|
|
5
5
|
/* Lightweight loader that dynamically loads Alpine.js and Manifest plugins
|
|
6
6
|
/* from jsDelivr CDN. Loads all plugins by default, or a subset if specified.
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* innerHTML) of every element that needs runtime hydration. This
|
|
21
21
|
* function runs once on page load BEFORE any plugin or Alpine starts —
|
|
22
22
|
* it walks the contract, restores source state, and removes its own
|
|
23
|
-
* markers. Every downstream plugin (
|
|
23
|
+
* markers. Every downstream plugin (colors, router, data, markdown,
|
|
24
24
|
* icons, …) then sees exactly the DOM the user authored, exactly as it
|
|
25
25
|
* would in a live SPA. No plugin needs a "prerender mode" branch.
|
|
26
26
|
*
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
tmp.innerHTML = newHTML;
|
|
101
101
|
const parsed = tmp.firstElementChild;
|
|
102
102
|
if (parsed) {
|
|
103
|
-
try { el.parentNode.replaceChild(parsed, el); } catch (_) {}
|
|
103
|
+
try { el.parentNode.replaceChild(parsed, el); } catch (_) { }
|
|
104
104
|
}
|
|
105
105
|
continue;
|
|
106
106
|
}
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
tmp.innerHTML = newHTML;
|
|
133
133
|
const parsed = tmp.firstElementChild;
|
|
134
134
|
if (parsed) {
|
|
135
|
-
try { el.parentNode.replaceChild(parsed, el); } catch (_) {}
|
|
135
|
+
try { el.parentNode.replaceChild(parsed, el); } catch (_) { }
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* Manifest Range */
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
|
|
5
|
+
input[type=range]:not(.unstyle) {
|
|
6
|
+
appearance: none;
|
|
7
|
+
background-color: transparent;
|
|
8
|
+
border-radius: var(--radius, 0.5rem);
|
|
9
|
+
cursor: default;
|
|
10
|
+
|
|
11
|
+
&::-webkit-slider-runnable-track {
|
|
12
|
+
height: calc(var(--spacing, 0.25rem) * 2);
|
|
13
|
+
background-color: var(--color-field-surface, oklch(91.79% 0.0029 264.26));
|
|
14
|
+
border-radius: var(--radius, 0.5rem);
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
transition: var(--transition)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&:hover::-webkit-slider-runnable-track {
|
|
20
|
+
background-color: var(--color-field-surface-hover, oklch(89.24% 0.0024 12.48));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&::-webkit-slider-thumb {
|
|
24
|
+
appearance: none;
|
|
25
|
+
position: relative;
|
|
26
|
+
top: 50%;
|
|
27
|
+
width: calc(var(--spacing-field-height, 2.25rem) * 0.5);
|
|
28
|
+
height: calc(var(--spacing-field-height, 2.25rem) * 0.5);
|
|
29
|
+
transform: translateY(-50%);
|
|
30
|
+
background-color: color-mix(in oklch, var(--color-field-surface, oklch(91.79% 0.0029 264.26)) 60%, var(--color-field-inverse, oklch(16.6% 0.026 267)));
|
|
31
|
+
border-radius: 200px;
|
|
32
|
+
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
transition: var(--transition)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&::-webkit-slider-thumb:hover {
|
|
38
|
+
background-color: color-mix(in oklch, var(--color-field-surface, oklch(91.79% 0.0029 264.26)) 30%, var(--color-field-inverse, oklch(16.6% 0.026 267)));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:where(datalist):not(.unstyle) {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-flow: row nowrap;
|
|
45
|
+
justify-content: space-between;
|
|
46
|
+
width: 100%;
|
|
47
|
+
max-width: 100%;
|
|
48
|
+
|
|
49
|
+
& option {
|
|
50
|
+
width: 2ch;
|
|
51
|
+
text-align: center;
|
|
52
|
+
font-size: 0.875rem;
|
|
53
|
+
color: var(--color-content-neutral, oklch(48.26% 0.0365 255.09));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
label:has(input[type=range]):not(.unstyle) {
|
|
58
|
+
cursor: default;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/lib/manifest.router.js
CHANGED
|
@@ -401,14 +401,74 @@ async function handleRouteChange() {
|
|
|
401
401
|
}, 50);
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
-
//
|
|
405
|
-
|
|
404
|
+
// Build the route-change event once so dispatch + transition wrapper share it.
|
|
405
|
+
const event = new CustomEvent('manifest:route-change', {
|
|
406
406
|
detail: {
|
|
407
407
|
from: prevRoute,
|
|
408
408
|
to: newRoute,
|
|
409
409
|
normalizedPath: newRoute === '/' ? '/' : newRoute.replace(/^\/|\/$/g, '')
|
|
410
410
|
}
|
|
411
|
-
})
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// SPA route changes use the View Transitions API when available so the
|
|
414
|
+
// visibility-toggle that listeners perform inside this dispatch is
|
|
415
|
+
// animated. Cross-document MPA navigations are already handled by
|
|
416
|
+
// `@view-transition { navigation: auto }` in the framework's reset CSS;
|
|
417
|
+
// the same `::view-transition-group(*)` rule (driven by
|
|
418
|
+
// `--view-transition-duration` / `--view-transition-easing`) covers both.
|
|
419
|
+
//
|
|
420
|
+
// The callback is synchronous: visibility/head/anchor listeners mutate
|
|
421
|
+
// the DOM inside `dispatchEvent` and return. Returning anything async
|
|
422
|
+
// here would freeze the rendered frame until the promise resolves,
|
|
423
|
+
// adding the entirety of Alpine's pending-update queue to the perceived
|
|
424
|
+
// navigation time (1–2s on busy pages).
|
|
425
|
+
if (shouldUseViewTransition()) {
|
|
426
|
+
document.startViewTransition(() => {
|
|
427
|
+
window.dispatchEvent(event);
|
|
428
|
+
});
|
|
429
|
+
} else {
|
|
430
|
+
window.dispatchEvent(event);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Decide whether SPA route changes should run inside a View Transition.
|
|
435
|
+
// Three modes, in priority order:
|
|
436
|
+
//
|
|
437
|
+
// 1. `<html data-no-view-transitions>` → force OFF
|
|
438
|
+
// 2. `<html data-view-transitions>` → force ON
|
|
439
|
+
// 3. (neither) → auto: ON when the current page is
|
|
440
|
+
// under VT_AUTO_THRESHOLD elements,
|
|
441
|
+
// OFF otherwise
|
|
442
|
+
//
|
|
443
|
+
// The auto threshold exists because the View Transitions API rasterizes the
|
|
444
|
+
// full viewport for the "before" and "after" snapshots; cost scales linearly
|
|
445
|
+
// with DOM size and gets noticeable above a few thousand elements (a 10k-
|
|
446
|
+
// element page measured ~500ms per snapshot in dev). Light pages keep the
|
|
447
|
+
// crossfade; heavy pages stay fast.
|
|
448
|
+
//
|
|
449
|
+
// Cross-document (MPA) navigations are unaffected — those use the browser's
|
|
450
|
+
// native cross-document path (`@view-transition { navigation: auto }`),
|
|
451
|
+
// which rasterizes in parallel with page load and doesn't expose the cost.
|
|
452
|
+
//
|
|
453
|
+
// Per-element opt-out (`data-no-view-transition`, singular) on individual
|
|
454
|
+
// elements is handled by the existing reset CSS rule that sets
|
|
455
|
+
// `view-transition-name: none` on them. `prefers-reduced-motion` is
|
|
456
|
+
// respected automatically — the browser falls back to a snap with no
|
|
457
|
+
// animation when the user has it set.
|
|
458
|
+
const VT_AUTO_THRESHOLD = 3000;
|
|
459
|
+
|
|
460
|
+
function shouldUseViewTransition() {
|
|
461
|
+
if (typeof document === 'undefined') return false;
|
|
462
|
+
if (typeof document.startViewTransition !== 'function') return false;
|
|
463
|
+
const html = document.documentElement;
|
|
464
|
+
if (!html) return false;
|
|
465
|
+
if (html.hasAttribute('data-no-view-transitions')) return false;
|
|
466
|
+
if (html.hasAttribute('data-view-transitions')) return true;
|
|
467
|
+
try {
|
|
468
|
+
return document.querySelectorAll('*').length < VT_AUTO_THRESHOLD;
|
|
469
|
+
} catch {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
412
472
|
}
|
|
413
473
|
|
|
414
474
|
// Resolve internal link to absolute pathname for pushState. Relative hrefs (e.g. "gadget") are resolved against the app base, not the current URL, so we never get additive paths like /src/dist/widget/gadget/widget/...
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://manifestx.dev/manifest.schema.json",
|
|
4
|
+
"title": "Manifest project configuration",
|
|
5
|
+
"description": "Combined PWA manifest and Manifest framework configuration. See https://manifestx.dev/docs/getting-started/setup.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": true,
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"format": "uri"
|
|
12
|
+
},
|
|
13
|
+
"name": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Full application name shown by the OS and browsers."
|
|
16
|
+
},
|
|
17
|
+
"short_name": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Short name used on home screens and limited-width contexts."
|
|
20
|
+
},
|
|
21
|
+
"description": {
|
|
22
|
+
"type": "string"
|
|
23
|
+
},
|
|
24
|
+
"author": {
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
27
|
+
"email": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"format": "email"
|
|
30
|
+
},
|
|
31
|
+
"live_url": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"format": "uri",
|
|
34
|
+
"description": "Production URL — used by the prerender pipeline for canonical/sitemap output."
|
|
35
|
+
},
|
|
36
|
+
"start_url": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"default": "/"
|
|
39
|
+
},
|
|
40
|
+
"scope": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"default": "/"
|
|
43
|
+
},
|
|
44
|
+
"display": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"enum": ["fullscreen", "standalone", "minimal-ui", "browser"]
|
|
47
|
+
},
|
|
48
|
+
"orientation": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"enum": ["any", "natural", "landscape", "landscape-primary", "landscape-secondary", "portrait", "portrait-primary", "portrait-secondary"]
|
|
51
|
+
},
|
|
52
|
+
"background_color": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"pattern": "^#([0-9A-Fa-f]{3}){1,2}$"
|
|
55
|
+
},
|
|
56
|
+
"theme_color": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"pattern": "^#([0-9A-Fa-f]{3}){1,2}$"
|
|
59
|
+
},
|
|
60
|
+
"icons": {
|
|
61
|
+
"type": "array",
|
|
62
|
+
"items": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"required": ["src"],
|
|
65
|
+
"properties": {
|
|
66
|
+
"src": { "type": "string" },
|
|
67
|
+
"sizes": { "type": "string" },
|
|
68
|
+
"type": { "type": "string" },
|
|
69
|
+
"purpose": { "type": "string" }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"components": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"description": "Component HTML files registered as <x-name> tags.",
|
|
76
|
+
"items": { "type": "string" }
|
|
77
|
+
},
|
|
78
|
+
"preloadedComponents": {
|
|
79
|
+
"type": "array",
|
|
80
|
+
"description": "Components loaded eagerly on initial page load (header, logo, etc.).",
|
|
81
|
+
"items": { "type": "string" }
|
|
82
|
+
},
|
|
83
|
+
"data": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"description": "Named data sources made available via $x.<sourceName>. Each value is either a string path/URL or a config object.",
|
|
86
|
+
"additionalProperties": {
|
|
87
|
+
"oneOf": [
|
|
88
|
+
{
|
|
89
|
+
"type": "string",
|
|
90
|
+
"description": "Path to a CSV, JSON, or YAML file, or an absolute HTTP URL."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"type": "object",
|
|
94
|
+
"description": "Localized source, HTTP source with config, or Appwrite-backed source.",
|
|
95
|
+
"properties": {
|
|
96
|
+
"url": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"description": "HTTP endpoint URL."
|
|
99
|
+
},
|
|
100
|
+
"headers": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"additionalProperties": { "type": "string" }
|
|
103
|
+
},
|
|
104
|
+
"params": {
|
|
105
|
+
"type": "object"
|
|
106
|
+
},
|
|
107
|
+
"transform": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "Optional dot-path into the response body (e.g. 'data.items')."
|
|
110
|
+
},
|
|
111
|
+
"defaultValue": {
|
|
112
|
+
"description": "Value used while loading or on error."
|
|
113
|
+
},
|
|
114
|
+
"locales": {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"description": "Single CSV with locale columns; locale resolution happens internally."
|
|
117
|
+
},
|
|
118
|
+
"appwriteTableId": { "type": "string" },
|
|
119
|
+
"appwriteBucketId": { "type": "string" },
|
|
120
|
+
"appwriteDatabaseId": { "type": "string" },
|
|
121
|
+
"appwriteProjectId": { "type": "string" },
|
|
122
|
+
"appwriteEndpoint": { "type": "string" },
|
|
123
|
+
"appwriteDevKey": { "type": "string" },
|
|
124
|
+
"scope": {
|
|
125
|
+
"oneOf": [
|
|
126
|
+
{ "type": "string", "enum": ["user", "team"] },
|
|
127
|
+
{ "type": "array", "items": { "type": "string", "enum": ["user", "team"] } }
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
"autoInjectUserId": { "type": "boolean" },
|
|
131
|
+
"autoInjectTeamId": { "type": "boolean" },
|
|
132
|
+
"queries": {
|
|
133
|
+
"type": "array",
|
|
134
|
+
"items": { "type": "string" }
|
|
135
|
+
},
|
|
136
|
+
"storage": {
|
|
137
|
+
"type": "object",
|
|
138
|
+
"description": "Map of storage-bucket source names to the column on this row that holds their file IDs.",
|
|
139
|
+
"additionalProperties": { "type": "string" }
|
|
140
|
+
},
|
|
141
|
+
"throttle": { "type": "number" },
|
|
142
|
+
"cleanupInterval": { "type": "number" },
|
|
143
|
+
"minChangeThreshold": { "type": "number" },
|
|
144
|
+
"idleThreshold": { "type": "number" },
|
|
145
|
+
"enableVisualRendering": { "type": "boolean" },
|
|
146
|
+
"includeVelocity": { "type": "boolean" }
|
|
147
|
+
},
|
|
148
|
+
"additionalProperties": true
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"appwrite": {
|
|
154
|
+
"type": "object",
|
|
155
|
+
"description": "Appwrite global configuration. Per-source overrides live on the data entry.",
|
|
156
|
+
"properties": {
|
|
157
|
+
"projectId": { "type": "string" },
|
|
158
|
+
"endpoint": { "type": "string", "format": "uri" },
|
|
159
|
+
"databaseId": { "type": "string" },
|
|
160
|
+
"devKey": {
|
|
161
|
+
"type": "string",
|
|
162
|
+
"description": "May reference an env var via ${VAR_NAME}."
|
|
163
|
+
},
|
|
164
|
+
"auth": {
|
|
165
|
+
"type": "object",
|
|
166
|
+
"properties": {
|
|
167
|
+
"methods": {
|
|
168
|
+
"type": "array",
|
|
169
|
+
"items": {
|
|
170
|
+
"type": "string",
|
|
171
|
+
"enum": ["guest", "guest-manual", "magic", "oauth", "password"]
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"teams": {
|
|
175
|
+
"type": "object",
|
|
176
|
+
"properties": {
|
|
177
|
+
"permanent": { "type": "array", "items": { "type": "string" } },
|
|
178
|
+
"template": { "type": "array", "items": { "type": "string" } }
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
"roles": {
|
|
182
|
+
"type": "object",
|
|
183
|
+
"additionalProperties": {
|
|
184
|
+
"type": "object",
|
|
185
|
+
"additionalProperties": {
|
|
186
|
+
"type": "array",
|
|
187
|
+
"items": { "type": "string" }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
"creatorRole": { "type": "string" }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
"prerender": {
|
|
197
|
+
"type": "object",
|
|
198
|
+
"description": "Static rendering configuration consumed by mnfst-render.",
|
|
199
|
+
"properties": {
|
|
200
|
+
"localUrl": { "type": "string", "format": "uri" },
|
|
201
|
+
"liveUrl": { "type": "string", "format": "uri" },
|
|
202
|
+
"output": { "type": "string", "default": "dist" },
|
|
203
|
+
"routerBase": { "type": "string", "default": "" },
|
|
204
|
+
"locales": {
|
|
205
|
+
"type": "array",
|
|
206
|
+
"items": { "type": "string" }
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
"additionalProperties": true
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
package/lib/manifest.tabs.js
CHANGED
|
@@ -109,12 +109,46 @@ function initializeTabsPlugin() {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
//
|
|
112
|
+
// ----- Accessibility wiring (WAI-ARIA Tabs pattern) -----
|
|
113
|
+
//
|
|
114
|
+
// Per the ARIA APG, a tabs widget needs:
|
|
115
|
+
// - role="tablist" on the tab container (parent of buttons)
|
|
116
|
+
// - role="tab" + aria-selected + aria-controls + tabindex on each button
|
|
117
|
+
// - role="tabpanel" + aria-labelledby + tabindex="0" on each panel
|
|
118
|
+
// - arrow-key navigation between tabs (with roving tabindex)
|
|
119
|
+
//
|
|
120
|
+
// We compute the tab-container element as the closest common ancestor of
|
|
121
|
+
// the relevant buttons (often a <nav> or <div>). Each button is assigned
|
|
122
|
+
// a stable id if it doesn't have one, so panels can reference it.
|
|
123
|
+
|
|
124
|
+
// Assign ids where missing.
|
|
125
|
+
const buttonIdByTabValue = {};
|
|
126
|
+
relevantButtons.forEach((button, i) => {
|
|
127
|
+
const tabValue = button.getAttribute('x-tab');
|
|
128
|
+
if (!tabValue) return;
|
|
129
|
+
if (!button.id) {
|
|
130
|
+
button.id = `mnfst-tab-${sanitizedPanelSet || 'g'}-${tabValue.replace(/[^a-zA-Z0-9_-]/g, '-')}-${i}`;
|
|
131
|
+
}
|
|
132
|
+
buttonIdByTabValue[tabValue] = button.id;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Process panels for this group - add x-show + a11y attributes
|
|
113
136
|
panels.forEach(panel => {
|
|
114
137
|
// Create condition that checks if tab property matches this panel's identifier
|
|
115
138
|
const showCondition = `${tabProp} === '${panel.id}'`;
|
|
116
139
|
panel.element.setAttribute('x-show', showCondition);
|
|
117
140
|
|
|
141
|
+
// Ensure panel has an id (Alpine needs one for aria-labelledby on buttons)
|
|
142
|
+
if (!panel.element.id) panel.element.id = panel.id;
|
|
143
|
+
|
|
144
|
+
// ARIA: role + label + focusable
|
|
145
|
+
panel.element.setAttribute('role', 'tabpanel');
|
|
146
|
+
if (!panel.element.hasAttribute('tabindex')) {
|
|
147
|
+
panel.element.setAttribute('tabindex', '0');
|
|
148
|
+
}
|
|
149
|
+
const labelledBy = buttonIdByTabValue[panel.id];
|
|
150
|
+
if (labelledBy) panel.element.setAttribute('aria-labelledby', labelledBy);
|
|
151
|
+
|
|
118
152
|
// Remove x-tabpanel attribute since we've converted it
|
|
119
153
|
panel.element.removeAttribute('x-tabpanel');
|
|
120
154
|
});
|
|
@@ -129,10 +163,57 @@ function initializeTabsPlugin() {
|
|
|
129
163
|
const clickHandler = `${tabProp} = '${tabValue}'`;
|
|
130
164
|
button.setAttribute('x-on:click', clickHandler);
|
|
131
165
|
|
|
166
|
+
// ARIA: role, selection state (reactive via :aria-selected), controls
|
|
167
|
+
button.setAttribute('role', 'tab');
|
|
168
|
+
button.setAttribute(':aria-selected', `String(${tabProp} === '${tabValue}')`);
|
|
169
|
+
// Roving tabindex: -1 when not active so arrow keys, not Tab, move between tabs.
|
|
170
|
+
button.setAttribute(':tabindex', `${tabProp} === '${tabValue}' ? '0' : '-1'`);
|
|
171
|
+
const panel = panels.find((p) => p.id === tabValue);
|
|
172
|
+
if (panel && panel.element.id) {
|
|
173
|
+
button.setAttribute('aria-controls', panel.element.id);
|
|
174
|
+
}
|
|
175
|
+
|
|
132
176
|
// Remove x-tab attribute since we've converted it
|
|
133
177
|
button.removeAttribute('x-tab');
|
|
134
178
|
});
|
|
135
179
|
|
|
180
|
+
// Find the tablist container — closest common ancestor of all relevant
|
|
181
|
+
// buttons. If they share a direct parent that's the tablist; otherwise
|
|
182
|
+
// walk up until one wraps them all. Set role="tablist" + a keydown
|
|
183
|
+
// handler that walks the focusable tabs on Left/Right/Home/End.
|
|
184
|
+
if (relevantButtons.length > 0) {
|
|
185
|
+
let tablistEl = relevantButtons[0].parentElement;
|
|
186
|
+
while (tablistEl && tablistEl !== document.body) {
|
|
187
|
+
if (relevantButtons.every((b) => tablistEl.contains(b))) break;
|
|
188
|
+
tablistEl = tablistEl.parentElement;
|
|
189
|
+
}
|
|
190
|
+
if (tablistEl) {
|
|
191
|
+
tablistEl.setAttribute('role', 'tablist');
|
|
192
|
+
if (!tablistEl.__mnfstTabsKeydown) {
|
|
193
|
+
tablistEl.__mnfstTabsKeydown = (e) => {
|
|
194
|
+
const target = e.target;
|
|
195
|
+
if (!target || target.getAttribute('role') !== 'tab') return;
|
|
196
|
+
const tabs = Array.from(tablistEl.querySelectorAll('[role="tab"]'));
|
|
197
|
+
const idx = tabs.indexOf(target);
|
|
198
|
+
if (idx === -1) return;
|
|
199
|
+
let nextIdx = null;
|
|
200
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') nextIdx = (idx + 1) % tabs.length;
|
|
201
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') nextIdx = (idx - 1 + tabs.length) % tabs.length;
|
|
202
|
+
else if (e.key === 'Home') nextIdx = 0;
|
|
203
|
+
else if (e.key === 'End') nextIdx = tabs.length - 1;
|
|
204
|
+
if (nextIdx == null) return;
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
// Automatic activation: focusing a tab selects it. This matches
|
|
207
|
+
// the most common APG variant and Manifest's existing click-to-
|
|
208
|
+
// activate semantics.
|
|
209
|
+
tabs[nextIdx].focus();
|
|
210
|
+
tabs[nextIdx].click();
|
|
211
|
+
};
|
|
212
|
+
tablistEl.addEventListener('keydown', tablistEl.__mnfstTabsKeydown);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
136
217
|
// Ensure Alpine processes the updated x-data and x-show attributes
|
|
137
218
|
if (window.Alpine && typeof window.Alpine.initTree === 'function') {
|
|
138
219
|
// If the parent already has Alpine initialized, we need to update it
|
package/lib/manifest.theme.css
CHANGED
package/lib/manifest.toasts.js
CHANGED
|
@@ -32,7 +32,14 @@ function initializeToastPlugin() {
|
|
|
32
32
|
|
|
33
33
|
// Create toast element
|
|
34
34
|
const toast = document.createElement('div');
|
|
35
|
-
|
|
35
|
+
// A11y: route by type. "negative" (and forward-compat "error" / "warning")
|
|
36
|
+
// interrupt with role="alert" (assertive live region). All other types —
|
|
37
|
+
// default, "brand", "accent", "positive" — use role="status" (polite)
|
|
38
|
+
// so screen readers finish what they're saying before announcing,
|
|
39
|
+
// matching the toast's intent as a non-urgent confirmation.
|
|
40
|
+
const isAssertive = type === 'negative' || type === 'error' || type === 'warning';
|
|
41
|
+
toast.setAttribute('role', isAssertive ? 'alert' : 'status');
|
|
42
|
+
if (!isAssertive) toast.setAttribute('aria-live', 'polite');
|
|
36
43
|
toast.setAttribute('class', type ? `toast ${type}` : 'toast');
|
|
37
44
|
|
|
38
45
|
// Create content with optional icon
|
package/lib/manifest.tooltips.js
CHANGED
|
@@ -155,6 +155,19 @@ function initializeTooltipPlugin() {
|
|
|
155
155
|
s.activeTrigger = trigger;
|
|
156
156
|
s.currentAnchorName = anchorName;
|
|
157
157
|
|
|
158
|
+
// A11y: link the trigger to the tooltip so screen readers announce the
|
|
159
|
+
// tooltip text as a description when the trigger receives focus or hover.
|
|
160
|
+
// Per WAI-ARIA, aria-describedby is the standard for this relationship.
|
|
161
|
+
if (!s.el.id) s.el.id = 'mnfst-tooltip-' + Math.random().toString(36).slice(2, 9);
|
|
162
|
+
s.el.setAttribute('role', 'tooltip');
|
|
163
|
+
// Preserve any author-provided aria-describedby so we don't stomp it.
|
|
164
|
+
if (!trigger._tooltipPriorDescribedBy) {
|
|
165
|
+
trigger._tooltipPriorDescribedBy = trigger.getAttribute('aria-describedby') || '';
|
|
166
|
+
}
|
|
167
|
+
const prior = trigger._tooltipPriorDescribedBy;
|
|
168
|
+
const merged = prior ? `${prior} ${s.el.id}` : s.el.id;
|
|
169
|
+
trigger.setAttribute('aria-describedby', merged);
|
|
170
|
+
|
|
158
171
|
if (!s.el.matches(':popover-open')) s.el.showPopover();
|
|
159
172
|
}
|
|
160
173
|
|
|
@@ -163,6 +176,16 @@ function initializeTooltipPlugin() {
|
|
|
163
176
|
document.querySelectorAll('.tooltip[popover="hint"]:popover-open').forEach(el => {
|
|
164
177
|
try { el.hidePopover(); } catch {}
|
|
165
178
|
});
|
|
179
|
+
// Restore each tooltip's prior aria-describedby on the trigger it had been
|
|
180
|
+
// bound to. We can't reach the trigger from the popover alone, so we walk
|
|
181
|
+
// the tooltipped triggers and remove our id from their describedby list.
|
|
182
|
+
document.querySelectorAll('[aria-describedby]').forEach((el) => {
|
|
183
|
+
if (!el._tooltipPriorDescribedBy && el._tooltipPriorDescribedBy !== '') return;
|
|
184
|
+
const prior = el._tooltipPriorDescribedBy;
|
|
185
|
+
if (prior) el.setAttribute('aria-describedby', prior);
|
|
186
|
+
else el.removeAttribute('aria-describedby');
|
|
187
|
+
el._tooltipPriorDescribedBy = undefined;
|
|
188
|
+
});
|
|
166
189
|
markTooltipHidden();
|
|
167
190
|
}
|
|
168
191
|
|
|
@@ -258,6 +281,11 @@ function initializeTooltipPlugin() {
|
|
|
258
281
|
el.addEventListener('mouseenter', requestShow);
|
|
259
282
|
el.addEventListener('mouseleave', requestHide);
|
|
260
283
|
|
|
284
|
+
// Keyboard / focus interactions — WCAG 2.1 SC 1.4.13 requires tooltip
|
|
285
|
+
// content to be accessible to keyboard users via focus, not hover only.
|
|
286
|
+
el.addEventListener('focus', requestShow);
|
|
287
|
+
el.addEventListener('blur', requestHide);
|
|
288
|
+
|
|
261
289
|
// Mousedown/click: always hide immediately; scheduleAnchorRestore so the
|
|
262
290
|
// trigger's anchor-name stays valid long enough for any dropdown popover
|
|
263
291
|
// it launches to position itself correctly.
|
|
@@ -1205,14 +1205,56 @@ TailwindCompiler.prototype.loadAndApplyCache = function () {
|
|
|
1205
1205
|
}
|
|
1206
1206
|
};
|
|
1207
1207
|
|
|
1208
|
-
//
|
|
1208
|
+
// Cap on persisted cache entries. Each entry stores a full compiled
|
|
1209
|
+
// stylesheet keyed by the union of classes seen on a given page, so on a
|
|
1210
|
+
// multi-page MPA this Map grows fast — and localStorage tops out at ~5MB per
|
|
1211
|
+
// origin. 20 covers typical hot paths; rarer routes recompile (cheap).
|
|
1212
|
+
TailwindCompiler.prototype.MAX_PERSISTED_CACHE_ENTRIES = 20;
|
|
1213
|
+
|
|
1214
|
+
// Drop the oldest entries from this.cache until at most `limit` remain.
|
|
1215
|
+
// Uses entry.timestamp; entries without one are evicted first.
|
|
1216
|
+
TailwindCompiler.prototype.evictOldestCacheEntries = function (limit) {
|
|
1217
|
+
if (this.cache.size <= limit) return;
|
|
1218
|
+
const sorted = Array.from(this.cache.entries())
|
|
1219
|
+
.sort((a, b) => (a[1].timestamp || 0) - (b[1].timestamp || 0));
|
|
1220
|
+
const toRemove = sorted.length - limit;
|
|
1221
|
+
for (let i = 0; i < toRemove; i++) {
|
|
1222
|
+
this.cache.delete(sorted[i][0]);
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
// Save cache to localStorage with size cap and quota-aware eviction.
|
|
1209
1227
|
TailwindCompiler.prototype.savePersistentCache = function () {
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1228
|
+
// Proactive cap so we don't write something we know is at risk.
|
|
1229
|
+
this.evictOldestCacheEntries(this.MAX_PERSISTED_CACHE_ENTRIES);
|
|
1230
|
+
let attempts = 0;
|
|
1231
|
+
while (this.cache.size > 0 && attempts < 4) {
|
|
1232
|
+
try {
|
|
1233
|
+
const serialized = JSON.stringify(Object.fromEntries(this.cache));
|
|
1234
|
+
localStorage.setItem('tailwind-cache', serialized);
|
|
1235
|
+
return;
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
// QuotaExceededError (name varies by browser): drop the oldest
|
|
1238
|
+
// half and retry. Anything else: bail.
|
|
1239
|
+
const isQuotaError =
|
|
1240
|
+
error && (
|
|
1241
|
+
error.name === 'QuotaExceededError' ||
|
|
1242
|
+
error.code === 22 ||
|
|
1243
|
+
error.code === 1014 // Firefox: NS_ERROR_DOM_QUOTA_REACHED
|
|
1244
|
+
);
|
|
1245
|
+
if (!isQuotaError) {
|
|
1246
|
+
console.warn('Failed to save cached styles:', error);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const halved = Math.max(1, Math.floor(this.cache.size / 2));
|
|
1250
|
+
this.evictOldestCacheEntries(halved);
|
|
1251
|
+
attempts++;
|
|
1252
|
+
}
|
|
1215
1253
|
}
|
|
1254
|
+
// Last resort: cache is unusable in this origin right now, clear the slot
|
|
1255
|
+
// so the next session starts clean. This is a perf optimization, not
|
|
1256
|
+
// correctness — drop quietly.
|
|
1257
|
+
try { localStorage.removeItem('tailwind-cache'); } catch (_) {}
|
|
1216
1258
|
};
|
|
1217
1259
|
|
|
1218
1260
|
// Load cache from localStorage
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mnfst",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.70",
|
|
4
4
|
"private": false,
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"templates/starter",
|
|
7
|
-
"packages/render"
|
|
7
|
+
"packages/render",
|
|
8
|
+
"packages/types",
|
|
9
|
+
"packages/test"
|
|
8
10
|
],
|
|
9
11
|
"main": "lib/manifest.js",
|
|
10
12
|
"style": "lib/manifest.css",
|
|
@@ -28,8 +30,10 @@
|
|
|
28
30
|
"release": "npm version patch --no-git-tag-version && npm publish",
|
|
29
31
|
"release:run": "cd packages/run && npm version patch --no-git-tag-version && npm publish --auth-type=web",
|
|
30
32
|
"release:render": "cd packages/render && npm version patch --no-git-tag-version && npm publish --auth-type=web",
|
|
33
|
+
"release:types": "cd packages/types && npm version patch --no-git-tag-version && npm publish --auth-type=web",
|
|
34
|
+
"release:test": "cd packages/test && npm version patch --no-git-tag-version && npm publish --auth-type=web",
|
|
31
35
|
"release:starter": "cd packages/create-starter && npm version patch --no-git-tag-version && npm publish --auth-type=web",
|
|
32
|
-
"release:all": "npm run release:run && npm run release:render && npm run release:starter && npm run release",
|
|
36
|
+
"release:all": "npm run release:run && npm run release:render && npm run release:types && npm run release:test && npm run release:starter && npm run release",
|
|
33
37
|
"prepublishOnly": "npm run build",
|
|
34
38
|
"test": "vitest run",
|
|
35
39
|
"lint": "echo 'No linting configured'"
|
|
@@ -60,7 +64,7 @@
|
|
|
60
64
|
"author": "Andrew Matlock",
|
|
61
65
|
"license": "MIT",
|
|
62
66
|
"description": "A modern, lightweight frontend framework with built-in components and utilities",
|
|
63
|
-
"homepage": "https://
|
|
67
|
+
"homepage": "https://manifestx.dev",
|
|
64
68
|
"repository": {
|
|
65
69
|
"type": "git",
|
|
66
70
|
"url": "https://github.com/Manifest-X/Manifest.git"
|
package/lib/manifest.themes.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/* Manifest Themes */
|
|
2
|
-
|
|
3
|
-
// Initialize plugin when either DOM is ready or Alpine is ready
|
|
4
|
-
function initializeThemePlugin() {
|
|
5
|
-
|
|
6
|
-
// Initialize theme state with Alpine reactivity
|
|
7
|
-
const theme = Alpine.reactive({
|
|
8
|
-
current: localStorage.getItem('theme') || 'system'
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
// Apply initial theme
|
|
12
|
-
applyTheme(theme.current)
|
|
13
|
-
|
|
14
|
-
// Setup system theme listener
|
|
15
|
-
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
16
|
-
mediaQuery.addEventListener('change', () => {
|
|
17
|
-
if (theme.current === 'system') {
|
|
18
|
-
applyTheme('system')
|
|
19
|
-
}
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
// Register theme directive
|
|
23
|
-
Alpine.directive('theme', (el, { expression }, { evaluate, cleanup }) => {
|
|
24
|
-
|
|
25
|
-
const handleClick = () => {
|
|
26
|
-
const newTheme = expression === 'toggle'
|
|
27
|
-
? (document.documentElement.classList.contains('dark') ? 'light' : 'dark')
|
|
28
|
-
: evaluate(expression)
|
|
29
|
-
setTheme(newTheme)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
el.addEventListener('click', handleClick)
|
|
33
|
-
cleanup(() => el.removeEventListener('click', handleClick))
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
// Add $theme magic method
|
|
37
|
-
Alpine.magic('theme', () => ({
|
|
38
|
-
get current() {
|
|
39
|
-
return theme.current
|
|
40
|
-
},
|
|
41
|
-
set current(value) {
|
|
42
|
-
setTheme(value)
|
|
43
|
-
}
|
|
44
|
-
}))
|
|
45
|
-
|
|
46
|
-
function setTheme(newTheme) {
|
|
47
|
-
if (newTheme === 'toggle') {
|
|
48
|
-
newTheme = theme.current === 'light' ? 'dark' : 'light'
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Update theme state
|
|
52
|
-
theme.current = newTheme
|
|
53
|
-
localStorage.setItem('theme', newTheme)
|
|
54
|
-
|
|
55
|
-
// Apply theme
|
|
56
|
-
applyTheme(newTheme)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function applyTheme(theme) {
|
|
60
|
-
const isDark = theme === 'system'
|
|
61
|
-
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
62
|
-
: theme === 'dark'
|
|
63
|
-
|
|
64
|
-
// Update document classes
|
|
65
|
-
document.documentElement.classList.remove('light', 'dark')
|
|
66
|
-
document.documentElement.classList.add(isDark ? 'dark' : 'light')
|
|
67
|
-
|
|
68
|
-
// Update meta theme-color
|
|
69
|
-
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
|
|
70
|
-
if (metaThemeColor) {
|
|
71
|
-
metaThemeColor.setAttribute('content', isDark ? '#000000' : '#FFFFFF')
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Track initialization to prevent duplicates
|
|
77
|
-
let themePluginInitialized = false;
|
|
78
|
-
|
|
79
|
-
function ensureThemePluginInitialized() {
|
|
80
|
-
if (themePluginInitialized) return;
|
|
81
|
-
if (!window.Alpine || typeof window.Alpine.directive !== 'function') return;
|
|
82
|
-
|
|
83
|
-
themePluginInitialized = true;
|
|
84
|
-
initializeThemePlugin();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Expose on window for loader to call if needed
|
|
88
|
-
window.ensureThemePluginInitialized = ensureThemePluginInitialized;
|
|
89
|
-
|
|
90
|
-
// Handle both DOMContentLoaded and alpine:init
|
|
91
|
-
if (document.readyState === 'loading') {
|
|
92
|
-
document.addEventListener('DOMContentLoaded', ensureThemePluginInitialized);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
document.addEventListener('alpine:init', ensureThemePluginInitialized);
|
|
96
|
-
|
|
97
|
-
// If Alpine is already initialized when this script loads, initialize immediately
|
|
98
|
-
if (window.Alpine && typeof window.Alpine.directive === 'function') {
|
|
99
|
-
setTimeout(ensureThemePluginInitialized, 0);
|
|
100
|
-
} else {
|
|
101
|
-
// If document is already loaded but Alpine isn't ready yet, wait for it
|
|
102
|
-
const checkAlpine = setInterval(() => {
|
|
103
|
-
if (window.Alpine && typeof window.Alpine.directive === 'function') {
|
|
104
|
-
clearInterval(checkAlpine);
|
|
105
|
-
ensureThemePluginInitialized();
|
|
106
|
-
}
|
|
107
|
-
}, 10);
|
|
108
|
-
setTimeout(() => clearInterval(checkAlpine), 5000);
|
|
109
|
-
}
|