@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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/package.json +52 -0
  4. package/src/components/BasicButton.tsx +81 -0
  5. package/src/components/CheckBox.tsx +87 -0
  6. package/src/components/CheckBoxes.tsx +93 -0
  7. package/src/components/CheckButton.tsx +66 -0
  8. package/src/components/DatePicker.tsx +154 -0
  9. package/src/components/DateRangePicker.tsx +59 -0
  10. package/src/components/DateSelector.tsx +162 -0
  11. package/src/components/DropdownUi.tsx +126 -0
  12. package/src/components/ErrorMessage.tsx +30 -0
  13. package/src/components/FileInput.tsx +93 -0
  14. package/src/components/InputBox.tsx +118 -0
  15. package/src/components/InputGroup.tsx +13 -0
  16. package/src/components/LabeledCheckbox.tsx +61 -0
  17. package/src/components/LabeledFieldset.tsx +18 -0
  18. package/src/components/LoadingSpinner.tsx +23 -0
  19. package/src/components/ModalBox.tsx +109 -0
  20. package/src/components/PopupBox.tsx +70 -0
  21. package/src/components/RadioButtons.tsx +94 -0
  22. package/src/components/SearchableSelectBox.tsx +108 -0
  23. package/src/components/SelectBox.tsx +152 -0
  24. package/src/components/SlideDownUi.tsx +123 -0
  25. package/src/components/TabUI.tsx +112 -0
  26. package/src/components/TextArea.tsx +102 -0
  27. package/src/components/TextBox.tsx +120 -0
  28. package/src/components/ToggleButton.tsx +102 -0
  29. package/src/components/icons/BackupIcon.tsx +16 -0
  30. package/src/components/icons/CalendarIcon.tsx +15 -0
  31. package/src/components/icons/CheckIcon.tsx +7 -0
  32. package/src/components/icons/CloseIcon.tsx +16 -0
  33. package/src/components/icons/KeyboardArrowDownIcon.tsx +16 -0
  34. package/src/constants/index.ts +2 -0
  35. package/src/hooks/useFormValidation.ts +69 -0
  36. package/src/index.ts +66 -0
  37. package/src/styles/tokens.css +61 -0
  38. package/src/types/index.ts +23 -0
@@ -0,0 +1,112 @@
1
+ 'use client'
2
+
3
+ import type { CSSProperties, KeyboardEvent, ReactNode } from 'react'
4
+ import { useId, useRef, useState } from 'react'
5
+ import { COLOR } from '../constants'
6
+
7
+ type Tab = {
8
+ key: string
9
+ label: string
10
+ }
11
+
12
+ type Props = {
13
+ tabs: Tab[]
14
+ color?: {
15
+ active: string
16
+ background: string
17
+ text: string
18
+ }
19
+ type?: 'tab' | 'button' | 'border'
20
+ initialIndex?: number
21
+ tabSlots?: Record<string, ReactNode>
22
+ panelSlots?: Record<string, ReactNode>
23
+ }
24
+
25
+ export function TabUI({
26
+ tabs,
27
+ color,
28
+ initialIndex = 0,
29
+ tabSlots,
30
+ panelSlots,
31
+ }: Props) {
32
+ const [activeTab, setActiveTab] = useState(
33
+ () => tabs[initialIndex]?.key ?? tabs[0]?.key ?? '',
34
+ )
35
+ const tabRefs = useRef<Array<HTMLButtonElement | null>>([])
36
+ // 複数インスタンス設置時の DOM id 重複を避ける
37
+ const uid = useId()
38
+ const tabId = (key: string) => `${uid}-tab-${key}`
39
+ const panelId = (key: string) => `${uid}-panel-${key}`
40
+
41
+ const changeTabs = (key: string) => setActiveTab(key)
42
+
43
+ const activateTab = (index: number) => {
44
+ setActiveTab(tabs[index].key)
45
+ tabRefs.current[index]?.focus()
46
+ }
47
+
48
+ // Vue 版(@geckou/ui-vue)は window 全体に keydown を張っていたため、フォーカス位置と
49
+ // 無関係にタブが切り替わり複数設置時に競合した。タブリストにフォーカスが
50
+ // あるときだけ矢印キーで移動する(WAI-ARIA Tabs パターン)
51
+ const handleKeydown = (event: KeyboardEvent<HTMLDivElement>) => {
52
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
53
+
54
+ const currentIndex = tabs.findIndex((tab) => tab.key === activeTab)
55
+ if (currentIndex === -1) return
56
+
57
+ event.preventDefault()
58
+ const lastIndex = tabs.length - 1
59
+
60
+ if (event.key === 'ArrowLeft') {
61
+ activateTab(currentIndex > 0 ? currentIndex - 1 : lastIndex)
62
+ } else {
63
+ activateTab(currentIndex < lastIndex ? currentIndex + 1 : 0)
64
+ }
65
+ }
66
+
67
+ const style = {
68
+ '--active-color': color?.active || COLOR.blue,
69
+ '--background-color': color?.background || 'transparent',
70
+ } as CSSProperties
71
+
72
+ return (
73
+ <div>
74
+ <div
75
+ style={style}
76
+ className="flex bg-(--background-color)"
77
+ role="tablist"
78
+ onKeyDown={handleKeydown}
79
+ >
80
+ {tabs.map((tab, index) => (
81
+ <button
82
+ key={tab.key}
83
+ id={tabId(tab.key)}
84
+ ref={(el) => {
85
+ tabRefs.current[index] = el
86
+ }}
87
+ type="button"
88
+ role="tab"
89
+ aria-controls={panelId(tab.key)}
90
+ aria-selected={activeTab === tab.key}
91
+ tabIndex={activeTab === tab.key ? 0 : -1}
92
+ className={`border-none bg-transparent px-4 py-2 text-base ${activeTab === tab.key ? 'cursor-auto' : 'cursor-pointer'}`}
93
+ onClick={() => changeTabs(tab.key)}
94
+ >
95
+ {tabSlots?.[tab.key] ?? tab.label}
96
+ </button>
97
+ ))}
98
+ </div>
99
+ {tabs.map((tab) => (
100
+ <div
101
+ key={`${tab.key}_panel`}
102
+ id={panelId(tab.key)}
103
+ role="tabpanel"
104
+ aria-labelledby={tabId(tab.key)}
105
+ hidden={activeTab !== tab.key}
106
+ >
107
+ {panelSlots?.[`${tab.key}Contents`]}
108
+ </div>
109
+ ))}
110
+ </div>
111
+ )
112
+ }
@@ -0,0 +1,102 @@
1
+ 'use client'
2
+
3
+ import type { InputBoxStyleForEachStatus, Validates } from '../types'
4
+ import { isEmptyValue, validateInputValue } from '@geckou/ui-core'
5
+ import { useEffect, useRef, useState } from 'react'
6
+ import { InputBox } from './InputBox'
7
+ import { ErrorMessage } from './ErrorMessage'
8
+
9
+ type InputValue = string | null
10
+
11
+ type Props = {
12
+ name: string
13
+ value?: InputValue
14
+ onChange?: (newValue: InputValue) => void
15
+ cssStyle?: InputBoxStyleForEachStatus
16
+ placeholder?: string
17
+ isDisabled?: boolean
18
+ isRequired?: boolean
19
+ rows?: number
20
+ maxLength?: number
21
+ autocomplete?: string
22
+ validates?: Validates
23
+ autoAdjustHeight?: boolean
24
+ }
25
+
26
+ export function TextArea({
27
+ name,
28
+ value,
29
+ onChange,
30
+ cssStyle,
31
+ placeholder = '入力してください',
32
+ isDisabled,
33
+ isRequired,
34
+ rows,
35
+ maxLength = 100,
36
+ autocomplete = 'off',
37
+ validates = [],
38
+ autoAdjustHeight,
39
+ }: Props) {
40
+ const [errorMessages, setErrorMessages] = useState<string[]>([])
41
+ const textareaRef = useRef<HTMLTextAreaElement>(null)
42
+ const inputValue = value ?? ''
43
+
44
+ const validateValue = () => {
45
+ setErrorMessages(validateInputValue(inputValue, { isRequired, validates }))
46
+ }
47
+
48
+ // Vue 版(@geckou/ui-vue)の watch(immediate: !!modelValue) と等価:
49
+ // 初期値ありならマウント時にも検証、以後は値が変化したときのみ検証
50
+ const initialValue = useRef(inputValue)
51
+ const hasChanged = useRef(false)
52
+
53
+ useEffect(() => {
54
+ if (autoAdjustHeight && textareaRef.current) {
55
+ textareaRef.current.style.height = 'auto'
56
+ textareaRef.current.style.height = `calc(${textareaRef.current.scrollHeight}px - 2rem)`
57
+ }
58
+
59
+ if (!hasChanged.current) {
60
+ if (inputValue === initialValue.current) {
61
+ // 数値 0 も初期値として扱うため truthy 判定は使わない
62
+ if (!isEmptyValue(inputValue)) validateValue()
63
+ return
64
+ }
65
+ hasChanged.current = true
66
+ }
67
+
68
+ validateValue()
69
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- Vue 版(@geckou/ui-vue)同様、値の変化時のみ検証・高さ調整する
70
+ }, [inputValue])
71
+
72
+ return (
73
+ <InputBox
74
+ cssStyle={cssStyle}
75
+ className="min-h-[4em]"
76
+ isErrored={!!errorMessages.length}
77
+ isDisabled={isDisabled}
78
+ >
79
+ <textarea
80
+ ref={textareaRef}
81
+ name={name}
82
+ value={inputValue}
83
+ required={isRequired}
84
+ placeholder={placeholder}
85
+ disabled={isDisabled}
86
+ autoComplete={autocomplete}
87
+ rows={rows}
88
+ maxLength={maxLength}
89
+ aria-invalid={errorMessages.length ? 'true' : undefined}
90
+ onChange={(event) => onChange?.(event.target.value)}
91
+ onBlur={() => validateValue()}
92
+ />
93
+ <ErrorMessage
94
+ errorMessages={errorMessages}
95
+ cssStyle={{
96
+ textColor: cssStyle?.error?.backgroundColor,
97
+ backgroundColor: cssStyle?.error?.textColor,
98
+ }}
99
+ />
100
+ </InputBox>
101
+ )
102
+ }
@@ -0,0 +1,120 @@
1
+ 'use client'
2
+
3
+ import type { ReactNode } from 'react'
4
+ import type { InputBoxStyleForEachStatus, Validates } from '../types'
5
+ import {
6
+ convertFullWidthToHalfWidth,
7
+ isEmptyValue,
8
+ validateInputValue,
9
+ } from '@geckou/ui-core'
10
+ import { useEffect, useRef, useState } from 'react'
11
+ import { InputBox } from './InputBox'
12
+ import { ErrorMessage } from './ErrorMessage'
13
+
14
+ type InputValue = string | number
15
+
16
+ type Props = {
17
+ name: string
18
+ value?: InputValue
19
+ onChange?: (newValue: InputValue) => void
20
+ cssStyle?: InputBoxStyleForEachStatus
21
+ inputType?: string
22
+ placeholder?: string
23
+ isDisabled?: boolean
24
+ isRequired?: boolean
25
+ maxLength?: number
26
+ autocomplete?: string
27
+ validates?: Validates
28
+ before?: ReactNode
29
+ after?: ReactNode
30
+ }
31
+
32
+ export function TextBox({
33
+ name,
34
+ value,
35
+ onChange,
36
+ cssStyle,
37
+ inputType = 'text',
38
+ placeholder = '入力してください',
39
+ isDisabled,
40
+ isRequired,
41
+ maxLength = 30,
42
+ autocomplete = 'off',
43
+ validates = [],
44
+ before,
45
+ after,
46
+ }: Props) {
47
+ const [errorMessages, setErrorMessages] = useState<string[]>([])
48
+ const inputValue = value ?? ''
49
+
50
+ const validateValue = () => {
51
+ setErrorMessages(validateInputValue(inputValue, { isRequired, validates }))
52
+ }
53
+
54
+ // Vue 版(@geckou/ui-vue)の watch(immediate: !!modelValue) と等価:
55
+ // 初期値ありならマウント時にも検証、以後は値が変化したときのみ検証
56
+ const initialValue = useRef(inputValue)
57
+ const hasChanged = useRef(false)
58
+
59
+ useEffect(() => {
60
+ if (!hasChanged.current) {
61
+ if (inputValue === initialValue.current) {
62
+ if (!isEmptyValue(inputValue)) validateValue()
63
+ return
64
+ }
65
+ hasChanged.current = true
66
+ }
67
+
68
+ validateValue()
69
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- Vue 版(@geckou/ui-vue)同様、値の変化時のみ検証する
70
+ }, [inputValue])
71
+
72
+ const isComposing = useRef(false)
73
+
74
+ return (
75
+ <InputBox
76
+ cssStyle={cssStyle}
77
+ className="inline-flex [&>input]:flex-auto"
78
+ isErrored={!!errorMessages.length}
79
+ isDisabled={isDisabled}
80
+ >
81
+ {before}
82
+ <input
83
+ type={inputType}
84
+ name={name}
85
+ value={inputValue}
86
+ required={isRequired}
87
+ placeholder={placeholder}
88
+ disabled={isDisabled}
89
+ autoComplete={autocomplete}
90
+ maxLength={maxLength}
91
+ aria-invalid={errorMessages.length ? 'true' : undefined}
92
+ onChange={(event) => {
93
+ // IME 変換中に全角→半角変換すると未確定文字列が壊れるため、確定後に変換する
94
+ const rawValue = event.target.value
95
+ onChange?.(
96
+ isComposing.current
97
+ ? rawValue
98
+ : convertFullWidthToHalfWidth(rawValue),
99
+ )
100
+ }}
101
+ onCompositionStart={() => {
102
+ isComposing.current = true
103
+ }}
104
+ onCompositionEnd={(event) => {
105
+ isComposing.current = false
106
+ onChange?.(convertFullWidthToHalfWidth(event.currentTarget.value))
107
+ }}
108
+ onBlur={() => validateValue()}
109
+ />
110
+ {after}
111
+ <ErrorMessage
112
+ errorMessages={errorMessages}
113
+ cssStyle={{
114
+ textColor: cssStyle?.error?.backgroundColor,
115
+ backgroundColor: cssStyle?.error?.textColor,
116
+ }}
117
+ />
118
+ </InputBox>
119
+ )
120
+ }
@@ -0,0 +1,102 @@
1
+ 'use client'
2
+
3
+ import type { CSSProperties } from 'react'
4
+ import type { BaseStyle, StyleForEachStatus } from '../types'
5
+ import { COLOR } from '../constants'
6
+
7
+ type ToggleStyle = {
8
+ on: BaseStyle
9
+ off: BaseStyle
10
+ }
11
+
12
+ type Props = {
13
+ name: string
14
+ label?: Record<'on' | 'off', string>
15
+ checked?: boolean
16
+ onChange?: (newValue: boolean) => void
17
+ isDisabled?: boolean
18
+ cssStyle?: StyleForEachStatus<ToggleStyle>
19
+ }
20
+
21
+ export function ToggleButton({
22
+ name,
23
+ label = { on: 'ON', off: 'OFF' },
24
+ checked,
25
+ onChange,
26
+ isDisabled,
27
+ cssStyle,
28
+ }: Props) {
29
+ const isChecked = checked ?? false
30
+ const maxTextLength = Math.max(label.on.length, label.off.length)
31
+ const baseStyle = isDisabled ? cssStyle?.disabled : cssStyle?.default
32
+
33
+ const currentCssStyle = {
34
+ on: {
35
+ textColor: isDisabled ? COLOR.lightGray : COLOR.white,
36
+ backgroundColor: isDisabled ? COLOR.gray : COLOR.blue,
37
+ border: {
38
+ color: isDisabled ? COLOR.gray : COLOR.blue,
39
+ size: '1px',
40
+ radius: '.25rem',
41
+ },
42
+ boxShadow: '0 0 0 0 rgba(0, 0, 0, 0)',
43
+ ...baseStyle?.on,
44
+ },
45
+ off: {
46
+ textColor: isDisabled ? COLOR.gray : COLOR.white,
47
+ backgroundColor: isDisabled ? COLOR.lightGray : COLOR.darkGray,
48
+ border: {
49
+ color: isDisabled ? COLOR.gray : COLOR.darkGray,
50
+ size: '1px',
51
+ radius: '.25rem',
52
+ },
53
+ boxShadow: '0 0 0 0 rgba(0, 0, 0, 0)',
54
+ ...baseStyle?.off,
55
+ },
56
+ }[isChecked ? 'on' : 'off']
57
+
58
+ const style = {
59
+ '--text-color': currentCssStyle.textColor,
60
+ '--border-color': currentCssStyle.border?.color,
61
+ '--border-size': currentCssStyle.border?.size,
62
+ '--radius-size': currentCssStyle.border?.radius,
63
+ '--background-color': currentCssStyle.backgroundColor,
64
+ '--box-shadow': currentCssStyle.boxShadow,
65
+ '--inline-size': `${maxTextLength * 2}ch`,
66
+ '--handle-size': '1.5rem',
67
+ '--padding-size': 'calc(var(--border-size) + 2px)',
68
+ '--duration': '.15s',
69
+ } as CSSProperties
70
+
71
+ return (
72
+ <button
73
+ type="button"
74
+ style={style}
75
+ disabled={isDisabled}
76
+ aria-pressed={isChecked}
77
+ aria-label={name}
78
+ onClick={(event) => {
79
+ event.stopPropagation()
80
+ if (!isDisabled) onChange?.(!isChecked)
81
+ }}
82
+ className="relative inline-block w-[calc(var(--inline-size)+var(--handle-size)+(var(--padding-size)*2))] cursor-pointer rounded-(--radius-size) border-none bg-(--background-color) p-(--padding-size) shadow-[0_0_0_var(--border-size)_var(--border-color)_inset,var(--box-shadow)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--border-color)"
83
+ >
84
+ <input
85
+ type="checkbox"
86
+ name={name}
87
+ checked={isChecked}
88
+ disabled={isDisabled}
89
+ readOnly
90
+ className="hidden"
91
+ />
92
+ <div
93
+ data-on={label.on}
94
+ data-off={label.off}
95
+ className={`absolute top-0 left-0 h-full w-full text-(--text-color) uppercase before:absolute before:right-0 before:m-auto before:inline-flex before:h-full before:w-[calc(100%-var(--handle-size)-var(--padding-size))] before:items-center before:justify-center before:leading-none before:transition-opacity before:duration-(--duration) before:ease-out before:content-[attr(data-off)] after:absolute after:left-0 after:m-auto after:inline-flex after:h-full after:w-[calc(100%-var(--handle-size)-var(--padding-size))] after:items-center after:justify-center after:leading-none after:transition-opacity after:duration-(--duration) after:ease-out after:content-[attr(data-on)] ${isChecked ? 'before:opacity-0 after:opacity-100' : 'before:opacity-100 after:opacity-0'}`}
96
+ />
97
+ <div
98
+ className={`relative m-0 aspect-square w-(--handle-size) rounded-[calc(var(--radius-size)-(var(--padding-size)/2))] bg-(--text-color) shadow-(--box-shadow) transition-[left] duration-(--duration) ease-out ${isChecked ? 'left-[calc(100%-var(--handle-size))]' : 'left-0'}`}
99
+ />
100
+ </button>
101
+ )
102
+ }
@@ -0,0 +1,16 @@
1
+ type Props = {
2
+ className?: string
3
+ }
4
+
5
+ export function BackupIcon({ className }: Props) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 -960 960 960"
10
+ fill="currentColor"
11
+ className={className}
12
+ >
13
+ <path d="M260-160q-91 0-155.5-63T40-377q0-78 47-139t123-78q25-92 100-149t170-57q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 75-52.5 127.5T740-160H520q-33 0-56.5-23.5T440-240v-206l-64 62-56-56 160-160 160 160-56 56-64-62v206h220q42 0 71-29t29-71q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-83 0-141.5 58.5T280-520h-20q-58 0-99 41t-41 99q0 58 41 99t99 41h100v80H260Z" />
14
+ </svg>
15
+ )
16
+ }
@@ -0,0 +1,15 @@
1
+ type Props = {
2
+ className?: string
3
+ }
4
+
5
+ export function CalendarIcon({ className }: Props) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 -960 960 960"
10
+ className={className}
11
+ >
12
+ <path d="M200-80q-33 0-56.5-23.5T120-160v-560q0-33 23.5-56.5T200-800h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840-720v560q0 33-23.5 56.5T760-80H200Zm0-80h560v-400H200v400Zm0-480h560v-80H200v80Z" />
13
+ </svg>
14
+ )
15
+ }
@@ -0,0 +1,7 @@
1
+ export function CheckIcon() {
2
+ return (
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
4
+ <path d="M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z" />
5
+ </svg>
6
+ )
7
+ }
@@ -0,0 +1,16 @@
1
+ type Props = {
2
+ className?: string
3
+ }
4
+
5
+ export function CloseIcon({ className }: Props) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 -960 960 960"
10
+ fill="currentColor"
11
+ className={className}
12
+ >
13
+ <path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z" />
14
+ </svg>
15
+ )
16
+ }
@@ -0,0 +1,16 @@
1
+ type Props = {
2
+ className?: string
3
+ }
4
+
5
+ export function KeyboardArrowDownIcon({ className }: Props) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 -960 960 960"
10
+ fill="currentColor"
11
+ className={className}
12
+ >
13
+ <path d="M480-344 240-584l56-56 184 184 184-184 56 56-240 240Z" />
14
+ </svg>
15
+ )
16
+ }
@@ -0,0 +1,2 @@
1
+ // 配色は @geckou/ui-core が正。Vue / React で共通のため再エクスポートのみ行う
2
+ export { COLOR, BORDER, MESSAGES, INPUT_BOX_DEFAULT_STYLES } from '@geckou/ui-core'
@@ -0,0 +1,69 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useMemo, useSyncExternalStore } from 'react'
4
+ import { createFormValidationStore } from '@geckou/ui-core'
5
+ import type { FormValidationStore } from '@geckou/ui-core'
6
+
7
+ export type UseFormValidationResult = {
8
+ /** 登録済みの入力がすべて有効かどうか */
9
+ isAllValid: boolean
10
+ /** 無効になっている入力の name 一覧 */
11
+ invalidNames: string[]
12
+ /** 入力の状態を登録・更新する */
13
+ setValid: FormValidationStore['setValid']
14
+ /** 管理対象から外す */
15
+ remove: FormValidationStore['remove']
16
+ /** すべての状態を破棄する */
17
+ reset: FormValidationStore['reset']
18
+ /** 子コンポーネントへ直接渡すためのストア本体 */
19
+ store: FormValidationStore
20
+ }
21
+
22
+ /**
23
+ * フォーム内の各入力のバリデーション状態をまとめて管理する。
24
+ *
25
+ * 状態そのものは @geckou/ui-core の createFormValidationStore が持つため、
26
+ * 判定ロジックは Vue 実装(@geckou/ui-vue の FormValidationManager)と共有される。
27
+ *
28
+ * ```tsx
29
+ * const { isAllValid, store } = useFormValidation()
30
+ * // <DatePicker name="startedOn" formValidationStore={store} />
31
+ * <button disabled={!isAllValid}>送信</button>
32
+ * ```
33
+ */
34
+ export function useFormValidation(): UseFormValidationResult {
35
+ const store = useMemo(() => createFormValidationStore(), [])
36
+ const snapshot = useSyncExternalStore(
37
+ store.subscribe,
38
+ store.getSnapshot,
39
+ // SSR 時は登録がまだ無いので、すべて有効なスナップショットを返す
40
+ store.getSnapshot,
41
+ )
42
+
43
+ return {
44
+ isAllValid: snapshot.isAllValid,
45
+ invalidNames: snapshot.invalidNames,
46
+ setValid: store.setValid,
47
+ remove: store.remove,
48
+ reset: store.reset,
49
+ store,
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 入力コンポーネント側から検証結果をストアへ通知する。
55
+ * アンマウント時には登録を解除し、無効判定が残らないようにする。
56
+ */
57
+ export function useRegisterValidation(
58
+ store: FormValidationStore | null | undefined,
59
+ name: string,
60
+ isValid: boolean,
61
+ ): void {
62
+ useEffect(() => {
63
+ store?.setValid(name, isValid)
64
+ }, [store, name, isValid])
65
+
66
+ useEffect(() => {
67
+ return () => store?.remove(name)
68
+ }, [store, name])
69
+ }
package/src/index.ts ADDED
@@ -0,0 +1,66 @@
1
+ export { BasicButton } from './components/BasicButton'
2
+ export { CheckBox } from './components/CheckBox'
3
+ export { CheckBoxes } from './components/CheckBoxes'
4
+ export { CheckButton } from './components/CheckButton'
5
+ export { DatePicker } from './components/DatePicker'
6
+ export { DateRangePicker } from './components/DateRangePicker'
7
+ export { DateSelector } from './components/DateSelector'
8
+ export { DropdownUi } from './components/DropdownUi'
9
+ export type { DropdownUiHandle } from './components/DropdownUi'
10
+ export { ErrorMessage } from './components/ErrorMessage'
11
+ export { FileInput } from './components/FileInput'
12
+ export { InputBox } from './components/InputBox'
13
+ export { InputGroup } from './components/InputGroup'
14
+ export { LabeledCheckbox } from './components/LabeledCheckbox'
15
+ export { LabeledFieldset } from './components/LabeledFieldset'
16
+ export { LoadingSpinner } from './components/LoadingSpinner'
17
+ export { ModalBox } from './components/ModalBox'
18
+ export { PopupBox } from './components/PopupBox'
19
+ export type { PopupBoxHandle } from './components/PopupBox'
20
+ export { RadioButtons } from './components/RadioButtons'
21
+ export { SearchableSelectBox } from './components/SearchableSelectBox'
22
+ export { SelectBox } from './components/SelectBox'
23
+ export { SlideDownUi } from './components/SlideDownUi'
24
+ export type { SlideDownUiHandle } from './components/SlideDownUi'
25
+ export { TabUI } from './components/TabUI'
26
+ export { TextArea } from './components/TextArea'
27
+ export { TextBox } from './components/TextBox'
28
+ export { ToggleButton } from './components/ToggleButton'
29
+
30
+ export { BackupIcon } from './components/icons/BackupIcon'
31
+ export { CheckIcon } from './components/icons/CheckIcon'
32
+ export { CloseIcon } from './components/icons/CloseIcon'
33
+ export { CalendarIcon } from './components/icons/CalendarIcon'
34
+ export { KeyboardArrowDownIcon } from './components/icons/KeyboardArrowDownIcon'
35
+
36
+ export {
37
+ useFormValidation,
38
+ useRegisterValidation,
39
+ } from './hooks/useFormValidation'
40
+ export type { UseFormValidationResult } from './hooks/useFormValidation'
41
+
42
+ export { COLOR, BORDER, MESSAGES, INPUT_BOX_DEFAULT_STYLES } from './constants'
43
+ export type {
44
+ StateVariation,
45
+ BorderStyle,
46
+ BaseStyle,
47
+ StyleForEachStatus,
48
+ InputBoxStyle,
49
+ InputBoxStyleForEachStatus,
50
+ ButtonStyle,
51
+ ButtonStyleForEachStatus,
52
+ CheckBoxStyle,
53
+ CheckBoxStyleForEachStatus,
54
+ RadioButtonStyle,
55
+ RadioButtonStyleForEachStatus,
56
+ SelectValue,
57
+ Option,
58
+ Validate,
59
+ Validates,
60
+ InputValue,
61
+ DateObject,
62
+ DateType,
63
+ ValidationResult,
64
+ } from './types'
65
+
66
+ export type { FormValidationStore } from '@geckou/ui-core'