@makolabs/ripple 3.0.5 → 3.0.6

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.
@@ -118,7 +118,7 @@
118
118
  </span>
119
119
  <button
120
120
  onclick={copyCode}
121
- class="flex items-center gap-1.5 rounded px-2 py-1 text-xs transition-all duration-200 {copied
121
+ class="flex cursor-pointer items-center gap-1.5 rounded px-2 py-1 text-xs transition-all duration-200 {copied
122
122
  ? 'border border-green-500 bg-green-500/10 text-green-400'
123
123
  : 'text-default-400 hover:bg-default-700 hover:text-default-200'}"
124
124
  title={copied ? 'Copied!' : 'Copy code'}
@@ -166,7 +166,7 @@
166
166
  bind:this={copyButtonRef}
167
167
  onclick={handleCopyClick}
168
168
  ondblclick={handleCopyDoubleClick}
169
- class="bg-default-100 text-default-600 hover:bg-default-200 hover:text-default-800 flex items-center gap-1 rounded-md px-3 py-1 text-xs font-medium opacity-0 transition-opacity duration-200 group-hover:opacity-100"
169
+ class="bg-default-100 text-default-600 hover:bg-default-200 hover:text-default-800 flex cursor-pointer items-center gap-1 rounded-md px-3 py-1 text-xs font-medium opacity-0 transition-opacity duration-200 group-hover:opacity-100"
170
170
  title="Click: Copy markdown • Double-click: Copy rich text"
171
171
  >
172
172
  {#if showCopied}
@@ -114,6 +114,17 @@ export interface FileBrowserProps {
114
114
  * @default true
115
115
  */
116
116
  allowFolderNavigation?: boolean;
117
+ /**
118
+ * Per-row selectability gate. Return `false` to prevent the row's
119
+ * checkbox from rendering — the file still appears in the list but
120
+ * can't be selected. When omitted every file is selectable.
121
+ *
122
+ * @example
123
+ * ```svelte
124
+ * <FileBrowser {adapter} isSelectable={(f) => !f.isFolder && f.size > 0} />
125
+ * ```
126
+ */
127
+ isSelectable?: (file: FileItem) => boolean;
117
128
  /** Additional CSS classes for the outer container. */
118
129
  class?: ClassValue;
119
130
  /**
@@ -22,11 +22,11 @@ export const buttonVariants = tv({
22
22
  [Color.DANGER]: 'bg-danger-600 text-white hover:bg-danger-700 focus-visible:ring-danger-500'
23
23
  },
24
24
  size: {
25
- [Size.XS]: 'h-7 px-2 text-xs',
26
- [Size.SM]: 'h-8 px-3 text-sm',
27
- [Size.MD]: 'h-9 px-4 text-sm',
28
- [Size.LG]: 'h-10 px-5 text-base',
29
- [Size.XL]: 'h-12 px-6 text-lg',
25
+ [Size.XS]: 'h-6 px-2 text-[10px]',
26
+ [Size.SM]: 'h-7 px-2.5 text-[11px]',
27
+ [Size.MD]: 'h-8 px-3 text-xs',
28
+ [Size.LG]: 'h-9 px-4 text-sm',
29
+ [Size.XL]: 'h-11 px-5 text-base',
30
30
  [Size.XXL]: 'h-14 px-8 text-xl'
31
31
  },
32
32
  rounded: {
@@ -8,11 +8,11 @@ export const badge = tv({
8
8
  variants: {
9
9
  size: {
10
10
  [Size.XS]: {
11
- base: 'h-4 px-1.5 text-xs rounded gap-0.5',
11
+ base: 'h-4 px-1.5 text-[10px] rounded gap-0.5',
12
12
  icon: 'h-2.5 w-2.5'
13
13
  },
14
14
  [Size.SM]: {
15
- base: 'h-5 px-2 text-xs rounded gap-1',
15
+ base: 'h-5 px-2 text-[11px] rounded gap-1',
16
16
  icon: 'h-3 w-3'
17
17
  },
18
18
  [Size.MD]: {
@@ -427,7 +427,7 @@
427
427
  {#if open && isMobile}
428
428
  <button
429
429
  type="button"
430
- class="fixed inset-0 z-[9998] bg-black/40 backdrop-blur-sm"
430
+ class="fixed inset-0 z-[9998] cursor-pointer bg-black/40 backdrop-blur-sm"
431
431
  aria-label="Close"
432
432
  onclick={() => (open = false)}
433
433
  ></button>
@@ -29,32 +29,32 @@ export const dropdownMenu = tv({
29
29
  },
30
30
  size: {
31
31
  [Size.XS]: {
32
- trigger: 'px-2 py-1 text-xs',
33
- item: 'px-2.5 py-1 text-xs',
34
- itemIcon: 'mr-1.5 size-3.5',
35
- header: 'px-2.5 py-1.5',
36
- headerTitle: 'text-xs',
37
- headerSubtitle: 'text-xs'
32
+ trigger: 'px-1.5 py-0.5 text-[10px]',
33
+ item: 'px-2 py-1 text-[10px]',
34
+ itemIcon: 'mr-1.5 size-3',
35
+ header: 'px-2 py-1',
36
+ headerTitle: 'text-[10px]',
37
+ headerSubtitle: 'text-[10px]'
38
38
  },
39
39
  [Size.SM]: {
40
+ trigger: 'px-2 py-1 text-[11px]',
41
+ item: 'px-2.5 py-1.5 text-[11px]',
42
+ itemIcon: 'mr-2 size-3.5',
43
+ header: 'px-2.5 py-1.5',
44
+ headerTitle: 'text-[11px]',
45
+ headerSubtitle: 'text-[11px]'
46
+ },
47
+ [Size.MD]: {
40
48
  trigger: 'px-2.5 py-1.5 text-xs',
41
- item: 'px-3 py-1.5 text-xs',
42
- itemIcon: 'mr-2 size-4',
49
+ item: 'px-3 py-2 text-xs',
50
+ itemIcon: 'mr-2.5 size-4',
43
51
  header: 'px-3 py-2',
44
52
  headerTitle: 'text-xs',
45
53
  headerSubtitle: 'text-xs'
46
54
  },
47
- [Size.MD]: {
48
- trigger: 'px-3 py-2 text-sm',
49
- item: 'px-4 py-2 text-sm',
50
- itemIcon: 'mr-3 size-5',
51
- header: 'px-4 py-3',
52
- headerTitle: 'text-sm',
53
- headerSubtitle: 'text-sm'
54
- },
55
55
  [Size.LG]: {
56
- trigger: 'px-4 py-2.5 text-base',
57
- item: 'px-5 py-2.5 text-base',
56
+ trigger: 'px-3 py-2 text-sm',
57
+ item: 'px-4 py-2.5 text-sm',
58
58
  itemIcon: 'mr-3 size-6',
59
59
  header: 'px-5 py-3.5',
60
60
  headerTitle: 'text-base',
@@ -234,7 +234,7 @@
234
234
  {#if open && useSheet}
235
235
  <button
236
236
  type="button"
237
- class="fixed inset-0 z-[9998] bg-black/40 backdrop-blur-sm"
237
+ class="fixed inset-0 z-[9998] cursor-pointer bg-black/40 backdrop-blur-sm"
238
238
  aria-label="Close"
239
239
  onclick={closeOnOutsideClick ? close : undefined}
240
240
  ></button>
@@ -8,11 +8,11 @@ export const spinner = tv({
8
8
  },
9
9
  variants: {
10
10
  size: {
11
- [Size.XS]: { svg: 'h-3 w-3', label: 'text-xs' },
12
- [Size.SM]: { svg: 'h-4 w-4', label: 'text-xs' },
13
- [Size.MD]: { svg: 'h-5 w-5', label: 'text-sm' },
14
- [Size.LG]: { svg: 'h-6 w-6', label: 'text-base' },
15
- [Size.XL]: { svg: 'h-8 w-8', label: 'text-lg' },
11
+ [Size.XS]: { svg: 'h-3 w-3', label: 'text-[10px]' },
12
+ [Size.SM]: { svg: 'h-4 w-4', label: 'text-[11px]' },
13
+ [Size.MD]: { svg: 'h-5 w-5', label: 'text-xs' },
14
+ [Size.LG]: { svg: 'h-6 w-6', label: 'text-sm' },
15
+ [Size.XL]: { svg: 'h-8 w-8', label: 'text-base' },
16
16
  [Size.XXL]: { svg: 'h-12 w-12', label: 'text-xl' }
17
17
  },
18
18
  color: {
@@ -19,6 +19,7 @@
19
19
  infoSection,
20
20
  selectAllScope = 'page',
21
21
  allowFolderNavigation = true,
22
+ isSelectable,
22
23
  height = 'h-[500px]',
23
24
  class: className = '',
24
25
  selectedItems = $bindable<FileItem[]>([]),
@@ -780,7 +781,7 @@
780
781
  <div class="flex min-w-0 flex-1 items-center">
781
782
  {#if breadcrumbs.length > 1}
782
783
  <button
783
- class="text-default-600 hover:bg-default-100 mr-1 rounded-full px-2 py-1"
784
+ class="text-default-600 hover:bg-default-100 mr-1 cursor-pointer rounded-full px-2 py-1"
784
785
  onclick={navigateUp}
785
786
  title="Go up"
786
787
  aria-label="Navigate to parent folder"
@@ -898,7 +899,7 @@
898
899
  />
899
900
  {#if searchQuery}
900
901
  <button
901
- class="text-default-400 hover:text-default-600 absolute right-1 text-xs"
902
+ class="text-default-400 hover:text-default-600 absolute right-1 cursor-pointer text-xs"
902
903
  onclick={clearSearch}
903
904
  title="Clear search"
904
905
  >
@@ -908,7 +909,7 @@
908
909
  </div>
909
910
  <button
910
911
  onclick={handleSearch}
911
- class="bg-default-200 hover:bg-default-300 rounded px-2 py-1 text-xs"
912
+ class="bg-default-200 hover:bg-default-300 cursor-pointer rounded px-2 py-1 text-xs"
912
913
  >
913
914
  Search
914
915
  </button>
@@ -955,6 +956,7 @@
955
956
  }}
956
957
  onsort={handleSort}
957
958
  selectable={true}
959
+ isRowSelectable={isSelectable}
958
960
  {selectAllScope}
959
961
  {onselect}
960
962
  {selected}
@@ -345,7 +345,7 @@
345
345
  <Portal target={datePickerRef}>
346
346
  <button
347
347
  type="button"
348
- class="fixed inset-0 z-[9998] bg-black/40 backdrop-blur-sm"
348
+ class="fixed inset-0 z-[9998] cursor-pointer bg-black/40 backdrop-blur-sm"
349
349
  aria-label="Close"
350
350
  onclick={() => (isOpen = false)}
351
351
  ></button>
@@ -455,7 +455,7 @@
455
455
  </button>
456
456
  <button
457
457
  type="button"
458
- class="text-default-700 inline-flex items-center rounded-md px-2 py-1 text-sm font-medium"
458
+ class="text-default-700 inline-flex cursor-pointer items-center rounded-md px-2 py-1 text-sm font-medium"
459
459
  >{viewDate.getFullYear() - 6} - {viewDate.getFullYear() + 5}</button
460
460
  >
461
461
  <button
@@ -2,7 +2,7 @@
2
2
  import { Color, Size } from '../variants.js';
3
3
  import SegmentedControl from './SegmentedControl.svelte';
4
4
  import { COUNTRY_NAMES } from './market/country-data.js';
5
- import { countryCodeToFlagEmoji } from './market/flag-emoji.js';
5
+ import FlagSvg from './market/FlagSvg.svelte';
6
6
  import type { CountryCode } from './market/country-data.js';
7
7
  import type { MarketSelectorProps } from './market/market-selector-types.js';
8
8
  import type { SegmentedOption } from '../index.js';
@@ -37,7 +37,8 @@
37
37
  value: code,
38
38
  label: code,
39
39
  title: COUNTRY_NAMES[code],
40
- prefix: effectiveShowFlags ? countryCodeToFlagEmoji(code) : undefined
40
+ prefix: effectiveShowFlags ? (FlagSvg as SegmentedOption['prefix']) : undefined,
41
+ prefixProps: effectiveShowFlags ? { code } : undefined
41
42
  }));
42
43
  });
43
44
 
@@ -153,7 +153,7 @@
153
153
  unit to pick from. Otherwise the unit is static text below. -->
154
154
  <button
155
155
  type="button"
156
- class="hover:bg-default-100 flex items-center gap-1 rounded px-1"
156
+ class="hover:bg-default-100 flex cursor-pointer items-center gap-1 rounded px-1"
157
157
  data-testid={buildTestId('numberinput', 'unit', testId)}
158
158
  onclick={handleUnitToggle}
159
159
  {disabled}
@@ -191,7 +191,7 @@
191
191
  {#each units as unitOption (unitOption.value)}
192
192
  <button
193
193
  type="button"
194
- class="hover:bg-default-100 w-full px-3 py-1.5 text-left text-sm"
194
+ class="hover:bg-default-100 w-full cursor-pointer px-3 py-1.5 text-left text-sm"
195
195
  class:bg-default-50={unit === unitOption.value}
196
196
  onclick={() => handleUnitSelect(unitOption.value)}
197
197
  >
@@ -155,14 +155,16 @@
155
155
  onkeydown={(e) => handleSegmentKeydown(e, index)}
156
156
  >
157
157
  {#if option.prefix}
158
- <span class="text-base leading-none" aria-hidden="true">{option.prefix}</span>
158
+ <span class="leading-none" aria-hidden="true">
159
+ {#if typeof option.prefix === 'string'}
160
+ {option.prefix}
161
+ {:else}
162
+ {@const PrefixComponent = option.prefix}
163
+ <PrefixComponent {...option.prefixProps} />
164
+ {/if}
165
+ </span>
159
166
  {/if}
160
- <span
161
- class={cn(
162
- !compact && (size === Size.XS || size === Size.SM) && 'text-sm',
163
- compact && 'sr-only'
164
- )}
165
- >
167
+ <span class={cn(compact && 'sr-only')}>
166
168
  {option.label}
167
169
  </span>
168
170
  </button>
@@ -99,7 +99,7 @@
99
99
  type="button"
100
100
  onclick={clear}
101
101
  aria-label="Clear date"
102
- class="text-default-400 hover:text-default-700 flex size-5 items-center justify-center rounded"
102
+ class="text-default-400 hover:text-default-700 flex size-5 cursor-pointer items-center justify-center rounded"
103
103
  >
104
104
  <svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
105
105
  <path
@@ -22,29 +22,29 @@ const xl = {
22
22
  };
23
23
  export const formSizeTokens = {
24
24
  [Size.XS]: {
25
- height: 'h-5',
26
- padX: 'px-1',
27
- padY: 'py-0',
28
- text: 'text-xs',
25
+ height: 'h-6',
26
+ padX: 'px-1.5',
27
+ padY: 'py-0.5',
28
+ text: 'text-[10px]',
29
29
  gap: 'gap-1',
30
30
  iconSize: 'size-3',
31
31
  radius: 'rounded-sm',
32
32
  shadow: 'shadow-none'
33
33
  },
34
34
  [Size.SM]: {
35
- height: 'h-6',
36
- padX: 'px-1.5',
37
- padY: 'py-0.5',
38
- text: 'text-xs',
35
+ height: 'h-7',
36
+ padX: 'px-2',
37
+ padY: 'py-1',
38
+ text: 'text-[11px]',
39
39
  gap: 'gap-1',
40
40
  iconSize: 'size-3',
41
41
  radius: 'rounded',
42
42
  shadow: 'shadow-xs'
43
43
  },
44
44
  [Size.MD]: {
45
- height: 'h-7',
46
- padX: 'px-2',
47
- padY: 'py-1',
45
+ height: 'h-8',
46
+ padX: 'px-2.5',
47
+ padY: 'py-1.5',
48
48
  text: 'text-xs',
49
49
  gap: 'gap-1.5',
50
50
  iconSize: 'size-3.5',
@@ -581,8 +581,14 @@ export interface SliderProps {
581
581
  * Unlike `TabGroup`, segments are purely a control (no panels).
582
582
  */
583
583
  export type SegmentedOption = RadioOption & {
584
- /** Content rendered before the label (e.g. flag emoji, icon). */
585
- prefix?: string;
584
+ /**
585
+ * Content rendered before the label. Pass a string (e.g. emoji) or
586
+ * a Svelte component (e.g. `FlagSvg` for SVG country flags).
587
+ * When a component, pass its props via `prefixProps`.
588
+ */
589
+ prefix?: string | Component;
590
+ /** Props forwarded to the `prefix` component (ignored for strings). */
591
+ prefixProps?: Record<string, any>;
586
592
  /** Native `title` attribute on the segment button (hover tooltip). */
587
593
  title?: string;
588
594
  disabled?: boolean;
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import { cn } from '../../helper/cls.js';
3
+
4
+ let {
5
+ code,
6
+ class: className = ''
7
+ }: {
8
+ /** ISO 3166-1 alpha-2 country code (e.g. 'DE', 'US'). */
9
+ code: string;
10
+ class?: string;
11
+ } = $props();
12
+
13
+ const lc = $derived(code.toLowerCase());
14
+
15
+ // flagcdn.com serves public-domain SVG flags — no API key, no CORS issues,
16
+ // cached aggressively by browsers. Falls back to a neutral gray placeholder
17
+ // if the image fails to load.
18
+ let failed = $state(false);
19
+ </script>
20
+
21
+ {#if !failed}
22
+ <img
23
+ src="https://flagcdn.com/{lc}.svg"
24
+ alt={code}
25
+ class={cn('inline-block h-3 w-4 rounded-sm object-cover', className)}
26
+ loading="lazy"
27
+ onerror={() => (failed = true)}
28
+ />
29
+ {:else}
30
+ <span
31
+ class={cn(
32
+ 'bg-default-200 text-default-500 inline-flex h-3 w-4 items-center justify-center rounded-sm text-[8px]',
33
+ className
34
+ )}
35
+ >
36
+ {code}
37
+ </span>
38
+ {/if}
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ /** ISO 3166-1 alpha-2 country code (e.g. 'DE', 'US'). */
3
+ code: string;
4
+ class?: string;
5
+ };
6
+ declare const FlagSvg: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type FlagSvg = ReturnType<typeof FlagSvg>;
8
+ export default FlagSvg;
package/dist/index.d.ts CHANGED
@@ -129,6 +129,7 @@ export { default as MonthPicker } from './forms/month-picker/MonthPicker.svelte'
129
129
  export { default as Tags } from './forms/Tags.svelte';
130
130
  export { default as SegmentedControl } from './forms/SegmentedControl.svelte';
131
131
  export { default as MarketSelector } from './forms/MarketSelector.svelte';
132
+ export { default as FlagSvg } from './forms/market/FlagSvg.svelte';
132
133
  export { dropdownMenu } from './elements/dropdown/dropdown.js';
133
134
  export { badge } from './elements/badge/badge.js';
134
135
  export { buttonVariants } from './button/button.js';
package/dist/index.js CHANGED
@@ -116,6 +116,7 @@ export { default as MonthPicker } from './forms/month-picker/MonthPicker.svelte'
116
116
  export { default as Tags } from './forms/Tags.svelte';
117
117
  export { default as SegmentedControl } from './forms/SegmentedControl.svelte';
118
118
  export { default as MarketSelector } from './forms/MarketSelector.svelte';
119
+ export { default as FlagSvg } from './forms/market/FlagSvg.svelte';
119
120
  // ============================================================================
120
121
  // Component Variant Utilities
121
122
  // ============================================================================
@@ -108,7 +108,7 @@
108
108
  <div class="min-w-0 flex-1" data-activity-custom="">
109
109
  {@render customContent?.(activityItem, index)}
110
110
  </div>
111
- {:else if timeline && activityItem.subtitle}
111
+ {:else if timeline && activityItem.subtitle && activityItem.timelineStyle === 'bubble'}
112
112
  <div class="min-w-0 flex-1">
113
113
  <div class="flex items-start justify-between gap-3">
114
114
  <div
@@ -76,6 +76,16 @@ export type ActivityItem = {
76
76
  * inside the snippet.
77
77
  */
78
78
  custom?: boolean;
79
+ /**
80
+ * Controls how the row renders when the parent list has `timeline`
81
+ * mode enabled and a `subtitle` is present.
82
+ * - `'flat'` — inline text, no card wrapper (default)
83
+ * - `'bubble'` — white rounded card with a ring border (comment style)
84
+ *
85
+ * Has no effect when `timeline` is false on the parent list.
86
+ * @default 'flat'
87
+ */
88
+ timelineStyle?: 'flat' | 'bubble';
79
89
  };
80
90
  /** Density variants specific to ActivityList. */
81
91
  export type ActivityListSize = 'sm' | 'md';
@@ -6,7 +6,7 @@ export const activityList = tv({
6
6
  header: '',
7
7
  title: 'font-medium text-default-900',
8
8
  content: 'divide-y divide-default-200',
9
- item: 'relative flex items-start hover:bg-default-50 transition-colors',
9
+ item: 'relative flex items-center hover:bg-default-50 transition-colors',
10
10
  accentBar: 'absolute inset-y-0 left-0 w-[3px]',
11
11
  timelineLine: 'absolute top-0 bottom-0 w-[2px] bg-default-200',
12
12
  iconWrapper: 'relative z-10 flex shrink-0 items-center justify-center rounded-full ring-4 ring-white',
@@ -155,7 +155,7 @@
155
155
  type="button"
156
156
  onclick={toggle}
157
157
  aria-label="Close navigation"
158
- class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm md:hidden"
158
+ class="fixed inset-0 z-40 cursor-pointer bg-black/50 backdrop-blur-sm md:hidden"
159
159
  ></button>
160
160
  {/if}
161
161
 
@@ -13,6 +13,7 @@
13
13
  let {
14
14
  data = [],
15
15
  columns = [],
16
+ size = 'md',
16
17
  bordered: externalBordered,
17
18
  striped = false,
18
19
  pageSize = 10,
@@ -49,6 +50,7 @@
49
50
  selectAllScope = 'page',
50
51
  rowKey,
51
52
  expandable = false,
53
+ isRowSelectable,
52
54
  testId
53
55
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
56
  }: TableProps<any> = $props();
@@ -100,6 +102,7 @@
100
102
  sortIcon: sortIconBaseClass
101
103
  } = $derived(
102
104
  table({
105
+ size,
103
106
  bordered,
104
107
  striped
105
108
  })
@@ -234,8 +237,12 @@
234
237
  return rowIdentity(a) === rowIdentity(b);
235
238
  }
236
239
 
240
+ function canSelectRow(row: DataRow): boolean {
241
+ return !isRowSelectable || isRowSelectable(row);
242
+ }
243
+
237
244
  function toggleRowSelection(row: DataRow) {
238
- if (!selectable) return;
245
+ if (!selectable || !canSelectRow(row)) return;
239
246
 
240
247
  const index = selected.findIndex((r) => sameRow(r, row));
241
248
  if (index === -1) {
@@ -280,7 +287,7 @@
280
287
 
281
288
  function handleSelectAll() {
282
289
  const pageData = getPaginatedData();
283
- const scopeData = selectAllScope === 'all' ? data : pageData;
290
+ const scopeData = (selectAllScope === 'all' ? data : pageData).filter(canSelectRow);
284
291
  const allSelected = scopeData.every((r) => isRowSelected(r));
285
292
  if (allSelected) {
286
293
  if (selectAllScope === 'all') {
@@ -290,11 +297,11 @@
290
297
  }
291
298
  } else {
292
299
  if (selectAllScope === 'all') {
293
- selected = [...data];
300
+ selected = [...data.filter(canSelectRow)];
294
301
  } else {
295
302
  const newSelected = [...selected];
296
303
  for (const r of pageData) {
297
- if (!isRowSelected(r)) {
304
+ if (canSelectRow(r) && !isRowSelected(r)) {
298
305
  newSelected.push(r);
299
306
  }
300
307
  }
@@ -310,6 +317,9 @@
310
317
  }
311
318
 
312
319
  function handleRowClick(row: DataRow, index: number) {
320
+ if (selectable && canSelectRow(row)) {
321
+ toggleRowSelection(row);
322
+ }
313
323
  onrowclick?.(row, index);
314
324
  }
315
325
 
@@ -490,7 +500,7 @@
490
500
  <tr
491
501
  class={cn(trClasses, rowClass(row, rowIndex), {
492
502
  'bg-primary-100': selectable && isRowSelected(row),
493
- 'cursor-pointer': onrowclick
503
+ 'cursor-pointer': onrowclick || (selectable && canSelectRow(row))
494
504
  })}
495
505
  onclick={() => handleRowClick(row, rowIndex)}
496
506
  aria-selected={selectable && isRowSelected(row)}
@@ -500,7 +510,7 @@
500
510
  <td class={cn(tdClasses, 'w-10')}>
501
511
  <button
502
512
  type="button"
503
- class="text-default-400 hover:text-default-600 flex items-center justify-center transition-transform"
513
+ class="text-default-400 hover:text-default-600 flex cursor-pointer items-center justify-center transition-transform"
504
514
  onclick={(e) => {
505
515
  e.stopPropagation();
506
516
  toggleRowExpanded(row, rowIndex);
@@ -527,16 +537,18 @@
527
537
  {/if}
528
538
  {#if selectable}
529
539
  <td class={cn(tdClasses, 'text-center')}>
530
- <input
531
- type="checkbox"
532
- checked={isRowSelected(row)}
533
- onclick={(e) => {
534
- e.stopPropagation(); // Prevent row click
535
- toggleRowSelection(row);
536
- }}
537
- aria-label={`Select row ${rowIndex + 1}`}
538
- data-testid={buildTestId('table', 'row-select', testId, rowIndex)}
539
- />
540
+ {#if canSelectRow(row)}
541
+ <input
542
+ type="checkbox"
543
+ checked={isRowSelected(row)}
544
+ onclick={(e) => {
545
+ e.stopPropagation();
546
+ toggleRowSelection(row);
547
+ }}
548
+ aria-label={`Select row ${rowIndex + 1}`}
549
+ data-testid={buildTestId('table', 'row-select', testId, rowIndex)}
550
+ />
551
+ {/if}
540
552
  </td>
541
553
  {/if}
542
554
 
@@ -114,6 +114,8 @@ export type TableProps<T = DataRow> = {
114
114
  data: T[];
115
115
  /** Column definitions — see `TableColumn`. */
116
116
  columns: TableColumn<T>[];
117
+ /** Cell/header density. @default 'md' */
118
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
117
119
  /** Show horizontal/vertical borders. @default true (unless a title/subtitle is set) */
118
120
  bordered?: boolean;
119
121
  /** Alternate row backgrounds. @default false */
@@ -160,6 +162,13 @@ export type TableProps<T = DataRow> = {
160
162
  onpagechange?: (page: number) => void;
161
163
  /** Per-row dynamic classes (e.g. tint by status). */
162
164
  rowClass?: (row: T, index: number) => ClassValue;
165
+ /**
166
+ * Per-row selectability gate. Return `false` to hide the checkbox
167
+ * for that row. When omitted every row is selectable (if `selectable`
168
+ * is true). Doesn't affect `select all` — unselectable rows are
169
+ * excluded automatically.
170
+ */
171
+ isRowSelectable?: (row: T) => boolean;
163
172
  /** Show a skeleton placeholder instead of rows while loading. @default false */
164
173
  loading?: boolean;
165
174
  /** Expanded-row content snippet. Only used when `expandable` is true. */
@@ -8,7 +8,7 @@ export const table = tv({
8
8
  thead: '',
9
9
  tbody: 'divide-y divide-default-200',
10
10
  tr: 'transition-colors hover:bg-default-50',
11
- th: 'text-xs font-medium tracking-wider text-gray-500 uppercase whitespace-nowrap',
11
+ th: 'text-xs font-medium tracking-wider text-gray-500 whitespace-nowrap',
12
12
  td: 'whitespace-nowrap',
13
13
  footer: 'p-4',
14
14
  pagination: 'flex items-center justify-between',
@@ -19,28 +19,28 @@ export const table = tv({
19
19
  variants: {
20
20
  size: {
21
21
  xs: {
22
- th: 'px-2 py-1.5 text-xs',
23
- td: 'px-2 py-1.5 text-xs'
22
+ th: 'px-1.5 py-1 text-[10px]',
23
+ td: 'px-1.5 py-1 text-[10px]'
24
24
  },
25
25
  sm: {
26
- th: 'px-3 py-2 text-xs',
27
- td: 'px-3 py-2 text-sm'
26
+ th: 'px-2 py-1.5 text-[11px]',
27
+ td: 'px-2 py-1.5 text-[11px]'
28
28
  },
29
29
  md: {
30
- th: 'px-4 py-3 text-sm',
31
- td: 'px-4 py-3 text-sm'
30
+ th: 'px-2.5 py-2 text-xs',
31
+ td: 'px-2.5 py-2 text-xs'
32
32
  },
33
33
  lg: {
34
- th: 'px-6 py-4 text-sm',
35
- td: 'px-6 py-4 text-base'
34
+ th: 'px-4 py-3 text-sm',
35
+ td: 'px-4 py-3 text-sm'
36
36
  },
37
37
  xl: {
38
- th: 'px-8 py-5 text-base',
39
- td: 'px-8 py-5 text-base'
38
+ th: 'px-6 py-4 text-base',
39
+ td: 'px-6 py-4 text-base'
40
40
  },
41
41
  '2xl': {
42
- th: 'px-10 py-6 text-lg',
43
- td: 'px-10 py-6 text-lg'
42
+ th: 'px-8 py-5 text-lg',
43
+ td: 'px-8 py-5 text-lg'
44
44
  }
45
45
  },
46
46
  color: {
@@ -25,16 +25,16 @@ export const tabs = tv({
25
25
  },
26
26
  size: {
27
27
  [Size.XS]: {
28
- trigger: 'px-2 py-1 text-xs'
28
+ trigger: 'px-1.5 py-0.5 text-[10px]'
29
29
  },
30
30
  [Size.SM]: {
31
- trigger: 'px-2 py-1 text-xs'
31
+ trigger: 'px-2 py-1 text-[11px]'
32
32
  },
33
33
  [Size.MD]: {
34
- trigger: 'px-3 py-2 text-sm'
34
+ trigger: 'px-2.5 py-1.5 text-xs'
35
35
  },
36
36
  [Size.LG]: {
37
- trigger: 'px-4 py-2.5 text-base'
37
+ trigger: 'px-3 py-2 text-sm'
38
38
  },
39
39
  [Size.XL]: {
40
40
  trigger: 'px-5 py-3 text-lg'
@@ -47,7 +47,7 @@ export const tabs = tv({
47
47
  line: {},
48
48
  pill: {
49
49
  list: 'flex flex-nowrap border-none gap-2 p-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
50
- trigger: 'inline-flex items-center shrink-0 whitespace-nowrap px-3 py-2 text-sm transition-all duration-200 ease-in-out cursor-pointer rounded-full border-0'
50
+ trigger: 'inline-flex items-center shrink-0 whitespace-nowrap transition-all duration-200 ease-in-out cursor-pointer rounded-full border-0'
51
51
  }
52
52
  },
53
53
  selected: {
@@ -166,7 +166,7 @@
166
166
  <button
167
167
  type="button"
168
168
  onclick={toggleApiKeyVisibility}
169
- class="bg-default-50 text-default-500 hover:text-default-700 absolute top-1/2 right-3 -translate-y-1/2"
169
+ class="bg-default-50 text-default-500 hover:text-default-700 absolute top-1/2 right-3 -translate-y-1/2 cursor-pointer"
170
170
  aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
171
171
  >
172
172
  {#if showApiKey}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makolabs/ripple",
3
- "version": "3.0.5",
3
+ "version": "3.0.6",
4
4
  "description": "Simple Svelte 5 powered component library ✨",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {