gradient-lab 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,135 @@
1
+ # AGENTS.md — Guide for AI agents working on Gradient Lab
2
+
3
+ ## Quick orientation
4
+
5
+ This is a zero-build, zero-dependency web app. Everything is native ES modules served by a small Node.js static file server. No bundler, no transpiler, no npm packages at runtime.
6
+
7
+ ## Module layout
8
+
9
+ | File | Purpose |
10
+ |------|---------|
11
+ | `reactive-framework.js` | Generic reactive primitives — `Scope`, `Signal`, `Concern`, `ReactiveHTMLElement`. **No app-specific code here.** |
12
+ | `utils.js` | Pure functions — color math, CSS string generation, data helpers. No DOM, no side effects. |
13
+ | `components/gradient-element.js` | App-specific base class that adds `.app`, `.state`, `.commit()`, `.watchState()` on top of `ReactiveHTMLElement`. |
14
+ | `components/gradient-lab-app.js` | Root component. Owns the single `Signal<AppState>`. All mutations go through `commit(draft => ...)`. |
15
+ | `components/*.js` | One file per custom element. Each file registers its element at the bottom with `customElements.define(...)`. |
16
+ | `app.js` | Entry point — imports every component file to trigger registration. No other logic here. |
17
+ | `app.css` | All application CSS. No CSS-in-JS. |
18
+ | `index.html` | Static HTML tree of web components + importmap + CDN links for Bootstrap. |
19
+
20
+ ## Importmap (bare specifier aliases)
21
+
22
+ Defined in `index.html`:
23
+
24
+ ```json
25
+ {
26
+ "imports": {
27
+ "reactive-framework": "./reactive-framework.js",
28
+ "utils": "./utils.js",
29
+ "gradient-element": "./components/gradient-element.js"
30
+ }
31
+ }
32
+ ```
33
+
34
+ Use bare specifiers in component imports:
35
+ ```js
36
+ import { ReactiveHTMLElement } from "reactive-framework"; // correct
37
+ import { escapeHtml } from "utils"; // correct
38
+ import { GradientElement } from "gradient-element"; // correct
39
+ ```
40
+
41
+ Do **not** use relative paths for these three — it defeats the importmap's purpose.
42
+
43
+ ## How to add a new web component
44
+
45
+ 1. Create `components/my-element.js`:
46
+
47
+ ```js
48
+ import { GradientElement } from "gradient-element";
49
+ import { escapeHtml } from "utils"; // import only what you need
50
+
51
+ class MyElement extends GradientElement {
52
+ mount() {
53
+ this.innerHTML = `<div>Hello</div>`;
54
+ this.watchState(state => this.render(state));
55
+ }
56
+
57
+ render(state) {
58
+ // update DOM from state — keep it declarative
59
+ }
60
+ }
61
+
62
+ customElements.define("my-element", MyElement);
63
+ export { MyElement };
64
+ ```
65
+
66
+ 2. Add the import to `app.js`:
67
+ ```js
68
+ import "./components/my-element.js";
69
+ ```
70
+
71
+ 3. Add to `index.html` body where it belongs in the component tree.
72
+
73
+ 4. Add `my-element { display: block; margin-bottom: 1rem; }` to `app.css` if it needs block layout.
74
+
75
+ ## How to add a new utility function
76
+
77
+ Add it to `utils.js` as a named export. No side effects, no DOM access, no imports from framework files.
78
+
79
+ ## How to modify application state shape
80
+
81
+ 1. Add the new field with its default value in `GradientLabApp` constructor (`components/gradient-lab-app.js`).
82
+ 2. If the field needs deep-copying on commit, update the `commit()` method's draft construction.
83
+ 3. Update `copyLine` or `copyLibraryItem` in `utils.js` if the new field lives inside a line or library item.
84
+
85
+ ## State mutation rules
86
+
87
+ - **Never mutate `this.state` directly.** Always use `this.commit(draft => { draft.field = value; })`.
88
+ - `commit()` deep-copies `lines` and `library` before passing the draft, so mutations are safe.
89
+ - `commit()` sets the signal, which triggers all `watchState` subscribers synchronously.
90
+
91
+ ## Reactive framework rules
92
+
93
+ ### Scope / lifecycle
94
+ - `Scope.collect(fn)` registers a cleanup function.
95
+ - `ReactiveHTMLElement` creates a `Concern` (a `Scope` subclass) in the constructor.
96
+ - `mount()` is called once when the element first connects to the DOM.
97
+ - `disconnectedCallback()` disposes the concern — all subscriptions, listeners, timers.
98
+ - Always register listeners via `this.concern.on(...)` or `this.on(...)`, **not** `addEventListener` directly, so they are disposed automatically.
99
+
100
+ ### Signal
101
+ - `signal.value = x` triggers subscribers only if `x !== signal.value` (by `Object.is`).
102
+ - Use `signal.mutate(fn)` when you need to notify subscribers after mutating the same object reference.
103
+ - Use `signal.set(x, { force: true })` to force notification even when value is equal.
104
+
105
+ ### Concern bindings
106
+ - `bindValue(source, input)` — two-way binding for text inputs.
107
+ - `bindChecked(source, checkbox)` — two-way binding for checkboxes.
108
+ - `bindText(source, node)` — one-way text content binding.
109
+ - `bindStyle(source, el, property, fn?)` — one-way style property binding.
110
+ - `render(source, host, fn)` — re-renders host innerHTML/children when signal changes.
111
+
112
+ ## Rendering approach
113
+
114
+ Components use **full innerHTML re-render** on state change (no virtual DOM diffing). This is intentional and simple. If a component owns interactive elements that must survive re-renders (e.g., a focused input), use targeted DOM updates instead of wholesale innerHTML replacement.
115
+
116
+ ## CSS conventions
117
+
118
+ - All custom properties are defined under `:root` in `app.css` with `--lux-*` prefix.
119
+ - Use Bootstrap utility classes for layout and spacing.
120
+ - App-specific structural classes live in `app.css`. Do not add `<style>` blocks to component JS files.
121
+ - The `--lux-gold` (`#ffc774`) color is used for selection, active states, and primary actions.
122
+ - The `--lux-cyan` (`#69d8ff`) color is used for informational labels.
123
+
124
+ ## Server
125
+
126
+ `server.js` serves all files from the project root. It handles MIME types and blocks path traversal. No changes needed unless you add a new file extension.
127
+
128
+ ## Do not
129
+
130
+ - Add a build step, bundler, or transpiler.
131
+ - Add npm runtime dependencies.
132
+ - Use Shadow DOM (all components use light DOM for Bootstrap compatibility).
133
+ - Put application logic in `reactive-framework.js` — it must stay generic.
134
+ - Put DOM code in `utils.js` — it must stay pure.
135
+ - Call `addEventListener` directly in component code — always go through `this.concern.on` or `this.on`.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Gradient Lab
2
+
3
+ An image-sampled CSS gradient studio. Drop a photo, draw sampling lines across it, refine the color stops, and export Bootstrap-ready CSS gradient classes.
4
+
5
+ ## Running
6
+
7
+ ```bash
8
+ npm start
9
+ ```
10
+
11
+ The dev server opens the app at `http://localhost:48187` (or next available port). Files are served from the project root — no build step required.
12
+
13
+ ## File structure
14
+
15
+ ```
16
+ gradient-lab/
17
+ ├── index.html # App shell — importmap + component tree
18
+ ├── app.js # Entry point — imports all components
19
+ ├── app.css # Application stylesheet
20
+ ├── reactive-framework.js # Generic reactive primitives (no dependencies)
21
+ ├── utils.js # Pure utility functions (color math, CSS generation)
22
+ ├── server.js # Zero-dependency static file server
23
+ └── components/
24
+ ├── gradient-element.js # GradientElement base class
25
+ ├── gradient-lab-app.js # Root component — owns all application state
26
+ ├── app-shell.js # Layout wrapper
27
+ ├── app-hero.js # Header section
28
+ ├── gradient-workbench.js # Two-column layout for sampler + editor
29
+ ├── lux-card.js # Styled card with title/icon/subtitle slots
30
+ ├── sampler-toolbar.js # File upload, sample count, angle controls
31
+ ├── image-sampling-stage.js # Canvas + SVG overlay, drag-to-sample
32
+ ├── sampled-gradient-list.js # List of sampled gradient lines
33
+ ├── gradient-editor.js # Stop bar, color pickers, CSS preview
34
+ ├── library-controls.js # Prefix / domain inputs, copy-all button
35
+ ├── gradient-library.js # Grid of saved gradients
36
+ ├── library-code.js # Collapsible full CSS output
37
+ └── toast-zone.js # Fixed toast notifications
38
+ ```
39
+
40
+ ## Module graph
41
+
42
+ ```
43
+ reactive-framework.js (no imports)
44
+ utils.js (no imports)
45
+ └── components/gradient-element.js ← reactive-framework
46
+ └── components/*.js ← gradient-element + utils
47
+ └── app.js ← all components
48
+ ```
49
+
50
+ The importmap in `index.html` resolves bare specifiers:
51
+
52
+ | Specifier | File |
53
+ |--------------------|-----------------------------------|
54
+ | `reactive-framework` | `./reactive-framework.js` |
55
+ | `utils` | `./utils.js` |
56
+ | `gradient-element` | `./components/gradient-element.js`|
57
+
58
+ ## Reactive framework overview
59
+
60
+ ### `Scope`
61
+ Tracks disposables (functions, objects with `.dispose()`, `Symbol.dispose`). Disposes them in reverse order on `.dispose()`. Supports `timeout`, `interval`, `frame`, and child scopes.
62
+
63
+ ### `Signal<T>`
64
+ A reactive value cell. Notifies subscribers only when the value changes (`Object.is` equality by default). Supports `.subscribe(fn)`, `.map(fn)`, `.once(fn)`, `.mutate(fn)`.
65
+
66
+ ### `Concern extends Scope`
67
+ Combines a `Signal` registry with binding helpers. Key methods:
68
+ - `signal(name, value?)` — get or create a named signal
69
+ - `effect(sources, fn)` — run `fn` whenever any source changes
70
+ - `computed(name, sources, fn)` — derived signal
71
+ - `on(target, event, handler)` — event listener tracked for disposal
72
+ - `bindText / bindHTML / bindValue / bindChecked / bindStyle / bindClass / bindAttribute` — one-way or two-way DOM bindings
73
+ - `render(source, host, fn)` — render signal value into a DOM host
74
+
75
+ ### `ReactiveHTMLElement extends HTMLElement`
76
+ Base class for all web components. Mounts once on first `connectedCallback`, disposes on `disconnectedCallback`. Exposes `signal`, `subscribe`, `effect`, `computed`, `on`, `delegate`, `emit`, `$`, `$$`, `html`, `appendTemplate`.
77
+
78
+ ### `GradientElement extends ReactiveHTMLElement`
79
+ Thin base for app-specific components. Adds `.app`, `.state`, `.commit(mutator)`, and `.watchState(fn)`.
80
+
81
+ ## Application state
82
+
83
+ All state lives in a single `Signal<AppState>` on `<gradient-lab-app>`. Components read it via `this.state` and write via `this.commit(draft => { ... })`, which deep-copies lines and library before mutating.
84
+
85
+ ```js
86
+ {
87
+ image: HTMLImageElement | null,
88
+ imageName: string,
89
+ sampleCount: number, // 2–16
90
+ direction: string, // e.g. "90deg"
91
+ domain: string, // for permalink generation
92
+ prefix: string, // CSS class prefix, e.g. "gr-"
93
+ selectedLineId: string | null,
94
+ selectedEndpoint: "start" | "end" | null,
95
+ selectedStopId: string | null,
96
+ lines: GradientLine[],
97
+ library: LibraryItem[],
98
+ }
99
+ ```
100
+
101
+ ## No build step
102
+
103
+ Everything runs as native ES modules. No bundler, no transpiler, no npm dependencies at runtime. Bootstrap and Bootstrap Icons are loaded from CDN.
package/app.css ADDED
@@ -0,0 +1,342 @@
1
+ :root {
2
+ --lux-bg-0: #05070b;
3
+ --lux-bg-1: #08111f;
4
+ --lux-bg-2: #0d1a2d;
5
+ --lux-card: rgba(13, 23, 39, .82);
6
+ --lux-border: rgba(178, 208, 255, .16);
7
+ --lux-text: #edf4ff;
8
+ --lux-muted: #93a7c4;
9
+ --lux-gold: #ffc774;
10
+ --lux-cyan: #69d8ff;
11
+ --lux-shadow: 0 24px 80px rgba(0, 0, 0, .48);
12
+ --bs-body-bg: var(--lux-bg-0);
13
+ --bs-body-color: var(--lux-text);
14
+ --bs-border-color: var(--lux-border);
15
+ --bs-secondary-color: var(--lux-muted);
16
+ --bs-tertiary-bg: rgba(255, 255, 255, .04);
17
+ }
18
+
19
+ body {
20
+ min-height: 100vh;
21
+ background:
22
+ radial-gradient(circle at 8% 0%, rgba(87, 159, 255, .18), transparent 31rem),
23
+ radial-gradient(circle at 92% 12%, rgba(255, 133, 78, .13), transparent 30rem),
24
+ radial-gradient(circle at 50% 100%, rgba(72, 255, 201, .10), transparent 28rem),
25
+ linear-gradient(135deg, var(--lux-bg-0), var(--lux-bg-1) 42%, #030508);
26
+ color: var(--lux-text);
27
+ }
28
+
29
+ gradient-lab-app,
30
+ app-shell,
31
+ app-hero,
32
+ gradient-workbench,
33
+ lux-card,
34
+ sampler-toolbar,
35
+ image-sampling-stage,
36
+ sampled-gradient-list,
37
+ gradient-editor,
38
+ library-controls,
39
+ gradient-library,
40
+ library-code,
41
+ toast-zone {
42
+ display: block;
43
+ margin-bottom: 1rem;
44
+ }
45
+
46
+ .app-shell {
47
+ max-width: 1440px;
48
+ }
49
+
50
+ .hero-card {
51
+ border: 1px solid var(--lux-border);
52
+ border-radius: 2rem;
53
+ background:
54
+ linear-gradient(135deg, rgba(255, 255, 255, .09), rgba(255, 255, 255, .03)),
55
+ linear-gradient(120deg, rgba(255, 199, 116, .11), rgba(105, 216, 255, .05));
56
+ box-shadow: var(--lux-shadow);
57
+ overflow: hidden;
58
+ position: relative;
59
+ }
60
+
61
+ .lux-card {
62
+ border: 1px solid var(--lux-border);
63
+ border-radius: 1.5rem;
64
+ background:
65
+ linear-gradient(180deg, rgba(255, 255, 255, .07), rgba(255, 255, 255, .025)),
66
+ var(--lux-card);
67
+ box-shadow: 0 18px 58px rgba(0, 0, 0, .32);
68
+ backdrop-filter: blur(18px);
69
+ overflow: hidden;
70
+ }
71
+
72
+ .lux-card .card-header {
73
+ border-color: var(--lux-border);
74
+ background: rgba(255, 255, 255, .035);
75
+ }
76
+
77
+ .text-lux-muted { color: var(--lux-muted); }
78
+ .text-lux-gold { color: var(--lux-gold); }
79
+ .text-lux-cyan { color: var(--lux-cyan); }
80
+
81
+ .soft-pill {
82
+ border: 1px solid var(--lux-border);
83
+ background: rgba(255, 255, 255, .06);
84
+ border-radius: 999px;
85
+ padding: .35rem .75rem;
86
+ color: var(--lux-muted);
87
+ font-size: .85rem;
88
+ }
89
+
90
+ .stage-frame {
91
+ position: relative;
92
+ min-height: 430px;
93
+ border-radius: 1.35rem;
94
+ border: 1px solid rgba(255, 255, 255, .14);
95
+ background:
96
+ linear-gradient(135deg, rgba(255,255,255,.05), rgba(255,255,255,.015)),
97
+ repeating-conic-gradient(from 45deg, rgba(255,255,255,.025) 0% 25%, transparent 0% 50%) 50% / 34px 34px,
98
+ #07101d;
99
+ display: grid;
100
+ place-items: center;
101
+ overflow: hidden;
102
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.08), inset 0 -30px 90px rgba(0,0,0,.32);
103
+ touch-action: none;
104
+ }
105
+
106
+ .canvas-wrap {
107
+ position: relative;
108
+ display: block;
109
+ max-width: 100%;
110
+ max-height: 620px;
111
+ border-radius: 1rem;
112
+ overflow: hidden;
113
+ box-shadow: 0 18px 58px rgba(0,0,0,.45);
114
+ }
115
+
116
+ .image-canvas,
117
+ .overlay-svg {
118
+ display: block;
119
+ max-width: 100%;
120
+ user-select: none;
121
+ touch-action: none;
122
+ }
123
+
124
+ .overlay-svg {
125
+ position: absolute;
126
+ inset: 0;
127
+ cursor: crosshair;
128
+ }
129
+
130
+ .drop-hint {
131
+ position: absolute;
132
+ inset: 1rem;
133
+ display: grid;
134
+ place-items: center;
135
+ text-align: center;
136
+ border: 1px dashed rgba(255,255,255,.22);
137
+ border-radius: 1.15rem;
138
+ background: rgba(0,0,0,.18);
139
+ z-index: 3;
140
+ }
141
+
142
+ .drop-hint.hidden { display: none; }
143
+
144
+ .drop-hint .display-icon {
145
+ width: 4.5rem;
146
+ height: 4.5rem;
147
+ display: grid;
148
+ place-items: center;
149
+ margin-inline: auto;
150
+ border-radius: 1.35rem;
151
+ color: #07101d;
152
+ background: linear-gradient(135deg, var(--lux-gold), var(--lux-cyan));
153
+ box-shadow: 0 20px 50px rgba(105, 216, 255, .16);
154
+ font-size: 2rem;
155
+ }
156
+
157
+ .form-range::-webkit-slider-thumb { background: var(--lux-gold); }
158
+ .form-range::-moz-range-thumb { background: var(--lux-gold); }
159
+
160
+ .gradient-display {
161
+ min-height: 150px;
162
+ border-radius: 1.25rem;
163
+ border: 0;
164
+ box-shadow:
165
+ inset 0 0 0 1px rgba(0,0,0,.28),
166
+ inset 0 1px 0 rgba(255,255,255,.10),
167
+ 0 22px 60px rgba(0,0,0,.28);
168
+ overflow: hidden;
169
+ position: relative;
170
+ }
171
+
172
+ .gradient-display::after,
173
+ .gradient-chip .preview::after,
174
+ .library-well::after {
175
+ content: "";
176
+ position: absolute;
177
+ inset: 0;
178
+ border-radius: inherit;
179
+ pointer-events: none;
180
+ background: linear-gradient(180deg, rgba(255,255,255,.18), transparent 34%, rgba(0,0,0,.12));
181
+ opacity: .82;
182
+ }
183
+
184
+ .gradient-track {
185
+ position: relative;
186
+ height: 54px;
187
+ border-radius: 999px;
188
+ border: 1px solid rgba(255,255,255,.17);
189
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.14), inset 0 -16px 32px rgba(0,0,0,.24);
190
+ cursor: copy;
191
+ }
192
+
193
+ .editor-stop {
194
+ position: absolute;
195
+ top: 50%;
196
+ width: 19px;
197
+ height: 34px;
198
+ border: 2px solid rgba(255,255,255,.86);
199
+ border-radius: 999px 999px 7px 7px;
200
+ transform: translate(-50%, -50%);
201
+ box-shadow: 0 10px 24px rgba(0,0,0,.4);
202
+ cursor: ew-resize;
203
+ }
204
+
205
+ .editor-stop.selected {
206
+ outline: 3px solid var(--lux-gold);
207
+ outline-offset: 3px;
208
+ }
209
+
210
+ .stop-row {
211
+ border: 1px solid rgba(255,255,255,.10);
212
+ border-radius: 1rem;
213
+ background: rgba(255,255,255,.035);
214
+ padding: .65rem;
215
+ }
216
+
217
+ .stop-row.selected {
218
+ border-color: rgba(255, 199, 116, .5);
219
+ box-shadow: 0 0 0 3px rgba(255, 199, 116, .10);
220
+ }
221
+
222
+ .gradient-stack {
223
+ display: grid;
224
+ gap: .75rem;
225
+ }
226
+
227
+ .gradient-chip {
228
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,.11);
229
+ border-radius: 1.1rem;
230
+ background: rgba(255,255,255,.035);
231
+ overflow: hidden;
232
+ transition: box-shadow .16s ease, transform .16s ease, background .16s ease;
233
+ }
234
+
235
+ .gradient-chip:hover,
236
+ .gradient-chip.selected {
237
+ box-shadow: inset 0 0 0 1px rgba(255, 199, 116, .52);
238
+ background: rgba(255, 255, 255, .055);
239
+ }
240
+
241
+ .gradient-chip.selected { transform: translateY(-1px); }
242
+
243
+ .gradient-chip .preview {
244
+ position: relative;
245
+ min-height: 52px;
246
+ border-radius: .85rem;
247
+ overflow: hidden;
248
+ isolation: isolate;
249
+ box-shadow:
250
+ inset 0 0 0 1px rgba(255,255,255,.16),
251
+ inset 0 1px 0 rgba(255,255,255,.18),
252
+ inset 0 -18px 32px rgba(0,0,0,.16);
253
+ background-clip: padding-box;
254
+ }
255
+
256
+ .library-grid {
257
+ display: grid;
258
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
259
+ gap: 1rem;
260
+ }
261
+
262
+ .library-item {
263
+ border: 1px solid rgba(255,255,255,.11);
264
+ border-radius: 1.2rem;
265
+ background: rgba(255,255,255,.035);
266
+ padding: .8rem;
267
+ }
268
+
269
+ .library-well {
270
+ position: relative;
271
+ min-height: 90px;
272
+ border: 0;
273
+ border-radius: 1rem;
274
+ overflow: hidden;
275
+ box-shadow:
276
+ inset 0 0 0 1px rgba(0,0,0,.30),
277
+ inset 0 1px 0 rgba(255,255,255,.08);
278
+ }
279
+
280
+ textarea.codebox {
281
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
282
+ min-height: 154px;
283
+ color: #d8e8ff;
284
+ background: rgba(2, 8, 16, .82);
285
+ border-color: rgba(255,255,255,.12);
286
+ }
287
+
288
+ .btn-lux {
289
+ --bs-btn-color: #08101d;
290
+ --bs-btn-bg: var(--lux-gold);
291
+ --bs-btn-border-color: var(--lux-gold);
292
+ --bs-btn-hover-color: #07101d;
293
+ --bs-btn-hover-bg: #ffd798;
294
+ --bs-btn-hover-border-color: #ffd798;
295
+ --bs-btn-active-bg: #dca95f;
296
+ --bs-btn-active-border-color: #dca95f;
297
+ box-shadow: 0 12px 28px rgba(255, 199, 116, .16);
298
+ }
299
+
300
+ .btn-soft {
301
+ --bs-btn-color: #dceaff;
302
+ --bs-btn-bg: rgba(255,255,255,.065);
303
+ --bs-btn-border-color: rgba(255,255,255,.14);
304
+ --bs-btn-hover-color: #fff;
305
+ --bs-btn-hover-bg: rgba(255,255,255,.105);
306
+ --bs-btn-hover-border-color: rgba(255,255,255,.24);
307
+ }
308
+
309
+ .empty-state {
310
+ border: 1px dashed rgba(255,255,255,.18);
311
+ border-radius: 1rem;
312
+ padding: 1rem;
313
+ color: var(--lux-muted);
314
+ background: rgba(255,255,255,.025);
315
+ }
316
+
317
+ .toast-lite {
318
+ position: fixed;
319
+ right: 1rem;
320
+ bottom: 1rem;
321
+ z-index: 1080;
322
+ border: 1px solid rgba(255,255,255,.16);
323
+ border-radius: 1rem;
324
+ background: rgba(7, 13, 24, .92);
325
+ color: #fff;
326
+ box-shadow: 0 18px 50px rgba(0,0,0,.35);
327
+ padding: .8rem 1rem;
328
+ opacity: 0;
329
+ transform: translateY(12px);
330
+ pointer-events: none;
331
+ transition: opacity .18s ease, transform .18s ease;
332
+ }
333
+
334
+ .toast-lite.show {
335
+ opacity: 1;
336
+ transform: translateY(0);
337
+ }
338
+
339
+ @media (max-width: 768px) {
340
+ .stage-frame { min-height: 320px; }
341
+ .gradient-display { min-height: 110px; }
342
+ }
package/app.js ADDED
@@ -0,0 +1,13 @@
1
+ import "./components/gradient-lab-app.js";
2
+ import "./components/app-shell.js";
3
+ import "./components/app-hero.js";
4
+ import "./components/gradient-workbench.js";
5
+ import "./components/lux-card.js";
6
+ import "./components/sampler-toolbar.js";
7
+ import "./components/image-sampling-stage.js";
8
+ import "./components/sampled-gradient-list.js";
9
+ import "./components/gradient-editor.js";
10
+ import "./components/library-controls.js";
11
+ import "./components/gradient-library.js";
12
+ import "./components/library-code.js";
13
+ import "./components/toast-zone.js";
@@ -0,0 +1,32 @@
1
+ import { GradientElement } from "gradient-element";
2
+
3
+ class AppHero extends GradientElement {
4
+ mount() {
5
+ this.innerHTML = `
6
+ <section class="hero-card p-4 p-xl-5 mb-4">
7
+ <div class="row align-items-end g-4">
8
+ <div class="col-lg-8">
9
+ <div class="soft-pill d-inline-flex align-items-center gap-2 mb-3">
10
+ <i class="bi bi-eyedropper"></i>
11
+ Image-sampled CSS gradient studio
12
+ </div>
13
+ <h1 class="display-5 fw-semibold mb-3">Sample gradients the way nature composes color.</h1>
14
+ <p class="lead text-lux-muted mb-0">
15
+ Drop a close-up photo, draw slash-lines through the interesting color movement, then refine the sampled stops into reusable Bootstrap-ready CSS classes.
16
+ </p>
17
+ </div>
18
+ <div class="col-lg-4">
19
+ <div class="d-flex flex-wrap justify-content-lg-end gap-2">
20
+ <span class="soft-pill"><i class="bi bi-bezier2 me-1"></i> draggable paths</span>
21
+ <span class="soft-pill"><i class="bi bi-palette2 me-1"></i> 2–16 stops</span>
22
+ <span class="soft-pill"><i class="bi bi-code-slash me-1"></i> copy CSS</span>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </section>
27
+ `;
28
+ }
29
+ }
30
+
31
+ customElements.define("app-hero", AppHero);
32
+ export { AppHero };
@@ -0,0 +1,12 @@
1
+ import { GradientElement } from "gradient-element";
2
+
3
+ class AppShell extends GradientElement {
4
+ mount() {
5
+ const children = [...this.childNodes];
6
+ this.innerHTML = `<main class="app-shell container py-4 py-xl-5"></main>`;
7
+ this.querySelector("main").append(...children);
8
+ }
9
+ }
10
+
11
+ customElements.define("app-shell", AppShell);
12
+ export { AppShell };