Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Knob (2) #2

Merged
merged 24 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"eslint.workingDirectories": [
"apps/docs",
"packages/eslint-config",
"packages/react-knob-headless"
]
Expand Down
1 change: 1 addition & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"test:lint": "next lint --max-warnings=0 --format=compact"
},
"dependencies": {
"clsx": "2.0.0",
"next": "13.5.3",
"react": "18.2.0",
"react-dom": "18.2.0",
Expand Down
51 changes: 50 additions & 1 deletion apps/docs/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,63 @@
import {KnobAbletonPan} from '@/components/KnobAbletonPan';
import {KnobHeadlessDemo} from '../components/KnobHeadlessDemo';
import {KnobMoisesPan} from '../components/KnobMoisesPan';

function IndexPage() {
return (
<>
<h1 className='px-4 py-8 text-center text-4xl'>React Knob Headless</h1>
<div className='flex items-center justify-center bg-stone-800 px-4 py-8'>
<div className='bg-black'>
<div className='flex flex-col max-w-lg mx-auto items-center justify-center px-4 py-8 gap-4'>
<AbletonPanCard title='Ableton: Pan knob' theme='mid-light' />
<AbletonPanCard title='Ableton: Pan knob' theme='ableton-9' />
<MoisesKnobs />
</div>
</div>
<div className='flex flex-col px-4 py-8 pt-32 max-w-sm'>
<h3>Playground</h3>
<KnobHeadlessDemo />
</div>
</>
);
}

function AbletonPanCard({
title,
theme,
}: {
readonly title: string;
readonly theme: 'mid-light' | 'ableton-9';
}) {
return (
<div className='flex-1 p-4 py-8 self-stretch bg-ableton-gray-light text-black'>
<h3 className='text-black text-sm'>{title}</h3>
<small className='text-stone-800 text-xs italic'>
Theme: &quot;{theme}&quot;
</small>
<div className='flex pt-2 gap-1'>
<KnobAbletonPan theme={theme} valueDefault={-1} />
<KnobAbletonPan theme={theme} valueDefault={-0.5} />
<KnobAbletonPan theme={theme} valueDefault={0} />
<KnobAbletonPan theme={theme} valueDefault={0.5} />
<KnobAbletonPan theme={theme} valueDefault={1} />
</div>
</div>
);
}

function MoisesKnobs() {
return (
<div className='flex-1 p-4 py-8 self-stretch bg-moises-black'>
<h3 className='text-white text-sm'>Moises pan knob</h3>
<div className='flex pt-2 gap-4'>
<KnobMoisesPan valueDefault={-1} />
<KnobMoisesPan valueDefault={-0.5} />
<KnobMoisesPan valueDefault={0} />
<KnobMoisesPan valueDefault={0.5} />
<KnobMoisesPan valueDefault={1} />
</div>
</div>
);
}

export default IndexPage;
96 changes: 96 additions & 0 deletions apps/docs/src/components/KnobAbletonPan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use client';
import {useId, useState} from 'react';
import clsx from 'clsx';
import {KnobHeadless, KnobHeadlessOutput} from 'react-knob-headless';

type KnobAbletonPanProps = {
readonly theme: 'mid-light' | 'ableton-9';
readonly valueDefault?: number;
};

export function KnobAbletonPan({theme, valueDefault = 0}: KnobAbletonPanProps) {
const knobId = useId();
const [valueRaw, setValueRaw] = useState(valueDefault);
const value = valueRawRoundFn(valueRaw);

const backgroundColorClass = clsx(
theme === 'mid-light' && 'bg-ableton-white',
theme === 'ableton-9' && 'bg-ableton-9-white',
);
const backgroundColorAccentClass = clsx(
theme === 'mid-light' && 'bg-ableton-blue',
theme === 'ableton-9' && 'bg-ableton-9-orange',
);
const borderColorClass = clsx(
theme === 'mid-light' && 'border-ableton-gray',
theme === 'ableton-9' &&
'border-ableton-9-gray focus:border-ableton-9-gray-dark',
);
const focusOutlineClass = clsx(
theme === 'mid-light' && 'focus:outline-ableton-gray-dark',
theme === 'ableton-9' && 'focus:outline-ableton-9-gray-dark',
);
const textColorClass = clsx(
theme === 'mid-light' && 'text-ableton-gray-dark',
theme === 'ableton-9' && 'text-ableton-9-gray-dark',
);

return (
<div className='flex flex-col gap-2 items-center'>
<KnobHeadless
id={knobId}
aria-label='Pan'
className={clsx(
'relative w-12 h-4 border flex items-center justify-center overflow-hidden focus:outline focus:outline-1',
backgroundColorClass,
borderColorClass,
focusOutlineClass,
)}
min={min}
max={max}
valueRaw={valueRaw}
valueDefault={valueDefault}
dragSensitivity={dragSensitivity}
valueRawRoundFn={valueRawRoundFn}
valueRawDisplayFn={valueRawDisplayFn}
onValueRawChange={setValueRaw}
>
<div
className={clsx(
'absolute inset-0 w-1/2',
backgroundColorAccentClass,
value > 0 && 'left-1/2',
)}
>
<div
className={clsx('absolute inset-0', backgroundColorClass)}
style={{transform: `translateX(${value * 100}%)`}}
/>
</div>
<KnobHeadlessOutput
htmlFor={knobId}
className={clsx(
'relative pointer-events-none select-none text-xs',
textColorClass,
)}
>
{valueRawDisplayFn(valueRaw)}
</KnobHeadlessOutput>
</KnobHeadless>
</div>
);
}

const min = -1;
const max = 1;
const dragSensitivity = 0.005;
const valueRawRoundFn = (x: number): number => Math.round(x * 100) / 100;
const valueRawDisplayFn = (valueRaw: number): string => {
const pan = Math.round(valueRawRoundFn(valueRaw) * 50);
if (pan === 0) {
return 'C';
}

const direction = pan < 0 ? 'L' : 'R';
return `${Math.abs(pan)}${direction}`;
};
60 changes: 58 additions & 2 deletions apps/docs/src/components/KnobHeadlessDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,62 @@
'use client';
import {KnobHeadless} from 'react-knob-headless';
import {useId, useState} from 'react';
import {KnobHeadless, KnobHeadlessLabel} from 'react-knob-headless';

const min = 0;
const max = 100;
const valueDefault = 70;
const angleMin = -145; // The minumum knob position angle, when x = 0
const angleMax = 145; // The maximum knob position angle, when x = 1
const valueRawRoundFn = Math.round;
const valueRawDisplayFn = (valueRaw: number): string =>
`${valueRawRoundFn(valueRaw)}%`;

export function KnobHeadlessDemo() {
return <KnobHeadless valueDefault={0.5} className='h-12 w-8 bg-blue-400' />;
const labelId = useId();
const [valueRaw, setValueRaw] = useState(valueDefault);
const value01 = mapTo01Linear(valueRaw, min, max);
const angle = mapFrom01Linear(value01, angleMin, angleMax);
return (
<div className='flex flex-col gap-2 items-center'>
<KnobHeadlessLabel id={labelId} className='text-xs'>
Demo knob
</KnobHeadlessLabel>
<KnobHeadless
includeIntoTabOrder
aria-labelledby={labelId}
className='relative w-12 h-12'
min={min}
max={max}
valueRaw={valueRaw}
valueDefault={valueDefault}
valueRawRoundFn={valueRawRoundFn}
valueRawDisplayFn={valueRawDisplayFn}
onValueRawChange={setValueRaw}
>
<div className='absolute h-full w-full rounded-full bg-gray-800'>
<div
className='absolute h-full w-full'
style={{rotate: `${angle}deg`}}
>
<div className='absolute left-1/2 top-0 h-1/2 w-[2px] -translate-x-1/2 rounded-sm bg-gray-500' />
</div>
</div>
</KnobHeadless>
<button
type='button'
className='p-2 border border-gray-500 rounded-sm'
onClick={() => {
setValueRaw(10);
}}
>
set to 10%
</button>
</div>
);
}

const mapFrom01Linear = (x: number, min: number, max: number): number =>
(max - min) * x + min;

const mapTo01Linear = (x: number, min: number, max: number): number =>
(x - min) / (max - min);
68 changes: 68 additions & 0 deletions apps/docs/src/components/KnobMoisesPan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client';
import {useId, useState} from 'react';
import clsx from 'clsx';
import {KnobHeadless} from 'react-knob-headless';

type KnobMoisesPanProps = {
readonly valueDefault?: number;
};

export function KnobMoisesPan({valueDefault = 0}: KnobMoisesPanProps) {
const knobId = useId();
const [valueRaw, setValueRaw] = useState(valueDefault);
const value = valueRawRoundFn(valueRaw);
const angle = value * 180;
return (
<div className='flex flex-col'>
<KnobHeadless
id={knobId}
aria-label='Moises Pan Knob'
className='relative w-9 h-9 bg-moises-gray rounded-full flex items-center justify-center overflow-hidden outline-none'
min={min}
max={max}
valueRaw={valueRaw}
valueDefault={valueDefault}
dragSensitivity={dragSensitivity}
valueRawRoundFn={valueRawRoundFn}
valueRawDisplayFn={valueRawDisplayFn}
onValueRawChange={setValueRaw}
>
<div
className={clsx('absolute inset-0', angle < 0 && '-scale-x-100')}
style={{
background: `conic-gradient(${
'#63fb97' /* Make sure the color is "moises-green" */
} ${Math.abs(angle / 3.6)}%, transparent 0)`,
}}
/>
<div className='absolute inset-[1px] bg-moises-gray-dark rounded-full' />
<div className='absolute inset-0' style={{rotate: `${angle}deg`}}>
<div
className={clsx(
'absolute left-1/2 top-0 h-1/2 w-px -translate-x-1/2',
angle === 0 ? 'bg-moises-gray' : 'bg-moises-green',
)}
/>
</div>
</KnobHeadless>
<div className='flex justify-between items-center px-0.5 text-xs text-moises-gray'>
<span>L</span>
<span>R</span>
</div>
</div>
);
}

const min = -1;
const max = 1;
const dragSensitivity = 0.007;
const valueRawRoundFn = (x: number): number => Math.round(x * 20) / 20;
const valueRawDisplayFn = (valueRaw: number): string => {
const pan = Math.round(valueRawRoundFn(valueRaw) * 50);
if (pan === 0) {
return 'C';
}

const direction = pan < 0 ? 'L' : 'R';
return `${Math.abs(pan)}${direction}`;
};
22 changes: 21 additions & 1 deletion apps/docs/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,27 @@ import type {Config} from 'tailwindcss';

const config: Config = {
content: ['./src/**/*.{ts,tsx}'],
theme: {},
theme: {
extend: {
colors: {
'ableton-gray-dark': '#282828',
'ableton-gray': '#464646',
'ableton-gray-light': '#AAAAAA',
'ableton-white': '#dcdcdc',
'ableton-blue': '#7BDCF3',

'ableton-9-gray-dark': '#323232',
'ableton-9-gray': '#999999',
'ableton-9-white': '#BFBFBF',
'ableton-9-orange': '#D5824A',

'moises-gray-dark': '#262626',
'moises-gray': '#666666',
'moises-green': '#63fb97',
'moises-black': '#000000',
},
},
},
plugins: [],
};

Expand Down
Loading