@geckou/ui-core 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 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,103 @@
1
+ # @geckou/ui-core
2
+
3
+ [`@geckou/ui-vue`](../vue) と [`@geckou/ui-react`](../react) が共有する、
4
+ フレームワーク非依存のロジック層。Vue も React も使わない純粋な TypeScript です。
5
+
6
+ コンポーネントを使わず、バリデーションや日付処理だけ利用することもできます。
7
+
8
+ ```bash
9
+ yarn add @geckou/ui-core
10
+ ```
11
+
12
+ ## なぜ分けているか
13
+
14
+ Vue 版と React 版を別々に実装していたため、同じバグが両方に存在し、
15
+ 片方だけ直ってもう片方に残る、という事故が起きました。
16
+ フレームワークに依存しないロジックをここへ集約し、テストを付けています。
17
+
18
+ ## API
19
+
20
+ ### バリデーション
21
+
22
+ ```ts
23
+ import { isEmptyValue, runValidates, validateInputValue } from '@geckou/ui-core'
24
+ ```
25
+
26
+ | 関数 | 説明 |
27
+ |---|---|
28
+ | `isEmptyValue(value)` | 空かどうか。**数値 `0` や `'0'` は空とみなさない**(`!value` 判定だと 0 が必須エラーになる) |
29
+ | `runValidates(value, validates)` | 各 `RegExp` を適用し、一致しなかった `message` を返す。`g` / `y` フラグ付きでも判定が安定するよう毎回クローンして評価し、**呼び出し側の `lastIndex` を変異させない** |
30
+ | `validateInputValue(value, { isRequired, validates })` | 必須チェックと `validates` をまとめて適用し、メッセージの配列を返す |
31
+
32
+ ### 日付
33
+
34
+ ```ts
35
+ import {
36
+ formatDateValue,
37
+ splitDate,
38
+ composeDateValue,
39
+ validateDateObject,
40
+ daysInMonth,
41
+ } from '@geckou/ui-core'
42
+ ```
43
+
44
+ | 関数 | 説明 |
45
+ |---|---|
46
+ | `formatDateValue(value, type?)` | `YYYY-MM-DD`(`type='month'` なら `YYYY-MM`)へ正規化。**`toISOString()` を使わないためタイムゾーンで日付がずれない**。不正な文字列は空文字を返す |
47
+ | `splitDate(value)` | `YYYY-MM-DD` を `{ year, month, day }` へ分解 |
48
+ | `composeDateValue(dateObject, type?)` | 年月日から日付文字列を組み立てる。要素が欠けていれば空文字 |
49
+ | `validateDateObject(dateObject, { type, isRequired })` | 桁数・月の範囲・その月に存在する日かを検証し `{ isValid, message }` を返す |
50
+ | `daysInMonth(year, month)` | 指定した年月の日数(`month` は 1 始まり。うるう年を考慮) |
51
+
52
+ ### 文字変換
53
+
54
+ ```ts
55
+ import { convertFullWidthToHalfWidth } from '@geckou/ui-core'
56
+ ```
57
+
58
+ 全角の英数字を半角へ変換します。全角以外はそのまま残します。
59
+
60
+ ### フォーム全体の検証状態
61
+
62
+ ```ts
63
+ import { createFormValidationStore } from '@geckou/ui-core'
64
+
65
+ const store = createFormValidationStore()
66
+ store.setValid('startedOn', false)
67
+ store.getSnapshot() // { isAllValid: false, invalidNames: ['startedOn'] }
68
+ ```
69
+
70
+ | メソッド | 説明 |
71
+ |---|---|
72
+ | `setValid(name, isValid)` | 入力の状態を登録・更新する |
73
+ | `isValid(name)` | 個別の入力が有効か(未登録なら `true`) |
74
+ | `remove(name)` | 管理対象から外す |
75
+ | `reset()` | すべての状態を破棄する |
76
+ | `getSnapshot()` | 現在の状態。**内容が変わらない限り同一参照を返す**ため `useSyncExternalStore` にそのまま渡せる |
77
+ | `subscribe(listener)` | 変更通知を購読する。戻り値を呼ぶと解除 |
78
+
79
+ 各フレームワークからは以下でつなぎます。
80
+
81
+ - Vue: `FormValidationManager`(`@geckou/ui-vue`)
82
+ - React: `useFormValidation`(`@geckou/ui-react`)
83
+
84
+ ### 定数・型
85
+
86
+ ```ts
87
+ import { COLOR, BORDER, MESSAGES, INPUT_BOX_DEFAULT_STYLES } from '@geckou/ui-core'
88
+ import type { Validates, Option, StateVariation, DateObject } from '@geckou/ui-core'
89
+ ```
90
+
91
+ `MESSAGES` はエラー文言の単一の入口です。Vue / React で文言がずれないよう、必ずここを参照します。
92
+
93
+ 型の一覧は [Vue パッケージの README](../vue/README.md#types) を参照してください。
94
+
95
+ ## テスト
96
+
97
+ ```bash
98
+ yarn workspace @geckou/ui-core test
99
+ ```
100
+
101
+ ## License
102
+
103
+ [MIT](../../LICENSE)
@@ -0,0 +1,25 @@
1
+ import type { InputBoxStyleForEachStatus, BorderStyle } from './types';
2
+ export declare const COLOR: {
3
+ white: string;
4
+ black: string;
5
+ darkGray: string;
6
+ gray: string;
7
+ lightGray: string;
8
+ red: string;
9
+ green: string;
10
+ blue: string;
11
+ };
12
+ export declare const BORDER: BorderStyle;
13
+ export declare const INPUT_BOX_DEFAULT_STYLES: InputBoxStyleForEachStatus;
14
+ /**
15
+ * エラーメッセージ。将来 i18n へ差し替えるための単一の入口。
16
+ * Vue / React で文言がずれないよう、必ずここを参照する。
17
+ */
18
+ export declare const MESSAGES: {
19
+ required: string;
20
+ invalidYear: string;
21
+ invalidMonth: string;
22
+ invalidDay: string;
23
+ monthOutOfRange: string;
24
+ dayOutOfRange: (maxDay: number) => string;
25
+ };
@@ -0,0 +1,66 @@
1
+ export const COLOR = {
2
+ white: '#fff',
3
+ black: '#333',
4
+ darkGray: '#999',
5
+ gray: '#ccc',
6
+ lightGray: '#f9f9f9',
7
+ red: '#aa0000',
8
+ green: '#28a745',
9
+ blue: '#1c4ac9',
10
+ };
11
+ const { black: TEXT_COLOR, darkGray: PLACEHOLDER_COLOR, gray: BORDER_COLOR, lightGray: DISABLED_COLOR, red: CAUTION_COLOR, green: VALID_COLOR, blue: FOCUS_COLOR, } = COLOR;
12
+ export const BORDER = {
13
+ color: BORDER_COLOR,
14
+ size: '1px',
15
+ radius: '.25rem',
16
+ };
17
+ const NO_SHADOW = '0 0 0 0 rgba(0, 0, 0, 0)';
18
+ export const INPUT_BOX_DEFAULT_STYLES = {
19
+ default: {
20
+ textColor: TEXT_COLOR,
21
+ placeholderColor: PLACEHOLDER_COLOR,
22
+ backgroundColor: 'inherit',
23
+ border: BORDER,
24
+ boxShadow: NO_SHADOW,
25
+ },
26
+ disabled: {
27
+ textColor: PLACEHOLDER_COLOR,
28
+ placeholderColor: PLACEHOLDER_COLOR,
29
+ backgroundColor: DISABLED_COLOR,
30
+ border: BORDER,
31
+ boxShadow: NO_SHADOW,
32
+ },
33
+ focus: {
34
+ textColor: TEXT_COLOR,
35
+ placeholderColor: PLACEHOLDER_COLOR,
36
+ backgroundColor: 'inherit',
37
+ border: { ...BORDER, color: FOCUS_COLOR },
38
+ boxShadow: `0 0 .1rem .1rem ${FOCUS_COLOR}55`,
39
+ },
40
+ error: {
41
+ textColor: TEXT_COLOR,
42
+ placeholderColor: PLACEHOLDER_COLOR,
43
+ backgroundColor: `${CAUTION_COLOR}11`,
44
+ border: { ...BORDER, color: CAUTION_COLOR },
45
+ boxShadow: `0 0 .1rem .1rem ${CAUTION_COLOR}55`,
46
+ },
47
+ valid: {
48
+ textColor: TEXT_COLOR,
49
+ placeholderColor: PLACEHOLDER_COLOR,
50
+ backgroundColor: `${VALID_COLOR}11`,
51
+ border: { ...BORDER, color: VALID_COLOR },
52
+ boxShadow: NO_SHADOW,
53
+ },
54
+ };
55
+ /**
56
+ * エラーメッセージ。将来 i18n へ差し替えるための単一の入口。
57
+ * Vue / React で文言がずれないよう、必ずここを参照する。
58
+ */
59
+ export const MESSAGES = {
60
+ required: '必須項目です',
61
+ invalidYear: '年は4桁の数字で入力してください',
62
+ invalidMonth: '月は2桁の数字で入力してください',
63
+ invalidDay: '日は2桁の数字で入力してください',
64
+ monthOutOfRange: '月は01から12の間で入力してください',
65
+ dayOutOfRange: (maxDay) => `日は01から${maxDay}の間で入力してください`,
66
+ };
package/dist/date.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { DateObject, DateType, ValidationResult } from './types';
2
+ /** 指定した年月の日数。month は 1 始まり */
3
+ export declare function daysInMonth(year: number, month: number): number;
4
+ /**
5
+ * 任意の日付文字列を YYYY-MM-DD(type='month' なら YYYY-MM)へ正規化する。
6
+ *
7
+ * YYYY-MM(-DD) 形式は正規表現で直接読む。`new Date()` + `toISOString()` を使うと
8
+ * UTC へ変換されるため、JST のようなプラス方向のタイムゾーンでは日付が前日へずれる。
9
+ * 正規表現で読めない形式のみ Date にフォールバックし、その場合もローカルの
10
+ * getFullYear / getMonth / getDate を使って変換する。
11
+ */
12
+ export declare function formatDateValue(value: string, type?: DateType): string;
13
+ /** YYYY-MM-DD を年・月・日へ分解する。欠けている要素は空文字 */
14
+ export declare function splitDate(value: string): DateObject;
15
+ /** 年・月・日から日付文字列を組み立てる。要素が欠けていれば空文字 */
16
+ export declare function composeDateValue(dateObject: DateObject, type?: DateType): string;
17
+ /**
18
+ * 年・月・日の入力内容を検証する。
19
+ * type='month' のときは日を見ない。
20
+ */
21
+ export declare function validateDateObject(dateObject: DateObject, options?: {
22
+ type?: DateType;
23
+ isRequired?: boolean;
24
+ }): ValidationResult;
package/dist/date.js ADDED
@@ -0,0 +1,89 @@
1
+ import { MESSAGES } from './constants';
2
+ function pad(value) {
3
+ return String(value).padStart(2, '0');
4
+ }
5
+ function isNumeric(value) {
6
+ return /^\d+$/.test(value);
7
+ }
8
+ /** 指定した年月の日数。month は 1 始まり */
9
+ export function daysInMonth(year, month) {
10
+ return new Date(year, month, 0).getDate();
11
+ }
12
+ /**
13
+ * 任意の日付文字列を YYYY-MM-DD(type='month' なら YYYY-MM)へ正規化する。
14
+ *
15
+ * YYYY-MM(-DD) 形式は正規表現で直接読む。`new Date()` + `toISOString()` を使うと
16
+ * UTC へ変換されるため、JST のようなプラス方向のタイムゾーンでは日付が前日へずれる。
17
+ * 正規表現で読めない形式のみ Date にフォールバックし、その場合もローカルの
18
+ * getFullYear / getMonth / getDate を使って変換する。
19
+ */
20
+ export function formatDateValue(value, type = 'date') {
21
+ if (!value)
22
+ return '';
23
+ const matched = value.match(/^(\d{4})-(\d{1,2})(?:-(\d{1,2}))?/);
24
+ if (matched) {
25
+ const [, year, month, day] = matched;
26
+ const yearMonth = `${year}-${pad(Number(month))}`;
27
+ if (type === 'month')
28
+ return yearMonth;
29
+ // type='date' は完全な日付を要求する。日が欠けていれば入力欄には反映しない
30
+ return day ? `${yearMonth}-${pad(Number(day))}` : '';
31
+ }
32
+ const parsed = new Date(value);
33
+ // 不正な日付文字列で toISOString() が throw していたため、先に弾く
34
+ if (Number.isNaN(parsed.getTime()))
35
+ return '';
36
+ const yearMonth = `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}`;
37
+ if (type === 'month')
38
+ return yearMonth;
39
+ return `${yearMonth}-${pad(parsed.getDate())}`;
40
+ }
41
+ /** YYYY-MM-DD を年・月・日へ分解する。欠けている要素は空文字 */
42
+ export function splitDate(value) {
43
+ const [year = '', month = '', day = ''] = value.split('-');
44
+ return { year, month, day };
45
+ }
46
+ /** 年・月・日から日付文字列を組み立てる。要素が欠けていれば空文字 */
47
+ export function composeDateValue(dateObject, type = 'date') {
48
+ const { year, month, day } = dateObject;
49
+ const parts = type === 'month' ? [year, month] : [year, month, day];
50
+ if (parts.some(part => !part))
51
+ return '';
52
+ return parts.join('-');
53
+ }
54
+ /**
55
+ * 年・月・日の入力内容を検証する。
56
+ * type='month' のときは日を見ない。
57
+ */
58
+ export function validateDateObject(dateObject, options = {}) {
59
+ const { type = 'date', isRequired = false } = options;
60
+ const { year, month } = dateObject;
61
+ const day = type === 'month' ? '' : dateObject.day;
62
+ const valid = { isValid: true, message: '' };
63
+ // 全部空で必須でなければ未入力として通す
64
+ if ([year, month, day].every(value => !value) && !isRequired)
65
+ return valid;
66
+ const requiredValues = type === 'month' ? [year, month] : [year, month, day];
67
+ if (requiredValues.some(value => isRequired && !value)) {
68
+ return { isValid: false, message: MESSAGES.required };
69
+ }
70
+ if (year.length !== 4 || !isNumeric(year)) {
71
+ return { isValid: false, message: MESSAGES.invalidYear };
72
+ }
73
+ if (month.length !== 2 || !isNumeric(month)) {
74
+ return { isValid: false, message: MESSAGES.invalidMonth };
75
+ }
76
+ if (day && (day.length !== 2 || !isNumeric(day))) {
77
+ return { isValid: false, message: MESSAGES.invalidDay };
78
+ }
79
+ const monthNumber = parseInt(month, 10);
80
+ if (monthNumber < 1 || monthNumber > 12) {
81
+ return { isValid: false, message: MESSAGES.monthOutOfRange };
82
+ }
83
+ const dayNumber = day ? parseInt(day, 10) : null;
84
+ const maxDay = daysInMonth(parseInt(year, 10), monthNumber);
85
+ if (dayNumber && (dayNumber < 1 || dayNumber > maxDay)) {
86
+ return { isValid: false, message: MESSAGES.dayOutOfRange(maxDay) };
87
+ }
88
+ return valid;
89
+ }
@@ -0,0 +1,38 @@
1
+ export type FormValidationSnapshot = {
2
+ /** 登録済みの入力がすべて有効か */
3
+ isAllValid: boolean;
4
+ /** 無効になっている入力の name 一覧 */
5
+ invalidNames: string[];
6
+ };
7
+ export type FormValidationStore = {
8
+ /** 入力の状態を登録・更新する */
9
+ setValid: (name: string, isValid: boolean) => void;
10
+ /** 個別の入力が有効か(未登録なら true) */
11
+ isValid: (name: string) => boolean;
12
+ /** 管理対象から外す(アンマウント時など) */
13
+ remove: (name: string) => void;
14
+ /** すべての状態を破棄する */
15
+ reset: () => void;
16
+ /**
17
+ * 現在のスナップショット。
18
+ * 状態が変わらない限り同一参照を返すため、React の useSyncExternalStore に
19
+ * そのまま渡せる(参照が変わり続けると無限再描画になる)。
20
+ */
21
+ getSnapshot: () => FormValidationSnapshot;
22
+ /** 変更通知を購読する。戻り値を呼ぶと解除される */
23
+ subscribe: (listener: () => void) => () => void;
24
+ };
25
+ /**
26
+ * フォーム内の各入力のバリデーション状態をまとめて管理する。
27
+ *
28
+ * フレームワークに依存しない。
29
+ * Vue は `subscribe` を `onMounted` / `onUnmounted` で、
30
+ * React は `useSyncExternalStore(store.subscribe, store.getSnapshot)` で繋ぐ。
31
+ *
32
+ * ```ts
33
+ * const store = createFormValidationStore()
34
+ * store.setValid('startedOn', false)
35
+ * store.getSnapshot() // { isAllValid: false, invalidNames: ['startedOn'] }
36
+ * ```
37
+ */
38
+ export declare function createFormValidationStore(): FormValidationStore;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * フォーム内の各入力のバリデーション状態をまとめて管理する。
3
+ *
4
+ * フレームワークに依存しない。
5
+ * Vue は `subscribe` を `onMounted` / `onUnmounted` で、
6
+ * React は `useSyncExternalStore(store.subscribe, store.getSnapshot)` で繋ぐ。
7
+ *
8
+ * ```ts
9
+ * const store = createFormValidationStore()
10
+ * store.setValid('startedOn', false)
11
+ * store.getSnapshot() // { isAllValid: false, invalidNames: ['startedOn'] }
12
+ * ```
13
+ */
14
+ export function createFormValidationStore() {
15
+ const states = new Map();
16
+ const listeners = new Set();
17
+ let snapshot = { isAllValid: true, invalidNames: [] };
18
+ const buildSnapshot = () => {
19
+ const invalidNames = [];
20
+ states.forEach((isValid, name) => {
21
+ if (!isValid)
22
+ invalidNames.push(name);
23
+ });
24
+ return { isAllValid: invalidNames.length === 0, invalidNames };
25
+ };
26
+ const notify = () => {
27
+ const next = buildSnapshot();
28
+ // 内容が同じなら参照を据え置き、不要な再描画を起こさない
29
+ const isSame = next.isAllValid === snapshot.isAllValid &&
30
+ next.invalidNames.length === snapshot.invalidNames.length &&
31
+ next.invalidNames.every((name, index) => name === snapshot.invalidNames[index]);
32
+ if (isSame)
33
+ return;
34
+ snapshot = next;
35
+ listeners.forEach(listener => listener());
36
+ };
37
+ return {
38
+ setValid(name, isValid) {
39
+ if (states.get(name) === isValid)
40
+ return;
41
+ states.set(name, isValid);
42
+ notify();
43
+ },
44
+ isValid(name) {
45
+ return states.get(name) ?? true;
46
+ },
47
+ remove(name) {
48
+ if (!states.delete(name))
49
+ return;
50
+ notify();
51
+ },
52
+ reset() {
53
+ if (states.size === 0)
54
+ return;
55
+ states.clear();
56
+ notify();
57
+ },
58
+ getSnapshot() {
59
+ return snapshot;
60
+ },
61
+ subscribe(listener) {
62
+ listeners.add(listener);
63
+ return () => listeners.delete(listener);
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,6 @@
1
+ export * from './types';
2
+ export * from './constants';
3
+ export * from './validation';
4
+ export * from './date';
5
+ export * from './text';
6
+ export * from './form-validation-store';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './types';
2
+ export * from './constants';
3
+ export * from './validation';
4
+ export * from './date';
5
+ export * from './text';
6
+ export * from './form-validation-store';
package/dist/text.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** 全角の英数字を半角へ変換する。全角以外の文字はそのまま残す */
2
+ export declare function convertFullWidthToHalfWidth(value: string): string;
package/dist/text.js ADDED
@@ -0,0 +1,5 @@
1
+ const FULL_WIDTH_ALPHANUMERIC = /[A-Za-z0-9]/g;
2
+ /** 全角の英数字を半角へ変換する。全角以外の文字はそのまま残す */
3
+ export function convertFullWidthToHalfWidth(value) {
4
+ return value.replace(FULL_WIDTH_ALPHANUMERIC, character => String.fromCharCode(character.charCodeAt(0) - 0xfee0));
5
+ }
@@ -0,0 +1,55 @@
1
+ export type StateVariation = 'default' | 'error' | 'disabled' | 'valid' | 'focus' | 'hover';
2
+ export type BorderStyle = {
3
+ color: string;
4
+ size: string;
5
+ radius: string;
6
+ };
7
+ export type BaseStyle = {
8
+ textColor?: string;
9
+ backgroundColor?: string;
10
+ border?: BorderStyle;
11
+ boxShadow?: string;
12
+ };
13
+ export type StyleForEachStatus<T> = Partial<Record<StateVariation, T>> & {
14
+ default: T;
15
+ };
16
+ export type InputBoxStyle = BaseStyle & {
17
+ placeholderColor?: string;
18
+ };
19
+ export type InputBoxStyleForEachStatus = StyleForEachStatus<InputBoxStyle>;
20
+ export type ButtonStyle = BaseStyle & {
21
+ backgroundImage?: string;
22
+ };
23
+ export type ButtonStyleForEachStatus = StyleForEachStatus<ButtonStyle>;
24
+ export type CheckBoxStyle = Pick<BaseStyle, 'textColor' | 'border' | 'backgroundColor'>;
25
+ export type CheckBoxStyleForEachStatus = StyleForEachStatus<CheckBoxStyle>;
26
+ export type RadioButtonStyle = {
27
+ border?: Omit<BorderStyle, 'radius'>;
28
+ backgroundColor?: string;
29
+ };
30
+ export type RadioButtonStyleForEachStatus = StyleForEachStatus<RadioButtonStyle>;
31
+ export type SelectValue = string | number;
32
+ export type Option = {
33
+ label: string;
34
+ value: SelectValue;
35
+ order?: number;
36
+ isDisabled?: boolean;
37
+ };
38
+ export type Validate = {
39
+ regex: RegExp;
40
+ message: string;
41
+ };
42
+ export type Validates = Validate[];
43
+ /** 入力値として受け付ける型。数値 0 も正当な値として扱う */
44
+ export type InputValue = string | number | null | undefined;
45
+ /** DatePicker が扱う日付の分解表現 */
46
+ export type DateObject = {
47
+ year: string;
48
+ month: string;
49
+ day: string;
50
+ };
51
+ export type DateType = 'date' | 'month';
52
+ export type ValidationResult = {
53
+ isValid: boolean;
54
+ message: string;
55
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import type { InputValue, Validates } from './types';
2
+ /**
3
+ * 入力が空かどうか。
4
+ *
5
+ * 数値 0 や文字列 '0' は正当な入力なので空とみなさない。
6
+ * truthy 判定(`!value`)を使うと 0 が必須エラーになる。
7
+ */
8
+ export declare function isEmptyValue(value: InputValue): boolean;
9
+ /**
10
+ * validates の各 RegExp を値に適用し、一致しなかったものの message を返す。
11
+ *
12
+ * `g` / `y` フラグ付きの RegExp は `.test()` の呼び出しで `lastIndex` が変異し、
13
+ * 2 回目以降の判定結果が変わる。呼び出し側が渡したオブジェクトを変異させないよう、
14
+ * 毎回同じ source / flags のクローンを作って判定する。
15
+ * 新規インスタンスは lastIndex = 0 なので、sticky の意味を保ったまま結果が安定する。
16
+ */
17
+ export declare function runValidates(value: InputValue, validates: Validates): string[];
18
+ /**
19
+ * 必須チェックと validates をまとめて適用する。
20
+ * TextBox / TextArea 等の入力部品はこれを呼ぶだけでよい。
21
+ */
22
+ export declare function validateInputValue(value: InputValue, options?: {
23
+ isRequired?: boolean;
24
+ validates?: Validates;
25
+ }): string[];
@@ -0,0 +1,38 @@
1
+ import { MESSAGES } from './constants';
2
+ /**
3
+ * 入力が空かどうか。
4
+ *
5
+ * 数値 0 や文字列 '0' は正当な入力なので空とみなさない。
6
+ * truthy 判定(`!value`)を使うと 0 が必須エラーになる。
7
+ */
8
+ export function isEmptyValue(value) {
9
+ return value === '' || value === null || value === undefined;
10
+ }
11
+ /**
12
+ * validates の各 RegExp を値に適用し、一致しなかったものの message を返す。
13
+ *
14
+ * `g` / `y` フラグ付きの RegExp は `.test()` の呼び出しで `lastIndex` が変異し、
15
+ * 2 回目以降の判定結果が変わる。呼び出し側が渡したオブジェクトを変異させないよう、
16
+ * 毎回同じ source / flags のクローンを作って判定する。
17
+ * 新規インスタンスは lastIndex = 0 なので、sticky の意味を保ったまま結果が安定する。
18
+ */
19
+ export function runValidates(value, validates) {
20
+ if (isEmptyValue(value))
21
+ return [];
22
+ return validates
23
+ .filter(validate => {
24
+ const regex = new RegExp(validate.regex.source, validate.regex.flags);
25
+ return !regex.test(String(value));
26
+ })
27
+ .map(validate => validate.message);
28
+ }
29
+ /**
30
+ * 必須チェックと validates をまとめて適用する。
31
+ * TextBox / TextArea 等の入力部品はこれを呼ぶだけでよい。
32
+ */
33
+ export function validateInputValue(value, options = {}) {
34
+ const { isRequired = false, validates = [] } = options;
35
+ if (isEmptyValue(value))
36
+ return isRequired ? [MESSAGES.required] : [];
37
+ return runValidates(value, validates);
38
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@geckou/ui-core",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic logic shared by @geckou/ui-vue and @geckou/ui-react",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": ["dist", "README.md", "LICENSE"],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/geckou/ui.git",
20
+ "directory": "packages/core"
21
+ },
22
+ "author": "geckou-shogo <shogo.nojima@geckou.net>",
23
+ "license": "MIT",
24
+ "engines": { "node": ">=20.0.0" },
25
+ "publishConfig": {
26
+ "registry": "https://registry.npmjs.org/",
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
31
+ "type-check": "tsc --noEmit",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "prepublishOnly": "yarn build"
35
+ },
36
+ "devDependencies": {
37
+ "typescript": "^5.5.3",
38
+ "vitest": "^2.1.0"
39
+ }
40
+ }