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 (
- + 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 \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 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 \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);