@r2digisolutions/components 0.14.4 → 0.14.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.
- package/dist/components/molecules/Popover/Popover.svelte +2 -2
- package/dist/components/molecules/Popover/PopoverStory.svelte +2 -4
- package/dist/components/organisms/DesktopShell/DesktopShell.svelte +74 -59
- package/dist/components/organisms/NavRail/NavRail.svelte +45 -38
- package/dist/components/organisms/NotificationCenter/NotificationCenter.svelte +41 -8
- package/dist/components/organisms/NotificationCenter/NotificationCenter.svelte.d.ts +14 -0
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
ontoggle={onToggle}
|
|
231
231
|
style={panelStyle}
|
|
232
232
|
class={[
|
|
233
|
-
'popover-panel m-0
|
|
233
|
+
'popover-panel m-0 rounded-xl border-border bg-surface-elevated p-3 shadow-xl max-w-[min(20rem,calc(100vw-1rem))] border outline-none',
|
|
234
234
|
panelClass
|
|
235
235
|
]}
|
|
236
236
|
>
|
|
@@ -241,7 +241,7 @@
|
|
|
241
241
|
bind:this={panelEl}
|
|
242
242
|
role="dialog"
|
|
243
243
|
class={[
|
|
244
|
-
'
|
|
244
|
+
'min-w-48 rounded-xl border-border bg-surface-elevated p-3 shadow-xl absolute z-50 border',
|
|
245
245
|
absolutePos[placement],
|
|
246
246
|
absoluteAlign[align][placement],
|
|
247
247
|
panelClass
|
|
@@ -9,12 +9,10 @@
|
|
|
9
9
|
let open = $state(false);
|
|
10
10
|
</script>
|
|
11
11
|
|
|
12
|
-
<div class="
|
|
12
|
+
<div class="min-h-40 flex items-center justify-center">
|
|
13
13
|
<Popover bind:open placement={props.placement ?? 'bottom'}>
|
|
14
14
|
{#snippet trigger()}
|
|
15
|
-
<Button size="sm" variant="secondary" data-popover-trigger>
|
|
16
|
-
Open popover
|
|
17
|
-
</Button>
|
|
15
|
+
<Button size="sm" variant="secondary" data-popover-trigger>Open popover</Button>
|
|
18
16
|
{/snippet}
|
|
19
17
|
<div class="space-y-1">
|
|
20
18
|
<p class="text-sm font-medium text-primary">Quick actions</p>
|
|
@@ -71,13 +71,7 @@
|
|
|
71
71
|
y?: number;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export type DesktopWallpaper =
|
|
75
|
-
| 'aurora'
|
|
76
|
-
| 'slate'
|
|
77
|
-
| 'mint'
|
|
78
|
-
| 'dusk'
|
|
79
|
-
| 'paper'
|
|
80
|
-
| 'custom';
|
|
74
|
+
export type DesktopWallpaper = 'aurora' | 'slate' | 'mint' | 'dusk' | 'paper' | 'custom';
|
|
81
75
|
|
|
82
76
|
export type DesktopShellHeight = 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
83
77
|
|
|
@@ -138,10 +132,8 @@
|
|
|
138
132
|
'bg-[radial-gradient(ellipse_at_20%_0%,rgb(56_189_248_/_0.28),transparent_50%),radial-gradient(ellipse_at_80%_10%,rgb(167_139_250_/_0.22),transparent_45%),linear-gradient(160deg,var(--surface),var(--surface-overlay))]',
|
|
139
133
|
slate:
|
|
140
134
|
'bg-[radial-gradient(circle_at_30%_20%,rgb(100_116_139_/_0.3),transparent_50%),linear-gradient(165deg,#0f172a,#1e293b_55%,#334155)]',
|
|
141
|
-
mint:
|
|
142
|
-
|
|
143
|
-
dusk:
|
|
144
|
-
'bg-[radial-gradient(ellipse_at_70%_0%,rgb(251_146_60_/_0.28),transparent_45%),radial-gradient(ellipse_at_10%_80%,rgb(99_102_241_/_0.2),transparent_40%),linear-gradient(165deg,#1e1b4b,#312e81_40%,#7c2d12)]',
|
|
135
|
+
mint: 'bg-[radial-gradient(ellipse_at_top,rgb(52_211_153_/_0.22),transparent_50%),linear-gradient(160deg,var(--surface),rgb(236_253_245))]',
|
|
136
|
+
dusk: 'bg-[radial-gradient(ellipse_at_70%_0%,rgb(251_146_60_/_0.28),transparent_45%),radial-gradient(ellipse_at_10%_80%,rgb(99_102_241_/_0.2),transparent_40%),linear-gradient(165deg,#1e1b4b,#312e81_40%,#7c2d12)]',
|
|
145
137
|
paper:
|
|
146
138
|
'bg-[linear-gradient(180deg,var(--surface-elevated),var(--surface)),repeating-linear-gradient(0deg,transparent,transparent_23px,rgb(0_0_0_/_0.03)_24px)]'
|
|
147
139
|
};
|
|
@@ -192,7 +184,14 @@
|
|
|
192
184
|
|
|
193
185
|
const DEFAULT_WIDGETS: DesktopWidget[] = [
|
|
194
186
|
{ id: 'w-weather', kind: 'weather', title: 'Weather', x: 16, y: 16 },
|
|
195
|
-
{
|
|
187
|
+
{
|
|
188
|
+
id: 'w-sticky',
|
|
189
|
+
kind: 'sticky',
|
|
190
|
+
title: 'Notes',
|
|
191
|
+
body: 'Ship desktop polish\n· Icons ✓\n· Menubar ✓',
|
|
192
|
+
x: 16,
|
|
193
|
+
y: 140
|
|
194
|
+
},
|
|
196
195
|
{ id: 'w-stats', kind: 'stats', title: 'Pulse', x: 16, y: 280 }
|
|
197
196
|
];
|
|
198
197
|
|
|
@@ -245,9 +244,7 @@
|
|
|
245
244
|
let contextTarget = $state<ContextTarget>({ type: 'desktop' });
|
|
246
245
|
|
|
247
246
|
const openWindows = $derived(
|
|
248
|
-
windows
|
|
249
|
-
.filter((w) => !w.closed)
|
|
250
|
-
.sort((a, b) => (a.rect.z ?? 0) - (b.rect.z ?? 0))
|
|
247
|
+
windows.filter((w) => !w.closed).sort((a, b) => (a.rect.z ?? 0) - (b.rect.z ?? 0))
|
|
251
248
|
);
|
|
252
249
|
const taskbarWindows = $derived(windows.filter((w) => !w.closed));
|
|
253
250
|
const visibleTaskbar = $derived(taskbarWindows.slice(0, TASKBAR_VISIBLE));
|
|
@@ -413,9 +410,7 @@
|
|
|
413
410
|
const z = nextZ();
|
|
414
411
|
emit(
|
|
415
412
|
windows.map((w) =>
|
|
416
|
-
w.id === id
|
|
417
|
-
? { ...w, closed: false, rect: { ...w.rect, z, minimized: false } }
|
|
418
|
-
: w
|
|
413
|
+
w.id === id ? { ...w, closed: false, rect: { ...w.rect, z, minimized: false } } : w
|
|
419
414
|
)
|
|
420
415
|
);
|
|
421
416
|
activeId = id;
|
|
@@ -424,9 +419,7 @@
|
|
|
424
419
|
|
|
425
420
|
function updateRect(id: string, rect: WindowRect) {
|
|
426
421
|
emit(
|
|
427
|
-
windows.map((w) =>
|
|
428
|
-
w.id === id ? { ...w, rect: { ...rect, z: rect.z ?? w.rect.z } } : w
|
|
429
|
-
)
|
|
422
|
+
windows.map((w) => (w.id === id ? { ...w, rect: { ...rect, z: rect.z ?? w.rect.z } } : w))
|
|
430
423
|
);
|
|
431
424
|
}
|
|
432
425
|
|
|
@@ -681,7 +674,20 @@
|
|
|
681
674
|
|
|
682
675
|
function resolveIcon(id?: DesktopIconId, kind?: string): DesktopIconId {
|
|
683
676
|
if (id) return id;
|
|
684
|
-
if (
|
|
677
|
+
if (
|
|
678
|
+
kind &&
|
|
679
|
+
[
|
|
680
|
+
'files',
|
|
681
|
+
'mail',
|
|
682
|
+
'music',
|
|
683
|
+
'notes',
|
|
684
|
+
'settings',
|
|
685
|
+
'stats',
|
|
686
|
+
'calendar',
|
|
687
|
+
'terminal',
|
|
688
|
+
'team'
|
|
689
|
+
].includes(kind)
|
|
690
|
+
) {
|
|
685
691
|
return kind as DesktopIconId;
|
|
686
692
|
}
|
|
687
693
|
return 'app';
|
|
@@ -698,7 +704,7 @@
|
|
|
698
704
|
|
|
699
705
|
<div
|
|
700
706
|
class={[
|
|
701
|
-
'relative flex w-full flex-col overflow-hidden
|
|
707
|
+
'rounded-2xl border-border bg-surface shadow-sm relative flex w-full flex-col overflow-hidden border',
|
|
702
708
|
HEIGHT[height],
|
|
703
709
|
className
|
|
704
710
|
]}
|
|
@@ -714,11 +720,11 @@
|
|
|
714
720
|
|
|
715
721
|
{#if showMenubar}
|
|
716
722
|
<div
|
|
717
|
-
class="
|
|
723
|
+
class="gap-2 border-border bg-surface-elevated/95 px-2.5 py-1.5 backdrop-blur relative z-40 flex shrink-0 items-center border-b"
|
|
718
724
|
>
|
|
719
|
-
<BrandMark mark={brandMark} name={brand} size="sm" showName class="
|
|
725
|
+
<BrandMark mark={brandMark} name={brand} size="sm" showName class="sm:inline-flex hidden" />
|
|
720
726
|
<BrandMark mark={brandMark} name={brand} size="sm" class="sm:hidden" />
|
|
721
|
-
<div class="h-4 w-px shrink-0
|
|
727
|
+
<div class="h-4 bg-border w-px shrink-0" aria-hidden="true"></div>
|
|
722
728
|
<Menubar items={menubarItems} class="min-w-0 flex-1" onselect={onMenubarSelect} />
|
|
723
729
|
{#if showPresence}
|
|
724
730
|
<SessionPresenceBadge size="sm" showLabel={false} />
|
|
@@ -731,7 +737,7 @@
|
|
|
731
737
|
<div
|
|
732
738
|
bind:this={desktopEl}
|
|
733
739
|
class={[
|
|
734
|
-
'
|
|
740
|
+
'min-h-0 relative flex-1 overflow-hidden',
|
|
735
741
|
wallpaper !== 'custom' && WALLPAPER_CLASS[wallpaper]
|
|
736
742
|
]}
|
|
737
743
|
style={wallpaperBg}
|
|
@@ -741,7 +747,7 @@
|
|
|
741
747
|
}}
|
|
742
748
|
>
|
|
743
749
|
{#if showDesktopIcons && launcherApps.length}
|
|
744
|
-
<div class="
|
|
750
|
+
<div class="left-3 top-3 gap-0.5 absolute z-[1] flex w-[4.75rem] flex-col">
|
|
745
751
|
{#each launcherApps.slice(0, 8) as app (app.id)}
|
|
746
752
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
747
753
|
<div
|
|
@@ -752,7 +758,7 @@
|
|
|
752
758
|
>
|
|
753
759
|
<button
|
|
754
760
|
type="button"
|
|
755
|
-
class="group
|
|
761
|
+
class="group gap-1 rounded-xl px-1 py-1.5 hover:bg-black/10 dark:hover:bg-white/10 flex w-full flex-col items-center text-center transition-colors"
|
|
756
762
|
onclick={(e) => {
|
|
757
763
|
e.stopPropagation();
|
|
758
764
|
openApp(app);
|
|
@@ -764,7 +770,7 @@
|
|
|
764
770
|
size="lg"
|
|
765
771
|
/>
|
|
766
772
|
<span
|
|
767
|
-
class="line-clamp-2 w-full text-center text-[10px]
|
|
773
|
+
class="font-medium leading-tight text-primary line-clamp-2 w-full text-center text-[10px]"
|
|
768
774
|
>
|
|
769
775
|
{app.title}
|
|
770
776
|
</span>
|
|
@@ -775,11 +781,11 @@
|
|
|
775
781
|
{/if}
|
|
776
782
|
|
|
777
783
|
{#if showWidgets}
|
|
778
|
-
<div class="pointer-events-none absolute
|
|
784
|
+
<div class="right-3 top-3 w-44 gap-2 pointer-events-none absolute z-[1] flex flex-col">
|
|
779
785
|
{#each widgets as widget (widget.id)}
|
|
780
786
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
781
787
|
<div
|
|
782
|
-
class="
|
|
788
|
+
class="rounded-xl border-border/70 bg-surface-elevated/90 p-3 shadow-sm backdrop-blur pointer-events-auto border"
|
|
783
789
|
onpointerdown={(e) => e.stopPropagation()}
|
|
784
790
|
oncontextmenu={(e) => {
|
|
785
791
|
e.stopPropagation();
|
|
@@ -787,33 +793,42 @@
|
|
|
787
793
|
}}
|
|
788
794
|
>
|
|
789
795
|
{#if widget.kind === 'weather'}
|
|
790
|
-
<div class="flex items-start
|
|
796
|
+
<div class="gap-2 flex items-start">
|
|
791
797
|
<span
|
|
792
|
-
class="
|
|
798
|
+
class="h-9 w-9 rounded-lg bg-sky-500/15 text-sky-700 dark:text-sky-300 flex shrink-0 items-center justify-center"
|
|
793
799
|
>
|
|
794
800
|
<CloudSun class="h-[18px] w-[18px]" strokeWidth={2} />
|
|
795
801
|
</span>
|
|
796
802
|
<div class="min-w-0">
|
|
797
803
|
<p class="text-xs font-medium text-muted">{widget.title ?? 'Weather'}</p>
|
|
798
|
-
<p class="text-lg font-semibold tabular-nums
|
|
799
|
-
<p class="text-[11px]
|
|
804
|
+
<p class="text-lg font-semibold text-primary tabular-nums">22°</p>
|
|
805
|
+
<p class="text-secondary text-[11px]">Partly cloudy</p>
|
|
800
806
|
</div>
|
|
801
807
|
</div>
|
|
802
808
|
{:else if widget.kind === 'sticky'}
|
|
803
|
-
<p
|
|
809
|
+
<p
|
|
810
|
+
class="font-semibold tracking-wide text-amber-700 dark:text-amber-300 text-[11px] uppercase"
|
|
811
|
+
>
|
|
804
812
|
{widget.title ?? 'Sticky'}
|
|
805
813
|
</p>
|
|
806
|
-
<p class="mt-1
|
|
814
|
+
<p class="mt-1 text-xs leading-relaxed text-primary whitespace-pre-line">
|
|
807
815
|
{widget.body ?? 'Add a note…'}
|
|
808
816
|
</p>
|
|
809
817
|
{:else if widget.kind === 'stats'}
|
|
810
|
-
<p class="mb-2
|
|
818
|
+
<p class="mb-2 font-medium text-muted text-[11px]">{widget.title ?? 'Stats'}</p>
|
|
811
819
|
<div class="space-y-1">
|
|
812
|
-
<StatCard
|
|
820
|
+
<StatCard
|
|
821
|
+
label="Active"
|
|
822
|
+
value="128"
|
|
823
|
+
delta="+4%"
|
|
824
|
+
trend="up"
|
|
825
|
+
variant="ghost"
|
|
826
|
+
class="!p-2"
|
|
827
|
+
/>
|
|
813
828
|
</div>
|
|
814
829
|
{:else}
|
|
815
|
-
<p class="text-sm font-semibold tabular-nums
|
|
816
|
-
<p class="text-[11px]
|
|
830
|
+
<p class="text-sm font-semibold text-primary tabular-nums">{clock}</p>
|
|
831
|
+
<p class="text-muted text-[11px]">{clockDate}</p>
|
|
817
832
|
{/if}
|
|
818
833
|
</div>
|
|
819
834
|
{/each}
|
|
@@ -856,10 +871,10 @@
|
|
|
856
871
|
{#if startOpen}
|
|
857
872
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
858
873
|
<div
|
|
859
|
-
class="
|
|
874
|
+
class="bottom-3 left-3 rounded-2xl border-border bg-surface-elevated/95 shadow-xl backdrop-blur absolute z-[1000] flex w-[19rem] flex-col overflow-hidden border"
|
|
860
875
|
onpointerdown={(e) => e.stopPropagation()}
|
|
861
876
|
>
|
|
862
|
-
<div class="
|
|
877
|
+
<div class="gap-2 border-border px-3 py-2.5 flex items-center border-b">
|
|
863
878
|
<BrandMark mark={brandMark} name={brand} size="sm" showName />
|
|
864
879
|
</div>
|
|
865
880
|
|
|
@@ -868,7 +883,7 @@
|
|
|
868
883
|
|
|
869
884
|
<div class="max-h-56 space-y-0.5 overflow-y-auto">
|
|
870
885
|
{#if !filteredApps.length}
|
|
871
|
-
<p class="px-2 py-4 text-
|
|
886
|
+
<p class="px-2 py-4 text-xs text-muted text-center">No apps found</p>
|
|
872
887
|
{:else}
|
|
873
888
|
{#each filteredApps as app (app.id)}
|
|
874
889
|
<Tile
|
|
@@ -894,7 +909,7 @@
|
|
|
894
909
|
</div>
|
|
895
910
|
</div>
|
|
896
911
|
|
|
897
|
-
<div class="
|
|
912
|
+
<div class="gap-1 border-border p-1.5 flex items-center border-t">
|
|
898
913
|
<Tooltip content="Cascade">
|
|
899
914
|
<IconButton label="Cascade windows" size="sm" onclick={cascadeWindows}>
|
|
900
915
|
<Layers class="h-4 w-4" strokeWidth={2} />
|
|
@@ -913,7 +928,7 @@
|
|
|
913
928
|
<div class="min-w-0 flex-1"></div>
|
|
914
929
|
<button
|
|
915
930
|
type="button"
|
|
916
|
-
class="
|
|
931
|
+
class="gap-1.5 rounded-lg px-2 py-1 text-muted hover:bg-surface-overlay hover:text-secondary inline-flex items-center text-[11px]"
|
|
917
932
|
onclick={() => {
|
|
918
933
|
startOpen = false;
|
|
919
934
|
commandOpen = true;
|
|
@@ -929,7 +944,7 @@
|
|
|
929
944
|
|
|
930
945
|
{#if showTaskbar}
|
|
931
946
|
<div
|
|
932
|
-
class="
|
|
947
|
+
class="gap-2 border-border bg-surface-elevated/95 px-2 py-2 backdrop-blur sm:px-3 relative z-40 flex shrink-0 items-center border-t"
|
|
933
948
|
>
|
|
934
949
|
{#if showStart}
|
|
935
950
|
<Tooltip content="Start" side="top">
|
|
@@ -972,9 +987,9 @@
|
|
|
972
987
|
{@render taskbarLeading()}
|
|
973
988
|
{/if}
|
|
974
989
|
|
|
975
|
-
<div class="
|
|
990
|
+
<div class="min-w-0 gap-1 px-1 flex flex-1 items-center justify-center overflow-x-auto">
|
|
976
991
|
<div
|
|
977
|
-
class="
|
|
992
|
+
class="gap-0.5 rounded-2xl border-border/70 bg-surface/70 p-1 shadow-sm inline-flex items-center border"
|
|
978
993
|
>
|
|
979
994
|
{#each visibleTaskbar as win (win.id)}
|
|
980
995
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
@@ -988,7 +1003,7 @@
|
|
|
988
1003
|
<button
|
|
989
1004
|
type="button"
|
|
990
1005
|
class={[
|
|
991
|
-
'
|
|
1006
|
+
'h-9 w-9 rounded-xl relative flex items-center justify-center transition-colors',
|
|
992
1007
|
activeId === win.id && !win.rect.minimized
|
|
993
1008
|
? 'bg-brand-500/15 text-brand-700 dark:text-brand-300'
|
|
994
1009
|
: 'text-secondary hover:bg-surface-overlay hover:text-primary',
|
|
@@ -1006,7 +1021,7 @@
|
|
|
1006
1021
|
/>
|
|
1007
1022
|
{#if activeId === win.id && !win.rect.minimized}
|
|
1008
1023
|
<span
|
|
1009
|
-
class="
|
|
1024
|
+
class="inset-x-2 bottom-0.5 h-0.5 bg-brand-500 absolute rounded-full"
|
|
1010
1025
|
aria-hidden="true"
|
|
1011
1026
|
></span>
|
|
1012
1027
|
{/if}
|
|
@@ -1026,7 +1041,7 @@
|
|
|
1026
1041
|
>
|
|
1027
1042
|
{#snippet trigger()}
|
|
1028
1043
|
<span
|
|
1029
|
-
class="
|
|
1044
|
+
class="h-9 w-9 rounded-xl text-secondary hover:bg-surface-overlay flex items-center justify-center"
|
|
1030
1045
|
aria-label={`More windows (${overflowTaskbar.length})`}
|
|
1031
1046
|
>
|
|
1032
1047
|
<MoreHorizontal class="h-4 w-4" strokeWidth={2} />
|
|
@@ -1037,7 +1052,7 @@
|
|
|
1037
1052
|
</div>
|
|
1038
1053
|
</div>
|
|
1039
1054
|
|
|
1040
|
-
<div class="flex shrink-0 items-center
|
|
1055
|
+
<div class="gap-1.5 flex shrink-0 items-center">
|
|
1041
1056
|
{#if showWindowCount}
|
|
1042
1057
|
<Badge size="sm" variant="secondary" class="tabular-nums">
|
|
1043
1058
|
{taskbarWindows.length - minimizedCount}/{taskbarWindows.length}
|
|
@@ -1074,20 +1089,20 @@
|
|
|
1074
1089
|
<button
|
|
1075
1090
|
type="button"
|
|
1076
1091
|
data-popover-trigger
|
|
1077
|
-
class="flex min-w-[4.5rem] flex-col items-end
|
|
1092
|
+
class="rounded-xl px-2.5 py-1 hover:bg-surface-overlay flex min-w-[4.5rem] flex-col items-end text-right transition-colors"
|
|
1078
1093
|
aria-expanded={clockOpen}
|
|
1079
1094
|
aria-haspopup="dialog"
|
|
1080
1095
|
onclick={() => {
|
|
1081
1096
|
startOpen = false;
|
|
1082
1097
|
}}
|
|
1083
1098
|
>
|
|
1084
|
-
<span class="text-xs font-semibold tabular-nums
|
|
1085
|
-
<span class="text-[10px]
|
|
1099
|
+
<span class="text-xs font-semibold text-primary tabular-nums">{clock}</span>
|
|
1100
|
+
<span class="text-muted text-[10px]">{clockDate}</span>
|
|
1086
1101
|
</button>
|
|
1087
1102
|
{/snippet}
|
|
1088
1103
|
<div class="space-y-3">
|
|
1089
|
-
<div class="border-
|
|
1090
|
-
<p class="text-2xl font-semibold
|
|
1104
|
+
<div class="border-border pb-2 border-b">
|
|
1105
|
+
<p class="text-2xl font-semibold tracking-tight text-primary tabular-nums">
|
|
1091
1106
|
{clock}
|
|
1092
1107
|
</p>
|
|
1093
1108
|
<p class="text-sm text-secondary">{clockDate}</p>
|
|
@@ -43,50 +43,57 @@
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const itemClass = (item: NavRailItem) => [
|
|
46
|
-
'relative flex h-10 w-10 items-center justify-center rounded-lg transition-colors',
|
|
46
|
+
'group relative flex h-10 w-10 items-center justify-center rounded-lg transition-colors',
|
|
47
47
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
|
|
48
48
|
value === item.id
|
|
49
|
-
? 'bg-
|
|
49
|
+
? 'bg-brand-500/15 text-brand-500'
|
|
50
50
|
: 'text-secondary hover:bg-surface-overlay hover:text-primary',
|
|
51
51
|
item.disabled && 'pointer-events-none cursor-not-allowed opacity-40'
|
|
52
52
|
];
|
|
53
|
+
|
|
54
|
+
const glyphClass = (item: NavRailItem) => [
|
|
55
|
+
'h-5 w-5',
|
|
56
|
+
value === item.id ? 'text-brand-500' : 'text-secondary group-hover:text-primary'
|
|
57
|
+
];
|
|
53
58
|
</script>
|
|
54
59
|
|
|
55
60
|
{#snippet railButton(item: NavRailItem)}
|
|
56
|
-
<Tooltip content={item.label} side="right">
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
61
|
+
<Tooltip content={item.label} side="right" class="relative w-full justify-center">
|
|
62
|
+
<RailMark active={value === item.id} length="full" class="z-10" />
|
|
63
|
+
{#if item.href && !item.disabled}
|
|
64
|
+
<a
|
|
65
|
+
href={item.href}
|
|
66
|
+
aria-label={item.label}
|
|
67
|
+
aria-current={value === item.id ? 'page' : undefined}
|
|
68
|
+
class={itemClass(item)}
|
|
69
|
+
onclick={() => select(item.id, item.disabled)}
|
|
70
|
+
>
|
|
71
|
+
{#if item.icon}
|
|
72
|
+
<item.icon class={glyphClass(item)} strokeWidth={1.75} />
|
|
73
|
+
{:else}
|
|
74
|
+
<span class={['text-xs font-semibold', value === item.id && 'text-brand-500']}>
|
|
75
|
+
{item.label.slice(0, 1)}
|
|
76
|
+
</span>
|
|
77
|
+
{/if}
|
|
78
|
+
</a>
|
|
79
|
+
{:else}
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
aria-label={item.label}
|
|
83
|
+
aria-current={value === item.id ? 'page' : undefined}
|
|
84
|
+
disabled={item.disabled}
|
|
85
|
+
class={itemClass(item)}
|
|
86
|
+
onclick={() => select(item.id, item.disabled)}
|
|
87
|
+
>
|
|
88
|
+
{#if item.icon}
|
|
89
|
+
<item.icon class={glyphClass(item)} strokeWidth={1.75} />
|
|
90
|
+
{:else}
|
|
91
|
+
<span class={['text-xs font-semibold', value === item.id && 'text-brand-500']}>
|
|
92
|
+
{item.label.slice(0, 1)}
|
|
93
|
+
</span>
|
|
94
|
+
{/if}
|
|
95
|
+
</button>
|
|
96
|
+
{/if}
|
|
90
97
|
</Tooltip>
|
|
91
98
|
{/snippet}
|
|
92
99
|
|
|
@@ -102,7 +109,7 @@
|
|
|
102
109
|
</div>
|
|
103
110
|
{/if}
|
|
104
111
|
|
|
105
|
-
<nav class="gap-1 flex flex-1 flex-col items-center" aria-label="Rail">
|
|
112
|
+
<nav class="gap-1 flex w-full flex-1 flex-col items-center" aria-label="Rail">
|
|
106
113
|
{#each items as item (item.id)}
|
|
107
114
|
{@render railButton(item)}
|
|
108
115
|
{/each}
|
|
@@ -110,7 +117,7 @@
|
|
|
110
117
|
|
|
111
118
|
{#if footerItems.length > 0}
|
|
112
119
|
<div class="w-8 py-2"><Divider /></div>
|
|
113
|
-
<nav class="gap-1 flex flex-col items-center" aria-label="Rail footer">
|
|
120
|
+
<nav class="gap-1 flex w-full flex-col items-center" aria-label="Rail footer">
|
|
114
121
|
{#each footerItems as item (item.id)}
|
|
115
122
|
{@render railButton(item)}
|
|
116
123
|
{/each}
|
|
@@ -14,11 +14,26 @@
|
|
|
14
14
|
type?: 'info' | 'success' | 'warning' | 'error';
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface NotificationCenterLabels {
|
|
18
|
+
title?: string;
|
|
19
|
+
/** Badge next to the title. `{n}` is replaced with unread count. */
|
|
20
|
+
newCount?: string;
|
|
21
|
+
markAllRead?: string;
|
|
22
|
+
clearAll?: string;
|
|
23
|
+
empty?: string;
|
|
24
|
+
/** Trigger aria-label when there are no unread items */
|
|
25
|
+
trigger?: string;
|
|
26
|
+
/** Appended to trigger aria-label when unread. `{n}` is unread count. */
|
|
27
|
+
unreadSuffix?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
interface NotificationCenterProps {
|
|
18
31
|
id?: string;
|
|
19
32
|
items?: NotificationItem[];
|
|
20
33
|
open?: boolean;
|
|
21
34
|
class?: string;
|
|
35
|
+
labels?: NotificationCenterLabels;
|
|
36
|
+
showClear?: boolean;
|
|
22
37
|
onitemclick?: (item: NotificationItem) => void;
|
|
23
38
|
onmarkallread?: () => void;
|
|
24
39
|
onclear?: () => void;
|
|
@@ -29,6 +44,8 @@
|
|
|
29
44
|
items = $bindable([]),
|
|
30
45
|
open = $bindable(false),
|
|
31
46
|
class: className = '',
|
|
47
|
+
showClear = true,
|
|
48
|
+
labels = {},
|
|
32
49
|
onitemclick,
|
|
33
50
|
onmarkallread,
|
|
34
51
|
onclear
|
|
@@ -42,6 +59,22 @@
|
|
|
42
59
|
const popoverId = $derived(`${baseId}-popover`);
|
|
43
60
|
const unreadCount = $derived(items.filter((n) => !n.read).length);
|
|
44
61
|
|
|
62
|
+
const DEFAULT_LABELS: Required<NotificationCenterLabels> = {
|
|
63
|
+
title: 'Notifications',
|
|
64
|
+
newCount: '{n} new',
|
|
65
|
+
markAllRead: 'Mark all read',
|
|
66
|
+
clearAll: 'Clear all',
|
|
67
|
+
empty: 'No notifications',
|
|
68
|
+
trigger: 'Notifications',
|
|
69
|
+
unreadSuffix: ', {n} unread'
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const copy = $derived({ ...DEFAULT_LABELS, ...labels });
|
|
73
|
+
|
|
74
|
+
function withCount(template: string, n: number) {
|
|
75
|
+
return template.replaceAll('{n}', String(n));
|
|
76
|
+
}
|
|
77
|
+
|
|
45
78
|
function positionPopover() {
|
|
46
79
|
if (!triggerEl || !popoverEl) return;
|
|
47
80
|
if (!popoverEl.matches(':popover-open')) return;
|
|
@@ -136,7 +169,7 @@
|
|
|
136
169
|
type="button"
|
|
137
170
|
popovertarget={popoverId}
|
|
138
171
|
popovertargetaction="toggle"
|
|
139
|
-
aria-label={
|
|
172
|
+
aria-label="{copy.trigger}{unreadCount > 0 ? withCount(copy.unreadSuffix, unreadCount) : ''}"
|
|
140
173
|
aria-expanded={open}
|
|
141
174
|
aria-controls={popoverId}
|
|
142
175
|
class="relative flex h-9 w-9 items-center justify-center rounded-lg text-secondary outline-none transition-colors hover:bg-surface-overlay hover:text-primary focus-visible:ring-2 focus-visible:ring-brand-500/30"
|
|
@@ -173,16 +206,16 @@
|
|
|
173
206
|
style={popoverStyle}
|
|
174
207
|
class="notif-panel inset-auto m-0 flex flex-col overflow-hidden rounded-xl border border-border bg-surface-elevated shadow-2xl outline-none"
|
|
175
208
|
role="dialog"
|
|
176
|
-
aria-label=
|
|
209
|
+
aria-label={copy.title}
|
|
177
210
|
>
|
|
178
211
|
<div class="flex shrink-0 items-center justify-between border-b border-border px-4 py-3">
|
|
179
212
|
<div class="flex items-center gap-2">
|
|
180
|
-
<h3 class="text-sm font-semibold text-primary">
|
|
213
|
+
<h3 class="text-sm font-semibold text-primary">{copy.title}</h3>
|
|
181
214
|
{#if unreadCount > 0}
|
|
182
215
|
<span
|
|
183
216
|
class="rounded-full bg-brand-100 px-2 py-0.5 text-xs font-medium text-brand-600 dark:bg-brand-900 dark:text-brand-400"
|
|
184
217
|
>
|
|
185
|
-
{unreadCount}
|
|
218
|
+
{withCount(copy.newCount, unreadCount)}
|
|
186
219
|
</span>
|
|
187
220
|
{/if}
|
|
188
221
|
</div>
|
|
@@ -193,16 +226,16 @@
|
|
|
193
226
|
onclick={markAllRead}
|
|
194
227
|
class="rounded-lg px-2 py-1 text-xs font-medium text-brand-600 transition-colors hover:bg-surface-overlay dark:text-brand-400"
|
|
195
228
|
>
|
|
196
|
-
|
|
229
|
+
{copy.markAllRead}
|
|
197
230
|
</button>
|
|
198
231
|
{/if}
|
|
199
|
-
{#if items.length > 0}
|
|
232
|
+
{#if showClear && items.length > 0}
|
|
200
233
|
<button
|
|
201
234
|
type="button"
|
|
202
235
|
onclick={clear}
|
|
203
236
|
class="rounded-lg px-2 py-1 text-xs font-medium text-muted transition-colors hover:bg-surface-overlay hover:text-primary"
|
|
204
237
|
>
|
|
205
|
-
|
|
238
|
+
{copy.clearAll}
|
|
206
239
|
</button>
|
|
207
240
|
{/if}
|
|
208
241
|
</div>
|
|
@@ -225,7 +258,7 @@
|
|
|
225
258
|
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
|
226
259
|
/>
|
|
227
260
|
</svg>
|
|
228
|
-
<p class="text-sm text-muted">
|
|
261
|
+
<p class="text-sm text-muted">{copy.empty}</p>
|
|
229
262
|
</div>
|
|
230
263
|
{:else}
|
|
231
264
|
<div class="divide-y divide-border">
|
|
@@ -9,11 +9,25 @@ export interface NotificationItem {
|
|
|
9
9
|
actor?: string;
|
|
10
10
|
type?: 'info' | 'success' | 'warning' | 'error';
|
|
11
11
|
}
|
|
12
|
+
export interface NotificationCenterLabels {
|
|
13
|
+
title?: string;
|
|
14
|
+
/** Badge next to the title. `{n}` is replaced with unread count. */
|
|
15
|
+
newCount?: string;
|
|
16
|
+
markAllRead?: string;
|
|
17
|
+
clearAll?: string;
|
|
18
|
+
empty?: string;
|
|
19
|
+
/** Trigger aria-label when there are no unread items */
|
|
20
|
+
trigger?: string;
|
|
21
|
+
/** Appended to trigger aria-label when unread. `{n}` is unread count. */
|
|
22
|
+
unreadSuffix?: string;
|
|
23
|
+
}
|
|
12
24
|
interface NotificationCenterProps {
|
|
13
25
|
id?: string;
|
|
14
26
|
items?: NotificationItem[];
|
|
15
27
|
open?: boolean;
|
|
16
28
|
class?: string;
|
|
29
|
+
labels?: NotificationCenterLabels;
|
|
30
|
+
showClear?: boolean;
|
|
17
31
|
onitemclick?: (item: NotificationItem) => void;
|
|
18
32
|
onmarkallread?: () => void;
|
|
19
33
|
onclear?: () => void;
|
package/dist/index.d.ts
CHANGED
|
@@ -678,7 +678,7 @@ export type { LoginFormVariant, LoginFormAlign } from './components/organisms/Lo
|
|
|
678
678
|
export { default as Menubar } from './components/organisms/Menubar/Menubar.svelte';
|
|
679
679
|
export type { MenubarItem, MenubarSubItem } from './components/organisms/Menubar/Menubar.svelte';
|
|
680
680
|
export { default as NotificationCenter } from './components/organisms/NotificationCenter/NotificationCenter.svelte';
|
|
681
|
-
export type { NotificationItem } from './components/organisms/NotificationCenter/NotificationCenter.svelte';
|
|
681
|
+
export type { NotificationItem, NotificationCenterLabels } from './components/organisms/NotificationCenter/NotificationCenter.svelte';
|
|
682
682
|
export { default as Kanban } from './components/organisms/Kanban/Kanban.svelte';
|
|
683
683
|
export type { KanbanColumn, KanbanCard, KanbanAssignee, KanbanPriority } from './components/organisms/Kanban/Kanban.svelte';
|
|
684
684
|
export { default as CookieConsent } from './components/organisms/CookieConsent/CookieConsent.svelte';
|