@geckou/ui-react 0.1.1 → 0.3.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/README.md +46 -5
- package/dist/components/BasicButton.d.ts +14 -0
- package/dist/components/BasicButton.js +57 -0
- package/dist/components/CheckBox.d.ts +13 -0
- package/dist/components/CheckBox.js +74 -0
- package/dist/components/CheckBoxes.d.ts +12 -0
- package/dist/components/CheckBoxes.js +68 -0
- package/dist/components/CheckButton.d.ts +13 -0
- package/dist/components/CheckButton.js +56 -0
- package/dist/components/DatePicker.d.ts +18 -0
- package/dist/components/DatePicker.js +131 -0
- package/dist/components/DateRangePicker.d.ts +22 -0
- package/dist/components/DateRangePicker.js +65 -0
- package/dist/components/DateSelector.d.ts +13 -0
- package/dist/components/DateSelector.js +129 -0
- package/dist/components/DropdownUi.d.ts +16 -0
- package/dist/components/DropdownUi.js +88 -0
- package/dist/components/ErrorMessage.d.ts +9 -0
- package/dist/components/ErrorMessage.js +21 -0
- package/dist/components/FileInput.d.ts +7 -0
- package/dist/components/FileInput.js +91 -0
- package/dist/components/InputBox.d.ts +11 -0
- package/dist/components/InputBox.js +75 -0
- package/dist/components/InputGroup.d.ts +6 -0
- package/dist/components/InputGroup.js +7 -0
- package/dist/components/LabeledCheckbox.d.ts +12 -0
- package/dist/components/LabeledCheckbox.js +46 -0
- package/dist/components/LabeledFieldset.d.ts +8 -0
- package/dist/components/LabeledFieldset.js +10 -0
- package/dist/components/LoadingSpinner.d.ts +1 -0
- package/dist/components/LoadingSpinner.js +26 -0
- package/dist/components/ModalBox.d.ts +12 -0
- package/dist/components/ModalBox.js +83 -0
- package/dist/components/PopupBox.d.ts +14 -0
- package/dist/components/PopupBox.js +41 -0
- package/dist/components/RadioButtons.d.ts +13 -0
- package/dist/components/RadioButtons.js +64 -0
- package/dist/components/SearchableSelectBox.d.ts +14 -0
- package/dist/components/SearchableSelectBox.js +61 -0
- package/dist/components/SelectBox.d.ts +16 -0
- package/dist/components/SelectBox.js +107 -0
- package/dist/components/SlideDownUi.d.ts +17 -0
- package/dist/components/SlideDownUi.js +86 -0
- package/dist/components/TabUI.d.ts +18 -0
- package/dist/components/TabUI.js +76 -0
- package/dist/components/TextArea.d.ts +18 -0
- package/dist/components/TextArea.js +75 -0
- package/dist/components/TextBox.d.ts +20 -0
- package/dist/components/TextBox.js +90 -0
- package/dist/components/ToggleButton.d.ts +15 -0
- package/dist/components/ToggleButton.js +92 -0
- package/dist/components/icons/BackupIcon.d.ts +5 -0
- package/dist/components/icons/BackupIcon.js +16 -0
- package/dist/components/icons/CalendarIcon.d.ts +5 -0
- package/dist/components/icons/CalendarIcon.js +15 -0
- package/dist/components/icons/CheckIcon.d.ts +1 -0
- package/dist/components/icons/CheckIcon.js +7 -0
- package/dist/components/icons/CloseIcon.d.ts +5 -0
- package/dist/components/icons/CloseIcon.js +16 -0
- package/dist/components/icons/KeyboardArrowDownIcon.d.ts +5 -0
- package/dist/components/icons/KeyboardArrowDownIcon.js +16 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/hooks/useFormValidation.d.ts +33 -0
- package/dist/hooks/useFormValidation.js +28 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +70 -0
- package/{src → dist}/styles/tokens.css +1 -1
- package/dist/types/index.d.ts +1 -0
- package/package.json +17 -12
- package/src/components/BasicButton.tsx +0 -81
- package/src/components/CheckBox.tsx +0 -87
- package/src/components/CheckBoxes.tsx +0 -93
- package/src/components/CheckButton.tsx +0 -66
- package/src/components/DatePicker.tsx +0 -154
- package/src/components/DateRangePicker.tsx +0 -59
- package/src/components/DateSelector.tsx +0 -162
- package/src/components/DropdownUi.tsx +0 -126
- package/src/components/ErrorMessage.tsx +0 -30
- package/src/components/FileInput.tsx +0 -93
- package/src/components/InputBox.tsx +0 -118
- package/src/components/InputGroup.tsx +0 -13
- package/src/components/LabeledCheckbox.tsx +0 -61
- package/src/components/LabeledFieldset.tsx +0 -18
- package/src/components/LoadingSpinner.tsx +0 -23
- package/src/components/ModalBox.tsx +0 -109
- package/src/components/PopupBox.tsx +0 -70
- package/src/components/RadioButtons.tsx +0 -94
- package/src/components/SearchableSelectBox.tsx +0 -108
- package/src/components/SelectBox.tsx +0 -152
- package/src/components/SlideDownUi.tsx +0 -123
- package/src/components/TabUI.tsx +0 -112
- package/src/components/TextArea.tsx +0 -102
- package/src/components/TextBox.tsx +0 -120
- package/src/components/ToggleButton.tsx +0 -102
- package/src/components/icons/BackupIcon.tsx +0 -16
- package/src/components/icons/CalendarIcon.tsx +0 -15
- package/src/components/icons/CheckIcon.tsx +0 -7
- package/src/components/icons/CloseIcon.tsx +0 -16
- package/src/components/icons/KeyboardArrowDownIcon.tsx +0 -16
- package/src/constants/index.ts +0 -2
- package/src/hooks/useFormValidation.ts +0 -69
- package/src/index.ts +0 -66
- package/src/types/index.ts +0 -23
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { CSSProperties, MouseEvent } from 'react'
|
|
4
|
-
import { useEffect, useState } from 'react'
|
|
5
|
-
import { daysInMonth, splitDate } from '@geckou/ui-core'
|
|
6
|
-
import { InputBox } from './InputBox'
|
|
7
|
-
import { KeyboardArrowDownIcon } from './icons/KeyboardArrowDownIcon'
|
|
8
|
-
import { COLOR } from '../constants'
|
|
9
|
-
|
|
10
|
-
type Birthday = {
|
|
11
|
-
year: string
|
|
12
|
-
month: string
|
|
13
|
-
day: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
type Props = {
|
|
17
|
-
name: string
|
|
18
|
-
value?: string
|
|
19
|
-
onChange?: (newValue: string) => void
|
|
20
|
-
isRequired?: boolean
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const EMPTY_BIRTHDAY: Birthday = { year: '', month: '', day: '' }
|
|
24
|
-
|
|
25
|
-
export function DateSelector({ name, value, onChange, isRequired }: Props) {
|
|
26
|
-
const [birthday, setBirthday] = useState<Birthday>(EMPTY_BIRTHDAY)
|
|
27
|
-
|
|
28
|
-
// Vue 版(@geckou/ui-vue)は value が空に戻されたときリセットされなかった
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
if (value) {
|
|
31
|
-
setBirthday(splitDate(value))
|
|
32
|
-
} else {
|
|
33
|
-
setBirthday(EMPTY_BIRTHDAY)
|
|
34
|
-
}
|
|
35
|
-
}, [value])
|
|
36
|
-
|
|
37
|
-
const today = new Date()
|
|
38
|
-
const maxYear = today.getFullYear() - 100
|
|
39
|
-
const currentYear = today.getFullYear() - 14
|
|
40
|
-
const yearsOptions = Array.from(
|
|
41
|
-
{ length: currentYear - maxYear + 1 },
|
|
42
|
-
(_, i) => (maxYear + i).toString(),
|
|
43
|
-
).map((year) => ({ label: year, value: year }))
|
|
44
|
-
|
|
45
|
-
const monthOptions = Array.from({ length: 12 }, (_, i) => {
|
|
46
|
-
const month = (i + 1).toString()
|
|
47
|
-
return { label: month, value: month.padStart(2, '0') }
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
// 年が未選択のときは閏年を含む最大日数になるよう 2000 年を使う
|
|
51
|
-
const getDaysInMonth = (year: string, month: string) =>
|
|
52
|
-
daysInMonth(Number(year) || 2000, Number(month))
|
|
53
|
-
|
|
54
|
-
const selectedMonthDays = getDaysInMonth(birthday.year, birthday.month)
|
|
55
|
-
const dayOptions = Array.from({ length: selectedMonthDays }, (_, i) => {
|
|
56
|
-
const day = (i + 1).toString()
|
|
57
|
-
return { label: day, value: day.padStart(2, '0') }
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
const emitBirthday = (next: Birthday) => {
|
|
61
|
-
setBirthday(next)
|
|
62
|
-
if (!next.year && !next.month && !next.day) onChange?.('')
|
|
63
|
-
else if (!next.year || !next.month || !next.day) return
|
|
64
|
-
else onChange?.(`${next.year}-${next.month}-${next.day}`)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const selectItem = (key: keyof Birthday, newValue: string) => {
|
|
68
|
-
const next = { ...birthday, [key]: newValue }
|
|
69
|
-
|
|
70
|
-
// 月・年を変えて日が存在しなくなった場合は、その月の末日に丸める
|
|
71
|
-
if (key !== 'day' && next.day && next.month) {
|
|
72
|
-
const maxDay = getDaysInMonth(next.year, next.month)
|
|
73
|
-
if (Number(next.day) > maxDay) next.day = String(maxDay).padStart(2, '0')
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
emitBirthday(next)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const openDropdown = (event: MouseEvent<HTMLDivElement>) => {
|
|
80
|
-
const target = event.currentTarget
|
|
81
|
-
const select = target.firstElementChild as HTMLSelectElement | null
|
|
82
|
-
if (select) select.click()
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const wrapperClass = 'relative w-max [&:has(select:focus)>svg]:rotate-180'
|
|
86
|
-
const iconClass =
|
|
87
|
-
'pointer-events-none absolute inset-y-0 right-[var(--sp-small,0.375rem)] m-auto size-[var(--icon-medium,1.125rem)] fill-(--icon-color)'
|
|
88
|
-
const selectClass =
|
|
89
|
-
'w-[calc(3ch+var(--sp-medium,0.75rem)*2+var(--icon-medium,1.125rem))]! p-[var(--sp-medium,0.75rem)]! pe-[calc(var(--sp-small,0.375rem)*2+var(--icon-medium,1.125rem))]! cursor-pointer'
|
|
90
|
-
const wrapperStyle = { '--icon-color': COLOR.blue } as CSSProperties
|
|
91
|
-
const deleteButtonStyle = { '--caution-color': COLOR.red } as CSSProperties
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<InputBox className="flex w-max items-center [&>*:not(:last-of-type)]:after:content-['/']">
|
|
95
|
-
<div className={wrapperClass} style={wrapperStyle} onClick={openDropdown}>
|
|
96
|
-
<select
|
|
97
|
-
value={birthday.year}
|
|
98
|
-
name={`${name}-year`}
|
|
99
|
-
required={isRequired}
|
|
100
|
-
onChange={(event) => selectItem('year', event.target.value)}
|
|
101
|
-
className="w-[calc(5ch+var(--sp-medium,0.75rem)*2+var(--icon-medium,1.125rem))]! cursor-pointer p-[var(--sp-medium,0.75rem)]! pe-[calc(var(--sp-small,0.375rem)*2+var(--icon-medium,1.125rem))]!"
|
|
102
|
-
>
|
|
103
|
-
<option disabled value="">
|
|
104
|
-
年
|
|
105
|
-
</option>
|
|
106
|
-
{yearsOptions.map((year) => (
|
|
107
|
-
<option key={year.value} value={year.value}>
|
|
108
|
-
{year.label}
|
|
109
|
-
</option>
|
|
110
|
-
))}
|
|
111
|
-
</select>
|
|
112
|
-
<KeyboardArrowDownIcon className={iconClass} />
|
|
113
|
-
</div>
|
|
114
|
-
<div className={wrapperClass} style={wrapperStyle} onClick={openDropdown}>
|
|
115
|
-
<select
|
|
116
|
-
value={birthday.month}
|
|
117
|
-
name={`${name}-month`}
|
|
118
|
-
required={isRequired}
|
|
119
|
-
onChange={(event) => selectItem('month', event.target.value)}
|
|
120
|
-
className={selectClass}
|
|
121
|
-
>
|
|
122
|
-
<option disabled value="">
|
|
123
|
-
月
|
|
124
|
-
</option>
|
|
125
|
-
{monthOptions.map((month) => (
|
|
126
|
-
<option key={month.value} value={month.value}>
|
|
127
|
-
{month.label}
|
|
128
|
-
</option>
|
|
129
|
-
))}
|
|
130
|
-
</select>
|
|
131
|
-
<KeyboardArrowDownIcon className={iconClass} />
|
|
132
|
-
</div>
|
|
133
|
-
<div className={wrapperClass} style={wrapperStyle} onClick={openDropdown}>
|
|
134
|
-
<select
|
|
135
|
-
value={birthday.day}
|
|
136
|
-
name={`${name}-day`}
|
|
137
|
-
required={isRequired}
|
|
138
|
-
onChange={(event) => selectItem('day', event.target.value)}
|
|
139
|
-
className={selectClass}
|
|
140
|
-
>
|
|
141
|
-
<option disabled value="">
|
|
142
|
-
日
|
|
143
|
-
</option>
|
|
144
|
-
{dayOptions.map((day) => (
|
|
145
|
-
<option key={day.value} value={day.value}>
|
|
146
|
-
{day.label}
|
|
147
|
-
</option>
|
|
148
|
-
))}
|
|
149
|
-
</select>
|
|
150
|
-
<KeyboardArrowDownIcon className={iconClass} />
|
|
151
|
-
</div>
|
|
152
|
-
<button
|
|
153
|
-
type="button"
|
|
154
|
-
style={deleteButtonStyle}
|
|
155
|
-
className="mx-[var(--sp-medium,0.75rem)] inline-flex cursor-pointer items-center gap-[var(--sp-min,0.1875rem)] text-[length:var(--fs-small,0.6875rem)] text-(--caution-color)"
|
|
156
|
-
onClick={() => emitBirthday(EMPTY_BIRTHDAY)}
|
|
157
|
-
>
|
|
158
|
-
削除
|
|
159
|
-
</button>
|
|
160
|
-
</InputBox>
|
|
161
|
-
)
|
|
162
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { CSSProperties, ReactNode, Ref } from 'react'
|
|
4
|
-
import { useEffect, useImperativeHandle, useRef, useState } from 'react'
|
|
5
|
-
import { KeyboardArrowDownIcon } from './icons/KeyboardArrowDownIcon'
|
|
6
|
-
import { COLOR } from '../constants'
|
|
7
|
-
|
|
8
|
-
// Vue 版(@geckou/ui-vue)の defineExpose({ isContentsOpened }) 相当
|
|
9
|
-
export type DropdownUiHandle = {
|
|
10
|
-
isContentsOpened: () => boolean
|
|
11
|
-
close: () => void
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type Props = {
|
|
15
|
-
isHiddenArrow?: boolean
|
|
16
|
-
contentAlignment?: 'left' | 'center' | 'right'
|
|
17
|
-
isDisabled?: boolean
|
|
18
|
-
contentsWidth?: string
|
|
19
|
-
trigger: ReactNode
|
|
20
|
-
contents?: ReactNode
|
|
21
|
-
ref?: Ref<DropdownUiHandle>
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function DropdownUi({
|
|
25
|
-
isHiddenArrow = false,
|
|
26
|
-
contentAlignment = 'left',
|
|
27
|
-
isDisabled = false,
|
|
28
|
-
contentsWidth = 'auto',
|
|
29
|
-
trigger,
|
|
30
|
-
contents,
|
|
31
|
-
ref,
|
|
32
|
-
}: Props) {
|
|
33
|
-
const [isContentsOpened, setIsContentsOpened] = useState(false)
|
|
34
|
-
const [contentsHeight, setContentsHeight] = useState(0)
|
|
35
|
-
const rootRef = useRef<HTMLDivElement>(null)
|
|
36
|
-
const contentsRef = useRef<HTMLDivElement>(null)
|
|
37
|
-
|
|
38
|
-
const toggleBox = () => setIsContentsOpened((current) => !current)
|
|
39
|
-
const closeDropDown = () => setIsContentsOpened(false)
|
|
40
|
-
|
|
41
|
-
useImperativeHandle(ref, () => ({
|
|
42
|
-
isContentsOpened: () => isContentsOpened,
|
|
43
|
-
close: closeDropDown,
|
|
44
|
-
}))
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
const handleClickOutside = (event: PointerEvent) => {
|
|
48
|
-
const root = rootRef.current
|
|
49
|
-
if (root && !root.contains(event.target as Node)) closeDropDown()
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
document.addEventListener('pointerdown', handleClickOutside)
|
|
53
|
-
return () => document.removeEventListener('pointerdown', handleClickOutside)
|
|
54
|
-
}, [])
|
|
55
|
-
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
const contentsElement = contentsRef.current
|
|
58
|
-
if (!contentsElement) return
|
|
59
|
-
|
|
60
|
-
const updateContentsHeight = () =>
|
|
61
|
-
setContentsHeight(contentsElement.clientHeight)
|
|
62
|
-
|
|
63
|
-
updateContentsHeight()
|
|
64
|
-
const observer = new ResizeObserver(updateContentsHeight)
|
|
65
|
-
observer.observe(contentsElement)
|
|
66
|
-
return () => observer.disconnect()
|
|
67
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- サイズ変化は ResizeObserver が検知するため、contents の有無が変わったときだけ再登録する
|
|
68
|
-
}, [Boolean(contents)])
|
|
69
|
-
|
|
70
|
-
const isInactive = isDisabled || !contents
|
|
71
|
-
|
|
72
|
-
const style = {
|
|
73
|
-
'--trigger-color': isInactive ? COLOR.black : COLOR.blue,
|
|
74
|
-
} as CSSProperties
|
|
75
|
-
|
|
76
|
-
const contentsStyle: CSSProperties = {
|
|
77
|
-
boxShadow: isContentsOpened ? '0 0 6px #33333333' : 'none',
|
|
78
|
-
blockSize: isContentsOpened ? `${contentsHeight}px` : 0,
|
|
79
|
-
inlineSize: contentsWidth,
|
|
80
|
-
right:
|
|
81
|
-
contentAlignment === 'right'
|
|
82
|
-
? 'calc(var(--bv, 0.375rem) * -0.5)'
|
|
83
|
-
: 'auto',
|
|
84
|
-
left:
|
|
85
|
-
contentAlignment === 'left' ? 'calc(var(--bv, 0.375rem) * -0.5)' : 'auto',
|
|
86
|
-
zIndex: 2,
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<div ref={rootRef} className="relative">
|
|
91
|
-
<button
|
|
92
|
-
type="button"
|
|
93
|
-
disabled={isDisabled}
|
|
94
|
-
aria-expanded={isContentsOpened}
|
|
95
|
-
style={{ ...style, cursor: isInactive ? 'auto' : 'pointer' }}
|
|
96
|
-
className="flex size-full items-center gap-[var(--sp-small,0.375rem)] text-(--trigger-color)"
|
|
97
|
-
onClick={(event) => {
|
|
98
|
-
event.preventDefault()
|
|
99
|
-
toggleBox()
|
|
100
|
-
}}
|
|
101
|
-
>
|
|
102
|
-
{trigger}
|
|
103
|
-
{!(isHiddenArrow || !contents || isDisabled) && (
|
|
104
|
-
<KeyboardArrowDownIcon
|
|
105
|
-
className={`size-[var(--icon-small,0.9375rem)] flex-none transition-all duration-100 ${isContentsOpened ? 'rotate-180' : ''}`}
|
|
106
|
-
/>
|
|
107
|
-
)}
|
|
108
|
-
</button>
|
|
109
|
-
{contents && (
|
|
110
|
-
<div
|
|
111
|
-
style={contentsStyle}
|
|
112
|
-
inert={!isContentsOpened}
|
|
113
|
-
className="absolute top-[calc(100%-var(--sp-min,0.1875rem))] cursor-pointer overflow-hidden rounded-[var(--radius-small,0.1875rem)] transition-[block-size] duration-100"
|
|
114
|
-
>
|
|
115
|
-
<div
|
|
116
|
-
ref={contentsRef}
|
|
117
|
-
className="max-h-[calc(var(--bv,0.375rem)*56)] min-w-[calc(var(--bv,0.375rem)*20)] overflow-auto bg-white"
|
|
118
|
-
onClick={closeDropDown}
|
|
119
|
-
>
|
|
120
|
-
{contents}
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
)}
|
|
124
|
-
</div>
|
|
125
|
-
)
|
|
126
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties } from 'react'
|
|
2
|
-
import { COLOR } from '../constants'
|
|
3
|
-
|
|
4
|
-
type Props = {
|
|
5
|
-
errorMessages?: string[]
|
|
6
|
-
cssStyle?: {
|
|
7
|
-
textColor?: string
|
|
8
|
-
backgroundColor?: string
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function ErrorMessage({ errorMessages, cssStyle }: Props) {
|
|
13
|
-
if (!errorMessages || !errorMessages.length) return null
|
|
14
|
-
|
|
15
|
-
const style = {
|
|
16
|
-
'--error-text-color': cssStyle?.textColor || COLOR.white,
|
|
17
|
-
'--error-background-color': cssStyle?.backgroundColor || COLOR.red,
|
|
18
|
-
} as CSSProperties
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<div
|
|
22
|
-
className="absolute top-[calc(100%+0.25rem)] flex w-max flex-col gap-2 rounded-sm bg-(--error-background-color) px-3 py-2 text-sm leading-none text-(--error-text-color) shadow-[0_0_0.5rem_0.25rem_#33333322] before:absolute before:bottom-[calc(100%-1px)] before:left-[0.85rem] before:block before:h-[0.53rem] before:w-[0.85rem] before:bg-(--error-background-color) before:content-[''] before:[clip-path:polygon(50%_0%,100%_100%,0%_100%)]"
|
|
23
|
-
style={style}
|
|
24
|
-
>
|
|
25
|
-
{errorMessages.map((message) => (
|
|
26
|
-
<span key={message}>{message}</span>
|
|
27
|
-
))}
|
|
28
|
-
</div>
|
|
29
|
-
)
|
|
30
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { CSSProperties, DragEvent } from 'react'
|
|
4
|
-
import { BackupIcon } from './icons/BackupIcon'
|
|
5
|
-
import { CloseIcon } from './icons/CloseIcon'
|
|
6
|
-
import { COLOR } from '../constants'
|
|
7
|
-
|
|
8
|
-
type Props = {
|
|
9
|
-
value: File[]
|
|
10
|
-
onChange?: (files: File[]) => void
|
|
11
|
-
accept?: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function FileInput({ value, onChange, accept = '' }: Props) {
|
|
15
|
-
const uploadFiles = value ?? []
|
|
16
|
-
|
|
17
|
-
const addFiles = (newFiles: FileList | null) => {
|
|
18
|
-
if (!newFiles || !newFiles.length) return
|
|
19
|
-
onChange?.([...uploadFiles, ...Array.from(newFiles)])
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const removeFile = (target: File) => {
|
|
23
|
-
onChange?.(uploadFiles.filter((file) => file !== target))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
|
|
27
|
-
event.preventDefault()
|
|
28
|
-
addFiles(event.dataTransfer.files)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const style = {
|
|
32
|
-
'--link-color': COLOR.blue,
|
|
33
|
-
'--caution-color': COLOR.red,
|
|
34
|
-
} as CSSProperties
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div
|
|
38
|
-
style={style}
|
|
39
|
-
className="w-full rounded-[var(--radius-small,0.1875rem)] border border-dashed border-[var(--link-color,#1c4ac9)] p-[var(--sp-medium,0.75rem)]"
|
|
40
|
-
>
|
|
41
|
-
<div
|
|
42
|
-
className="flex h-full flex-col items-center justify-center gap-[var(--sp-small,0.375rem)] [&>span]:inline-block [&>span]:p-[var(--sp-small,0.375rem)]"
|
|
43
|
-
onDragOver={(event) => event.preventDefault()}
|
|
44
|
-
onDrop={handleDrop}
|
|
45
|
-
>
|
|
46
|
-
<span>ファイルをドロップしてアップロード</span>
|
|
47
|
-
<span>または</span>
|
|
48
|
-
<label className="relative inline-flex cursor-pointer items-center gap-[var(--sp-small,0.375rem)] rounded-[var(--radius-small,0.1875rem)] bg-transparent p-[var(--sp-small,0.375rem)] font-medium text-[var(--link-color,#1c4ac9)] transition-colors duration-100 hover:bg-[var(--hover-color,#EEF7FB)] has-[input:focus-visible]:outline-2 has-[input:focus-visible]:outline-offset-2 has-[input:focus-visible]:outline-[var(--link-color,#1c4ac9)]">
|
|
49
|
-
<BackupIcon className="size-[var(--icon-small,0.9375rem)]" />
|
|
50
|
-
<span>ファイルを選択</span>
|
|
51
|
-
<input
|
|
52
|
-
type="file"
|
|
53
|
-
multiple
|
|
54
|
-
accept={accept}
|
|
55
|
-
className="sr-only"
|
|
56
|
-
onChange={(event) => {
|
|
57
|
-
addFiles(event.target.files)
|
|
58
|
-
event.target.value = ''
|
|
59
|
-
}}
|
|
60
|
-
/>
|
|
61
|
-
</label>
|
|
62
|
-
</div>
|
|
63
|
-
{uploadFiles.length > 0 && (
|
|
64
|
-
<div className="mt-[var(--sp-medium,0.75rem)] flex flex-col items-center gap-[var(--sp-small,0.375rem)] bg-[var(--light-blue,#d2e4f7)] p-[var(--sp-medium,0.75rem)] [&>*:last-child]:mt-[var(--sp-small,0.375rem)]">
|
|
65
|
-
{uploadFiles.map((file, index) => (
|
|
66
|
-
<span
|
|
67
|
-
key={`${file.name}_${index}`}
|
|
68
|
-
className="inline-flex items-center gap-[var(--sp-min,0.1875rem)]"
|
|
69
|
-
>
|
|
70
|
-
{file.name}
|
|
71
|
-
<button
|
|
72
|
-
type="button"
|
|
73
|
-
aria-label={`${file.name} を削除`}
|
|
74
|
-
className="inline-flex cursor-pointer items-center"
|
|
75
|
-
onClick={() => removeFile(file)}
|
|
76
|
-
>
|
|
77
|
-
<CloseIcon className="size-[var(--icon-small,0.9375rem)] text-[var(--caution-color,#aa0000)]" />
|
|
78
|
-
</button>
|
|
79
|
-
</span>
|
|
80
|
-
))}
|
|
81
|
-
<button
|
|
82
|
-
type="button"
|
|
83
|
-
className="inline-flex cursor-pointer items-center gap-[var(--sp-min,0.1875rem)] rounded-[var(--radius-small,0.1875rem)] border border-[var(--caution-color,#aa0000)] bg-white px-[var(--sp-medium,0.75rem)] py-[var(--sp-small,0.375rem)] text-[length:var(--fs-small,0.6875rem)] text-[var(--caution-color,#aa0000)]"
|
|
84
|
-
onClick={() => onChange?.([])}
|
|
85
|
-
>
|
|
86
|
-
<CloseIcon className="size-[var(--icon-min,0.75rem)]" />
|
|
87
|
-
全て削除
|
|
88
|
-
</button>
|
|
89
|
-
</div>
|
|
90
|
-
)}
|
|
91
|
-
</div>
|
|
92
|
-
)
|
|
93
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { CSSProperties, ReactNode } from 'react'
|
|
4
|
-
import type {
|
|
5
|
-
StateVariation,
|
|
6
|
-
InputBoxStyle,
|
|
7
|
-
InputBoxStyleForEachStatus,
|
|
8
|
-
} from '../types'
|
|
9
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
10
|
-
import { INPUT_BOX_DEFAULT_STYLES } from '../constants'
|
|
11
|
-
|
|
12
|
-
type Props = {
|
|
13
|
-
cssStyle?: InputBoxStyleForEachStatus
|
|
14
|
-
isErrored?: boolean
|
|
15
|
-
isDisabled?: boolean
|
|
16
|
-
className?: string
|
|
17
|
-
children: ReactNode
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function InputBox({
|
|
21
|
-
cssStyle,
|
|
22
|
-
isErrored,
|
|
23
|
-
isDisabled,
|
|
24
|
-
className,
|
|
25
|
-
children,
|
|
26
|
-
}: Props) {
|
|
27
|
-
const inputBoxRef = useRef<HTMLDivElement>(null)
|
|
28
|
-
const [currentState, setCurrentState] = useState<StateVariation>('default')
|
|
29
|
-
|
|
30
|
-
const isErroredRef = useRef(isErrored)
|
|
31
|
-
isErroredRef.current = isErrored
|
|
32
|
-
|
|
33
|
-
const updateState = useCallback(() => {
|
|
34
|
-
const el = inputBoxRef.current?.querySelector('input, textarea, select')
|
|
35
|
-
|
|
36
|
-
if (!el) return setCurrentState('default')
|
|
37
|
-
if (el.matches(':disabled')) return setCurrentState('disabled')
|
|
38
|
-
if (el.matches(':focus')) return setCurrentState('focus')
|
|
39
|
-
|
|
40
|
-
if (el.tagName.toLowerCase() === 'select') {
|
|
41
|
-
const select = el as HTMLSelectElement
|
|
42
|
-
return setCurrentState(
|
|
43
|
-
select.required && !select.value ? 'error' : 'valid',
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (isErroredRef.current || el.matches(':invalid'))
|
|
48
|
-
{return setCurrentState('error')}
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
el.matches(':valid') &&
|
|
52
|
-
el.matches(':not(:placeholder-shown)') &&
|
|
53
|
-
el.matches(':not(:invalid)')
|
|
54
|
-
) {
|
|
55
|
-
return setCurrentState('valid')
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
setCurrentState('default')
|
|
59
|
-
}, [])
|
|
60
|
-
|
|
61
|
-
useEffect(() => {
|
|
62
|
-
const inputBox = inputBoxRef.current
|
|
63
|
-
if (!inputBox) return
|
|
64
|
-
|
|
65
|
-
const observer = new MutationObserver(updateState)
|
|
66
|
-
observer.observe(inputBox, { childList: true, subtree: true })
|
|
67
|
-
inputBox.addEventListener('focusin', updateState, true)
|
|
68
|
-
inputBox.addEventListener('blur', updateState, true)
|
|
69
|
-
|
|
70
|
-
return () => {
|
|
71
|
-
observer.disconnect()
|
|
72
|
-
inputBox.removeEventListener('focusin', updateState, true)
|
|
73
|
-
inputBox.removeEventListener('blur', updateState, true)
|
|
74
|
-
}
|
|
75
|
-
}, [updateState])
|
|
76
|
-
|
|
77
|
-
// Vue 版(@geckou/ui-vue)はマウント時に状態判定せず、初回の focus / blur まで default を維持する
|
|
78
|
-
const isFirstRender = useRef(true)
|
|
79
|
-
const prevDisabled = useRef(isDisabled)
|
|
80
|
-
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
const isDisabledChanged = prevDisabled.current !== isDisabled
|
|
83
|
-
prevDisabled.current = isDisabled
|
|
84
|
-
|
|
85
|
-
if (isFirstRender.current) {
|
|
86
|
-
isFirstRender.current = false
|
|
87
|
-
if (isDisabled) setCurrentState('disabled')
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (isDisabledChanged)
|
|
92
|
-
{return setCurrentState(isDisabled ? 'disabled' : 'default')}
|
|
93
|
-
|
|
94
|
-
updateState()
|
|
95
|
-
}, [isDisabled, isErrored, updateState])
|
|
96
|
-
|
|
97
|
-
const cssStylesUsed = { ...INPUT_BOX_DEFAULT_STYLES, ...cssStyle }
|
|
98
|
-
const currentCssStyle: InputBoxStyle =
|
|
99
|
-
cssStylesUsed[currentState] ?? cssStylesUsed.default
|
|
100
|
-
|
|
101
|
-
const style = {
|
|
102
|
-
'--text-color': currentCssStyle.textColor,
|
|
103
|
-
'--placeholder-color': currentCssStyle.placeholderColor,
|
|
104
|
-
'--background-color': currentCssStyle.backgroundColor,
|
|
105
|
-
'--radius-size': currentCssStyle.border?.radius || '.25rem',
|
|
106
|
-
'--input-box-shadow': `0 0 0 ${currentCssStyle.border?.size || '1px'} ${currentCssStyle.border?.color} inset, ${currentCssStyle.boxShadow || '0 0 0 0 rgba(0, 0, 0, 0)'}`,
|
|
107
|
-
} as CSSProperties
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<div
|
|
111
|
-
ref={inputBoxRef}
|
|
112
|
-
className={`relative inline-flex max-w-full rounded-(--radius-size) bg-(--background-color) text-(--text-color) shadow-(--input-box-shadow) has-[:disabled]:pointer-events-none has-[:disabled]:before:pointer-events-auto has-[:disabled]:before:absolute has-[:disabled]:before:inset-0 has-[:disabled]:before:cursor-not-allowed has-[:disabled]:before:content-[''] [&_*::placeholder]:font-normal [&_*::placeholder]:text-(--placeholder-color) [&_*:focus]:outline-none [&_:is(input,textarea,select)]:w-full [&_:is(input,textarea,select)]:appearance-none [&_:is(input,textarea,select)]:border-none [&_:is(input,textarea,select)]:bg-transparent [&_:is(input,textarea,select)]:p-4 [&_:is(input,textarea,select)]:outline-none ${className || ''}`}
|
|
113
|
-
style={style}
|
|
114
|
-
>
|
|
115
|
-
{children}
|
|
116
|
-
</div>
|
|
117
|
-
)
|
|
118
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react'
|
|
2
|
-
|
|
3
|
-
type Props = {
|
|
4
|
-
children: ReactNode
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function InputGroup({ children }: Props) {
|
|
8
|
-
return (
|
|
9
|
-
<div className="inline-flex gap-px max-md:flex-wrap [&>*]:flex-auto [&>*]:rounded-none [&>*:first-child:not(:only-child)]:rounded-l-[var(--radius-small,0.1875rem)] [&>*:last-child:not(:only-child)]:rounded-r-[var(--radius-small,0.1875rem)] [&>button]:flex-none">
|
|
10
|
-
{children}
|
|
11
|
-
</div>
|
|
12
|
-
)
|
|
13
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { CSSProperties } from 'react'
|
|
4
|
-
import type { CheckBoxStyleForEachStatus } from '../types'
|
|
5
|
-
import { CheckBox } from './CheckBox'
|
|
6
|
-
import { COLOR } from '../constants'
|
|
7
|
-
|
|
8
|
-
type Props = {
|
|
9
|
-
name: string
|
|
10
|
-
label: string
|
|
11
|
-
checked?: boolean
|
|
12
|
-
onChange?: (newValue: boolean) => void
|
|
13
|
-
isDisabled?: boolean
|
|
14
|
-
cssStyle?: CheckBoxStyleForEachStatus
|
|
15
|
-
isDisableAnimation?: boolean
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function LabeledCheckbox({
|
|
19
|
-
name,
|
|
20
|
-
label,
|
|
21
|
-
checked,
|
|
22
|
-
onChange,
|
|
23
|
-
isDisabled,
|
|
24
|
-
cssStyle,
|
|
25
|
-
isDisableAnimation,
|
|
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.black,
|
|
32
|
-
backgroundColor: isDisabled ? COLOR.darkGray : COLOR.blue,
|
|
33
|
-
...(baseStyle ?? {}),
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const style = {
|
|
37
|
-
'--text-color': currentCssStyle.textColor,
|
|
38
|
-
'--checked-color': currentCssStyle.backgroundColor,
|
|
39
|
-
} as CSSProperties
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<label className="inline-flex cursor-pointer items-center gap-[.5em] has-[input:disabled]:cursor-auto">
|
|
43
|
-
<span className="pointer-events-none">
|
|
44
|
-
<CheckBox
|
|
45
|
-
name={name}
|
|
46
|
-
checked={isChecked}
|
|
47
|
-
onChange={onChange}
|
|
48
|
-
isDisabled={isDisabled}
|
|
49
|
-
cssStyle={cssStyle}
|
|
50
|
-
isDisableAnimation={isDisableAnimation}
|
|
51
|
-
/>
|
|
52
|
-
</span>
|
|
53
|
-
<span
|
|
54
|
-
style={style}
|
|
55
|
-
className={isChecked ? 'text-(--text-color)' : 'text-(--checked-color)'}
|
|
56
|
-
>
|
|
57
|
-
{label}
|
|
58
|
-
</span>
|
|
59
|
-
</label>
|
|
60
|
-
)
|
|
61
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react'
|
|
2
|
-
|
|
3
|
-
type Props = {
|
|
4
|
-
isDisabled?: boolean
|
|
5
|
-
label?: ReactNode
|
|
6
|
-
children: ReactNode
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function LabeledFieldset({ isDisabled, label, children }: Props) {
|
|
10
|
-
return (
|
|
11
|
-
<div className="grid gap-2">
|
|
12
|
-
<fieldset className="contents" disabled={isDisabled}>
|
|
13
|
-
{label && <legend className="flex items-center">{label}</legend>}
|
|
14
|
-
<div className="[&>*]:w-full">{children}</div>
|
|
15
|
-
</fieldset>
|
|
16
|
-
</div>
|
|
17
|
-
)
|
|
18
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export function LoadingSpinner() {
|
|
2
|
-
return (
|
|
3
|
-
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
4
|
-
<g>
|
|
5
|
-
<circle cx="12" cy="2.5" r="1.5" opacity=".14" />
|
|
6
|
-
<circle cx="16.75" cy="3.77" r="1.5" opacity=".29" />
|
|
7
|
-
<circle cx="20.23" cy="7.25" r="1.5" opacity=".43" />
|
|
8
|
-
<circle cx="21.50" cy="12.00" r="1.5" opacity=".57" />
|
|
9
|
-
<circle cx="20.23" cy="16.75" r="1.5" opacity=".71" />
|
|
10
|
-
<circle cx="16.75" cy="20.23" r="1.5" opacity=".86" />
|
|
11
|
-
<circle cx="12" cy="21.5" r="1.5" />
|
|
12
|
-
<animateTransform
|
|
13
|
-
attributeName="transform"
|
|
14
|
-
type="rotate"
|
|
15
|
-
calcMode="discrete"
|
|
16
|
-
dur="0.75s"
|
|
17
|
-
values="0 12 12;30 12 12;60 12 12;90 12 12;120 12 12;150 12 12;180 12 12;210 12 12;240 12 12;270 12 12;300 12 12;330 12 12;360 12 12"
|
|
18
|
-
repeatCount="indefinite"
|
|
19
|
-
/>
|
|
20
|
-
</g>
|
|
21
|
-
</svg>
|
|
22
|
-
)
|
|
23
|
-
}
|