{"_id":"@dkirkby/vintage-potentiometer","_rev":"2-77484485ba5474b6b8c4757d9715e7e2","name":"@dkirkby/vintage-potentiometer","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@dkirkby/vintage-potentiometer","version":"0.1.0","keywords":["react","potentiometer","knob","rotary-control","slider","accessible"],"author":{"url":"https://github.com/dkirkby","name":"David Kirkby","email":"dkirkby@uci.edu"},"license":"MIT","_id":"@dkirkby/vintage-potentiometer@0.1.0","maintainers":[{"name":"dkirkby","email":"dkirkby@uci.edu"}],"homepage":"https://github.com/dkirkby/vintage-potentiometer#readme","bugs":{"url":"https://github.com/dkirkby/vintage-potentiometer/issues"},"dist":{"shasum":"e10d5225cc557154f3df697714b1deb81ff17c58","tarball":"https://registry.npmjs.org/@dkirkby/vintage-potentiometer/-/vintage-potentiometer-0.1.0.tgz","fileCount":9,"integrity":"sha512-0GEC+eHBwIeNb4Met2D2dqQC28+oUxhdD9VObzyQmZV18utQgx6eFFbZzdpQw1hZaVolSeEglXj0ylZY0WPJ/A==","signatures":[{"sig":"MEUCIQCQBM8PltRnjsejtSnJB+h+6v3fOAhXgdmRX4wuQjOuegIgArNAcQNrnru6vUSz8R3/n3a+d05s0J/VfuOVJZXvnwo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":32107},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./style.css":"./dist/style.css"},"gitHead":"ab4359599ec5167d0f43b193fa515c0a8993c616","scripts":{"dev":"vite","test":"vitest","build":"vite build","check":"npm run typecheck && npm run test:run && npm run build","test:run":"vitest run","typecheck":"tsc --noEmit","prepublishOnly":"npm run check"},"_npmUser":{"name":"dkirkby","email":"dkirkby@uci.edu"},"repository":{"url":"git+https://github.com/dkirkby/vintage-potentiometer.git","type":"git"},"_npmVersion":"11.5.1","description":"An accessible, reusable 1950s-style rotary potentiometer control for React.","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"24.7.0","_hasShrinkwrap":false,"devDependencies":{"vite":"^8.1.2","jsdom":"^27.0.0","react":"^19.2.7","vitest":"^4.0.0","react-dom":"^19.2.7","typescript":"^6.0.3","@types/node":"^26.1.0","@types/react":"^19.2.0","vite-plugin-dts":"^5.0.3","@types/react-dom":"^19.2.0","@vitejs/plugin-react":"^5.0.4","@testing-library/react":"^16.3.0","@testing-library/jest-dom":"^6.6.3","vite-plugin-lib-inject-css":"^2.2.2","@testing-library/user-event":"^14.6.1"},"peerDependencies":{"react":">=18","react-dom":">=18"},"_npmOperationalInternal":{"tmp":"tmp/vintage-potentiometer_0.1.0_1783451031885_0.586688267839472","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@dkirkby/vintage-potentiometer","version":"0.2.0","description":"An accessible, reusable 1950s-style rotary potentiometer control for React.","type":"module","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./style.css":"./dist/style.css"},"main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","sideEffects":["**/*.css"],"peerDependencies":{"react":">=18","react-dom":">=18"},"devDependencies":{"@testing-library/jest-dom":"^6.6.3","@testing-library/react":"^16.3.0","@testing-library/user-event":"^14.6.1","@types/node":"^26.1.0","@types/react":"^19.2.0","@types/react-dom":"^19.2.0","@vitejs/plugin-react":"^5.0.4","jsdom":"^27.0.0","react":"^19.2.7","react-dom":"^19.2.7","typescript":"^6.0.3","vite":"^8.1.2","vite-plugin-dts":"^5.0.3","vite-plugin-lib-inject-css":"^2.2.2","vitest":"^4.0.0"},"scripts":{"dev":"vite","build":"vite build","test":"vitest","test:run":"vitest run","typecheck":"tsc --noEmit","check":"npm run typecheck && npm run test:run && npm run build","prepublishOnly":"npm run check"},"keywords":["react","potentiometer","knob","rotary-control","slider","accessible"],"author":{"name":"David Kirkby","email":"dkirkby@uci.edu","url":"https://github.com/dkirkby"},"repository":{"type":"git","url":"git+https://github.com/dkirkby/vintage-potentiometer.git"},"bugs":{"url":"https://github.com/dkirkby/vintage-potentiometer/issues"},"homepage":"https://github.com/dkirkby/vintage-potentiometer#readme","license":"MIT","gitHead":"df284e52fd8d2616d7547962a1efa61bea55bf01","_id":"@dkirkby/vintage-potentiometer@0.2.0","_nodeVersion":"22.23.1","_npmVersion":"11.18.0","dist":{"integrity":"sha512-hFdqOY6MmPMD5zH4vNTUS1pvfU4nYYi+KR5XW4lGtrYY8I800iqAR0RsSjss0q9ZnxTDxEWH+Z4QI0nalww5ug==","shasum":"05e561dbe9f92bf11200d10c259e1d6f1b6a346e","tarball":"https://registry.npmjs.org/@dkirkby/vintage-potentiometer/-/vintage-potentiometer-0.2.0.tgz","fileCount":9,"unpackedSize":48366,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@dkirkby%2fvintage-potentiometer@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD9BPjvX5eKCsY7ZUoKV62h4xlIP+otlzYNiONJXltjrgIgVrgYjNiuye7CCvKJK6ktBV/J33SDD56PvkPoQJf75VI="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:855af079-5268-47f1-93f7-24116f2a8b2b"}},"directories":{},"maintainers":[{"name":"dkirkby","email":"dkirkby@uci.edu"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/vintage-potentiometer_0.2.0_1783562325404_0.0720186779897769"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-07T19:03:51.626Z","modified":"2026-07-09T01:58:45.856Z","0.1.0":"2026-07-07T19:03:52.039Z","0.2.0":"2026-07-09T01:58:45.533Z"},"bugs":{"url":"https://github.com/dkirkby/vintage-potentiometer/issues"},"author":{"name":"David Kirkby","email":"dkirkby@uci.edu","url":"https://github.com/dkirkby"},"license":"MIT","homepage":"https://github.com/dkirkby/vintage-potentiometer#readme","keywords":["react","potentiometer","knob","rotary-control","slider","accessible"],"repository":{"type":"git","url":"git+https://github.com/dkirkby/vintage-potentiometer.git"},"description":"An accessible, reusable 1950s-style rotary potentiometer control for React.","maintainers":[{"name":"dkirkby","email":"dkirkby@uci.edu"}],"readme":"# Vintage Potentiometer\n\nAn accessible, controlled React input that behaves like a range slider but looks like a 1950s-era potentiometer knob.\n\nThe component supports mouse, pen, and touch dragging; keyboard control; custom value mappings; double-click reset; ARIA slider semantics; and CSS-variable theming.\n\n## Requirements\n\n- React 18 or later\n- React DOM 18 or later\n- For package development: Node.js 20.19+ or 22.12+\n\n## Installation\n\nConsumers install the published package with:\n\n```bash\nnpm install @dkirkby/vintage-potentiometer\n```\n\nThe stylesheet is imported automatically by the package entry point. Consumers do not need a separate CSS import.\n\n## Basic usage\n\n```tsx\nimport { useState } from \"react\";\nimport { Potentiometer } from \"@dkirkby/vintage-potentiometer\";\n\nexport function GainControl() {\n  const [gain, setGain] = useState(5);\n\n  return (\n    <Potentiometer\n      label=\"Gain\"\n      value={gain}\n      onChange={setGain}\n      min={0}\n      max={10}\n      step={0.1}\n      defaultValue={5}\n      formatValue={value => value.toFixed(1)}\n    />\n  );\n}\n```\n\nThe component is controlled: the parent owns `value`, and `onChange` reports the requested new value.\n\n## Logarithmic values\n\n```tsx\nimport {\n  Potentiometer,\n  type PositionToValue,\n  type ValueToPosition\n} from \"@dkirkby/vintage-potentiometer\";\n\nconst logValueToPosition: ValueToPosition = (value, min, max) =>\n  Math.log(value / min) / Math.log(max / min);\n\nconst logPositionToValue: PositionToValue = (position, min, max) =>\n  min * Math.pow(max / min, position);\n\n<Potentiometer\n  label=\"Frequency\"\n  value={frequency}\n  onChange={setFrequency}\n  min={100}\n  max={5000}\n  step={10}\n  valueToPosition={logValueToPosition}\n  positionToValue={logPositionToValue}\n  formatValue={value => `${Math.round(value)} Hz`}\n/>;\n```\n\nCustom mapping functions map values to normalized positions in `[0, 1]`, and positions back to values in `[min, max]`.\n\n## Interaction\n\n| Action | Result |\n|---|---|\n| Drag upward/downward | Increase/decrease |\n| Shift + drag | Fine adjustment |\n| Arrow keys | Change by `step` |\n| Shift + arrow | Change by `step / 10` |\n| Page Up/Down (Mac: Fn + Up/Down) | Change by about 10% of the range |\n| Home/End (Mac: Fn + Left/Right) | Select minimum/maximum |\n| Double-click | Restore `defaultValue` |\n\n## Important props\n\n```ts\ninterface PotentiometerProps {\n  value: number;\n  onChange: (value: number) => void;\n  label: string;\n  min?: number;\n  max?: number;\n  step?: number;\n  defaultValue?: number;\n  size?: number;\n  disabled?: boolean;\n  sweepDegrees?: number;\n  dragDistance?: number;\n  tickCount?: number;\n  className?: string;\n  style?: CSSProperties;\n  showLabel?: boolean;\n  showValue?: boolean;\n  scalloped?: boolean;\n  scallopCount?: number;\n  scallopFlat?: number;\n  scallopRadius?: number;\n  formatValue?: (value: number) => string;\n  valueToPosition?: ValueToPosition;\n  positionToValue?: PositionToValue;\n  onChangeStart?: () => void;\n  onChangeEnd?: (value: number) => void;\n}\n```\n\n`showLabel` and `showValue` (both default `true`) toggle the visible label row and value readout independently; the surrounding layout collapses to fit whatever is still shown, rather than leaving empty space. `label` is still required even when `showLabel` is `false` — it's used as the slider's `aria-label` so the control stays accessible without a visible label.\n\n`scalloped` (default `false`) gives the knob a fluted edge, made of `scallopCount` (default `10`) concave notches cut into the plain circle, like a vintage Bakelite knob, colored to match `--pot-knob-edge` so it follows theming automatically. Each notch spans `1 - scallopFlat` of its `360 / scallopCount` wedge (the rest stays a flat circular arc); `scallopFlat` defaults to `0.25`. `scallopRadius` (default `0.5`) sets each notch's radius of curvature as a multiple of the knob's plain radius — smaller values cut deeper, sharper notches; it's floored automatically at whatever minimum is geometrically possible for the current `scallopCount`/`scallopFlat` so the shape never becomes invalid.\n\n## Theming\n\nOnly `--pot-panel`, `--pot-knob-face`, `--pot-text`, `--pot-indicator`, and `--pot-focus-ring` need to be set — the panel/knob highlight and edge tones are derived from `--pot-panel` and `--pot-knob-face` automatically (via `color-mix()`), so a single base color still renders a coherent lit/shaded surface:\n\n```tsx\n<Potentiometer className=\"red-knob\" label=\"Tone\" value={tone} onChange={setTone} />\n```\n\n```css\n.red-knob {\n  --pot-panel: #542b25;\n  --pot-knob-face: #74463d;\n  --pot-indicator: #fff0c2;\n}\n```\n\nSet `--pot-panel-highlight`, `--pot-panel-edge`, `--pot-knob-highlight`, or `--pot-knob-edge` directly to override any of the derived tones with an exact color instead.\n\n`--pot-light-angle` (default `-35deg`, 0deg is straight above, increasing clockwise) controls the simulated light direction — it repositions the highlight on the panel and knob gradients and the direction their inset/drop shadows fall:\n\n```css\n.red-knob {\n  --pot-light-angle: 120deg; /* light from the lower right */\n}\n```\n\n`--pot-label-font-family` and `--pot-value-font-family` set the label and value readout typefaces. `--pot-label-font-size`, `--pot-value-font-size`, and `--pot-gap` (the spacing between the label, knob, and value) are also optional — left unset, they scale proportionally with `size` instead of a fixed default, so a larger or smaller knob keeps its text and spacing in proportion:\n\n```css\n.red-knob {\n  --pot-label-font-family: Georgia, serif;\n  --pot-value-font-size: 1rem; /* fixed instead of scaling with size */\n}\n```\n\n`--pot-indicator-width` and `--pot-knob-border-width` are optional too, following the same pattern, but clamped with a floor (and a ceiling) rather than scaling all the way down to nothing at small sizes or up indefinitely at large ones.\n\nFor colors computed at runtime (e.g. a live theme picker), set the same custom properties via `style` instead — the values must land on the component's own root element, so an ancestor element's `style` will not work:\n\n```tsx\n<Potentiometer\n  label=\"Tone\"\n  value={tone}\n  onChange={setTone}\n  style={{ \"--pot-panel\": panelColor, \"--pot-knob-face\": knobColor } as CSSProperties}\n/>\n```\n\nDeriving the highlight/edge tones and light angle relies on `color-mix()` and the CSS trig functions (`sin()`/`cos()`), both supported in current versions of Chrome, Firefox, Safari, and Edge (roughly 2023 or later).\n\n## Using it in an Observable notebook\n\nUse **[Observable Framework](https://observablehq.com/framework/)** (the static-site builder), not a classic notebook (observablehq.com). Framework does a real build (Vite) with actual npm dependency resolution, so `react`/`react-dom` get deduped the normal way, like any bundler-based app. Classic notebooks have no real dependency graph — importing a compiled React component library through ad-hoc CDN imports there is prone to loading two separate copies of React and hitting \"invalid hook call\"-style failures, and isn't a reliable path for this package.\n\nFramework supports JSX fenced code blocks with React/ReactDOM available by default:\n\n````md\n```jsx\nimport {Potentiometer} from \"npm:@dkirkby/vintage-potentiometer\";\n\nfunction Gain() {\n  const [gain, setGain] = React.useState(5);\n  return <Potentiometer label=\"Gain\" value={gain} onChange={setGain} min={0} max={10} step={0.1} />;\n}\ndisplay(<Gain />);\n```\n````\n\nIf the stylesheet doesn't load automatically, add it explicitly as plain HTML in the page:\n\n```html\n<link rel=\"stylesheet\" href=\"npm:@dkirkby/vintage-potentiometer/dist/style.css\">\n```\n\n## Development\n\n```bash\nnpm install\nnpm run dev\n```\n\nOther commands:\n\n```bash\nnpm run test:run\nnpm run typecheck\nnpm run build\nnpm run check\n```\n\nThe build output is written to `dist/`. Since `src/index.ts` imports `Potentiometer.css`, Vite emits `dist/style.css` and consuming bundlers load it automatically through the JavaScript entry point.\n\n## Testing the packed package\n\n```bash\nnpm pack --dry-run\nnpm pack\n```\n\nInstall the generated tarball in a separate React app before publishing.\n\n## Publishing\n\nReleases are automated: pushing a `vX.Y.Z` tag triggers `.github/workflows/publish.yml`, which runs `npm run check` (via `prepublishOnly`) and publishes via npm's trusted publishing (OIDC) — no token, no manual `npm login`.\n\n1. Bump the version and tag it:\n\n```bash\nnpm version patch   # or minor / major, or an explicit x.y.z\ngit push --follow-tags\n```\n\n2. Watch the **Actions** tab on GitHub for the `publish.yml` run — it verifies the pushed tag matches `package.json`'s version before publishing, so a mismatch fails loudly instead of publishing the wrong thing.\n\nThis relies on a trusted publisher already configured on npmjs.com for this package (Settings → Trusted Publisher: organization/user `dkirkby`, repository `vintage-potentiometer`, workflow filename `publish.yml`), which itself requires the package to already exist on the registry — npm can't configure trusted publishing before a package's first release. If CI is unavailable, or you're bootstrapping a brand new package for the first time, fall back to publishing manually:\n\n```bash\nnpm run check\nnpm pack --dry-run\nnpm login\nnpm publish --access public --otp=<code from your authenticator app>\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md"}