@stonecrop/atable 0.8.1 → 0.8.3

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,376 @@
1
+ <template>
2
+ <td
3
+ ref="actionsCell"
4
+ class="atable-row-actions"
5
+ :class="{ 'sticky-column': position === 'before-index', 'dropdown-active': dropdownOpen }">
6
+ <!-- Dropdown mode -->
7
+ <div v-if="showDropdown" class="row-actions-dropdown">
8
+ <button
9
+ ref="toggleButton"
10
+ type="button"
11
+ class="row-actions-toggle"
12
+ :aria-expanded="dropdownOpen"
13
+ aria-haspopup="true"
14
+ @click.stop="toggleDropdown">
15
+ <span class="dropdown-icon">⋮</span>
16
+ </button>
17
+ <div
18
+ v-show="dropdownOpen"
19
+ class="row-actions-menu"
20
+ :class="{ 'menu-flipped': dropdownFlipped }"
21
+ :style="menuStyle"
22
+ role="menu">
23
+ <button
24
+ v-for="action in enabledActions"
25
+ :key="action.type"
26
+ type="button"
27
+ class="row-action-menu-item"
28
+ role="menuitem"
29
+ @click.stop="executeAction(action.type)">
30
+ <span class="action-icon" v-html="action.icon" />
31
+ <span class="action-label">{{ action.label }}</span>
32
+ </button>
33
+ </div>
34
+ </div>
35
+
36
+ <!-- Icon mode -->
37
+ <div v-else class="row-actions-icons">
38
+ <button
39
+ v-for="action in enabledActions"
40
+ :key="action.type"
41
+ type="button"
42
+ class="row-action-btn"
43
+ :title="action.label"
44
+ :aria-label="action.label"
45
+ @click.stop="executeAction(action.type)">
46
+ <span class="action-icon" v-html="action.icon" />
47
+ </button>
48
+ </div>
49
+ </td>
50
+ </template>
51
+
52
+ <script setup lang="ts">
53
+ import { useResizeObserver, onClickOutside } from '@vueuse/core'
54
+ import { computed, ref, useTemplateRef } from 'vue'
55
+
56
+ import { actionIcons } from '../icons'
57
+ import { createTableStore } from '../stores/table'
58
+ import type { RowActionsConfig, RowActionType } from '../types'
59
+
60
+ const props = defineProps<{
61
+ rowIndex: number
62
+ store: ReturnType<typeof createTableStore>
63
+ config: RowActionsConfig
64
+ position?: 'before-index' | 'after-index' | 'end'
65
+ }>()
66
+
67
+ const emit = defineEmits<{
68
+ action: [type: RowActionType, rowIndex: number]
69
+ }>()
70
+
71
+ const actionsCellRef = useTemplateRef<HTMLTableCellElement>('actionsCell')
72
+ const toggleButtonRef = useTemplateRef<HTMLButtonElement>('toggleButton')
73
+ const cellWidth = ref(0)
74
+ const dropdownOpen = ref(false)
75
+ const dropdownFlipped = ref(false)
76
+ const menuPosition = ref({ top: 0, left: 0 })
77
+
78
+ // Default labels for actions
79
+ const defaultLabels: Record<RowActionType, string> = {
80
+ add: 'Add Row',
81
+ delete: 'Delete Row',
82
+ duplicate: 'Duplicate Row',
83
+ insertAbove: 'Insert Above',
84
+ insertBelow: 'Insert Below',
85
+ move: 'Move Row',
86
+ }
87
+
88
+ // Determine which actions are enabled
89
+ const enabledActions = computed(() => {
90
+ const actions: Array<{ type: RowActionType; label: string; icon: string }> = []
91
+ const configActions = props.config.actions || {}
92
+
93
+ const actionTypes: RowActionType[] = ['add', 'delete', 'duplicate', 'insertAbove', 'insertBelow', 'move']
94
+
95
+ for (const type of actionTypes) {
96
+ const actionConfig = configActions[type]
97
+
98
+ // Skip if explicitly disabled
99
+ if (actionConfig === false) continue
100
+
101
+ // Skip if not configured and we're being explicit
102
+ if (actionConfig === undefined) continue
103
+
104
+ let enabled = true
105
+ let label = defaultLabels[type]
106
+ let icon = actionIcons[type]
107
+
108
+ if (typeof actionConfig === 'object') {
109
+ enabled = actionConfig.enabled !== false
110
+ label = actionConfig.label || label
111
+ icon = actionConfig.icon || icon
112
+ }
113
+
114
+ if (enabled) {
115
+ actions.push({ type, label, icon })
116
+ }
117
+ }
118
+
119
+ return actions
120
+ })
121
+
122
+ // Determine if we should show dropdown mode
123
+ const showDropdown = computed(() => {
124
+ if (props.config.forceDropdown) return true
125
+
126
+ const threshold = props.config.dropdownThreshold ?? 150
127
+ if (threshold === 0) return false
128
+
129
+ return cellWidth.value > 0 && cellWidth.value < threshold
130
+ })
131
+
132
+ // Compute menu style for fixed positioning
133
+ const menuStyle = computed(() => {
134
+ if (!dropdownOpen.value) return {}
135
+
136
+ if (dropdownFlipped.value) {
137
+ return {
138
+ position: 'fixed' as const,
139
+ bottom: `${window.innerHeight - menuPosition.value.top}px`,
140
+ left: `${menuPosition.value.left}px`,
141
+ top: 'auto',
142
+ }
143
+ }
144
+ return {
145
+ position: 'fixed' as const,
146
+ top: `${menuPosition.value.top}px`,
147
+ left: `${menuPosition.value.left}px`,
148
+ }
149
+ })
150
+
151
+ // Track cell width for responsive behavior
152
+ useResizeObserver(actionsCellRef, entries => {
153
+ const entry = entries[0]
154
+ if (entry) {
155
+ cellWidth.value = entry.contentRect.width
156
+ }
157
+ })
158
+
159
+ // Toggle dropdown menu
160
+ const toggleDropdown = () => {
161
+ if (!dropdownOpen.value) {
162
+ // Opening - check if we need to flip
163
+ checkDropdownPosition()
164
+ }
165
+ dropdownOpen.value = !dropdownOpen.value
166
+ }
167
+
168
+ // Check if dropdown should flip upward and calculate position
169
+ const checkDropdownPosition = () => {
170
+ if (!toggleButtonRef.value) return
171
+
172
+ const buttonRect = toggleButtonRef.value.getBoundingClientRect()
173
+ const viewportHeight = window.innerHeight
174
+ const estimatedMenuHeight = enabledActions.value.length * 40 + 16 // ~40px per item + padding
175
+
176
+ // Check if menu would extend beyond viewport bottom
177
+ const spaceBelow = viewportHeight - buttonRect.bottom
178
+ const spaceAbove = buttonRect.top
179
+
180
+ // Flip if not enough space below but enough space above
181
+ dropdownFlipped.value = spaceBelow < estimatedMenuHeight && spaceAbove > estimatedMenuHeight
182
+
183
+ // Calculate fixed position
184
+ if (dropdownFlipped.value) {
185
+ menuPosition.value = {
186
+ top: buttonRect.top,
187
+ left: buttonRect.left,
188
+ }
189
+ } else {
190
+ menuPosition.value = {
191
+ top: buttonRect.bottom,
192
+ left: buttonRect.left,
193
+ }
194
+ }
195
+ }
196
+
197
+ onClickOutside(actionsCellRef, () => {
198
+ dropdownOpen.value = false
199
+ })
200
+
201
+ // Execute an action
202
+ const executeAction = (actionType: RowActionType) => {
203
+ dropdownOpen.value = false
204
+
205
+ // Check for custom handler
206
+ const actionConfig = props.config.actions?.[actionType]
207
+ if (typeof actionConfig === 'object' && actionConfig.handler) {
208
+ const result = actionConfig.handler(props.rowIndex, props.store)
209
+ if (result === false) {
210
+ // Handler returned false, don't proceed with default behavior
211
+ return
212
+ }
213
+ }
214
+
215
+ // Emit the action event for parent to handle
216
+ emit('action', actionType, props.rowIndex)
217
+ }
218
+ </script>
219
+
220
+ <style>
221
+ @import url('@stonecrop/themes/default.css');
222
+
223
+ .atable-row-actions {
224
+ width: 2rem;
225
+ min-width: 2rem;
226
+ padding: 0 0.25rem;
227
+ vertical-align: middle;
228
+ white-space: nowrap;
229
+ border-top: 1px solid var(--sc-row-border-color);
230
+ background: white;
231
+ user-select: none;
232
+ position: relative;
233
+ }
234
+
235
+ .atable-row-actions.dropdown-active {
236
+ z-index: 500;
237
+ }
238
+
239
+ .row-actions-icons {
240
+ display: flex;
241
+ gap: 0.25rem;
242
+ align-items: center;
243
+ justify-content: center;
244
+ }
245
+
246
+ .row-action-btn {
247
+ display: inline-flex;
248
+ align-items: center;
249
+ justify-content: center;
250
+ width: 1.5rem;
251
+ height: 1.5rem;
252
+ padding: 0.125rem;
253
+ border: none;
254
+ background: transparent;
255
+ cursor: pointer;
256
+ border-radius: 0.25rem;
257
+ transition: background-color 0.15s ease;
258
+ }
259
+
260
+ .row-action-btn:hover {
261
+ background-color: var(--sc-gray-10, #e5e5e5);
262
+ }
263
+
264
+ .row-action-btn:focus {
265
+ outline: 2px solid var(--sc-focus-cell-outline, #3b82f6);
266
+ outline-offset: 1px;
267
+ }
268
+
269
+ .row-action-btn .action-icon {
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ width: 1rem;
274
+ height: 1rem;
275
+ }
276
+
277
+ .row-action-btn .action-icon :deep(svg) {
278
+ width: 100%;
279
+ height: 100%;
280
+ }
281
+
282
+ /* Dropdown mode styles */
283
+ .row-actions-dropdown {
284
+ position: relative;
285
+ display: inline-block;
286
+ }
287
+ .row-actions-dropdown:has(button:focus) {
288
+ outline: 2px solid var(--sc-focus-cell-outline);
289
+ outline-offset: -2px;
290
+ }
291
+
292
+ .row-actions-toggle {
293
+ display: inline-flex;
294
+ align-items: center;
295
+ justify-content: center;
296
+ width: 1.5rem;
297
+ height: 1.5rem;
298
+ padding: 0;
299
+ border: none;
300
+ background: transparent;
301
+ cursor: pointer;
302
+ border-radius: 0.25rem;
303
+ font-size: 1rem;
304
+ font-weight: bold;
305
+ transition: background-color 0.15s ease;
306
+ }
307
+
308
+ .row-actions-toggle:hover {
309
+ background-color: var(--sc-gray-10, #e5e5e5);
310
+ }
311
+
312
+ .row-actions-toggle:focus {
313
+ /* outline: 2px solid var(--sc-focus-cell-outline, #3b82f6);
314
+ outline-offset: 1px; */
315
+ }
316
+
317
+ .dropdown-icon {
318
+ line-height: 1;
319
+ }
320
+
321
+ .row-actions-menu {
322
+ position: fixed;
323
+ z-index: 9999;
324
+ min-width: 10rem;
325
+ padding: 0.25rem 0;
326
+ background: white;
327
+ border: 1px solid var(--sc-row-border-color);
328
+ border-left: 4px solid var(--sc-row-border-color);
329
+ border-radius: 0;
330
+ }
331
+
332
+ .row-actions-menu.menu-flipped {
333
+ box-shadow: 0 -4px 6px -1px rgb(0 0 0 / 0.1), 0 -2px 4px -2px rgb(0 0 0 / 0.1);
334
+ }
335
+
336
+ .row-action-menu-item {
337
+ display: flex;
338
+ align-items: center;
339
+ gap: 0.5rem;
340
+ width: 100%;
341
+ padding: 0.5rem 0.75rem;
342
+ border: none;
343
+ background: transparent;
344
+ cursor: pointer;
345
+ text-align: left;
346
+ font-size: 0.875rem;
347
+ transition: background-color 0.15s ease;
348
+ }
349
+
350
+ .row-action-menu-item:hover {
351
+ background-color: var(--sc-gray-10, #f5f5f5);
352
+ }
353
+
354
+ .row-action-menu-item:focus {
355
+ outline: none;
356
+ background-color: var(--sc-gray-10, #f5f5f5);
357
+ }
358
+
359
+ .row-action-menu-item .action-icon {
360
+ display: flex;
361
+ align-items: center;
362
+ justify-content: center;
363
+ width: 1rem;
364
+ height: 1rem;
365
+ flex-shrink: 0;
366
+ }
367
+
368
+ .row-action-menu-item .action-icon :deep(svg) {
369
+ width: 100%;
370
+ height: 100%;
371
+ }
372
+
373
+ .row-action-menu-item .action-label {
374
+ flex: 1;
375
+ }
376
+ </style>
@@ -18,7 +18,8 @@
18
18
  :key="`${row.originalIndex}-${filteredIndex}`"
19
19
  :row="row"
20
20
  :rowIndex="row.originalIndex"
21
- :store="store">
21
+ :store="store"
22
+ @row:action="handleRowAction">
22
23
  <template v-for="(column, colIndex) in getProcessedColumnsForRow(row)" :key="column.name">
23
24
  <component
24
25
  :is="column.ganttComponent || 'AGanttCell'"
@@ -91,7 +92,20 @@ import ARow from './ARow.vue'
91
92
  import ATableHeader from './ATableHeader.vue'
92
93
  import ATableModal from './ATableModal.vue'
93
94
  import { createTableStore } from '../stores/table'
94
- import type { ConnectionEvent, ConnectionPath, GanttDragEvent, TableColumn, TableConfig, TableRow } from '../types'
95
+ import type {
96
+ ConnectionEvent,
97
+ ConnectionPath,
98
+ GanttDragEvent,
99
+ RowActionType,
100
+ RowAddEvent,
101
+ RowDeleteEvent,
102
+ RowDuplicateEvent,
103
+ RowInsertEvent,
104
+ RowMoveEvent,
105
+ TableColumn,
106
+ TableConfig,
107
+ TableRow,
108
+ } from '../types'
95
109
 
96
110
  const rows = defineModel<TableRow[]>('rows', { required: true })
97
111
  const columns = defineModel<TableColumn[]>('columns', { required: true })
@@ -106,6 +120,12 @@ const emit = defineEmits<{
106
120
  'gantt:drag': [event: GanttDragEvent]
107
121
  'connection:event': [event: ConnectionEvent]
108
122
  'columns:update': [columns: TableColumn[]]
123
+ 'row:add': [event: RowAddEvent]
124
+ 'row:delete': [event: RowDeleteEvent]
125
+ 'row:duplicate': [event: RowDuplicateEvent]
126
+ 'row:insert-above': [event: RowInsertEvent]
127
+ 'row:insert-below': [event: RowInsertEvent]
128
+ 'row:move': [event: RowMoveEvent]
109
129
  }>()
110
130
 
111
131
  const tableRef = useTemplateRef<HTMLTableElement>('table')
@@ -267,12 +287,73 @@ const handleConnectionDelete = (connection: ConnectionPath) => {
267
287
  emit('connection:event', { type: 'delete', connection })
268
288
  }
269
289
 
290
+ /**
291
+ * Handle row action events from ARow components.
292
+ * Performs the default action and emits the appropriate event.
293
+ */
294
+ const handleRowAction = (actionType: RowActionType, rowIndex: number) => {
295
+ switch (actionType) {
296
+ case 'add': {
297
+ // Add a new row after the current row
298
+ const newIndex = store.addRow({}, rowIndex + 1)
299
+ const newRow = store.rows[newIndex]
300
+ rows.value = [...store.rows]
301
+ emit('row:add', { rowIndex: newIndex, row: newRow })
302
+ break
303
+ }
304
+ case 'delete': {
305
+ const deletedRow = store.deleteRow(rowIndex)
306
+ if (deletedRow) {
307
+ rows.value = [...store.rows]
308
+ emit('row:delete', { rowIndex, row: deletedRow })
309
+ }
310
+ break
311
+ }
312
+ case 'duplicate': {
313
+ const newIndex = store.duplicateRow(rowIndex)
314
+ if (newIndex >= 0) {
315
+ const newRow = store.rows[newIndex]
316
+ rows.value = [...store.rows]
317
+ emit('row:duplicate', { sourceIndex: rowIndex, newIndex, row: newRow })
318
+ }
319
+ break
320
+ }
321
+ case 'insertAbove': {
322
+ const newIndex = store.insertRowAbove(rowIndex)
323
+ const newRow = store.rows[newIndex]
324
+ rows.value = [...store.rows]
325
+ emit('row:insert-above', { targetIndex: rowIndex, newIndex, row: newRow })
326
+ break
327
+ }
328
+ case 'insertBelow': {
329
+ const newIndex = store.insertRowBelow(rowIndex)
330
+ const newRow = store.rows[newIndex]
331
+ rows.value = [...store.rows]
332
+ emit('row:insert-below', { targetIndex: rowIndex, newIndex, row: newRow })
333
+ break
334
+ }
335
+ case 'move': {
336
+ // Move action requires a target index - for now, emit an event
337
+ // The consumer should handle showing a UI for selecting the target
338
+ emit('row:move', { fromIndex: rowIndex, toIndex: -1 })
339
+ break
340
+ }
341
+ }
342
+ }
343
+
270
344
  defineExpose({
271
345
  store,
272
346
  createConnection: store.createConnection,
273
347
  deleteConnection: store.deleteConnection,
274
348
  getConnectionsForBar: store.getConnectionsForBar,
275
349
  getHandlesForBar: store.getHandlesForBar,
350
+ // Row action methods
351
+ addRow: store.addRow,
352
+ deleteRow: store.deleteRow,
353
+ duplicateRow: store.duplicateRow,
354
+ insertRowAbove: store.insertRowAbove,
355
+ insertRowBelow: store.insertRowBelow,
356
+ moveRow: store.moveRow,
276
357
  })
277
358
  </script>
278
359
 
@@ -1,45 +1,63 @@
1
1
  <template>
2
- <thead v-if="columns.length">
2
+ <thead v-if="props.columns.length">
3
3
  <!-- Header row -->
4
4
  <tr class="atable-header-row" tabindex="-1">
5
+ <!-- Row actions header cell (before-index position) -->
5
6
  <th
6
- v-if="store.zeroColumn"
7
+ v-if="showRowActionsHeader && rowActionsPosition === 'before-index'"
8
+ class="row-actions-header"
9
+ :class="{ 'sticky-column': rowActionsPosition === 'before-index' }" />
10
+ <th
11
+ v-if="props.store.zeroColumn"
7
12
  id="header-index"
8
13
  :class="[
9
- store.hasPinnedColumns ? 'sticky-index' : '',
10
- store.isTreeView ? 'tree-index' : '',
11
- store.config.view === 'list-expansion' ? 'list-expansion-index' : '',
14
+ props.store.hasPinnedColumns ? 'sticky-index' : '',
15
+ props.store.isTreeView ? 'tree-index' : '',
16
+ props.store.config.view === 'list-expansion' ? 'list-expansion-index' : '',
12
17
  ]"
13
18
  class="list-index" />
19
+ <!-- Row actions header cell (after-index position) -->
20
+ <th v-if="showRowActionsHeader && rowActionsPosition === 'after-index'" class="row-actions-header" />
14
21
  <th
15
- v-for="(column, colKey) in columns"
22
+ v-for="(column, colKey) in props.columns"
16
23
  :key="column.name"
17
24
  v-resize-observer="onResize"
18
25
  :data-colindex="colKey"
19
26
  tabindex="-1"
20
- :style="store.getHeaderCellStyle(column)"
27
+ :style="props.store.getHeaderCellStyle(column)"
21
28
  :class="`${column.pinned ? 'sticky-column' : ''} ${column.sortable === false ? '' : 'cursor-pointer'}`"
22
29
  @click="column.sortable !== false ? handleSort(colKey) : undefined">
23
30
  <slot>{{ column.label || String.fromCharCode(colKey + 97).toUpperCase() }}</slot>
24
31
  </th>
32
+ <!-- Row actions header cell (end position) -->
33
+ <th v-if="showRowActionsHeader && rowActionsPosition === 'end'" class="row-actions-header" />
25
34
  </tr>
26
35
  <!-- Filters row -->
27
36
  <tr v-if="filterableColumns.length > 0" class="atable-filters-row">
37
+ <!-- Row actions filter cell (before-index position) -->
38
+ <th
39
+ v-if="showRowActionsHeader && rowActionsPosition === 'before-index'"
40
+ class="row-actions-header"
41
+ :class="{ 'sticky-column': rowActionsPosition === 'before-index' }" />
28
42
  <th
29
- v-if="store.zeroColumn"
43
+ v-if="props.store.zeroColumn"
30
44
  :class="[
31
- store.hasPinnedColumns ? 'sticky-index' : '',
32
- store.isTreeView ? 'tree-index' : '',
33
- store.config.view === 'list-expansion' ? 'list-expansion-index' : '',
45
+ props.store.hasPinnedColumns ? 'sticky-index' : '',
46
+ props.store.isTreeView ? 'tree-index' : '',
47
+ props.store.config.view === 'list-expansion' ? 'list-expansion-index' : '',
34
48
  ]"
35
49
  class="list-index" />
50
+ <!-- Row actions filter cell (after-index position) -->
51
+ <th v-if="showRowActionsHeader && rowActionsPosition === 'after-index'" class="row-actions-header" />
36
52
  <th
37
- v-for="(column, colKey) in columns"
53
+ v-for="(column, colKey) in props.columns"
38
54
  :key="`filter-${column.name}`"
39
55
  :class="`${column.pinned ? 'sticky-column' : ''}`"
40
- :style="store.getHeaderCellStyle(column)">
41
- <ATableColumnFilter v-if="column.filterable" :column="column" :col-index="colKey" :store="store" />
56
+ :style="props.store.getHeaderCellStyle(column)">
57
+ <ATableColumnFilter v-if="column.filterable" :column="column" :col-index="colKey" :store="props.store" />
42
58
  </th>
59
+ <!-- Row actions filter cell (end position) -->
60
+ <th v-if="showRowActionsHeader && rowActionsPosition === 'end'" class="row-actions-header" />
43
61
  </tr>
44
62
  </thead>
45
63
  </template>
@@ -51,14 +69,18 @@ import ATableColumnFilter from './ATableColumnFilter.vue'
51
69
  import { createTableStore } from '../stores/table'
52
70
  import type { TableColumn } from '../types'
53
71
 
54
- const { columns, store } = defineProps<{
72
+ const props = defineProps<{
55
73
  columns: TableColumn[]
56
74
  store: ReturnType<typeof createTableStore>
57
75
  }>()
58
76
 
59
- const filterableColumns = computed(() => columns.filter(column => column.filterable))
77
+ const filterableColumns = computed(() => props.columns.filter(column => column.filterable))
78
+
79
+ // Row actions header support
80
+ const showRowActionsHeader = computed(() => props.store.config.value?.rowActions?.enabled ?? false)
81
+ const rowActionsPosition = computed(() => props.store.config.value?.rowActions?.position ?? 'before-index')
60
82
 
61
- const handleSort = (colIndex: number) => store.sortByColumn(colIndex)
83
+ const handleSort = (colIndex: number) => props.store.sortByColumn(colIndex)
62
84
 
63
85
  const onResize = (entries: ReadonlyArray<ResizeObserverEntry>) => {
64
86
  for (const entry of entries) {
@@ -66,10 +88,10 @@ const onResize = (entries: ReadonlyArray<ResizeObserverEntry>) => {
66
88
  const observedCell = entry.borderBoxSize[0]
67
89
  const observedWidth = observedCell.inlineSize
68
90
  const colIndex = Number((entry.target as HTMLElement).dataset.colindex)
69
- const currentWidth = store.columns[colIndex]?.width
91
+ const currentWidth = props.store.columns[colIndex]?.width
70
92
 
71
93
  if (typeof currentWidth === 'number' && currentWidth !== observedWidth) {
72
- store.resizeColumn(colIndex, observedWidth)
94
+ props.store.resizeColumn(colIndex, observedWidth)
73
95
  }
74
96
  }
75
97
  }
@@ -81,6 +103,7 @@ const onResize = (entries: ReadonlyArray<ResizeObserverEntry>) => {
81
103
  .atable-header-row th {
82
104
  padding-left: 0.5ch !important;
83
105
  font-weight: 700;
106
+ min-width: 3ch;
84
107
  padding-top: var(--sc-atable-row-padding);
85
108
  padding-bottom: var(--sc-atable-row-padding);
86
109
  box-sizing: border-box;
@@ -90,6 +113,7 @@ const onResize = (entries: ReadonlyArray<ResizeObserverEntry>) => {
90
113
  #header-index {
91
114
  padding-left: var(--sc-atable-row-padding);
92
115
  box-sizing: border-box;
116
+ border-top: none;
93
117
  }
94
118
  .tree-index {
95
119
  padding-right: 0;
@@ -110,4 +134,10 @@ th {
110
134
  padding: 0.25rem 0.5ch;
111
135
  vertical-align: top;
112
136
  }
137
+
138
+ .row-actions-header {
139
+ width: 2rem;
140
+ min-width: 2rem;
141
+ padding: 0 0.25rem;
142
+ }
113
143
  </style>
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Icon exports for ATable row actions.
3
+ * Icons are exported as raw SVG strings for flexibility in rendering.
4
+ * @packageDocumentation
5
+ */
6
+
7
+ // @ts-expect-error Vite raw import
8
+ import AddIcon from './stonecrop-ui-icon-add.svg?raw'
9
+ // @ts-expect-error Vite raw import
10
+ import DeleteIcon from './stonecrop-ui-icon-delete.svg?raw'
11
+ // @ts-expect-error Vite raw import
12
+ import DuplicateIcon from './stonecrop-ui-icon-duplicate.svg?raw'
13
+ // @ts-expect-error Vite raw import
14
+ import InsertAboveIcon from './stonecrop-ui-icon-insert-above.svg?raw'
15
+ // @ts-expect-error Vite raw import
16
+ import InsertBelowIcon from './stonecrop-ui-icon-insert-below.svg?raw'
17
+ // @ts-expect-error Vite raw import
18
+ import MoveIcon from './stonecrop-ui-icon-move.svg?raw'
19
+
20
+ export { AddIcon, DeleteIcon, DuplicateIcon, InsertAboveIcon, InsertBelowIcon, MoveIcon }
21
+
22
+ /**
23
+ * Map of action types to their default icons.
24
+ *
25
+ * @public
26
+ */
27
+ export const actionIcons: Record<string, string> = {
28
+ add: AddIcon as string,
29
+ delete: DeleteIcon as string,
30
+ duplicate: DuplicateIcon as string,
31
+ insertAbove: InsertAboveIcon as string,
32
+ insertBelow: InsertBelowIcon as string,
33
+ move: MoveIcon as string,
34
+ }
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
3
+ <path d="M32,64C14.35,64,0,49.65,0,32S14.35,0,32,0s32,14.35,32,32-14.35,32-32,32ZM32,4c-15.44,0-28,12.56-28,28s12.56,28,28,28,28-12.56,28-28S47.44,4,32,4Z" style="fill: #000; stroke-width: 0px;"/>
4
+ <polygon points="34 18 30 18 30 30 18 30 18 34 30 34 30 46 34 46 34 34 46 34 46 30 34 30 34 18" style="fill: #000; stroke-width: 0px;"/>
5
+ </svg>