Controls

Controls

Overview

hudsonkit/controls provides two categories of components for building inspector and settings panels: a suite of typed parameter controls (ParamSlider, ParamToggle, ParamColor, ParamEnum, ParamText, ParamRepeatable, ParamGrid) and text/code surfaces (CodeViewer, CodeEditor, TextDocumentSurface, TextDiffSurface). All components are styled to match the active Hudson theme and template via the shared token surface.

hudsonkit/controls is a client entry. In React Server Component apps, import these controls from a Client Component boundary.

Optional editor peers

The heavier editor/markdown/diff runtimes are BYO optional peers and are loaded dynamically only by the components that need them:

  • CodeEditor: @codemirror/*, @lezer/highlight
  • TextDocumentSurface markdown preview: react-markdown, remark-gfm
  • TextDiffSurface: @pierre/diffs

Install those packages in the consuming app when you use the corresponding control. Param controls and CodeViewer do not need them.

Param controls

ParamSection

Collapsible labeled group. Wrap related controls to let users expand or collapse a category.

nametyperequireddescription
labelstringyesSection heading text (rendered uppercase)
defaultExpandedbooleannoInitial expanded state
childrenReactNodeyesControl components to render inside
'use client';

import { useState } from 'react';
import { ParamSection, ParamSlider } from 'hudsonkit/controls';

export function TypographyInspector() {
  const [size, setSize] = useState(16);

  return (
    <ParamSection label="Typography" defaultExpanded>
      <ParamSlider label="Font size" value={size} min={8} max={72} step={1} onChange={setSize} />
    </ParamSection>
  );
}

ParamSlider

Numeric range input with a live value readout.

nametyperequireddescription
labelstringyesField label
valuenumberyesCurrent value
minnumberyesMinimum value
maxnumberyesMaximum value
stepnumberyesStep increment
onChange(v: number) => voidyesCalled with the new value
format(v: number) => stringnoCustom display formatter
import { useState } from 'react';
import { ParamSlider } from 'hudsonkit/controls';

export function OpacityControl() {
  const [opacity, setOpacity] = useState(1);

  return (
    <ParamSlider
      label="Opacity"
      value={opacity}
      min={0}
      max={1}
      step={0.01}
      onChange={setOpacity}
      format={v => `${Math.round(v * 100)}%`}
    />
  );
}

ParamToggle

Boolean switch with label on the left and a pill toggle on the right.

nametyperequireddescription
labelstringyesField label
valuebooleanyesCurrent state
onChange(v: boolean) => voidyesCalled with the new state
import { useState } from 'react';
import { ParamToggle } from 'hudsonkit/controls';

export function VisibilityToggle() {
  const [visible, setVisible] = useState(true);
  return <ParamToggle label="Visible" value={visible} onChange={setVisible} />;
}

ParamColor

Hex color picker with a swatch and a hex readout. When value starts with rgba, the picker is suppressed and the raw string is displayed.

nametyperequireddescription
labelstringyesField label
valuestringyesHex or rgba color string
onChange(v: string) => voidyesCalled with the new color string
import { useState } from 'react';
import { ParamColor } from 'hudsonkit/controls';

export function FillControl() {
  const [color, setColor] = useState('#6366f1');
  return <ParamColor label="Fill" value={color} onChange={setColor} />;
}

ParamEnum

<select> dropdown for a fixed set of string options.

nametyperequireddescription
labelstringyesField label
valuestringyesCurrently selected option
optionsstring[]yesAvailable options
onChange(v: string) => voidyesCalled with the selected value
import { useState } from 'react';
import { ParamEnum } from 'hudsonkit/controls';

export function BlendModeControl() {
  const [mode, setMode] = useState('normal');
  return (
    <ParamEnum
      label="Blend mode"
      value={mode}
      options={['normal', 'multiply', 'screen', 'overlay']}
      onChange={setMode}
    />
  );
}

ParamText

Single-line text input.

nametyperequireddescription
labelstringyesField label
valuestringyesCurrent text value
placeholderstringnoInput placeholder
onChange(v: string) => voidyesCalled on every keystroke
import { useState } from 'react';
import { ParamText } from 'hudsonkit/controls';

export function LabelControl() {
  const [label, setLabel] = useState('');
  return <ParamText label="Layer name" value={label} placeholder="Untitled" onChange={setLabel} />;
}

ParamRepeatable

Variable-length list of structured field rows. Each row is rendered from a ParamRepeatableField schema. Users can add rows with + Add and remove individual rows.

ParamRepeatableField

Schema for a single field within each repeatable row.

nametyperequireddescription
keystringyesProperty key on the row object
labelstringyesField label
type'number' | 'color' | 'toggle' | 'enum' | 'text'yesControl type to render
defaultnumber | string | booleanyesInitial value for new rows
minnumbernoFor number type
maxnumbernoFor number type
stepnumbernoFor number type
optionsstring[]noFor enum type
placeholderstringnoFor text type

ParamRepeatableProps

nametyperequireddescription
labelstringyesSection label above the list
valueRecord<string, unknown>[]yesCurrent array of row objects
itemFieldsParamRepeatableField[]noField schema for each row
itemTemplateRecord<string, unknown>noObject merged into new rows on add
onChange(v: Record<string, unknown>[]) => voidyesCalled with the updated array
import { useState } from 'react';
import { ParamRepeatable } from 'hudsonkit/controls';
import type { ParamRepeatableField } from 'hudsonkit/controls';

const fields: ParamRepeatableField[] = [
  { key: 'label', label: 'Label', type: 'text', default: '', placeholder: 'Step name' },
  { key: 'color', label: 'Color', type: 'color', default: '#6366f1' },
  { key: 'weight', label: 'Weight', type: 'number', default: 1, min: 0, max: 10, step: 0.5 },
];

const template = { label: '', color: '#6366f1', weight: 1 };

export function StepsControl() {
  const [steps, setSteps] = useState<Record<string, unknown>[]>([]);

  return (
    <ParamRepeatable
      label="Steps"
      value={steps}
      itemFields={fields}
      itemTemplate={template}
      onChange={setSteps}
    />
  );
}

ParamGrid

Schema-driven panel that auto-renders a flat array of ParamDefinition objects, grouping them into ParamSection wrappers when a group field is set.

ParamDefinition

nametyperequireddescription
keystringyesValue lookup key
labelstringyesDisplayed label
type'number' | 'color' | 'toggle' | 'enum' | 'text' | 'repeatable'yesControl type
defaultnumber | string | boolean | Record<string, unknown>[]yesFallback when key is absent from values
minnumbernoFor number
maxnumbernoFor number
stepnumbernoFor number
optionsstring[]noFor enum
placeholderstringnoFor text
groupstringnoGroups this param under a named ParamSection
itemTemplateRecord<string, unknown>noFor repeatable
itemFieldsParamRepeatableField[]noFor repeatable

ParamGridProps

nametyperequireddescription
paramsParamDefinition[]yesOrdered schema array
valuesRecord<string, unknown>yesCurrent values keyed by ParamDefinition.key
onChange(key: string, value: unknown) => voidyesCalled with the changed key and new value
flatUngroupedbooleannoWhen true, params with no group render without a section wrapper
defaultExpandedbooleannoInitial expanded state for generated sections
import { useState } from 'react';
import { ParamGrid } from 'hudsonkit/controls';
import type { ParamDefinition } from 'hudsonkit/controls';

const params: ParamDefinition[] = [
  { key: 'opacity',   label: 'Opacity',    type: 'number', default: 1,         min: 0, max: 1, step: 0.01, group: 'Appearance' },
  { key: 'fill',      label: 'Fill',       type: 'color',  default: '#6366f1',                             group: 'Appearance' },
  { key: 'visible',   label: 'Visible',    type: 'toggle', default: true,                                  group: 'Appearance' },
  { key: 'blendMode', label: 'Blend mode', type: 'enum',   default: 'normal',  options: ['normal', 'multiply', 'screen'], group: 'Appearance' },
  { key: 'label',     label: 'Label',      type: 'text',   default: '',        placeholder: 'Untitled' },
];

export function LayerInspector() {
  const [values, setValues] = useState<Record<string, unknown>>({});

  return (
    <ParamGrid
      params={params}
      values={values}
      onChange={(key, value) => setValues(prev => ({ ...prev, [key]: value }))}
    />
  );
}

CodeViewer

Read-only syntax-highlighted code block. Supports line numbers, line highlighting, a filename header, and a copy button. Built-in tokenizer covers TypeScript, JavaScript, JSON, CSS, HTML, and shell.

CodeLanguage

type CodeLanguage = 'typescript' | 'javascript' | 'json' | 'css' | 'html' | 'shell' | 'plain';

CodeViewerProps

nametyperequireddescription
codestringyesSource text to display
languageCodeLanguagenoTokenizer to apply
filenamestringnoShown in the header bar
startLinenumbernoLine number offset for the gutter
highlightLinesnumber[]noLine numbers to highlight
maxHeightstring | numbernoCSS max-height for the scroll container
showCopybooleannoRender the copy button
showLineNumbersbooleannoRender the line number gutter
classNamestringnoExtra classes on the root element
import { CodeViewer } from 'hudsonkit/controls';

const snippet = `const greet = (name: string) => \`Hello, \${name}!\`;`;

export function SnippetPanel() {
  return (
    <CodeViewer
      code={snippet}
      language="typescript"
      filename="greet.ts"
      highlightLines={[1]}
      showCopy
    />
  );
}

CodeEditor

Editable CodeMirror-backed code input.

  • Tab uses the CodeMirror indent binding.
  • Cmd+S / Ctrl+S calls onSave and clears the dirty indicator.

CodeEditorProps

nametyperequireddescription
codestringyesInitial source text
languageCodeLanguagenoTokenizer to apply
filenamestringnoShown in the header bar; also enables the dirty indicator
onSave(content: string) => voidnoCalled on Cmd+S / Ctrl+S
onChange(content: string) => voidnoCalled on every keystroke
showLineNumbersbooleannoRender the line number gutter
readOnlybooleannoDisable editing while keeping the editor surface
classNamestringnoExtra classes on the root element
import { useState } from 'react';
import { CodeEditor } from 'hudsonkit/controls';

export function ScriptEditor() {
  const [saved, setSaved] = useState('const x = 1;');

  return (
    <CodeEditor
      code={saved}
      language="typescript"
      filename="script.ts"
      onSave={setSaved}
      className="h-64"
    />
  );
}
For AI agents