@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.
Files changed (103) hide show
  1. package/README.md +46 -5
  2. package/dist/components/BasicButton.d.ts +14 -0
  3. package/dist/components/BasicButton.js +57 -0
  4. package/dist/components/CheckBox.d.ts +13 -0
  5. package/dist/components/CheckBox.js +74 -0
  6. package/dist/components/CheckBoxes.d.ts +12 -0
  7. package/dist/components/CheckBoxes.js +68 -0
  8. package/dist/components/CheckButton.d.ts +13 -0
  9. package/dist/components/CheckButton.js +56 -0
  10. package/dist/components/DatePicker.d.ts +18 -0
  11. package/dist/components/DatePicker.js +131 -0
  12. package/dist/components/DateRangePicker.d.ts +22 -0
  13. package/dist/components/DateRangePicker.js +65 -0
  14. package/dist/components/DateSelector.d.ts +13 -0
  15. package/dist/components/DateSelector.js +129 -0
  16. package/dist/components/DropdownUi.d.ts +16 -0
  17. package/dist/components/DropdownUi.js +88 -0
  18. package/dist/components/ErrorMessage.d.ts +9 -0
  19. package/dist/components/ErrorMessage.js +21 -0
  20. package/dist/components/FileInput.d.ts +7 -0
  21. package/dist/components/FileInput.js +91 -0
  22. package/dist/components/InputBox.d.ts +11 -0
  23. package/dist/components/InputBox.js +75 -0
  24. package/dist/components/InputGroup.d.ts +6 -0
  25. package/dist/components/InputGroup.js +7 -0
  26. package/dist/components/LabeledCheckbox.d.ts +12 -0
  27. package/dist/components/LabeledCheckbox.js +46 -0
  28. package/dist/components/LabeledFieldset.d.ts +8 -0
  29. package/dist/components/LabeledFieldset.js +10 -0
  30. package/dist/components/LoadingSpinner.d.ts +1 -0
  31. package/dist/components/LoadingSpinner.js +26 -0
  32. package/dist/components/ModalBox.d.ts +12 -0
  33. package/dist/components/ModalBox.js +83 -0
  34. package/dist/components/PopupBox.d.ts +14 -0
  35. package/dist/components/PopupBox.js +41 -0
  36. package/dist/components/RadioButtons.d.ts +13 -0
  37. package/dist/components/RadioButtons.js +64 -0
  38. package/dist/components/SearchableSelectBox.d.ts +14 -0
  39. package/dist/components/SearchableSelectBox.js +61 -0
  40. package/dist/components/SelectBox.d.ts +16 -0
  41. package/dist/components/SelectBox.js +107 -0
  42. package/dist/components/SlideDownUi.d.ts +17 -0
  43. package/dist/components/SlideDownUi.js +86 -0
  44. package/dist/components/TabUI.d.ts +18 -0
  45. package/dist/components/TabUI.js +76 -0
  46. package/dist/components/TextArea.d.ts +18 -0
  47. package/dist/components/TextArea.js +75 -0
  48. package/dist/components/TextBox.d.ts +20 -0
  49. package/dist/components/TextBox.js +90 -0
  50. package/dist/components/ToggleButton.d.ts +15 -0
  51. package/dist/components/ToggleButton.js +92 -0
  52. package/dist/components/icons/BackupIcon.d.ts +5 -0
  53. package/dist/components/icons/BackupIcon.js +16 -0
  54. package/dist/components/icons/CalendarIcon.d.ts +5 -0
  55. package/dist/components/icons/CalendarIcon.js +15 -0
  56. package/dist/components/icons/CheckIcon.d.ts +1 -0
  57. package/dist/components/icons/CheckIcon.js +7 -0
  58. package/dist/components/icons/CloseIcon.d.ts +5 -0
  59. package/dist/components/icons/CloseIcon.js +16 -0
  60. package/dist/components/icons/KeyboardArrowDownIcon.d.ts +5 -0
  61. package/dist/components/icons/KeyboardArrowDownIcon.js +16 -0
  62. package/dist/constants/index.d.ts +1 -0
  63. package/dist/hooks/useFormValidation.d.ts +33 -0
  64. package/dist/hooks/useFormValidation.js +28 -0
  65. package/dist/index.d.ts +38 -0
  66. package/dist/index.js +70 -0
  67. package/{src → dist}/styles/tokens.css +1 -1
  68. package/dist/types/index.d.ts +1 -0
  69. package/package.json +17 -12
  70. package/src/components/BasicButton.tsx +0 -81
  71. package/src/components/CheckBox.tsx +0 -87
  72. package/src/components/CheckBoxes.tsx +0 -93
  73. package/src/components/CheckButton.tsx +0 -66
  74. package/src/components/DatePicker.tsx +0 -154
  75. package/src/components/DateRangePicker.tsx +0 -59
  76. package/src/components/DateSelector.tsx +0 -162
  77. package/src/components/DropdownUi.tsx +0 -126
  78. package/src/components/ErrorMessage.tsx +0 -30
  79. package/src/components/FileInput.tsx +0 -93
  80. package/src/components/InputBox.tsx +0 -118
  81. package/src/components/InputGroup.tsx +0 -13
  82. package/src/components/LabeledCheckbox.tsx +0 -61
  83. package/src/components/LabeledFieldset.tsx +0 -18
  84. package/src/components/LoadingSpinner.tsx +0 -23
  85. package/src/components/ModalBox.tsx +0 -109
  86. package/src/components/PopupBox.tsx +0 -70
  87. package/src/components/RadioButtons.tsx +0 -94
  88. package/src/components/SearchableSelectBox.tsx +0 -108
  89. package/src/components/SelectBox.tsx +0 -152
  90. package/src/components/SlideDownUi.tsx +0 -123
  91. package/src/components/TabUI.tsx +0 -112
  92. package/src/components/TextArea.tsx +0 -102
  93. package/src/components/TextBox.tsx +0 -120
  94. package/src/components/ToggleButton.tsx +0 -102
  95. package/src/components/icons/BackupIcon.tsx +0 -16
  96. package/src/components/icons/CalendarIcon.tsx +0 -15
  97. package/src/components/icons/CheckIcon.tsx +0 -7
  98. package/src/components/icons/CloseIcon.tsx +0 -16
  99. package/src/components/icons/KeyboardArrowDownIcon.tsx +0 -16
  100. package/src/constants/index.ts +0 -2
  101. package/src/hooks/useFormValidation.ts +0 -69
  102. package/src/index.ts +0 -66
  103. 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
- }