Files
planet/docs/technical/en/tactile-ui-components.md
rayd1o 5bf5c73ca0
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled
release: bump version to 0.66.0
2026-05-26 03:41:47 +08:00

9.4 KiB

Tactile UI Components

Tactile UI is Planet's portable React control layer. It was extracted from the Admin button, switch, scrollbar, and tooltip work, but the components themselves do not depend on Admin, AntD, Radix, Tailwind, or an-* classes. The immediate goal is stable in-repo usage; the structure is intentionally close to something that can later be published as an npm package.

Design Goals

  • Light tactile feel: controls default to a white or themed surface, thin borders, and external shadow, matching the subtle depth of the Docs theme slider rather than large colored blocks or glow.
  • Portable styling: classes use the tui-* prefix and styles live in frontend/src/components/tactile-ui/styles.css.
  • Low dependency surface: components assume React and React DOM. Preset icons currently use lucide-react, and callers may also pass custom React nodes.
  • Theme friendly: CSS variables expose the default styling surface. Planet pages adapt the library through theme variables and props.
  • Clear semantics: unambiguous actions should prefer icon-only buttons with tooltips; strong-intent actions such as save, confirm, create, and run can keep text.

Import

In this repository:

import { TactileButton, TactileSwitch, ControlGroup } from '@/components/tactile-ui'
import '@/components/tactile-ui/styles.css'

After a future package extraction:

import { TactileButton, TactileSwitch } from '@planet/tactile-ui'
import '@planet/tactile-ui/styles.css'

Theme Tokens

Core tokens are exposed as --tui-* CSS variables. Product themes should override variables instead of rewriting internal component classes.

:root {
  --tui-surface: #ffffff;
  --tui-surface-raised-hover: #f8fbff;
  --tui-border-soft: #d6dfeb;
  --tui-border-hover: #b8c6d9;
  --tui-text: #0f172a;
  --tui-primary: #2563eb;
  --tui-danger: #dc2626;
}

[data-theme='dark'] {
  --tui-surface: #111827;
  --tui-text: #e5edf8;
}

Most controls also accept a tactile prop for local width, height, radius, background, border, and shadow overrides. Use local overrides for small special cases; use CSS variables for product-wide styling.

Portals and Dark Theme

TactileTooltip and the Admin Dialog, Select, and toast controls that use Tactile UI may render through a portal attached to document.body. Those nodes are not descendants of .admin-theme-root[data-theme='dark'], so dark tokens cannot rely only on an ancestor selector inside the Admin root.

The Admin theme provider mirrors the active theme to body[data-admin-theme]. Shared styles need to support both selector paths:

[data-theme='dark'] .tui-button,
body[data-admin-theme='dark'] .tui-button {
  --tui-surface: #172033;
  --tui-text: #e5edf8;
}

When adding a portal-based control, first check whether it renders into body. If it does, add a body[data-admin-theme='dark'] branch in that component's style entry, or reuse the already covered --tui-* / --an-* tokens. Avoid hard-coding a one-off dark modal style, because the same contrast problem can reappear in dropdowns, tooltips, toasts, and confirmation dialogs.

TactileButton

The button component covers regular buttons, icon buttons, strong-intent buttons, and link-like buttons.

Common props:

Prop Description
variant neutral, primary, danger, subtle, or ghost
size sm, md, lg, or icon
shape square or pill
icon Preset icon name or a custom React node
iconOnly Fixed-size icon button; provide tooltip or aria-label
tooltip Rendered through a portal and offset away from the cursor
loading Disables the button and exposes aria-disabled
tactile Overrides size, radius, shadow, background, and dark-mode variables
<TactileButton icon="refresh" iconOnly tooltip="Refresh" />
<TactileButton variant="primary" icon="save">Save</TactileButton>
<TactileButton variant="danger" icon="delete" iconOnly tooltip="Delete" />

variant="neutral" defaults to a white tactile button. Colored buttons should still keep the same height and external shadow instead of relying on page-specific CSS overrides.

Colored button borders must not use the exact fill color. primary, danger, and future colored variants should use a lighter border from the same hue, such as color-mix(in srgb, var(--tui-danger) 64%, white). The border still reads as part of the button color, but its visual weight is lower than the fill surface, so red or blue buttons do not look one outline larger than neutral buttons. Hover states should brighten rather than darken: mix a little white into the current fill color, and keep the hover border lighter than the hover fill.

Icon Presets

Preset icons are maintained in tactileIconPresets. Feature pages should call icons by semantic name so actions remain consistent across the console.

Common semantics:

Name Use
refresh Refresh data
save Save
delete Delete
trigger / collect Trigger collection
start / play Start
stop Stop
connect / test Connectivity check
guide Credential guide
generate AI generation
reset Restore defaults
detail View details
copy Copy
upload Upload

Custom icons are also supported:

<TactileButton icon={<MyIcon aria-hidden="true" />} iconOnly tooltip="Custom action" />

TactileSwitch

TactileSwitch is for binary settings. It is not a large iOS-style switch; it matches the compact tactile slider used by Docs.

<TactileSwitch
  checked={enabled}
  onCheckedChange={setEnabled}
  label="Enable WebSearch"
  tooltip="Enable WebSearch"
/>

The label defaults to tooltip content. If visible text is needed, render it in the surrounding layout rather than inside the switch.

ControlGroup

ControlGroup arranges action buttons with consistent spacing and alignment. It has no grey base by default; use withBase or baseTactile only when a base surface is intentional.

<ControlGroup align="end" gap={8}>
  <TactileButton icon="refresh" iconOnly tooltip="Refresh" />
  <TactileButton variant="primary" icon="save">Save</TactileButton>
</ControlGroup>

In list footers, the group should appear at the end of the scroll content, not float in the middle of the list. In detail toolbars, size and shadow should come from the shared component defaults.

TactileTooltip

Tooltips render to document.body via createPortal, support top, right, bottom, and left, and perform simple collision correction. The default position is offset down and to the right from the trigger so it does not sit under the cursor.

<TactileTooltip label="Refresh" anchor={buttonRef.current} open={open} side="bottom" />

Application code usually does not need to use it directly because TactileButton and TactileSwitch include tooltip handling.

Scrollbar

Scrollbar wraps ordinary scrollable content. Native scrolling remains on the viewport; Tactile UI only renders the overlay track and thumb.

<Scrollbar axis="both" minThumbSize={28} autoHide>
  <LongContent />
</Scrollbar>

Common props:

Prop Description
axis x, y, or both
autoHide Fade/soften when not hovered or dragged
alwaysVisible Keep scrollbars visible
trackSize / thumbSize Track and thumb dimensions
inset / radius Edge inset and radius
thumbColor / trackColor Local color overrides
viewportRef External real scroll node

Do not force overlay scrollbars onto textareas. Text selection and the resize grip need native behavior. If a consistent textarea look is needed, use a dedicated resizable textarea style that preserves the native grip hit area.

ScrollbarOverlay

ScrollbarOverlay is for areas that already own a scroll node, such as a third-party table or custom viewport. It does not create a new scroll container; it observes the target and paints overlay scrollbars.

<div ref={hostRef}>
  <ThirdPartyTable />
  <ScrollbarOverlay containerRef={hostRef} targetSelector=".tui-scroll-target" />
</div>

TableScrollRegion

TableScrollRegion is a convenience wrapper for table scroll areas. The default target selector is .tui-scroll-target; Admin passes its table viewport selector explicitly so package code does not contain product-specific names.

<TableScrollRegion targetSelector=".table-viewport">
  <EntityTable />
</TableScrollRegion>

Accessibility

  • iconOnly buttons must provide tooltip or aria-label.
  • TactileSwitch uses role="switch" and aria-checked.
  • Tooltip text is explanatory only; state must still be represented by text, badges, or aria-* attributes.
  • Disabled and loading states set aria-disabled; real button elements also receive disabled.

Admin Migration Rules

Admin should use Tactile UI for global tool buttons, detail-panel toolbars, and list footer actions:

  • Unambiguous actions: TactileButton iconOnly tooltip
  • Save/create/confirm: TactileButton variant="primary", usually with text
  • Delete/stop: TactileButton variant="danger", with text only when risk or ambiguity requires it
  • Switches: TactileSwitch
  • Long lists, tables, logs, and Markdown: Scrollbar / ScrollbarOverlay / TableScrollRegion

The boundary is deliberate: Tactile UI owns touch feel, size, shadow, tooltip, and scrollbar behavior; feature pages own APIs, state machines, permissions, and copy.