@simplysm/solid 13.0.85 → 13.0.86

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 (116) hide show
  1. package/README.md +143 -28
  2. package/dist/components/data/list/ListItem.d.ts.map +1 -1
  3. package/dist/components/data/list/ListItem.js +11 -4
  4. package/dist/components/data/list/ListItem.js.map +2 -2
  5. package/dist/components/data/list/ListItem.styles.d.ts +2 -0
  6. package/dist/components/data/list/ListItem.styles.d.ts.map +1 -1
  7. package/dist/components/data/list/ListItem.styles.js +11 -1
  8. package/dist/components/data/list/ListItem.styles.js.map +1 -1
  9. package/dist/components/features/crud-sheet/CrudSheet.d.ts.map +1 -1
  10. package/dist/components/features/crud-sheet/CrudSheet.js +7 -0
  11. package/dist/components/features/crud-sheet/CrudSheet.js.map +2 -2
  12. package/dist/components/features/data-select-button/DataSelectButton.d.ts.map +1 -1
  13. package/dist/components/features/data-select-button/DataSelectButton.js +30 -26
  14. package/dist/components/features/data-select-button/DataSelectButton.js.map +2 -2
  15. package/dist/components/features/permission-table/PermissionTable.js +5 -1
  16. package/dist/components/features/permission-table/PermissionTable.js.map +2 -2
  17. package/dist/components/form-control/DropdownTrigger.styles.js +1 -1
  18. package/dist/components/form-control/combobox/Combobox.d.ts +19 -5
  19. package/dist/components/form-control/combobox/Combobox.d.ts.map +1 -1
  20. package/dist/components/form-control/combobox/Combobox.js +2 -4
  21. package/dist/components/form-control/combobox/Combobox.js.map +1 -1
  22. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts +2 -2
  23. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts.map +1 -1
  24. package/dist/components/form-control/date-range-picker/DateRangePicker.js +10 -1
  25. package/dist/components/form-control/date-range-picker/DateRangePicker.js.map +2 -2
  26. package/dist/components/form-control/editor/RichTextEditor.d.ts +2 -2
  27. package/dist/components/form-control/editor/RichTextEditor.d.ts.map +1 -1
  28. package/dist/components/form-control/editor/RichTextEditor.js +2 -2
  29. package/dist/components/form-control/editor/RichTextEditor.js.map +1 -1
  30. package/dist/components/form-control/field/DatePicker.d.ts +2 -2
  31. package/dist/components/form-control/field/DatePicker.d.ts.map +1 -1
  32. package/dist/components/form-control/field/DatePicker.js.map +1 -1
  33. package/dist/components/form-control/field/DateTimePicker.d.ts +2 -2
  34. package/dist/components/form-control/field/DateTimePicker.d.ts.map +1 -1
  35. package/dist/components/form-control/field/DateTimePicker.js.map +1 -1
  36. package/dist/components/form-control/field/Field.styles.d.ts +6 -7
  37. package/dist/components/form-control/field/Field.styles.d.ts.map +1 -1
  38. package/dist/components/form-control/field/Field.styles.js.map +1 -1
  39. package/dist/components/form-control/field/NumberInput.d.ts +2 -2
  40. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  41. package/dist/components/form-control/field/NumberInput.js.map +1 -1
  42. package/dist/components/form-control/field/TextInput.d.ts +2 -2
  43. package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
  44. package/dist/components/form-control/field/TextInput.js.map +1 -1
  45. package/dist/components/form-control/field/Textarea.d.ts +2 -2
  46. package/dist/components/form-control/field/Textarea.d.ts.map +1 -1
  47. package/dist/components/form-control/field/Textarea.js.map +1 -1
  48. package/dist/components/form-control/field/TimePicker.d.ts +2 -2
  49. package/dist/components/form-control/field/TimePicker.d.ts.map +1 -1
  50. package/dist/components/form-control/field/TimePicker.js.map +1 -1
  51. package/dist/components/form-control/numpad/Numpad.d.ts.map +1 -1
  52. package/dist/components/form-control/numpad/Numpad.js +4 -17
  53. package/dist/components/form-control/numpad/Numpad.js.map +2 -2
  54. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  55. package/dist/components/form-control/select/Select.js +19 -6
  56. package/dist/components/form-control/select/Select.js.map +2 -2
  57. package/dist/components/form-control/state-preset/StatePreset.d.ts +1 -3
  58. package/dist/components/form-control/state-preset/StatePreset.d.ts.map +1 -1
  59. package/dist/components/form-control/state-preset/StatePreset.js +69 -91
  60. package/dist/components/form-control/state-preset/StatePreset.js.map +2 -2
  61. package/dist/components/layout/FormGroup.js +1 -1
  62. package/dist/components/layout/FormGroup.js.map +1 -1
  63. package/dist/components/layout/FormTable.js +3 -3
  64. package/dist/components/layout/FormTable.js.map +1 -1
  65. package/dist/components/layout/sidebar/Sidebar.d.ts.map +1 -1
  66. package/dist/components/layout/sidebar/Sidebar.js +3 -6
  67. package/dist/components/layout/sidebar/Sidebar.js.map +2 -2
  68. package/dist/providers/i18n/locales/en.d.ts +2 -3
  69. package/dist/providers/i18n/locales/en.d.ts.map +1 -1
  70. package/dist/providers/i18n/locales/en.js +3 -4
  71. package/dist/providers/i18n/locales/en.js.map +1 -1
  72. package/dist/providers/i18n/locales/ko.d.ts +2 -3
  73. package/dist/providers/i18n/locales/ko.d.ts.map +1 -1
  74. package/dist/providers/i18n/locales/ko.js +3 -4
  75. package/dist/providers/i18n/locales/ko.js.map +1 -1
  76. package/docs/display-feedback.md +279 -0
  77. package/docs/features.md +357 -213
  78. package/docs/form-controls.md +261 -403
  79. package/docs/layout-data.md +386 -0
  80. package/docs/providers-hooks.md +411 -0
  81. package/package.json +5 -5
  82. package/src/components/data/list/ListItem.styles.ts +14 -2
  83. package/src/components/data/list/ListItem.tsx +13 -4
  84. package/src/components/features/crud-sheet/CrudSheet.tsx +8 -0
  85. package/src/components/features/data-select-button/DataSelectButton.tsx +39 -32
  86. package/src/components/features/permission-table/PermissionTable.tsx +1 -1
  87. package/src/components/form-control/DropdownTrigger.styles.ts +1 -1
  88. package/src/components/form-control/combobox/Combobox.tsx +42 -16
  89. package/src/components/form-control/date-range-picker/DateRangePicker.tsx +6 -4
  90. package/src/components/form-control/editor/RichTextEditor.tsx +5 -6
  91. package/src/components/form-control/field/DatePicker.tsx +3 -2
  92. package/src/components/form-control/field/DateTimePicker.tsx +3 -2
  93. package/src/components/form-control/field/Field.styles.ts +6 -8
  94. package/src/components/form-control/field/NumberInput.tsx +3 -2
  95. package/src/components/form-control/field/TextInput.tsx +3 -2
  96. package/src/components/form-control/field/Textarea.tsx +3 -2
  97. package/src/components/form-control/field/TimePicker.tsx +3 -2
  98. package/src/components/form-control/numpad/Numpad.tsx +16 -18
  99. package/src/components/form-control/select/Select.tsx +19 -5
  100. package/src/components/form-control/state-preset/StatePreset.tsx +32 -57
  101. package/src/components/layout/FormGroup.tsx +1 -1
  102. package/src/components/layout/FormTable.tsx +3 -3
  103. package/src/components/layout/sidebar/Sidebar.tsx +2 -3
  104. package/src/providers/i18n/locales/en.ts +2 -3
  105. package/src/providers/i18n/locales/ko.ts +2 -3
  106. package/tests/components/features/data-select-button/DataSelectButton.spec.tsx +62 -7
  107. package/tests/components/form-control/combobox/Combobox.spec.tsx +3 -3
  108. package/tests/components/form-control/date-range-picker/DateRangePicker.spec.tsx +56 -0
  109. package/docs/data.md +0 -204
  110. package/docs/disclosure.md +0 -146
  111. package/docs/display.md +0 -125
  112. package/docs/feedback.md +0 -156
  113. package/docs/helpers.md +0 -173
  114. package/docs/hooks.md +0 -146
  115. package/docs/layout.md +0 -94
  116. package/docs/providers.md +0 -180
@@ -0,0 +1,411 @@
1
+ # 프로바이더 & 훅
2
+
3
+ ## SystemProvider
4
+
5
+ 모든 필수 프로바이더를 한 번에 감싸는 편의 컴포넌트. 대부분의 앱에서는 이것 하나로 충분하다.
6
+
7
+ ```tsx
8
+ import { SystemProvider } from "@simplysm/solid";
9
+
10
+ <SystemProvider clientName="my-app" busyVariant="spinner">
11
+ <App />
12
+ </SystemProvider>
13
+ ```
14
+
15
+ | Prop | 타입 | 설명 |
16
+ |------|------|------|
17
+ | `clientName` | `string` | 클라이언트 식별 이름 (localStorage 키 접두사 등에 사용) |
18
+ | `busyVariant` | `"spinner" \| "bar"` | 글로벌 BusyProvider 변형 |
19
+
20
+ 내부 프로바이더 스택 (위에서 아래 순서):
21
+ `ConfigProvider` > `I18nProvider` > `SyncStorageProvider` > `LoggerProvider` > `NotificationProvider` + `NotificationBanner` > `ErrorLoggerProvider` > `PwaUpdateProvider` > `ClipboardProvider` > `ThemeProvider` > `ServiceClientProvider` > `SharedDataProvider` > `BusyProvider`
22
+
23
+ ---
24
+
25
+ ## ThemeProvider
26
+
27
+ 라이트/다크/시스템 테마 관리. localStorage에 설정 저장.
28
+
29
+ ```tsx
30
+ import { ThemeProvider, useTheme } from "@simplysm/solid";
31
+
32
+ <ThemeProvider>
33
+ <App />
34
+ </ThemeProvider>
35
+
36
+ const theme = useTheme();
37
+
38
+ theme.mode(); // "light" | "dark" | "system"
39
+ theme.resolvedTheme(); // "light" | "dark" (OS 설정 반영)
40
+ theme.setMode("dark");
41
+ theme.cycleMode(); // light → system → dark → light
42
+ ```
43
+
44
+ ---
45
+
46
+ ## I18nProvider
47
+
48
+ 다국어 지원. 한국어(ko), 영어(en) 내장.
49
+
50
+ ```tsx
51
+ import { I18nProvider, useI18n } from "@simplysm/solid";
52
+
53
+ <I18nProvider>
54
+ <App />
55
+ </I18nProvider>
56
+
57
+ const i18n = useI18n();
58
+
59
+ i18n.t("save"); // 번역 조회
60
+ i18n.t("greeting", { name: "Alice" }); // 파라미터 치환
61
+ i18n.locale(); // 현재 로케일
62
+ i18n.setLocale("en");
63
+
64
+ // 사전 확장
65
+ i18n.configure({
66
+ dictionaries: {
67
+ ko: { myKey: "내 값" },
68
+ en: { myKey: "My Value" },
69
+ },
70
+ });
71
+ ```
72
+
73
+ ---
74
+
75
+ ## SharedDataProvider
76
+
77
+ 서버 데이터를 구독하고 실시간 동기화하는 프로바이더. `ServiceClientProvider`와 `NotificationProvider` 내부에서 사용해야 한다.
78
+
79
+ ```tsx
80
+ import { SharedDataProvider, useSharedData } from "@simplysm/solid";
81
+
82
+ // 프로바이더 설정 (SystemProvider 사용 시 자동 포함)
83
+ <SharedDataProvider>
84
+ <App />
85
+ </SharedDataProvider>
86
+
87
+ // 데이터 정의 (자식 컴포넌트에서 한 번만 호출)
88
+ const sharedData = useSharedData<{
89
+ users: User;
90
+ departments: Department;
91
+ }>();
92
+
93
+ sharedData.configure(() => ({
94
+ users: {
95
+ fetch: async (changeKeys) => await api.getUsers(changeKeys),
96
+ getKey: (item) => item.id,
97
+ orderBy: [[(item) => item.name, "asc"]],
98
+ itemSearchText: (item) => item.name,
99
+ isItemHidden: (item) => item.isDeleted,
100
+ },
101
+ departments: {
102
+ fetch: async (changeKeys) => await api.getDepartments(changeKeys),
103
+ getKey: (item) => item.id,
104
+ orderBy: [[(item) => item.sortOrder, "asc"]],
105
+ getParentKey: (item) => item.parentId, // 트리 구조 지원
106
+ },
107
+ }));
108
+
109
+ // 데이터 사용
110
+ const users = sharedData.users.items(); // 반응형 배열
111
+ const user = sharedData.users.get(userId); // 키로 조회
112
+
113
+ // 변경 이벤트 발행 (서버의 모든 구독자에게 전파)
114
+ await sharedData.users.emit([changedUserId]);
115
+
116
+ // 전체 로딩 대기
117
+ await sharedData.wait();
118
+ ```
119
+
120
+ ### SharedDataDefinition
121
+
122
+ | 속성 | 타입 | 설명 |
123
+ |------|------|------|
124
+ | `fetch` | `(changeKeys?) => Promise<TData[]>` | 데이터 조회 함수 |
125
+ | `getKey` | `(item) => string \| number` | 항목 고유 키 추출 |
126
+ | `orderBy` | `[(item) => unknown, "asc" \| "desc"][]` | 정렬 기준 (다중) |
127
+ | `serviceKey` | `string` | 서비스 연결 키 (기본: `"default"`) |
128
+ | `filter` | `unknown` | 서버 이벤트 필터 |
129
+ | `itemSearchText` | `(item) => string` | 검색 텍스트 추출 |
130
+ | `isItemHidden` | `(item) => boolean` | 숨김 여부 |
131
+ | `getParentKey` | `(item) => string \| number \| undefined` | 부모 키 (트리 구조) |
132
+
133
+ ### SharedDataAccessor
134
+
135
+ | 메서드/속성 | 타입 | 설명 |
136
+ |------------|------|------|
137
+ | `items()` | `Accessor<TData[]>` | 반응형 데이터 배열 |
138
+ | `get(key)` | `(key) => TData \| undefined` | 키로 단건 조회 |
139
+ | `emit(changeKeys?)` | `(keys?) => Promise<void>` | 변경 이벤트 발행 |
140
+ | `getKey` | `(item) => string \| number` | 키 추출 함수 |
141
+
142
+ ---
143
+
144
+ ## 기타 프로바이더
145
+
146
+ | 프로바이더 | 설명 |
147
+ |-----------|------|
148
+ | `ConfigProvider` | 클라이언트 설정 (localStorage 키 접두사). prop: `clientName: string` |
149
+ | `ServiceClientProvider` | `@simplysm/service-client` WebSocket 연결 통합 |
150
+ | `SyncStorageProvider` | localStorage 동기화 |
151
+ | `LoggerProvider` | 로깅 설정 |
152
+ | `ErrorLoggerProvider` | 글로벌 에러 핸들링 (window.onerror 등) |
153
+ | `ClipboardProvider` | 클립보드 기능 |
154
+ | `PwaUpdateProvider` | PWA Service Worker 업데이트 감지 (5분 간격 폴링, 알림 표시) |
155
+
156
+ ---
157
+
158
+ ## 훅
159
+
160
+ ### createControllableSignal
161
+
162
+ 제어/비제어 컴포넌트 패턴 구현.
163
+
164
+ ```typescript
165
+ import { createControllableSignal } from "@simplysm/solid";
166
+
167
+ const [value, setValue] = createControllableSignal({
168
+ value: () => props.value,
169
+ onChange: () => props.onValueChange,
170
+ });
171
+ ```
172
+
173
+ ### createControllableStore
174
+
175
+ 객체 상태용 제어/비제어 패턴.
176
+
177
+ ### createIMEHandler
178
+
179
+ IME(한글 등) 입력 처리.
180
+
181
+ ```typescript
182
+ const ime = createIMEHandler();
183
+ // ime.handleCompositionStart()
184
+ // ime.handleCompositionEnd()
185
+ // ime.handleInput()
186
+ // ime.composingValue()
187
+ ```
188
+
189
+ ### useLocalStorage
190
+
191
+ 반응형 localStorage.
192
+
193
+ ```typescript
194
+ import { useLocalStorage } from "@simplysm/solid";
195
+
196
+ const [theme, setTheme] = useLocalStorage("theme", "light");
197
+ ```
198
+
199
+ ### useSyncConfig
200
+
201
+ 클라이언트명 접두사 붙은 localStorage 동기화.
202
+
203
+ ```typescript
204
+ const [config, setConfig] = useSyncConfig("my-setting", defaultValue);
205
+ ```
206
+
207
+ ### useLogger
208
+
209
+ ```typescript
210
+ const logger = useLogger();
211
+ ```
212
+
213
+ ### useRouterLink
214
+
215
+ ```typescript
216
+ const navigate = useRouterLink();
217
+ navigate("/users");
218
+ ```
219
+
220
+ ### createMountTransition
221
+
222
+ 마운트 애니메이션 상태 관리.
223
+
224
+ ```typescript
225
+ const { mounted, animating, unmount } = createMountTransition(() => isVisible());
226
+ ```
227
+
228
+ ---
229
+
230
+ ## createAppStructure
231
+
232
+ 앱 메뉴, 라우트, 권한 구조를 선언적으로 정의한다. 모듈별 필터링과 권한 기반 접근 제어를 지원한다.
233
+
234
+ ```typescript
235
+ import { createAppStructure } from "@simplysm/solid";
236
+ import type { AppStructureItem } from "@simplysm/solid";
237
+
238
+ type Module = "basic" | "pro" | "enterprise";
239
+
240
+ const items: AppStructureItem<Module>[] = [
241
+ {
242
+ code: "admin",
243
+ title: "관리",
244
+ children: [
245
+ {
246
+ code: "users",
247
+ title: "사용자 관리",
248
+ component: UserPage,
249
+ perms: ["use", "edit"],
250
+ modules: ["basic"],
251
+ },
252
+ {
253
+ code: "roles",
254
+ title: "역할 관리",
255
+ component: RolePage,
256
+ perms: ["use", "edit"],
257
+ modules: ["pro"],
258
+ subPerms: [
259
+ { code: "advanced", title: "고급 설정", perms: ["use", "edit"], modules: ["enterprise"] },
260
+ ],
261
+ },
262
+ ],
263
+ },
264
+ ];
265
+
266
+ const { AppStructureProvider, useAppStructure } = createAppStructure(() => ({
267
+ items,
268
+ usableModules: () => activeModules(),
269
+ permRecord: () => userPermissions(),
270
+ }));
271
+
272
+ // 프로바이더 설정
273
+ <AppStructureProvider>
274
+ <App />
275
+ </AppStructureProvider>
276
+
277
+ // 사용
278
+ const app = useAppStructure();
279
+
280
+ app.usableRoutes(); // 접근 가능한 라우트 배열
281
+ app.usableMenus(); // 접근 가능한 메뉴 트리
282
+ app.usableFlatMenus(); // 플랫 메뉴 배열 (검색용)
283
+ app.usablePerms(); // 권한 트리 (PermissionTable에 전달)
284
+ app.allFlatPerms; // 모든 권한 목록 (관리용)
285
+ app.perms; // 타입 안전한 권한 객체 (app.perms.admin.users.use)
286
+
287
+ app.getTitleChainByHref("/admin/users"); // ["관리", "사용자 관리"]
288
+ ```
289
+
290
+ ### AppStructure 반환 타입
291
+
292
+ | 속성 | 타입 | 설명 |
293
+ |------|------|------|
294
+ | `usableRoutes` | `Accessor<AppRoute[]>` | 모듈/권한 필터링된 라우트 |
295
+ | `usableMenus` | `Accessor<AppMenu[]>` | 필터링된 메뉴 트리 |
296
+ | `usableFlatMenus` | `Accessor<AppFlatMenu[]>` | 플랫 메뉴 배열 |
297
+ | `usablePerms` | `Accessor<AppPerm[]>` | 필터링된 권한 트리 |
298
+ | `allFlatPerms` | `AppFlatPerm[]` | 전체 권한 목록 |
299
+ | `perms` | `InferPerms<TItems>` | 타입 추론된 권한 접근 객체 |
300
+ | `getTitleChainByHref` | `(href: string) => string[]` | href로 타이틀 체인 조회 |
301
+
302
+ ---
303
+
304
+ ## 스타일 유틸리티
305
+
306
+ ### 기본 스타일
307
+
308
+ ```typescript
309
+ import { bg, border, text } from "@simplysm/solid";
310
+
311
+ // Tailwind 클래스 프리셋
312
+ bg.surface // bg-white dark:bg-base-900
313
+ bg.muted // bg-base-100 dark:bg-base-800
314
+ bg.subtle // bg-base-200 dark:bg-base-700
315
+ border.default // border-base-200 dark:border-base-700
316
+ text.default // text-base-900 dark:text-base-100
317
+ text.muted // text-base-400 dark:text-base-500
318
+ ```
319
+
320
+ ### 컨트롤 스타일
321
+
322
+ ```typescript
323
+ import { pad, gap } from "@simplysm/solid";
324
+
325
+ pad.xs // px-1 py-0
326
+ pad.sm // px-1.5 py-0.5
327
+ pad.md // px-2 py-1
328
+ pad.lg // px-3 py-2
329
+ pad.xl // px-4 py-3
330
+
331
+ gap.xs // gap-0
332
+ gap.sm // gap-0.5
333
+ gap.md // gap-1
334
+ gap.lg // gap-1.5
335
+ gap.xl // gap-2
336
+ ```
337
+
338
+ ### 테마 토큰
339
+
340
+ ```typescript
341
+ import { themeTokens, type SemanticTheme } from "@simplysm/solid";
342
+
343
+ // 각 semantic theme(base, primary, success, warning, danger, info)별:
344
+ themeTokens.primary.solid // bg-primary-500 text-white
345
+ themeTokens.primary.solidHover // hover:bg-primary-600 dark:hover:bg-primary-400
346
+ themeTokens.primary.light // bg-primary-100 text-primary-900 ...
347
+ themeTokens.primary.text // text-primary-600 dark:text-primary-400
348
+ themeTokens.primary.hoverBg // hover:bg-primary-100 ...
349
+ themeTokens.primary.border // border-primary-300 ...
350
+ ```
351
+
352
+ ---
353
+
354
+ ## 디렉티브
355
+
356
+ ### ripple
357
+
358
+ Material Design 스타일 리플 효과.
359
+
360
+ ```tsx
361
+ import { ripple } from "@simplysm/solid";
362
+
363
+ <button use:ripple>클릭</button>
364
+ ```
365
+
366
+ ---
367
+
368
+ ## 헬퍼
369
+
370
+ ### mergeStyles
371
+
372
+ 인라인 CSS 문자열 병합.
373
+
374
+ ```typescript
375
+ import { mergeStyles } from "@simplysm/solid";
376
+
377
+ mergeStyles("color: red", "font-size: 14px"); // "color: red; font-size: 14px"
378
+ ```
379
+
380
+ ### createSlot / createSlots
381
+
382
+ 컴포넌트 합성을 위한 슬롯 패턴. `createSlot`은 단일 슬롯, `createSlots`는 복수 슬롯을 지원한다.
383
+
384
+ ```typescript
385
+ import { createSlot } from "@simplysm/solid";
386
+
387
+ // 슬롯 정의
388
+ const [MySlot, createMySlotAccessor] = createSlot<{ children: JSX.Element }>();
389
+
390
+ // 컴포넌트 내부에서 슬롯 접근
391
+ const [slotValue, SlotProvider] = createMySlotAccessor();
392
+
393
+ // 사용
394
+ <SlotProvider>
395
+ <MySlot><span>슬롯 내용</span></MySlot>
396
+ {/* slotValue()로 접근 */}
397
+ </SlotProvider>
398
+ ```
399
+
400
+ ### startPointerDrag
401
+
402
+ 포인터 드래그 인터랙션 관리. 포인터 캡처를 설정하고 move/end 이벤트를 관리한다.
403
+
404
+ ```typescript
405
+ import { startPointerDrag } from "@simplysm/solid";
406
+
407
+ startPointerDrag(element, event.pointerId, {
408
+ onMove: (e) => { /* 드래그 중 */ },
409
+ onEnd: (e) => { /* 드래그 종료 */ },
410
+ });
411
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/solid",
3
- "version": "13.0.85",
3
+ "version": "13.0.86",
4
4
  "description": "Simplysm package - SolidJS library",
5
5
  "author": "simplysm",
6
6
  "license": "Apache-2.0",
@@ -52,10 +52,10 @@
52
52
  "tabbable": "^6.4.0",
53
53
  "tailwind-merge": "^3.5.0",
54
54
  "tailwindcss": "^3.4.19",
55
- "@simplysm/core-browser": "13.0.85",
56
- "@simplysm/core-common": "13.0.85",
57
- "@simplysm/service-client": "13.0.85",
58
- "@simplysm/service-common": "13.0.85"
55
+ "@simplysm/core-browser": "13.0.86",
56
+ "@simplysm/core-common": "13.0.86",
57
+ "@simplysm/service-client": "13.0.86",
58
+ "@simplysm/service-common": "13.0.86"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@solidjs/testing-library": "^0.8.10"
@@ -40,8 +40,20 @@ export const listItemDisabledClass = clsx("pointer-events-none cursor-auto opaci
40
40
  // Readonly state
41
41
  export const listItemReadonlyClass = clsx("cursor-auto select-text hover:bg-transparent");
42
42
 
43
- // Indent guide (for nested items)
44
- export const listItemIndentGuideClass = clsx("ml-4 w-2 border-l", border.default);
43
+ // Indent guide (for nested items, absolutely positioned)
44
+ export const listItemIndentGuideClass = clsx("absolute inset-y-0 border-l", border.default);
45
+
46
+ // Base left padding per size (rem) for indent calculation
47
+ export const listItemBasePadLeft: Record<ComponentSize, number> = {
48
+ xs: 0.25,
49
+ sm: 0.375,
50
+ md: 0.5,
51
+ lg: 0.75,
52
+ xl: 1,
53
+ };
54
+
55
+ // Indent size per nesting level (rem)
56
+ export const LIST_ITEM_INDENT_SIZE = 1.5;
45
57
 
46
58
  // Item content area
47
59
  export const listItemContentClass = clsx("flex flex-1 flex-row", "items-center gap-1", "text-left");
@@ -23,6 +23,8 @@ import {
23
23
  listItemIndentGuideClass,
24
24
  listItemContentClass,
25
25
  getListItemSelectedIconClass,
26
+ listItemBasePadLeft,
27
+ LIST_ITEM_INDENT_SIZE,
26
28
  } from "./ListItem.styles";
27
29
  import type { ComponentSize } from "../../../styles/control.styles";
28
30
 
@@ -153,6 +155,13 @@ const ListItemInner: ParentComponent<ListItemProps> = (props) => {
153
155
 
154
156
  const getSelectedIconClassName = () => getListItemSelectedIconClass(local.selected ?? false);
155
157
 
158
+ const indentPaddingLeft = level > 1
159
+ ? `${listItemBasePadLeft[local.size ?? "md"] + (level - 1) * LIST_ITEM_INDENT_SIZE}rem`
160
+ : undefined;
161
+
162
+ const getGuideLeft = () =>
163
+ `${listItemBasePadLeft[local.size ?? "md"] + (level - 1) * LIST_ITEM_INDENT_SIZE + LIST_ITEM_INDENT_SIZE * 0.5}rem`;
164
+
156
165
  return (
157
166
  <ChildrenProvider>
158
167
  <button
@@ -160,7 +169,7 @@ const ListItemInner: ParentComponent<ListItemProps> = (props) => {
160
169
  type="button"
161
170
  use:ripple={useRipple()}
162
171
  class={getHeaderClassName()}
163
- style={local.style}
172
+ style={{ ...(local.style as JSX.CSSProperties), "padding-left": indentPaddingLeft }}
164
173
  data-list-item
165
174
  role="treeitem"
166
175
  aria-expanded={hasChildren() ? openState() : undefined}
@@ -187,9 +196,9 @@ const ListItemInner: ParentComponent<ListItemProps> = (props) => {
187
196
  </button>
188
197
  <Show when={hasChildren()}>
189
198
  <Collapse open={openState()} data-collapsed={!openState() || undefined}>
190
- <div class="flex">
191
- <div class={listItemIndentGuideClass} />
192
- <List inset class="flex-1">
199
+ <div class="relative">
200
+ <div class={listItemIndentGuideClass} style={{ left: getGuideLeft() }} />
201
+ <List inset>
193
202
  {childrenSlot()!.children}
194
203
  </List>
195
204
  </div>
@@ -348,6 +348,14 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, unknown>>(
348
348
 
349
349
  setSelectedKeys(merged);
350
350
  setSelection(newSelection);
351
+
352
+ // Auto-confirm for single selection mode in dialog
353
+ if (isInDialog && local.selectionMode === "single" && newSelection.length > 0) {
354
+ local.onSelect?.({
355
+ items: newSelection,
356
+ keys: [...merged],
357
+ });
358
+ }
351
359
  }
352
360
 
353
361
  function clearSelection() {
@@ -149,7 +149,6 @@ export function DataSelectButton<
149
149
  "onValueChange",
150
150
  "load",
151
151
  "dialog",
152
- "dialogProps",
153
152
  "dialogOptions",
154
153
  "renderItem",
155
154
  "multiple",
@@ -174,9 +173,9 @@ export function DataSelectButton<
174
173
  // Controlled/uncontrolled pattern
175
174
  type ValueType = TKey | TKey[] | undefined;
176
175
  const [getValue, setValue] = createControllableSignal<ValueType>({
177
- value: () => local.value,
176
+ value: () => local.value as ValueType,
178
177
  onChange: () => local.onValueChange as ((v: ValueType) => void) | undefined,
179
- } as Parameters<typeof createControllableSignal<ValueType>>[0]);
178
+ });
180
179
 
181
180
  // Track keys for loading
182
181
  const [loadKeys, setLoadKeys] = createSignal<TKey[]>(normalizeKeys(local.value));
@@ -216,27 +215,38 @@ export function DataSelectButton<
216
215
  return local.validate?.(v);
217
216
  });
218
217
 
218
+ // Dialog open state for aria-expanded
219
+ const [isDialogOpen, setIsDialogOpen] = createSignal(false);
220
+
219
221
  // Open dialog
220
222
  const handleOpenDialog = async () => {
221
223
  if (local.disabled) return;
222
224
 
223
- const result = (await dialog.show(
224
- local.dialog,
225
- {
226
- ...((local as any).dialogProps ?? {}),
225
+ setIsDialogOpen(true);
226
+ try {
227
+ const dialogProps =
228
+ (props as { dialogProps?: Record<string, unknown> }).dialogProps ?? {};
229
+ const showProps = {
230
+ ...dialogProps,
227
231
  selectionMode: local.multiple ? "multiple" : "single",
228
232
  selectedKeys: normalizeKeys(getValue()) as (string | number)[],
229
- },
230
- local.dialogOptions,
231
- )) as DataSelectDialogResult<TKey> | undefined;
233
+ } as Omit<TDialogProps, "close">;
234
+ const result = (await dialog.show(
235
+ local.dialog as Component<SelectDialogBaseProps<TKey>>,
236
+ showProps as Omit<SelectDialogBaseProps<TKey>, "close">,
237
+ local.dialogOptions,
238
+ ));
232
239
 
233
- if (result) {
234
- const newKeys = result.selectedKeys;
235
- if (local.multiple) {
236
- setValue(newKeys);
237
- } else {
238
- setValue(newKeys.length > 0 ? newKeys[0] : undefined);
240
+ if (result) {
241
+ const newKeys = result.selectedKeys;
242
+ if (local.multiple) {
243
+ setValue(newKeys);
244
+ } else {
245
+ setValue(newKeys.length > 0 ? newKeys[0] : undefined);
246
+ }
239
247
  }
248
+ } finally {
249
+ setIsDialogOpen(false);
240
250
  }
241
251
  };
242
252
 
@@ -283,23 +293,17 @@ export function DataSelectButton<
283
293
  return (
284
294
  <Invalid message={errorMsg()} variant="border" lazyValidation={local.lazyValidation}>
285
295
  <div data-data-select-button class="group inline-flex items-center">
286
- <div
287
- role="combobox"
296
+ <button
297
+ type="button"
298
+ data-trigger
288
299
  aria-haspopup="dialog"
289
- aria-expanded={false}
290
- aria-disabled={local.disabled || undefined}
300
+ aria-expanded={isDialogOpen()}
291
301
  aria-required={local.required || undefined}
292
- tabIndex={local.disabled ? -1 : 0}
293
- class={triggerClassName()}
294
- onKeyDown={(e) => {
295
- if (local.disabled) return;
296
- if (e.key === "Enter" || e.key === " ") {
297
- e.preventDefault();
298
- void handleOpenDialog();
299
- }
300
- }}
302
+ disabled={local.disabled || undefined}
303
+ class={twMerge("appearance-none font-inherit text-inherit text-left", triggerClassName())}
304
+ onClick={() => void handleOpenDialog()}
301
305
  >
302
- <div class="flex-1 truncate">{renderSelectedDisplay()}</div>
306
+ <div class="flex-1 whitespace-nowrap">{renderSelectedDisplay()}</div>
303
307
  <div class={clsx("flex items-center", gap.sm)}>
304
308
  <Show when={clearable()}>
305
309
  <button
@@ -318,7 +322,10 @@ export function DataSelectButton<
318
322
  type="button"
319
323
  data-search-button
320
324
  class={twMerge(actionButtonClass, text.muted, "hover:text-primary-500")}
321
- onClick={() => void handleOpenDialog()}
325
+ onClick={(e) => {
326
+ e.stopPropagation();
327
+ void handleOpenDialog();
328
+ }}
322
329
  tabIndex={-1}
323
330
  aria-label={i18n.t("dataSelectButton.search")}
324
331
  >
@@ -326,7 +333,7 @@ export function DataSelectButton<
326
333
  </button>
327
334
  </Show>
328
335
  </div>
329
- </div>
336
+ </button>
330
337
  </div>
331
338
  </Invalid>
332
339
  );
@@ -288,7 +288,7 @@ export const PermissionTable: Component<PermissionTableProps> = (props) => {
288
288
  </DataSheet.Column>
289
289
  <For each={allPerms()}>
290
290
  {(perm) => (
291
- <DataSheet.Column key={`perm-${perm}`} header={perm} sortable={false} resizable={false}>
291
+ <DataSheet.Column key={`perm-${perm}`} header={(() => { const key = `permissionTable.${perm}`; const translated = i18n.t(key); return translated === key ? perm : translated; })()} sortable={false} resizable={false}>
292
292
  {(ctx) => {
293
293
  const item = ctx.item as AppPerm;
294
294
  return (
@@ -5,7 +5,7 @@ import { type ComponentSize, gap, pad } from "../../styles/control.styles";
5
5
 
6
6
  export const triggerBaseClass = clsx(
7
7
  "inline-flex items-center gap-2",
8
- "w-40",
8
+ "min-w-40",
9
9
  "border",
10
10
  border.default,
11
11
  "rounded",