@vibes.diy/prompts 14.3.36 → 14.3.38
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/llms/d3.draft.md +154 -0
- package/llms/d3.js +1 -0
- package/llms/d3.js.map +1 -1
- package/llms/image-gen.md +1 -0
- package/llms/p5.draft.md +179 -0
- package/llms/p5.js +1 -0
- package/llms/p5.js.map +1 -1
- package/llms/spotify.draft.md +179 -0
- package/llms/spotify.js +1 -0
- package/llms/spotify.js.map +1 -1
- package/llms/three-js.draft.md +161 -0
- package/llms/three-js.js +1 -0
- package/llms/three-js.js.map +1 -1
- package/llms/voxel.draft.md +99 -147
- package/llms/web-audio.draft.md +180 -0
- package/llms/web-audio.js +1 -0
- package/llms/web-audio.js.map +1 -1
- package/llms/webxr.draft.md +159 -0
- package/llms/webxr.js +1 -0
- package/llms/webxr.js.map +1 -1
- package/llms/youtube.draft.md +183 -0
- package/llms/youtube.js +1 -0
- package/llms/youtube.js.map +1 -1
- package/package.json +4 -4
package/llms/d3.draft.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# D3 — the first draft: a small live bar chart
|
|
2
|
+
|
|
3
|
+
A D3 app's first draft is the whole page around the chart — the title, the
|
|
4
|
+
card that frames it, a tiny form that adds a row, the seeded documents the
|
|
5
|
+
chart reads — with one small, correct chart actually drawn by d3 inside the
|
|
6
|
+
card: a bar chart of the seeded rows with a band scale, a linear scale and both
|
|
7
|
+
axes, following the theme through its CSS variables. Add a row and the bar appears.
|
|
8
|
+
Transitions, tooltips, brushing, maps and force layouts are the next pass. What
|
|
9
|
+
the draft owes that pass is the right skeleton: d3 owns one `<svg>` through a
|
|
10
|
+
ref, the width comes from the host through a `ResizeObserver`, and the draw
|
|
11
|
+
effect is a select → data join → `join("rect")` that re-runs when `docs`
|
|
12
|
+
change — so the next pass adds marks and interactions to that effect rather
|
|
13
|
+
than rewriting the file.
|
|
14
|
+
|
|
15
|
+
### `App.jsx`
|
|
16
|
+
```jsx
|
|
17
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
18
|
+
import { useFireproof } from "use-fireproof";
|
|
19
|
+
import * as d3 from "d3";
|
|
20
|
+
import { Button, Input, Badge, Card, CardHeader, CardTitle, CardContent } from "@vibes.diy/look";
|
|
21
|
+
|
|
22
|
+
const HEIGHT = 260;
|
|
23
|
+
const MARGIN = { top: 12, right: 12, bottom: 32, left: 40 };
|
|
24
|
+
|
|
25
|
+
// One pass of the D3 pattern: select the svg, bind the rows, transform the marks.
|
|
26
|
+
// Called on every data or width change; the join keeps it idempotent.
|
|
27
|
+
function drawChart(svgEl, rows, width) {
|
|
28
|
+
const innerW = Math.max(0, width - MARGIN.left - MARGIN.right);
|
|
29
|
+
const innerH = HEIGHT - MARGIN.top - MARGIN.bottom;
|
|
30
|
+
const x = d3.scaleBand().domain(rows.map((d) => d.label)).range([0, innerW]).padding(0.2);
|
|
31
|
+
const y = d3.scaleLinear().domain([0, d3.max(rows, (d) => d.value) || 1]).nice().range([innerH, 0]);
|
|
32
|
+
|
|
33
|
+
const svg = d3.select(svgEl).attr("width", width).attr("height", HEIGHT).attr("viewBox", `0 0 ${width} ${HEIGHT}`);
|
|
34
|
+
const g = svg.selectAll("g.plot").data([null]).join("g").attr("class", "plot").attr("transform", `translate(${MARGIN.left},${MARGIN.top})`);
|
|
35
|
+
|
|
36
|
+
g.selectAll("rect.bar")
|
|
37
|
+
.data(rows, (d) => d._id)
|
|
38
|
+
.join("rect")
|
|
39
|
+
.attr("class", "bar")
|
|
40
|
+
.attr("x", (d) => x(d.label))
|
|
41
|
+
.attr("width", x.bandwidth())
|
|
42
|
+
.attr("y", (d) => y(d.value))
|
|
43
|
+
.attr("height", (d) => innerH - y(d.value))
|
|
44
|
+
.attr("rx", 3)
|
|
45
|
+
.style("fill", "var(--primary, currentColor)"); // the theme's primary, so a restyle recolors it
|
|
46
|
+
|
|
47
|
+
g.selectAll("g.x-axis").data([null]).join("g").attr("class", "x-axis").attr("transform", `translate(0,${innerH})`).call(d3.axisBottom(x));
|
|
48
|
+
g.selectAll("g.y-axis").data([null]).join("g").attr("class", "y-axis").call(d3.axisLeft(y).ticks(5));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function App() {
|
|
52
|
+
const { database, useLiveQuery } = useFireproof("bakeryShelf");
|
|
53
|
+
const { docs } = useLiveQuery("type", { key: "loaf" });
|
|
54
|
+
const hostRef = useRef(null);
|
|
55
|
+
const svgRef = useRef(null);
|
|
56
|
+
const [width, setWidth] = useState(0);
|
|
57
|
+
const [label, setLabel] = useState("");
|
|
58
|
+
const [value, setValue] = useState("");
|
|
59
|
+
|
|
60
|
+
// The width follows the card, not the window.
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const host = hostRef.current;
|
|
63
|
+
const observer = new window.ResizeObserver(([entry]) => setWidth(Math.floor(entry.contentRect.width)));
|
|
64
|
+
observer.observe(host);
|
|
65
|
+
return () => observer.disconnect();
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
// Redraw whenever the rows or the width change.
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!svgRef.current || width === 0) return;
|
|
71
|
+
const rows = [...docs].sort((a, b) => a.createdAt - b.createdAt);
|
|
72
|
+
drawChart(svgRef.current, rows, width);
|
|
73
|
+
}, [docs, width]);
|
|
74
|
+
|
|
75
|
+
const addRow = () => {
|
|
76
|
+
const n = Number(value);
|
|
77
|
+
if (label.trim().length === 0 || !Number.isFinite(n) || n < 0) return;
|
|
78
|
+
database.put({ type: "loaf", label: label.trim(), value: n, createdAt: Date.now() });
|
|
79
|
+
setLabel("");
|
|
80
|
+
setValue("");
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const total = docs.reduce((sum, d) => sum + d.value, 0);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<main className="min-h-screen bg-background text-foreground font-sans p-6">
|
|
87
|
+
<h1 className="text-3xl font-bold">Morning Loaves</h1>
|
|
88
|
+
<p className="mt-1 text-muted-foreground">What left the shelf at Crumb & Co. before ten. Add a bake and watch its bar rise.</p>
|
|
89
|
+
|
|
90
|
+
<Card className="mt-6 max-w-2xl">
|
|
91
|
+
<CardHeader>
|
|
92
|
+
<CardTitle>Sold by 10am</CardTitle>
|
|
93
|
+
</CardHeader>
|
|
94
|
+
<CardContent>
|
|
95
|
+
<div className="flex items-center gap-2">
|
|
96
|
+
<Badge variant="secondary">{docs.length} bakes</Badge>
|
|
97
|
+
<Badge variant="secondary">{total} loaves</Badge>
|
|
98
|
+
</div>
|
|
99
|
+
<div ref={hostRef} className="mt-4 w-full">
|
|
100
|
+
<svg ref={svgRef} className="block text-muted-foreground" role="img" aria-label="Loaves sold by bake" />
|
|
101
|
+
</div>
|
|
102
|
+
{/* TODO(next pass): bars grow in with a transition, a tooltip per bar, brushing a range, and other chart kinds (maps, force layouts) */}
|
|
103
|
+
<div className="mt-4 flex flex-wrap gap-2">
|
|
104
|
+
<Input className="flex-1" value={label} onChange={(e) => setLabel(e.target.value)} placeholder="Bake, e.g. Rye" />
|
|
105
|
+
<Input className="w-28" type="number" min={0} value={value} onChange={(e) => setValue(e.target.value)} placeholder="Loaves" />
|
|
106
|
+
<Button onClick={addRow}>Add bake</Button>
|
|
107
|
+
</div>
|
|
108
|
+
</CardContent>
|
|
109
|
+
</Card>
|
|
110
|
+
</main>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `seed.json`
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"bakeryShelf": [
|
|
119
|
+
{ "key": "sourdough", "type": "loaf", "label": "Sourdough", "value": 42, "createdAt": 1758400000000 },
|
|
120
|
+
{ "key": "baguette", "type": "loaf", "label": "Baguette", "value": 35, "createdAt": 1758400060000 },
|
|
121
|
+
{ "key": "rye", "type": "loaf", "label": "Rye", "value": 18, "createdAt": 1758400120000 },
|
|
122
|
+
{ "key": "brioche", "type": "loaf", "label": "Brioche", "value": 24, "createdAt": 1758400180000 },
|
|
123
|
+
{ "key": "focaccia", "type": "loaf", "label": "Focaccia", "value": 29, "createdAt": 1758400240000 },
|
|
124
|
+
{ "key": "seeded", "type": "loaf", "label": "Seeded", "value": 15, "createdAt": 1758400300000 }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Draft note: the next pass adds bars that grow in with transitions, a tooltip per bar, brushing a range, and richer chart kinds like maps and force layouts.
|
|
130
|
+
|
|
131
|
+
## What stays the same in a D3 draft
|
|
132
|
+
|
|
133
|
+
- **The import is the namespace**: `import * as d3 from "d3"`, and d3 owns
|
|
134
|
+
exactly one `<svg>` reached through a ref. React renders the svg element and
|
|
135
|
+
the page around it; d3 renders everything inside it.
|
|
136
|
+
- **The D3 pattern, idempotent**: select the svg, bind with
|
|
137
|
+
`.data(rows, (d) => d._id)`, and `.join("rect")`; the group and each axis are
|
|
138
|
+
joined the same way (`.data([null]).join("g")`), so a redraw updates in place
|
|
139
|
+
and never appends a second chart.
|
|
140
|
+
- **Scales and axes**: `scaleBand` for categories, `scaleLinear().nice()` from
|
|
141
|
+
zero for values, `axisBottom` and `axisLeft` called into their own groups
|
|
142
|
+
inside a margin.
|
|
143
|
+
- **Width from the host**: a `ResizeObserver` on the wrapper sets `width` in
|
|
144
|
+
React state; the height is a constant; the observer disconnects in cleanup.
|
|
145
|
+
- **Redraw on data**: the draw effect depends on `[docs, width]`, and the rows
|
|
146
|
+
come from `useLiveQuery`, so a row added by the form appears as a bar with no
|
|
147
|
+
extra wiring.
|
|
148
|
+
- **Theme colors, never hex**: bars fill with `var(--primary, currentColor)`
|
|
149
|
+
as a style, and the axes draw in the svg's `text-muted-foreground` through
|
|
150
|
+
d3's own `currentColor` defaults, so a restyle recolors the chart with no
|
|
151
|
+
redraw.
|
|
152
|
+
- **What waits for the next pass** is named in the `TODO(next pass)` comment
|
|
153
|
+
and the draft note: transitions, tooltips, brushing, maps, force layouts. The
|
|
154
|
+
draft draws none of it.
|
package/llms/d3.js
CHANGED
package/llms/d3.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"d3.js","sourceRoot":"","sources":["../../jsr/llms/d3.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,QAAQ,GAAc;IACjC,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,OAAO;IACd,WAAW,EACT,oRAAoR;IACtR,IAAI,EAAE;QACJ,OAAO;QACP,QAAQ;QACR,OAAO;QACP,QAAQ;QACR,MAAM;QACN,OAAO;QACP,IAAI;QACJ,eAAe;QACf,eAAe;QACf,SAAS;QACT,UAAU;QACV,SAAS;QACT,WAAW;QACX,SAAS;QAGT,KAAK;QACL,MAAM;QACN,YAAY;KACb;
|
|
1
|
+
{"version":3,"file":"d3.js","sourceRoot":"","sources":["../../jsr/llms/d3.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,QAAQ,GAAc;IACjC,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,OAAO;IACd,WAAW,EACT,oRAAoR;IACtR,IAAI,EAAE;QACJ,OAAO;QACP,QAAQ;QACR,OAAO;QACP,QAAQ;QACR,MAAM;QACN,OAAO;QACP,IAAI;QACJ,eAAe;QACf,eAAe;QACf,SAAS;QACT,UAAU;QACV,SAAS;QACT,WAAW;QACX,SAAS;QAGT,KAAK;QACL,MAAM;QACN,YAAY;KACb;IAGD,YAAY,EAAE,IAAI;IAClB,YAAY,EAAE,IAAI;IAClB,UAAU,EAAE,IAAI;IAChB,UAAU,EAAE,WAAW;CACxB,CAAC"}
|
package/llms/image-gen.md
CHANGED
|
@@ -161,6 +161,7 @@ function Gallery() {
|
|
|
161
161
|
- Same prompt produces a deterministic `_id` (hash-based), so results are cached across reloads.
|
|
162
162
|
- Each image has a regenerate button that appends a new version (writes a new `_files.v<N>` entry).
|
|
163
163
|
- Prev / next controls navigate between stored versions.
|
|
164
|
+
- In the Vibes iPhone app, on iPhones with Apple Intelligence, pictures come from Apple's Image Playground, free. A picture started by a tap — ↻, or a button that shows the `<ImgGen>` — opens Image Playground with its prompt (and the `images` input picture, for an edit), and the image the person confirms is saved as a version like any other. A picture that starts on its own shows a **Create with Image Playground** button that does the same. Your code stays the same, and everywhere else pictures are made as usual. Passing `model` keeps that picture on the platform's cloud generation.
|
|
164
165
|
- Set `showControls={false}` to hide regenerate and version navigation.
|
|
165
166
|
|
|
166
167
|
## Private drafts and public images
|
package/llms/p5.draft.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# p5.js — the first draft: a small seeded sketch that moves
|
|
2
|
+
|
|
3
|
+
A p5 app's first draft is the whole page around the piece — the title, the
|
|
4
|
+
card that frames it, the button that re-seeds it, the seeded document it reads
|
|
5
|
+
— with one small generative sketch actually running inside the card: a handful
|
|
6
|
+
of dots wandering on Perlin noise, each leaving a soft trail, seeded from the
|
|
7
|
+
document so the same seed always draws the same drift. The real piece (the
|
|
8
|
+
flow field, the kaleidoscope, the thousand-particle study), saving still
|
|
9
|
+
renders, palettes and drawing with a finger are the next pass. What the draft
|
|
10
|
+
owes that pass is the right skeleton: the pinned import, instance mode with a
|
|
11
|
+
sketch factory that receives its host element, the canvas sized from the host,
|
|
12
|
+
`pixelDensity` set, the seed read through a ref, and `instance.remove()` on
|
|
13
|
+
cleanup — so the next pass grows `makeSketch` rather than rewriting the file.
|
|
14
|
+
|
|
15
|
+
### `App.jsx`
|
|
16
|
+
```jsx
|
|
17
|
+
import React, { useEffect, useRef } from "react";
|
|
18
|
+
import { useFireproof } from "use-fireproof";
|
|
19
|
+
import p5 from "https://esm.sh/p5@1.11.13";
|
|
20
|
+
import { Button, Badge, Card, CardHeader, CardTitle, CardContent } from "@vibes.diy/look";
|
|
21
|
+
|
|
22
|
+
const WANDERERS = 7;
|
|
23
|
+
|
|
24
|
+
// Canvas pixels cannot follow a CSS variable, so resolve a theme token to a
|
|
25
|
+
// concrete color once per mount. The fallback covers an app with no theme.
|
|
26
|
+
function cssColor(host, token, fallback) {
|
|
27
|
+
const probe = document.createElement("span");
|
|
28
|
+
probe.style.display = "none";
|
|
29
|
+
probe.style.color = `var(${token}, ${fallback})`;
|
|
30
|
+
host.appendChild(probe);
|
|
31
|
+
const resolved = window.getComputedStyle(probe).color;
|
|
32
|
+
probe.remove();
|
|
33
|
+
return resolved || fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The sketch is a pure function of its host and the seed it reads. The host is
|
|
37
|
+
// passed in; the current seed arrives through seedRef, so a new seed restarts
|
|
38
|
+
// the drift without rebuilding the instance.
|
|
39
|
+
function makeSketch({ host, seedRef }) {
|
|
40
|
+
return (p) => {
|
|
41
|
+
let w = 0;
|
|
42
|
+
let h = 0;
|
|
43
|
+
let seed = null;
|
|
44
|
+
let t = 0;
|
|
45
|
+
let dots = [];
|
|
46
|
+
let bg = null;
|
|
47
|
+
let fade = null;
|
|
48
|
+
let ink = null;
|
|
49
|
+
|
|
50
|
+
const start = () => {
|
|
51
|
+
seed = seedRef.current;
|
|
52
|
+
p.randomSeed(seed);
|
|
53
|
+
p.noiseSeed(seed);
|
|
54
|
+
t = 0;
|
|
55
|
+
dots = Array.from({ length: WANDERERS }, (_, i) => ({ x: p.random(w), y: p.random(h), r: 6 + i * 2, off: p.random(1000) }));
|
|
56
|
+
p.background(bg);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
p.setup = () => {
|
|
60
|
+
w = Math.max(120, host.clientWidth || 320);
|
|
61
|
+
h = Math.max(120, host.clientHeight || 320);
|
|
62
|
+
const c = p.createCanvas(w, h);
|
|
63
|
+
c.elt.style.display = "block";
|
|
64
|
+
p.pixelDensity(Math.min(window.devicePixelRatio || 1, 2));
|
|
65
|
+
bg = p.color(cssColor(host, "--card", "#fdfaf4"));
|
|
66
|
+
fade = p.color(cssColor(host, "--card", "#fdfaf4"));
|
|
67
|
+
fade.setAlpha(24);
|
|
68
|
+
ink = p.color(cssColor(host, "--primary", "#3b5bdb"));
|
|
69
|
+
ink.setAlpha(170);
|
|
70
|
+
p.noStroke();
|
|
71
|
+
start();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
p.draw = () => {
|
|
75
|
+
if (seedRef.current !== seed) start(); // the button wrote a new seed
|
|
76
|
+
t += 0.004;
|
|
77
|
+
p.fill(fade); // trails: dim what is there instead of erasing it
|
|
78
|
+
p.rect(0, 0, w, h);
|
|
79
|
+
p.fill(ink);
|
|
80
|
+
for (const d of dots) {
|
|
81
|
+
d.x = p.noise(d.off, t) * w;
|
|
82
|
+
d.y = p.noise(d.off + 50, t) * h;
|
|
83
|
+
p.circle(d.x, d.y, d.r);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
p.windowResized = () => {
|
|
88
|
+
w = Math.max(120, host.clientWidth);
|
|
89
|
+
h = Math.max(120, host.clientHeight);
|
|
90
|
+
p.resizeCanvas(w, h);
|
|
91
|
+
p.background(bg);
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default function App() {
|
|
97
|
+
const { database, useLiveQuery } = useFireproof("driftStudio");
|
|
98
|
+
const { docs: pieces } = useLiveQuery("type", { key: "piece" });
|
|
99
|
+
const piece = pieces[0];
|
|
100
|
+
const hostRef = useRef(null);
|
|
101
|
+
const seedRef = useRef(piece?.seed ?? 1234);
|
|
102
|
+
|
|
103
|
+
// The sketch reads the seed from the ref on its next frame.
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (piece) seedRef.current = piece.seed;
|
|
106
|
+
}, [piece?.seed]);
|
|
107
|
+
|
|
108
|
+
// The instance, built once into the host element, removed on unmount.
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const node = hostRef.current;
|
|
111
|
+
if (!node) return undefined;
|
|
112
|
+
const instance = new p5(makeSketch({ host: node, seedRef }), node);
|
|
113
|
+
return () => instance.remove();
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
const reseed = () => {
|
|
117
|
+
if (!piece) return;
|
|
118
|
+
database.put({ ...piece, seed: Math.floor(Math.random() * 100000), reseededAt: Date.now() });
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<main className="min-h-screen bg-background text-foreground font-sans p-6">
|
|
123
|
+
<h1 className="text-3xl font-bold">{piece?.title ?? "Drift"}</h1>
|
|
124
|
+
<p className="mt-1 text-muted-foreground">Seven dots wander on Perlin noise. Every seed is its own dance, and the same seed always plays it again.</p>
|
|
125
|
+
|
|
126
|
+
<Card className="mt-6 max-w-xl">
|
|
127
|
+
<CardHeader>
|
|
128
|
+
<CardTitle>{piece?.subtitle ?? "Study no. 1"}</CardTitle>
|
|
129
|
+
</CardHeader>
|
|
130
|
+
<CardContent>
|
|
131
|
+
<div ref={hostRef} className="aspect-square w-full overflow-hidden rounded-lg border border-border touch-none" />
|
|
132
|
+
<div className="mt-4 flex items-center gap-3">
|
|
133
|
+
<Button onClick={reseed}>New seed</Button>
|
|
134
|
+
<Badge variant="secondary">seed {piece?.seed ?? "…"}</Badge>
|
|
135
|
+
</div>
|
|
136
|
+
</CardContent>
|
|
137
|
+
</Card>
|
|
138
|
+
|
|
139
|
+
{/* TODO(next pass): grow makeSketch into the real piece, save still renders of a seed, palette picker, drawing with a finger */}
|
|
140
|
+
</main>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### `seed.json`
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"driftStudio": [
|
|
149
|
+
{ "key": "drift", "type": "piece", "title": "Drift", "subtitle": "Study no. 1 — seven wanderers", "seed": 4417 }
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Draft note: the next pass adds the real generative piece, saving still renders of a favourite seed, a palette picker, and drawing into the sketch with a finger.
|
|
155
|
+
|
|
156
|
+
## What stays the same in a p5 draft
|
|
157
|
+
|
|
158
|
+
- **The import is pinned**: `import p5 from "https://esm.sh/p5@1.11.13"`,
|
|
159
|
+
exactly — never a bare `"p5"`, which resolves to the 2.x line.
|
|
160
|
+
- **Instance mode, built once**: `new p5(makeSketch({ host, ... }), host)`
|
|
161
|
+
inside `useEffect(() => {...}, [])`, and the cleanup returns
|
|
162
|
+
`instance.remove()` so a remount never leaves a second draw loop running.
|
|
163
|
+
Every hook is assigned on `p` (`p.setup`, `p.draw`, `p.windowResized`).
|
|
164
|
+
- **The host is passed in and sizes the canvas**: `createCanvas` reads
|
|
165
|
+
`host.clientWidth`/`clientHeight`, the host is an `aspect-square` (or
|
|
166
|
+
`aspect-video`) box inside a Card, and `p.pixelDensity(Math.min(devicePixelRatio, 2))`
|
|
167
|
+
is set in setup.
|
|
168
|
+
- **The seed lives in a document and reaches the sketch through a ref**:
|
|
169
|
+
the button writes a new `seed` with `database.put`, an effect copies it into
|
|
170
|
+
`seedRef`, and `draw` notices the change and restarts with
|
|
171
|
+
`randomSeed`/`noiseSeed`, so the instance is never rebuilt for a new seed and
|
|
172
|
+
a seed always redraws the same piece.
|
|
173
|
+
- **The colors come from the theme**: `cssColor` resolves `--card` and
|
|
174
|
+
`--primary` into canvas colors; the page itself uses semantic classes.
|
|
175
|
+
- **The hello is small and visibly generative**: a few noise-driven dots with
|
|
176
|
+
a soft trail, never a blank canvas and never a placeholder picture.
|
|
177
|
+
- **What waits for the next pass** is named in the `TODO(next pass)` comment
|
|
178
|
+
and the draft note: the real piece, still renders, palettes, touch drawing.
|
|
179
|
+
The draft draws none of it.
|
package/llms/p5.js
CHANGED
package/llms/p5.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"p5.js","sourceRoot":"","sources":["../../jsr/llms/p5.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,QAAQ,GAAc;IACjC,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,OAAO;IACd,WAAW,EACT,uFAAuF;QACvF,wFAAwF;QACxF,sFAAsF;QACtF,wFAAwF;QACxF,0FAA0F;QAC1F,uFAAuF;QACvF,yFAAyF;IAC3F,IAAI,EAAE;QACJ,IAAI;QACJ,OAAO;QACP,YAAY;QACZ,QAAQ;QACR,gBAAgB;QAChB,iBAAiB;QACjB,YAAY;QACZ,cAAc;QACd,gBAAgB;QAChB,cAAc;QACd,aAAa;KACd;
|
|
1
|
+
{"version":3,"file":"p5.js","sourceRoot":"","sources":["../../jsr/llms/p5.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,QAAQ,GAAc;IACjC,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,OAAO;IACd,WAAW,EACT,uFAAuF;QACvF,wFAAwF;QACxF,sFAAsF;QACtF,wFAAwF;QACxF,0FAA0F;QAC1F,uFAAuF;QACvF,yFAAyF;IAC3F,IAAI,EAAE;QACJ,IAAI;QACJ,OAAO;QACP,YAAY;QACZ,QAAQ;QACR,gBAAgB;QAChB,iBAAiB;QACjB,YAAY;QACZ,cAAc;QACd,gBAAgB;QAChB,cAAc;QACd,aAAa;KACd;IAMD,YAAY,EAAE,IAAI;IAClB,YAAY,EAAE,2BAA2B;IACzC,UAAU,EAAE,IAAI;IAChB,UAAU,EAAE,SAAS;CACtB,CAAC"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Spotify catalog — the first draft: search that works on sample tracks
|
|
2
|
+
|
|
3
|
+
A Spotify catalog page's first draft is the finished page, painted from seeded
|
|
4
|
+
`track` documents shaped like the rows the real search route returns: a search
|
|
5
|
+
box and button that filter those tracks right in the browser by track name,
|
|
6
|
+
artist or album, result cards with the fields a real row carries, and the setup
|
|
7
|
+
card that walks the owner through registering a Spotify app. A Badge on the
|
|
8
|
+
header reads "sample data until the key is set", so searching works from the
|
|
9
|
+
first paint and the page says plainly where the live catalog comes from.
|
|
10
|
+
|
|
11
|
+
The next pass writes `backend.js`: it mints a client-credentials token from
|
|
12
|
+
`SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET`, holds it in memory, and
|
|
13
|
+
searches the catalog through the app's own `/_api/search` route. What the draft
|
|
14
|
+
owes that pass is the row shape: each `track` document carries the fields
|
|
15
|
+
`trackRow` returns (`id`, `name`, `artists` as one joined string, `album`,
|
|
16
|
+
`image`, `url`) plus `durationMs`, so the next pass swaps the filter for the
|
|
17
|
+
route and keeps every card. The draft writes no `backend.js`, makes no network
|
|
18
|
+
call, reads no secret, and never asks a visitor for a token: the credentials
|
|
19
|
+
are the owner's, set once in the vibe's settings.
|
|
20
|
+
|
|
21
|
+
### `App.jsx`
|
|
22
|
+
```jsx
|
|
23
|
+
import React, { useState } from "react";
|
|
24
|
+
import { useFireproof } from "use-fireproof";
|
|
25
|
+
import { Button, Card, CardHeader, CardTitle, CardContent, Badge, Input } from "@vibes.diy/look";
|
|
26
|
+
|
|
27
|
+
// The owner's one-time trip to the Spotify developer dashboard, carried in the app.
|
|
28
|
+
const SETUP_STEPS = [
|
|
29
|
+
"Open the Spotify developer dashboard and log in with a Spotify account.",
|
|
30
|
+
"Create an app with any name and description; put this page's URL in if the form asks for a redirect URI.",
|
|
31
|
+
"Open the app's settings and copy the Client ID.",
|
|
32
|
+
"Reveal and copy the Client secret.",
|
|
33
|
+
"Set both on this vibe as SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET in the vibe's settings page.",
|
|
34
|
+
];
|
|
35
|
+
const SECRET_NAMES = ["SPOTIFY_CLIENT_ID", "SPOTIFY_CLIENT_SECRET"];
|
|
36
|
+
|
|
37
|
+
function formatDuration(ms) {
|
|
38
|
+
const total = Math.round(ms / 1000);
|
|
39
|
+
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function matches(track, terms) {
|
|
43
|
+
const haystack = `${track.name} ${track.artists} ${track.album ?? ""}`.toLowerCase();
|
|
44
|
+
return terms.every((t) => haystack.includes(t));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function TrackCard({ track }) {
|
|
48
|
+
return (
|
|
49
|
+
<Card>
|
|
50
|
+
<CardContent>
|
|
51
|
+
<div className="flex items-center gap-4">
|
|
52
|
+
{track.image ? (
|
|
53
|
+
<img src={track.image} alt="" className="h-16 w-16 rounded-md object-cover" />
|
|
54
|
+
) : (
|
|
55
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-md bg-muted text-2xl font-bold text-muted-foreground">
|
|
56
|
+
{track.name.charAt(0)}
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
<div className="min-w-0 flex-1">
|
|
60
|
+
<p className="truncate font-semibold">
|
|
61
|
+
{track.url ? <a href={track.url} target="_blank" rel="noreferrer" className="underline">{track.name}</a> : track.name}
|
|
62
|
+
</p>
|
|
63
|
+
<p className="truncate text-muted-foreground">{track.artists}</p>
|
|
64
|
+
<p className="truncate text-muted-foreground">{track.album}</p>
|
|
65
|
+
</div>
|
|
66
|
+
<span className="tabular-nums text-muted-foreground">{formatDuration(track.durationMs)}</span>
|
|
67
|
+
</div>
|
|
68
|
+
</CardContent>
|
|
69
|
+
</Card>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default function App() {
|
|
74
|
+
const { useLiveQuery } = useFireproof("spotifyCatalog");
|
|
75
|
+
const { docs: tracks } = useLiveQuery("type", { key: "track" });
|
|
76
|
+
const [query, setQuery] = useState("");
|
|
77
|
+
const [applied, setApplied] = useState("");
|
|
78
|
+
|
|
79
|
+
const terms = applied.toLowerCase().split(/\s+/).filter(Boolean);
|
|
80
|
+
const results = terms.length === 0 ? tracks : tracks.filter((t) => matches(t, terms));
|
|
81
|
+
const search = () => setApplied(query.trim());
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<main className="min-h-screen bg-background text-foreground font-sans p-6">
|
|
85
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
86
|
+
<h1 className="text-3xl font-bold">Crate Digger</h1>
|
|
87
|
+
<Badge variant="secondary">sample data until the key is set</Badge>
|
|
88
|
+
</div>
|
|
89
|
+
<p className="mt-1 text-muted-foreground">Search Spotify's catalog by song, artist or album.</p>
|
|
90
|
+
|
|
91
|
+
<form
|
|
92
|
+
className="mt-6 flex gap-2"
|
|
93
|
+
onSubmit={(e) => {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
search();
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<Input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Try “harbor” or “Juniper Static”" />
|
|
99
|
+
<Button type="submit">Search</Button>
|
|
100
|
+
</form>
|
|
101
|
+
{/* TODO(next pass): the server handler mints the client-credentials token from SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET and searches the catalog through /_api/search; these cards render its rows */}
|
|
102
|
+
|
|
103
|
+
<p className="mt-4 text-muted-foreground">
|
|
104
|
+
{applied ? `${results.length} tracks for “${applied}”` : `${results.length} tracks to explore`}
|
|
105
|
+
</p>
|
|
106
|
+
<div className="mt-3 grid gap-3">
|
|
107
|
+
{results.map((track) => (
|
|
108
|
+
<TrackCard key={track.id} track={track} />
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
{applied && results.length === 0 && (
|
|
112
|
+
<p className="mt-3 text-muted-foreground">Try a different word from a song, artist or album.</p>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
<Card className="mt-8">
|
|
116
|
+
<CardHeader>
|
|
117
|
+
<CardTitle>Search the whole Spotify catalog</CardTitle>
|
|
118
|
+
</CardHeader>
|
|
119
|
+
<CardContent>
|
|
120
|
+
<p className="text-muted-foreground">Register a Spotify app once, and this search reaches every track on Spotify.</p>
|
|
121
|
+
<ol className="mt-3 grid list-decimal gap-2 pl-5">
|
|
122
|
+
{SETUP_STEPS.map((step) => (
|
|
123
|
+
<li key={step}>{step}</li>
|
|
124
|
+
))}
|
|
125
|
+
</ol>
|
|
126
|
+
<p className="mt-3">
|
|
127
|
+
Both secrets, {SECRET_NAMES.map((name, i) => (
|
|
128
|
+
<span key={name}>
|
|
129
|
+
{i > 0 && " and "}
|
|
130
|
+
<code className="rounded bg-muted px-1">{name}</code>
|
|
131
|
+
</span>
|
|
132
|
+
))}, stay on the server.
|
|
133
|
+
</p>
|
|
134
|
+
</CardContent>
|
|
135
|
+
</Card>
|
|
136
|
+
</main>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### `seed.json`
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"spotifyCatalog": [
|
|
145
|
+
{ "key": "t1", "type": "track", "id": "4kXq0sample1Harbor", "name": "Harbor Lights at Low Tide", "artists": "Juniper Static", "album": "Saltwater Radio", "image": null, "url": null, "durationMs": 214000 },
|
|
146
|
+
{ "key": "t2", "type": "track", "id": "7bRt2sample2Tangerine", "name": "Tangerine Freeway", "artists": "The Velvet Hours, Mara Quill", "album": "Summer in Reverse", "image": null, "url": null, "durationMs": 187500 },
|
|
147
|
+
{ "key": "t3", "type": "track", "id": "2mPz9sample3Lantern", "name": "Lantern Song", "artists": "Juniper Static", "album": "Saltwater Radio", "image": null, "url": null, "durationMs": 243800 },
|
|
148
|
+
{ "key": "t4", "type": "track", "id": "9cWe4sample4Paper", "name": "Paper Planets", "artists": "Odessa Bloom", "album": "Small Weather", "image": null, "url": null, "durationMs": 198200 },
|
|
149
|
+
{ "key": "t5", "type": "track", "id": "1hVn6sample5Midnight", "name": "Midnight Laundromat", "artists": "Kit Arden", "album": "Quarters", "image": null, "url": null, "durationMs": 226400 }
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Draft note: the next pass adds backend.js, which mints the client-credentials token from `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` and searches the public catalog, so the same cards show live tracks with album art and Spotify links.
|
|
155
|
+
|
|
156
|
+
## What stays the same in a Spotify draft
|
|
157
|
+
|
|
158
|
+
- **The whole page paints on the first draft**: header, search, result cards
|
|
159
|
+
and the setup card. Searching already works, filtering the seeded tracks in
|
|
160
|
+
the browser by track name, artist and album.
|
|
161
|
+
- **Seeded rows mirror the backend's rows**: each `track` document carries
|
|
162
|
+
`trackRow`'s fields (`id`, `name`, `artists` joined into one string, `album`,
|
|
163
|
+
`image`, `url`) plus `durationMs`, so the next pass swaps the source and keeps
|
|
164
|
+
every card. A card handles a missing `image` or `url` gracefully.
|
|
165
|
+
- **The capability is labeled, never hidden**: a Badge on the header reads
|
|
166
|
+
"sample data until the key is set", and the setup card names the Spotify
|
|
167
|
+
developer dashboard, `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` in so
|
|
168
|
+
many words.
|
|
169
|
+
- **The credentials are the owner's**: the setup steps are for the owner, once.
|
|
170
|
+
No input asks a visitor for a token, and no visitor is sent to a developer
|
|
171
|
+
console.
|
|
172
|
+
- **The page reads the public catalog**: search, artists, albums and tracks. A
|
|
173
|
+
listener's own top tracks, playlists and saved songs belong to their own
|
|
174
|
+
Spotify account, which is a connection and not this flow, so the draft draws
|
|
175
|
+
no section for them.
|
|
176
|
+
- **What waits for the next pass** is named in the `TODO(next pass)` comment and
|
|
177
|
+
the draft note: `backend.js`, the client-credentials token held in memory, the
|
|
178
|
+
`/_api/search` route and its refusal reasons. The draft writes no
|
|
179
|
+
`backend.js`, makes no network call and reads no secret.
|
package/llms/spotify.js
CHANGED
|
@@ -2,6 +2,7 @@ export const spotifyConfig = {
|
|
|
2
2
|
name: "spotify",
|
|
3
3
|
label: "Spotify catalog",
|
|
4
4
|
description: "Reading Spotify's public catalog with the owner's own Spotify app credentials: the setup steps the app shows when SPOTIFY_CLIENT_ID/SPOTIFY_CLIENT_SECRET are missing, the client-credentials token minted in backend.js and cached in memory rather than stored, searching artists/albums/tracks, the boundary that a listener's own top tracks and playlists need a connection instead, and keeping every refusal's reason",
|
|
5
|
+
draftVariant: true,
|
|
5
6
|
cues: [
|
|
6
7
|
"spotify",
|
|
7
8
|
"spotify search",
|
package/llms/spotify.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spotify.js","sourceRoot":"","sources":["../../jsr/llms/spotify.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,iBAAiB;IACxB,WAAW,EACT,8ZAA8Z;
|
|
1
|
+
{"version":3,"file":"spotify.js","sourceRoot":"","sources":["../../jsr/llms/spotify.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,iBAAiB;IACxB,WAAW,EACT,8ZAA8Z;IAGha,YAAY,EAAE,IAAI;IAClB,IAAI,EAAE;QACJ,SAAS;QACT,gBAAgB;QAChB,WAAW;QACX,cAAc;QACd,eAAe;QACf,eAAe;QACf,gBAAgB;KACjB;CACF,CAAC"}
|