@14ch/svelte-ui 0.0.17 → 0.0.19
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/dist/components/ConfirmDialog.svelte +3 -0
- package/dist/components/ConfirmDialog.svelte.d.ts +1 -0
- package/dist/components/Dialog.svelte +3 -0
- package/dist/components/Dialog.svelte.d.ts +1 -0
- package/dist/components/Drawer.svelte +3 -16
- package/dist/components/Drawer.svelte.d.ts +1 -0
- package/dist/components/Modal.svelte +20 -25
- package/dist/components/Modal.svelte.d.ts +1 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +32 -0
- package/dist/i18n/index.js +0 -3
- package/package.json +6 -2
- package/dist/components/COMPONENT_DESIGN_GUIDELINES.md +0 -127
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
// 状態/動作
|
|
28
28
|
isOpen?: boolean;
|
|
29
29
|
closeIfClickOutside?: boolean;
|
|
30
|
+
focusFirstOnOpen?: boolean;
|
|
30
31
|
|
|
31
32
|
// イベントハンドラー
|
|
32
33
|
onSubmit?: () => void;
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
// 状態/動作
|
|
52
53
|
isOpen = $bindable(false),
|
|
53
54
|
closeIfClickOutside = true,
|
|
55
|
+
focusFirstOnOpen = false,
|
|
54
56
|
|
|
55
57
|
// イベントハンドラー
|
|
56
58
|
onSubmit = () => {}, // No params for type inference
|
|
@@ -92,6 +94,7 @@
|
|
|
92
94
|
{width}
|
|
93
95
|
{scrollable}
|
|
94
96
|
{closeIfClickOutside}
|
|
97
|
+
{focusFirstOnOpen}
|
|
95
98
|
id={id ? `${id}-dialog` : undefined}
|
|
96
99
|
>
|
|
97
100
|
<div class="confirm-dialog-message">
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
scrollable?: boolean;
|
|
44
44
|
closeIfClickOutside?: boolean;
|
|
45
45
|
restoreFocus?: boolean;
|
|
46
|
+
focusFirstOnOpen?: boolean;
|
|
46
47
|
|
|
47
48
|
// ARIA/アクセシビリティ
|
|
48
49
|
ariaLabel?: string;
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
scrollable = false,
|
|
73
74
|
closeIfClickOutside = true,
|
|
74
75
|
restoreFocus = false,
|
|
76
|
+
focusFirstOnOpen = false,
|
|
75
77
|
|
|
76
78
|
// ARIA/アクセシビリティ
|
|
77
79
|
ariaLabel,
|
|
@@ -130,6 +132,7 @@
|
|
|
130
132
|
bind:isOpen
|
|
131
133
|
{closeIfClickOutside}
|
|
132
134
|
{restoreFocus}
|
|
135
|
+
{focusFirstOnOpen}
|
|
133
136
|
componentType="Dialog"
|
|
134
137
|
{ariaLabel}
|
|
135
138
|
{ariaLabelledby}
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
scrollable?: boolean;
|
|
45
45
|
closeIfClickOutside?: boolean;
|
|
46
46
|
restoreFocus?: boolean;
|
|
47
|
+
focusFirstOnOpen?: boolean;
|
|
47
48
|
|
|
48
49
|
// ARIA/アクセシビリティ
|
|
49
50
|
ariaLabel?: string;
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
scrollable = false,
|
|
75
76
|
closeIfClickOutside = true,
|
|
76
77
|
restoreFocus = false,
|
|
78
|
+
focusFirstOnOpen = false,
|
|
77
79
|
|
|
78
80
|
// ARIA/アクセシビリティ
|
|
79
81
|
ariaLabel = 'Drawer',
|
|
@@ -143,6 +145,7 @@
|
|
|
143
145
|
bind:isOpen
|
|
144
146
|
{closeIfClickOutside}
|
|
145
147
|
{restoreFocus}
|
|
148
|
+
{focusFirstOnOpen}
|
|
146
149
|
componentType="Drawer"
|
|
147
150
|
{ariaLabel}
|
|
148
151
|
{ariaLabelledby}
|
|
@@ -267,18 +270,10 @@
|
|
|
267
270
|
animation: fadeInFromRight var(--svelte-ui-transition-duration, 300ms) forwards;
|
|
268
271
|
}
|
|
269
272
|
|
|
270
|
-
:global(.drawer-wrapper--right.fade-in::backdrop) {
|
|
271
|
-
animation: fadeIn var(--svelte-ui-transition-duration, 300ms) forwards;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
273
|
:global(.drawer-wrapper--left.fade-in) {
|
|
275
274
|
animation: fadeInFromLeft var(--svelte-ui-transition-duration, 300ms) forwards;
|
|
276
275
|
}
|
|
277
276
|
|
|
278
|
-
:global(.drawer-wrapper--left.fade-in::backdrop) {
|
|
279
|
-
animation: fadeIn var(--svelte-ui-transition-duration, 300ms) forwards;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
277
|
:global(.drawer-wrapper--left.fade-out) {
|
|
283
278
|
animation: fadeOutToLeft var(--svelte-ui-transition-duration, 300ms) forwards;
|
|
284
279
|
}
|
|
@@ -287,10 +282,6 @@
|
|
|
287
282
|
animation: fadeOutToRight var(--svelte-ui-transition-duration, 300ms) forwards;
|
|
288
283
|
}
|
|
289
284
|
|
|
290
|
-
:global(.drawer-wrapper.fade-out::backdrop) {
|
|
291
|
-
animation: fadeOut var(--svelte-ui-transition-duration, 300ms) forwards;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
285
|
.drawer {
|
|
295
286
|
display: flex;
|
|
296
287
|
flex-direction: column;
|
|
@@ -362,14 +353,10 @@
|
|
|
362
353
|
/* Reduced motion support */
|
|
363
354
|
@media (prefers-reduced-motion: reduce) {
|
|
364
355
|
:global(.drawer-wrapper.fade-in),
|
|
365
|
-
:global(.drawer-wrapper.fade-in::backdrop),
|
|
366
356
|
:global(.drawer-wrapper.fade-out),
|
|
367
|
-
:global(.drawer-wrapper.fade-out::backdrop),
|
|
368
357
|
:global(.drawer-wrapper--left.fade-in),
|
|
369
|
-
:global(.drawer-wrapper--left.fade-in::backdrop),
|
|
370
358
|
:global(.drawer-wrapper--left.fade-out),
|
|
371
359
|
:global(.drawer-wrapper--right.fade-in),
|
|
372
|
-
:global(.drawer-wrapper--right.fade-in::backdrop),
|
|
373
360
|
:global(.drawer-wrapper--right.fade-out) {
|
|
374
361
|
animation-duration: 0.01s;
|
|
375
362
|
}
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
isOpen?: boolean;
|
|
34
34
|
closeIfClickOutside?: boolean;
|
|
35
35
|
restoreFocus?: boolean;
|
|
36
|
+
focusFirstOnOpen?: boolean;
|
|
36
37
|
|
|
37
38
|
// ARIA/アクセシビリティ
|
|
38
39
|
ariaLabel?: string;
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
isOpen = $bindable(false),
|
|
59
60
|
closeIfClickOutside = true,
|
|
60
61
|
restoreFocus = false,
|
|
62
|
+
focusFirstOnOpen = false,
|
|
61
63
|
|
|
62
64
|
// ARIA/アクセシビリティ
|
|
63
65
|
ariaLabel,
|
|
@@ -172,19 +174,11 @@
|
|
|
172
174
|
dialogRef.showModal();
|
|
173
175
|
|
|
174
176
|
setTimeout(() => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
firstFocusableElement?.focus();
|
|
179
|
-
|
|
177
|
+
if (!focusFirstOnOpen) {
|
|
178
|
+
dialogRef?.focus();
|
|
179
|
+
}
|
|
180
180
|
announceOpenClose(componentType, true, title || ariaLabel || '');
|
|
181
181
|
}, 0);
|
|
182
|
-
|
|
183
|
-
// 自動フォーカス時の枠線制御用クラス
|
|
184
|
-
dialogRef.classList.add('modal-opening');
|
|
185
|
-
setTimeout(() => {
|
|
186
|
-
dialogRef?.classList.remove('modal-opening');
|
|
187
|
-
}, 100); // 短い時間で制御
|
|
188
182
|
};
|
|
189
183
|
|
|
190
184
|
export const close = (title?: string): void => {
|
|
@@ -222,6 +216,7 @@
|
|
|
222
216
|
bind:this={dialogRef}
|
|
223
217
|
class="modal {customClass} {isOpen ? 'fade-in' : 'fade-out'}"
|
|
224
218
|
style={customStyles}
|
|
219
|
+
tabindex="-1"
|
|
225
220
|
aria-modal="true"
|
|
226
221
|
aria-label={ariaLabel}
|
|
227
222
|
aria-labelledby={ariaLabelledby}
|
|
@@ -265,11 +260,6 @@
|
|
|
265
260
|
outline-offset: var(--svelte-ui-focus-outline-offset-outer);
|
|
266
261
|
}
|
|
267
262
|
|
|
268
|
-
/* 自動フォーカス時の枠線制御用 */
|
|
269
|
-
.modal.modal-opening *:focus {
|
|
270
|
-
outline: none !important;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
263
|
.modal-contents {
|
|
274
264
|
width: 100%;
|
|
275
265
|
height: 100%;
|
|
@@ -294,22 +284,27 @@
|
|
|
294
284
|
opacity: 0;
|
|
295
285
|
}
|
|
296
286
|
}
|
|
297
|
-
.fade-in
|
|
298
|
-
.fade-in::backdrop {
|
|
287
|
+
.modal.fade-in:not([class*=drawer-wrapper]) {
|
|
299
288
|
animation: fadeIn var(--svelte-ui-transition-duration, 300ms) forwards;
|
|
300
289
|
}
|
|
301
290
|
|
|
302
|
-
.fade-
|
|
303
|
-
|
|
291
|
+
.modal.fade-in::backdrop {
|
|
292
|
+
animation: fadeIn var(--svelte-ui-transition-duration, 300ms) forwards;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.modal.fade-out:not([class*=drawer-wrapper]) {
|
|
296
|
+
animation: fadeOut var(--svelte-ui-transition-duration, 300ms) forwards;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.modal.fade-out::backdrop {
|
|
304
300
|
animation: fadeOut var(--svelte-ui-transition-duration, 300ms) forwards;
|
|
305
301
|
}
|
|
306
302
|
|
|
307
|
-
/* Reduced motion support */
|
|
308
303
|
@media (prefers-reduced-motion: reduce) {
|
|
309
|
-
.fade-in,
|
|
310
|
-
.fade-in::backdrop,
|
|
311
|
-
.fade-out,
|
|
312
|
-
.fade-out::backdrop {
|
|
304
|
+
.modal.fade-in,
|
|
305
|
+
.modal.fade-in::backdrop,
|
|
306
|
+
.modal.fade-out,
|
|
307
|
+
.modal.fade-out::backdrop {
|
|
313
308
|
animation-duration: 0.01s;
|
|
314
309
|
}
|
|
315
310
|
}</style>
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SvelteUiConfig = {
|
|
2
|
+
/**
|
|
3
|
+
* ライブラリ全体で使用するロケール。
|
|
4
|
+
* 各コンポーネントの props で locale が明示されている場合は、そちらが優先されます。
|
|
5
|
+
*/
|
|
6
|
+
locale?: 'en' | 'ja' | 'fr' | 'de' | 'es' | 'zh-cn';
|
|
7
|
+
/**
|
|
8
|
+
* グローバル / コンポーネントの locale がどちらも未指定の場合に、
|
|
9
|
+
* ブラウザの言語設定からロケールを推測するかどうか。
|
|
10
|
+
* SSR 環境では navigator がないため、このフォールバックは無効になり、'en' になります。
|
|
11
|
+
*/
|
|
12
|
+
useBrowserLocale?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare const setSvelteUiConfig: (newConfig: SvelteUiConfig) => void;
|
|
15
|
+
export declare const getSvelteUiConfig: () => SvelteUiConfig;
|
|
16
|
+
export declare const getLocale: () => SvelteUiConfig["locale"];
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
let config = {
|
|
2
|
+
useBrowserLocale: true
|
|
3
|
+
};
|
|
4
|
+
export const setSvelteUiConfig = (newConfig) => {
|
|
5
|
+
config = { ...config, ...newConfig };
|
|
6
|
+
};
|
|
7
|
+
export const getSvelteUiConfig = () => config;
|
|
8
|
+
// グローバル設定からロケールを取得
|
|
9
|
+
export const getLocale = () => {
|
|
10
|
+
// 1. グローバル設定で明示されていればそれを使う
|
|
11
|
+
if (config.locale) {
|
|
12
|
+
return config.locale;
|
|
13
|
+
}
|
|
14
|
+
// 2. ブラウザの言語設定から推測(SPA / CSR 時のみ有効)
|
|
15
|
+
if (config.useBrowserLocale !== false && typeof navigator !== 'undefined') {
|
|
16
|
+
const languages = navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language];
|
|
17
|
+
const primary = (languages[0] || '').toLowerCase();
|
|
18
|
+
if (primary.startsWith('ja'))
|
|
19
|
+
return 'ja';
|
|
20
|
+
if (primary.startsWith('fr'))
|
|
21
|
+
return 'fr';
|
|
22
|
+
if (primary.startsWith('de'))
|
|
23
|
+
return 'de';
|
|
24
|
+
if (primary.startsWith('es'))
|
|
25
|
+
return 'es';
|
|
26
|
+
if (primary.startsWith('zh'))
|
|
27
|
+
return 'zh-cn';
|
|
28
|
+
return 'en';
|
|
29
|
+
}
|
|
30
|
+
// 3. それ以外は English をデフォルトとする
|
|
31
|
+
return 'en';
|
|
32
|
+
};
|
package/dist/i18n/index.js
CHANGED
|
@@ -28,9 +28,6 @@ export const t = (key, params) => {
|
|
|
28
28
|
const locale = (globalLocale && globalLocale in TRANSLATIONS) ? globalLocale : 'en';
|
|
29
29
|
const message = key.split('.').reduce((obj, k) => obj?.[k], TRANSLATIONS[locale]);
|
|
30
30
|
if (typeof message !== 'string') {
|
|
31
|
-
if (import.meta.env.DEV) {
|
|
32
|
-
console.warn(`Translation key "${key}" not found for locale "${locale}"`);
|
|
33
|
-
}
|
|
34
31
|
return key;
|
|
35
32
|
}
|
|
36
33
|
return replaceParams(message, params);
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@14ch/svelte-ui",
|
|
3
3
|
"description": "Modern Svelte UI components library with TypeScript support",
|
|
4
4
|
"private": false,
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.19",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"svelte",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"dist/constants/",
|
|
42
42
|
"dist/assets/",
|
|
43
43
|
"dist/i18n/",
|
|
44
|
+
"dist/config.js",
|
|
45
|
+
"dist/config.d.ts",
|
|
44
46
|
"dist/index.js",
|
|
45
47
|
"dist/index.d.ts"
|
|
46
48
|
],
|
|
@@ -61,7 +63,8 @@
|
|
|
61
63
|
"test:browser": "vitest --config vitest.config.ts --run",
|
|
62
64
|
"storybook": "storybook dev -p 6006",
|
|
63
65
|
"build-storybook": "storybook build",
|
|
64
|
-
"
|
|
66
|
+
"verify-package": "node scripts/verify-package.js",
|
|
67
|
+
"prepublishOnly": "npm run package && npm run verify-package",
|
|
65
68
|
"publish": "npm publish"
|
|
66
69
|
},
|
|
67
70
|
"devDependencies": {
|
|
@@ -103,6 +106,7 @@
|
|
|
103
106
|
"sass": "^1.89.2"
|
|
104
107
|
},
|
|
105
108
|
"peerDependencies": {
|
|
109
|
+
"@sveltejs/kit": "^2.0.0",
|
|
106
110
|
"svelte": "^5.0.0"
|
|
107
111
|
}
|
|
108
112
|
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
# コンポーネント設計ガイドライン
|
|
2
|
-
|
|
3
|
-
## ID命名ルール
|
|
4
|
-
|
|
5
|
-
### コンポーネント内包時のID受け渡しルール
|
|
6
|
-
|
|
7
|
-
#### 基本ルール
|
|
8
|
-
|
|
9
|
-
- **親コンポーネント**: `id`プロパティを受け取る
|
|
10
|
-
- **子コンポーネント**: 受け取った`id`にsuffixを付けて自身のIDを生成
|
|
11
|
-
- **内部要素**: 受け取った`id`にsuffixを付けてIDを生成
|
|
12
|
-
|
|
13
|
-
#### 命名パターン
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
// 親 → 子コンポーネント
|
|
17
|
-
<ChildComponent id={id ? `${id}-child-suffix` : undefined} />
|
|
18
|
-
|
|
19
|
-
// 内部要素
|
|
20
|
-
<div id={id ? `${id}-element-suffix` : undefined}>
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
#### 具体例
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
// ConfirmDialog → Dialog → Modal
|
|
27
|
-
<Dialog id={id ? `${id}-dialog` : undefined} />
|
|
28
|
-
<Modal id={id ? `${id}-modal` : undefined} />
|
|
29
|
-
|
|
30
|
-
// 内部要素
|
|
31
|
-
<div id={id ? `${id}-dialog-title` : undefined}>
|
|
32
|
-
<div id={id ? `${id}-modal-description` : undefined}>
|
|
33
|
-
|
|
34
|
-
// 他の例
|
|
35
|
-
<Input id={id ? `${id}-input` : undefined} />
|
|
36
|
-
<Popup id={id ? `${id}-popup` : undefined} />
|
|
37
|
-
<DatepickerCalendar id={id ? `${id}-calendar` : undefined} />
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
#### 判断基準
|
|
41
|
-
|
|
42
|
-
- **✅ IDを渡すべき**: 1対1の親子関係、アクセシビリティ上重要な要素、テストで個別特定が必要
|
|
43
|
-
- **❌ IDを渡さない**: 同じコンポーネントが複数存在、内部実装の詳細、動的生成要素
|
|
44
|
-
|
|
45
|
-
### 理由
|
|
46
|
-
|
|
47
|
-
- **階層構造の明確化**: `${id}-suffix`パターンにより、コンポーネントの階層関係が分かりやすい
|
|
48
|
-
- **グローバル一意性**: 親のIDをベースにすることで、DOM全体で一意なIDを保証
|
|
49
|
-
- **アクセシビリティ**: ARIA属性との連携で、スクリーンリーダーなどの支援技術に対応
|
|
50
|
-
- **テストの安定性**: 一意なIDにより、テストでの要素特定が確実
|
|
51
|
-
|
|
52
|
-
## コンポーネント設計原則
|
|
53
|
-
|
|
54
|
-
### プロパティ設計
|
|
55
|
-
|
|
56
|
-
- **基本プロパティ**: 必須の機能に関わるプロパティ
|
|
57
|
-
- **HTML属性**: 標準的なHTML属性(id, class, style等)
|
|
58
|
-
- **スタイル/レイアウト**: 見た目やレイアウトに関わるプロパティ
|
|
59
|
-
- **状態/動作**: コンポーネントの動作状態に関わるプロパティ
|
|
60
|
-
- **ARIA/アクセシビリティ**: アクセシビリティに関わるプロパティ
|
|
61
|
-
- **イベントハンドラー**: イベント処理に関わるプロパティ
|
|
62
|
-
|
|
63
|
-
### イベントハンドラー設計
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
// Svelte 5の推奨パターン
|
|
67
|
-
onclick = () => {}, // パラメータなしで型推論を可能にする
|
|
68
|
-
onchange = () => {}, // パラメータなしで型推論を可能にする
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### データテストID
|
|
72
|
-
|
|
73
|
-
- 最上位のDOM要素に`data-testid`を設定
|
|
74
|
-
- テストでの要素特定を容易にする
|
|
75
|
-
- コンポーネント名をベースに命名(例: `data-testid="dialog"`)
|
|
76
|
-
|
|
77
|
-
## 実装例
|
|
78
|
-
|
|
79
|
-
### 基本的なコンポーネント構造
|
|
80
|
-
|
|
81
|
-
```svelte
|
|
82
|
-
<script lang="ts">
|
|
83
|
-
// Props, States & Constants
|
|
84
|
-
let {
|
|
85
|
-
// 基本プロパティ
|
|
86
|
-
title = 'Default Title',
|
|
87
|
-
|
|
88
|
-
// HTML属性
|
|
89
|
-
id,
|
|
90
|
-
|
|
91
|
-
// スタイル/レイアウト
|
|
92
|
-
variant = 'default',
|
|
93
|
-
|
|
94
|
-
// 状態/動作
|
|
95
|
-
isOpen = $bindable(false),
|
|
96
|
-
|
|
97
|
-
// イベントハンドラー
|
|
98
|
-
onclick = () => {}
|
|
99
|
-
}: {
|
|
100
|
-
// 型定義
|
|
101
|
-
title?: string;
|
|
102
|
-
id?: string;
|
|
103
|
-
variant?: 'default' | 'primary';
|
|
104
|
-
isOpen?: boolean;
|
|
105
|
-
onclick?: () => void;
|
|
106
|
-
} = $props();
|
|
107
|
-
</script>
|
|
108
|
-
|
|
109
|
-
<div class="component" {id} data-testid="component">
|
|
110
|
-
<!-- コンテンツ -->
|
|
111
|
-
</div>
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### 内包コンポーネントの例
|
|
115
|
-
|
|
116
|
-
```svelte
|
|
117
|
-
<script lang="ts">
|
|
118
|
-
let { id }: { id?: string } = $props();
|
|
119
|
-
</script>
|
|
120
|
-
|
|
121
|
-
<div class="parent" {id} data-testid="parent">
|
|
122
|
-
<ChildComponent id={id ? `${id}-child` : undefined} />
|
|
123
|
-
<div id={id ? `${id}-element` : undefined}>
|
|
124
|
-
<!-- 内部要素 -->
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
```
|