mnfst 0.5.69 → 0.5.71

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 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://manifestjs.org/getting-started/setup) for existing projects or try the [starter project](https://manifestjs.org/getting-started/starter-project) for new ones.
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 [manifestjs.org](https://manifestjs.org).
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, $theme)
962
+ // Add $auth magic method (like $locale, $colors)
963
963
  Alpine.magic('auth', () => {
964
964
  const store = Alpine.store('auth');
965
965
  if (!store) {
@@ -1,6 +1,6 @@
1
1
  /* Manifest Code CSS
2
2
  /* By Andrew Matlock under MIT license
3
- /* https://manifestjs.org/styles/code
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');
@@ -1,65 +1,65 @@
1
- /* Manifest Themes */
1
+ /* Manifest Color */
2
2
 
3
3
  // Initialize plugin when either DOM is ready or Alpine is ready
4
- function initializeThemePlugin() {
4
+ function initializeColorPlugin() {
5
5
 
6
- // Initialize theme state with Alpine reactivity
7
- const theme = Alpine.reactive({
6
+ // Initialize color mode state with Alpine reactivity
7
+ const color = Alpine.reactive({
8
8
  current: localStorage.getItem('theme') || 'system'
9
9
  })
10
10
 
11
- // Apply initial theme
12
- applyTheme(theme.current)
11
+ // Apply initial color mode
12
+ applyColorMode(color.current)
13
13
 
14
- // Setup system theme listener
14
+ // Setup system color mode listener
15
15
  const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
16
16
  mediaQuery.addEventListener('change', () => {
17
- if (theme.current === 'system') {
18
- applyTheme('system')
17
+ if (color.current === 'system') {
18
+ applyColorMode('system')
19
19
  }
20
20
  })
21
21
 
22
- // Register theme directive
23
- Alpine.directive('theme', (el, { expression }, { evaluate, cleanup }) => {
22
+ // Register color directive
23
+ Alpine.directive('color', (el, { expression }, { evaluate, cleanup }) => {
24
24
 
25
25
  const handleClick = () => {
26
- const newTheme = expression === 'toggle'
26
+ const newMode = expression === 'toggle'
27
27
  ? (document.documentElement.classList.contains('dark') ? 'light' : 'dark')
28
28
  : evaluate(expression)
29
- setTheme(newTheme)
29
+ setColorMode(newMode)
30
30
  }
31
31
 
32
32
  el.addEventListener('click', handleClick)
33
33
  cleanup(() => el.removeEventListener('click', handleClick))
34
34
  })
35
35
 
36
- // Add $theme magic method
37
- Alpine.magic('theme', () => ({
36
+ // Add $color magic method
37
+ Alpine.magic('color', () => ({
38
38
  get current() {
39
- return theme.current
39
+ return color.current
40
40
  },
41
41
  set current(value) {
42
- setTheme(value)
42
+ setColorMode(value)
43
43
  }
44
44
  }))
45
45
 
46
- function setTheme(newTheme) {
47
- if (newTheme === 'toggle') {
48
- newTheme = theme.current === 'light' ? 'dark' : 'light'
46
+ function setColorMode(newMode) {
47
+ if (newMode === 'toggle') {
48
+ newMode = color.current === 'light' ? 'dark' : 'light'
49
49
  }
50
50
 
51
- // Update theme state
52
- theme.current = newTheme
53
- localStorage.setItem('theme', newTheme)
51
+ // Update color mode state
52
+ color.current = newMode
53
+ localStorage.setItem('theme', newMode)
54
54
 
55
- // Apply theme
56
- applyTheme(newTheme)
55
+ // Apply color mode
56
+ applyColorMode(newMode)
57
57
  }
58
58
 
59
- function applyTheme(theme) {
60
- const isDark = theme === 'system'
59
+ function applyColorMode(mode) {
60
+ const isDark = mode === 'system'
61
61
  ? window.matchMedia('(prefers-color-scheme: dark)').matches
62
- : theme === 'dark'
62
+ : mode === 'dark'
63
63
 
64
64
  // Update document classes
65
65
  document.documentElement.classList.remove('light', 'dark')
@@ -74,36 +74,36 @@ function initializeThemePlugin() {
74
74
  }
75
75
 
76
76
  // Track initialization to prevent duplicates
77
- let themePluginInitialized = false;
77
+ let colorPluginInitialized = false;
78
78
 
79
- function ensureThemePluginInitialized() {
80
- if (themePluginInitialized) return;
79
+ function ensureColorPluginInitialized() {
80
+ if (colorPluginInitialized) return;
81
81
  if (!window.Alpine || typeof window.Alpine.directive !== 'function') return;
82
82
 
83
- themePluginInitialized = true;
84
- initializeThemePlugin();
83
+ colorPluginInitialized = true;
84
+ initializeColorPlugin();
85
85
  }
86
86
 
87
87
  // Expose on window for loader to call if needed
88
- window.ensureThemePluginInitialized = ensureThemePluginInitialized;
88
+ window.ensureColorPluginInitialized = ensureColorPluginInitialized;
89
89
 
90
90
  // Handle both DOMContentLoaded and alpine:init
91
91
  if (document.readyState === 'loading') {
92
- document.addEventListener('DOMContentLoaded', ensureThemePluginInitialized);
92
+ document.addEventListener('DOMContentLoaded', ensureColorPluginInitialized);
93
93
  }
94
94
 
95
- document.addEventListener('alpine:init', ensureThemePluginInitialized);
95
+ document.addEventListener('alpine:init', ensureColorPluginInitialized);
96
96
 
97
97
  // If Alpine is already initialized when this script loads, initialize immediately
98
98
  if (window.Alpine && typeof window.Alpine.directive === 'function') {
99
- setTimeout(ensureThemePluginInitialized, 0);
99
+ setTimeout(ensureColorPluginInitialized, 0);
100
100
  } else {
101
101
  // If document is already loaded but Alpine isn't ready yet, wait for it
102
102
  const checkAlpine = setInterval(() => {
103
103
  if (window.Alpine && typeof window.Alpine.directive === 'function') {
104
104
  clearInterval(checkAlpine);
105
- ensureThemePluginInitialized();
105
+ ensureColorPluginInitialized();
106
106
  }
107
107
  }, 10);
108
108
  setTimeout(() => clearInterval(checkAlpine), 5000);
109
- }
109
+ }
@@ -162,7 +162,7 @@ window.ManifestComponentsProcessor = {
162
162
  }
163
163
  if (element.hasAttribute('data-pre-rendered') || element.hasAttribute('data-processed')) {
164
164
  // Pre-rendered components skip re-fetching, but hydrate-marked content
165
- // still needs Alpine initialization (x-data, @click, :class, x-colors etc.).
165
+ // still needs Alpine initialization (x-data, @click, :class, x-color etc.).
166
166
  if (element.hasAttribute('data-pre-rendered') && window.Alpine && typeof window.Alpine.initTree === 'function') {
167
167
  try { window.Alpine.initTree(element); } catch (e) { /* graceful */ }
168
168
  }
package/lib/manifest.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /* Manifest CSS
2
2
  /* By Andrew Matlock under MIT license
3
- /* https://manifestjs.org
3
+ /* https://manifestx.dev
4
4
  /* Modify referenced variables in manifest.theme.css
5
5
  */
6
6
 
@@ -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://manifestjs.org
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 (themes, router, data, markdown,
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
 
@@ -191,7 +191,7 @@
191
191
  'markdown',
192
192
  'svg',
193
193
  'code',
194
- 'colors',
194
+ 'color',
195
195
  'toasts',
196
196
  'tooltips',
197
197
  'dropdowns',
@@ -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
+ }
@@ -401,14 +401,74 @@ async function handleRouteChange() {
401
401
  }, 50);
402
402
  }
403
403
 
404
- // Emit route change event
405
- window.dispatchEvent(new CustomEvent('manifest:route-change', {
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
+ }
@@ -109,12 +109,46 @@ function initializeTabsPlugin() {
109
109
  }
110
110
  }
111
111
 
112
- // Process panels for this group - add x-show attributes
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
@@ -1,6 +1,6 @@
1
1
  /* Manifest Theme CSS
2
2
  /* By Andrew Matlock under MIT license
3
- /* https://manifestjs.org/styles/theme
3
+ /* https://manifestx.dev/styles/theme
4
4
  /* Modify values to customize your project theme
5
5
  */
6
6
 
@@ -32,7 +32,14 @@ function initializeToastPlugin() {
32
32
 
33
33
  // Create toast element
34
34
  const toast = document.createElement('div');
35
- toast.setAttribute('role', 'alert');
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
@@ -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
- // Save cache to localStorage
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
- try {
1211
- const serialized = JSON.stringify(Object.fromEntries(this.cache));
1212
- localStorage.setItem('tailwind-cache', serialized);
1213
- } catch (error) {
1214
- console.warn('Failed to save cached styles:', error);
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.69",
3
+ "version": "0.5.71",
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://manifestjs.org",
67
+ "homepage": "https://manifestx.dev",
64
68
  "repository": {
65
69
  "type": "git",
66
70
  "url": "https://github.com/Manifest-X/Manifest.git"