mnfst 0.5.68 → 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.accordion.css +3 -2
- package/lib/manifest.appwrite.auth.js +1 -1
- package/lib/manifest.code.css +1 -1
- package/lib/manifest.css +4 -3
- package/lib/manifest.d.ts +244 -0
- package/lib/manifest.dropdowns.js +26 -0
- package/lib/manifest.js +4 -4
- package/lib/manifest.min.css +1 -1
- 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
|
@@ -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
|
-
}
|