@makolabs/ripple 3.0.2 → 3.0.4

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 (37) hide show
  1. package/dist/ai/ai-types.d.ts +7 -0
  2. package/dist/charts/Chart.svelte +18 -37
  3. package/dist/charts/chart-types.d.ts +7 -0
  4. package/dist/drawer/Drawer.svelte +29 -36
  5. package/dist/drawer/drawer.js +2 -2
  6. package/dist/elements/combobox/ComboBox.svelte +6 -6
  7. package/dist/elements/dropdown/Select.svelte +6 -8
  8. package/dist/elements/popover/Popover.svelte +1 -1
  9. package/dist/elements/progress/Progress.svelte +18 -5
  10. package/dist/elements/progress/progress-types.d.ts +8 -0
  11. package/dist/file-browser/FileBrowser.svelte +6 -3
  12. package/dist/filters/CompactFilters.svelte +78 -68
  13. package/dist/filters/filter-types.d.ts +11 -0
  14. package/dist/forms/DateRange.svelte +31 -20
  15. package/dist/forms/NumberInput.svelte +6 -2
  16. package/dist/forms/Tags.svelte +9 -4
  17. package/dist/forms/Toggle.svelte +8 -6
  18. package/dist/forms/calendar/Calendar.svelte +8 -2
  19. package/dist/forms/calendar/calendar-types.d.ts +9 -0
  20. package/dist/forms/date-picker/DatePicker.svelte +8 -1
  21. package/dist/forms/form-types.d.ts +23 -0
  22. package/dist/forms/month-picker/MonthPicker.svelte +12 -3
  23. package/dist/forms/month-picker/month-picker-types.d.ts +7 -0
  24. package/dist/header/Breadcrumbs.svelte +19 -5
  25. package/dist/header/header-types.d.ts +7 -0
  26. package/dist/layout/activity-list/ActivityList.svelte +29 -7
  27. package/dist/layout/activity-list/activity-list-types.d.ts +8 -0
  28. package/dist/layout/card/MetricCard.svelte +34 -8
  29. package/dist/layout/card/RankedCard.svelte +34 -10
  30. package/dist/layout/card/card-types.d.ts +8 -0
  31. package/dist/layout/card/ranked-card.d.ts +10 -0
  32. package/dist/layout/table/Table.svelte +5 -0
  33. package/dist/layout/table/table-types.d.ts +8 -0
  34. package/dist/modal/modal.js +4 -4
  35. package/dist/pipeline/Pipeline.svelte +7 -3
  36. package/dist/pipeline/pipeline-types.d.ts +6 -0
  37. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../helper/cls.js';
3
+ import { buildTestId } from '../helper/testid.js';
3
4
  import { Size } from '../variants.js';
4
5
  import { formSizeTokens } from './form-size.js';
5
6
  import type { DateRangeProps } from '../index.js';
@@ -23,7 +24,8 @@
23
24
  size = Size.MD,
24
25
  id,
25
26
  name,
26
- onselect
27
+ onselect,
28
+ testId
27
29
  }: DateRangeProps = $props();
28
30
 
29
31
  const tokens = $derived(formSizeTokens[size]);
@@ -249,7 +251,11 @@
249
251
  <input type="hidden" name={`${name}[format]`} value={format} />
250
252
  <input type="hidden" name={`${name}`} value={getValue()} />
251
253
 
252
- <div class={cn('relative block w-full', className)} bind:this={datePickerRef}>
254
+ <div
255
+ class={cn('relative block w-full', className)}
256
+ bind:this={datePickerRef}
257
+ data-testid={buildTestId('date-range', undefined, testId)}
258
+ >
253
259
  <div class="relative">
254
260
  <button
255
261
  {id}
@@ -268,6 +274,7 @@
268
274
  : 'focus-visible:border-primary-500 focus-visible:ring-primary-500 hover:border-default-400 focus-visible:ring-2'
269
275
  )}
270
276
  onclick={toggleDatepicker}
277
+ data-testid={buildTestId('date-range', 'trigger', testId)}
271
278
  aria-haspopup="true"
272
279
  aria-expanded={isOpen}
273
280
  aria-describedby={errors?.length ? `${id}-errors` : undefined}
@@ -299,6 +306,7 @@
299
306
  'right-8'
300
307
  )}
301
308
  onclick={clearDates}
309
+ data-testid={buildTestId('date-range', 'clear', testId)}
302
310
  aria-label="Clear dates"
303
311
  >
304
312
  <svg class={cn(tokens.iconSize)} viewBox="0 0 20 20" fill="currentColor">
@@ -313,7 +321,7 @@
313
321
  </div>
314
322
 
315
323
  {#if errors?.length}
316
- <div id="{id}-errors">
324
+ <div id="{id}-errors" data-testid={buildTestId('date-range', 'errors', testId)}>
317
325
  {#each errors as error, i (i)}
318
326
  <p class="text-danger-600 mt-1 text-sm">{error}</p>
319
327
  {/each}
@@ -325,6 +333,7 @@
325
333
  <div
326
334
  bind:this={calendarRef}
327
335
  class="ring-opacity-5 ring-default-300 absolute z-10 mt-1 w-full origin-top-left rounded-md bg-white p-4 shadow-lg ring-1 focus:outline-none"
336
+ data-testid={buildTestId('date-range', 'panel', testId)}
328
337
  transition:fly={{ y: -8, duration: 300, easing: quintOut }}
329
338
  >
330
339
  {@render calendarContent()}
@@ -333,24 +342,26 @@
333
342
  {/if}
334
343
 
335
344
  {#if isOpen && isMobile}
336
- <button
337
- type="button"
338
- class="fixed inset-0 z-[9998] bg-black/40 backdrop-blur-sm"
339
- aria-label="Close"
340
- onclick={() => (isOpen = false)}
341
- ></button>
342
- <div
343
- class="fixed inset-x-0 bottom-0 z-[9999] flex max-h-[85vh] min-h-48 flex-col overflow-hidden rounded-t-2xl bg-white shadow-2xl"
344
- transition:fly={{ y: 300, duration: 200, easing: quintOut }}
345
- bind:this={calendarRef}
346
- >
347
- <div class="flex justify-center py-2">
348
- <div class="bg-default-300 h-1 w-8 rounded-full"></div>
349
- </div>
350
- <div class="flex-1 cursor-pointer overflow-y-auto p-4">
351
- {@render calendarContent()}
345
+ <Portal target={datePickerRef}>
346
+ <button
347
+ type="button"
348
+ class="fixed inset-0 z-[9998] bg-black/40 backdrop-blur-sm"
349
+ aria-label="Close"
350
+ onclick={() => (isOpen = false)}
351
+ ></button>
352
+ <div
353
+ class="fixed inset-x-0 bottom-0 z-[9999] flex max-h-[85vh] min-h-48 flex-col overflow-hidden rounded-t-2xl bg-white shadow-2xl"
354
+ transition:fly={{ y: 300, duration: 200, easing: quintOut }}
355
+ bind:this={calendarRef}
356
+ >
357
+ <div class="flex justify-center py-2">
358
+ <div class="bg-default-300 h-1 w-8 rounded-full"></div>
359
+ </div>
360
+ <div class="flex-1 cursor-pointer overflow-y-auto p-4">
361
+ {@render calendarContent()}
362
+ </div>
352
363
  </div>
353
- </div>
364
+ </Portal>
354
365
  {/if}
355
366
  </div>
356
367
 
@@ -154,6 +154,7 @@
154
154
  <button
155
155
  type="button"
156
156
  class="hover:bg-default-100 flex items-center gap-1 rounded px-1"
157
+ data-testid={buildTestId('numberinput', 'unit', testId)}
157
158
  onclick={handleUnitToggle}
158
159
  {disabled}
159
160
  >
@@ -173,7 +174,10 @@
173
174
  {:else if unit}
174
175
  <!-- Static unit label — no chevron, not clickable. Matches the
175
176
  field's text size so it sits inline. -->
176
- <span class={cn('text-default-500 flex items-center gap-1 pr-2', tokens.text)}>
177
+ <span
178
+ class={cn('text-default-500 flex items-center gap-1 pr-2', tokens.text)}
179
+ data-testid={buildTestId('numberinput', 'unit', testId)}
180
+ >
177
181
  {#if selectedOption?.icon}
178
182
  {@const Icon = selectedOption.icon}
179
183
  <Icon />
@@ -183,7 +187,7 @@
183
187
  {/if}
184
188
 
185
189
  {#if showUnitDropdown && hasMultipleUnits}
186
- <div class={dropdownClass}>
190
+ <div class={dropdownClass} data-testid={buildTestId('numberinput', 'unit-dropdown', testId)}>
187
191
  {#each units as unitOption (unitOption.value)}
188
192
  <button
189
193
  type="button"
@@ -2,6 +2,7 @@
2
2
  import Badge from '../elements/badge/Badge.svelte';
3
3
  import { Size } from '../variants.js';
4
4
  import { cn } from '../helper/cls.js';
5
+ import { buildTestId } from '../helper/testid.js';
5
6
  import { formSizeTokens } from './form-size.js';
6
7
  import { fade } from 'svelte/transition';
7
8
  import { flip } from 'svelte/animate';
@@ -18,7 +19,8 @@
18
19
  class: className = '',
19
20
  suggestions = [],
20
21
  onaddtag: onAddTag,
21
- onremovetag: onRemoveTag
22
+ onremovetag: onRemoveTag,
23
+ testId
22
24
  }: TagsProps = $props();
23
25
 
24
26
  let inputValue = $state('');
@@ -154,16 +156,17 @@
154
156
  );
155
157
  </script>
156
158
 
157
- <div class="space-y-1">
159
+ <div class="space-y-1" data-testid={buildTestId('tags', undefined, testId)}>
158
160
  {#if label}
159
161
  <label for={name} class="text-default-700 block text-sm font-medium">{label}</label>
160
162
  {/if}
161
163
  <div class={containerClass} onfocusout={handleFocusOut}>
162
- {#each value as tag (tag)}
164
+ {#each value as tag, index (tag)}
163
165
  <div
164
166
  class="inline-flex"
165
167
  transition:fade={{ duration: 250, easing: quintOut }}
166
168
  animate:flip={{ duration: 300, easing: quintOut }}
169
+ data-testid={buildTestId('tags', 'tag', testId, index)}
167
170
  >
168
171
  <Badge size={chipSize} color="info" onclose={() => handleTagRemoval(tag)} class="shadow-xs">
169
172
  {tag}
@@ -184,10 +187,11 @@
184
187
  autocomplete="off"
185
188
  onkeydown={handleKeydown}
186
189
  onfocus={handleFocusIn}
190
+ data-testid={buildTestId('tags', 'input', testId)}
187
191
  />
188
192
 
189
193
  {#if showSuggestions && filteredSuggestions.length > 0}
190
- <div class={suggestionClass}>
194
+ <div class={suggestionClass} data-testid={buildTestId('tags', 'suggestions', testId)}>
191
195
  {#each filteredSuggestions as suggestion, i (suggestion)}
192
196
  <button
193
197
  type="button"
@@ -196,6 +200,7 @@
196
200
  onmouseover={() => handleSuggestionHover(i)}
197
201
  onfocus={() => handleSuggestionHover(i)}
198
202
  tabindex="-1"
203
+ data-testid={buildTestId('tags', 'suggestion', testId, i)}
199
204
  >
200
205
  {suggestion}
201
206
  </button>
@@ -68,15 +68,17 @@
68
68
  )[size]
69
69
  );
70
70
 
71
+ // On-position = track_width − thumb_width − 2px margin.
72
+ // Off-position = 2px margin (translate-x-0.5).
71
73
  const thumbPosition = $derived(
72
74
  (
73
75
  {
74
- [Size.XS]: value ? 'translate-x-3.5' : 'translate-x-0.5',
75
- [Size.SM]: value ? 'translate-x-4' : 'translate-x-0.5',
76
- [Size.MD]: value ? 'translate-x-5' : 'translate-x-0.5',
77
- [Size.LG]: value ? 'translate-x-6' : 'translate-x-0.5',
78
- [Size.XL]: value ? 'translate-x-7' : 'translate-x-0.5',
79
- [Size.XXL]: value ? 'translate-x-7' : 'translate-x-0.5'
76
+ [Size.XS]: value ? 'translate-x-4' : 'translate-x-0.5',
77
+ [Size.SM]: value ? 'translate-x-[18px]' : 'translate-x-0.5',
78
+ [Size.MD]: value ? 'translate-x-[22px]' : 'translate-x-0.5',
79
+ [Size.LG]: value ? 'translate-x-[26px]' : 'translate-x-0.5',
80
+ [Size.XL]: value ? 'translate-x-[30px]' : 'translate-x-0.5',
81
+ [Size.XXL]: value ? 'translate-x-[30px]' : 'translate-x-0.5'
80
82
  } satisfies Record<VariantSizes, string>
81
83
  )[size]
82
84
  );
@@ -230,7 +230,6 @@
230
230
  class={cn(
231
231
  'inline-block bg-white select-none',
232
232
  'border-default-200 rounded-lg border shadow-xs',
233
- 'max-sm:w-full max-sm:rounded-none max-sm:border-0 max-sm:shadow-none',
234
233
  density.panel,
235
234
  density.padding,
236
235
  className
@@ -247,6 +246,7 @@
247
246
  density.navBtn
248
247
  )}
249
248
  aria-label="Previous month"
249
+ data-testid={buildTestId('calendar', 'prev-month', testId)}
250
250
  {disabled}
251
251
  >
252
252
  <svg class={density.navIcon} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
@@ -257,7 +257,10 @@
257
257
  />
258
258
  </svg>
259
259
  </button>
260
- <span class={cn('text-default-800 font-semibold', density.monthText)}>{monthLabel}</span>
260
+ <span
261
+ class={cn('text-default-800 font-semibold', density.monthText)}
262
+ data-testid={buildTestId('calendar', 'month-label', testId)}>{monthLabel}</span
263
+ >
261
264
  <button
262
265
  type="button"
263
266
  onclick={nextMonth}
@@ -266,6 +269,7 @@
266
269
  density.navBtn
267
270
  )}
268
271
  aria-label="Next month"
272
+ data-testid={buildTestId('calendar', 'next-month', testId)}
269
273
  {disabled}
270
274
  >
271
275
  <svg class={density.navIcon} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
@@ -284,6 +288,7 @@
284
288
  'text-default-400 mb-1 grid grid-cols-7 gap-0.5 text-center font-medium',
285
289
  density.dayHeaderText
286
290
  )}
291
+ data-testid={buildTestId('calendar', 'day-headers', testId)}
287
292
  >
288
293
  {#each dayHeaders() as d (d)}
289
294
  <div>{d}</div>
@@ -298,6 +303,7 @@
298
303
  disabled={cell.disabled}
299
304
  aria-pressed={cell.isSelected}
300
305
  aria-label={cell.date.toLocaleDateString()}
306
+ data-testid={buildTestId('calendar', 'day', testId, cell.date.getDate())}
301
307
  class={cn(
302
308
  'relative flex items-center justify-center rounded transition-colors',
303
309
  density.cell,
@@ -50,5 +50,14 @@ export type CalendarProps = {
50
50
  from: Date | null;
51
51
  to: Date | null;
52
52
  }) => void;
53
+ /**
54
+ * Test ID prefix. When set, the component emits these selectors:
55
+ * - `{testId}-calendar` — root wrapper
56
+ * - `{testId}-calendar-prev-month` — prev button
57
+ * - `{testId}-calendar-next-month` — next button
58
+ * - `{testId}-calendar-month-label` — month/year header
59
+ * - `{testId}-calendar-day-headers` — weekday row
60
+ * - `{testId}-calendar-day-{dayNumber}` — each day cell
61
+ */
53
62
  testId?: string;
54
63
  };
@@ -127,7 +127,14 @@
127
127
  </button>
128
128
 
129
129
  {#snippet content()}
130
- <Calendar {value} {minDate} {maxDate} {size} onselect={(d) => handleSelect(d as Date)} />
130
+ <Calendar
131
+ {value}
132
+ {minDate}
133
+ {maxDate}
134
+ {size}
135
+ class="max-sm:w-full max-sm:rounded-none max-sm:border-0 max-sm:shadow-none"
136
+ onselect={(d) => handleSelect(d as Date)}
137
+ />
131
138
  {/snippet}
132
139
  </Popover>
133
140
 
@@ -280,6 +280,13 @@ export type NumberInputProps = {
280
280
  * BCP 47 locale used for thousands-separator formatting. @default 'en-US'
281
281
  */
282
282
  locale?: string;
283
+ /**
284
+ * Test ID prefix. When set, the component emits these selectors:
285
+ * - `{testId}-numberinput` — root input
286
+ * - `{testId}-numberinput-label` — label
287
+ * - `{testId}-numberinput-unit` — unit label/button
288
+ * - `{testId}-numberinput-unit-dropdown` — unit dropdown
289
+ */
283
290
  testId?: string;
284
291
  };
285
292
  /**
@@ -334,6 +341,14 @@ export interface DateRangeProps {
334
341
  }) => void;
335
342
  id?: string;
336
343
  name?: string;
344
+ /**
345
+ * Test ID prefix. When set, the component emits these selectors:
346
+ * - `{testId}-date-range` — root wrapper
347
+ * - `{testId}-date-range-trigger` — trigger button
348
+ * - `{testId}-date-range-clear` — clear button
349
+ * - `{testId}-date-range-errors` — error container
350
+ * - `{testId}-date-range-panel` — calendar panel
351
+ */
337
352
  testId?: string;
338
353
  }
339
354
  /**
@@ -379,6 +394,14 @@ export type TagsProps = {
379
394
  onaddtag?: (tag: string) => void;
380
395
  /** Fires when a tag is removed (via × or backspace). */
381
396
  onremovetag?: (tag: string) => void;
397
+ /**
398
+ * Test ID prefix. When set, the component emits these selectors:
399
+ * - `{testId}-tags` — root wrapper
400
+ * - `{testId}-tags-input` — text input
401
+ * - `{testId}-tags-tag-{i}` — each tag badge
402
+ * - `{testId}-tags-suggestions` — dropdown
403
+ * - `{testId}-tags-suggestion-{i}` — each suggestion
404
+ */
382
405
  testId?: string;
383
406
  };
384
407
  /** Selection mode for `<Slider>`. */
@@ -118,7 +118,10 @@
118
118
 
119
119
  function setThisMonth() {
120
120
  const now = new Date();
121
- value = new Date(now.getFullYear(), now.getMonth(), 1);
121
+ const d = new Date(now.getFullYear(), now.getMonth(), 1);
122
+ if (minDate && d < new Date(minDate.getFullYear(), minDate.getMonth(), 1)) return;
123
+ if (maxDate && d > new Date(maxDate.getFullYear(), maxDate.getMonth(), 1)) return;
124
+ value = d;
122
125
  onselect?.(value);
123
126
  open = false;
124
127
  }
@@ -135,14 +138,18 @@
135
138
  });
136
139
  </script>
137
140
 
138
- <div class={cn('w-full', className)}>
141
+ <div class={cn('w-full', className)} data-testid={buildTestId('month-picker', 'root', testId)}>
139
142
  {#if label}
140
143
  <label for={id} class="text-default-700 mb-1 block text-sm font-medium">
141
144
  {label}
142
145
  </label>
143
146
  {/if}
144
147
 
145
- <input type="hidden" {name} value={value?.toISOString() ?? ''} />
148
+ <input
149
+ type="hidden"
150
+ {name}
151
+ value={value ? `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}` : ''}
152
+ />
146
153
 
147
154
  <Popover trigger="manual" bind:open placement="bottom" {disabled}>
148
155
  <button
@@ -239,6 +246,7 @@
239
246
  yearSelected ? 'bg-primary-50 text-primary-700' : 'bg-default-50 text-default-800'
240
247
  )}
241
248
  data-year={year}
249
+ data-testid={buildTestId('month-picker', 'year', testId, year)}
242
250
  >
243
251
  {year}
244
252
  </div>
@@ -251,6 +259,7 @@
251
259
  <button
252
260
  type="button"
253
261
  data-month={m}
262
+ data-testid={buildTestId('month-picker', 'month', testId, `${year}-${m}`)}
254
263
  data-selected={sel || undefined}
255
264
  data-current={cur || undefined}
256
265
  onclick={() => entry && pick(entry)}
@@ -18,5 +18,12 @@ export type MonthPickerProps = {
18
18
  errors?: string[];
19
19
  class?: ClassValue;
20
20
  onselect?: (value: Date | null) => void;
21
+ /**
22
+ * Test ID prefix. When set, the component emits these selectors:
23
+ * - `{testId}-month-picker-root` — root wrapper
24
+ * - `{testId}-month-picker` — trigger button
25
+ * - `{testId}-month-picker-year-{year}` — year header
26
+ * - `{testId}-month-picker-month-{year}-{month}` — month cell
27
+ */
21
28
  testId?: string;
22
29
  };
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../helper/cls.js';
3
+ import { buildTestId } from '../helper/testid.js';
3
4
  import { breadcrumbs } from './breadcrumbs.js';
4
5
  import type { BreadcrumbsProps } from '../index.js';
5
6
  import { resolve } from '$app/paths';
@@ -14,7 +15,8 @@
14
15
  listClass = '',
15
16
  itemClass = '',
16
17
  separatorClass = '',
17
- wrapperClass = ''
18
+ wrapperClass = '',
19
+ testId
18
20
  }: BreadcrumbsProps = $props();
19
21
 
20
22
  const { base, list, item, separator, wrapper } = $derived(
@@ -33,10 +35,14 @@
33
35
  const wrapperClasses = $derived(cn(wrapper(), wrapperClass));
34
36
  </script>
35
37
 
36
- <nav class={baseClass} aria-label="Breadcrumb">
38
+ <nav
39
+ class={baseClass}
40
+ aria-label="Breadcrumb"
41
+ data-testid={buildTestId('breadcrumbs', undefined, testId)}
42
+ >
37
43
  <ol role="list" class={listClasses}>
38
44
  {#each items as breadcrumbItem, index (index)}
39
- <li>
45
+ <li data-testid={buildTestId('breadcrumbs', 'item', testId, index)}>
40
46
  <div class={wrapperClasses}>
41
47
  {#if index > 0}
42
48
  <span class={separatorClasses} aria-hidden="true">
@@ -58,7 +64,11 @@
58
64
  </span>
59
65
  {/if}
60
66
  {#if breadcrumbItem.current}
61
- <span class={activeItemClasses} aria-current="page">
67
+ <span
68
+ class={activeItemClasses}
69
+ aria-current="page"
70
+ data-testid={buildTestId('breadcrumbs', 'current', testId, index)}
71
+ >
62
72
  {#if breadcrumbItem.icon}
63
73
  {@const ItemIcon = breadcrumbItem.icon}
64
74
  <ItemIcon class="size-4 flex-shrink-0" />
@@ -66,7 +76,11 @@
66
76
  {breadcrumbItem.label}
67
77
  </span>
68
78
  {:else}
69
- <a href={resolve(breadcrumbItem.href as `/`)} class={itemClasses}>
79
+ <a
80
+ href={resolve(breadcrumbItem.href as `/`)}
81
+ class={itemClasses}
82
+ data-testid={buildTestId('breadcrumbs', 'link', testId, index)}
83
+ >
70
84
  {#if breadcrumbItem.icon}
71
85
  {@const ItemIcon = breadcrumbItem.icon}
72
86
  <ItemIcon class="size-4 flex-shrink-0" />
@@ -49,6 +49,13 @@ export type BreadcrumbsProps = {
49
49
  separatorClass?: ClassValue;
50
50
  /** Classes on the inner item wrapper (icon + label). */
51
51
  wrapperClass?: ClassValue;
52
+ /**
53
+ * Test ID prefix. When set, the component emits these selectors:
54
+ * - `{testId}-breadcrumbs` — root nav
55
+ * - `{testId}-breadcrumbs-item-{i}` — each item
56
+ * - `{testId}-breadcrumbs-link-{i}` — link element
57
+ * - `{testId}-breadcrumbs-current-{i}` — current page
58
+ */
52
59
  testId?: string;
53
60
  };
54
61
  /**
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../../helper/cls.js';
3
+ import { buildTestId } from '../../helper/testid.js';
3
4
  import {
4
5
  activityList,
5
6
  iconCircleClasses,
@@ -25,7 +26,8 @@
25
26
  itemClass = '',
26
27
  onitemclick,
27
28
  children,
28
- customContent
29
+ customContent,
30
+ testId
29
31
  }: ActivityListProps = $props();
30
32
 
31
33
  const {
@@ -54,7 +56,7 @@
54
56
  const contentClasses = $derived(cn(content(), contentClass, timeline && 'relative divide-y-0'));
55
57
  </script>
56
58
 
57
- <div class={baseClass}>
59
+ <div class={baseClass} data-testid={buildTestId('activity-list', undefined, testId)}>
58
60
  {#if title}
59
61
  <div class={headerClasses}>
60
62
  <h2 class={titleClasses}>{title}</h2>
@@ -85,6 +87,7 @@
85
87
  <div
86
88
  class={cn(item(), highlightClass, itemClass)}
87
89
  data-activity-row=""
90
+ data-testid={buildTestId('activity-list', 'item', testId, index)}
88
91
  data-activity-highlighted={activityItem.highlighted ? '' : undefined}
89
92
  >
90
93
  {#if accentBarColor}
@@ -113,9 +116,13 @@
113
116
  data-activity-comment=""
114
117
  >
115
118
  <div class="mb-1 flex justify-between gap-x-4">
116
- <div class="flex flex-wrap items-center gap-2">
119
+ <div
120
+ class="flex flex-wrap items-center gap-2"
121
+ data-testid={buildTestId('activity-list', 'badges', testId, index)}
122
+ >
117
123
  <button
118
124
  class="text-default-900 cursor-pointer text-sm font-medium"
125
+ data-testid={buildTestId('activity-list', 'title', testId, index)}
119
126
  onclick={() => onitemclick?.(activityItem, index)}
120
127
  >
121
128
  {activityItem.title}
@@ -140,7 +147,11 @@
140
147
  {/if}
141
148
  </div>
142
149
  {#if activityItem.timestamp}
143
- <div class={itemTimestamp()} data-activity-timestamp="">
150
+ <div
151
+ class={itemTimestamp()}
152
+ data-activity-timestamp=""
153
+ data-testid={buildTestId('activity-list', 'timestamp', testId, index)}
154
+ >
144
155
  {activityItem.timestamp}
145
156
  </div>
146
157
  {/if}
@@ -166,8 +177,15 @@
166
177
  {:else}
167
178
  <div class={itemContent()}>
168
179
  <div class={itemMain()}>
169
- <div class={itemHeader()}>
170
- <button class={itemTitle()} onclick={() => onitemclick?.(activityItem, index)}>
180
+ <div
181
+ class={itemHeader()}
182
+ data-testid={buildTestId('activity-list', 'badges', testId, index)}
183
+ >
184
+ <button
185
+ class={itemTitle()}
186
+ data-testid={buildTestId('activity-list', 'title', testId, index)}
187
+ onclick={() => onitemclick?.(activityItem, index)}
188
+ >
171
189
  {activityItem.title}
172
190
  </button>
173
191
  {#if activityItem.badges}
@@ -197,7 +215,11 @@
197
215
  </div>
198
216
 
199
217
  {#if activityItem.timestamp}
200
- <div class={itemTimestamp()} data-activity-timestamp="">
218
+ <div
219
+ class={itemTimestamp()}
220
+ data-activity-timestamp=""
221
+ data-testid={buildTestId('activity-list', 'timestamp', testId, index)}
222
+ >
201
223
  {activityItem.timestamp}
202
224
  </div>
203
225
  {/if}
@@ -145,5 +145,13 @@ export type ActivityListProps = {
145
145
  * `custom: true` continue to use the default rendering.
146
146
  */
147
147
  customContent?: Snippet<[ActivityItem, number]>;
148
+ /**
149
+ * Test ID prefix. When set, the component emits these selectors:
150
+ * - `{testId}-activity-list` — root wrapper
151
+ * - `{testId}-activity-list-item-{i}` — each item
152
+ * - `{testId}-activity-list-title-{i}` — item title
153
+ * - `{testId}-activity-list-timestamp-{i}` — timestamp
154
+ * - `{testId}-activity-list-badges-{i}` — badges container
155
+ */
148
156
  testId?: string;
149
157
  };