@streamscloud/kit 0.9.2 → 0.9.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.
@@ -2,6 +2,11 @@ export declare class ArrayHelper {
2
2
  static distinct<T>(items: T[]): T[];
3
3
  static distinctBy<T>(items: T[], keyFn: (elem: T) => unknown): T[];
4
4
  static intersect<T>(arrays: T[][]): T[];
5
+ /**
6
+ * Splits `items` into sequential chunks of at most `size` elements.
7
+ * If `size < 1`, returns a single chunk containing all items.
8
+ */
9
+ static chunk<T>(items: T[], size: number): T[][];
5
10
  static assertHasValue<T>(value: T | null | undefined): value is T;
6
11
  /**
7
12
  * Find position ("from" and "to" indexes) of the element that was moved inside array (only one element should be moved)
@@ -11,6 +11,20 @@ export class ArrayHelper {
11
11
  }
12
12
  return arrays.reduce((result, current) => result.filter((item) => current.includes(item)));
13
13
  }
14
+ /**
15
+ * Splits `items` into sequential chunks of at most `size` elements.
16
+ * If `size < 1`, returns a single chunk containing all items.
17
+ */
18
+ static chunk(items, size) {
19
+ if (size < 1) {
20
+ return [[...items]];
21
+ }
22
+ const result = [];
23
+ for (let i = 0; i < items.length; i += size) {
24
+ result.push(items.slice(i, i + size));
25
+ }
26
+ return result;
27
+ }
14
28
  static assertHasValue(value) {
15
29
  return value !== null && value !== undefined;
16
30
  }
@@ -0,0 +1,3 @@
1
+ export declare class AnnouncementBannerLocalization {
2
+ get dismiss(): string;
3
+ }
@@ -0,0 +1,12 @@
1
+ import { AppLocale } from '../../core/locale';
2
+ const loc = {
3
+ dismiss: {
4
+ en: 'Dismiss',
5
+ no: 'Lukk'
6
+ }
7
+ };
8
+ export class AnnouncementBannerLocalization {
9
+ get dismiss() {
10
+ return loc.dismiss[AppLocale.current];
11
+ }
12
+ }
@@ -0,0 +1,153 @@
1
+ <script lang="ts">import { Icon, IconSlot } from '../icon';
2
+ import { AnnouncementBannerLocalization } from './announcement-banner-localization';
3
+ import IconDismiss from '@fluentui/svg-icons/icons/dismiss_12_regular.svg?raw';
4
+ let { variant = 'info', title, dismissible = false, icon, action, children, on } = $props();
5
+ const localization = new AnnouncementBannerLocalization();
6
+ let visible = $state(true);
7
+ const role = $derived(variant === 'maintenance' ? 'alert' : 'status');
8
+ const dismiss = () => {
9
+ visible = false;
10
+ on?.dismiss?.();
11
+ };
12
+ </script>
13
+
14
+ {#if visible}
15
+ <div class="announcement-banner announcement-banner--{variant}" class:announcement-banner--dismissible={dismissible} role={role}>
16
+ <div class="announcement-banner__content">
17
+ {#if icon}
18
+ <span class="announcement-banner__icon" aria-hidden="true"><IconSlot icon={icon} /></span>
19
+ {/if}
20
+ <div class="announcement-banner__body">
21
+ {#if title}<strong class="announcement-banner__title">{title}</strong>{/if}
22
+ {#if children}<span class="announcement-banner__description">{@render children()}</span>{/if}
23
+ </div>
24
+ {#if action}<div class="announcement-banner__action">{@render action()}</div>{/if}
25
+ </div>
26
+ {#if dismissible}
27
+ <button class="announcement-banner__close" type="button" aria-label={localization.dismiss} onclick={dismiss}>
28
+ <Icon src={IconDismiss} />
29
+ </button>
30
+ {/if}
31
+ </div>
32
+ {/if}
33
+
34
+ <!--
35
+ @component
36
+ Full-width ribbon notification for site-wide announcements placed at the top of a page or layout.
37
+ Supports four visual variants; `variant='maintenance'` uses `role="alert"` (assertive); all others use `role="status"` (polite).
38
+
39
+ ### CSS Custom Properties
40
+ | Property | Description | Default |
41
+ |---|---|---|
42
+ | `--sc-kit--announcement-banner--padding-block` | Vertical padding | `--sc-kit--space--3` |
43
+ | `--sc-kit--announcement-banner--padding-inline` | Horizontal padding | `--sc-kit--space--5` |
44
+ | `--sc-kit--announcement-banner--gap` | Gap between icon / body / action | `--sc-kit--space--3` |
45
+ | `--sc-kit--announcement-banner--background` | Background color | per `variant` |
46
+ | `--sc-kit--announcement-banner--border-color` | Bottom border color | per `variant` |
47
+ | `--sc-kit--announcement-banner--accent-color` | Icon + dismiss glyph color | per `variant` |
48
+ | `--sc-kit--announcement-banner--title--color` | Title text color | `--sc-kit--color--text--primary` |
49
+ | `--sc-kit--announcement-banner--description--color` | Body text color | `--sc-kit--color--text--secondary` |
50
+ -->
51
+
52
+ <style>.announcement-banner {
53
+ --_ab--padding-block: var(--sc-kit--announcement-banner--padding-block, var(--sc-kit--space--3));
54
+ --_ab--padding-inline: var(--sc-kit--announcement-banner--padding-inline, var(--sc-kit--space--5));
55
+ --_ab--gap: var(--sc-kit--announcement-banner--gap, var(--sc-kit--space--3));
56
+ --_ab--title-color: var(--sc-kit--announcement-banner--title--color, var(--sc-kit--color--text--primary));
57
+ --_ab--description-color: var(--sc-kit--announcement-banner--description--color, var(--sc-kit--color--text--secondary));
58
+ --_ab--background-default: var(--sc-kit--color--accent--softer);
59
+ --_ab--border-default: var(--sc-kit--color--accent--soft);
60
+ --_ab--accent-default: var(--sc-kit--color--accent);
61
+ --_ab--background: var(--sc-kit--announcement-banner--background, var(--_ab--background-default));
62
+ --_ab--border-color: var(--sc-kit--announcement-banner--border-color, var(--_ab--border-default));
63
+ --_ab--accent-color: var(--sc-kit--announcement-banner--accent-color, var(--_ab--accent-default));
64
+ display: flex;
65
+ width: 100%;
66
+ align-items: center;
67
+ justify-content: center;
68
+ padding: var(--_ab--padding-block) var(--_ab--padding-inline);
69
+ background: var(--_ab--background);
70
+ border-bottom: 1px solid var(--_ab--border-color);
71
+ color: var(--_ab--accent-color);
72
+ position: relative;
73
+ }
74
+ .announcement-banner--dismissible {
75
+ padding-inline-end: calc(var(--_ab--padding-inline) + 2rem);
76
+ }
77
+ .announcement-banner--info {
78
+ --_ab--background-default: var(--sc-kit--color--accent--softer);
79
+ --_ab--border-default: var(--sc-kit--color--accent--soft);
80
+ --_ab--accent-default: var(--sc-kit--color--accent);
81
+ }
82
+ .announcement-banner--announcement {
83
+ --_ab--background-default: var(--sc-kit--color--accent--soft);
84
+ --_ab--border-default: var(--sc-kit--color--accent);
85
+ --_ab--accent-default: var(--sc-kit--color--accent);
86
+ }
87
+ .announcement-banner--warning {
88
+ --_ab--background-default: var(--sc-kit--color--warning--soft);
89
+ --_ab--border-default: var(--sc-kit--color--warning);
90
+ --_ab--accent-default: var(--sc-kit--color--warning);
91
+ }
92
+ .announcement-banner--maintenance {
93
+ --_ab--background-default: var(--sc-kit--color--danger--soft);
94
+ --_ab--border-default: var(--sc-kit--color--danger);
95
+ --_ab--accent-default: var(--sc-kit--color--danger);
96
+ }
97
+ .announcement-banner__content {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: var(--_ab--gap);
101
+ }
102
+ .announcement-banner__icon {
103
+ flex-shrink: 0;
104
+ display: inline-flex;
105
+ align-items: center;
106
+ line-height: 0;
107
+ }
108
+ .announcement-banner__body {
109
+ display: flex;
110
+ align-items: baseline;
111
+ flex-wrap: wrap;
112
+ gap: var(--sc-kit--space--2);
113
+ }
114
+ .announcement-banner__title {
115
+ font-size: var(--sc-kit--font-size--sm);
116
+ font-weight: var(--sc-kit--font-weight--semibold);
117
+ line-height: var(--sc-kit--leading--tight);
118
+ color: var(--_ab--title-color);
119
+ }
120
+ .announcement-banner__description {
121
+ font-size: var(--sc-kit--font-size--sm);
122
+ line-height: var(--sc-kit--leading--normal);
123
+ color: var(--_ab--description-color);
124
+ }
125
+ .announcement-banner__action {
126
+ flex-shrink: 0;
127
+ }
128
+ .announcement-banner__close {
129
+ position: absolute;
130
+ inset-inline-end: var(--_ab--padding-inline);
131
+ top: 50%;
132
+ transform: translateY(-50%);
133
+ appearance: none;
134
+ width: 1.25rem;
135
+ height: 1.25rem;
136
+ border: 0;
137
+ border-radius: var(--sc-kit--radius--sm);
138
+ background: transparent;
139
+ color: currentColor;
140
+ display: inline-flex;
141
+ align-items: center;
142
+ justify-content: center;
143
+ opacity: 0.7;
144
+ cursor: pointer;
145
+ --sc-kit--icon--size: 0.75rem;
146
+ }
147
+ .announcement-banner__close:hover {
148
+ opacity: 1;
149
+ }
150
+ .announcement-banner__close:focus-visible {
151
+ outline: 2px solid var(--sc-kit--color--border--focus);
152
+ outline-offset: 2px;
153
+ }</style>
@@ -0,0 +1,39 @@
1
+ import { type IconProp } from '../icon';
2
+ import type { AnnouncementBannerVariant } from './types';
3
+ import type { Snippet } from 'svelte';
4
+ type Props = {
5
+ /** @default 'info' */
6
+ variant?: AnnouncementBannerVariant;
7
+ /** Short headline rendered in bold. */
8
+ title?: string;
9
+ /** Show a dismiss button. Hides the banner locally when clicked and fires `on.dismiss`. @default false */
10
+ dismissible?: boolean;
11
+ /** Leading icon — string SVG source, `{ src, color?, size? }` object, or custom snippet. */
12
+ icon?: IconProp;
13
+ /** Optional inline call-to-action area (e.g. "Learn more →", "See status page"). */
14
+ action?: Snippet;
15
+ /** Announcement body text. */
16
+ children?: Snippet;
17
+ on?: {
18
+ dismiss?: () => void;
19
+ };
20
+ };
21
+ /**
22
+ * Full-width ribbon notification for site-wide announcements placed at the top of a page or layout.
23
+ * Supports four visual variants; `variant='maintenance'` uses `role="alert"` (assertive); all others use `role="status"` (polite).
24
+ *
25
+ * ### CSS Custom Properties
26
+ * | Property | Description | Default |
27
+ * |---|---|---|
28
+ * | `--sc-kit--announcement-banner--padding-block` | Vertical padding | `--sc-kit--space--3` |
29
+ * | `--sc-kit--announcement-banner--padding-inline` | Horizontal padding | `--sc-kit--space--5` |
30
+ * | `--sc-kit--announcement-banner--gap` | Gap between icon / body / action | `--sc-kit--space--3` |
31
+ * | `--sc-kit--announcement-banner--background` | Background color | per `variant` |
32
+ * | `--sc-kit--announcement-banner--border-color` | Bottom border color | per `variant` |
33
+ * | `--sc-kit--announcement-banner--accent-color` | Icon + dismiss glyph color | per `variant` |
34
+ * | `--sc-kit--announcement-banner--title--color` | Title text color | `--sc-kit--color--text--primary` |
35
+ * | `--sc-kit--announcement-banner--description--color` | Body text color | `--sc-kit--color--text--secondary` |
36
+ */
37
+ declare const Cmp: import("svelte").Component<Props, {}, "">;
38
+ type Cmp = ReturnType<typeof Cmp>;
39
+ export default Cmp;
@@ -0,0 +1,2 @@
1
+ export { default as AnnouncementBanner } from './cmp.announcement-banner.svelte';
2
+ export type { AnnouncementBannerVariant } from './types';
@@ -0,0 +1 @@
1
+ export { default as AnnouncementBanner } from './cmp.announcement-banner.svelte';
@@ -0,0 +1 @@
1
+ export type AnnouncementBannerVariant = 'info' | 'announcement' | 'warning' | 'maintenance';
@@ -0,0 +1 @@
1
+ export {};
@@ -1,30 +1,48 @@
1
1
  <script lang="ts">import { Image } from '../image';
2
- let { src = null, name = '', size = 'md', status = null } = $props();
3
- const initials = $derived(name
2
+ let { src = null, name = '', size = 'md', status = null, badge = null } = $props();
3
+ const deriveInitials = (value) => value
4
4
  .split(/\s+/)
5
5
  .filter(Boolean)
6
6
  .slice(0, 2)
7
7
  .map((p) => p[0])
8
8
  .join('')
9
- .toUpperCase());
9
+ .toUpperCase();
10
+ const initials = $derived(deriveInitials(name));
11
+ const badgeInitials = $derived(deriveInitials(badge?.name ?? ''));
10
12
  </script>
11
13
 
14
+ {#snippet personIcon()}
15
+ <span class="avatar__fallback" aria-hidden="true">
16
+ <svg class="avatar__fallback-icon" viewBox="0 0 24 24">
17
+ <circle cx="12" cy="9" r="4" fill="currentColor" opacity="0.6"></circle>
18
+ <path d="M4 21c0-4 4-7 8-7s8 3 8 7" fill="currentColor" opacity="0.6"></path>
19
+ </svg>
20
+ </span>
21
+ {/snippet}
22
+
12
23
  {#snippet fallback()}
13
24
  {#if name && initials}
14
25
  <span class="avatar__initials">{initials}</span>
15
26
  {:else}
16
- <span class="avatar__fallback" aria-hidden="true">
17
- <svg class="avatar__fallback-icon" viewBox="0 0 24 24">
18
- <circle cx="12" cy="9" r="4" fill="currentColor" opacity="0.6"></circle>
19
- <path d="M4 21c0-4 4-7 8-7s8 3 8 7" fill="currentColor" opacity="0.6"></path>
20
- </svg>
21
- </span>
27
+ {@render personIcon()}
22
28
  {/if}
23
29
  {/snippet}
24
30
 
25
- <span class="avatar avatar--{size}" class:avatar--with-status={!!status} role="img" aria-label={name || 'Avatar'}>
31
+ {#snippet badgeFallback()}
32
+ {#if badge?.name && badgeInitials}
33
+ <span class="avatar__initials avatar__initials--badge">{badgeInitials}</span>
34
+ {:else}
35
+ {@render personIcon()}
36
+ {/if}
37
+ {/snippet}
38
+
39
+ <span class="avatar avatar--{size}" class:avatar--with-status={!!status && !badge} role="img" aria-label={name || 'Avatar'}>
26
40
  <Image src={src} alt={name} showStubOnError stub={fallback} />
27
- {#if status}
41
+ {#if badge}
42
+ <span class="avatar__badge" role={badge.name ? 'img' : undefined} aria-label={badge.name || undefined} aria-hidden={badge.name ? undefined : true}>
43
+ <Image src={badge.src ?? null} alt={badge.name ?? ''} showStubOnError stub={badgeFallback} />
44
+ </span>
45
+ {:else if status}
28
46
  <span class="avatar__dot avatar__dot--{status}" aria-hidden="true"></span>
29
47
  {/if}
30
48
  </span>
@@ -33,13 +51,16 @@ const initials = $derived(name
33
51
  @component
34
52
  A circular avatar with three render modes (priority order): image → initials (derived from `name`)
35
53
  → generic person icon. If `src` fails to load, automatically falls back to initials/icon. Optional
36
- status dot in the bottom-right corner. Built on top of the kit `Image` component, which handles
37
- the load / error / stub state machine.
54
+ status dot in the bottom-right corner, or a square `badge` mini-avatar in the same corner (the
55
+ workspace/org-with-user pattern, same fallback chain at badge scale) — `badge` and `status` are
56
+ mutually exclusive, `badge` wins. Built on top of the kit `Image` component, which handles the
57
+ load / error / stub state machine.
38
58
 
39
59
  ### CSS Custom Properties
40
60
  | Property | Description | Default |
41
61
  |---|---|---|
42
62
  | `--sc-kit--avatar--size` | Diameter (overrides size preset) | per `size` preset (20 / 24 / 32 / 40 / 56 px) |
63
+ | `--sc-kit--avatar--border-radius` | Avatar shape (set e.g. `--sc-kit--radius--sm` for a rounded square) | `50%` |
43
64
  | `--sc-kit--avatar--background` | Fallback container background (initials / icon mode) | `--sc-kit--color--bg--active` |
44
65
  | `--sc-kit--avatar--color` | Fallback foreground (generic icon tint) | `--sc-kit--color--text--muted` |
45
66
  | `--sc-kit--avatar--initials--background` | Initials chip background | `--sc-kit--color--accent--soft` |
@@ -48,6 +69,9 @@ the load / error / stub state machine.
48
69
  | `--sc-kit--avatar--dot--color-online` | Online dot color | `--sc-kit--color--success` |
49
70
  | `--sc-kit--avatar--dot--color-busy` | Busy dot color | `--sc-kit--color--danger` |
50
71
  | `--sc-kit--avatar--dot--color-offline` | Offline dot color | `--sc-kit--color--text--muted` |
72
+ | `--sc-kit--avatar--badge--size` | Badge mini-avatar box | 45% of the avatar diameter |
73
+ | `--sc-kit--avatar--badge--ring-color` | Badge separation ring (override on active/hover row backgrounds to blend) | `--sc-kit--color--bg--panel` |
74
+ | `--sc-kit--avatar--badge--ring-width` | Badge separation ring thickness | `1.5px` |
51
75
  -->
52
76
 
53
77
  <style>.avatar {
@@ -59,7 +83,11 @@ the load / error / stub state machine.
59
83
  --_avatar--dot-online: var(--sc-kit--avatar--dot--color-online, var(--sc-kit--color--success));
60
84
  --_avatar--dot-busy: var(--sc-kit--avatar--dot--color-busy, var(--sc-kit--color--danger));
61
85
  --_avatar--dot-offline: var(--sc-kit--avatar--dot--color-offline, var(--sc-kit--color--text--muted));
62
- --sc-kit--image--border-radius: 50%;
86
+ --_avatar--badge-size: var(--sc-kit--avatar--badge--size, calc(var(--_avatar--size) * 0.45));
87
+ --_avatar--badge-ring-color: var(--sc-kit--avatar--badge--ring-color, var(--sc-kit--color--bg--panel));
88
+ --_avatar--badge-ring-width: var(--sc-kit--avatar--badge--ring-width, 1.5px);
89
+ --_avatar--border-radius: var(--sc-kit--avatar--border-radius, 50%);
90
+ --sc-kit--image--border-radius: var(--_avatar--border-radius);
63
91
  --sc-kit--image--background: var(--_avatar--background);
64
92
  position: relative;
65
93
  display: flex;
@@ -67,7 +95,7 @@ the load / error / stub state machine.
67
95
  justify-content: center;
68
96
  width: var(--_avatar--size);
69
97
  height: var(--_avatar--size);
70
- border-radius: 50%;
98
+ border-radius: var(--_avatar--border-radius);
71
99
  flex-shrink: 0;
72
100
  background: var(--_avatar--background);
73
101
  color: var(--_avatar--color);
@@ -91,7 +119,7 @@ the load / error / stub state machine.
91
119
  .avatar__initials {
92
120
  width: 100%;
93
121
  height: 100%;
94
- border-radius: 50%;
122
+ border-radius: var(--_avatar--border-radius);
95
123
  display: flex;
96
124
  align-items: center;
97
125
  justify-content: center;
@@ -102,6 +130,25 @@ the load / error / stub state machine.
102
130
  line-height: var(--sc-kit--leading--tight);
103
131
  user-select: none;
104
132
  }
133
+ .avatar__initials--badge {
134
+ border-radius: var(--sc-kit--radius--sm);
135
+ font-size: calc(var(--_avatar--badge-size) * 0.5);
136
+ }
137
+ .avatar__badge {
138
+ position: absolute;
139
+ right: -0.125rem;
140
+ bottom: -0.125rem;
141
+ width: var(--_avatar--badge-size);
142
+ height: var(--_avatar--badge-size);
143
+ border-radius: var(--sc-kit--radius--sm);
144
+ box-shadow: 0 0 0 var(--_avatar--badge-ring-width) var(--_avatar--badge-ring-color);
145
+ overflow: hidden;
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ background: var(--_avatar--background);
150
+ --sc-kit--image--border-radius: var(--sc-kit--radius--sm);
151
+ }
105
152
  .avatar__fallback {
106
153
  width: 100%;
107
154
  height: 100%;
@@ -1,5 +1,4 @@
1
- type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
2
- type AvatarStatus = 'online' | 'busy' | 'offline';
1
+ import type { AvatarData, AvatarSize, AvatarStatus } from './types';
3
2
  type Props = {
4
3
  /** Image URL. If omitted or fails to load, falls back to initials, then to a generic icon. */
5
4
  src?: string | null;
@@ -7,19 +6,24 @@ type Props = {
7
6
  name?: string;
8
7
  /** Size preset. @default 'md' */
9
8
  size?: AvatarSize;
10
- /** Optional status dot rendered in the bottom-right corner. */
9
+ /** Optional status dot rendered in the bottom-right corner. Ignored while `badge` is set — both occupy the same corner. */
11
10
  status?: AvatarStatus | null;
11
+ /** Secondary mini-avatar layered on the bottom-right corner — the workspace/org-with-user pattern. Reuses the image → initials → icon fallback chain at badge scale. Takes the corner over `status`. */
12
+ badge?: AvatarData | null;
12
13
  };
13
14
  /**
14
15
  * A circular avatar with three render modes (priority order): image → initials (derived from `name`)
15
16
  * → generic person icon. If `src` fails to load, automatically falls back to initials/icon. Optional
16
- * status dot in the bottom-right corner. Built on top of the kit `Image` component, which handles
17
- * the load / error / stub state machine.
17
+ * status dot in the bottom-right corner, or a square `badge` mini-avatar in the same corner (the
18
+ * workspace/org-with-user pattern, same fallback chain at badge scale) — `badge` and `status` are
19
+ * mutually exclusive, `badge` wins. Built on top of the kit `Image` component, which handles the
20
+ * load / error / stub state machine.
18
21
  *
19
22
  * ### CSS Custom Properties
20
23
  * | Property | Description | Default |
21
24
  * |---|---|---|
22
25
  * | `--sc-kit--avatar--size` | Diameter (overrides size preset) | per `size` preset (20 / 24 / 32 / 40 / 56 px) |
26
+ * | `--sc-kit--avatar--border-radius` | Avatar shape (set e.g. `--sc-kit--radius--sm` for a rounded square) | `50%` |
23
27
  * | `--sc-kit--avatar--background` | Fallback container background (initials / icon mode) | `--sc-kit--color--bg--active` |
24
28
  * | `--sc-kit--avatar--color` | Fallback foreground (generic icon tint) | `--sc-kit--color--text--muted` |
25
29
  * | `--sc-kit--avatar--initials--background` | Initials chip background | `--sc-kit--color--accent--soft` |
@@ -28,6 +32,9 @@ type Props = {
28
32
  * | `--sc-kit--avatar--dot--color-online` | Online dot color | `--sc-kit--color--success` |
29
33
  * | `--sc-kit--avatar--dot--color-busy` | Busy dot color | `--sc-kit--color--danger` |
30
34
  * | `--sc-kit--avatar--dot--color-offline` | Offline dot color | `--sc-kit--color--text--muted` |
35
+ * | `--sc-kit--avatar--badge--size` | Badge mini-avatar box | 45% of the avatar diameter |
36
+ * | `--sc-kit--avatar--badge--ring-color` | Badge separation ring (override on active/hover row backgrounds to blend) | `--sc-kit--color--bg--panel` |
37
+ * | `--sc-kit--avatar--badge--ring-width` | Badge separation ring thickness | `1.5px` |
31
38
  */
32
39
  declare const Cmp: import("svelte").Component<Props, {}, "">;
33
40
  type Cmp = ReturnType<typeof Cmp>;
@@ -1 +1,2 @@
1
1
  export { default as Avatar } from './cmp.avatar.svelte';
2
+ export type { AvatarData, AvatarSize, AvatarStatus } from './types';
@@ -0,0 +1,8 @@
1
+ export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
2
+ export type AvatarStatus = 'online' | 'busy' | 'offline';
3
+ export type AvatarData = {
4
+ /** Image URL. `null` / `undefined` skips to the initials → icon fallback chain. */
5
+ src?: string | null;
6
+ /** Display name — drives the initials fallback and the accessible label. */
7
+ name?: string;
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,18 +1,12 @@
1
- <script lang="ts">import { Icon } from '../icon';
1
+ <script lang="ts">import { Avatar } from '../avatar';
2
+ import { Icon } from '../icon';
2
3
  import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_20_regular.svg?raw';
3
- const { name, subtitle, avatar, secondaryAvatar, position = 'top' } = $props();
4
+ const { name, subtitle, avatar, badge = null, position = 'top' } = $props();
4
5
  </script>
5
6
 
6
- <div class="nav-menu-account-row" class:nav-menu-account-row--dual={secondaryAvatar} class:nav-menu-account-row--top={position === 'top'}>
7
- <span class="nav-menu-account-row__avatar-stack">
8
- <span class="nav-menu-account-row__avatar nav-menu-account-row__avatar--primary">
9
- {@render avatar()}
10
- </span>
11
- {#if secondaryAvatar}
12
- <span class="nav-menu-account-row__avatar nav-menu-account-row__avatar--secondary">
13
- {@render secondaryAvatar()}
14
- </span>
15
- {/if}
7
+ <div class="nav-menu-account-row" class:nav-menu-account-row--top={position === 'top'}>
8
+ <span class="nav-menu-account-row__avatar" class:nav-menu-account-row__avatar--dual={!!badge}>
9
+ <Avatar src={avatar.src} name={avatar.name} badge={badge} />
16
10
  </span>
17
11
  <span class="nav-menu-account-row__info">
18
12
  <span class="nav-menu-account-row__name">{name}</span>
@@ -29,18 +23,13 @@ const { name, subtitle, avatar, secondaryAvatar, position = 'top' } = $props();
29
23
  @component
30
24
  NavMenuAccountRow — workspace / account trigger row for the top of a `NavMenu` panel. Purely
31
25
  presentational: render it inside a `Popover` `trigger` snippet to make it a real switcher. Layout:
32
- 32px avatar on the left, 2-line text (name + subtitle), chevron-down on the right. It owns its own
33
- padding + hover; for a full-width trigger set `--sc-kit--popover--width: 100%` on the `Popover`.
34
-
35
- ### CSS Custom Properties
36
- | Property | Description | Default |
37
- |---|---|---|
38
- | `--sc-kit--nav-menu-account-row--avatar-size` | Primary avatar box | `32px` |
39
- | `--sc-kit--nav-menu-account-row--secondary-avatar-size` | Secondary badge box | `14px` |
26
+ kit `Avatar` on the left at the `md` preset (rounded square; circle + corner badge in
27
+ org-with-user mode when `badge` is set), 2-line text (name + subtitle), chevron-down on the
28
+ right. No public CSS vars of its own — avatar sizing/colors are tuned via the
29
+ `--sc-kit--avatar--*` vars directly. It owns its own padding + hover; for a full-width trigger
30
+ set `--sc-kit--popover--width: 100%` on the `Popover`.
40
31
  -->
41
32
  <style>.nav-menu-account-row {
42
- --_avatar-size: var(--sc-kit--nav-menu-account-row--avatar-size, 2rem);
43
- --_secondary-size: var(--sc-kit--nav-menu-account-row--secondary-avatar-size, 0.875rem);
44
33
  display: flex;
45
34
  align-items: center;
46
35
  gap: var(--sc-kit--space--2);
@@ -53,56 +42,17 @@ padding + hover; for a full-width trigger set `--sc-kit--popover--width: 100%` o
53
42
  .nav-menu-account-row:hover {
54
43
  background: var(--sc-kit--color--bg--hover);
55
44
  }
56
-
57
45
  .nav-menu-account-row--top {
58
46
  margin-top: var(--sc-kit--space--4);
59
47
  }
60
-
61
- .nav-menu-account-row__avatar-stack {
62
- position: relative;
63
- display: inline-flex;
64
- width: var(--_avatar-size);
65
- height: var(--_avatar-size);
66
- flex-shrink: 0;
67
- }
68
-
69
48
  .nav-menu-account-row__avatar {
49
+ --sc-kit--avatar--border-radius: var(--sc-kit--radius--sm);
70
50
  display: inline-flex;
71
- align-items: center;
72
- justify-content: center;
73
- overflow: hidden;
74
- background: var(--sc-kit--color--bg--field-alt);
75
- color: var(--sc-kit--color--text--on-accent);
76
- font-size: var(--sc-kit--font-size--sm);
77
- font-weight: var(--sc-kit--font-weight--semibold);
78
- }
79
- .nav-menu-account-row__avatar :global(img) {
80
- width: 100%;
81
- height: 100%;
82
- object-fit: cover;
83
- }
84
-
85
- .nav-menu-account-row__avatar--primary {
86
- width: 100%;
87
- height: 100%;
88
- border-radius: var(--sc-kit--radius--sm);
89
- }
90
-
91
- .nav-menu-account-row--dual .nav-menu-account-row__avatar--primary {
92
- border-radius: var(--sc-kit--radius--circle);
51
+ flex-shrink: 0;
93
52
  }
94
-
95
- .nav-menu-account-row__avatar--secondary {
96
- position: absolute;
97
- right: -2px;
98
- bottom: -2px;
99
- width: var(--_secondary-size);
100
- height: var(--_secondary-size);
101
- border-radius: var(--sc-kit--radius--sm);
102
- box-shadow: 0 0 0 1.5px var(--sc-kit--color--bg--panel);
103
- font-size: 0.5rem;
53
+ .nav-menu-account-row__avatar--dual {
54
+ --sc-kit--avatar--border-radius: 50%;
104
55
  }
105
-
106
56
  .nav-menu-account-row__info {
107
57
  display: flex;
108
58
  flex-direction: column;
@@ -110,7 +60,6 @@ padding + hover; for a full-width trigger set `--sc-kit--popover--width: 100%` o
110
60
  min-width: 0;
111
61
  gap: 0.125rem;
112
62
  }
113
-
114
63
  .nav-menu-account-row__name {
115
64
  font-size: var(--sc-kit--font-size--md);
116
65
  font-weight: var(--sc-kit--font-weight--semibold);
@@ -121,7 +70,6 @@ padding + hover; for a full-width trigger set `--sc-kit--popover--width: 100%` o
121
70
  white-space: nowrap;
122
71
  overflow: hidden;
123
72
  }
124
-
125
73
  .nav-menu-account-row__subtitle {
126
74
  font-size: 0.625rem;
127
75
  color: var(--sc-kit--color--text--secondary);
@@ -131,16 +79,11 @@ padding + hover; for a full-width trigger set `--sc-kit--popover--width: 100%` o
131
79
  white-space: nowrap;
132
80
  overflow: hidden;
133
81
  }
134
-
135
82
  .nav-menu-account-row__chevron {
83
+ --sc-kit--icon--size: 0.75rem;
136
84
  flex-shrink: 0;
137
85
  display: inline-flex;
138
86
  align-items: center;
139
87
  justify-content: center;
140
88
  color: var(--sc-kit--color--text--secondary);
141
- }
142
- .nav-menu-account-row__chevron :global(svg) {
143
- width: 0.75rem;
144
- height: 0.75rem;
145
- fill: currentColor;
146
89
  }</style>
@@ -1,27 +1,24 @@
1
- import type { Snippet } from 'svelte';
1
+ import { type AvatarData } from '../avatar';
2
2
  type Props = {
3
3
  /** Primary line — workspace / account name. Truncates with ellipsis. */
4
4
  name: string;
5
5
  /** Secondary line — workspace type, role, email, etc. Truncates. */
6
6
  subtitle?: string;
7
- /** Primary avatar slot (32 × 32px). Renders as a rounded square by default; becomes a circle when `secondaryAvatar` is also provided (org-with-user pattern). */
8
- avatar: Snippet;
7
+ /** Primary avatar data (image initials → icon fallback). Renders as a rounded square by default; becomes a circle when `badge` is also provided (org-with-user pattern). */
8
+ avatar: AvatarData;
9
9
  /** Optional secondary avatar — square badge layered on the bottom-right of the primary. Use it to show the active user when the primary is the workspace / organization. */
10
- secondaryAvatar?: Snippet;
10
+ badge?: AvatarData | null;
11
11
  /** Placement within the menu — `top` adds spacing above so the row clears the panel's top edge; `bottom` leaves it flush (footer use). @default 'top' */
12
12
  position?: 'top' | 'bottom';
13
13
  };
14
14
  /**
15
15
  * NavMenuAccountRow — workspace / account trigger row for the top of a `NavMenu` panel. Purely
16
16
  * presentational: render it inside a `Popover` `trigger` snippet to make it a real switcher. Layout:
17
- * 32px avatar on the left, 2-line text (name + subtitle), chevron-down on the right. It owns its own
18
- * padding + hover; for a full-width trigger set `--sc-kit--popover--width: 100%` on the `Popover`.
19
- *
20
- * ### CSS Custom Properties
21
- * | Property | Description | Default |
22
- * |---|---|---|
23
- * | `--sc-kit--nav-menu-account-row--avatar-size` | Primary avatar box | `32px` |
24
- * | `--sc-kit--nav-menu-account-row--secondary-avatar-size` | Secondary badge box | `14px` |
17
+ * kit `Avatar` on the left at the `md` preset (rounded square; circle + corner badge in
18
+ * org-with-user mode when `badge` is set), 2-line text (name + subtitle), chevron-down on the
19
+ * right. No public CSS vars of its own — avatar sizing/colors are tuned via the
20
+ * `--sc-kit--avatar--*` vars directly. It owns its own padding + hover; for a full-width trigger
21
+ * set `--sc-kit--popover--width: 100%` on the `Popover`.
25
22
  */
26
23
  declare const Cmp: import("svelte").Component<Props, {}, "">;
27
24
  type Cmp = ReturnType<typeof Cmp>;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">import { popoverIgnore } from './popover-ignore';
2
- const { disabled = false, keepOpen = false, divider = false, on, children } = $props();
2
+ const { disabled = false, keepOpen = false, divider = false, inset = false, on, children } = $props();
3
3
  const handleClick = () => {
4
4
  if (disabled) {
5
5
  return;
@@ -12,6 +12,7 @@ const handleClick = () => {
12
12
  class="popover-item"
13
13
  class:popover-item--disabled={disabled}
14
14
  class:popover-item--divider={divider}
15
+ class:popover-item--inset={inset}
15
16
  use:popoverIgnore={keepOpen}
16
17
  role="menuitem"
17
18
  tabindex={disabled ? -1 : 0}
@@ -42,6 +43,8 @@ for toggle items inside menus).
42
43
  | `--sc-kit--popover-item--divider-color` | Divider line color | `var(--sc-kit--color--border)` |
43
44
  | `--sc-kit--popover-item--divider-spacing` | Vertical gap added by divider | `var(--sc-kit--space--1)` |
44
45
  | `--sc-kit--popover-item--text-align` | Text alignment | `left` |
46
+ | `--sc-kit--popover-item--margin-inline` | Inline margin when `inset` | `var(--sc-kit--space--2)` |
47
+ | `--sc-kit--popover-item--border-radius` | Corner radius when `inset` | `var(--sc-kit--radius--sm)` |
45
48
  -->
46
49
 
47
50
  <style>.popover-item {
@@ -56,6 +59,8 @@ for toggle items inside menus).
56
59
  --_di--divider-color: var(--sc-kit--popover-item--divider-color, var(--sc-kit--color--border));
57
60
  --_di--divider-spacing: var(--sc-kit--popover-item--divider-spacing, var(--sc-kit--space--1));
58
61
  --_di--text-align: var(--sc-kit--popover-item--text-align, left);
62
+ --_di--margin-inline: var(--sc-kit--popover-item--margin-inline, var(--sc-kit--space--2));
63
+ --_di--border-radius: var(--sc-kit--popover-item--border-radius, var(--sc-kit--radius--sm));
59
64
  display: flex;
60
65
  align-items: center;
61
66
  gap: var(--_di--gap);
@@ -81,6 +86,10 @@ for toggle items inside menus).
81
86
  margin-bottom: var(--_di--divider-spacing);
82
87
  padding-bottom: calc(var(--_di--padding-block) + 1px);
83
88
  }
89
+ .popover-item--inset {
90
+ margin-inline: var(--_di--margin-inline);
91
+ border-radius: var(--_di--border-radius);
92
+ }
84
93
  .popover-item__content {
85
94
  flex: 1;
86
95
  min-width: 0;
@@ -5,6 +5,8 @@ type Props = {
5
5
  keepOpen?: boolean;
6
6
  /** Render a separator line below this item — useful for grouping menu sections. */
7
7
  divider?: boolean;
8
+ /** Inset the item from the popover edges: adds an inline margin and rounds the hover background. */
9
+ inset?: boolean;
8
10
  on?: {
9
11
  click?: () => void;
10
12
  };
@@ -29,6 +31,8 @@ type Props = {
29
31
  * | `--sc-kit--popover-item--divider-color` | Divider line color | `var(--sc-kit--color--border)` |
30
32
  * | `--sc-kit--popover-item--divider-spacing` | Vertical gap added by divider | `var(--sc-kit--space--1)` |
31
33
  * | `--sc-kit--popover-item--text-align` | Text alignment | `left` |
34
+ * | `--sc-kit--popover-item--margin-inline` | Inline margin when `inset` | `var(--sc-kit--space--2)` |
35
+ * | `--sc-kit--popover-item--border-radius` | Corner radius when `inset` | `var(--sc-kit--radius--sm)` |
32
36
  */
33
37
  declare const Cmp: import("svelte").Component<Props, {}, "">;
34
38
  type Cmp = ReturnType<typeof Cmp>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",