@geckou/ui-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/package.json +52 -0
- package/src/components/BasicButton.tsx +81 -0
- package/src/components/CheckBox.tsx +87 -0
- package/src/components/CheckBoxes.tsx +93 -0
- package/src/components/CheckButton.tsx +66 -0
- package/src/components/DatePicker.tsx +154 -0
- package/src/components/DateRangePicker.tsx +59 -0
- package/src/components/DateSelector.tsx +162 -0
- package/src/components/DropdownUi.tsx +126 -0
- package/src/components/ErrorMessage.tsx +30 -0
- package/src/components/FileInput.tsx +93 -0
- package/src/components/InputBox.tsx +118 -0
- package/src/components/InputGroup.tsx +13 -0
- package/src/components/LabeledCheckbox.tsx +61 -0
- package/src/components/LabeledFieldset.tsx +18 -0
- package/src/components/LoadingSpinner.tsx +23 -0
- package/src/components/ModalBox.tsx +109 -0
- package/src/components/PopupBox.tsx +70 -0
- package/src/components/RadioButtons.tsx +94 -0
- package/src/components/SearchableSelectBox.tsx +108 -0
- package/src/components/SelectBox.tsx +152 -0
- package/src/components/SlideDownUi.tsx +123 -0
- package/src/components/TabUI.tsx +112 -0
- package/src/components/TextArea.tsx +102 -0
- package/src/components/TextBox.tsx +120 -0
- package/src/components/ToggleButton.tsx +102 -0
- package/src/components/icons/BackupIcon.tsx +16 -0
- package/src/components/icons/CalendarIcon.tsx +15 -0
- package/src/components/icons/CheckIcon.tsx +7 -0
- package/src/components/icons/CloseIcon.tsx +16 -0
- package/src/components/icons/KeyboardArrowDownIcon.tsx +16 -0
- package/src/constants/index.ts +2 -0
- package/src/hooks/useFormValidation.ts +69 -0
- package/src/index.ts +66 -0
- package/src/styles/tokens.css +61 -0
- package/src/types/index.ts +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Geckou LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @geckou/ui-react
|
|
2
|
+
|
|
3
|
+
Web(React 19 / Next.js)用の UI コンポーネント集。
|
|
4
|
+
|
|
5
|
+
ロジックは [`@geckou/ui-core`](../core) が持ち、このパッケージは DOM の記述だけを担当する。
|
|
6
|
+
同じ core を [`@geckou/ui-vue`](../vue) も使うため、バリデーション・日付処理の挙動は両者で一致する。
|
|
7
|
+
|
|
8
|
+
Mobile(Expo / NativeWind)では使えない(DOM 前提のため)。
|
|
9
|
+
|
|
10
|
+
## インストール
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
yarn add @geckou/ui-react
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
ソースをそのまま配布しているため、利用側でトランスパイルの対象に含める。
|
|
17
|
+
|
|
18
|
+
1. `next.config.ts` の `transpilePackages` に `'@geckou/ui-react'` を追加
|
|
19
|
+
2. `tailwind.config.ts` の `content` に `'./node_modules/@geckou/ui-react/src/**/*.{ts,tsx}'` を追加
|
|
20
|
+
3. グローバル CSS に `@import '@geckou/ui-react/styles/tokens.css';` を追加(デザイントークン)
|
|
21
|
+
|
|
22
|
+
`tokens.css` は既定値です。上書きする変数の一覧はリポジトリの [README](../../README.md#design-tokens) を参照してください。
|
|
23
|
+
|
|
24
|
+
## 使い方
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { TextBox, BasicButton, ModalBox } from '@geckou/ui-react'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### フォーム全体の検証状態
|
|
31
|
+
|
|
32
|
+
`useFormValidation` で各入力の検証結果を集約する。
|
|
33
|
+
Vue 版の `FormValidationManager` と同じストア(`@geckou/ui-core`)を使う。
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
const { isAllValid, store } = useFormValidation()
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<form>
|
|
40
|
+
<DatePicker name="startedOn" isRequired />
|
|
41
|
+
<button disabled={!isAllValid}>送信</button>
|
|
42
|
+
</form>
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 収録コンポーネント
|
|
47
|
+
|
|
48
|
+
フォーム系 25 種(`TextBox` / `TextArea` / `SelectBox` / `SearchableSelectBox` / `CheckBox` 系 /
|
|
49
|
+
`RadioButtons` / `ToggleButton` / `DatePicker` / `DateRangePicker` / `DateSelector` /
|
|
50
|
+
`FileInput` / `ModalBox` / `PopupBox` / `DropdownUi` / `SlideDownUi` / `TabUI` ほか)とアイコン 5 種。
|
|
51
|
+
|
|
52
|
+
記事一覧(ArticleList)は Vue 版のみに収録している。
|
|
53
|
+
|
|
54
|
+
## テスト
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
yarn workspace @geckou/ui-react test
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
[MIT](../../LICENSE)
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geckou/ui-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A set of reusable React UI components (forms) by Geckou",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"default": "./src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"./styles/tokens.css": "./src/styles/tokens.css",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/geckou/ui.git",
|
|
24
|
+
"directory": "packages/react"
|
|
25
|
+
},
|
|
26
|
+
"author": "geckou-shogo <shogo.nojima@geckou.net>",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20.0.0"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"registry": "https://registry.npmjs.org/",
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": "^19.0.0",
|
|
37
|
+
"react-dom": "^19.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@geckou/ui-core": "^0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "echo 'ソースをそのまま配布する(利用側で transpilePackages に指定する)'",
|
|
44
|
+
"type-check": "tsc --noEmit",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/react": "^19.0.0",
|
|
50
|
+
"@types/react-dom": "^19.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties, MouseEventHandler, ReactNode } from 'react'
|
|
4
|
+
import type { ButtonStyleForEachStatus } from '../types'
|
|
5
|
+
import { LoadingSpinner } from './LoadingSpinner'
|
|
6
|
+
import { COLOR } from '../constants'
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
cssStyle?: ButtonStyleForEachStatus
|
|
10
|
+
duration?: number
|
|
11
|
+
isLoading?: boolean
|
|
12
|
+
isDisabled?: boolean
|
|
13
|
+
buttonType?: 'button' | 'submit' | 'reset'
|
|
14
|
+
onClick?: MouseEventHandler<HTMLButtonElement>
|
|
15
|
+
loading?: ReactNode
|
|
16
|
+
children?: ReactNode
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function BasicButton({
|
|
20
|
+
cssStyle,
|
|
21
|
+
duration = 0.3,
|
|
22
|
+
isLoading,
|
|
23
|
+
isDisabled,
|
|
24
|
+
buttonType = 'button',
|
|
25
|
+
onClick,
|
|
26
|
+
loading,
|
|
27
|
+
children,
|
|
28
|
+
}: Props) {
|
|
29
|
+
const baseStyle = isDisabled ? cssStyle?.disabled : cssStyle?.default
|
|
30
|
+
|
|
31
|
+
const currentCssStyle = {
|
|
32
|
+
textColor: COLOR.white,
|
|
33
|
+
backgroundColor: isDisabled ? COLOR.darkGray : COLOR.blue,
|
|
34
|
+
backgroundImage: 'none',
|
|
35
|
+
border: {
|
|
36
|
+
color: isDisabled ? COLOR.darkGray : COLOR.blue,
|
|
37
|
+
size: '1px',
|
|
38
|
+
radius: '.25rem',
|
|
39
|
+
},
|
|
40
|
+
boxShadow: '0 0 0 0 transparent',
|
|
41
|
+
...baseStyle,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const hoverStyle = cssStyle?.hover || {}
|
|
45
|
+
|
|
46
|
+
const style = {
|
|
47
|
+
'--text-color': currentCssStyle.textColor,
|
|
48
|
+
'--background-color': currentCssStyle.backgroundColor,
|
|
49
|
+
'--background-image': currentCssStyle.backgroundImage,
|
|
50
|
+
'--radius-size': currentCssStyle.border?.radius,
|
|
51
|
+
'--button-shadow': `0 0 0 ${currentCssStyle.border?.size} ${currentCssStyle.border?.color} inset, ${currentCssStyle.boxShadow}`,
|
|
52
|
+
'--hover-text-color': hoverStyle.textColor || currentCssStyle.textColor,
|
|
53
|
+
'--hover-background-color':
|
|
54
|
+
hoverStyle.backgroundColor ||
|
|
55
|
+
(currentCssStyle.backgroundColor &&
|
|
56
|
+
`${currentCssStyle.backgroundColor}cc`),
|
|
57
|
+
'--hover-background-image':
|
|
58
|
+
hoverStyle.backgroundImage || currentCssStyle.backgroundImage,
|
|
59
|
+
'--hover-radius-size':
|
|
60
|
+
hoverStyle.border?.radius || currentCssStyle.border?.radius,
|
|
61
|
+
'--hover-button-shadow': `0 0 0 ${hoverStyle.border?.size || currentCssStyle.border?.size} ${hoverStyle.border?.color || currentCssStyle.border?.color} inset, ${hoverStyle.boxShadow || currentCssStyle.boxShadow}`,
|
|
62
|
+
'--duration': `${duration}s`,
|
|
63
|
+
} as CSSProperties
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<button
|
|
67
|
+
type={buttonType}
|
|
68
|
+
disabled={isDisabled || isLoading}
|
|
69
|
+
onClick={onClick}
|
|
70
|
+
style={style}
|
|
71
|
+
className="relative inline-flex w-max cursor-pointer items-center justify-center rounded-(--radius-size) border-none bg-(--background-color) px-8 py-4 leading-none text-(--text-color) shadow-(--button-shadow) transition-all duration-(--duration) enabled:hover:rounded-(--hover-radius-size) enabled:hover:bg-(--hover-background-color) enabled:hover:text-(--hover-text-color) enabled:hover:shadow-(--hover-button-shadow) disabled:pointer-events-none disabled:before:pointer-events-auto disabled:before:absolute disabled:before:inset-0 disabled:before:cursor-not-allowed disabled:before:content-['']"
|
|
72
|
+
>
|
|
73
|
+
{isLoading && (
|
|
74
|
+
<span className="absolute inset-0 m-auto flex max-h-[calc(100%-1.5rem)] items-center justify-center fill-current text-current [&_*]:max-h-full [&_*]:fill-current">
|
|
75
|
+
{loading ?? <LoadingSpinner />}
|
|
76
|
+
</span>
|
|
77
|
+
)}
|
|
78
|
+
<span className={isLoading ? 'invisible' : undefined}>{children}</span>
|
|
79
|
+
</button>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
4
|
+
import type { CheckBoxStyleForEachStatus } from '../types'
|
|
5
|
+
import { CheckIcon } from './icons/CheckIcon'
|
|
6
|
+
import { COLOR } from '../constants'
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
name: string
|
|
10
|
+
checked?: boolean
|
|
11
|
+
onChange?: (newValue: boolean) => void
|
|
12
|
+
isDisabled?: boolean
|
|
13
|
+
cssStyle?: CheckBoxStyleForEachStatus
|
|
14
|
+
isDisableAnimation?: boolean
|
|
15
|
+
check?: ReactNode
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function CheckBox({
|
|
19
|
+
name,
|
|
20
|
+
checked,
|
|
21
|
+
onChange,
|
|
22
|
+
isDisabled,
|
|
23
|
+
cssStyle,
|
|
24
|
+
isDisableAnimation,
|
|
25
|
+
check,
|
|
26
|
+
}: Props) {
|
|
27
|
+
const isChecked = checked ?? false
|
|
28
|
+
const baseStyle = isDisabled ? cssStyle?.disabled : cssStyle?.default
|
|
29
|
+
|
|
30
|
+
const currentCssStyle = {
|
|
31
|
+
textColor: isDisabled ? COLOR.darkGray : COLOR.blue,
|
|
32
|
+
backgroundColor: isDisabled ? COLOR.lightGray : COLOR.white,
|
|
33
|
+
border: {
|
|
34
|
+
color: isDisabled ? COLOR.darkGray : COLOR.blue,
|
|
35
|
+
size: '1px',
|
|
36
|
+
radius: '.25rem',
|
|
37
|
+
},
|
|
38
|
+
...baseStyle,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const style = {
|
|
42
|
+
'--text-color': currentCssStyle.textColor,
|
|
43
|
+
'--border-color': currentCssStyle.border?.color,
|
|
44
|
+
'--border-size': currentCssStyle.border?.size,
|
|
45
|
+
'--radius-size': currentCssStyle.border?.radius,
|
|
46
|
+
'--background-color': currentCssStyle.backgroundColor,
|
|
47
|
+
'--duration': isDisableAnimation ? '0s' : '.3s',
|
|
48
|
+
} as CSSProperties
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<style>
|
|
53
|
+
{
|
|
54
|
+
'@keyframes uiCheckPop{0%{scale:1}10%{scale:.8}50%{scale:1.1}100%{scale:1}}'
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
style={style}
|
|
60
|
+
aria-label={name}
|
|
61
|
+
aria-pressed={isChecked}
|
|
62
|
+
disabled={isDisabled}
|
|
63
|
+
onClick={(event) => {
|
|
64
|
+
event.stopPropagation()
|
|
65
|
+
if (!isDisabled) onChange?.(!isChecked)
|
|
66
|
+
}}
|
|
67
|
+
className={`relative flex h-6 w-6 cursor-pointer items-center justify-center rounded-(--radius-size) border-none shadow-[0_0_0_var(--border-size)_var(--border-color)_inset] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--border-color) has-[input:disabled]:pointer-events-none has-[input:disabled]:before:pointer-events-auto has-[input:disabled]:before:absolute has-[input:disabled]:before:inset-0 has-[input:disabled]:before:cursor-not-allowed has-[input:disabled]:before:content-[''] ${isChecked ? 'animate-[uiCheckPop_var(--duration)_ease-out] bg-(--border-color)' : 'bg-(--background-color)'}`}
|
|
68
|
+
>
|
|
69
|
+
<input
|
|
70
|
+
type="checkbox"
|
|
71
|
+
name={name}
|
|
72
|
+
checked={isChecked}
|
|
73
|
+
disabled={isDisabled}
|
|
74
|
+
onChange={(event) => {
|
|
75
|
+
if (!isDisabled) onChange?.(event.target.checked)
|
|
76
|
+
}}
|
|
77
|
+
className="hidden"
|
|
78
|
+
/>
|
|
79
|
+
<div
|
|
80
|
+
className={`flex h-4 w-4 items-center justify-center [&>*]:fill-current [&>*]:text-current ${isChecked ? 'text-(--background-color)' : 'text-(--border-color)'}`}
|
|
81
|
+
>
|
|
82
|
+
{check ?? <CheckIcon />}
|
|
83
|
+
</div>
|
|
84
|
+
</button>
|
|
85
|
+
</>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { Option, CheckBoxStyleForEachStatus } from '../types'
|
|
4
|
+
import { MESSAGES } from '@geckou/ui-core'
|
|
5
|
+
import { useEffect, useRef, useState } from 'react'
|
|
6
|
+
import { LabeledCheckbox } from './LabeledCheckbox'
|
|
7
|
+
import { ErrorMessage } from './ErrorMessage'
|
|
8
|
+
import { COLOR } from '../constants'
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
name: string
|
|
12
|
+
options: Option[]
|
|
13
|
+
value?: string[]
|
|
14
|
+
onChange?: (newValue: string[]) => void
|
|
15
|
+
isDisabled?: boolean
|
|
16
|
+
isRequired?: boolean
|
|
17
|
+
cssStyle?: CheckBoxStyleForEachStatus
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function CheckBoxes({
|
|
21
|
+
name,
|
|
22
|
+
options,
|
|
23
|
+
value,
|
|
24
|
+
onChange,
|
|
25
|
+
isDisabled,
|
|
26
|
+
isRequired,
|
|
27
|
+
cssStyle,
|
|
28
|
+
}: Props) {
|
|
29
|
+
const [checkedValues, setCheckedValues] = useState<string[]>(value ?? [])
|
|
30
|
+
const [errorMessage, setErrorMessage] = useState('')
|
|
31
|
+
|
|
32
|
+
const validateInput = (newValues: string[]) =>
|
|
33
|
+
setErrorMessage(isRequired && newValues.length === 0 ? MESSAGES.required : '')
|
|
34
|
+
|
|
35
|
+
const isFirstRender = useRef(true)
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (isFirstRender.current) {
|
|
39
|
+
isFirstRender.current = false
|
|
40
|
+
if (value?.length) validateInput(value)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (value) setCheckedValues(value)
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- Vue 版(@geckou/ui-vue)同様、外部からの値変更時のみ同期する
|
|
46
|
+
}, [value])
|
|
47
|
+
|
|
48
|
+
const toggleValue = (optionValue: string, checked: boolean) => {
|
|
49
|
+
const newValues = options
|
|
50
|
+
.map((option) => String(option.value))
|
|
51
|
+
.filter((candidate) =>
|
|
52
|
+
candidate === optionValue ? checked : checkedValues.includes(candidate),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
setCheckedValues(newValues)
|
|
56
|
+
validateInput(newValues)
|
|
57
|
+
onChange?.(newValues)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const errorColor = {
|
|
61
|
+
...(cssStyle?.error ?? {}),
|
|
62
|
+
textColor: COLOR.white,
|
|
63
|
+
backgroundColor: COLOR.red,
|
|
64
|
+
border: {
|
|
65
|
+
color: COLOR.red,
|
|
66
|
+
size: '1px',
|
|
67
|
+
radius: '.25rem',
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="relative flex flex-wrap gap-x-6 gap-y-4">
|
|
73
|
+
{options.map((option) => (
|
|
74
|
+
<LabeledCheckbox
|
|
75
|
+
key={option.value}
|
|
76
|
+
name={name}
|
|
77
|
+
label={option.label}
|
|
78
|
+
checked={checkedValues.includes(String(option.value))}
|
|
79
|
+
onChange={(checked) => toggleValue(String(option.value), checked)}
|
|
80
|
+
isDisabled={isDisabled}
|
|
81
|
+
cssStyle={cssStyle}
|
|
82
|
+
/>
|
|
83
|
+
))}
|
|
84
|
+
<ErrorMessage
|
|
85
|
+
errorMessages={errorMessage ? [errorMessage] : []}
|
|
86
|
+
cssStyle={{
|
|
87
|
+
textColor: errorColor.textColor,
|
|
88
|
+
backgroundColor: errorColor.backgroundColor,
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
4
|
+
import type { CheckBoxStyleForEachStatus } from '../types'
|
|
5
|
+
import { CheckIcon } from './icons/CheckIcon'
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
name: string
|
|
9
|
+
checked?: boolean
|
|
10
|
+
onChange?: (newValue: boolean) => void
|
|
11
|
+
isDisabled?: boolean
|
|
12
|
+
cssStyle?: CheckBoxStyleForEachStatus
|
|
13
|
+
isDisableAnimation?: boolean
|
|
14
|
+
check?: ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CheckButton({
|
|
18
|
+
name,
|
|
19
|
+
checked,
|
|
20
|
+
onChange,
|
|
21
|
+
isDisabled,
|
|
22
|
+
cssStyle = { default: {} },
|
|
23
|
+
isDisableAnimation,
|
|
24
|
+
check,
|
|
25
|
+
}: Props) {
|
|
26
|
+
const isChecked = checked ?? false
|
|
27
|
+
const currentCssStyle = isDisabled ? cssStyle.disabled : cssStyle.default
|
|
28
|
+
|
|
29
|
+
const style = {
|
|
30
|
+
'--border-color': currentCssStyle?.border?.color || 'blue',
|
|
31
|
+
'--border-size': currentCssStyle?.border?.size || '1px',
|
|
32
|
+
'--radius-size': currentCssStyle?.border?.radius || '0.25rem',
|
|
33
|
+
'--background-color': currentCssStyle?.backgroundColor || '#fff',
|
|
34
|
+
'--duration': isDisableAnimation ? '0s' : '.3s',
|
|
35
|
+
} as CSSProperties
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<style>
|
|
40
|
+
{
|
|
41
|
+
'@keyframes uiCheckPop{0%{scale:1}10%{scale:.8}50%{scale:1.1}100%{scale:1}}'
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
44
|
+
<label
|
|
45
|
+
style={style}
|
|
46
|
+
className={`relative flex h-6 w-6 cursor-pointer items-center justify-center rounded-(--radius-size) shadow-[0_0_0_var(--border-size)_var(--border-color)_inset] has-[input:disabled]:pointer-events-none has-[input:disabled]:before:pointer-events-auto has-[input:disabled]:before:absolute has-[input:disabled]:before:inset-0 has-[input:disabled]:before:cursor-not-allowed has-[input:disabled]:before:content-[''] has-[input:focus-visible]:outline-2 has-[input:focus-visible]:outline-offset-2 has-[input:focus-visible]:outline-(--border-color) ${isChecked ? 'animate-[uiCheckPop_var(--duration)_ease-out] bg-(--border-color)' : 'bg-(--background-color)'}`}
|
|
47
|
+
>
|
|
48
|
+
<input
|
|
49
|
+
type="checkbox"
|
|
50
|
+
name={name}
|
|
51
|
+
checked={isChecked}
|
|
52
|
+
disabled={isDisabled}
|
|
53
|
+
onChange={(event) => {
|
|
54
|
+
if (!isDisabled) onChange?.(event.target.checked)
|
|
55
|
+
}}
|
|
56
|
+
className="sr-only"
|
|
57
|
+
/>
|
|
58
|
+
<div
|
|
59
|
+
className={`flex h-4 w-4 items-center justify-center [&>*]:fill-current [&>*]:text-current ${isChecked ? 'text-(--background-color)' : 'text-(--border-color)'}`}
|
|
60
|
+
>
|
|
61
|
+
{check ?? <CheckIcon />}
|
|
62
|
+
</div>
|
|
63
|
+
</label>
|
|
64
|
+
</>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties } from 'react'
|
|
4
|
+
import type { DateObject, InputBoxStyleForEachStatus } from '../types'
|
|
5
|
+
import {
|
|
6
|
+
MESSAGES,
|
|
7
|
+
composeDateValue,
|
|
8
|
+
formatDateValue,
|
|
9
|
+
splitDate,
|
|
10
|
+
validateDateObject,
|
|
11
|
+
} from '@geckou/ui-core'
|
|
12
|
+
import { useEffect, useRef, useState } from 'react'
|
|
13
|
+
import { InputBox } from './InputBox'
|
|
14
|
+
import { ErrorMessage } from './ErrorMessage'
|
|
15
|
+
import { CalendarIcon } from './icons/CalendarIcon'
|
|
16
|
+
import { COLOR } from '../constants'
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
name: string
|
|
20
|
+
value?: string
|
|
21
|
+
onChange?: (newValue: string) => void
|
|
22
|
+
cssStyle?: InputBoxStyleForEachStatus
|
|
23
|
+
isDisabled?: boolean
|
|
24
|
+
isRequired?: boolean
|
|
25
|
+
minDate?: string
|
|
26
|
+
maxDate?: string
|
|
27
|
+
size?: 'small' | 'medium'
|
|
28
|
+
type?: 'date' | 'month'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function DatePicker({
|
|
32
|
+
name,
|
|
33
|
+
value,
|
|
34
|
+
onChange,
|
|
35
|
+
cssStyle,
|
|
36
|
+
isDisabled,
|
|
37
|
+
isRequired,
|
|
38
|
+
minDate = '',
|
|
39
|
+
maxDate = '',
|
|
40
|
+
size = 'medium',
|
|
41
|
+
type = 'date',
|
|
42
|
+
}: Props) {
|
|
43
|
+
const [dateValue, setDateValue] = useState(() =>
|
|
44
|
+
value ? formatDateValue(value, type) : '',
|
|
45
|
+
)
|
|
46
|
+
const [dateObject, setDateObject] = useState<DateObject>(() =>
|
|
47
|
+
value ? splitDate(formatDateValue(value, type)) : { year: '', month: '', day: '' },
|
|
48
|
+
)
|
|
49
|
+
const [errorMessage, setErrorMessage] = useState('')
|
|
50
|
+
|
|
51
|
+
// prop が実際に変わったときだけ同期する(内部編集を上書きしないため)
|
|
52
|
+
const lastValueProp = useRef(value)
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (lastValueProp.current === value) return
|
|
56
|
+
lastValueProp.current = value
|
|
57
|
+
|
|
58
|
+
const normalized = value ? formatDateValue(value, type) : ''
|
|
59
|
+
setDateValue(normalized)
|
|
60
|
+
setDateObject(splitDate(normalized))
|
|
61
|
+
}, [value, type])
|
|
62
|
+
|
|
63
|
+
const validateInput = (newValue: string) => {
|
|
64
|
+
if (!newValue && isRequired)
|
|
65
|
+
{return { isValid: false, message: MESSAGES.required }}
|
|
66
|
+
return { isValid: true, message: '' }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const validateObject = (object: DateObject) =>
|
|
70
|
+
validateDateObject(object, { type, isRequired })
|
|
71
|
+
|
|
72
|
+
const handleDateValueChange = (newValue: string) => {
|
|
73
|
+
setDateValue(newValue)
|
|
74
|
+
setDateObject(splitDate(newValue))
|
|
75
|
+
const { message } = validateInput(newValue)
|
|
76
|
+
setErrorMessage(message)
|
|
77
|
+
onChange?.(newValue)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const handleObjectChange = (key: keyof DateObject, newValue: string) => {
|
|
81
|
+
const next = { ...dateObject, [key]: newValue }
|
|
82
|
+
setDateObject(next)
|
|
83
|
+
const { isValid, message } = validateObject(next)
|
|
84
|
+
setErrorMessage(message)
|
|
85
|
+
|
|
86
|
+
if (isValid) {
|
|
87
|
+
const joined = composeDateValue(next, type)
|
|
88
|
+
setDateValue(joined)
|
|
89
|
+
onChange?.(joined)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const isSmall = size === 'small'
|
|
94
|
+
const textInputClass = `flex-none! ${isSmall ? 'px-[var(--sp-small,0.375rem)]! py-[var(--sp-min,0.1875rem)]! text-[length:var(--fs-small,0.6875rem)]' : 'p-[var(--sp-medium,0.75rem)]!'}`
|
|
95
|
+
const iconStyle = { '--icon-color': COLOR.blue } as CSSProperties
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<InputBox
|
|
99
|
+
cssStyle={cssStyle}
|
|
100
|
+
isDisabled={isDisabled}
|
|
101
|
+
isErrored={!!errorMessage}
|
|
102
|
+
className={`flex w-full items-center leading-none ${isSmall ? 'h-[calc(var(--bv,0.375rem)*5)] px-[var(--sp-min,0.1875rem)]' : 'h-[calc(var(--bv,0.375rem)*6)] px-[var(--sp-small,0.375rem)]'}`}
|
|
103
|
+
>
|
|
104
|
+
<div className="relative flex-none" style={iconStyle}>
|
|
105
|
+
<CalendarIcon
|
|
106
|
+
className={`pointer-events-none absolute inset-y-0 left-[var(--sp-small,0.375rem)] m-auto fill-(--icon-color) ${isSmall ? 'size-[var(--icon-small,0.9375rem)]' : 'size-[var(--icon-medium,1.125rem)]'}`}
|
|
107
|
+
/>
|
|
108
|
+
<input
|
|
109
|
+
type={type}
|
|
110
|
+
name={name}
|
|
111
|
+
value={dateValue}
|
|
112
|
+
max={maxDate}
|
|
113
|
+
min={minDate}
|
|
114
|
+
required={isRequired}
|
|
115
|
+
disabled={isDisabled}
|
|
116
|
+
onChange={(event) => handleDateValueChange(event.target.value)}
|
|
117
|
+
className={`w-[calc(var(--icon-medium,1.125rem)+var(--sp-small,0.375rem)*2)]! cursor-pointer px-[var(--sp-small,0.375rem)]! opacity-0 [&::-webkit-calendar-picker-indicator]:absolute [&::-webkit-calendar-picker-indicator]:left-0 [&::-webkit-calendar-picker-indicator]:h-full [&::-webkit-calendar-picker-indicator]:w-full [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-0 ${isSmall ? 'py-[var(--sp-min,0.1875rem)]!' : 'py-[var(--sp-medium,0.75rem)]!'}`}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
<input
|
|
121
|
+
value={dateObject.year}
|
|
122
|
+
placeholder="年"
|
|
123
|
+
maxLength={4}
|
|
124
|
+
type="text"
|
|
125
|
+
disabled={isDisabled}
|
|
126
|
+
onChange={(event) => handleObjectChange('year', event.target.value)}
|
|
127
|
+
className={`w-[calc(var(--sp-medium,0.75rem)*2+5ch)]! ${textInputClass}`}
|
|
128
|
+
/>
|
|
129
|
+
/
|
|
130
|
+
<input
|
|
131
|
+
value={dateObject.month}
|
|
132
|
+
placeholder="月"
|
|
133
|
+
maxLength={2}
|
|
134
|
+
type="text"
|
|
135
|
+
disabled={isDisabled}
|
|
136
|
+
onChange={(event) => handleObjectChange('month', event.target.value)}
|
|
137
|
+
className={`w-[calc(var(--sp-medium,0.75rem)*2+3ch)]! ${textInputClass}`}
|
|
138
|
+
/>
|
|
139
|
+
{type === 'date' && <span>/</span>}
|
|
140
|
+
{type === 'date' && (
|
|
141
|
+
<input
|
|
142
|
+
value={dateObject.day}
|
|
143
|
+
placeholder="日"
|
|
144
|
+
maxLength={2}
|
|
145
|
+
type="text"
|
|
146
|
+
disabled={isDisabled}
|
|
147
|
+
onChange={(event) => handleObjectChange('day', event.target.value)}
|
|
148
|
+
className={`w-[calc(var(--sp-medium,0.75rem)*2+3ch)]! ${textInputClass}`}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
<ErrorMessage errorMessages={errorMessage ? [errorMessage] : undefined} />
|
|
152
|
+
</InputBox>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties } from 'react'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import { DatePicker } from './DatePicker'
|
|
6
|
+
import { COLOR } from '../constants'
|
|
7
|
+
|
|
8
|
+
type DateRange = {
|
|
9
|
+
startDate: string
|
|
10
|
+
endDate: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
isDisabled?: boolean
|
|
15
|
+
onChange?: (newValue: DateRange) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function DateRangePicker({ isDisabled, onChange }: Props) {
|
|
19
|
+
const [startDate, setStartDate] = useState('')
|
|
20
|
+
const [endDate, setEndDate] = useState('')
|
|
21
|
+
|
|
22
|
+
const handleStartChange = (newValue: string) => {
|
|
23
|
+
setStartDate(newValue)
|
|
24
|
+
onChange?.({ startDate: newValue, endDate })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const handleEndChange = (newValue: string) => {
|
|
28
|
+
setEndDate(newValue)
|
|
29
|
+
onChange?.({ startDate, endDate: newValue })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const style = {
|
|
33
|
+
'--range-background-color': COLOR.white,
|
|
34
|
+
'--range-border-color': COLOR.gray,
|
|
35
|
+
} as CSSProperties
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
style={style}
|
|
40
|
+
className="flex bg-(--range-background-color) [&>*]:flex-auto [&>*]:rounded-none! [&>*:first-child]:rounded-s-[calc(var(--bv,0.375rem)/2)]! [&>*:last-child]:rounded-e-[calc(var(--bv,0.375rem)/2)]! [&>*:not(:last-child)]:border-e [&>*:not(:last-child)]:border-(--range-border-color)"
|
|
41
|
+
>
|
|
42
|
+
<DatePicker
|
|
43
|
+
value={startDate}
|
|
44
|
+
name="startDate"
|
|
45
|
+
isDisabled={isDisabled}
|
|
46
|
+
onChange={handleStartChange}
|
|
47
|
+
/>
|
|
48
|
+
<div className="flex flex-none! items-center px-[var(--bv,0.375rem)]">
|
|
49
|
+
〜
|
|
50
|
+
</div>
|
|
51
|
+
<DatePicker
|
|
52
|
+
value={endDate}
|
|
53
|
+
name="endDate"
|
|
54
|
+
isDisabled={isDisabled}
|
|
55
|
+
onChange={handleEndChange}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|