From a6348d5c46d176a3db3a73b953923716503573b3 Mon Sep 17 00:00:00 2001 From: Dreamodus Date: Tue, 14 Jul 2026 06:11:26 -0700 Subject: [PATCH 1/4] v1.1.2: action button, responsive faders, label scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third integration-feedback release from building the streaming studio. - New `Button` — a click-to-fire command button in the panel style, the non-latching sibling of PushButton. Implemented as PushButton's new `mode="action"` (never latches, press-down + brief LED flash, `onClick`); led="dot" (default) matches the corner-dot look, led={false} is plain. - `fill` on LEDFader (parity with Fader) — faders stretch to fill their container in resizable panels instead of a fixed pixel length. - `labelSize` on IndicatorLamp (and LampRow) — shrink the caption below its 10px floor to match dense header rows. - Docs: a dedicated "Buttons & switches" section with a decision guide (Button/PushButton/TransportButton/ToggleSwitch/SegmentSwitch) + live demos. Library + docs typecheck and build; 38 tests pass. --- README.md | 2 +- apps/docs/src/App.tsx | 3 + apps/docs/src/sections/ApiDocs.tsx | 7 +- apps/docs/src/sections/Buttons.tsx | 156 ++++++++++++++++++ apps/docs/src/styles.css | 70 ++++++++ packages/dreamknob/CHANGELOG.md | 32 ++++ packages/dreamknob/README.md | 2 +- packages/dreamknob/package.json | 2 +- packages/dreamknob/src/index.ts | 2 +- .../dreamknob/src/skins/IndicatorLamp.tsx | 9 +- packages/dreamknob/src/skins/LEDFader.tsx | 32 +++- packages/dreamknob/src/skins/LampRow.tsx | 5 +- packages/dreamknob/src/skins/PushButton.tsx | 77 +++++++-- 13 files changed, 372 insertions(+), 27 deletions(-) create mode 100644 apps/docs/src/sections/Buttons.tsx diff --git a/README.md b/README.md index 546f3ec..04cf677 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ See `packages/dreamknob/README.md` and the docs app for the full API. The easiest way is the published release on our Forgejo: ```bash -pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.1/dreamknob-1.1.1.tgz +pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.2/dreamknob-1.1.2.tgz ``` Or build locally — the package builds to `packages/dreamknob/dist` (ESM + CJS + types): diff --git a/apps/docs/src/App.tsx b/apps/docs/src/App.tsx index 09bca6d..1d02c03 100644 --- a/apps/docs/src/App.tsx +++ b/apps/docs/src/App.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { AdvancedGuide } from './sections/AdvancedGuide'; import { ApiDocs } from './sections/ApiDocs'; +import { Buttons } from './sections/Buttons'; import { Console } from './sections/Console'; import { Gallery } from './sections/Gallery'; import { GettingStarted } from './sections/GettingStarted'; @@ -26,6 +27,7 @@ export const App: React.FC = () => (
Start Gallery + Buttons Synth Playground Guide @@ -36,6 +38,7 @@ export const App: React.FC = () => ( + diff --git a/apps/docs/src/sections/ApiDocs.tsx b/apps/docs/src/sections/ApiDocs.tsx index 2d4f086..ac44fa2 100644 --- a/apps/docs/src/sections/ApiDocs.tsx +++ b/apps/docs/src/sections/ApiDocs.tsx @@ -129,13 +129,14 @@ export const ApiDocs: React.FC = () => ( - + - - + + + diff --git a/apps/docs/src/sections/Buttons.tsx b/apps/docs/src/sections/Buttons.tsx new file mode 100644 index 0000000..02676f4 --- /dev/null +++ b/apps/docs/src/sections/Buttons.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { + Button, + PushButton, + TransportButton, + ToggleSwitch, + SegmentSwitch, +} from 'dreamknob'; + +/** Small glyph for a button's leadingIcon (the docs app has no icon dep). */ +const Glyph: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + +); + +/** The button/switch family, side by side, with a "which do I use?" guide. */ +export const Buttons: React.FC = () => { + const [playing, setPlaying] = React.useState(false); + const [armed, setArmed] = React.useState(false); + const [bypass, setBypass] = React.useState(false); + const [fresh, setFresh] = React.useState(true); + const [mode, setMode] = React.useState(0); + const [fired, setFired] = React.useState(0); + + return ( +
+
+
Buttons & switches
+

Press, latch, or pick

+

+ Five components cover every panel control that isn't a dial. The + only question is what the press means: +

+ +
+ + + + + +
+ +
+ — action, fired ${fired}×`}> + + + + + + + + Bypass + + + Talk + + + + + setPlaying(p => !p)} size={34} /> + setPlaying(false)} size={34} /> + setArmed(a => !a)} size={34} /> + + + + + + +
+ +

+ <Button> is <PushButton mode="action"> — + it never latches, shows a press-down and a brief LED flash, and calls{' '} + onClick. led="dot" gives the corner-dot + look; led={false} a plain button. +

+
+
+ ); +}; + +const GuideRow: React.FC<{ code: string; when: string; eg: string }> = ({ code, when, eg }) => ( +
+ {code} + {when} + {eg} +
+); + +const Demo: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => ( +
+
{label}
+
{children}
+
+); diff --git a/apps/docs/src/styles.css b/apps/docs/src/styles.css index 1ea24c2..b17d254 100644 --- a/apps/docs/src/styles.css +++ b/apps/docs/src/styles.css @@ -353,6 +353,76 @@ h2.section-title { text-transform: uppercase; } +/* Buttons section: decision guide + live demo grid */ +.guide-grid { + display: flex; + flex-direction: column; + gap: 1px; + margin: 8px 0 28px; + border-radius: 12px; + overflow: hidden; + border: 1px solid var(--line); + background: var(--line); +} +.guide-row { + display: grid; + grid-template-columns: 190px 1fr; + gap: 4px 18px; + align-items: baseline; + padding: 12px 16px; + background: #101219; +} +.guide-code { + font-family: var(--font-mono); + font-size: 13px; + color: #4cc2ff; +} +.guide-when { + font-size: 14px; + color: var(--text); +} +.guide-eg { + grid-column: 2; + font-size: 12.5px; + color: var(--text-dim); +} +.btn-demo { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 12px; +} +.btn-demo-cell { + display: flex; + flex-direction: column; + gap: 14px; + padding: 18px; + border-radius: 12px; + background: linear-gradient(180deg, #16181f, #0e0f14); + border: 1px solid var(--line-strong); +} +.btn-demo-label { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.08em; + color: var(--text-dim); + text-transform: uppercase; +} +.btn-demo-row { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + min-height: 40px; +} +@media (max-width: 560px) { + .guide-row { + grid-template-columns: 1fr; + } + .guide-eg { + grid-column: 1; + } +} + /* ---- synth panel ---- */ .synth-rows { display: flex; diff --git a/packages/dreamknob/CHANGELOG.md b/packages/dreamknob/CHANGELOG.md index eb9e2ab..07ab660 100644 --- a/packages/dreamknob/CHANGELOG.md +++ b/packages/dreamknob/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## 1.1.2 — 2026-07-14 + +Third integration-feedback release — a general action button, responsive +faders everywhere, and the label-scaling gap, all surfaced building a +resizable streaming studio. + +### New components +- `Button` — a normal **click-to-fire** command button in the panel style + (Regenerate, Add, Apply, Refresh), the non-latching sibling of `PushButton`. + It's `PushButton` locked to the new `mode="action"`: never latches, shows a + press-down + a brief LED flash, calls `onClick`. `led="dot"` (default) gives + the corner-dot look; `led={false}` a plain button. + +### Buttons +- `PushButton` gains **`mode="action"`** (alongside `toggle`/`momentary`) and + an `onClick` prop — the stateless command-button behavior `Button` wraps. + +### Responsive faders +- **`fill`** on `LEDFader` (matching `Fader`) — the fader stretches along its + travel axis to fill the container instead of a fixed `length`, so faders in + resizable dock panels size themselves with no wrapper measurement. + +### Label scaling +- **`labelSize`** on `IndicatorLamp` — the caption otherwise tracks `size` but + is floored at 10px; set this to shrink it with a small lamp (e.g. matching a + dense header row). Also on `LampRow` for parity. + +### Docs +- A dedicated **Buttons & switches** section with a "which do I use?" guide + (Button = action · PushButton = latch · TransportButton = transport · + ToggleSwitch = boolean · SegmentSwitch = mode) and a live demo of each. + ## 1.1.1 — 2026-07-13 Second integration-feedback release — the friction that showed up once the diff --git a/packages/dreamknob/README.md b/packages/dreamknob/README.md index 4687401..0a03e50 100644 --- a/packages/dreamknob/README.md +++ b/packages/dreamknob/README.md @@ -5,7 +5,7 @@ headless core, any number range, any decimal precision, and colors you fully con Zero runtime dependencies. ``` -pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.1/dreamknob-1.1.1.tgz +pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.2/dreamknob-1.1.2.tgz ``` (Latest release tarballs: https://git.dreamodus.software/Dreamodus/DreamKnob/releases) diff --git a/packages/dreamknob/package.json b/packages/dreamknob/package.json index be13c56..b587886 100644 --- a/packages/dreamknob/package.json +++ b/packages/dreamknob/package.json @@ -1,6 +1,6 @@ { "name": "dreamknob", - "version": "1.1.1", + "version": "1.1.2", "description": "A feature-full, studio-grade React knob & fader library. Rotary, linear, 2D, 3D, digital and analog styles with a headless core.", "license": "MIT", "author": "Dreamodus Software Inc.", diff --git a/packages/dreamknob/src/index.ts b/packages/dreamknob/src/index.ts index 7f58d1c..f030813 100644 --- a/packages/dreamknob/src/index.ts +++ b/packages/dreamknob/src/index.ts @@ -70,7 +70,7 @@ export { ImageKnob, type ImageKnobProps } from './skins/ImageKnob'; export { Meter, type MeterProps } from './skins/Meter'; export { MeterBridge, type MeterBridgeProps, type MeterBridgeChannel } from './skins/MeterBridge'; export { Gauge, type GaugeProps } from './skins/Gauge'; -export { PushButton, type PushButtonProps } from './skins/PushButton'; +export { PushButton, type PushButtonProps, Button, type ButtonProps } from './skins/PushButton'; export { IndicatorLamp, type IndicatorLampProps } from './skins/IndicatorLamp'; export { LampRow, type LampRowProps, type LampRowItem } from './skins/LampRow'; export { ToggleSwitch, type ToggleSwitchProps } from './skins/ToggleSwitch'; diff --git a/packages/dreamknob/src/skins/IndicatorLamp.tsx b/packages/dreamknob/src/skins/IndicatorLamp.tsx index 8154410..f02f48c 100644 --- a/packages/dreamknob/src/skins/IndicatorLamp.tsx +++ b/packages/dreamknob/src/skins/IndicatorLamp.tsx @@ -19,6 +19,12 @@ export interface IndicatorLampProps { readOnly?: boolean; /** Caption beside the lamp. */ label?: string; + /** + * Caption font size in px, independent of the lamp diameter. Without it the + * label tracks `size` but is floored at 10px; set this to shrink the caption + * with a small lamp (e.g. matching a dense header row's height). + */ + labelSize?: number; labelPosition?: 'right' | 'left'; disabled?: boolean; /** Render a hidden form input carrying "on"/"off". */ @@ -45,6 +51,7 @@ export const IndicatorLamp = React.forwardRef { orientation?: 'vertical' | 'horizontal'; /** Travel length in px. Default: 160. */ length?: number; + /** + * Stretch along the travel axis to fill the container (horizontal fills + * width, vertical fills height) instead of a fixed `length` — the same + * responsive behavior as `Fader`, so faders in resizable dock panels size + * themselves instead of needing wrapper measurement. + */ + fill?: boolean; /** Cross-axis size in px. Default: 34. */ breadth?: number; /** Number of LED segments. Default: 24. */ @@ -57,6 +64,7 @@ export interface LEDFaderProps extends Omit { export const LEDFader = React.forwardRef(function LEDFader({ orientation = 'vertical', length = 160, + fill = false, breadth = 34, segments = 24, color: colorProp, @@ -95,6 +103,12 @@ export const LEDFader = React.forwardRef(function const w = vertical ? breadth : length; const h = vertical ? length : breadth; + // Fill stretches the travel axis (via preserveAspectRatio='none'); the cross + // axis stays at `breadth`. The LED geometry is computed in the fixed viewBox + // and scaled to the element — pointer mapping measures the real element, so + // interaction stays correct at any rendered width. + const ctrlW = fill && !vertical ? '100%' : w; + const ctrlH = fill && vertical ? '100%' : h; const zoneList: readonly LEDFaderZone[] = React.useMemo(() => { if (zones) return [...zones].sort((a, b) => a.upTo - b.upTo); if (color) return [{ upTo: 1, color }]; @@ -157,7 +171,8 @@ export const LEDFader = React.forwardRef(function flexDirection: 'column', alignItems: 'center', gap: 6, - width: w, + width: fill && !vertical ? '100%' : w, + height: fill && vertical ? '100%' : undefined, opacity: core.disabled ? 0.45 : undefined, ...style, }} @@ -218,14 +233,21 @@ export const LEDFader = React.forwardRef(function style={{ ...knob.bind.style, opacity: undefined, // the outer wrapper owns disabled dimming - width: w, - height: h, - display: 'inline-flex', + width: ctrlW, + height: ctrlH, + display: fill ? 'flex' : 'inline-flex', borderRadius: 6, ...(knob.isFocusVisible && ring ? { boxShadow: `0 0 0 2px ${ring}` } : undefined), }} > - void; - /** 'toggle' latches; 'momentary' is only on while held. Default: 'toggle'. */ - mode?: 'toggle' | 'momentary'; + /** + * 'toggle' latches; 'momentary' is on only while held; 'action' is a normal + * click-to-fire command button — it never latches, shows a press-down + a + * brief LED flash, and calls `onClick`. Default: 'toggle'. + */ + mode?: 'toggle' | 'momentary' | 'action'; + /** Fired on activation in `action` mode (click, Enter, or Space). */ + onClick?: () => void; /** LED / active color. */ color?: string; /** @@ -39,6 +45,7 @@ export const PushButton = React.forwardRef( pressed, defaultPressed = false, onChange, + onClick, mode = 'toggle', color, led = true, @@ -59,16 +66,42 @@ export const PushButton = React.forwardRef( const isControlled = pressed !== undefined; const [internal, setInternal] = React.useState(defaultPressed); const [focusVisible, setFocusVisible] = React.useState(false); - const on = isControlled ? (pressed as boolean) : internal; + // Action mode is stateless: a transient `held` (pointer down) drives the + // press-down look and a short `flash` lights the LED on activation. + const [held, setHeld] = React.useState(false); + const [flash, setFlash] = React.useState(false); + const flashTimer = React.useRef>(); + React.useEffect(() => () => clearTimeout(flashTimer.current), []); + const latched = isControlled ? (pressed as boolean) : internal; + const on = mode === 'action' ? held : latched; // press-down / background inset + const ledOn = mode === 'action' ? held || flash : latched; const set = (next: boolean) => { - if (next === on) return; + if (next === latched) return; if (!isControlled) setInternal(next); onChange?.(next); }; + const fire = () => { + onClick?.(); + setFlash(true); + clearTimeout(flashTimer.current); + flashTimer.current = setTimeout(() => setFlash(false), 160); + }; + const momentaryProps = - mode === 'momentary' + mode === 'action' + ? { + onClick: fire, + onPointerDown: (e: React.PointerEvent) => { + e.currentTarget.setPointerCapture(e.pointerId); + setHeld(true); + }, + onPointerUp: () => setHeld(false), + onPointerCancel: () => setHeld(false), + onPointerLeave: () => setHeld(false), + } + : mode === 'momentary' ? { onPointerDown: (e: React.PointerEvent) => { e.currentTarget.setPointerCapture(e.pointerId); @@ -83,14 +116,14 @@ export const PushButton = React.forwardRef( if (e.key === ' ' || e.key === 'Enter') set(false); }, } - : { onClick: () => set(!on) }; + : { onClick: () => set(!latched) }; return ( ); }, ); + +export interface ButtonProps + extends Omit { + onClick?: () => void; +} + +/** + * A normal click-to-fire command button in the studio-panel style — the + * non-latching sibling of `PushButton`. Use it for actions (Regenerate, + * Add, Apply, Refresh). It's `PushButton` locked to `mode="action"`; pass + * `led="dot"` for the corner-dot look (like a Fresh Seed button) or + * `led={false}` for a plain button. + */ +export const Button = React.forwardRef( + function Button({ led = 'dot', ...rest }, ref) { + return ; + }, +); From 605c38af51216a4565605c3164c4e5959f119b51 Mon Sep 17 00:00:00 2001 From: Dreamodus Date: Tue, 14 Jul 2026 06:36:39 -0700 Subject: [PATCH 2/4] docs: point the site install command at the v1.1.2 release tarball The READMEs were bumped for 1.1.2 but the docs Hero/GettingStarted still showed the v1.1.1 URL. --- apps/docs/src/sections/GettingStarted.tsx | 4 ++-- apps/docs/src/sections/Hero.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/docs/src/sections/GettingStarted.tsx b/apps/docs/src/sections/GettingStarted.tsx index f973723..895817d 100644 --- a/apps/docs/src/sections/GettingStarted.tsx +++ b/apps/docs/src/sections/GettingStarted.tsx @@ -30,12 +30,12 @@ export const GettingStarted: React.FC = () => ( ) · peer deps: react >= 18, react-dom >= 18`} + code={`pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.2/dreamknob-1.1.2.tgz\n# (or npm install ) · peer deps: react >= 18, react-dom >= 18`} />

Installs straight from the{' '} - v1.1.1 release + v1.1.2 release {' '} on our Forgejo. Zero runtime dependencies, ESM + CJS, full TypeScript types. No CSS file to import — everything is SVG and inline styles. diff --git a/apps/docs/src/sections/Hero.tsx b/apps/docs/src/sections/Hero.tsx index f71020d..f66c4b4 100644 --- a/apps/docs/src/sections/Hero.tsx +++ b/apps/docs/src/sections/Hero.tsx @@ -25,10 +25,10 @@ export const Hero: React.FC = () => { range, any decimal precision, and colors you fully control.

- pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.1/dreamknob-1.1.1.tgz + pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.2/dreamknob-1.1.2.tgz + + {/* … */} +`} />

Precision & number handling

diff --git a/apps/docs/src/sections/Buttons.tsx b/apps/docs/src/sections/Buttons.tsx index 02676f4..1cf6753 100644 --- a/apps/docs/src/sections/Buttons.tsx +++ b/apps/docs/src/sections/Buttons.tsx @@ -82,7 +82,7 @@ export const Buttons: React.FC = () => {
diff --git a/apps/docs/src/sections/Gallery.tsx b/apps/docs/src/sections/Gallery.tsx index 050b87f..aefbe68 100644 --- a/apps/docs/src/sections/Gallery.tsx +++ b/apps/docs/src/sections/Gallery.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { AlphaDisplay, Arc, + Button, DreamknobProvider, Fader, FlatKnob, @@ -18,6 +19,7 @@ import { Meter, MeterBridge, NeonKnob, + PanKnob, Pointer, PushButton, Rack, @@ -280,7 +282,10 @@ const PanelDemo: React.FC = () => { - +
+ + +
{ ); }; +const DbMeterDemo: React.FC = () => { + // Drive with linear amplitude; the meter converts to dBFS internally. + const [amp, setAmp] = React.useState<[number, number]>([0.3, 0.28]); + React.useEffect(() => { + let t = 0; + const id = setInterval(() => { + t += 0.11; + const spike = Math.random() < 0.05 ? 0.4 : 0; + setAmp([ + Math.max(0.001, Math.min(1.2, 0.34 + Math.sin(t) * 0.22 + Math.random() * 0.16 + spike)), + Math.max(0.001, Math.min(1.2, 0.32 + Math.sin(t * 1.25 + 1) * 0.22 + Math.random() * 0.16 + spike)), + ]); + }, 90); + return () => clearInterval(id); + }, []); + return ( +
+ + +
+ ); +}; + +const PanDemo: React.FC = () => ( +
+ + +
+); + const ImageKnobDemo: React.FC = () => { const src = useGeneratedStrip(); if (!src) return null; @@ -509,6 +569,10 @@ export const Gallery: React.FC = () => ( + + + + @@ -536,6 +600,10 @@ export const Gallery: React.FC = () => ( + + + + ( ) · peer deps: react >= 18, react-dom >= 18`} + code={`pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.3/dreamknob-1.1.3.tgz\n# (or npm install ) · peer deps: react >= 18, react-dom >= 18`} />

Installs straight from the{' '} - v1.1.2 release + v1.1.3 release {' '} on our Forgejo. Zero runtime dependencies, ESM + CJS, full TypeScript types. No CSS file to import — everything is SVG and inline styles. diff --git a/apps/docs/src/sections/Hero.tsx b/apps/docs/src/sections/Hero.tsx index f66c4b4..2808d9c 100644 --- a/apps/docs/src/sections/Hero.tsx +++ b/apps/docs/src/sections/Hero.tsx @@ -25,10 +25,10 @@ export const Hero: React.FC = () => { range, any decimal precision, and colors you fully control.

- pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.2/dreamknob-1.1.2.tgz + pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.3/dreamknob-1.1.3.tgz
); +const LightThemeDemo: React.FC = () => { + const [play, setPlay] = React.useState(false); + const [mute, setMute] = React.useState(true); + return ( + +
+
+ + + +
+
+ + Mute + + + setPlay(p => !p)} size={32} /> + +
+
+
+ ); +}; + const ImageKnobDemo: React.FC = () => { const src = useGeneratedStrip(); if (!src) return null; @@ -565,6 +601,10 @@ export const Gallery: React.FC = () => (
+ + + + diff --git a/apps/docs/src/sections/GettingStarted.tsx b/apps/docs/src/sections/GettingStarted.tsx index 526962c..32dbbc9 100644 --- a/apps/docs/src/sections/GettingStarted.tsx +++ b/apps/docs/src/sections/GettingStarted.tsx @@ -30,12 +30,12 @@ export const GettingStarted: React.FC = () => ( ) · peer deps: react >= 18, react-dom >= 18`} + code={`pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.4/dreamknob-1.1.4.tgz\n# (or npm install ) · peer deps: react >= 18, react-dom >= 18`} />

Installs straight from the{' '} - v1.1.3 release + v1.1.4 release {' '} on our Forgejo. Zero runtime dependencies, ESM + CJS, full TypeScript types. No CSS file to import — everything is SVG and inline styles. diff --git a/apps/docs/src/sections/Hero.tsx b/apps/docs/src/sections/Hero.tsx index 2808d9c..4669d40 100644 --- a/apps/docs/src/sections/Hero.tsx +++ b/apps/docs/src/sections/Hero.tsx @@ -25,10 +25,10 @@ export const Hero: React.FC = () => { range, any decimal precision, and colors you fully control.

- pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.3/dreamknob-1.1.3.tgz + pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.4/dreamknob-1.1.4.tgz