@phillipsharring/graspr-framework 0.2.6 → 0.2.8
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/CLAUDE.md +257 -0
- package/package.json +1 -1
- package/src/helpers/handlebars-helpers.js +2 -2
- package/src/ui/confirm-dialog.js +4 -4
- package/src/ui/toast.js +4 -4
- package/styles/base.css +105 -12
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Graspr Frontend Framework — Claude Code Notes
|
|
2
|
+
|
|
3
|
+
## Architecture
|
|
4
|
+
|
|
5
|
+
HTMX + Handlebars + Tailwind CSS frontend framework, built with Vite. Installed via npm as `@phillipsharring/graspr-framework`.
|
|
6
|
+
|
|
7
|
+
## HTMX Boosted Navigation (CRITICAL)
|
|
8
|
+
|
|
9
|
+
Apps use `hx-boost="true"` on `<body>`. Every `<a>` and `<form>` is automatically AJAX-ified.
|
|
10
|
+
|
|
11
|
+
### How It Works
|
|
12
|
+
|
|
13
|
+
- `<body>` has `hx-target="#app" hx-select="#app" hx-swap="outerHTML"` — all boosted links swap only `<main id="app">`, preserving header/footer/templates.
|
|
14
|
+
- Elements outside `#app` (header widgets) persist across navigation.
|
|
15
|
+
- Elements inside `#app` are fresh DOM nodes after every nav.
|
|
16
|
+
- Cross-layout navigation forces a full page reload (layout mismatch detection).
|
|
17
|
+
|
|
18
|
+
### The Rule
|
|
19
|
+
|
|
20
|
+
**Every feature must work in BOTH scenarios:**
|
|
21
|
+
1. Full page load (direct URL, refresh, cross-layout nav)
|
|
22
|
+
2. Boosted nav (clicking a link within the same layout)
|
|
23
|
+
|
|
24
|
+
Key implications:
|
|
25
|
+
- `DOMContentLoaded` does NOT fire on boosted nav — only `htmx:afterSwap`/`htmx:afterSettle`
|
|
26
|
+
- HTMX lifecycle: `beforeSwap` → DOM swap → `afterSwap` → settle (processes `hx-trigger`) → `afterSettle`
|
|
27
|
+
- Fire custom events in `afterSettle`, NOT `afterSwap` — new elements' `hx-trigger` isn't wired until settle
|
|
28
|
+
|
|
29
|
+
## Self-Loading Elements
|
|
30
|
+
|
|
31
|
+
Elements that load their own content via HTMX:
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<div hx-get="/api/..." hx-trigger="auth-load, refresh"
|
|
35
|
+
hx-target="this" hx-select="unset" hx-swap="innerHTML"
|
|
36
|
+
handlebars-array-template="..." data-requires-auth>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- `hx-target="this"` + `hx-select="unset"` overrides body's `#app` targeting
|
|
40
|
+
- `data-requires-auth` needed for `auth-load` to fire
|
|
41
|
+
- Do NOT use `hx-disinherit` — breaks boosted nav by blocking `hx-boost` inheritance
|
|
42
|
+
- Do NOT use `hx-push-url="false"` — unnecessary and child links inherit it, breaking URL updates
|
|
43
|
+
|
|
44
|
+
## Auth System
|
|
45
|
+
|
|
46
|
+
- `auth.js` makes a single `/api/auth/me` call per page load, caches the result
|
|
47
|
+
- `checkAuth()` → `Promise<boolean>`
|
|
48
|
+
- `getAuthData()` → `Promise<{authenticated, username, permissions}>`
|
|
49
|
+
- `refreshAuthData()` → invalidates cache, re-fetches
|
|
50
|
+
|
|
51
|
+
### Auth-Gated Elements
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<!-- Loads content only when authenticated -->
|
|
55
|
+
<div data-requires-auth hx-trigger="auth-load, refresh" ...></div>
|
|
56
|
+
|
|
57
|
+
<!-- Visible only when logged in -->
|
|
58
|
+
<div data-show-if-auth hidden>...</div>
|
|
59
|
+
|
|
60
|
+
<!-- Visible only when logged out -->
|
|
61
|
+
<div data-hide-if-auth hidden>...</div>
|
|
62
|
+
|
|
63
|
+
<!-- Login/logout links (one of each, both start hidden) -->
|
|
64
|
+
<a data-auth-login hidden>Login</a>
|
|
65
|
+
<a data-auth-logout hidden>Logout</a>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `data-requires-auth` widgets must use `hx-trigger="auth-load"`, never `"load"` (fires before auth resolves)
|
|
69
|
+
- `data-show-if-auth` / `data-hide-if-auth` must include `hidden` attribute in markup (no flash)
|
|
70
|
+
- `auth-load` fires from `applyAuthState()` in `afterSettle`, NOT `afterSwap`
|
|
71
|
+
|
|
72
|
+
### Permission Gating
|
|
73
|
+
|
|
74
|
+
```html
|
|
75
|
+
<div data-requires-permission="admin.access" hidden>Admin only content</div>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Revealed by `applyAuthState()` if user has the permission.
|
|
79
|
+
|
|
80
|
+
## Dynamic Routes
|
|
81
|
+
|
|
82
|
+
File-based routing with `[id]` or `[slug]` parameters:
|
|
83
|
+
- `pages/things/[id]/index.html` → route `/things/{uuid}/`
|
|
84
|
+
- Both `[id]/index.html` and `[id].html` work
|
|
85
|
+
|
|
86
|
+
### Pattern
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
window.onReady(function() {
|
|
90
|
+
var params = App.getRouteParams('/things/[id]/');
|
|
91
|
+
if (!params || !params.id) return;
|
|
92
|
+
detail.setAttribute('hx-get', '/api/things/' + params.id);
|
|
93
|
+
window.htmx.process(detail);
|
|
94
|
+
window.htmx.trigger(detail, 'refresh');
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- Don't use `hx-trigger="auth-load"` with dynamically-set `hx-get` — race condition
|
|
99
|
+
- After changing `hx-get`, call `htmx.process(el)` before triggering
|
|
100
|
+
- CloudFront URL rewrite function must be updated for new dynamic routes in production
|
|
101
|
+
|
|
102
|
+
## `window.onReady(fn)` (CRITICAL)
|
|
103
|
+
|
|
104
|
+
Defined in each layout's `<head>`. Defers callback to `DOMContentLoaded` unless `readyState` is `'complete'`.
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
window.onReady = function(fn) {
|
|
108
|
+
if (document.readyState === 'complete') {
|
|
109
|
+
fn();
|
|
110
|
+
} else {
|
|
111
|
+
document.addEventListener('DOMContentLoaded', fn);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Must check for `'complete'`, NOT `'loading'`. Module scripts (`type="module"`) execute between `'interactive'` and `DOMContentLoaded`. Checking `readyState !== 'loading'` would run the callback immediately at `'interactive'`, before modules have set up `window.App`.
|
|
117
|
+
|
|
118
|
+
## Handlebars Templates
|
|
119
|
+
|
|
120
|
+
### Two Template Types
|
|
121
|
+
|
|
122
|
+
- `handlebars-array-template` — renders ONCE with `{ data: rows }`. Template must use `{{#each data}}`.
|
|
123
|
+
- `handlebars-template` (non-array) — spreads data into context. Use `{{data.field}}` for fields that collide with the response envelope (e.g. `status`).
|
|
124
|
+
|
|
125
|
+
### Template Gotchas
|
|
126
|
+
|
|
127
|
+
- `{{#if}}` as bare HTML attributes inside `<template>` tags gets mangled by the HTML parser. Duplicate the element with `{{#if}}/{{else}}` between tags instead.
|
|
128
|
+
- `{{> partial}}` in templates: `innerHTML` escapes `>` to `>`. Framework handles this centrally.
|
|
129
|
+
- `eq` helper is an expression helper, not a block helper. Use `{{#if (eq status "active")}}`, NOT `{{#eq status "active"}}` (outputs "true"/"false" as text).
|
|
130
|
+
- Other expression helpers: `neq`, `and`, `or`, `notin`, `truncate`, `upper`, `humanize`, `json`, `timeAgo`, `formatDateTime`.
|
|
131
|
+
|
|
132
|
+
## Modal Form API
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
App.ui.openFormModal({
|
|
136
|
+
templateId: 'my-form-template',
|
|
137
|
+
title: 'Edit Thing',
|
|
138
|
+
formUrl: '/api/things/' + id,
|
|
139
|
+
formMethod: 'patch',
|
|
140
|
+
fields: { name: 'current value' },
|
|
141
|
+
size: 'sm', // 'sm' | 'lg' | 'takeover'
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- `fields: { name: value }` — populates form inputs by name attribute
|
|
146
|
+
- `formUrl` + `formMethod` must both be specified
|
|
147
|
+
|
|
148
|
+
### Modal Refresh After Submit
|
|
149
|
+
|
|
150
|
+
On the `<form>` element:
|
|
151
|
+
- `data-refresh-target="#some-element"` — fires HTMX `refresh` trigger on that element
|
|
152
|
+
- `data-refresh-event="event-name"` — dispatches custom event on `document.body`
|
|
153
|
+
|
|
154
|
+
## Inline Script Rules
|
|
155
|
+
|
|
156
|
+
- Scope event listeners to elements INSIDE `#app` so they're GC'd on boosted nav
|
|
157
|
+
- Exception: `DOMContentLoaded` listeners are fine (fire once per page load)
|
|
158
|
+
- All inline scripts should be wrapped in `window.onReady(function() { ... })`
|
|
159
|
+
|
|
160
|
+
## Lifecycle Hooks
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
import { onPageLoad, onAfterSwap, onAfterSettle } from '@phillipsharring/graspr-framework';
|
|
164
|
+
onPageLoad(function(doc) { ... }); // DOMContentLoaded (safe if already fired)
|
|
165
|
+
onAfterSwap(function(target) { ... }); // #app swap via boosted nav
|
|
166
|
+
onAfterSettle(function(target) { ... }); // after HTMX processes hx-trigger on new elements
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Namespace Convention
|
|
170
|
+
|
|
171
|
+
Apps define a global namespace (e.g. `window.App`) in their entry point:
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
window.App = {
|
|
175
|
+
api: { fetch: apiFetch },
|
|
176
|
+
getRouteParams,
|
|
177
|
+
escapeHtml,
|
|
178
|
+
ui: {
|
|
179
|
+
toast: GrasprToast,
|
|
180
|
+
modal: { open, close, isOpen },
|
|
181
|
+
confirm: GrasprConfirm,
|
|
182
|
+
openFormModal,
|
|
183
|
+
},
|
|
184
|
+
hooks: { onAfterSwap, onAfterSettle, onPageLoad },
|
|
185
|
+
};
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The namespace name is app-specific (not framework-defined).
|
|
189
|
+
|
|
190
|
+
## Tailwind + Framework CSS
|
|
191
|
+
|
|
192
|
+
App's `style.css` must scan framework JS for dynamic class names:
|
|
193
|
+
|
|
194
|
+
```css
|
|
195
|
+
@source "../../node_modules/@phillipsharring/graspr-framework/src/**/*.js";
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Tailwind v4 doesn't detect classes in inline `<script>` tags. Use `@source inline("...")` to safelist dynamic class names built in JS.
|
|
199
|
+
|
|
200
|
+
## A/B Testing
|
|
201
|
+
|
|
202
|
+
### HTML Markup
|
|
203
|
+
|
|
204
|
+
```html
|
|
205
|
+
<!-- Variant A -->
|
|
206
|
+
<div data-ab-test="test-name" data-ab-variant="a" style="display:none">...</div>
|
|
207
|
+
<!-- Variant B -->
|
|
208
|
+
<div data-ab-test="test-name" data-ab-variant="b" style="display:none">...</div>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- Both variants start `display:none`
|
|
212
|
+
- Framework fetches assignments from `/api/ab/assignments`, shows the assigned variant, removes the other
|
|
213
|
+
- Fallback on API failure: shows variant "a"
|
|
214
|
+
- When a test is paused/completed: no assignment returned, both variants removed (hole in page)
|
|
215
|
+
- Intended workflow: run test → pick winner → update HTML to only have winning variant (remove `data-ab-test` attributes)
|
|
216
|
+
|
|
217
|
+
### Conversion Tracking
|
|
218
|
+
|
|
219
|
+
```html
|
|
220
|
+
<button data-ab-capture="signup">Sign Up</button>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Or programmatically: `App.ab.capture('event-name')`
|
|
224
|
+
|
|
225
|
+
## FOUC Prevention
|
|
226
|
+
|
|
227
|
+
For production builds, inject inline CSS that hides the page until the stylesheet loads:
|
|
228
|
+
|
|
229
|
+
```js
|
|
230
|
+
// In build script (html-compiler)
|
|
231
|
+
'<style>.css-loading{visibility:hidden}.css-loading body{visibility:hidden}</style>' +
|
|
232
|
+
'<link rel="stylesheet" href="..." onload="document.documentElement.classList.remove(\'css-loading\')" />'
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Add `css-loading` class to `<html>` in all layouts.
|
|
236
|
+
|
|
237
|
+
## Common Pitfalls
|
|
238
|
+
|
|
239
|
+
1. **Forgetting boosted nav**: Works on refresh but not link-click = boosted nav issue.
|
|
240
|
+
2. **`hx-trigger="load"` for auth widgets**: Fires before auth resolves. Use `auth-load`.
|
|
241
|
+
3. **`hx-disinherit`**: Breaks boosted nav. Never use it.
|
|
242
|
+
4. **`hx-push-url="false"` on self-loading elements**: Unnecessary and breaks child link URL updates.
|
|
243
|
+
5. **Body-level inheritance**: All children inherit `hx-target="#app" hx-select="#app"`. Self-loading elements must set `hx-target="this" hx-select="unset"`.
|
|
244
|
+
6. **Listener accumulation**: Scope to elements inside `#app`, not `document.body`.
|
|
245
|
+
7. **`afterSwap` vs `afterSettle`**: Custom events targeting `hx-trigger` must fire in `afterSettle`.
|
|
246
|
+
8. **`handlebars-template` on elements with links**: Boosted links inherit the template attribute. Framework handles this centrally.
|
|
247
|
+
9. **Template wrapper divs eat negative margins**: Put bleed classes on the wrapper, not template content.
|
|
248
|
+
10. **HTMX + json-enc can't send arrays**: `FormData` collapses duplicate keys. Collect in `htmx:configRequest` and `JSON.stringify()`.
|
|
249
|
+
11. **Module script timing**: `window.onReady` must check `readyState === 'complete'`, not `!== 'loading'`, or callbacks run before modules define globals like `window.App`.
|
|
250
|
+
|
|
251
|
+
## S3/CloudFront Deployment
|
|
252
|
+
|
|
253
|
+
- `aws s3 sync` flag order matters: `--exclude "*" --include "*.html"` (exclude FIRST).
|
|
254
|
+
- Reversed order excludes everything.
|
|
255
|
+
- HTML files get short cache (`max-age=300`), assets get immutable cache (`max-age=31536000`).
|
|
256
|
+
- CloudFront Function handles URL rewriting for dynamic routes (`/things/{uuid}/` → `/things/[id]/index.html`).
|
|
257
|
+
- Don't use `--delete` on the HTML sync pass — it deletes assets uploaded by the first pass.
|
package/package.json
CHANGED
|
@@ -6,10 +6,10 @@ export function registerHandlebarsHelpers(Handlebars) {
|
|
|
6
6
|
// ─── Partials ───
|
|
7
7
|
|
|
8
8
|
Handlebars.registerPartial('formButtons', `<div class="flex justify-end gap-3 pt-2">
|
|
9
|
-
<button type="button" class="border rounded px-3 py-2
|
|
9
|
+
<button type="button" class="graspr-btn-cancel border rounded px-3 py-2" data-modal-close>
|
|
10
10
|
Cancel
|
|
11
11
|
</button>
|
|
12
|
-
<button type="submit" class="rounded px-3 py-2
|
|
12
|
+
<button type="submit" class="graspr-btn-primary rounded px-3 py-2">
|
|
13
13
|
{{label}}
|
|
14
14
|
</button>
|
|
15
15
|
</div>`);
|
package/src/ui/confirm-dialog.js
CHANGED
|
@@ -235,11 +235,11 @@ async function fetchPendingCount(progressUrl) {
|
|
|
235
235
|
function renderProgressUI(total, { progressLabel = 'Processing...', progressItemLabel = 'processed' } = {}) {
|
|
236
236
|
return `
|
|
237
237
|
<div class="space-y-4 py-2" data-progress-container>
|
|
238
|
-
<p class="text-sm
|
|
239
|
-
<div class="w-full
|
|
240
|
-
<div class="confirm-progress-bar
|
|
238
|
+
<p class="text-sm font-medium confirm-message">${progressLabel}</p>
|
|
239
|
+
<div class="w-full rounded-full h-3 overflow-hidden" style="background: var(--graspr-progress-bg)">
|
|
240
|
+
<div class="confirm-progress-bar h-3 rounded-full" style="width: 0%; background: var(--graspr-progress-fill)"></div>
|
|
241
241
|
</div>
|
|
242
|
-
<p class="text-sm
|
|
242
|
+
<p class="text-sm confirm-subtext" data-progress-text>0 of ${total} ${progressItemLabel}</p>
|
|
243
243
|
</div>
|
|
244
244
|
`;
|
|
245
245
|
}
|
package/src/ui/toast.js
CHANGED
|
@@ -89,17 +89,17 @@ export const GrasprToast = {
|
|
|
89
89
|
export function registerToastHelpers(Handlebars) {
|
|
90
90
|
Handlebars.registerHelper('toastClass', (status) => {
|
|
91
91
|
const s = String(status || 'success').toLowerCase();
|
|
92
|
-
// Note: accept the user's typo "eror" as error.
|
|
93
92
|
const normalized = s === 'eror' ? 'error' : s;
|
|
94
93
|
|
|
94
|
+
// Use CSS-var-driven classes for themeable toasts
|
|
95
95
|
switch (normalized) {
|
|
96
96
|
case 'warning':
|
|
97
|
-
return '
|
|
97
|
+
return 'graspr-toast-warning';
|
|
98
98
|
case 'error':
|
|
99
|
-
return '
|
|
99
|
+
return 'graspr-toast-error';
|
|
100
100
|
case 'success':
|
|
101
101
|
default:
|
|
102
|
-
return '
|
|
102
|
+
return 'graspr-toast-success';
|
|
103
103
|
}
|
|
104
104
|
});
|
|
105
105
|
}
|
package/styles/base.css
CHANGED
|
@@ -1,9 +1,78 @@
|
|
|
1
1
|
/* Graspr Framework — base styles
|
|
2
2
|
Import this from your app's CSS after @import 'tailwindcss' and @source directives. */
|
|
3
3
|
|
|
4
|
+
/* ── Theme variables ──
|
|
5
|
+
Override these in your app's CSS to customize the framework's UI components.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
:root {
|
|
9
|
+
--graspr-text: #1C1C1C;
|
|
10
|
+
--graspr-primary: #A3B8A5;
|
|
11
|
+
--graspr-primary-hover: #8AA38D;
|
|
12
|
+
--graspr-surface: #FEFBF8;
|
|
13
|
+
}
|
|
14
|
+
*/
|
|
15
|
+
:root {
|
|
16
|
+
/* Text */
|
|
17
|
+
--graspr-text: rgb(15 23 42); /* slate-900 */
|
|
18
|
+
--graspr-text-secondary: rgb(71 85 105); /* slate-600 */
|
|
19
|
+
--graspr-text-muted: rgb(100 116 139); /* slate-500 */
|
|
20
|
+
|
|
21
|
+
/* Primary action (confirm buttons, active states) */
|
|
22
|
+
--graspr-primary: rgb(15 23 42); /* slate-900 */
|
|
23
|
+
--graspr-primary-hover: rgb(30 41 59); /* slate-800 */
|
|
24
|
+
--graspr-primary-text: white;
|
|
25
|
+
|
|
26
|
+
/* Secondary/cancel */
|
|
27
|
+
--graspr-border: rgb(203 213 225); /* slate-300 */
|
|
28
|
+
--graspr-border-light: rgb(226 232 240); /* slate-200 */
|
|
29
|
+
--graspr-surface: rgb(248 250 252); /* slate-50 */
|
|
30
|
+
--graspr-surface-hover: rgb(241 245 249); /* slate-100 */
|
|
31
|
+
|
|
32
|
+
/* Toast: success */
|
|
33
|
+
--graspr-toast-success-bg: rgb(22 163 74); /* green-600 */
|
|
34
|
+
--graspr-toast-success-text: white;
|
|
35
|
+
--graspr-toast-success-border: rgb(21 128 61); /* green-700 */
|
|
36
|
+
|
|
37
|
+
/* Toast: warning */
|
|
38
|
+
--graspr-toast-warning-bg: rgb(254 243 199); /* amber-100 */
|
|
39
|
+
--graspr-toast-warning-text: rgb(120 53 15); /* amber-900 */
|
|
40
|
+
--graspr-toast-warning-border: rgb(253 230 138); /* amber-200 */
|
|
41
|
+
|
|
42
|
+
/* Toast: error */
|
|
43
|
+
--graspr-toast-error-bg: rgb(220 38 38); /* red-600 */
|
|
44
|
+
--graspr-toast-error-text: white;
|
|
45
|
+
--graspr-toast-error-border: rgb(185 28 28); /* red-700 */
|
|
46
|
+
|
|
47
|
+
/* Modal */
|
|
48
|
+
--graspr-modal-bg: white;
|
|
49
|
+
--graspr-modal-border: var(--graspr-border-light);
|
|
50
|
+
--graspr-modal-backdrop: rgba(0, 0, 0, 0.25);
|
|
51
|
+
|
|
52
|
+
/* Progress bar */
|
|
53
|
+
--graspr-progress-bg: var(--graspr-border-light);
|
|
54
|
+
--graspr-progress-fill: var(--graspr-text-secondary);
|
|
55
|
+
}
|
|
56
|
+
|
|
4
57
|
/* Override inline FOUC prevention — body becomes visible once this stylesheet loads */
|
|
5
58
|
body { opacity: 1; }
|
|
6
59
|
|
|
60
|
+
/* ── Buttons ── */
|
|
61
|
+
.graspr-btn-primary {
|
|
62
|
+
background: var(--graspr-primary);
|
|
63
|
+
color: var(--graspr-primary-text);
|
|
64
|
+
}
|
|
65
|
+
.graspr-btn-primary:hover {
|
|
66
|
+
background: var(--graspr-primary-hover);
|
|
67
|
+
}
|
|
68
|
+
.graspr-btn-cancel {
|
|
69
|
+
border-color: var(--graspr-border);
|
|
70
|
+
color: var(--graspr-text);
|
|
71
|
+
}
|
|
72
|
+
.graspr-btn-cancel:hover {
|
|
73
|
+
background: var(--graspr-surface);
|
|
74
|
+
}
|
|
75
|
+
|
|
7
76
|
/* Form error styles */
|
|
8
77
|
.field-error {
|
|
9
78
|
@apply border-red-600;
|
|
@@ -56,12 +125,19 @@ button[disabled] {
|
|
|
56
125
|
transition: transform 150ms ease-out;
|
|
57
126
|
transform: translateY(2rem);
|
|
58
127
|
outline: none;
|
|
128
|
+
background: var(--graspr-modal-bg);
|
|
129
|
+
border-color: var(--graspr-modal-border);
|
|
59
130
|
}
|
|
60
131
|
|
|
61
132
|
#global-modal.modal-open [role="dialog"] {
|
|
62
133
|
transform: translateY(0);
|
|
63
134
|
}
|
|
64
135
|
|
|
136
|
+
/* Modal backdrop */
|
|
137
|
+
#global-modal > .absolute.inset-0 {
|
|
138
|
+
background: var(--graspr-modal-backdrop);
|
|
139
|
+
}
|
|
140
|
+
|
|
65
141
|
/* Darker backdrop when examples layout is active */
|
|
66
142
|
body:has(#app[data-layout="examples"]) #global-modal .absolute.inset-0 {
|
|
67
143
|
background: rgba(255, 255, 255, 0.50);
|
|
@@ -128,8 +204,8 @@ body:has(#app[data-layout="examples"]) #global-modal .absolute.inset-0 {
|
|
|
128
204
|
}
|
|
129
205
|
|
|
130
206
|
/* Sortable drag-and-drop */
|
|
131
|
-
.sortable-ghost { opacity: 0.4; background-color:
|
|
132
|
-
.sortable-chosen { background-color:
|
|
207
|
+
.sortable-ghost { opacity: 0.4; background-color: var(--graspr-surface); }
|
|
208
|
+
.sortable-chosen { background-color: var(--graspr-surface); }
|
|
133
209
|
.sortable-disabled .drag-handle { visibility: hidden; pointer-events: none; }
|
|
134
210
|
|
|
135
211
|
/* Active nav link — faux-bold via text-shadow to avoid layout shift */
|
|
@@ -137,20 +213,37 @@ body:has(#app[data-layout="examples"]) #global-modal .absolute.inset-0 {
|
|
|
137
213
|
text-shadow: 0 0 0.01px currentColor, 0 0 0.01px currentColor;
|
|
138
214
|
}
|
|
139
215
|
|
|
140
|
-
/*
|
|
141
|
-
.confirm-message { color:
|
|
142
|
-
.confirm-subtext { color:
|
|
143
|
-
.confirm-checkbox { color:
|
|
216
|
+
/* ── Confirm dialog ── */
|
|
217
|
+
.confirm-message { color: var(--graspr-text); }
|
|
218
|
+
.confirm-subtext { color: var(--graspr-text-secondary); }
|
|
219
|
+
.confirm-checkbox { color: var(--graspr-text); }
|
|
144
220
|
.confirm-cancel-btn {
|
|
145
|
-
border-color:
|
|
146
|
-
color:
|
|
221
|
+
border-color: var(--graspr-border);
|
|
222
|
+
color: var(--graspr-text);
|
|
147
223
|
}
|
|
148
|
-
.confirm-cancel-btn:hover { background:
|
|
224
|
+
.confirm-cancel-btn:hover { background: var(--graspr-surface); }
|
|
149
225
|
.confirm-ok-btn {
|
|
150
|
-
background:
|
|
151
|
-
color:
|
|
226
|
+
background: var(--graspr-primary);
|
|
227
|
+
color: var(--graspr-primary-text);
|
|
228
|
+
}
|
|
229
|
+
.confirm-ok-btn:hover { background: var(--graspr-primary-hover); }
|
|
230
|
+
|
|
231
|
+
/* ── Toast variants ── */
|
|
232
|
+
.graspr-toast-success {
|
|
233
|
+
background: var(--graspr-toast-success-bg);
|
|
234
|
+
color: var(--graspr-toast-success-text);
|
|
235
|
+
border-color: var(--graspr-toast-success-border);
|
|
236
|
+
}
|
|
237
|
+
.graspr-toast-warning {
|
|
238
|
+
background: var(--graspr-toast-warning-bg);
|
|
239
|
+
color: var(--graspr-toast-warning-text);
|
|
240
|
+
border-color: var(--graspr-toast-warning-border);
|
|
241
|
+
}
|
|
242
|
+
.graspr-toast-error {
|
|
243
|
+
background: var(--graspr-toast-error-bg);
|
|
244
|
+
color: var(--graspr-toast-error-text);
|
|
245
|
+
border-color: var(--graspr-toast-error-border);
|
|
152
246
|
}
|
|
153
|
-
.confirm-ok-btn:hover { background: rgb(30 41 59); } /* slate-800 */
|
|
154
247
|
|
|
155
248
|
/* Table column sort headers */
|
|
156
249
|
th[data-sort] { cursor: pointer; user-select: none; }
|