Skip to content

Commit f75d67d

Browse files
committed
module categories
1 parent 838d7e4 commit f75d67d

4 files changed

Lines changed: 67 additions & 26 deletions

File tree

src/components/Palette.jsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useEffect, useState } from "react";
22
import { useSynthStore } from "../store/useSynthStore.js";
3-
import { paletteList } from "../modules/_registry.js";
3+
import { paletteGroups } from "../modules/_registry.js";
44
import { usePuzzleConfig } from "../content/puzzleHooks.js";
55

6-
// Free-mode palette: lists every module type listed in PALETTE_ORDER
7-
// (see _registry.js), in array order. Each click adds a fresh instance to
6+
// Free-mode palette: lists every module type in PALETTE_GROUPS
7+
// (see _registry.js), grouped by category in display order. Each click adds a fresh instance to
88
// the store; the bridge picks it up on reconcile. Collapsible — the header
99
// is a button that toggles the list. Collapse state is purely local (no
1010
// store/persist) because it's a transient UI preference.
@@ -24,7 +24,7 @@ export function Palette() {
2424
// Puzzle mode owns the module set (journey deltas) and the layout (auto-snap),
2525
// so an "Add module" affordance has nothing useful to do — hide it entirely.
2626
if (puzzle) return null;
27-
const items = paletteList();
27+
const groups = paletteGroups();
2828

2929
return (
3030
<aside className={"palette" + (open ? "" : " collapsed")}>
@@ -39,16 +39,21 @@ export function Palette() {
3939
</button>
4040
{open && (
4141
<div className="palette-list">
42-
{items.map((m) => (
43-
<button
44-
key={m.type}
45-
className={"palette-item " + (m.Cls.KIND === "control" ? "control" : "audio")}
46-
onClick={() => addModuleInstance(m.type, m.defaults())}
47-
title={`Add ${m.meta.title}`}
48-
>
49-
<span className="plus" aria-hidden="true">+</span>
50-
<span className="palette-name">{m.meta.title}</span>
51-
</button>
42+
{groups.map((g) => (
43+
<div className="palette-group" key={g.key}>
44+
<div className="palette-group-head">{g.label}</div>
45+
{g.items.map((m) => (
46+
<button
47+
key={m.type}
48+
className={"palette-item " + (m.Cls.KIND === "control" ? "control" : "audio")}
49+
onClick={() => addModuleInstance(m.type, m.defaults())}
50+
title={`Add ${m.meta.title}`}
51+
>
52+
<span className="plus" aria-hidden="true">+</span>
53+
<span className="palette-name">{m.meta.title}</span>
54+
</button>
55+
))}
56+
</div>
5257
))}
5358
</div>
5459
)}

src/modules/_registry.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,29 +33,44 @@ export const MODULES = [
3333
Inverter, CvMixer, Attenuator, Attenuverter, Offset, AudioMixer,
3434
];
3535

36-
// Palette order. Membership = inclusion; index = display order. Modules absent
37-
// from this array don't appear in the palette. `output` is intentionally
38-
// excluded — every patch has exactly one Output module which the store
39-
// guarantees, so it isn't user-addable.
40-
const PALETTE_ORDER = [
41-
"oscillator", "filter", "audiomixer", "amp", "env", "arenv", "adenv", "lfo",
42-
"keyboard", "trigger", "clock", "drumseq", "counter", "counter3", "multiplexer", "mux8", "quantizer",
43-
"inverter", "attenuator", "attenuverter", "cvmixer", "offset",
36+
// Palette grouping. Modules are organised into categories; within each group
37+
// the `types` order is the display order, and the groups themselves display
38+
// top-to-bottom in array order. Membership here = palette inclusion: a module
39+
// absent from every group doesn't appear in the palette. `output` is
40+
// intentionally excluded — every patch has exactly one Output module which the
41+
// store guarantees, so it isn't user-addable.
42+
const PALETTE_GROUPS = [
43+
{ key: "audio", label: "Audio", types: ["oscillator", "filter", "audiomixer", "amp"] },
44+
{ key: "modulation", label: "Modulation", types: ["env", "arenv", "adenv", "lfo"] },
45+
{ key: "trigger", label: "Trigger", types: ["keyboard", "trigger", "clock", "drumseq"] },
46+
{ key: "logic", label: "Logic", types: ["counter", "counter3", "multiplexer", "mux8"] },
47+
{ key: "utility", label: "Utility", types: ["quantizer", "inverter", "attenuator", "attenuverter", "cvmixer", "offset"] },
4448
];
4549

50+
// Flattened palette order, derived from the groups.
51+
const PALETTE_ORDER = PALETTE_GROUPS.flatMap((g) => g.types);
52+
4653
// Validate at import time. A malformed manifest crashes startup loudly
4754
// instead of producing silent UI/audio bugs deep in some lookup.
4855
for (const m of MODULES) validateManifest(m);
4956
if (new Set(PALETTE_ORDER).size !== PALETTE_ORDER.length) {
50-
throw new Error(`[modules] PALETTE_ORDER contains duplicates: ${PALETTE_ORDER.join(", ")}`);
57+
throw new Error(`[modules] PALETTE_GROUPS contains duplicate types: ${PALETTE_ORDER.join(", ")}`);
5158
}
5259
for (const type of PALETTE_ORDER) {
5360
if (!MODULES.some((m) => m.type === type)) {
54-
throw new Error(`[modules] PALETTE_ORDER references unknown module type: ${type}`);
61+
throw new Error(`[modules] PALETTE_GROUPS references unknown module type: ${type}`);
5562
}
5663
}
5764

5865
const _byTypeMap = new Map(MODULES.map((m) => [m.type, m]));
5966

6067
export const byType = (type) => _byTypeMap.get(type) || null;
6168
export const paletteList = () => PALETTE_ORDER.map((type) => _byTypeMap.get(type));
69+
// Same modules as paletteList(), but bucketed by category for a grouped UI:
70+
// [{ key, label, items: Manifest[] }, …] in display order.
71+
export const paletteGroups = () =>
72+
PALETTE_GROUPS.map((g) => ({
73+
key: g.key,
74+
label: g.label,
75+
items: g.types.map((type) => _byTypeMap.get(type)),
76+
}));

src/modules/_types.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
* @property {string} placard HTML body shown in the narrator on click
2121
* @property {*} [glyph] m-head silkscreen SVG element (or null)
2222
*
23-
* Free-mode palette membership/order lives in `_registry.js` (PALETTE_ORDER).
23+
* Free-mode palette membership/order/category lives in `_registry.js` (PALETTE_GROUPS).
2424
*/
2525

2626
const REQUIRED_FIELDS = ["type", "Cls", "Panel", "meta", "defaults", "placard"];

src/styles/global.css

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1539,7 +1539,9 @@ body.module-dragging * { cursor: grabbing !important; }
15391539
.palette {
15401540
position: absolute;
15411541
left: 18px; bottom: 18px;
1542-
width: 184px;
1542+
/* Wide enough for two columns of items; capped to the viewport (keeping the
1543+
18px left/right margins) so it never overflows on a narrow phone. */
1544+
width: min(340px, calc(100vw - 36px));
15431545
/* The "Add module" head sits at the bottom; padding-bottom (7px) is the
15441546
gap between the head and the palette's outer bottom edge. Collapsed
15451547
state mirrors this value top-and-bottom so the head's vertical
@@ -1599,6 +1601,25 @@ body.module-dragging * { cursor: grabbing !important; }
15991601
flex: 1 1 auto;
16001602
min-height: 0;
16011603
}
1604+
/* Category buckets within the list. Items flow in a 2-column grid so the
1605+
list stays short; the head spans both columns as a band above its items. */
1606+
.palette-group {
1607+
display: grid;
1608+
grid-template-columns: 1fr 1fr;
1609+
gap: 6px;
1610+
align-content: start;
1611+
}
1612+
.palette-group-head {
1613+
grid-column: 1 / -1;
1614+
font-family: var(--f-mono);
1615+
font-size: 9px; letter-spacing: var(--t-tracking-eyebrow); text-transform: uppercase;
1616+
color: var(--ink-quiet);
1617+
padding-bottom: 2px;
1618+
border-bottom: 1px solid var(--ink-rule);
1619+
}
1620+
/* First group's head hugs the list top; later heads get breathing room
1621+
above so the categories read as distinct bands. */
1622+
.palette-group + .palette-group { margin-top: 6px; }
16021623
.palette-item {
16031624
font-family: var(--f-sans);
16041625
font-size: 13px; letter-spacing: 0;

0 commit comments

Comments
 (0)