Tail Icon
Action icon affordance attached to sidebar rows (sm/lg, default/hover/disabled).
Installation
$
npx @309-thingspire/ui@latest add tail-iconUsage
import { TailIcon } from "@/components/tail-icon/tail-icon"<TailIcon />Examples
Live preview rendered from TailIcon.preview.tsx. Switch to the Code tab to view the underlying component source.
Loading preview…
import React, { useState } from 'react';
import { colors, radius, spacing } from '../../style-tokens';
import { IconAddLine } from '../icons';
import type { TailIconProps, TailIconSize, TailIconState } from './TailIcon.types';
const palette = colors.primitive.palette;
const SIZE_TO_PADDING: Record<TailIconSize, number> = {
sm: spacing.scale['2'],
lg: spacing.scale['4'],
};
const SIZE_TO_PX: Record<TailIconSize, number> = {
sm: spacing.scale['16'],
lg: spacing.scale['20'],
};
function resolveState(
forceState: TailIconState | undefined,
hovered: boolean,
disabled: boolean,
): TailIconState {
if (disabled || forceState === 'disabled') {
return 'disabled';
}
if (forceState) {
return forceState;
}
return hovered ? 'hover' : 'default';
}
function getBackgroundColor(state: TailIconState): string {
if (state === 'hover') {
return palette.gray['1a'];
}
return palette.base.transparent;
}
function getIconColor(state: TailIconState): string {
if (state === 'disabled') {
return palette.gray['5a'];
}
return palette.gray['9a'];
}
export function TailIcon({
size = 'lg',
state,
forceState,
icon,
disabled = false,
interactive,
className,
style,
onClick,
onMouseEnter,
onMouseLeave,
}: TailIconProps) {
const [hovered, setHovered] = useState(false);
const resolvedState = resolveState(forceState ?? state, hovered, disabled);
const padding = SIZE_TO_PADDING[size];
const iconPx = SIZE_TO_PX[size];
const backgroundColor = getBackgroundColor(resolvedState);
const iconColor = getIconColor(resolvedState);
const isInteractive = interactive ?? Boolean(onClick);
const iconNode = icon ?? <IconAddLine aria-hidden style={{ width: iconPx, height: iconPx, display: 'block' }} />;
const sharedStyles: React.CSSProperties = {
display: 'inline-flex',
alignItems: 'flex-start',
justifyContent: 'center',
padding,
borderRadius: radius.scale.xs,
backgroundColor,
color: iconColor,
boxSizing: 'border-box',
...style,
};
if (isInteractive) {
return (
<button
type="button"
className={className}
disabled={resolvedState === 'disabled'}
onMouseEnter={(event) => {
if (!disabled) {
setHovered(true);
}
onMouseEnter?.(event);
}}
onMouseLeave={(event) => {
if (!disabled) {
setHovered(false);
}
onMouseLeave?.(event);
}}
onClick={(event) => {
if (resolvedState === 'disabled') {
event.preventDefault();
return;
}
onClick?.(event);
}}
style={{
...sharedStyles,
borderStyle: 'none',
cursor: resolvedState === 'disabled' ? 'not-allowed' : 'pointer',
appearance: 'none',
outline: 'none',
}}
>
{iconNode}
</button>
);
}
return (
<span
className={className}
onMouseEnter={(event) => {
if (!disabled) {
setHovered(true);
}
onMouseEnter?.(event);
}}
onMouseLeave={(event) => {
if (!disabled) {
setHovered(false);
}
onMouseLeave?.(event);
}}
style={sharedStyles}
>
{iconNode}
</span>
);
}
export default TailIcon;
API Reference
Props 문서 준비 중입니다. (component-spec.json 추가 시 표시됩니다.)