@simplysm/solid 13.0.62 → 13.0.64

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 (86) hide show
  1. package/README.md +6 -0
  2. package/dist/components/data/crud-detail/CrudDetail.d.ts.map +1 -1
  3. package/dist/components/data/crud-detail/CrudDetail.js +62 -41
  4. package/dist/components/data/crud-detail/CrudDetail.js.map +2 -2
  5. package/dist/components/data/crud-sheet/CrudSheet.d.ts.map +1 -1
  6. package/dist/components/data/crud-sheet/CrudSheet.js +164 -19
  7. package/dist/components/data/crud-sheet/CrudSheet.js.map +2 -2
  8. package/dist/components/data/crud-sheet/types.d.ts +9 -3
  9. package/dist/components/data/crud-sheet/types.d.ts.map +1 -1
  10. package/dist/components/data/sheet/DataSheet.d.ts.map +1 -1
  11. package/dist/components/data/sheet/DataSheet.js +3 -2
  12. package/dist/components/data/sheet/DataSheet.js.map +2 -2
  13. package/dist/components/form-control/checkbox/Checkbox.d.ts.map +1 -1
  14. package/dist/components/form-control/checkbox/Checkbox.js +10 -10
  15. package/dist/components/form-control/checkbox/Checkbox.js.map +2 -2
  16. package/dist/components/form-control/checkbox/Checkbox.styles.d.ts.map +1 -1
  17. package/dist/components/form-control/checkbox/Checkbox.styles.js +2 -2
  18. package/dist/components/form-control/checkbox/Checkbox.styles.js.map +1 -1
  19. package/dist/components/form-control/checkbox/Radio.d.ts.map +1 -1
  20. package/dist/components/form-control/checkbox/Radio.js +13 -13
  21. package/dist/components/form-control/checkbox/Radio.js.map +2 -2
  22. package/dist/components/form-control/data-select-button/DataSelectButton.d.ts +38 -0
  23. package/dist/components/form-control/data-select-button/DataSelectButton.d.ts.map +1 -0
  24. package/dist/components/form-control/data-select-button/DataSelectButton.js +184 -0
  25. package/dist/components/form-control/data-select-button/DataSelectButton.js.map +6 -0
  26. package/dist/components/form-control/select/Select.d.ts +7 -3
  27. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  28. package/dist/components/form-control/select/Select.js +146 -45
  29. package/dist/components/form-control/select/Select.js.map +2 -2
  30. package/dist/components/form-control/select-list/SelectList.d.ts +54 -0
  31. package/dist/components/form-control/select-list/SelectList.d.ts.map +1 -0
  32. package/dist/components/form-control/select-list/SelectList.js +280 -0
  33. package/dist/components/form-control/select-list/SelectList.js.map +6 -0
  34. package/dist/components/form-control/select-list/SelectListContext.d.ts +13 -0
  35. package/dist/components/form-control/select-list/SelectListContext.d.ts.map +1 -0
  36. package/dist/components/form-control/select-list/SelectListContext.js +14 -0
  37. package/dist/components/form-control/select-list/SelectListContext.js.map +6 -0
  38. package/dist/components/form-control/shared-data/SharedDataSelect.d.ts +32 -0
  39. package/dist/components/form-control/shared-data/SharedDataSelect.d.ts.map +1 -0
  40. package/dist/components/form-control/shared-data/SharedDataSelect.js +74 -0
  41. package/dist/components/form-control/shared-data/SharedDataSelect.js.map +6 -0
  42. package/dist/components/form-control/shared-data/SharedDataSelectButton.d.ts +29 -0
  43. package/dist/components/form-control/shared-data/SharedDataSelectButton.d.ts.map +1 -0
  44. package/dist/components/form-control/shared-data/SharedDataSelectButton.js +17 -0
  45. package/dist/components/form-control/shared-data/SharedDataSelectButton.js.map +6 -0
  46. package/dist/components/form-control/shared-data/SharedDataSelectList.d.ts +29 -0
  47. package/dist/components/form-control/shared-data/SharedDataSelectList.d.ts.map +1 -0
  48. package/dist/components/form-control/shared-data/SharedDataSelectList.js +80 -0
  49. package/dist/components/form-control/shared-data/SharedDataSelectList.js.map +6 -0
  50. package/dist/features/address/AddressSearch.d.ts +8 -0
  51. package/dist/features/address/AddressSearch.d.ts.map +1 -0
  52. package/dist/features/address/AddressSearch.js +72 -0
  53. package/dist/features/address/AddressSearch.js.map +6 -0
  54. package/dist/index.d.ts +6 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +6 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/providers/shared-data/SharedDataContext.d.ts +14 -0
  59. package/dist/providers/shared-data/SharedDataContext.d.ts.map +1 -1
  60. package/dist/providers/shared-data/SharedDataContext.js.map +1 -1
  61. package/dist/providers/shared-data/SharedDataProvider.d.ts.map +1 -1
  62. package/dist/providers/shared-data/SharedDataProvider.js +5 -1
  63. package/dist/providers/shared-data/SharedDataProvider.js.map +2 -2
  64. package/docs/data-components.md +15 -4
  65. package/docs/form-controls.md +257 -0
  66. package/docs/hooks.md +30 -0
  67. package/docs/providers.md +7 -0
  68. package/package.json +3 -3
  69. package/src/components/data/crud-detail/CrudDetail.tsx +51 -26
  70. package/src/components/data/crud-sheet/CrudSheet.tsx +157 -20
  71. package/src/components/data/crud-sheet/types.ts +13 -3
  72. package/src/components/data/sheet/DataSheet.tsx +6 -7
  73. package/src/components/form-control/checkbox/Checkbox.styles.ts +2 -2
  74. package/src/components/form-control/checkbox/Checkbox.tsx +18 -20
  75. package/src/components/form-control/checkbox/Radio.tsx +18 -20
  76. package/src/components/form-control/data-select-button/DataSelectButton.tsx +279 -0
  77. package/src/components/form-control/select/Select.tsx +192 -36
  78. package/src/components/form-control/select-list/SelectList.tsx +385 -0
  79. package/src/components/form-control/select-list/SelectListContext.ts +23 -0
  80. package/src/components/form-control/shared-data/SharedDataSelect.tsx +101 -0
  81. package/src/components/form-control/shared-data/SharedDataSelectButton.tsx +47 -0
  82. package/src/components/form-control/shared-data/SharedDataSelectList.tsx +85 -0
  83. package/src/features/address/AddressSearch.tsx +75 -0
  84. package/src/index.ts +18 -0
  85. package/src/providers/shared-data/SharedDataContext.ts +14 -0
  86. package/src/providers/shared-data/SharedDataProvider.tsx +4 -0
@@ -436,6 +436,263 @@ import { Select } from "@simplysm/solid";
436
436
 
437
437
  ---
438
438
 
439
+ ## SelectList
440
+
441
+ List-style single selection component with built-in search, pagination, and slot-based customization. Uses compound component pattern with `SelectList.Header`, `SelectList.Filter`, and `SelectList.ItemTemplate`.
442
+
443
+ ```tsx
444
+ import { SelectList } from "@simplysm/solid";
445
+
446
+ // Basic usage with search
447
+ <SelectList
448
+ items={users()}
449
+ value={selectedUser()}
450
+ onValueChange={setSelectedUser}
451
+ getSearchText={(user) => user.name}
452
+ >
453
+ <SelectList.ItemTemplate>
454
+ {(user) => <>{user.name} ({user.email})</>}
455
+ </SelectList.ItemTemplate>
456
+ </SelectList>
457
+
458
+ // With pagination and header
459
+ <SelectList
460
+ items={items()}
461
+ value={selected()}
462
+ onValueChange={setSelected}
463
+ pageSize={20}
464
+ header="Select an item"
465
+ >
466
+ <SelectList.ItemTemplate>
467
+ {(item) => <>{item.name}</>}
468
+ </SelectList.ItemTemplate>
469
+ </SelectList>
470
+
471
+ // With custom header and filter slots
472
+ <SelectList
473
+ items={items()}
474
+ value={selected()}
475
+ onValueChange={setSelected}
476
+ >
477
+ <SelectList.Header>
478
+ <div class="flex items-center gap-1">Custom Header</div>
479
+ </SelectList.Header>
480
+ <SelectList.Filter>
481
+ <MyCustomFilter />
482
+ </SelectList.Filter>
483
+ <SelectList.ItemTemplate>
484
+ {(item) => <>{item.label}</>}
485
+ </SelectList.ItemTemplate>
486
+ </SelectList>
487
+
488
+ // With canChange guard
489
+ <SelectList
490
+ items={items()}
491
+ value={selected()}
492
+ onValueChange={setSelected}
493
+ canChange={async (item) => {
494
+ if (hasUnsavedChanges()) return confirm("Discard changes?");
495
+ return true;
496
+ }}
497
+ >
498
+ <SelectList.ItemTemplate>
499
+ {(item) => <>{item.name}</>}
500
+ </SelectList.ItemTemplate>
501
+ </SelectList>
502
+ ```
503
+
504
+ | Prop | Type | Default | Description |
505
+ |------|------|---------|-------------|
506
+ | `items` | `T[]` | **(required)** | Item array |
507
+ | `value` | `T` | - | Selected value |
508
+ | `onValueChange` | `(value: T \| undefined) => void` | - | Value change callback |
509
+ | `required` | `boolean` | - | Required (hides "unspecified" item) |
510
+ | `disabled` | `boolean` | - | Disabled state |
511
+ | `getSearchText` | `(item: T) => string` | - | Search text extractor (auto-shows search input) |
512
+ | `getIsHidden` | `(item: T) => boolean` | - | Hidden item filter |
513
+ | `filterFn` | `(item: T, index: number) => boolean` | - | Custom filter function |
514
+ | `canChange` | `(item: T \| undefined) => boolean \| Promise<boolean>` | - | Change guard (return `false` to block) |
515
+ | `pageSize` | `number` | - | Page size (auto-shows pagination) |
516
+ | `header` | `string` | - | Header text |
517
+ | `class` | `string` | - | CSS class |
518
+ | `style` | `JSX.CSSProperties` | - | Inline style |
519
+
520
+ **Sub-components:**
521
+ - `SelectList.Header` -- Custom header slot (overrides `header` prop)
522
+ - `SelectList.Filter` -- Custom filter slot (overrides default search input)
523
+ - `SelectList.ItemTemplate` -- Item rendering template
524
+
525
+ ---
526
+
527
+ ## DataSelectButton
528
+
529
+ Modal-based selection button. Displays selected items inline and opens a dialog for selection. Supports single and multiple selection with key-based loading.
530
+
531
+ ```tsx
532
+ import { DataSelectButton, type DataSelectModalResult } from "@simplysm/solid";
533
+
534
+ // Single selection
535
+ <DataSelectButton
536
+ value={selectedUserId()}
537
+ onValueChange={setSelectedUserId}
538
+ load={async (keys) => await api.getUsersByIds(keys)}
539
+ modal={() => <UserSelectModal />}
540
+ renderItem={(user) => <>{user.name}</>}
541
+ />
542
+
543
+ // Multiple selection
544
+ <DataSelectButton
545
+ value={selectedIds()}
546
+ onValueChange={setSelectedIds}
547
+ multiple
548
+ load={async (keys) => await api.getItemsByIds(keys)}
549
+ modal={() => <ItemSelectModal />}
550
+ renderItem={(item) => <>{item.name}</>}
551
+ />
552
+ ```
553
+
554
+ The modal should return `DataSelectModalResult<TKey>` via `dialogInstance.close({ selectedKeys: [...] })`.
555
+
556
+ | Prop | Type | Default | Description |
557
+ |------|------|---------|-------------|
558
+ | `value` | `TKey \| TKey[]` | - | Selected key(s) |
559
+ | `onValueChange` | `(value: TKey \| TKey[] \| undefined) => void` | - | Value change callback |
560
+ | `load` | `(keys: TKey[]) => TItem[] \| Promise<TItem[]>` | **(required)** | Load items by keys |
561
+ | `modal` | `() => JSX.Element` | **(required)** | Selection modal factory |
562
+ | `renderItem` | `(item: TItem) => JSX.Element` | **(required)** | Item display renderer |
563
+ | `multiple` | `boolean` | - | Multiple selection mode |
564
+ | `required` | `boolean` | - | Required field |
565
+ | `disabled` | `boolean` | - | Disabled state |
566
+ | `size` | `"xs" \| "sm" \| "lg" \| "xl"` | - | Size |
567
+ | `inset` | `boolean` | - | Inset style |
568
+ | `validate` | `(value: unknown) => string \| undefined` | - | Custom validation function |
569
+ | `touchMode` | `boolean` | - | Show error only after focus loss |
570
+ | `dialogOptions` | `DialogShowOptions` | - | Dialog options for modal |
571
+
572
+ **DataSelectModalResult:**
573
+
574
+ ```typescript
575
+ interface DataSelectModalResult<TKey> {
576
+ selectedKeys: TKey[];
577
+ }
578
+ ```
579
+
580
+ ---
581
+
582
+ ## SharedData Wrappers
583
+
584
+ Convenience wrappers that connect `SharedDataAccessor` (from `useSharedData()`) to `Select`, `DataSelectButton`, and `SelectList` components. They auto-wire `items`, tree structure (`getChildren`), search, and hidden filtering from the shared data definition.
585
+
586
+ ### SharedDataSelect
587
+
588
+ Wraps `Select` with `SharedDataAccessor`. Optionally adds search modal and edit modal action buttons.
589
+
590
+ ```tsx
591
+ import { SharedDataSelect } from "@simplysm/solid";
592
+
593
+ const sharedData = useSharedData<MySharedData>();
594
+
595
+ <SharedDataSelect data={sharedData.departments} value={deptId()} onValueChange={setDeptId}>
596
+ {(dept) => <>{dept.name}</>}
597
+ </SharedDataSelect>
598
+
599
+ // With search modal and edit modal
600
+ <SharedDataSelect
601
+ data={sharedData.users}
602
+ value={userId()}
603
+ onValueChange={setUserId}
604
+ modal={() => <UserSearchModal />}
605
+ editModal={() => <UserEditModal />}
606
+ >
607
+ {(user) => <>{user.name}</>}
608
+ </SharedDataSelect>
609
+ ```
610
+
611
+ | Prop | Type | Default | Description |
612
+ |------|------|---------|-------------|
613
+ | `data` | `SharedDataAccessor<TItem>` | **(required)** | Shared data accessor |
614
+ | `value` | `unknown` | - | Selected value |
615
+ | `onValueChange` | `(value: unknown) => void` | - | Value change callback |
616
+ | `multiple` | `boolean` | - | Multiple selection |
617
+ | `required` | `boolean` | - | Required field |
618
+ | `disabled` | `boolean` | - | Disabled state |
619
+ | `size` | `"xs" \| "sm" \| "lg" \| "xl"` | - | Size |
620
+ | `inset` | `boolean` | - | Inset style |
621
+ | `filterFn` | `(item: TItem, index: number) => boolean` | - | Item filter function |
622
+ | `modal` | `() => JSX.Element` | - | Search modal factory (adds search icon action) |
623
+ | `editModal` | `() => JSX.Element` | - | Edit modal factory (adds edit icon action) |
624
+ | `children` | `(item: TItem, index: number, depth: number) => JSX.Element` | **(required)** | Item render function |
625
+
626
+ ### SharedDataSelectButton
627
+
628
+ Wraps `DataSelectButton` with `SharedDataAccessor`. Auto-wires `load` from shared data items.
629
+
630
+ ```tsx
631
+ import { SharedDataSelectButton } from "@simplysm/solid";
632
+
633
+ const sharedData = useSharedData<MySharedData>();
634
+
635
+ <SharedDataSelectButton
636
+ data={sharedData.users}
637
+ value={userId()}
638
+ onValueChange={setUserId}
639
+ modal={() => <UserSelectModal />}
640
+ >
641
+ {(user) => <>{user.name}</>}
642
+ </SharedDataSelectButton>
643
+ ```
644
+
645
+ | Prop | Type | Default | Description |
646
+ |------|------|---------|-------------|
647
+ | `data` | `SharedDataAccessor<TItem>` | **(required)** | Shared data accessor |
648
+ | `value` | `TKey \| TKey[]` | - | Selected key(s) |
649
+ | `onValueChange` | `(value: TKey \| TKey[] \| undefined) => void` | - | Value change callback |
650
+ | `multiple` | `boolean` | - | Multiple selection |
651
+ | `required` | `boolean` | - | Required field |
652
+ | `disabled` | `boolean` | - | Disabled state |
653
+ | `size` | `"xs" \| "sm" \| "lg" \| "xl"` | - | Size |
654
+ | `inset` | `boolean` | - | Inset style |
655
+ | `modal` | `() => JSX.Element` | **(required)** | Selection modal factory |
656
+ | `children` | `(item: TItem) => JSX.Element` | **(required)** | Item render function |
657
+
658
+ ### SharedDataSelectList
659
+
660
+ Wraps `SelectList` with `SharedDataAccessor`. Optionally adds a management modal link in header.
661
+
662
+ ```tsx
663
+ import { SharedDataSelectList } from "@simplysm/solid";
664
+
665
+ const sharedData = useSharedData<MySharedData>();
666
+
667
+ <SharedDataSelectList
668
+ data={sharedData.categories}
669
+ value={selectedCategory()}
670
+ onValueChange={setSelectedCategory}
671
+ header="Category"
672
+ modal={() => <CategoryManageModal />}
673
+ >
674
+ <SelectList.ItemTemplate>
675
+ {(cat) => <>{cat.name}</>}
676
+ </SelectList.ItemTemplate>
677
+ </SharedDataSelectList>
678
+ ```
679
+
680
+ | Prop | Type | Default | Description |
681
+ |------|------|---------|-------------|
682
+ | `data` | `SharedDataAccessor<TItem>` | **(required)** | Shared data accessor |
683
+ | `value` | `TItem` | - | Selected value |
684
+ | `onValueChange` | `(value: TItem \| undefined) => void` | - | Value change callback |
685
+ | `required` | `boolean` | - | Required field |
686
+ | `disabled` | `boolean` | - | Disabled state |
687
+ | `filterFn` | `(item: TItem, index: number) => boolean` | - | Item filter function |
688
+ | `canChange` | `(item: TItem \| undefined) => boolean \| Promise<boolean>` | - | Change guard |
689
+ | `pageSize` | `number` | - | Page size (auto-shows pagination) |
690
+ | `header` | `string` | - | Header text |
691
+ | `modal` | `() => JSX.Element` | - | Management modal factory (adds link icon in header) |
692
+ | `children` | `JSX.Element` | **(required)** | Sub-component children (e.g., `SelectList.ItemTemplate`) |
693
+
694
+ ---
695
+
439
696
  ## Combobox
440
697
 
441
698
  Autocomplete component with async search and item selection support. Debouncing is built-in.
package/docs/hooks.md CHANGED
@@ -269,6 +269,36 @@ const {
269
269
 
270
270
  ---
271
271
 
272
+ ## createSlotSignal
273
+
274
+ Signal factory for slot registration pattern. Used internally by compound components (`SelectList`, etc.) to register child slot content.
275
+
276
+ ```tsx
277
+ import { createSlotSignal, type SlotAccessor } from "@simplysm/solid";
278
+
279
+ const [headerSlot, setHeader] = createSlotSignal();
280
+
281
+ // Register slot content (typically from a sub-component)
282
+ setHeader(() => <div>Header Content</div>);
283
+
284
+ // Read slot content (in parent render)
285
+ <Show when={headerSlot()}>
286
+ {headerSlot()!()}
287
+ </Show>
288
+
289
+ // Unregister
290
+ setHeader(undefined);
291
+ ```
292
+
293
+ | Return | Type | Description |
294
+ |--------|------|-------------|
295
+ | `[0]` | `Accessor<SlotAccessor>` | Slot accessor (`(() => JSX.Element) \| undefined`) |
296
+ | `[1]` | `(content: SlotAccessor) => void` | Slot setter |
297
+
298
+ **`SlotAccessor` type:** `(() => JSX.Element) | undefined`
299
+
300
+ ---
301
+
272
302
  ## useRouterLink
273
303
 
274
304
  `@solidjs/router`-based navigation hook. Automatically handles Ctrl/Alt + click (new tab), Shift + click (new window), and regular click (SPA routing).
package/docs/providers.md CHANGED
@@ -106,6 +106,9 @@ interface SharedDataDefinition<TData> {
106
106
  getKey: (item: TData) => string | number; // Primary key extractor
107
107
  orderBy: [(item: TData) => unknown, "asc" | "desc"][]; // Sort order
108
108
  filter?: unknown; // Optional filter for change events
109
+ getSearchText?: (item: TData) => string; // Search text extractor (for SelectList/Select filtering)
110
+ getIsHidden?: (item: TData) => boolean; // Hidden item filter
111
+ getParentKey?: (item: TData) => string | number | undefined; // Parent key extractor (for tree structure)
109
112
  }
110
113
  ```
111
114
 
@@ -116,6 +119,10 @@ interface SharedDataDefinition<TData> {
116
119
  | `items` | `Accessor<TData[]>` | Reactive item array |
117
120
  | `get` | `(key: string \| number \| undefined) => TData \| undefined` | Get item by key |
118
121
  | `emit` | `(changeKeys?: Array<string \| number>) => Promise<void>` | Emit change event to server (triggers refetch in all subscribers) |
122
+ | `getKey` | `(item: TData) => string \| number` | Primary key extractor |
123
+ | `getSearchText` | `((item: TData) => string) \| undefined` | Search text extractor |
124
+ | `getIsHidden` | `((item: TData) => boolean) \| undefined` | Hidden item filter |
125
+ | `getParentKey` | `((item: TData) => string \| number \| undefined) \| undefined` | Parent key extractor (tree structure) |
119
126
 
120
127
  **SharedDataValue extras:**
121
128
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/solid",
3
- "version": "13.0.62",
3
+ "version": "13.0.64",
4
4
  "description": "심플리즘 패키지 - SolidJS 라이브러리",
5
5
  "author": "김석래",
6
6
  "license": "Apache-2.0",
@@ -50,8 +50,8 @@
50
50
  "solid-tiptap": "^0.8.0",
51
51
  "tailwind-merge": "^3.5.0",
52
52
  "tailwindcss": "^3.4.19",
53
- "@simplysm/core-common": "13.0.62",
54
- "@simplysm/core-browser": "13.0.62"
53
+ "@simplysm/core-common": "13.0.64",
54
+ "@simplysm/core-browser": "13.0.64"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@solidjs/testing-library": "^0.8.10"
@@ -200,6 +200,23 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
200
200
  저장
201
201
  </Button>
202
202
  </Show>
203
+ <Show
204
+ when={
205
+ canEdit() && local.toggleDelete && info() && !info()!.isNew && (local.deletable ?? true)
206
+ }
207
+ >
208
+ {(_) => (
209
+ <Button
210
+ size="lg"
211
+ variant="ghost"
212
+ theme="danger"
213
+ onClick={() => void handleToggleDelete()}
214
+ >
215
+ <Icon icon={info()!.isDeleted ? IconTrashOff : IconTrash} class="mr-1" />
216
+ {info()!.isDeleted ? "복구" : "삭제"}
217
+ </Button>
218
+ )}
219
+ </Show>
203
220
  <Button size="lg" variant="ghost" theme="info" onClick={() => void handleRefresh()}>
204
221
  <Icon icon={IconRefresh} class="mr-1" />
205
222
  새로고침
@@ -255,38 +272,46 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
255
272
  busy={busyCount() > 0}
256
273
  class={clsx("flex h-full flex-col", local.class)}
257
274
  >
258
- {/* Toolbar (page/control mode) */}
259
- <Show when={!isModal && canEdit()}>
275
+ {/* Toolbar */}
276
+ <Show when={(!isModal && !topbarCtx) || defs().tools}>
260
277
  <div class="flex gap-2 p-2 pb-0">
261
- <Show when={local.submit}>
262
- <Button
263
- size="sm"
264
- theme="primary"
265
- variant="ghost"
266
- onClick={() => formRef?.requestSubmit()}
267
- >
268
- <Icon icon={IconDeviceFloppy} class="mr-1" />
269
- 저장
270
- </Button>
271
- </Show>
272
- <Button size="sm" theme="info" variant="ghost" onClick={() => void handleRefresh()}>
273
- <Icon icon={IconRefresh} class="mr-1" />
274
- 새로고침
275
- </Button>
276
- <Show
277
- when={local.toggleDelete && info() && !info()!.isNew && (local.deletable ?? true)}
278
- >
279
- {(_) => (
278
+ <Show when={!topbarCtx && !isModal}>
279
+ <Show when={canEdit() && local.submit}>
280
280
  <Button
281
281
  size="sm"
282
- theme="danger"
282
+ theme="primary"
283
283
  variant="ghost"
284
- onClick={() => void handleToggleDelete()}
284
+ onClick={() => formRef?.requestSubmit()}
285
285
  >
286
- <Icon icon={info()!.isDeleted ? IconTrashOff : IconTrash} class="mr-1" />
287
- {info()!.isDeleted ? "복구" : "삭제"}
286
+ <Icon icon={IconDeviceFloppy} class="mr-1" />
287
+ 저장
288
288
  </Button>
289
- )}
289
+ </Show>
290
+ <Show
291
+ when={
292
+ canEdit() &&
293
+ local.toggleDelete &&
294
+ info() &&
295
+ !info()!.isNew &&
296
+ (local.deletable ?? true)
297
+ }
298
+ >
299
+ {(_) => (
300
+ <Button
301
+ size="sm"
302
+ theme="danger"
303
+ variant="ghost"
304
+ onClick={() => void handleToggleDelete()}
305
+ >
306
+ <Icon icon={info()!.isDeleted ? IconTrashOff : IconTrash} class="mr-1" />
307
+ {info()!.isDeleted ? "복구" : "삭제"}
308
+ </Button>
309
+ )}
310
+ </Show>
311
+ <Button size="sm" theme="info" variant="ghost" onClick={() => void handleRefresh()}>
312
+ <Icon icon={IconRefresh} class="mr-1" />
313
+ 새로고침
314
+ </Button>
290
315
  </Show>
291
316
  <Show when={defs().tools}>{(toolsDef) => toolsDef().children}</Show>
292
317
  </div>