@datalayer/core 1.1.57 → 1.1.60

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.
@@ -16,7 +16,15 @@ export type UserAvatarProps = {
16
16
  fallbackBackground?: string;
17
17
  /** Optional icon foreground color override for the default avatar. */
18
18
  fallbackForeground?: string;
19
+ /**
20
+ * Whether a ring is drawn around the avatar.
21
+ *
22
+ * Off by default: an avatar sits on its own in most places, and a ring
23
+ * there is one more line for nothing. On, it gives the avatar an edge of
24
+ * its own against a background of the same colour — a profile page, a card.
25
+ */
26
+ ring?: boolean;
19
27
  className?: string;
20
28
  };
21
- export declare const UserAvatar: ({ avatarUrl, avatarIcon, size, square, iconSize, fallbackBackground, fallbackForeground, className, }: UserAvatarProps) => JSX.Element;
29
+ export declare const UserAvatar: ({ avatarUrl, avatarIcon, size, square, iconSize, fallbackBackground, fallbackForeground, ring, className, }: UserAvatarProps) => JSX.Element;
22
30
  export default UserAvatar;
@@ -28,8 +28,18 @@ export function hasRealAvatar(url) {
28
28
  }
29
29
  return true;
30
30
  }
31
- export const UserAvatar = ({ avatarUrl, avatarIcon, size = 100, square = true, iconSize, fallbackBackground, fallbackForeground, className, }) => {
31
+ export const UserAvatar = ({ avatarUrl, avatarIcon, size = 100, square = true, iconSize, fallbackBackground, fallbackForeground, ring = false, className, }) => {
32
32
  const palette = useColorPalette();
33
+ /*
34
+ * The ring, as a shadow rather than a border.
35
+ *
36
+ * A border would take from the size asked for — the avatar is laid out
37
+ * against others of the same measure — so it is drawn just outside the
38
+ * edge instead, following whatever shape the avatar has.
39
+ */
40
+ const ringSx = ring
41
+ ? { boxShadow: '0 0 0 1px var(--borderColor-default, currentColor)' }
42
+ : undefined;
33
43
  const SelectedIcon = getPrincipalAvatarIcon(avatarIcon);
34
44
  if (SelectedIcon) {
35
45
  return (_jsx(Box, { className: className, sx: {
@@ -37,13 +47,16 @@ export const UserAvatar = ({ avatarUrl, avatarIcon, size = 100, square = true, i
37
47
  height: size,
38
48
  borderRadius: square ? 2 : '50%',
39
49
  overflow: 'hidden',
40
- display: 'inline-flex',
50
+ display: 'flex',
41
51
  alignItems: 'center',
42
52
  justifyContent: 'center',
43
- }, children: _jsx(SelectedIcon, { size: size, colored: true }) }));
53
+ bg: fallbackBackground || 'accent.subtle',
54
+ '--datalayer-icon-fg': fallbackForeground || palette.primary,
55
+ ...ringSx,
56
+ }, children: _jsx(SelectedIcon, { size: iconSize ?? Math.round(size * 0.62), themed: true, colormode: true }) }));
44
57
  }
45
58
  if (hasRealAvatar(avatarUrl)) {
46
- return _jsx(DLAvatar, { className: className, square: square, src: avatarUrl, size: size });
59
+ return (_jsx(DLAvatar, { className: className, square: square, src: avatarUrl, size: size, sx: ringSx }));
47
60
  }
48
61
  const resolvedIconSize = iconSize ?? Math.round(size * 0.48);
49
62
  return (_jsx(Box, { className: className, sx: {
@@ -55,6 +68,7 @@ export const UserAvatar = ({ avatarUrl, avatarIcon, size = 100, square = true, i
55
68
  alignItems: 'center',
56
69
  justifyContent: 'center',
57
70
  '--datalayer-icon-fg': fallbackForeground || palette.primary,
71
+ ...ringSx,
58
72
  }, children: _jsx(AlienIcon, { size: resolvedIconSize, themed: true, colormode: true }) }));
59
73
  };
60
74
  export default UserAvatar;
@@ -35,10 +35,22 @@ function collaboratorOfState(clientId, state) {
35
35
  if (!user) {
36
36
  return undefined;
37
37
  }
38
+ // Not a person: the presence of the collaboration service itself — the
39
+ // spacer joins its own rooms, wearing an `agent` field.
40
+ if (user.agent) {
41
+ return undefined;
42
+ }
38
43
  const name = user.display_name || user.displayName || user.name || user.username || '';
39
44
  if (!name) {
40
45
  return undefined;
41
46
  }
47
+ // Nobody either: the seed identity of a Jupyter Server that does not
48
+ // authenticate its users — "Anonymous Hegemone" and kin. A face for it
49
+ // would be one avatar too many, wearing the default fallback, next to the
50
+ // real identity written over it a moment later.
51
+ if (user.anonymous === true || /^anonymous(\s|$)/i.test(name)) {
52
+ return undefined;
53
+ }
42
54
  return {
43
55
  id: String(user.uid ?? user.id ?? user.username ?? clientId),
44
56
  name,
@@ -102,6 +114,16 @@ export function LiveEditorCollaborators(props) {
102
114
  if (!faces.length) {
103
115
  return null;
104
116
  }
105
- return (_jsx(AvatarStack, { size: size, children: faces.map(face => (_jsx(PrincipalAvatar, { kind: "personal", size: size, alt: face.name, avatarUrl: face.avatarUrl, avatarIcon: face.avatarIcon, square: false }, face.id))) }));
117
+ return (
118
+ /*
119
+ * Expanding to the LEFT.
120
+ *
121
+ * A stack of faces spreads out on hover, and it spreads from where it is
122
+ * anchored: anchored on its left, it grew rightwards over whatever the
123
+ * toolbar holds next to it. `alignRight` anchors it on its right, so it
124
+ * opens back over the space it came from — empty by construction, since
125
+ * the stack was collapsed there a moment before.
126
+ */
127
+ _jsx(AvatarStack, { alignRight: true, size: size, children: faces.map(face => (_jsx(PrincipalAvatar, { kind: "personal", size: size, alt: face.name, avatarUrl: face.avatarUrl, avatarIcon: face.avatarIcon, square: false }, face.id))) }));
106
128
  }
107
129
  export default LiveEditorCollaborators;
@@ -39,4 +39,13 @@ export declare function AdminLabel(props?: StatusLabelProps): JSX.Element;
39
39
  * The one that is taken when none is chosen — the default space of an account.
40
40
  */
41
41
  export declare function DefaultLabel(props?: StatusLabelProps): JSX.Element;
42
+ /**
43
+ * Something announced and not there yet.
44
+ *
45
+ * A tone of its own, and deliberately not the accent an alpha wears: one says
46
+ * "you may try this, it may bite", the other "there is nothing to try". Read
47
+ * side by side in a navigation, two labels of one colour say the same thing
48
+ * about two entries that mean different things.
49
+ */
50
+ export declare function SoonLabel(props?: StatusLabelProps): JSX.Element;
42
51
  export default StatusLabel;
@@ -33,7 +33,24 @@ export function StatusLabel(props) {
33
33
  return (_jsx(Label, { variant: "secondary", size: size, sx: {
34
34
  textTransform: 'lowercase',
35
35
  lineHeight: 1.2,
36
- ...TONES[tone],
36
+ /*
37
+ * The tone, under a doubled selector.
38
+ *
39
+ * Primer ships `NavList` and `ActionList` as CSS modules, and the
40
+ * colour they set on the contents of an item ties with the one Emotion
41
+ * generates for `sx` — so the same label came out in the colour of the
42
+ * navigation in one place and in its own colour in another, depending
43
+ * on which stylesheet happened to come last. `&&` raises the
44
+ * specificity enough that the tone always lands, wherever the label is
45
+ * put.
46
+ */
47
+ '&&': {
48
+ ...TONES[tone],
49
+ // The navigation of Primer colours the contents of its items, and
50
+ // its stylesheet is loaded after ours: a doubled selector was not
51
+ // enough on its own, so the colour of the tone is stated as final.
52
+ color: `${TONES[tone].color} !important`,
53
+ },
37
54
  ...sx,
38
55
  }, children: children }));
39
56
  }
@@ -58,4 +75,16 @@ export function DefaultLabel(props = {}) {
58
75
  const { children = 'default', ...rest } = props;
59
76
  return (_jsx(StatusLabel, { tone: "accent", ...rest, children: children }));
60
77
  }
78
+ /**
79
+ * Something announced and not there yet.
80
+ *
81
+ * A tone of its own, and deliberately not the accent an alpha wears: one says
82
+ * "you may try this, it may bite", the other "there is nothing to try". Read
83
+ * side by side in a navigation, two labels of one colour say the same thing
84
+ * about two entries that mean different things.
85
+ */
86
+ export function SoonLabel(props = {}) {
87
+ const { children = 'soon', ...rest } = props;
88
+ return (_jsx(StatusLabel, { tone: "neutral", ...rest, children: children }));
89
+ }
61
90
  export default StatusLabel;
@@ -23,11 +23,23 @@ import { SvgUsecasesHero } from '@datalayer/design/lib/svg/SvgUsecasesHero';
23
23
  export type PrincipalType = 'personal' | 'organization' | 'team';
24
24
  type AvatarComponent = ComponentType<SVGProps<SVGSVGElement> & {
25
25
  size?: number | 'small' | 'medium' | 'large';
26
+ /**
27
+ * The multi-colour variant, of the emoji the icon is drawn from.
28
+ *
29
+ * The avatars are never drawn with it — see the picker: they follow the
30
+ * colour of the theme instead, through `themed` and `colormode`.
31
+ */
26
32
  colored?: boolean;
33
+ /** Take the colour from `--datalayer-icon-fg`, or from `currentColor`. */
34
+ themed?: boolean;
35
+ /** Follow the colour mode of the page. */
36
+ colormode?: boolean | 'light' | 'dark';
27
37
  }>;
28
38
  export declare const PRINCIPAL_AVATAR_ICONS: ReadonlyArray<{
29
39
  name: string;
30
40
  label: string;
41
+ /** What the picture is of, as the emoji catalogue describes it. */
42
+ description?: string;
31
43
  Icon: AvatarComponent;
32
44
  }>;
33
45
  export declare const PRINCIPAL_BANNERS: readonly [{
@@ -1,40 +1,80 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2023-2025 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
2
6
  /*
3
7
  * Copyright (c) 2023-2026 Datalayer, Inc.
4
8
  * Distributed under the terms of the Modified BSD License.
5
9
  */
6
10
  import { useEffect, useState } from 'react';
7
- import { Box, Button, Text } from '@primer/react';
11
+ import { Box, Button, Text, Tooltip } from '@primer/react';
8
12
  import { Dialog } from '@primer/react/experimental';
13
+ import AlienMaskIcon from '@datalayer/icons-react/data2/AlienMaskIcon';
9
14
  import AlienMonsterIcon from '@datalayer/icons-react/data2/AlienMonsterIcon';
10
15
  import AstronautIcon from '@datalayer/icons-react/data2/AstronautIcon';
16
+ import AtomSymbolIcon from '@datalayer/icons-react/data2/AtomSymbolIcon';
17
+ import BankIcon from '@datalayer/icons-react/data2/BankIcon';
18
+ import BlackNibIcon from '@datalayer/icons-react/data2/BlackNibIcon';
19
+ import BriefcaseIcon from '@datalayer/icons-react/data2/BriefcaseIcon';
20
+ import BuildingClassicIcon from '@datalayer/icons-react/data2/BuildingClassicIcon';
21
+ import BuildingConstructionIcon from '@datalayer/icons-react/data2/BuildingConstructionIcon';
22
+ import BuildingOfficeIcon from '@datalayer/icons-react/data2/BuildingOfficeIcon';
23
+ import BullseyeIcon from '@datalayer/icons-react/data2/BullseyeIcon';
24
+ import CloudGreyIcon from '@datalayer/icons-react/data2/CloudGreyIcon';
25
+ import ConstructionIcon from '@datalayer/icons-react/data2/ConstructionIcon';
11
26
  import ConstructionWorkerIcon from '@datalayer/icons-react/data2/ConstructionWorkerIcon';
12
27
  import CowboyHatFaceIcon from '@datalayer/icons-react/data2/CowboyHatFaceIcon';
28
+ import DashboardGreyIcon from '@datalayer/icons-react/data1/DashboardGreyIcon';
29
+ import DnaIcon from '@datalayer/icons-react/data2/DnaIcon';
30
+ import DraftIcon from '@datalayer/icons-react/data1/DraftIcon';
13
31
  import DragonFaceIcon from '@datalayer/icons-react/data2/DragonFaceIcon';
32
+ import DragonIcon from '@datalayer/icons-react/data2/DragonIcon';
14
33
  import ElfManIcon from '@datalayer/icons-react/data2/ElfManIcon';
34
+ import FireIcon from '@datalayer/icons-react/data2/FireIcon';
35
+ import FireworksIcon from '@datalayer/icons-react/data2/FireworksIcon';
15
36
  import FlyingSaucerIcon from '@datalayer/icons-react/data2/FlyingSaucerIcon';
37
+ import FourLeafCloverIcon from '@datalayer/icons-react/data2/FourLeafCloverIcon';
38
+ import GraduationCapIcon from '@datalayer/icons-react/data2/GraduationCapIcon';
16
39
  import GremlinIcon from '@datalayer/icons-react/data2/GremlinIcon';
17
40
  import GrinningFaceIcon from '@datalayer/icons-react/data2/GrinningFaceIcon';
41
+ import HouseIcon from '@datalayer/icons-react/data2/HouseIcon';
18
42
  import LizardIcon from '@datalayer/icons-react/data2/LizardIcon';
19
43
  import MagicWandIcon from '@datalayer/icons-react/data2/MagicWandIcon';
20
44
  import ManOfficeWorkerIcon from '@datalayer/icons-react/data2/ManOfficeWorkerIcon';
21
45
  import ManTechnologistIcon from '@datalayer/icons-react/data2/ManTechnologistIcon';
46
+ import MusicalNoteIcon from '@datalayer/icons-react/data2/MusicalNoteIcon';
22
47
  import NinjaIcon from '@datalayer/icons-react/data2/NinjaIcon';
48
+ import OpenHandsIcon from '@datalayer/icons-react/data2/OpenHandsIcon';
49
+ import PenIcon from '@datalayer/icons-react/data2/PenIcon';
23
50
  import PenguinIcon from '@datalayer/icons-react/data2/PenguinIcon';
51
+ import PictureIcon from '@datalayer/icons-react/data2/PictureIcon';
52
+ import PlaneDepartureIcon from '@datalayer/icons-react/data2/PlaneDepartureIcon';
24
53
  import PersonSurfingIcon from '@datalayer/icons-react/data2/PersonSurfingIcon';
25
54
  import PersonSwimmingIcon from '@datalayer/icons-react/data2/PersonSwimmingIcon';
55
+ import PictureFramedIcon from '@datalayer/icons-react/data2/PictureFramedIcon';
26
56
  import RingedPlanetIcon from '@datalayer/icons-react/data2/RingedPlanetIcon';
27
57
  import RobotIcon from '@datalayer/icons-react/data2/RobotIcon';
28
58
  import RocketIcon from '@datalayer/icons-react/data2/RocketIcon';
29
59
  import SantaClausIcon from '@datalayer/icons-react/data2/SantaClausIcon';
60
+ import SatelliteIcon from '@datalayer/icons-react/data2/SatelliteIcon';
30
61
  import ScientistIcon from '@datalayer/icons-react/data2/ScientistIcon';
31
62
  import SharkIcon from '@datalayer/icons-react/data2/SharkIcon';
32
63
  import SnowmanIcon from '@datalayer/icons-react/data2/SnowmanIcon';
64
+ import SpaceInvadersAlien1Icon from '@datalayer/icons-react/eggs/SpaceInvadersAlien1Icon';
65
+ import SpaceInvadersAlien2Icon from '@datalayer/icons-react/eggs/SpaceInvadersAlien2Icon';
66
+ import SpaceInvadersAlien3Icon from '@datalayer/icons-react/eggs/SpaceInvadersAlien3Icon';
67
+ import SparklerIcon from '@datalayer/icons-react/data2/SparklerIcon';
33
68
  import StarIcon from '@datalayer/icons-react/data2/StarIcon';
34
- import StudentWomanIcon from '@datalayer/icons-react/data2/StudentWomanIcon';
69
+ import StudentIcon from '@datalayer/icons-react/data2/StudentIcon';
70
+ import StudioMicrophoneIcon from '@datalayer/icons-react/data2/StudioMicrophoneIcon';
35
71
  import SunIcon from '@datalayer/icons-react/data2/SunIcon';
72
+ import WavingHandIcon from '@datalayer/icons-react/data2/WavingHandIcon';
36
73
  import WhaleSpoutingIcon from '@datalayer/icons-react/data2/WhaleSpoutingIcon';
37
74
  import WomanTechnologistIcon from '@datalayer/icons-react/data2/WomanTechnologistIcon';
75
+ import WrappedGiftIcon from '@datalayer/icons-react/data2/WrappedGiftIcon';
76
+ import WritingHandIcon from '@datalayer/icons-react/data2/WritingHandIcon';
77
+ import YinYangIcon from '@datalayer/icons-react/data2/YinYangIcon';
38
78
  import { SvgAboutHero } from '@datalayer/design/lib/svg/SvgAboutHero';
39
79
  import { SvgAgentsHero } from '@datalayer/design/lib/svg/SvgAgentsHero';
40
80
  import { SvgAgentsHomeHero } from '@datalayer/design/lib/svg/SvgAgentsHomeHero';
@@ -57,40 +97,405 @@ import { SvgTermsHero } from '@datalayer/design/lib/svg/SvgTermsHero';
57
97
  import { SvgTutorialsHero } from '@datalayer/design/lib/svg/SvgTutorialsHero';
58
98
  import { SvgUsecasesHero } from '@datalayer/design/lib/svg/SvgUsecasesHero';
59
99
  export const PRINCIPAL_AVATAR_ICONS = [
60
- { name: 'AlienMonsterIcon', label: 'Alien monster', Icon: AlienMonsterIcon },
61
- { name: 'AstronautIcon', label: 'Astronaut', Icon: AstronautIcon },
62
- { name: 'ConstructionWorkerIcon', label: 'Construction worker', Icon: ConstructionWorkerIcon },
63
- { name: 'CowboyHatFaceIcon', label: 'Cowboy', Icon: CowboyHatFaceIcon },
64
- { name: 'DragonFaceIcon', label: 'Dragon', Icon: DragonFaceIcon },
65
- { name: 'ElfManIcon', label: 'Elf', Icon: ElfManIcon },
66
- { name: 'FlyingSaucerIcon', label: 'Flying saucer', Icon: FlyingSaucerIcon },
67
- { name: 'GremlinIcon', label: 'Gremlin', Icon: GremlinIcon },
68
- { name: 'GrinningFaceIcon', label: 'Grinning face', Icon: GrinningFaceIcon },
69
- { name: 'LizardIcon', label: 'Lizard', Icon: LizardIcon },
70
- { name: 'MagicWandIcon', label: 'Magic wand', Icon: MagicWandIcon },
71
- { name: 'ManOfficeWorkerIcon', label: 'Office worker', Icon: ManOfficeWorkerIcon },
72
- { name: 'ManTechnologistIcon', label: 'Man technologist', Icon: ManTechnologistIcon },
73
- { name: 'NinjaIcon', label: 'Ninja', Icon: NinjaIcon },
74
- { name: 'PenguinIcon', label: 'Penguin', Icon: PenguinIcon },
75
- { name: 'PersonSurfingIcon', label: 'Surfer', Icon: PersonSurfingIcon },
76
- { name: 'PersonSwimmingIcon', label: 'Swimmer', Icon: PersonSwimmingIcon },
77
- { name: 'RingedPlanetIcon', label: 'Planet', Icon: RingedPlanetIcon },
78
- { name: 'RobotIcon', label: 'Robot', Icon: RobotIcon },
79
- { name: 'RocketIcon', label: 'Rocket', Icon: RocketIcon },
80
- { name: 'SantaClausIcon', label: 'Santa', Icon: SantaClausIcon },
81
- { name: 'ScientistIcon', label: 'Scientist', Icon: ScientistIcon },
82
- { name: 'SharkIcon', label: 'Shark', Icon: SharkIcon },
83
- { name: 'SnowmanIcon', label: 'Snowman', Icon: SnowmanIcon },
84
- { name: 'StarIcon', label: 'Star', Icon: StarIcon },
85
- { name: 'StudentWomanIcon', label: 'Student', Icon: StudentWomanIcon },
86
- { name: 'SunIcon', label: 'Sun', Icon: SunIcon },
87
- { name: 'WhaleSpoutingIcon', label: 'Whale', Icon: WhaleSpoutingIcon },
88
- { name: 'WomanTechnologistIcon', label: 'Woman technologist', Icon: WomanTechnologistIcon },
100
+ {
101
+ name: 'AlienMaskIcon',
102
+ label: 'Alien mask',
103
+ description: 'A mask of an alien face, worn for disguise.',
104
+ Icon: AlienMaskIcon,
105
+ },
106
+ {
107
+ name: 'AlienMonsterIcon',
108
+ label: 'Alien monster',
109
+ description: 'The pixelated alien of the arcade cabinets.',
110
+ Icon: AlienMonsterIcon,
111
+ },
112
+ {
113
+ name: 'AstronautIcon',
114
+ label: 'Astronaut',
115
+ description: 'A person in a spacesuit, ready for orbit.',
116
+ Icon: AstronautIcon,
117
+ },
118
+ {
119
+ name: 'AtomSymbolIcon',
120
+ label: 'Atom',
121
+ description: 'A nucleus circled by electrons, the mark of physics.',
122
+ Icon: AtomSymbolIcon,
123
+ },
124
+ {
125
+ name: 'BankIcon',
126
+ label: 'Bank',
127
+ description: 'A columned building where money is kept.',
128
+ Icon: BankIcon,
129
+ },
130
+ {
131
+ name: 'BlackNibIcon',
132
+ label: 'Black nib',
133
+ description: 'The nib of a fountain pen, poised to write.',
134
+ Icon: BlackNibIcon,
135
+ },
136
+ {
137
+ name: 'BriefcaseIcon',
138
+ label: 'Briefcase',
139
+ description: 'A case for papers, carried to work.',
140
+ Icon: BriefcaseIcon,
141
+ },
142
+ {
143
+ name: 'BuildingClassicIcon',
144
+ label: 'Classic building',
145
+ description: 'A columned building of the classical order.',
146
+ Icon: BuildingClassicIcon,
147
+ },
148
+ {
149
+ name: 'BuildingConstructionIcon',
150
+ label: 'Building construction',
151
+ description: 'A building going up, crane and all.',
152
+ Icon: BuildingConstructionIcon,
153
+ },
154
+ {
155
+ name: 'BuildingOfficeIcon',
156
+ label: 'Office building',
157
+ description: 'An office block of many identical windows.',
158
+ Icon: BuildingOfficeIcon,
159
+ },
160
+ {
161
+ name: 'BullseyeIcon',
162
+ label: 'Bullseye',
163
+ description: 'A dart in the centre of the target.',
164
+ Icon: BullseyeIcon,
165
+ },
166
+ {
167
+ name: 'CloudGreyIcon',
168
+ label: 'Cloud',
169
+ description: 'A cloud, of the sky or of the servers.',
170
+ Icon: CloudGreyIcon,
171
+ },
172
+ {
173
+ name: 'ConstructionIcon',
174
+ label: 'Construction',
175
+ description: 'A striped barrier: work in progress.',
176
+ Icon: ConstructionIcon,
177
+ },
178
+ {
179
+ name: 'ConstructionWorkerIcon',
180
+ label: 'Construction worker',
181
+ description: 'A worker in a hard hat.',
182
+ Icon: ConstructionWorkerIcon,
183
+ },
184
+ {
185
+ name: 'CowboyHatFaceIcon',
186
+ label: 'Cowboy',
187
+ description: 'A grinning face under a cowboy hat.',
188
+ Icon: CowboyHatFaceIcon,
189
+ },
190
+ {
191
+ name: 'DashboardGreyIcon',
192
+ label: 'Dashboard',
193
+ description: 'A gauge with its needle in the red.',
194
+ Icon: DashboardGreyIcon,
195
+ },
196
+ {
197
+ name: 'DnaIcon',
198
+ label: 'DNA',
199
+ description: 'The double helix that carries the code of life.',
200
+ Icon: DnaIcon,
201
+ },
202
+ {
203
+ name: 'DraftIcon',
204
+ label: 'Draft',
205
+ description: 'A sheet still being drawn up.',
206
+ Icon: DraftIcon,
207
+ },
208
+ {
209
+ name: 'DragonFaceIcon',
210
+ label: 'Dragon face',
211
+ description: 'The face of a dragon, whiskers and horns.',
212
+ Icon: DragonFaceIcon,
213
+ },
214
+ {
215
+ name: 'DragonIcon',
216
+ label: 'Dragon',
217
+ description: 'A dragon in full, coiled and winged.',
218
+ Icon: DragonIcon,
219
+ },
220
+ {
221
+ name: 'ElfManIcon',
222
+ label: 'Elf',
223
+ description: 'A pointy-eared elf of the folk tales.',
224
+ Icon: ElfManIcon,
225
+ },
226
+ {
227
+ name: 'FireIcon',
228
+ label: 'Fire',
229
+ description: 'A flame — hot, fast, or simply on fire.',
230
+ Icon: FireIcon,
231
+ },
232
+ {
233
+ name: 'FireworksIcon',
234
+ label: 'Fireworks',
235
+ description: 'Fireworks bursting over a night sky.',
236
+ Icon: FireworksIcon,
237
+ },
238
+ {
239
+ name: 'FlyingSaucerIcon',
240
+ label: 'Flying saucer',
241
+ description: 'A saucer from elsewhere, beam and all.',
242
+ Icon: FlyingSaucerIcon,
243
+ },
244
+ {
245
+ name: 'FourLeafCloverIcon',
246
+ label: 'Four-leaf clover',
247
+ description: 'The rare fourth leaf, for luck.',
248
+ Icon: FourLeafCloverIcon,
249
+ },
250
+ {
251
+ name: 'GraduationCapIcon',
252
+ label: 'Graduation cap',
253
+ description: 'The square cap thrown on graduation day.',
254
+ Icon: GraduationCapIcon,
255
+ },
256
+ {
257
+ name: 'GremlinIcon',
258
+ label: 'Gremlin',
259
+ description: 'A small mischief-maker, blamed for the bugs.',
260
+ Icon: GremlinIcon,
261
+ },
262
+ {
263
+ name: 'GrinningFaceIcon',
264
+ label: 'Grinning face',
265
+ description: 'A face grinning from ear to ear.',
266
+ Icon: GrinningFaceIcon,
267
+ },
268
+ {
269
+ name: 'HouseIcon',
270
+ label: 'House',
271
+ description: 'A house with its roof and door.',
272
+ Icon: HouseIcon,
273
+ },
274
+ {
275
+ name: 'LizardIcon',
276
+ label: 'Lizard',
277
+ description: 'A lizard, still and watchful.',
278
+ Icon: LizardIcon,
279
+ },
280
+ {
281
+ name: 'MagicWandIcon',
282
+ label: 'Magic wand',
283
+ description: 'A wand trailing sparks.',
284
+ Icon: MagicWandIcon,
285
+ },
286
+ {
287
+ name: 'ManOfficeWorkerIcon',
288
+ label: 'Office worker',
289
+ description: 'A worker at a desk in an office.',
290
+ Icon: ManOfficeWorkerIcon,
291
+ },
292
+ {
293
+ name: 'ManTechnologistIcon',
294
+ label: 'Man technologist',
295
+ description: 'A man at a laptop, writing code.',
296
+ Icon: ManTechnologistIcon,
297
+ },
298
+ {
299
+ name: 'MusicalNoteIcon',
300
+ label: 'Musical note',
301
+ description: 'A single note off a stave.',
302
+ Icon: MusicalNoteIcon,
303
+ },
304
+ {
305
+ name: 'NinjaIcon',
306
+ label: 'Ninja',
307
+ description: 'A masked figure, quick and unseen.',
308
+ Icon: NinjaIcon,
309
+ },
310
+ {
311
+ name: 'OpenHandsIcon',
312
+ label: 'Open hands',
313
+ description: 'Two open hands, offered or welcoming.',
314
+ Icon: OpenHandsIcon,
315
+ },
316
+ {
317
+ name: 'PenIcon',
318
+ label: 'Pen',
319
+ description: 'A ballpoint pen for everyday writing.',
320
+ Icon: PenIcon,
321
+ },
322
+ {
323
+ name: 'PenguinIcon',
324
+ label: 'Penguin',
325
+ description: 'A penguin in its black and white.',
326
+ Icon: PenguinIcon,
327
+ },
328
+ {
329
+ name: 'PersonSurfingIcon',
330
+ label: 'Surfer',
331
+ description: 'A surfer riding the face of a wave.',
332
+ Icon: PersonSurfingIcon,
333
+ },
334
+ {
335
+ name: 'PersonSwimmingIcon',
336
+ label: 'Swimmer',
337
+ description: 'A swimmer mid-stroke.',
338
+ Icon: PersonSwimmingIcon,
339
+ },
340
+ {
341
+ name: 'PictureFramedIcon',
342
+ label: 'Framed picture',
343
+ description: 'A painting hung in its frame.',
344
+ Icon: PictureFramedIcon,
345
+ },
346
+ {
347
+ name: 'PictureIcon',
348
+ label: 'Picture',
349
+ description: 'A photograph of mountains and sun.',
350
+ Icon: PictureIcon,
351
+ },
352
+ {
353
+ name: 'PlaneDepartureIcon',
354
+ label: 'Plane',
355
+ description: 'An aeroplane lifting off the runway.',
356
+ Icon: PlaneDepartureIcon,
357
+ },
358
+ {
359
+ name: 'RingedPlanetIcon',
360
+ label: 'Planet',
361
+ description: 'A planet circled by its rings.',
362
+ Icon: RingedPlanetIcon,
363
+ },
364
+ {
365
+ name: 'RobotIcon',
366
+ label: 'Robot',
367
+ description: 'The square face of a robot.',
368
+ Icon: RobotIcon,
369
+ },
370
+ {
371
+ name: 'RocketIcon',
372
+ label: 'Rocket',
373
+ description: 'A rocket climbing on its exhaust.',
374
+ Icon: RocketIcon,
375
+ },
376
+ {
377
+ name: 'SantaClausIcon',
378
+ label: 'Santa',
379
+ description: 'Santa Claus, red hat and white beard.',
380
+ Icon: SantaClausIcon,
381
+ },
382
+ {
383
+ name: 'SatelliteIcon',
384
+ label: 'Satellite',
385
+ description: 'A satellite with its panels spread.',
386
+ Icon: SatelliteIcon,
387
+ },
388
+ {
389
+ name: 'ScientistIcon',
390
+ label: 'Scientist',
391
+ description: 'A scientist at the microscope.',
392
+ Icon: ScientistIcon,
393
+ },
394
+ {
395
+ name: 'SharkIcon',
396
+ label: 'Shark',
397
+ description: 'A shark, fin first.',
398
+ Icon: SharkIcon,
399
+ },
400
+ {
401
+ name: 'SnowmanIcon',
402
+ label: 'Snowman',
403
+ description: 'A snowman built up in the cold.',
404
+ Icon: SnowmanIcon,
405
+ },
406
+ {
407
+ name: 'SpaceInvadersAlien1Icon',
408
+ label: 'Space invader 1',
409
+ description: 'The first invader of the arcade fleet.',
410
+ Icon: SpaceInvadersAlien1Icon,
411
+ },
412
+ {
413
+ name: 'SpaceInvadersAlien2Icon',
414
+ label: 'Space invader 2',
415
+ description: 'The second invader of the arcade fleet.',
416
+ Icon: SpaceInvadersAlien2Icon,
417
+ },
418
+ {
419
+ name: 'SpaceInvadersAlien3Icon',
420
+ label: 'Space invader 3',
421
+ description: 'The third invader of the arcade fleet.',
422
+ Icon: SpaceInvadersAlien3Icon,
423
+ },
424
+ {
425
+ name: 'SparklerIcon',
426
+ label: 'Sparkler',
427
+ description: 'A hand-held sparkler throwing light.',
428
+ Icon: SparklerIcon,
429
+ },
430
+ {
431
+ name: 'StarIcon',
432
+ label: 'Star',
433
+ description: 'A five-pointed star.',
434
+ Icon: StarIcon,
435
+ },
436
+ {
437
+ name: 'StudentIcon',
438
+ label: 'Student',
439
+ description: 'A student with book and cap.',
440
+ Icon: StudentIcon,
441
+ },
442
+ {
443
+ name: 'StudioMicrophoneIcon',
444
+ label: 'Studio microphone',
445
+ description: 'The microphone of a recording studio.',
446
+ Icon: StudioMicrophoneIcon,
447
+ },
448
+ {
449
+ name: 'SunIcon',
450
+ label: 'Sun',
451
+ description: 'The sun at full strength.',
452
+ Icon: SunIcon,
453
+ },
454
+ {
455
+ name: 'WavingHandIcon',
456
+ label: 'Waving hand',
457
+ description: 'A hand raised in hello.',
458
+ Icon: WavingHandIcon,
459
+ },
460
+ {
461
+ name: 'WhaleSpoutingIcon',
462
+ label: 'Whale',
463
+ description: 'A whale blowing a spout of water.',
464
+ Icon: WhaleSpoutingIcon,
465
+ },
466
+ {
467
+ name: 'WomanTechnologistIcon',
468
+ label: 'Woman technologist',
469
+ description: 'A woman at a laptop, writing code.',
470
+ Icon: WomanTechnologistIcon,
471
+ },
472
+ {
473
+ name: 'WrappedGiftIcon',
474
+ label: 'Wrapped gift',
475
+ description: 'A present tied with a ribbon.',
476
+ Icon: WrappedGiftIcon,
477
+ },
478
+ {
479
+ name: 'WritingHandIcon',
480
+ label: 'Writing hand',
481
+ description: 'A hand writing with a pen.',
482
+ Icon: WritingHandIcon,
483
+ },
484
+ {
485
+ name: 'YinYangIcon',
486
+ label: 'Yin yang',
487
+ description: 'The two halves that make a whole.',
488
+ Icon: YinYangIcon,
489
+ },
89
490
  ];
90
491
  export const PRINCIPAL_BANNERS = [
91
492
  { name: 'SvgAboutHero', label: 'About', Component: SvgAboutHero },
92
493
  { name: 'SvgAgentsHero', label: 'Agents', Component: SvgAgentsHero },
93
- { name: 'SvgAgentsHomeHero', label: 'Agents home', Component: SvgAgentsHomeHero },
494
+ {
495
+ name: 'SvgAgentsHomeHero',
496
+ label: 'Agents home',
497
+ Component: SvgAgentsHomeHero,
498
+ },
94
499
  { name: 'SvgBlogHero', label: 'Blog', Component: SvgBlogHero },
95
500
  { name: 'SvgCareersHero', label: 'Careers', Component: SvgCareersHero },
96
501
  { name: 'SvgChangelogHero', label: 'Changelog', Component: SvgChangelogHero },
@@ -99,7 +504,11 @@ export const PRINCIPAL_BANNERS = [
99
504
  { name: 'SvgEarthHero', label: 'Earth', Component: SvgEarthHero },
100
505
  { name: 'SvgEvalsHero', label: 'Evaluations', Component: SvgEvalsHero },
101
506
  { name: 'SvgEventsHero', label: 'Events', Component: SvgEventsHero },
102
- { name: 'SvgIntegrationsHero', label: 'Integrations', Component: SvgIntegrationsHero },
507
+ {
508
+ name: 'SvgIntegrationsHero',
509
+ label: 'Integrations',
510
+ Component: SvgIntegrationsHero,
511
+ },
103
512
  { name: 'SvgLoginHero', label: 'Login', Component: SvgLoginHero },
104
513
  { name: 'SvgPartnersHero', label: 'Partners', Component: SvgPartnersHero },
105
514
  { name: 'SvgPricingHero', label: 'Pricing', Component: SvgPricingHero },
@@ -145,28 +554,95 @@ export function PrincipalBannerImage({ banner, height = 180, }) {
145
554
  export function PrincipalAvatarPicker({ value, onChange, disabled = false, }) {
146
555
  const [open, setOpen] = useState(false);
147
556
  const selected = PRINCIPAL_AVATAR_ICONS.find(option => option.name === value);
148
- return (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", disabled: disabled, onClick: () => setOpen(true), children: selected ? `Change Avatar (${selected.label})` : 'Choose Avatar' }), open ? (_jsx(Dialog, { title: "Choose an Avatar", onClose: () => setOpen(false), width: "large", sx: { width: 'min(720px, calc(100vw - 32px))', maxWidth: 'none' }, children: _jsx(Box, { sx: {
557
+ return (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", disabled: disabled, onClick: () => setOpen(true), children: selected ? `Change Avatar (${selected.label})` : 'Choose Avatar' }), open ? (_jsx(Dialog, { title: "Choose an Avatar", onClose: () => setOpen(false),
558
+ // The widest the dialog names — 640px — which is what it falls
559
+ // back to should the width below ever stop applying.
560
+ width: "xlarge",
561
+ /*
562
+ * Wider than any name of the scale, for the twelve avatars of a
563
+ * line to have room, and bounded by the window.
564
+ *
565
+ * Under `&&`, which doubles the specificity of the class it
566
+ * generates: the width of a named size is written
567
+ * `.prc-Dialog-Dialog:where([data-width=xlarge])`, whose `:where`
568
+ * weighs nothing, so the rule and a plain `sx` are of the very
569
+ * same specificity and the one applied is whichever the
570
+ * stylesheets happen to order last.
571
+ */
572
+ sx: {
573
+ '&&': {
574
+ width: 'min(1080px, calc(100vw - 32px))',
575
+ maxWidth: 'none',
576
+ },
577
+ }, children: _jsx(Box, { sx: {
149
578
  display: 'grid',
150
- gridTemplateColumns: 'repeat(auto-fill, minmax(72px, 1fr))',
579
+ // Twelve to a line, whatever the dialog is wide: the columns
580
+ // share it evenly and `minmax(0, …)` lets them shrink under
581
+ // their content, which a long name would otherwise widen.
582
+ gridTemplateColumns: [
583
+ 'repeat(6, minmax(0, 1fr))',
584
+ 'repeat(12, minmax(0, 1fr))',
585
+ ],
151
586
  gap: 2,
152
- }, children: PRINCIPAL_AVATAR_ICONS.map(({ name, label, Icon }) => (_jsx(Button, { type: "button", "aria-label": label, "aria-pressed": value === name, onClick: () => {
153
- onChange(name);
154
- setOpen(false);
155
- }, sx: {
156
- height: 72,
157
- p: 1,
158
- borderColor: value === name ? 'accent.emphasis' : 'border.default',
159
- bg: value === name ? 'accent.subtle' : 'canvas.default',
160
- }, children: _jsx(Icon, { size: 52, colored: true }) }, name))) }) })) : null] }));
587
+ }, children: PRINCIPAL_AVATAR_ICONS.map(({ name, label, description, Icon }) => (
588
+ // A button drawn as a card rather than a Primer `Button`: the
589
+ // avatar is named under it, as the banners are under theirs.
590
+ // The name under a card is clipped when it is long, and a
591
+ // picture says only so much: the tooltip gives the whole name
592
+ // and what the picture is of.
593
+ _jsx(Tooltip, { text: description ? `${label} — ${description}` : label, direction: "n", children: _jsxs(Box, { as: "button", type: "button", "aria-label": description ? `${label} — ${description}` : label, "aria-pressed": value === name, onClick: () => {
594
+ onChange(name);
595
+ setOpen(false);
596
+ }, sx: {
597
+ display: 'block',
598
+ width: '100%',
599
+ p: 0,
600
+ overflow: 'hidden',
601
+ appearance: 'none',
602
+ color: 'fg.default',
603
+ cursor: 'pointer',
604
+ border: '1px solid',
605
+ borderRadius: 2,
606
+ borderColor: value === name ? 'accent.emphasis' : 'border.default',
607
+ bg: value === name ? 'accent.subtle' : 'canvas.default',
608
+ ':hover': { borderColor: 'accent.emphasis' },
609
+ }, children: [_jsx(Box, { sx: {
610
+ height: 72,
611
+ display: 'flex',
612
+ alignItems: 'center',
613
+ justifyContent: 'center',
614
+ }, children: _jsx(Icon, { size: 52, themed: true, colormode: true }) }), _jsx(Text, { sx: {
615
+ display: 'block',
616
+ px: 2,
617
+ py: 1,
618
+ fontSize: 0,
619
+ textAlign: 'center',
620
+ // A name too long for the card is cut rather than
621
+ // stretching the grid it sits in.
622
+ overflow: 'hidden',
623
+ textOverflow: 'ellipsis',
624
+ whiteSpace: 'nowrap',
625
+ }, children: label })] }) }, name))) }) })) : null] }));
161
626
  }
162
627
  export function PrincipalBannerPicker({ value, onChange, disabled = false, showPreview = true, }) {
163
628
  const [open, setOpen] = useState(false);
164
629
  const [selectedName, setSelectedName] = useState(value);
165
630
  useEffect(() => setSelectedName(value), [value]);
166
631
  const selected = PRINCIPAL_BANNERS.find(option => option.name === selectedName);
167
- return (_jsxs(_Fragment, { children: [showPreview && selected ? (_jsx(Box, { sx: { mb: 2 }, children: _jsx(PrincipalBannerImage, { banner: selected.name, height: 120 }) })) : null, _jsx(Button, { type: "button", disabled: disabled, onClick: () => setOpen(true), children: selected ? `Change Banner (${selected.label})` : 'Choose Banner' }), open ? (_jsx(Dialog, { title: "Choose a Banner", onClose: () => setOpen(false), width: "xlarge", sx: { width: 'min(960px, calc(100vw - 32px))', maxWidth: 'none' }, children: _jsx(Box, { sx: {
632
+ return (_jsxs(_Fragment, { children: [showPreview && selected ? (_jsx(Box, { sx: { mb: 2 }, children: _jsx(PrincipalBannerImage, { banner: selected.name, height: 120 }) })) : null, _jsx(Button, { type: "button", disabled: disabled, onClick: () => setOpen(true), children: selected ? `Change Banner (${selected.label})` : 'Choose Banner' }), open ? (_jsx(Dialog, { title: "Choose a Banner", onClose: () => setOpen(false), width: "xlarge",
633
+ // Under `&&` for the reason given on the avatar picker: a plain
634
+ // `sx` ties with the named width and may lose to it.
635
+ sx: {
636
+ '&&': {
637
+ width: 'min(960px, calc(100vw - 32px))',
638
+ maxWidth: 'none',
639
+ },
640
+ }, children: _jsx(Box, { sx: {
168
641
  display: 'grid',
169
- gridTemplateColumns: ['1fr', 'repeat(3, minmax(0, 1fr))'],
642
+ // Four to a row, so a banner is seen against its neighbours
643
+ // rather than one at a time; one column on a narrow overlay,
644
+ // where four would be four slivers.
645
+ gridTemplateColumns: ['1fr', 'repeat(4, minmax(0, 1fr))'],
170
646
  gap: 3,
171
647
  }, children: PRINCIPAL_BANNERS.map(({ name, label, Component }) => (_jsxs(Box, { as: "button", type: "button", "aria-label": label, "aria-pressed": selectedName === name, onClick: () => {
172
648
  setSelectedName(name);
@@ -182,7 +658,9 @@ export function PrincipalBannerPicker({ value, onChange, disabled = false, showP
182
658
  cursor: 'pointer',
183
659
  border: '1px solid',
184
660
  borderRadius: 2,
185
- borderColor: selectedName === name ? 'accent.emphasis' : 'border.default',
661
+ borderColor: selectedName === name
662
+ ? 'accent.emphasis'
663
+ : 'border.default',
186
664
  bg: selectedName === name ? 'accent.subtle' : 'canvas.default',
187
665
  textAlign: 'left',
188
666
  ':hover': { borderColor: 'accent.emphasis' },
@@ -7,7 +7,14 @@ export type PrincipalAvatarProps = {
7
7
  alt?: string;
8
8
  size?: number;
9
9
  square?: boolean;
10
+ /**
11
+ * Whether a ring is drawn around the avatar.
12
+ *
13
+ * Off by default, and the same option `UserAvatar` carries — a public
14
+ * profile wants the edge, a menu entry does not.
15
+ */
16
+ ring?: boolean;
10
17
  className?: string;
11
18
  };
12
- export declare function PrincipalAvatar({ kind, avatarUrl, avatarIcon, alt, size, square, className, }: PrincipalAvatarProps): JSX.Element;
19
+ export declare function PrincipalAvatar({ kind, avatarUrl, avatarIcon, alt, size, square, ring, className, }: PrincipalAvatarProps): JSX.Element;
13
20
  export default PrincipalAvatar;
@@ -10,8 +10,13 @@ import { getPrincipalAvatarIcon } from './PrincipalAppearance.js';
10
10
  function getFallbackIconSize(size) {
11
11
  return Math.max(12, Math.round(size * 0.62));
12
12
  }
13
- export function PrincipalAvatar({ kind, avatarUrl, avatarIcon, alt, size = 20, square = false, className, }) {
13
+ export function PrincipalAvatar({ kind, avatarUrl, avatarIcon, alt, size = 20, square = false, ring = false, className, }) {
14
14
  const SelectedIcon = getPrincipalAvatarIcon(avatarIcon);
15
+ // Outside the edge rather than a border, so the avatar keeps the size it
16
+ // was asked for — see `UserAvatar`, which draws its ring the same way.
17
+ const ringSx = ring
18
+ ? { boxShadow: '0 0 0 1px var(--borderColor-default, currentColor)' }
19
+ : undefined;
15
20
  if (SelectedIcon) {
16
21
  return (_jsx(Box, { className: className, sx: {
17
22
  width: size,
@@ -21,10 +26,11 @@ export function PrincipalAvatar({ kind, avatarUrl, avatarIcon, alt, size = 20, s
21
26
  justifyContent: 'center',
22
27
  overflow: 'hidden',
23
28
  borderRadius: square ? 2 : '50%',
24
- }, "aria-label": alt || `${kind} avatar`, children: _jsx(SelectedIcon, { size: size, colored: true }) }));
29
+ ...ringSx,
30
+ }, "aria-label": alt || `${kind} avatar`, children: _jsx(SelectedIcon, { size: size, themed: true, colormode: true }) }));
25
31
  }
26
32
  if (kind === 'personal') {
27
- return (_jsx(UserAvatar, { avatarUrl: avatarUrl, avatarIcon: avatarIcon, size: size, square: square, iconSize: getFallbackIconSize(size), className: className }));
33
+ return (_jsx(UserAvatar, { avatarUrl: avatarUrl, avatarIcon: avatarIcon, size: size, square: square, iconSize: getFallbackIconSize(size), ring: ring, className: className }));
28
34
  }
29
35
  const iconSize = getFallbackIconSize(size);
30
36
  const borderRadius = square ? 2 : '50%';
@@ -39,6 +45,7 @@ export function PrincipalAvatar({ kind, avatarUrl, avatarIcon, alt, size = 20, s
39
45
  borderRadius,
40
46
  border: '1px solid',
41
47
  borderColor: 'border.default',
48
+ ...ringSx,
42
49
  }, "aria-label": alt || (kind === 'team' ? 'Team' : 'Organization'), children: _jsx(Icon, { size: iconSize }) }));
43
50
  }
44
51
  export default PrincipalAvatar;
@@ -9,6 +9,7 @@ import { OrganizationIcon, PeopleIcon, PersonIcon, TriangleDownIcon, } from '@pr
9
9
  import { useCache, useAuthorization } from '../../hooks/index.js';
10
10
  import { useCoreStore } from '../../state/index.js';
11
11
  import { useIAMStore } from '../../state/substates/index.js';
12
+ import { UserAvatar } from '../avatars/index.js';
12
13
  import { memberships as fetchMemberships } from '../../api/iam/profile.js';
13
14
  import { usePrincipalStore } from '../../hooks/usePrincipalStore.js';
14
15
  import { useBillingEntityStore } from '../../hooks/useBillingEntityStore.js';
@@ -284,7 +285,7 @@ export function PrincipalSwitcherMenu({ maxLabelChars = 48, fullWidth = true, sh
284
285
  if (personalUid && personalHandle) {
285
286
  selectUser(personalUid, personalHandle);
286
287
  }
287
- }, children: [_jsx(ActionList.LeadingVisual, { children: _jsx(PersonIcon, {}) }), _jsxs(Box, { as: "span", sx: {
288
+ }, children: [_jsx(ActionList.LeadingVisual, { children: _jsx(UserAvatar, { avatarUrl: user?.avatarUrl, avatarIcon: user?.avatarIcon, size: 16, square: false, ring: true }) }), _jsxs(Box, { as: "span", sx: {
288
289
  display: 'block',
289
290
  minWidth: 0,
290
291
  overflow: 'hidden',
@@ -2021,8 +2021,17 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2021
2021
  onSuccess: (resp, _variables) => {
2022
2022
  if (resp.success && resp.document) {
2023
2023
  const document = toDocument(resp.document);
2024
- // Set detail cache
2025
- queryClient.setQueryData(queryKeys.documents.detail(document.id), document);
2024
+ /*
2025
+ * The creation answer carries no model — the spacer keeps the
2026
+ * model with the content, and only the GET of a document answers
2027
+ * with it. Seeding the detail cache here would hand the editor a
2028
+ * document without a model, shown as a blank page: the cache is
2029
+ * invalidated instead, so an editor opening on the creation reads
2030
+ * the document from the server, model included.
2031
+ */
2032
+ queryClient.invalidateQueries({
2033
+ queryKey: queryKeys.documents.detail(document.id),
2034
+ });
2026
2035
  // Refetch all document queries immediately (including bySpace)
2027
2036
  queryClient.refetchQueries({
2028
2037
  queryKey: queryKeys.documents.all(),
@@ -15,6 +15,28 @@ const isOnPublicPath = () => {
15
15
  }
16
16
  return isPublicPath(window.location?.pathname || '');
17
17
  };
18
+ /**
19
+ * Whether these views are running inside JupyterLab.
20
+ *
21
+ * Read from the configuration JupyterLab writes into its own page, so nothing
22
+ * has to be imported from it: this module is used by the web application as
23
+ * well, which has no such configuration.
24
+ */
25
+ const isInsideJupyterLab = () => {
26
+ if (typeof document === 'undefined') {
27
+ return false;
28
+ }
29
+ const config = document.getElementById('jupyter-config-data');
30
+ if (!config) {
31
+ return false;
32
+ }
33
+ try {
34
+ return JSON.parse(config.textContent || '{}').appName === 'JupyterLab';
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ };
18
40
  export const useUser = (role) => {
19
41
  const { user } = useIAMStore();
20
42
  if (role) {
@@ -22,6 +44,10 @@ export const useUser = (role) => {
22
44
  if (isOnPublicPath()) {
23
45
  return user;
24
46
  }
47
+ if (isInsideJupyterLab()) {
48
+ // Same reason as below: the router of these views answers this.
49
+ return user;
50
+ }
25
51
  console.warn(`User should have role ${role.handle} - Forcing navigation to login page...`);
26
52
  window.location.href = LOGIN_HREF;
27
53
  throw new Error(`User should have role ${role.handle}`);
@@ -31,6 +57,22 @@ export const useUser = (role) => {
31
57
  if (isOnPublicPath()) {
32
58
  return user;
33
59
  }
60
+ /*
61
+ * Inside JupyterLab, nobody leaves the page.
62
+ *
63
+ * `/login` is a page of the web application, at the root of the origin —
64
+ * and the origin of a JupyterLab is the Jupyter Server, which serves
65
+ * something else entirely there. Signing out of a view thus threw the user
66
+ * out of JupyterLab and onto a page of the server, which is not even a
67
+ * sign-in form.
68
+ *
69
+ * The views of Datalayer carry their own sign-in route under the base of
70
+ * JupyterLab, and their router shows it as soon as there is no user — so
71
+ * the way to reach it is to let that render, not to navigate away from it.
72
+ */
73
+ if (isInsideJupyterLab()) {
74
+ return user;
75
+ }
34
76
  console.warn('No user found... Forcing navigation to /login');
35
77
  window.location.href = LOGIN_HREF;
36
78
  }
@@ -1,3 +1,7 @@
1
+ /*
2
+ * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
1
5
  /*
2
6
  * Copyright (c) 2023-2026 Datalayer, Inc.
3
7
  * Distributed under the terms of the Modified BSD License.
@@ -63,6 +63,6 @@ export const Datasources = ({ newDatasourceRoute = '/new/datasource', datasource
63
63
  gap: 3,
64
64
  flexWrap: 'wrap',
65
65
  mb: 4,
66
- }, children: [_jsxs(Box, { children: [_jsx(Heading, { as: "h2", sx: { fontSize: 3, mb: 1 }, children: "Datasources" }), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Configure external data providers available to your workspaces and agents." })] }), _jsx(Button, { size: "small", variant: "primary", leadingVisual: DatabaseIcon, onClick: e => navigate(newDatasourceRoute, e), children: "New datasource" })] }), _jsx(DatasourcesTable, { datasourcesListRoute: datasourcesListRoute, principalUid: principalUid, principalKind: principalKind, showInlineLoadingIndicator: showInlineLoadingIndicator })] }) }) }));
66
+ }, children: [_jsxs(Box, { children: [_jsx(Heading, { as: "h2", sx: { fontSize: 3, mb: 1 }, children: "Datasources" }), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Configure external data providers available to your workspaces and agents." })] }), _jsx(Button, { size: "small", variant: "primary", leadingVisual: DatabaseIcon, onClick: e => navigate(newDatasourceRoute, e), children: "New Datasource" })] }), _jsx(DatasourcesTable, { datasourcesListRoute: datasourcesListRoute, principalUid: principalUid, principalKind: principalKind, showInlineLoadingIndicator: showInlineLoadingIndicator })] }) }) }));
67
67
  };
68
68
  export default Datasources;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/core",
3
- "version": "1.1.57",
3
+ "version": "1.1.60",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",