@r2digisolutions/components 0.8.14 → 0.9.1

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.
@@ -90,7 +90,11 @@
90
90
  let path = $state<string[]>([]);
91
91
  let lastSelectedId = $state<string | null>(null);
92
92
 
93
- function findPathToItem(list: DropdownItem[], targetId: string, parents: string[] = []): string[] | null {
93
+ function findPathToItem(
94
+ list: DropdownItem[],
95
+ targetId: string,
96
+ parents: string[] = []
97
+ ): string[] | null {
94
98
  for (const item of list) {
95
99
  if (item.id === targetId) return parents;
96
100
  if (item.children?.length) {
@@ -101,7 +105,10 @@
101
105
  return null;
102
106
  }
103
107
 
104
- function levelFromPath(root: DropdownItem[], pathIds: string[]): { title: string; items: DropdownItem[] } {
108
+ function levelFromPath(
109
+ root: DropdownItem[],
110
+ pathIds: string[]
111
+ ): { title: string; items: DropdownItem[] } {
105
112
  let current = root;
106
113
  let title = label;
107
114
  for (const id of pathIds) {
@@ -147,7 +154,8 @@
147
154
  path = nextPath;
148
155
  const level = levelFromPath(items, nextPath);
149
156
  const idx = level.items.findIndex((item) => item.id === lastSelectedId);
150
- highlighted = idx >= 0 ? idx : (level.items.findIndex((i) => !i.separator && !i.disabled) ?? -1);
157
+ highlighted =
158
+ idx >= 0 ? idx : (level.items.findIndex((i) => !i.separator && !i.disabled) ?? -1);
151
159
  }
152
160
 
153
161
  /** Measure natural height without expanding the positioned popover (avoids jump). */
@@ -240,6 +248,24 @@
240
248
  if (menuEl?.matches(':popover-open')) menuEl.hidePopover();
241
249
  }
242
250
 
251
+ $effect(() => {
252
+ const btn = triggerEl;
253
+ if (!btn) return;
254
+ const onClick = (event: MouseEvent) => {
255
+ event.stopPropagation();
256
+ if (disabled) return;
257
+ const el = menuEl;
258
+ if (!el) return;
259
+ const isOpen = el.matches(':popover-open');
260
+ setTimeout(() => {
261
+ if (isOpen) closeMenu();
262
+ else openMenu();
263
+ }, 0);
264
+ };
265
+ btn.addEventListener('click', onClick);
266
+ return () => btn.removeEventListener('click', onClick);
267
+ });
268
+
243
269
  function handleBeforeToggle(event: ToggleEvent) {
244
270
  if (event.newState === 'open') {
245
271
  if (disabled) {
@@ -404,18 +430,16 @@
404
430
  <div class={['relative inline-flex', className]}>
405
431
  <button
406
432
  bind:this={triggerEl}
407
- id={id}
433
+ {id}
408
434
  type="button"
409
435
  {disabled}
410
- popovertarget={menuId}
411
- popovertargetaction="toggle"
412
436
  onkeydown={onTriggerKeydown}
413
437
  class={[
414
- 'inline-flex items-center gap-2 rounded-lg border border-border bg-surface-elevated font-medium text-primary transition-colors',
415
- 'hover:bg-surface-overlay focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
438
+ 'gap-2 rounded-lg border-border bg-surface-elevated font-medium text-primary inline-flex items-center border transition-colors',
439
+ 'hover:bg-surface-overlay focus-visible:ring-brand-500/30 focus-visible:ring-2 focus-visible:outline-none',
416
440
  size === 'sm' ? 'min-h-8 px-2.5 text-xs' : 'min-h-9 px-3 text-sm',
417
441
  disabled && 'cursor-not-allowed opacity-50',
418
- trigger && 'border-0 bg-transparent p-0 hover:bg-transparent'
442
+ trigger && 'p-0 border-0 bg-transparent hover:bg-transparent'
419
443
  ]}
420
444
  aria-label={trigger ? label : undefined}
421
445
  aria-haspopup="menu"
@@ -452,20 +476,20 @@
452
476
  ontoggle={handleToggle}
453
477
  onbeforetoggle={handleBeforeToggle}
454
478
  onkeydown={onMenuKeydown}
455
- class="dropdown-menu m-0 inset-auto flex flex-col overflow-hidden rounded-xl border border-border bg-surface-elevated p-1.5 shadow-xl outline-none"
479
+ class="dropdown-menu m-0 rounded-xl border-border bg-surface-elevated p-1.5 shadow-xl inset-auto flex-col overflow-hidden border text-left outline-none"
456
480
  >
457
481
  {#if canGoBack}
458
482
  <button
459
483
  type="button"
460
484
  onclick={goBack}
461
485
  class={[
462
- 'mb-0.5 flex w-full items-center gap-2 rounded-lg text-left font-medium text-primary transition-colors',
463
- 'hover:bg-surface-overlay focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
486
+ 'mb-0.5 gap-2 rounded-lg font-medium text-primary flex w-full items-center text-left transition-colors',
487
+ 'hover:bg-surface-overlay focus-visible:ring-brand-500/30 focus-visible:ring-2 focus-visible:outline-none',
464
488
  sizePad
465
489
  ]}
466
490
  >
467
491
  <svg
468
- class="shrink-0 text-secondary"
492
+ class="text-secondary shrink-0"
469
493
  width={iconSize}
470
494
  height={iconSize}
471
495
  viewBox="0 0 24 24"
@@ -478,13 +502,13 @@
478
502
  </svg>
479
503
  <span class="truncate">{current.title}</span>
480
504
  </button>
481
- <div class="mb-1 h-px bg-border" role="separator"></div>
505
+ <div class="mb-1 bg-border h-px" role="separator"></div>
482
506
  {/if}
483
507
 
484
508
  <div bind:this={listEl} class="dropdown-menu-list min-h-0 flex-1">
485
509
  {#each current.items as item, index (item.id)}
486
510
  {#if item.separator}
487
- <div class="my-1 h-px bg-border" role="separator"></div>
511
+ <div class="my-1 bg-border h-px" role="separator"></div>
488
512
  {:else}
489
513
  {@const Icon = item.icon}
490
514
  {@const hasChildren = Boolean(item.children?.length)}
@@ -501,7 +525,7 @@
501
525
  if (!item.disabled) highlighted = index;
502
526
  }}
503
527
  class={[
504
- 'flex w-full cursor-pointer gap-2 rounded-lg transition-colors',
528
+ 'gap-2 rounded-lg flex w-full cursor-pointer text-left transition-colors',
505
529
  item.description ? 'items-start' : 'items-center',
506
530
  sizePad,
507
531
  item.disabled && 'cursor-not-allowed opacity-40',
@@ -512,7 +536,7 @@
512
536
  {#if item.checked !== undefined}
513
537
  <span
514
538
  class={[
515
- 'flex h-4 w-4 shrink-0 items-center justify-center',
539
+ 'h-4 w-4 flex shrink-0 items-center justify-center',
516
540
  item.description && 'mt-0.5'
517
541
  ]}
518
542
  aria-hidden="true"
@@ -533,7 +557,7 @@
533
557
  {:else if showIconColumn}
534
558
  <span
535
559
  class={[
536
- 'flex h-4 w-4 shrink-0 items-center justify-center',
560
+ 'h-4 w-4 flex shrink-0 items-center justify-center',
537
561
  item.description && 'mt-0.5'
538
562
  ]}
539
563
  aria-hidden="true"
@@ -544,24 +568,26 @@
544
568
  </span>
545
569
  {/if}
546
570
 
547
- <span class="min-w-0 flex-1 leading-tight">
548
- <span class="block truncate font-medium leading-tight">{item.label}</span>
571
+ <span class="min-w-0 leading-tight flex-1">
572
+ <span class="font-medium leading-tight block truncate">{item.label}</span>
549
573
  {#if item.description}
550
- <span class="block truncate text-[11px] font-normal leading-snug text-secondary">
574
+ <span class="font-normal leading-snug text-secondary block truncate text-[11px]">
551
575
  {item.description}
552
576
  </span>
553
577
  {/if}
554
578
  </span>
555
579
 
556
580
  {#if item.shortcut && !hasChildren}
557
- <kbd class="ml-2 shrink-0 rounded border border-border bg-surface-overlay px-1.5 py-0.5 text-[10px] font-mono text-secondary">
581
+ <kbd
582
+ class="ml-2 rounded border-border bg-surface-overlay px-1.5 py-0.5 font-mono text-secondary shrink-0 border text-[10px]"
583
+ >
558
584
  {item.shortcut}
559
585
  </kbd>
560
586
  {/if}
561
587
 
562
588
  {#if hasChildren}
563
589
  <svg
564
- class={['ml-1 shrink-0 text-secondary', item.description && 'mt-0.5']}
590
+ class={['ml-1 text-secondary shrink-0', item.description && 'mt-0.5']}
565
591
  width={iconSize}
566
592
  height={iconSize}
567
593
  viewBox="0 0 24 24"
@@ -594,8 +620,4 @@
594
620
  .dropdown-menu:popover-open {
595
621
  display: flex;
596
622
  }
597
-
598
- .dropdown-menu:not(:popover-open) {
599
- display: none;
600
- }
601
623
  </style>
@@ -232,7 +232,7 @@
232
232
 
233
233
  <div class="min-w-0">
234
234
  {#if children}
235
- <div class={['grid', gapClass, columns === 2 && 'sm:grid-cols-2']}>
235
+ <div class={['grid grid-cols-1', gapClass, columns === 2 && 'sm:grid-cols-2']}>
236
236
  {@render children()}
237
237
  </div>
238
238
  {/if}
@@ -58,8 +58,8 @@
58
58
  sizeClasses[size],
59
59
  fullWidth && 'flex-1',
60
60
  active === item.id
61
- ? 'bg-surface-elevated text-primary shadow-sm'
62
- : 'text-secondary hover:text-primary',
61
+ ? 'bg-brand-500 text-white shadow-sm'
62
+ : 'text-secondary hover:bg-surface-elevated/70 hover:text-primary',
63
63
  item.disabled && 'cursor-not-allowed opacity-40'
64
64
  ]}
65
65
  >
@@ -118,6 +118,13 @@
118
118
  if (sortDir) onsort?.(column.id, sortDir);
119
119
  }
120
120
 
121
+ function isInteractiveTarget(target: EventTarget | null): boolean {
122
+ return (
123
+ target instanceof Element &&
124
+ Boolean(target.closest('a, button, input, select, textarea, label, [role="menuitem"]'))
125
+ );
126
+ }
127
+
121
128
  const alignClass = {
122
129
  left: 'text-left',
123
130
  center: 'text-center',
@@ -125,12 +132,14 @@
125
132
  } as const;
126
133
  </script>
127
134
 
128
- <div class={['w-full overflow-x-auto rounded-xl border border-border bg-surface-elevated', className]}>
129
- <table class="w-full border-collapse text-sm">
135
+ <div
136
+ class={['rounded-xl border-border bg-surface-elevated w-full overflow-x-auto border', className]}
137
+ >
138
+ <table class="text-sm w-full border-collapse">
130
139
  <thead
131
140
  class={[
132
- 'border-b border-border bg-surface-overlay/80 text-xs font-semibold tracking-wide text-secondary uppercase',
133
- stickyHeader && 'sticky top-0 z-10'
141
+ 'border-border bg-surface-overlay/80 text-xs font-semibold tracking-wide text-secondary border-b uppercase',
142
+ stickyHeader && 'top-0 sticky z-10'
134
143
  ]}
135
144
  >
136
145
  <tr>
@@ -141,22 +150,20 @@
141
150
  'px-3 font-semibold',
142
151
  compact ? 'py-2' : 'py-2.5',
143
152
  alignClass[column.align ?? 'left'],
144
- canSort(column) && 'cursor-pointer select-none hover:text-primary',
153
+ canSort(column) && 'hover:text-primary cursor-pointer select-none',
145
154
  column.class
146
155
  ]}
147
156
  style={column.width ? `width:${column.width}` : undefined}
148
- aria-sort={
149
- sortId === column.id
150
- ? sortDir === 'asc'
151
- ? 'ascending'
152
- : 'descending'
153
- : canSort(column)
154
- ? 'none'
155
- : undefined
156
- }
157
+ aria-sort={sortId === column.id
158
+ ? sortDir === 'asc'
159
+ ? 'ascending'
160
+ : 'descending'
161
+ : canSort(column)
162
+ ? 'none'
163
+ : undefined}
157
164
  onclick={() => canSort(column) && toggleSort(column)}
158
165
  >
159
- <span class="inline-flex items-center gap-1">
166
+ <span class="gap-1 inline-flex items-center">
160
167
  {column.header}
161
168
  {#if canSort(column)}
162
169
  <span class="text-muted" aria-hidden="true">
@@ -174,7 +181,7 @@
174
181
  {#if sortedRows.length === 0}
175
182
  <tr>
176
183
  <td
177
- class="px-3 py-8 text-center text-sm text-muted"
184
+ class="px-3 py-8 text-sm text-muted text-center"
178
185
  colspan={Math.max(columns.length, 1)}
179
186
  >
180
187
  {empty}
@@ -184,12 +191,15 @@
184
191
  {#each sortedRows as row, index (getKey(row, index))}
185
192
  <tr
186
193
  class={[
187
- 'border-b border-border last:border-b-0',
194
+ 'border-border border-b last:border-b-0',
188
195
  striped && index % 2 === 1 && 'bg-surface-overlay/40',
189
196
  hoverable && 'hover:bg-surface-overlay/70',
190
197
  onrowclick && 'cursor-pointer'
191
198
  ]}
192
- onclick={() => onrowclick?.(row)}
199
+ onclick={(e) => {
200
+ if (isInteractiveTarget(e.target)) return;
201
+ onrowclick?.(row);
202
+ }}
193
203
  >
194
204
  {#each columns as column (column.id)}
195
205
  <td
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.8.14",
3
+ "version": "0.9.1",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",