diff --git a/apps/docs/content/docs/en/input.mdx b/apps/docs/content/docs/en/input.mdx
index 157bbb5..59d4e04 100644
--- a/apps/docs/content/docs/en/input.mdx
+++ b/apps/docs/content/docs/en/input.mdx
@@ -1,16 +1,37 @@
export const metadata = {
title: 'Input',
- description: 'Text field primitive for compact forms and control panels.',
+ description:
+ 'Production-ready text field blending Splatoon street graffiti and in-game lobby terminal aesthetics.',
category: 'forms',
example: 'input',
}
## Usage
-Use Input with Label for named text fields. Keep validation and helper text near the field so repeated form workflows stay scannable.
+`Input` is built for web forms and game UI interactions, faithfully capturing the authentic physical card styling of the _Splatoon 3_ Lobby Terminal. It includes prefix/suffix icon slots, one-click clear, password reveal, and invalid shake feedback.
-## Design Notes
+```tsx
+import { Input } from 'splatoon-ui/input'
+import { Search } from 'lucide-react'
+; }
+ clearable
+ placeholder="Enter pool keyword..."
+/>
+```
-- Always pair inputs with labels.
-- Reserve placeholder text for examples, not field names.
-- Keep field width tied to the expected input length.
+## Design Style (Official Reference)
+
+- **`field-cut` (Default)**: 4-corner asymmetric organic cut (`7px 11px 8px 12px`), faithfully matching the _Splatoon 3_ Lobby Terminal Pool Keyword input screen. Includes a 2.5px border, tactile controller focus displacement (`translate(-1px, -1px)`), and a solid black shadow.
+
+> Note: The 4-digit Private Battle passcode uses segmented physical slots and is tracked separately as a dedicated `PinInput` component.
+
+## Features
+
+- **Ink Focus Rings (`focusColor`)**: Supports `yellow`, `blue`, `green`, `purple`, `orange`, and `red` solid ink halos.
+- **One-Click Clear (`clearable`)**: Blob clear button appears when text is entered, supporting both controlled and uncontrolled usage.
+- **Password Toggle (`showPasswordToggle`)**: Automatically enables an eye toggle button for `type="password"`.
+- **Validation Feedback (`invalid` & `errorMessage`)**: Triggers an authentic mechanical `splat-shake` tremor and an alert red error indicator.
diff --git a/apps/docs/content/docs/ja/input.mdx b/apps/docs/content/docs/ja/input.mdx
index de9e624..6c31936 100644
--- a/apps/docs/content/docs/ja/input.mdx
+++ b/apps/docs/content/docs/ja/input.mdx
@@ -1,16 +1,37 @@
export const metadata = {
title: 'Input',
- description: 'コンパクトなフォームと操作パネルのためのテキストフィールド primitive です。',
+ description:
+ 'スプラトゥーンのストリートグラフィティとゲーム内ロビーターミナルを融合させたプロダクション対応テキスト入力フィールド。',
category: 'forms',
example: 'input',
}
## 使い方
-Input は Label と一緒に使います。検証や補助文はフィールドの近くに置き、反復するフォーム作業を読み取りやすくします。
+`Input` は Web フォームやゲーム UI 向けに設計され、『スプラトゥーン 3』のロビーターミナル(Lobby Terminal)の物理カードスタイルを忠実に再現しています。プレフィックス/サフィックスアイコン、ワンクリッククリア、パスワード表示切り替え、バリデーションエラー時のシェイク演出を備えています。
-## デザインメモ
+```tsx
+import { Input } from 'splatoon-ui/input'
+import { Search } from 'lucide-react'
+; }
+ clearable
+ placeholder="合流キーワードを入力..."
+/>
+```
-- 入力には必ずラベルを付けます。
-- placeholder は例に使い、フィールド名には使いません。
-- 幅は想定する入力長に合わせます。
+## デザインスタイル(公式実機考証)
+
+- **`field-cut`(デフォルト)**:4 角の非対称カッティング(`7px 11px 8px 12px`)を採用し、『スプラトゥーン 3』ロビーターミナルの合流キーワード(Pool Keyword)入力画面を忠実に再現。2.5px ボーダー、コントローラー選択時の立体押し込み動效(`translate(-1px, -1px)`)、ソリッドシャドウを搭載。
+
+> 注意:プライベートマッチ(Private Battle)の 4 桁パスコード入力は独立したスロット構造のため、別コンポーネント `PinInput` として計画されています。
+
+## 主な機能
+
+- **インクフォーカスリング (`focusColor`)**:`yellow`、`blue`、`green`、`purple`、`orange`、`red` のソリッドインク光輪に対応。
+- **ワンクリッククリア (`clearable`)**:文字入力時にインク滴型の削除ボタンが出現し、制御・非制御フォームに対応。
+- **パスワード切り替え (`showPasswordToggle`)**:`type="password"` 時に表示切り替えボタンを自動配置。
+- **エラーフィードバック (`invalid` & `errorMessage`)**:エラー時にメカニカルな `splat-shake` 振動動效とアラート表示を実行。
diff --git a/apps/docs/content/docs/zh/input.mdx b/apps/docs/content/docs/zh/input.mdx
index ba09688..f46822c 100644
--- a/apps/docs/content/docs/zh/input.mdx
+++ b/apps/docs/content/docs/zh/input.mdx
@@ -1,16 +1,36 @@
export const metadata = {
title: 'Input',
- description: '用于紧凑表单和控制面板的文本字段 primitive。',
+ description: '融合 Splatoon 街头涂鸦与游戏内终端风格的生产级文本输入框。',
category: 'forms',
example: 'input',
}
## 用法
-Input 应与 Label 搭配使用。校验和辅助说明应靠近字段,方便重复表单流程快速扫描。
+`Input` 针对前端表单与游戏交互场景进行了全面升级,严谨复刻《Splatoon 3》大厅终端(Lobby Terminal)的实体卡片风格,内置前后缀插槽、一键清空、密码显隐切换及错误状态提示。
-## 设计备注
+```tsx
+import { Input } from 'splatoon-ui/input'
+import { Search } from 'lucide-react'
+; }
+ clearable
+ placeholder="输入组队关键词..."
+/>
+```
-- 输入框必须搭配标签。
-- placeholder 用于示例,不用于字段名。
-- 字段宽度应匹配预期输入长度。
+## 设计风格(官方实机考据)
+
+- **`field-cut`(默认形态)**:四角非对称有机切割(`7px 11px 8px 12px`),严格还原《Splatoon 3》大厅终端关键词输入(Pool Keyword)界面。包含 2.5px 墨黑边框、手柄聚焦弹跳动效(`translate(-1px, -1px)`)及实体黑色落影。
+
+> 提示:私人对战(Private Match)的 4 位房间密码锁属于分体式插槽形态,已规划为独立的 `PinInput` 组件实现。
+
+## 核心特性
+
+- **聚焦高亮态 (`focusColor`)**:支持 `yellow`、`blue`、`green`、`purple`、`orange`、`red` 实体墨水光晕。
+- **一键清空 (`clearable`)**:输入框有内容时浮现墨水圆滴清除按钮,兼容受控与非受控表单。
+- **密码切换 (`showPasswordToggle`)**:在 `type="password"` 时自动挂载眨眼显隐开关。
+- **校验反馈 (`invalid` & `errorMessage`)**:错误时触发 `splat-shake` 机械震颤动效并展示高亮警报红标签。
diff --git a/apps/docs/src/docs/examples/input.tsx b/apps/docs/src/docs/examples/input.tsx
index 4236676..4aead1c 100644
--- a/apps/docs/src/docs/examples/input.tsx
+++ b/apps/docs/src/docs/examples/input.tsx
@@ -1,21 +1,51 @@
'use client'
+import * as React from 'react'
import type { DocsExampleDefinitionInput } from '@/docs/types'
// docs-source-start
-import { Input } from 'splatoon-ui/input'
+import { Input, type InputFocusColor, type InputVariant } from 'splatoon-ui/input'
type InputExampleProps = {
value: string
+ placeholder: string
+ variant: string
+ focusColor: string
+ disabled: boolean
+ invalid: boolean
}
-export function InputExample({ value }: InputExampleProps) {
+export function InputExample({
+ value: initialValue,
+ placeholder,
+ variant,
+ focusColor,
+ disabled,
+ invalid,
+}: InputExampleProps) {
+ const [value, setValue] = React.useState(initialValue)
+ const [prevInitialValue, setPrevInitialValue] = React.useState(initialValue)
+
+ if (initialValue !== prevInitialValue) {
+ setPrevInitialValue(initialValue)
+ setValue(initialValue)
+ }
+
return (
- Player name
+ Pool / Player Keyword
-
+ setValue(e.target.value)}
+ />
)
}
@@ -24,8 +54,55 @@ export function InputExample({ value }: InputExampleProps) {
export const inputExample: DocsExampleDefinitionInput = {
id: 'input',
title: 'Input',
- description: 'Check sizing and text rendering inside form fields.',
- controls: [{ type: 'text', prop: 'value', label: 'Value', defaultValue: 'Inkling' }],
- initialProps: { value: 'Inkling' },
+ description:
+ 'Splatoon 3 Lobby Terminal keyword input with authentic 4-corner asymmetric cut, tactile displacement, and solid black shadow.',
+ controls: [
+ {
+ type: 'text',
+ prop: 'value',
+ label: 'Value',
+ defaultValue: 'SPLAT-2026',
+ },
+ {
+ type: 'text',
+ prop: 'placeholder',
+ label: 'Placeholder',
+ defaultValue: 'Enter pool keyword...',
+ },
+ {
+ type: 'select',
+ prop: 'variant',
+ label: 'Variant',
+ options: ['field-cut'],
+ defaultValue: 'field-cut',
+ },
+ {
+ type: 'select',
+ prop: 'focusColor',
+ label: 'Focus Color',
+ options: ['yellow', 'blue', 'purple', 'pink', 'green', 'orange'],
+ defaultValue: 'yellow',
+ },
+ {
+ type: 'boolean',
+ prop: 'disabled',
+ label: 'Disabled',
+ defaultValue: false,
+ },
+ {
+ type: 'boolean',
+ prop: 'invalid',
+ label: 'Invalid',
+ defaultValue: false,
+ },
+ ],
+ initialProps: {
+ value: 'SPLAT-2026',
+ placeholder: 'Enter pool keyword...',
+ variant: 'field-cut',
+ focusColor: 'yellow',
+ disabled: false,
+ invalid: false,
+ },
Component: InputExample,
}
diff --git a/apps/docs/src/docs/generated/api.json b/apps/docs/src/docs/generated/api.json
index 074da45..bd2181c 100644
--- a/apps/docs/src/docs/generated/api.json
+++ b/apps/docs/src/docs/generated/api.json
@@ -2600,15 +2600,43 @@
"type": "React.Ref",
"defaultValue": "",
"description": ""
+ },
+ {
+ "name": "variant",
+ "optional": true,
+ "type": "InputVariant",
+ "defaultValue": "",
+ "description": ""
+ },
+ {
+ "name": "focusColor",
+ "optional": true,
+ "type": "InputFocusColor",
+ "defaultValue": "",
+ "description": ""
}
]
},
+ {
+ "name": "InputFocusColor",
+ "kind": "type",
+ "type": "export type InputFocusColor = 'yellow' | 'blue' | 'purple' | 'pink' | 'green' | 'orange'",
+ "description": "",
+ "props": []
+ },
{
"name": "InputProps",
"kind": "interface",
"type": "export interface InputProps extends Omit, 'children' | 'ref'>",
"description": "",
"props": []
+ },
+ {
+ "name": "InputVariant",
+ "kind": "type",
+ "type": "export type InputVariant = 'field-cut'",
+ "description": "",
+ "props": []
}
]
},
diff --git a/apps/docs/src/docs/generated/example-source.json b/apps/docs/src/docs/generated/example-source.json
index 4a4c5a7..6334d3e 100644
--- a/apps/docs/src/docs/generated/example-source.json
+++ b/apps/docs/src/docs/generated/example-source.json
@@ -11,7 +11,7 @@
"dotted-divider": "import {\n DottedDivider,\n type DottedDividerProps,\n type SplatoonColorValue,\n} from 'splatoon-ui/dotted-divider'\n\ntype DottedDividerExampleProps = {\n orientation: DottedDividerProps['orientation']\n color: SplatoonColorValue\n}\n\nexport function DottedDividerExample({ orientation, color }: DottedDividerExampleProps) {\n return (\n \n \n
\n )\n}",
"heading-tape": "import { HeadingTape, type HeadingTapeProps } from 'splatoon-ui/heading-tape'\n\ntype HeadingTapeExampleProps = {\n size: HeadingTapeProps['size']\n decorationSet: NonNullable\n overlapTop: boolean\n label: string\n}\n\nexport function HeadingTapeExample({\n size,\n decorationSet,\n overlapTop,\n label,\n}: HeadingTapeExampleProps) {\n return (\n \n \n {label}\n \n
\n )\n}",
"icon-button": "import { Play } from 'lucide-react'\nimport {\n IconButton,\n type IconButtonAnimation,\n type IconButtonDirection,\n type IconButtonSize,\n type IconButtonVariant,\n} from 'splatoon-ui/icon-button'\n\ntype IconButtonExampleProps = {\n variant: IconButtonVariant\n size: IconButtonSize\n animation: IconButtonAnimation\n direction: IconButtonDirection\n}\n\nexport function IconButtonExample({ variant, size, animation, direction }: IconButtonExampleProps) {\n return (\n \n \n }\n />\n
\n )\n}",
- "input": "import { Input } from 'splatoon-ui/input'\n\ntype InputExampleProps = {\n value: string\n}\n\nexport function InputExample({ value }: InputExampleProps) {\n return (\n \n \n Player name\n \n \n
\n )\n}",
+ "input": "import { Input, type InputFocusColor, type InputVariant } from 'splatoon-ui/input'\n\ntype InputExampleProps = {\n value: string\n placeholder: string\n variant: string\n focusColor: string\n disabled: boolean\n invalid: boolean\n}\n\nexport function InputExample({\n value: initialValue,\n placeholder,\n variant,\n focusColor,\n disabled,\n invalid,\n}: InputExampleProps) {\n const [value, setValue] = React.useState(initialValue)\n const [prevInitialValue, setPrevInitialValue] = React.useState(initialValue)\n\n if (initialValue !== prevInitialValue) {\n setPrevInitialValue(initialValue)\n setValue(initialValue)\n }\n\n return (\n \n \n Pool / Player Keyword\n \n setValue(e.target.value)}\n />\n
\n )\n}",
"label": "import { Label } from 'splatoon-ui/label'\n\ntype LabelExampleProps = {\n label: string\n disabled: boolean\n}\n\nexport function LabelExample({ label, disabled }: LabelExampleProps) {\n return (\n \n {label} \n \n
\n )\n}",
"list": "import { List, ListItem, type SplatoonColorValue } from 'splatoon-ui/list'\n\ntype ListExampleProps = {\n start: number\n markerColor: SplatoonColorValue\n}\n\nexport function ListExample({ start, markerColor }: ListExampleProps) {\n return (\n \n Pick a weapon role. \n Queue with the squad. \n Review the rotation. \n
\n )\n}",
"loader": "import { Loader, type LoaderAnimation, type LoaderProps } from 'splatoon-ui/loader'\n\ntype LoaderExampleProps = {\n variant: LoaderProps['variant']\n animation: LoaderAnimation\n size: string\n label: string\n}\n\nexport function LoaderExample({ variant, animation, size, label }: LoaderExampleProps) {\n return (\n \n \n {label} \n
\n )\n}",
diff --git a/packages/ui/src/components/ui/input.module.css b/packages/ui/src/components/ui/input.module.css
new file mode 100644
index 0000000..5154e4f
--- /dev/null
+++ b/packages/ui/src/components/ui/input.module.css
@@ -0,0 +1,87 @@
+.inputField {
+ appearance: none;
+ width: 100%;
+ min-width: 0;
+ height: 2.75rem;
+ border-radius: 7px 11px 8px 12px;
+ background-color: var(--desert-sand, #f4eee0);
+ color: var(--color-chaos-black, #0d0d0d);
+ border: 2.5px solid var(--color-chaos-black, #0d0d0d);
+ box-shadow: 2.5px 2.5px 0px var(--color-chaos-black, #0d0d0d);
+ padding: 0.5rem 0.875rem;
+ font-family: inherit;
+ font-size: 0.95rem;
+ font-weight: 700;
+ outline: none;
+ transition:
+ transform 0.15s cubic-bezier(0.2, 0.8, 0.2, 1),
+ box-shadow 0.15s cubic-bezier(0.2, 0.8, 0.2, 1),
+ border-color 0.15s ease,
+ background-color 0.15s ease;
+}
+
+.inputField::placeholder {
+ color: #8c857b;
+ font-weight: 500;
+}
+
+.inputField:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+/* Authentic Splatoon 3 Lobby Terminal focus state with tactile displacement & solid shadow */
+.inputField:focus,
+.inputField:focus-visible {
+ outline: none;
+ border-color: var(--color-chaos-black, #0d0d0d);
+ box-shadow:
+ 0 0 0 3px var(--input-focus-color, var(--color-yellow, #eaff3d)),
+ 3px 3px 0px var(--color-chaos-black, #0d0d0d);
+ transform: translate(-1px, -1px);
+}
+
+.inputField[aria-invalid='true'],
+.inputField:invalid {
+ border-color: var(--color-red, #ff585e);
+ animation: splat-shake 0.38s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
+}
+
+.inputField[aria-invalid='true']:focus,
+.inputField:invalid:focus {
+ box-shadow:
+ 0 0 0 3px var(--color-red, #ff585e),
+ 3px 3px 0px var(--color-chaos-black, #0d0d0d);
+ transform: translate(-1px, -1px);
+}
+
+@keyframes splat-shake {
+ 10%,
+ 90% {
+ transform: translate3d(-1px, 0, 0) rotate(-0.5deg);
+ }
+ 20%,
+ 80% {
+ transform: translate3d(2px, 0, 0) rotate(0.5deg);
+ }
+ 30%,
+ 50%,
+ 70% {
+ transform: translate3d(-3px, 0, 0) rotate(-1deg);
+ }
+ 40%,
+ 60% {
+ transform: translate3d(3px, 0, 0) rotate(1deg);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .inputField[aria-invalid='true'],
+ .inputField:invalid {
+ animation: none;
+ }
+ .inputField {
+ transition: none;
+ }
+}
diff --git a/packages/ui/src/components/ui/input.tsx b/packages/ui/src/components/ui/input.tsx
index a820232..7e851c1 100644
--- a/packages/ui/src/components/ui/input.tsx
+++ b/packages/ui/src/components/ui/input.tsx
@@ -1,22 +1,43 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
+import styles from './input.module.css'
+
+export type InputVariant = 'field-cut'
+export type InputFocusColor = 'yellow' | 'blue' | 'purple' | 'pink' | 'green' | 'orange'
export interface InputProps extends Omit, 'children' | 'ref'> {
ref?: React.Ref
+ variant?: InputVariant
+ focusColor?: InputFocusColor
+}
+
+const focusColorMap: Record = {
+ yellow: 'var(--color-yellow, #eaff3d)',
+ blue: 'var(--color-blue, #2b3bf6)',
+ purple: 'var(--color-purple, #6a0dad)',
+ pink: 'var(--color-pink, #ff007f)',
+ green: 'var(--color-green, #00e676)',
+ orange: 'var(--color-orange, #ff6600)',
}
function Input({ ref, className, type, ...props }: InputProps) {
+ const { variant = 'field-cut', focusColor = 'yellow', style, ...rest } = props
+
+ const inlineStyle: React.CSSProperties = {
+ '--input-focus-color': focusColorMap[focusColor] ?? focusColor,
+ ...style,
+ } as React.CSSProperties
+
return (
)
}
diff --git a/packages/ui/src/lib/splatoon-color-tokens.ts b/packages/ui/src/lib/splatoon-color-tokens.ts
index 6e02de1..9ace4d5 100644
--- a/packages/ui/src/lib/splatoon-color-tokens.ts
+++ b/packages/ui/src/lib/splatoon-color-tokens.ts
@@ -10,6 +10,8 @@ export type SplatoonColorToken =
| 'black'
| 'trueBlack'
| 'salmonRunGreen'
+ | 'desertSand'
+ | 'tapeWhite'
export const splatoonColorVars = {
yellow: 'var(--color-yellow)',
@@ -23,6 +25,8 @@ export const splatoonColorVars = {
black: 'var(--color-black)',
trueBlack: 'var(--color-true-black)',
salmonRunGreen: 'var(--color-salmon-run-green)',
+ desertSand: 'var(--desert-sand)',
+ tapeWhite: 'var(--tape-white)',
} as const satisfies Record
export type SplatoonColorValue = SplatoonColorToken | (string & {})
diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css
index 505269d..b1690fc 100644
--- a/packages/ui/src/styles/globals.css
+++ b/packages/ui/src/styles/globals.css
@@ -102,6 +102,8 @@
--color-chaos-black: #0d0d0d;
--color-black: #0d0d0d;
--color-white: #ffffff;
+ --color-desert-sand: var(--desert-sand);
+ --color-tape-white: var(--tape-white);
--color-danger-surface-title: var(--danger-surface-title);
--color-danger-surface-description: var(--danger-surface-description);
--color-sidebar-ring: var(--sidebar-ring);