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 +135 -0
- package/README.md +103 -0
- package/app.css +342 -0
- package/app.js +13 -0
- package/components/app-hero.js +32 -0
- package/components/app-shell.js +12 -0
- package/components/gradient-editor.js +224 -0
- package/components/gradient-element.js +8 -0
- package/components/gradient-lab-app.js +120 -0
- package/components/gradient-library.js +60 -0
- package/components/gradient-workbench.js +18 -0
- package/components/image-sampling-stage.js +316 -0
- package/components/library-code.js +24 -0
- package/components/library-controls.js +45 -0
- package/components/lux-card.js +31 -0
- package/components/sampled-gradient-list.js +63 -0
- package/components/sampler-toolbar.js +100 -0
- package/components/toast-zone.js +19 -0
- package/index.html +56 -0
- package/package.json +41 -0
- package/reactive-framework.js +371 -0
- package/server.js +108 -0
- package/utils.js +114 -0
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 };
|