@fundamental-engine/create 0.8.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/LICENSE +21 -0
- package/README.md +22 -0
- package/dist/index.js +51 -0
- package/dist/scaffold.js +33 -0
- package/package.json +38 -0
- package/templates/react/README.md +16 -0
- package/templates/react/_gitignore +4 -0
- package/templates/react/index.html +13 -0
- package/templates/react/package.json +23 -0
- package/templates/react/src/App.tsx +43 -0
- package/templates/react/src/main.tsx +9 -0
- package/templates/react/src/styles.css +24 -0
- package/templates/react/tsconfig.json +15 -0
- package/templates/react/vite.config.ts +4 -0
- package/templates/vanilla/README.md +15 -0
- package/templates/vanilla/_gitignore +4 -0
- package/templates/vanilla/index.html +26 -0
- package/templates/vanilla/package.json +18 -0
- package/templates/vanilla/src/main.ts +23 -0
- package/templates/vanilla/src/styles.css +33 -0
- package/templates/vanilla/tsconfig.json +14 -0
- package/templates/web-component/README.md +16 -0
- package/templates/web-component/_gitignore +4 -0
- package/templates/web-component/index.html +28 -0
- package/templates/web-component/package.json +18 -0
- package/templates/web-component/src/main.ts +9 -0
- package/templates/web-component/src/styles.css +22 -0
- package/templates/web-component/tsconfig.json +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zach Shallbetter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @fundamental-engine/create
|
|
2
|
+
|
|
3
|
+
Scaffold a [Fundamental](https://fundamental-engine.com) starter in one command:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm create @fundamental-engine my-field-app
|
|
7
|
+
# or pick the variant up front:
|
|
8
|
+
npm create @fundamental-engine my-field-app -- --template react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Three variants, all **signals-first** (the field draws nothing by default — it writes `--field-*` CSS
|
|
12
|
+
variables your styles read; particles are one opt-in surface):
|
|
13
|
+
|
|
14
|
+
- **`vanilla`** (default) — a *contained, signals-only* field (`FieldField` + `bounds`, `render: 'none'`)
|
|
15
|
+
over a real list. No canvas; the rows react through CSS. The "this is what it's for" starter.
|
|
16
|
+
- **`web-component`** — the `<field-root>` custom element; works in any framework or plain HTML.
|
|
17
|
+
- **`react`** — `<FieldField>` from `@fundamental-engine/react`.
|
|
18
|
+
|
|
19
|
+
Each scaffolds a minimal Vite app you run with `npm install && npm run dev`. Every template is explicit
|
|
20
|
+
about `render`, so it behaves the same whichever engine version it resolves to.
|
|
21
|
+
|
|
22
|
+
Run with no arguments for an interactive prompt (directory + template).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join, resolve, basename } from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { stdin, stdout, argv, cwd, exit } from 'node:process';
|
|
6
|
+
import { scaffold, isTemplate, TEMPLATES } from "./scaffold.js";
|
|
7
|
+
const templatesRoot = join(dirname(fileURLToPath(import.meta.url)), '../templates');
|
|
8
|
+
function parseArgs(args) {
|
|
9
|
+
const out = {};
|
|
10
|
+
for (let i = 0; i < args.length; i++) {
|
|
11
|
+
const a = args[i];
|
|
12
|
+
if (a === '--template' || a === '-t')
|
|
13
|
+
out.template = args[++i];
|
|
14
|
+
else if (a.startsWith('--template='))
|
|
15
|
+
out.template = a.slice('--template='.length);
|
|
16
|
+
else if (!a.startsWith('-') && out.dir === undefined)
|
|
17
|
+
out.dir = a;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
async function main() {
|
|
22
|
+
const args = parseArgs(argv.slice(2));
|
|
23
|
+
let dir = args.dir;
|
|
24
|
+
let template = args.template;
|
|
25
|
+
const interactive = Boolean(stdin.isTTY && stdout.isTTY);
|
|
26
|
+
const rl = interactive && (!dir || !template) ? createInterface({ input: stdin, output: stdout }) : undefined;
|
|
27
|
+
if (!dir)
|
|
28
|
+
dir = (rl ? (await rl.question('Project directory [my-field-app]: ')).trim() : '') || 'my-field-app';
|
|
29
|
+
if (!template) {
|
|
30
|
+
const ans = rl ? (await rl.question(`Template — ${TEMPLATES.join(' / ')} [vanilla]: `)).trim() : '';
|
|
31
|
+
template = ans || 'vanilla';
|
|
32
|
+
}
|
|
33
|
+
rl?.close();
|
|
34
|
+
if (!isTemplate(template)) {
|
|
35
|
+
console.error(`Unknown template "${template}". Choose one of: ${TEMPLATES.join(', ')}`);
|
|
36
|
+
exit(1);
|
|
37
|
+
}
|
|
38
|
+
const targetDir = resolve(cwd(), dir);
|
|
39
|
+
const name = basename(targetDir);
|
|
40
|
+
try {
|
|
41
|
+
await scaffold({ templatesRoot, template: template, targetDir, name });
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
console.error(`✗ ${e.message ?? e}`);
|
|
45
|
+
exit(1);
|
|
46
|
+
}
|
|
47
|
+
console.log(`\n✓ Created ${name} — the ${template} starter (signals-first).\n\n` +
|
|
48
|
+
`Next:\n cd ${dir}\n npm install\n npm run dev\n\n` +
|
|
49
|
+
`The field draws nothing by default — it writes --field-* signals your CSS reads. See src/ for the wiring.\n`);
|
|
50
|
+
}
|
|
51
|
+
void main();
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { cp, readFile, writeFile, readdir, rename } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/** The starter variants. All three are signals-first; vanilla is contained (field scoped to a list). */
|
|
5
|
+
export const TEMPLATES = ['vanilla', 'react', 'web-component'];
|
|
6
|
+
export function isTemplate(t) {
|
|
7
|
+
return TEMPLATES.includes(t);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Copy a template into `targetDir` and stamp the project name into its package.json. Throws if the
|
|
11
|
+
* template is unknown or the target exists and is non-empty. Pure file IO — no prompts, so it tests
|
|
12
|
+
* directly.
|
|
13
|
+
*/
|
|
14
|
+
export async function scaffold(opts) {
|
|
15
|
+
const { templatesRoot, template, targetDir, name } = opts;
|
|
16
|
+
const src = join(templatesRoot, template);
|
|
17
|
+
if (!existsSync(src))
|
|
18
|
+
throw new Error(`unknown template: ${template}`);
|
|
19
|
+
if (existsSync(targetDir) && (await readdir(targetDir)).length > 0)
|
|
20
|
+
throw new Error(`target directory is not empty: ${targetDir}`);
|
|
21
|
+
await cp(src, targetDir, { recursive: true });
|
|
22
|
+
// npm strips a `.gitignore` from published packages, so templates ship it as `_gitignore`; restore it.
|
|
23
|
+
const ignore = join(targetDir, '_gitignore');
|
|
24
|
+
if (existsSync(ignore))
|
|
25
|
+
await rename(ignore, join(targetDir, '.gitignore'));
|
|
26
|
+
// stamp the chosen project name into the scaffolded package.json
|
|
27
|
+
const pkgPath = join(targetDir, 'package.json');
|
|
28
|
+
if (existsSync(pkgPath)) {
|
|
29
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
30
|
+
pkg.name = name;
|
|
31
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
32
|
+
}
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fundamental-engine/create",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Scaffold a Fundamental starter — `npm create @fundamental-engine` — vanilla, React, or web-component, contained and signals-first by default.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"fundamental",
|
|
8
|
+
"scaffold",
|
|
9
|
+
"create",
|
|
10
|
+
"starter",
|
|
11
|
+
"field"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/zachshallbetter/fundamental-engine.git",
|
|
17
|
+
"directory": "packages/create"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"create-fundamental-engine": "dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"templates"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5.9.3",
|
|
31
|
+
"@types/node": "^22"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
36
|
+
"test": "node --test"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# field-app — React starter
|
|
2
|
+
|
|
3
|
+
A [Fundamental](https://fundamental-engine.com) starter using `<FieldField>` from
|
|
4
|
+
`@fundamental-engine/react`. Signals-first: the field draws nothing by default and writes `--field-*`
|
|
5
|
+
variables your CSS reads.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- `src/App.tsx` — drop in `<FieldField />`, mark elements `data-body data-feedback`; engagement is a
|
|
13
|
+
`data-active` attribute set on hover/focus.
|
|
14
|
+
- `src/styles.css` — reads `--field-density` (`--d`) for weight, lift, glow.
|
|
15
|
+
|
|
16
|
+
Want particles? Pass `render="dots"` to `<FieldField />`. Prefer the handle? Use `useFieldField()`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Fundamental — React starter</title>
|
|
7
|
+
<link rel="stylesheet" href="/src/styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "field-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@fundamental-engine/react": "latest",
|
|
13
|
+
"react": "^18",
|
|
14
|
+
"react-dom": "^18"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/react": "^18",
|
|
18
|
+
"@types/react-dom": "^18",
|
|
19
|
+
"@vitejs/plugin-react": "^4",
|
|
20
|
+
"typescript": "^5.9.3",
|
|
21
|
+
"vite": "^6"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { FieldField } from '@fundamental-engine/react';
|
|
2
|
+
|
|
3
|
+
const ITEMS = [
|
|
4
|
+
{ label: 'Production incident', strength: 1 },
|
|
5
|
+
{ label: 'Design review', strength: 0.62 },
|
|
6
|
+
{ label: 'PR #318 — signals-first', strength: 0.78 },
|
|
7
|
+
{ label: 'Weekly digest', strength: 0.3 },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
// engagement is just an attribute the field reads — set it on hover/focus, live.
|
|
11
|
+
const engage = (e: React.SyntheticEvent<HTMLElement>) => e.currentTarget.setAttribute('data-active', '1');
|
|
12
|
+
const release = (e: React.SyntheticEvent<HTMLElement>) => e.currentTarget.setAttribute('data-active', '0');
|
|
13
|
+
|
|
14
|
+
export function App() {
|
|
15
|
+
return (
|
|
16
|
+
<main>
|
|
17
|
+
{/* The window field, signals-first: it writes --field-* onto the [data-body] elements below and
|
|
18
|
+
draws nothing. Pass render="dots" to see the particles instead. */}
|
|
19
|
+
<FieldField />
|
|
20
|
+
|
|
21
|
+
<h1 data-body="attract" data-feedback data-strength="1" data-range="320">Elements have mass.</h1>
|
|
22
|
+
<p className="sub">Hover an item — the field reacts to engagement, no particles drawn.</p>
|
|
23
|
+
<ul className="inbox">
|
|
24
|
+
{ITEMS.map((it) => (
|
|
25
|
+
<li
|
|
26
|
+
key={it.label}
|
|
27
|
+
tabIndex={0}
|
|
28
|
+
data-body="attract"
|
|
29
|
+
data-feedback
|
|
30
|
+
data-strength={it.strength}
|
|
31
|
+
data-range="180"
|
|
32
|
+
onPointerEnter={engage}
|
|
33
|
+
onPointerLeave={release}
|
|
34
|
+
onFocus={engage}
|
|
35
|
+
onBlur={release}
|
|
36
|
+
>
|
|
37
|
+
{it.label}
|
|
38
|
+
</li>
|
|
39
|
+
))}
|
|
40
|
+
</ul>
|
|
41
|
+
</main>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
:root { --accent: #4da3ff; color-scheme: dark; }
|
|
2
|
+
body { margin: 0; min-height: 100vh; background: #0b0e14; color: #e8e8ef; font: 16px/1.5 system-ui, sans-serif; display: grid; place-items: center; }
|
|
3
|
+
main { width: min(560px, 92vw); padding: 2rem 0; }
|
|
4
|
+
h1 {
|
|
5
|
+
font-size: clamp(2rem, 6vw, 3rem); letter-spacing: -0.02em; margin: 0 0 0.3rem;
|
|
6
|
+
font-weight: calc(500 + var(--d, 0) * 340);
|
|
7
|
+
transition: font-weight 0.5s ease;
|
|
8
|
+
}
|
|
9
|
+
.sub { color: #9aa3b2; margin: 0 0 1.6rem; }
|
|
10
|
+
.inbox { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
|
|
11
|
+
|
|
12
|
+
/* read what the field wrote — --field-density (--d) becomes weight, lift, glow */
|
|
13
|
+
.inbox li {
|
|
14
|
+
padding: calc(0.6rem + var(--d, 0) * 0.5rem) 0.9rem;
|
|
15
|
+
border: 1px solid color-mix(in srgb, var(--accent) calc(var(--d, 0) * 55%), #2a2a35);
|
|
16
|
+
border-radius: 10px;
|
|
17
|
+
background: color-mix(in srgb, var(--accent) calc(var(--d, 0) * 14%), transparent);
|
|
18
|
+
font-weight: calc(400 + var(--d, 0) * 360);
|
|
19
|
+
transform: translateX(calc(var(--d, 0) * 6px));
|
|
20
|
+
box-shadow: 0 0 calc(var(--d, 0) * 26px) color-mix(in srgb, var(--accent) calc(var(--d, 0) * 45%), transparent);
|
|
21
|
+
transition: all 0.45s ease; cursor: default;
|
|
22
|
+
}
|
|
23
|
+
.inbox li:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
24
|
+
@media (prefers-reduced-motion: reduce) { h1, .inbox li { transition: none; } }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"isolatedModules": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# field-app — vanilla starter
|
|
2
|
+
|
|
3
|
+
A [Fundamental](https://fundamental-engine.com) starter: a **contained, signals-only** field over a real
|
|
4
|
+
list. No canvas, no particles — the field writes `--field-*` CSS variables and the list reacts.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install
|
|
8
|
+
npm run dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- `src/main.ts` — `new FieldField({ bounds: list, render: 'none' })` scopes the field to the list and
|
|
12
|
+
draws nothing; engagement (`data-active`) drives it live.
|
|
13
|
+
- `src/styles.css` — reads `--field-density` (`--d`) to turn density into weight, lift, and glow.
|
|
14
|
+
|
|
15
|
+
Want to see the particles? Change `render: 'none'` → `render: 'dots'` in `src/main.ts`.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Fundamental — vanilla starter</title>
|
|
7
|
+
<link rel="stylesheet" href="/src/styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>
|
|
11
|
+
<h1>Elements have mass.</h1>
|
|
12
|
+
<p class="sub">A contained, signals-only field. No canvas — the list reacts through CSS variables.</p>
|
|
13
|
+
|
|
14
|
+
<!-- The field is scoped to this list (bounds). Each row is a body; the field writes
|
|
15
|
+
--field-density (--d) back, and the CSS below turns that into weight + glow. -->
|
|
16
|
+
<ul class="inbox" data-field>
|
|
17
|
+
<li data-body="attract" data-feedback data-strength="1.0" data-range="180">Production incident</li>
|
|
18
|
+
<li data-body="attract" data-feedback data-strength="0.62" data-range="180">Design review</li>
|
|
19
|
+
<li data-body="attract" data-feedback data-strength="0.78" data-range="180">PR #318 — signals-first</li>
|
|
20
|
+
<li data-body="attract" data-feedback data-strength="0.30" data-range="180">Weekly digest</li>
|
|
21
|
+
<li data-body="attract" data-feedback data-strength="0.18" data-range="180">Coffee with Sam</li>
|
|
22
|
+
</ul>
|
|
23
|
+
</main>
|
|
24
|
+
<script type="module" src="/src/main.ts"></script>
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "field-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@fundamental-engine/vanilla": "latest"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5.9.3",
|
|
16
|
+
"vite": "^6"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { FieldField } from '@fundamental-engine/vanilla';
|
|
2
|
+
|
|
3
|
+
const list = document.querySelector<HTMLElement>('[data-field]')!;
|
|
4
|
+
|
|
5
|
+
// A CONTAINED, signals-only field: it runs the full simulation over the rows but draws nothing
|
|
6
|
+
// (render: 'none'). The only output is the --field-* CSS variables — the field is behaviour, not a
|
|
7
|
+
// particle background. `bounds` scopes it to this list instead of the window.
|
|
8
|
+
const field = new FieldField({ bounds: list, render: 'none', density: 1.4 });
|
|
9
|
+
|
|
10
|
+
// Engagement is just an attribute the field reads: hover or focus a row and it gathers the (invisible)
|
|
11
|
+
// matter toward itself; neighbours feel the shift. The CSS reads --field-density (--d) to show it.
|
|
12
|
+
for (const row of list.querySelectorAll<HTMLElement>('li')) {
|
|
13
|
+
row.tabIndex = 0;
|
|
14
|
+
const on = () => row.setAttribute('data-active', '1');
|
|
15
|
+
const off = () => row.setAttribute('data-active', '0');
|
|
16
|
+
row.addEventListener('pointerenter', on);
|
|
17
|
+
row.addEventListener('pointerleave', off);
|
|
18
|
+
row.addEventListener('focus', on);
|
|
19
|
+
row.addEventListener('blur', off);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// `field` is the live FieldHandle — field.version, field.scan(), field.setRender('dots'), …
|
|
23
|
+
// To SEE particles instead of pure signals, swap render: 'none' → 'dots' above.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--accent: #4da3ff;
|
|
3
|
+
color-scheme: dark;
|
|
4
|
+
}
|
|
5
|
+
body {
|
|
6
|
+
margin: 0;
|
|
7
|
+
min-height: 100vh;
|
|
8
|
+
background: #0b0e14;
|
|
9
|
+
color: #e8e8ef;
|
|
10
|
+
font: 16px/1.5 system-ui, sans-serif;
|
|
11
|
+
display: grid;
|
|
12
|
+
place-items: center;
|
|
13
|
+
}
|
|
14
|
+
main { width: min(560px, 92vw); padding: 2rem 0; }
|
|
15
|
+
h1 { font-size: clamp(2rem, 6vw, 3rem); margin: 0 0 0.3rem; letter-spacing: -0.02em; }
|
|
16
|
+
.sub { color: #9aa3b2; margin: 0 0 1.6rem; }
|
|
17
|
+
.inbox { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
|
|
18
|
+
|
|
19
|
+
/* The field wrote --field-density (alias --d) onto each [data-feedback] row.
|
|
20
|
+
Read it to turn density into presence — weight, lift, and glow. No canvas involved. */
|
|
21
|
+
.inbox li {
|
|
22
|
+
padding: calc(0.6rem + var(--d, 0) * 0.5rem) 0.9rem;
|
|
23
|
+
border: 1px solid color-mix(in srgb, var(--accent) calc(var(--d, 0) * 55%), #2a2a35);
|
|
24
|
+
border-radius: 10px;
|
|
25
|
+
background: color-mix(in srgb, var(--accent) calc(var(--d, 0) * 14%), transparent);
|
|
26
|
+
font-weight: calc(400 + var(--d, 0) * 360);
|
|
27
|
+
transform: translateX(calc(var(--d, 0) * 6px));
|
|
28
|
+
box-shadow: 0 0 calc(var(--d, 0) * 26px) color-mix(in srgb, var(--accent) calc(var(--d, 0) * 45%), transparent);
|
|
29
|
+
transition: all 0.45s ease;
|
|
30
|
+
cursor: default;
|
|
31
|
+
}
|
|
32
|
+
.inbox li:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
33
|
+
@media (prefers-reduced-motion: reduce) { .inbox li { transition: none; } }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"verbatimModuleSyntax": true,
|
|
11
|
+
"isolatedModules": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# field-app — web-component starter
|
|
2
|
+
|
|
3
|
+
A [Fundamental](https://fundamental-engine.com) starter using the `<field-root>` custom element — works
|
|
4
|
+
in any framework or plain HTML. Signals-first: the field draws nothing by default and writes `--field-*`
|
|
5
|
+
variables your CSS reads.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- `index.html` — drop in `<field-root>`, mark elements `[data-body] [data-feedback]`.
|
|
13
|
+
- `src/main.ts` — importing `@fundamental-engine/elements` is the whole wiring; it registers the element.
|
|
14
|
+
- `src/styles.css` — reads `--field-density` (`--d`) for weight + glow.
|
|
15
|
+
|
|
16
|
+
Want particles? Add `render="dots"` to `<field-root>`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Fundamental — web-component starter</title>
|
|
7
|
+
<link rel="stylesheet" href="/src/styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<!-- One field for the page. No `render` attribute → signals-first (#538): it runs the simulation
|
|
11
|
+
and writes --field-* variables, but draws no canvas. Add render="dots" to see the particles. -->
|
|
12
|
+
<field-root></field-root>
|
|
13
|
+
|
|
14
|
+
<main>
|
|
15
|
+
<!-- Bodies: the field gathers toward them and writes --field-density (--d) back; the CSS reads it. -->
|
|
16
|
+
<h1 data-body="attract" data-feedback data-strength="1" data-range="320">Elements have mass.</h1>
|
|
17
|
+
<p class="sub" data-body="attract" data-feedback data-strength="0.4" data-range="240">
|
|
18
|
+
Hover the items — the field reacts to engagement, no particles drawn.
|
|
19
|
+
</p>
|
|
20
|
+
<ul class="row">
|
|
21
|
+
<li data-body="attract" data-feedback data-strength="0.7" data-range="160">Signal</li>
|
|
22
|
+
<li data-body="attract" data-feedback data-strength="0.5" data-range="160">Relation</li>
|
|
23
|
+
<li data-body="attract" data-feedback data-strength="0.3" data-range="160">Change</li>
|
|
24
|
+
</ul>
|
|
25
|
+
</main>
|
|
26
|
+
<script type="module" src="/src/main.ts"></script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "field-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@fundamental-engine/elements": "latest"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5.9.3",
|
|
16
|
+
"vite": "^6"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Importing the package registers <field-root> and boots the engine — that's the whole wiring.
|
|
2
|
+
// The field is signals-first: it writes --field-* onto the [data-body] elements; styles.css reacts.
|
|
3
|
+
import '@fundamental-engine/elements';
|
|
4
|
+
|
|
5
|
+
// Engage bodies on hover so the field gathers toward them (and neighbours respond), live.
|
|
6
|
+
for (const el of document.querySelectorAll<HTMLElement>('[data-body]')) {
|
|
7
|
+
el.addEventListener('pointerenter', () => el.setAttribute('data-active', '1'));
|
|
8
|
+
el.addEventListener('pointerleave', () => el.setAttribute('data-active', '0'));
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
:root { --accent: #4da3ff; color-scheme: dark; }
|
|
2
|
+
body { margin: 0; min-height: 100vh; background: #0b0e14; color: #e8e8ef; font: 16px/1.5 system-ui, sans-serif; display: grid; place-items: center; }
|
|
3
|
+
main { width: min(620px, 92vw); padding: 2rem 0; text-align: center; }
|
|
4
|
+
|
|
5
|
+
/* read what the field writes — weight + glow rise with --field-density (--d) */
|
|
6
|
+
h1 {
|
|
7
|
+
font-size: clamp(2.2rem, 7vw, 3.4rem); letter-spacing: -0.02em; margin: 0 0 0.4rem;
|
|
8
|
+
font-weight: calc(500 + var(--d, 0) * 360);
|
|
9
|
+
text-shadow: 0 0 calc(var(--d, 0) * 30px) color-mix(in srgb, var(--accent) calc(var(--d, 0) * 70%), transparent);
|
|
10
|
+
transition: all 0.5s ease;
|
|
11
|
+
}
|
|
12
|
+
.sub { color: #9aa3b2; margin: 0 0 2rem; }
|
|
13
|
+
.row { list-style: none; display: flex; gap: 0.6rem; justify-content: center; padding: 0; }
|
|
14
|
+
.row li {
|
|
15
|
+
padding: 0.6rem 1.1rem; border-radius: 999px;
|
|
16
|
+
border: 1px solid color-mix(in srgb, var(--accent) calc(var(--d, 0) * 60%), #2a2a35);
|
|
17
|
+
background: color-mix(in srgb, var(--accent) calc(var(--d, 0) * 16%), transparent);
|
|
18
|
+
font-weight: calc(400 + var(--d, 0) * 320);
|
|
19
|
+
transform: scale(calc(1 + var(--d, 0) * 0.08));
|
|
20
|
+
transition: all 0.45s ease; cursor: default;
|
|
21
|
+
}
|
|
22
|
+
@media (prefers-reduced-motion: reduce) { h1, .row li { transition: none; } }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"verbatimModuleSyntax": true,
|
|
11
|
+
"isolatedModules": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|