Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions apps/docs/content/docs/en/input.mdx
Original file line number Diff line number Diff line change
@@ -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'
;<Input
variant="field-cut"
size="default"
focusColor="yellow"
prefixIcon={<Search />}
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.
33 changes: 27 additions & 6 deletions apps/docs/content/docs/ja/input.mdx
Original file line number Diff line number Diff line change
@@ -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'
;<Input
variant="field-cut"
size="default"
focusColor="yellow"
prefixIcon={<Search />}
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` 振動動效とアラート表示を実行。
32 changes: 26 additions & 6 deletions apps/docs/content/docs/zh/input.mdx
Original file line number Diff line number Diff line change
@@ -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'
;<Input
variant="field-cut"
size="default"
focusColor="yellow"
prefixIcon={<Search />}
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` 机械震颤动效并展示高亮警报红标签。
91 changes: 84 additions & 7 deletions apps/docs/src/docs/examples/input.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="grid w-full max-w-sm gap-2">
<label className="text-sm font-black tracking-wider" htmlFor="docs-input-example">
Player name
Pool / Player Keyword
</label>
<Input id="docs-input-example" value={value} readOnly />
<Input
id="docs-input-example"
variant={variant as InputVariant}
focusColor={focusColor as InputFocusColor}
value={value}
placeholder={placeholder}
disabled={disabled}
aria-invalid={invalid}
onChange={(e) => setValue(e.target.value)}
/>
</div>
)
}
Expand All @@ -24,8 +54,55 @@ export function InputExample({ value }: InputExampleProps) {
export const inputExample: DocsExampleDefinitionInput<InputExampleProps> = {
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,
}
28 changes: 28 additions & 0 deletions apps/docs/src/docs/generated/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -2600,15 +2600,43 @@
"type": "React.Ref<HTMLInputElement>",
"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<React.ComponentProps<'input'>, 'children' | 'ref'>",
"description": "",
"props": []
},
{
"name": "InputVariant",
"kind": "type",
"type": "export type InputVariant = 'field-cut'",
"description": "",
"props": []
}
]
},
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/docs/generated/example-source.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div\n className={\n orientation === 'vertical'\n ? 'flex h-48 items-stretch justify-center'\n : 'grid w-full max-w-md gap-4'\n }\n >\n <DottedDivider orientation={orientation} color={color} />\n </div>\n )\n}",
"heading-tape": "import { HeadingTape, type HeadingTapeProps } from 'splatoon-ui/heading-tape'\n\ntype HeadingTapeExampleProps = {\n size: HeadingTapeProps['size']\n decorationSet: NonNullable<HeadingTapeProps['decorationSet']>\n overlapTop: boolean\n label: string\n}\n\nexport function HeadingTapeExample({\n size,\n decorationSet,\n overlapTop,\n label,\n}: HeadingTapeExampleProps) {\n return (\n <div className=\"grid min-h-40 w-full place-items-center\">\n <HeadingTape size={size} decorationSet={decorationSet} overlapTop={overlapTop}>\n {label}\n </HeadingTape>\n </div>\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 <div className=\"flex flex-wrap items-center justify-center gap-4\">\n <IconButton\n aria-label={`Move ${direction}`}\n variant={variant}\n size={size}\n animation={animation}\n direction={direction}\n />\n <IconButton\n aria-label=\"Play\"\n variant={variant}\n size={size}\n animation={animation}\n icon={<Play className=\"h-[calc(var(--size)*0.42)] w-[calc(var(--size)*0.42)]\" />}\n />\n </div>\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 <div className=\"grid w-full max-w-sm gap-2\">\n <label className=\"text-sm font-black tracking-wider\" htmlFor=\"docs-input-example\">\n Player name\n </label>\n <Input id=\"docs-input-example\" value={value} readOnly />\n </div>\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 <div className=\"grid w-full max-w-sm gap-2\">\n <label className=\"text-sm font-black tracking-wider\" htmlFor=\"docs-input-example\">\n Pool / Player Keyword\n </label>\n <Input\n id=\"docs-input-example\"\n variant={variant as InputVariant}\n focusColor={focusColor as InputFocusColor}\n value={value}\n placeholder={placeholder}\n disabled={disabled}\n aria-invalid={invalid}\n onChange={(e) => setValue(e.target.value)}\n />\n </div>\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 <div data-disabled={disabled ? 'true' : undefined} className=\"grid w-full max-w-sm gap-2\">\n <Label htmlFor=\"docs-public-label\">{label}</Label>\n <input\n id=\"docs-public-label\"\n className=\"border-chaos-black h-10 border-2 px-3 font-bold\"\n disabled={disabled}\n readOnly\n value=\"Agent 3\"\n />\n </div>\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 <List start={start} markerColor={markerColor} className=\"w-full max-w-md\">\n <ListItem>Pick a weapon role.</ListItem>\n <ListItem>Queue with the squad.</ListItem>\n <ListItem showDivider={false}>Review the rotation.</ListItem>\n </List>\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 <div className=\"flex flex-wrap items-center justify-center gap-4\">\n <Loader variant={variant} animation={animation} size={size} label={label} />\n <span className=\"font-alt text-2xl font-black\">{label}</span>\n </div>\n )\n}",
Expand Down
87 changes: 87 additions & 0 deletions packages/ui/src/components/ui/input.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading