@h-ai/ui 0.1.0-alpha.23 → 0.1.0-alpha.25

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.
@@ -0,0 +1,196 @@
1
+ <!--
2
+ @component CrudDetailDrawer
3
+ CRUD 详情组件(只读展示),支持抽屉(drawer)与弹窗(modal)两种展示形式
4
+
5
+ 使用 Svelte 5 Runes ($props, $derived)
6
+ 使用 compounds 组件:Drawer, Modal, Badge
7
+ -->
8
+ <script lang='ts'>
9
+ import type { Snippet } from 'svelte'
10
+ import type { Size } from '../../../types.js'
11
+ import type { CrudFormVariant } from './crud-types.js'
12
+ import { uiM } from '../../../messages.js'
13
+ import Drawer from '../../compounds/Drawer.svelte'
14
+ import Modal from '../../compounds/Modal.svelte'
15
+ import Badge from '../../primitives/Badge.svelte'
16
+ import Button from '../../primitives/Button.svelte'
17
+
18
+ type FieldDef = {
19
+ id: string
20
+ label: string | (() => string)
21
+ type: string
22
+ inDetail?: boolean
23
+ options?: Array<{ label: string, value: string | number | boolean }> | (() => Array<{ label: string, value: string | number | boolean }>)
24
+ render?: (value: unknown, item: Record<string, unknown>) => string
25
+ order?: number
26
+ }
27
+
28
+ let {
29
+ open = $bindable(false),
30
+ item = null,
31
+ fields = [],
32
+ title = '',
33
+ variant = 'drawer' as CrudFormVariant,
34
+ size = '2xl' as Size,
35
+ drawerWidth,
36
+ modalSize = '2xl' as Size | 'full',
37
+ modalWidth,
38
+ modalHeight,
39
+ canEdit = false,
40
+ onedit,
41
+ onclose,
42
+ detailExtra,
43
+ }: {
44
+ open?: boolean
45
+ item?: Record<string, unknown> | null
46
+ fields?: FieldDef[]
47
+ title?: string
48
+ variant?: CrudFormVariant
49
+ size?: Size
50
+ drawerWidth?: string
51
+ modalSize?: Size | 'full'
52
+ modalWidth?: string
53
+ modalHeight?: string
54
+ canEdit?: boolean
55
+ onedit?: () => void
56
+ onclose?: () => void
57
+ detailExtra?: Snippet<[Record<string, unknown>]>
58
+ } = $props()
59
+
60
+ function resolveText(text: string | (() => string)): string {
61
+ return typeof text === 'function' ? text() : text
62
+ }
63
+
64
+ function resolveOptions(options?: FieldDef['options']): Array<{ label: string, value: string | number | boolean }> {
65
+ if (!options)
66
+ return []
67
+ return typeof options === 'function' ? options() : options
68
+ }
69
+
70
+ function formatValue(field: FieldDef, value: unknown): string {
71
+ if (value === null || value === undefined)
72
+ return uiM('crud_detail_empty')
73
+
74
+ if (field.render && item) {
75
+ return field.render(value, item)
76
+ }
77
+
78
+ // 选项类型:将 value 映射为 label
79
+ if (field.type === 'select' || field.type === 'radio') {
80
+ const opts = resolveOptions(field.options)
81
+ const found = opts.find(o => String(o.value) === String(value))
82
+ return found?.label ?? String(value)
83
+ }
84
+
85
+ // 多选类型
86
+ if (field.type === 'multi-select' && Array.isArray(value)) {
87
+ const opts = resolveOptions(field.options)
88
+ return value.map((v) => {
89
+ const found = opts.find(o => String(o.value) === String(v))
90
+ return found?.label ?? String(v)
91
+ }).join(', ')
92
+ }
93
+
94
+ // 布尔
95
+ if (field.type === 'boolean' || field.type === 'checkbox') {
96
+ return value ? '✓' : '✗'
97
+ }
98
+
99
+ // 日期/时间
100
+ if (field.type === 'date' && (typeof value === 'string' || typeof value === 'number' || value instanceof Date)) {
101
+ return new Date(value as string | number).toLocaleDateString()
102
+ }
103
+ if (field.type === 'datetime' && (typeof value === 'string' || typeof value === 'number' || value instanceof Date)) {
104
+ return new Date(value as string | number).toLocaleString()
105
+ }
106
+
107
+ return String(value)
108
+ }
109
+
110
+ const detailFields = $derived(
111
+ [...fields]
112
+ .filter(f => f.inDetail !== false)
113
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
114
+ )
115
+
116
+ function handleClose() {
117
+ open = false
118
+ onclose?.()
119
+ }
120
+ </script>
121
+
122
+ {#snippet detailBody()}
123
+ {#if item}
124
+ <div class='space-y-4'>
125
+ {#each detailFields as field (field.id)}
126
+ {@const value = item[field.id]}
127
+ <div>
128
+ <div class='text-xs font-medium text-base-content/50 mb-1'>
129
+ {resolveText(field.label)}
130
+ </div>
131
+ <div class='text-sm text-base-content'>
132
+ {#if field.type === 'multi-select' && Array.isArray(value) && value.length > 0}
133
+ <div class='flex flex-wrap gap-1'>
134
+ {#each value as v, index (`${String(v)}:${index}`)}
135
+ {@const opts = resolveOptions(field.options)}
136
+ {@const found = opts.find(o => String(o.value) === String(v))}
137
+ <Badge variant='ghost' size='sm'>{found?.label ?? String(v)}</Badge>
138
+ {/each}
139
+ </div>
140
+ {:else}
141
+ {formatValue(field, value)}
142
+ {/if}
143
+ </div>
144
+ </div>
145
+ {/each}
146
+
147
+ {#if detailExtra && item}
148
+ <div class='border-t border-base-content/5 pt-4'>
149
+ {@render detailExtra(item)}
150
+ </div>
151
+ {/if}
152
+ </div>
153
+ {/if}
154
+ {/snippet}
155
+
156
+ {#snippet footerActions()}
157
+ {#if canEdit}
158
+ <Button variant='primary' onclick={onedit}>
159
+ <span class='icon-[tabler--edit] size-4 mr-1'></span>
160
+ {uiM('crud_edit')}
161
+ </Button>
162
+ {/if}
163
+ <Button variant='ghost' onclick={handleClose}>
164
+ {uiM('crud_close')}
165
+ </Button>
166
+ {/snippet}
167
+
168
+ {#if variant === 'modal'}
169
+ <Modal
170
+ bind:open
171
+ {title}
172
+ size={modalSize}
173
+ width={modalWidth}
174
+ height={modalHeight}
175
+ onclose={handleClose}
176
+ >
177
+ {@render detailBody()}
178
+
179
+ {#snippet footer()}
180
+ {@render footerActions()}
181
+ {/snippet}
182
+ </Modal>
183
+ {:else}
184
+ <Drawer bind:open {title} position='right' {size} width={drawerWidth} onclose={handleClose}>
185
+ {#if item}
186
+ <div class='pb-20'>
187
+ {@render detailBody()}
188
+ </div>
189
+
190
+ <!-- 底部操作栏 -->
191
+ <div class='absolute bottom-0 left-0 right-0 p-4 bg-base-200 border-t border-base-content/10 flex justify-end gap-2'>
192
+ {@render footerActions()}
193
+ </div>
194
+ {/if}
195
+ </Drawer>
196
+ {/if}
@@ -0,0 +1,44 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { Size } from '../../../types.js';
3
+ import type { CrudFormVariant } from './crud-types.js';
4
+ type FieldDef = {
5
+ id: string;
6
+ label: string | (() => string);
7
+ type: string;
8
+ inDetail?: boolean;
9
+ options?: Array<{
10
+ label: string;
11
+ value: string | number | boolean;
12
+ }> | (() => Array<{
13
+ label: string;
14
+ value: string | number | boolean;
15
+ }>);
16
+ render?: (value: unknown, item: Record<string, unknown>) => string;
17
+ order?: number;
18
+ };
19
+ type $$ComponentProps = {
20
+ open?: boolean;
21
+ item?: Record<string, unknown> | null;
22
+ fields?: FieldDef[];
23
+ title?: string;
24
+ variant?: CrudFormVariant;
25
+ size?: Size;
26
+ drawerWidth?: string;
27
+ modalSize?: Size | 'full';
28
+ modalWidth?: string;
29
+ modalHeight?: string;
30
+ canEdit?: boolean;
31
+ onedit?: () => void;
32
+ onclose?: () => void;
33
+ detailExtra?: Snippet<[Record<string, unknown>]>;
34
+ };
35
+ /**
36
+ * CrudDetailDrawer
37
+ * CRUD 详情组件(只读展示),支持抽屉(drawer)与弹窗(modal)两种展示形式
38
+ *
39
+ * 使用 Svelte 5 Runes ($props, $derived)
40
+ * 使用 compounds 组件:Drawer, Modal, Badge
41
+ */
42
+ declare const CrudDetailDrawer: import("svelte").Component<$$ComponentProps, {}, "open">;
43
+ type CrudDetailDrawer = ReturnType<typeof CrudDetailDrawer>;
44
+ export default CrudDetailDrawer;
@@ -8,7 +8,7 @@
8
8
  <script lang='ts'>
9
9
  import type { Snippet } from 'svelte'
10
10
  import type { Size } from '../../../types.js'
11
- import type { CrudFormVariant } from './crud-types.js'
11
+ import type { CrudDensity, CrudFormVariant } from './crud-types.js'
12
12
  import { uiM } from '../../../messages.js'
13
13
  import Drawer from '../../compounds/Drawer.svelte'
14
14
  import Modal from '../../compounds/Modal.svelte'
@@ -29,6 +29,7 @@
29
29
  open = $bindable(false),
30
30
  item = null,
31
31
  fields = [],
32
+ density = 'normal' as CrudDensity,
32
33
  title = '',
33
34
  variant = 'drawer' as CrudFormVariant,
34
35
  size = '2xl' as Size,
@@ -44,6 +45,7 @@
44
45
  open?: boolean
45
46
  item?: Record<string, unknown> | null
46
47
  fields?: FieldDef[]
48
+ density?: CrudDensity
47
49
  title?: string
48
50
  variant?: CrudFormVariant
49
51
  size?: Size
@@ -109,6 +111,16 @@
109
111
  .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
110
112
  )
111
113
 
114
+ const isCompact = $derived(density === 'compact')
115
+ const contentClass = $derived(isCompact ? 'space-y-3' : 'space-y-4')
116
+ const labelClass = $derived(isCompact ? 'text-[11px] font-medium text-base-content/50 mb-1' : 'text-xs font-medium text-base-content/50 mb-1')
117
+ const valueClass = $derived(isCompact ? 'text-[13px] text-base-content' : 'text-sm text-base-content')
118
+ const badgeSize = $derived(isCompact ? 'xs' : 'sm')
119
+ const detailExtraClass = $derived(isCompact ? 'border-t border-base-content/5 pt-3' : 'border-t border-base-content/5 pt-4')
120
+ const iconClass = $derived(isCompact ? 'size-3.5 mr-1' : 'size-4 mr-1')
121
+ const drawerBodyClass = $derived(isCompact ? 'pb-16' : 'pb-20')
122
+ const drawerFooterClass = $derived(isCompact ? 'absolute bottom-0 left-0 right-0 p-3 bg-base-200 border-t border-base-content/10 flex justify-end gap-2' : 'absolute bottom-0 left-0 right-0 p-4 bg-base-200 border-t border-base-content/10 flex justify-end gap-2')
123
+
112
124
  function handleClose() {
113
125
  open = false
114
126
  onclose?.()
@@ -117,20 +129,20 @@
117
129
 
118
130
  {#snippet detailBody()}
119
131
  {#if item}
120
- <div class='space-y-4'>
132
+ <div class={contentClass}>
121
133
  {#each detailFields as field (field.id)}
122
134
  {@const value = item[field.id]}
123
135
  <div>
124
- <div class='text-xs font-medium text-base-content/50 mb-1'>
136
+ <div class={labelClass}>
125
137
  {resolveText(field.label)}
126
138
  </div>
127
- <div class='text-sm text-base-content'>
139
+ <div class={valueClass}>
128
140
  {#if field.type === 'multi-select' && Array.isArray(value) && value.length > 0}
129
141
  <div class='flex flex-wrap gap-1'>
130
142
  {#each value as v, index (`${String(v)}:${index}`)}
131
143
  {@const opts = resolveOptions(field.options)}
132
144
  {@const found = opts.find(o => String(o.value) === String(v))}
133
- <Badge variant='ghost' size='sm'>{found?.label ?? String(v)}</Badge>
145
+ <Badge variant='ghost' size={badgeSize}>{found?.label ?? String(v)}</Badge>
134
146
  {/each}
135
147
  </div>
136
148
  {:else}
@@ -141,7 +153,7 @@
141
153
  {/each}
142
154
 
143
155
  {#if detailExtra && item}
144
- <div class='border-t border-base-content/5 pt-4'>
156
+ <div class={detailExtraClass}>
145
157
  {@render detailExtra(item)}
146
158
  </div>
147
159
  {/if}
@@ -151,12 +163,12 @@
151
163
 
152
164
  {#snippet footerActions()}
153
165
  {#if canEdit}
154
- <Button variant='primary' onclick={onedit}>
155
- <span class='icon-[tabler--edit] size-4 mr-1'></span>
166
+ <Button variant='primary' size='sm' onclick={onedit}>
167
+ <span class='icon-[tabler--edit] {iconClass}'></span>
156
168
  {uiM('crud_edit')}
157
169
  </Button>
158
170
  {/if}
159
- <Button variant='ghost' onclick={handleClose}>
171
+ <Button variant='ghost' size='sm' onclick={handleClose}>
160
172
  {uiM('crud_close')}
161
173
  </Button>
162
174
  {/snippet}
@@ -179,12 +191,12 @@
179
191
  {:else}
180
192
  <Drawer bind:open {title} position='right' {size} width={drawerWidth} onclose={handleClose}>
181
193
  {#if item}
182
- <div class='pb-20'>
194
+ <div class={drawerBodyClass}>
183
195
  {@render detailBody()}
184
196
  </div>
185
197
 
186
198
  <!-- 底部操作栏 -->
187
- <div class='absolute bottom-0 left-0 right-0 p-4 bg-base-200 border-t border-base-content/10 flex justify-end gap-2'>
199
+ <div class={drawerFooterClass}>
188
200
  {@render footerActions()}
189
201
  </div>
190
202
  {/if}
@@ -1,6 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { Size } from '../../../types.js';
3
- import type { CrudFormVariant } from './crud-types.js';
3
+ import type { CrudDensity, CrudFormVariant } from './crud-types.js';
4
4
  type FieldDef = {
5
5
  id: string;
6
6
  label: string | (() => string);
@@ -20,6 +20,7 @@ type $$ComponentProps = {
20
20
  open?: boolean;
21
21
  item?: Record<string, unknown> | null;
22
22
  fields?: FieldDef[];
23
+ density?: CrudDensity;
23
24
  title?: string;
24
25
  variant?: CrudFormVariant;
25
26
  size?: Size;
@@ -8,7 +8,7 @@
8
8
  <script lang='ts'>
9
9
  import type { Snippet } from 'svelte'
10
10
  import type { Size } from '../../../types.js'
11
- import type { CrudFormVariant } from './crud-types.js'
11
+ import type { CrudDensity, CrudFormVariant } from './crud-types.js'
12
12
  import { uiM } from '../../../messages.js'
13
13
  import Drawer from '../../compounds/Drawer.svelte'
14
14
  import FormField from '../../compounds/FormField.svelte'
@@ -35,6 +35,7 @@
35
35
  mode = 'create' as 'create' | 'edit',
36
36
  fields = [],
37
37
  formData = $bindable<Record<string, unknown>>({}),
38
+ density = 'normal' as CrudDensity,
38
39
  title = '',
39
40
  variant = 'drawer' as CrudFormVariant,
40
41
  size = '2xl' as Size,
@@ -53,6 +54,7 @@
53
54
  mode?: 'create' | 'edit'
54
55
  fields?: FieldDef[]
55
56
  formData?: Record<string, unknown>
57
+ density?: CrudDensity
56
58
  title?: string
57
59
  variant?: CrudFormVariant
58
60
  size?: Size
@@ -82,6 +84,16 @@
82
84
  [...fields].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
83
85
  )
84
86
 
87
+ const isCompact = $derived(density === 'compact')
88
+ const controlSize = $derived(isCompact ? 'xs' : 'sm')
89
+ const formClass = $derived(isCompact ? 'space-y-3' : 'space-y-4')
90
+ const extraClass = $derived(isCompact ? 'border-t border-base-content/5 pt-3' : 'border-t border-base-content/5 pt-4')
91
+ const multiSelectWrapClass = $derived(isCompact ? 'flex flex-wrap gap-2 p-2.5 bg-base-200 rounded-lg max-h-48 overflow-y-auto' : 'flex flex-wrap gap-3 p-3 bg-base-200 rounded-lg max-h-48 overflow-y-auto')
92
+ const multiSelectLabelClass = $derived(isCompact ? 'inline-flex items-center gap-1.5 cursor-pointer' : 'inline-flex items-center gap-2 cursor-pointer')
93
+ const multiSelectTextClass = $derived(isCompact ? 'text-xs text-base-content' : 'text-sm text-base-content')
94
+ const drawerFormClass = $derived(isCompact ? 'space-y-3 pb-16' : 'space-y-4 pb-20')
95
+ const drawerFooterClass = $derived(isCompact ? 'absolute bottom-0 left-0 right-0 p-3 bg-base-200 border-t border-base-content/10 flex justify-end gap-2' : 'absolute bottom-0 left-0 right-0 p-4 bg-base-200 border-t border-base-content/10 flex justify-end gap-2')
96
+
85
97
  function handleClose() {
86
98
  open = false
87
99
  onclose?.()
@@ -139,6 +151,7 @@
139
151
  <FormField label={resolveText(field.label)} required={isRequired}>
140
152
  <Textarea
141
153
  id={field.id}
154
+ size={controlSize}
142
155
  value={String(fieldValue ?? '')}
143
156
  {placeholder}
144
157
  disabled={submitting || isReadonly}
@@ -152,6 +165,7 @@
152
165
  <FormField label={resolveText(field.label)} required={isRequired}>
153
166
  <Select
154
167
  id={field.id}
168
+ size={controlSize}
155
169
  value={String(fieldValue ?? '')}
156
170
  disabled={submitting || isReadonly}
157
171
  onchange={value => updateField(field.id, value)}
@@ -167,20 +181,20 @@
167
181
  {@const opts = resolveOptions(field.options)}
168
182
  {@const selectedValues = ((fieldValue ?? []) as Array<string | number | boolean>).map(String)}
169
183
  <FormField label={resolveText(field.label)} required={isRequired}>
170
- <div class='flex flex-wrap gap-3 p-3 bg-base-200 rounded-lg max-h-48 overflow-y-auto'>
184
+ <div class={multiSelectWrapClass}>
171
185
  {#each opts as opt (String(opt.value))}
172
- <label class='inline-flex items-center gap-2 cursor-pointer'>
186
+ <label class={multiSelectLabelClass}>
173
187
  <Checkbox
174
- size='sm'
188
+ size={controlSize}
175
189
  checked={selectedValues.includes(String(opt.value))}
176
190
  onchange={() => toggleMultiSelect(field.id, opt.value)}
177
191
  disabled={submitting || isReadonly}
178
192
  />
179
- <span class='text-sm text-base-content'>{opt.label}</span>
193
+ <span class={multiSelectTextClass}>{opt.label}</span>
180
194
  </label>
181
195
  {/each}
182
196
  {#if opts.length === 0}
183
- <span class='text-sm text-base-content/60'>{uiM('crud_no_data')}</span>
197
+ <span class={multiSelectTextClass}>{uiM('crud_no_data')}</span>
184
198
  {/if}
185
199
  </div>
186
200
  </FormField>
@@ -188,6 +202,7 @@
188
202
  {:else if field.type === 'boolean' || field.type === 'checkbox'}
189
203
  <FormField label={resolveText(field.label)}>
190
204
  <Checkbox
205
+ size={controlSize}
191
206
  checked={Boolean(fieldValue)}
192
207
  onchange={(checked: boolean) => updateField(field.id, checked)}
193
208
  disabled={submitting || isReadonly}
@@ -199,6 +214,7 @@
199
214
  <Input
200
215
  id={field.id}
201
216
  type={getInputType(field.type)}
217
+ size={controlSize}
202
218
  value={String(fieldValue ?? '')}
203
219
  {placeholder}
204
220
  disabled={submitting || isReadonly}
@@ -215,17 +231,17 @@
215
231
  {/each}
216
232
 
217
233
  {#if editFormExtra}
218
- <div class='border-t border-base-content/5 pt-4'>
234
+ <div class={extraClass}>
219
235
  {@render editFormExtra(editingItem, mode)}
220
236
  </div>
221
237
  {/if}
222
238
  {/snippet}
223
239
 
224
240
  {#snippet footerActions()}
225
- <Button variant='ghost' onclick={handleClose} disabled={submitting}>
241
+ <Button variant='ghost' size='sm' onclick={handleClose} disabled={submitting}>
226
242
  {uiM('crud_cancel')}
227
243
  </Button>
228
- <Button variant='primary' onclick={handleSubmit} disabled={submitting}>
244
+ <Button variant='primary' size='sm' onclick={handleSubmit} disabled={submitting}>
229
245
  {#if submitting}
230
246
  <span class='loading loading-spinner loading-xs mr-2'></span>
231
247
  {/if}
@@ -243,7 +259,7 @@
243
259
  closeOnBackdrop={false}
244
260
  onclose={handleClose}
245
261
  >
246
- <form onsubmit={handleSubmit} class='space-y-4'>
262
+ <form onsubmit={handleSubmit} class={formClass}>
247
263
  {@render formFields()}
248
264
  </form>
249
265
 
@@ -253,12 +269,12 @@
253
269
  </Modal>
254
270
  {:else}
255
271
  <Drawer bind:open {title} position='right' {size} width={drawerWidth} onclose={handleClose} closeOnBackdrop={false}>
256
- <form onsubmit={handleSubmit} class='space-y-4 pb-20'>
272
+ <form onsubmit={handleSubmit} class={drawerFormClass}>
257
273
  {@render formFields()}
258
274
  </form>
259
275
 
260
276
  <!-- 底部操作栏 -->
261
- <div class='absolute bottom-0 left-0 right-0 p-4 bg-base-200 border-t border-base-content/10 flex justify-end gap-2'>
277
+ <div class={drawerFooterClass}>
262
278
  {@render footerActions()}
263
279
  </div>
264
280
  </Drawer>
@@ -1,6 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { Size } from '../../../types.js';
3
- import type { CrudFormVariant } from './crud-types.js';
3
+ import type { CrudDensity, CrudFormVariant } from './crud-types.js';
4
4
  type FieldDef = {
5
5
  id: string;
6
6
  label: string | (() => string);
@@ -28,6 +28,7 @@ type $$ComponentProps = {
28
28
  mode?: 'create' | 'edit';
29
29
  fields?: FieldDef[];
30
30
  formData?: Record<string, unknown>;
31
+ density?: CrudDensity;
31
32
  title?: string;
32
33
  variant?: CrudFormVariant;
33
34
  size?: Size;
@@ -57,15 +57,16 @@
57
57
  }
58
58
  </script>
59
59
 
60
- <div class='p-4'>
61
- <div class='flex flex-col sm:flex-row gap-3'>
60
+ <div class='grid gap-3 md:grid-cols-[minmax(0,1fr)_auto] md:items-center'>
61
+ <div class='flex min-w-0 flex-wrap items-center gap-3 [&_.fieldset]:m-0 [&_.fieldset]:min-w-0'>
62
62
  {#if searchable}
63
- <div class='flex-1 relative'>
64
- <span class='icon-[tabler--search] size-4.5 absolute left-3 top-1/2 -translate-y-1/2 text-base-content/35 z-10'></span>
63
+ <div class='relative min-w-[15rem] flex-[1_1_20rem]'>
64
+ <span class='icon-[tabler--search] pointer-events-none absolute left-3.5 top-1/2 z-10 size-4 -translate-y-1/2 text-base-content/35'></span>
65
65
  <Input
66
66
  type='text'
67
+ size='sm'
67
68
  placeholder={searchPlaceholder || uiM('crud_search_placeholder')}
68
- class='pl-10'
69
+ class='pl-10 shadow-none'
69
70
  bind:value={searchValue}
70
71
  oninput={handleSearchInput}
71
72
  autocomplete='off'
@@ -76,21 +77,24 @@
76
77
  {#each filterFields as field (field.id)}
77
78
  {@const opts = resolveOptions(field.options)}
78
79
  {#if opts.length > 0}
79
- <Select
80
- class='min-w-32'
81
- value={String(filterValues[field.id] ?? '')}
82
- onchange={value => handleFilterChange(field.id, value)}
83
- >
84
- <option value="">{uiM('crud_filter_all')}</option>
85
- {#each opts as opt (String(opt.value))}
86
- <option value={String(opt.value)}>{opt.label}</option>
87
- {/each}
88
- </Select>
80
+ <div class='w-full sm:w-[11rem] sm:flex-none'>
81
+ <Select
82
+ size='sm'
83
+ class='border-base-content/10 bg-base-100 shadow-none'
84
+ value={String(filterValues[field.id] ?? '')}
85
+ onchange={value => handleFilterChange(field.id, value)}
86
+ >
87
+ <option value="">{uiM('crud_filter_all')}</option>
88
+ {#each opts as opt (String(opt.value))}
89
+ <option value={String(opt.value)}>{opt.label}</option>
90
+ {/each}
91
+ </Select>
92
+ </div>
89
93
  {/if}
90
94
  {/each}
95
+ </div>
91
96
 
92
- <div class='text-sm text-base-content/50 self-center whitespace-nowrap'>
93
- {uiM('crud_total', { count: total })}
94
- </div>
97
+ <div class='inline-flex items-center justify-self-start rounded-full border border-base-content/8 bg-base-200/60 px-3 py-1 text-xs font-medium text-base-content/60 md:justify-self-end'>
98
+ {uiM('crud_total', { count: total })}
95
99
  </div>
96
100
  </div>