@datalayer/core 1.2.23 → 1.2.24

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/README.md CHANGED
@@ -100,10 +100,11 @@ Use `agent-runtimes` for runtime workloads (`RuntimeClient`, `@datalayer`, snaps
100
100
 
101
101
  ## Examples
102
102
 
103
- Examples have moved to Agent Runtimes:
103
+ The [examples](./examples/README.md) show the Datalayer Client at work:
104
104
 
105
- - [Agent Runtimes examples README](https://github.com/datalayer/agent-runtimes/blob/main/examples/README.md)
106
- - [Agent Runtimes examples directory](https://github.com/datalayer/agent-runtimes/tree/main/examples)
105
+ - [OpenTelemetry observability](./examples/otel/README.md): instrument Datalayer usage with OpenTelemetry traces and metrics, with a small FastAPI service and a React UI to visualize them (`make` targets in [examples/otel](./examples/otel)).
106
+
107
+ For agents, chats and sandboxes built on the client, see the [Agent Runtimes examples](https://github.com/datalayer/agent-runtimes/tree/main/examples).
107
108
 
108
109
  ## Platform Integration
109
110
 
@@ -0,0 +1,13 @@
1
+ export type CopyToClipboardButtonProps = {
2
+ value: string;
3
+ ariaLabel?: string;
4
+ successText?: string;
5
+ errorText?: string;
6
+ direction?: 'n' | 's' | 'e' | 'w' | 'nw' | 'ne' | 'sw' | 'se';
7
+ disabled?: boolean;
8
+ size?: 'small' | 'medium';
9
+ variant?: 'default' | 'invisible' | 'primary' | 'danger';
10
+ onResult?: (copied: boolean) => void;
11
+ };
12
+ export declare function CopyToClipboardButton({ value, ariaLabel, successText, errorText, direction, disabled, size, variant, onResult, }: CopyToClipboardButtonProps): import("react").JSX.Element;
13
+ export default CopyToClipboardButton;
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2023-2025 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ /**
7
+ * A button that copies a value, its tooltip saying what it copies, then
8
+ * whether it did.
9
+ *
10
+ * @module components/buttons/CopyToClipboardButton
11
+ */
12
+ import { useMemo, useState } from 'react';
13
+ import { IconButton, Tooltip } from '@primer/react';
14
+ import { CopyIcon } from '@primer/octicons-react';
15
+ const ellipsize = (value, max = 96) => {
16
+ if (!value) {
17
+ return '';
18
+ }
19
+ if (value.length <= max) {
20
+ return value;
21
+ }
22
+ const head = Math.floor((max - 1) / 2);
23
+ const tail = max - head - 1;
24
+ return `${value.slice(0, head)}…${value.slice(-tail)}`;
25
+ };
26
+ export function CopyToClipboardButton({ value, ariaLabel = 'Copy to clipboard', successText = 'Copied', errorText = 'Unable to copy', direction = 'n', disabled = false, size = 'small', variant = 'invisible', onResult, }) {
27
+ const [status, setStatus] = useState('idle');
28
+ const hasValue = Boolean(value && value.trim());
29
+ const tooltip = useMemo(() => {
30
+ if (status === 'success') {
31
+ return successText;
32
+ }
33
+ if (status === 'error') {
34
+ return errorText;
35
+ }
36
+ return hasValue ? `Copy: ${ellipsize(value)}` : 'Nothing to copy';
37
+ }, [status, successText, errorText, hasValue, value]);
38
+ const handleClick = async () => {
39
+ if (!hasValue || disabled) {
40
+ return;
41
+ }
42
+ try {
43
+ if (!navigator?.clipboard?.writeText) {
44
+ throw new Error('Clipboard API unavailable');
45
+ }
46
+ await navigator.clipboard.writeText(value);
47
+ setStatus('success');
48
+ onResult?.(true);
49
+ window.setTimeout(() => setStatus('idle'), 1500);
50
+ }
51
+ catch {
52
+ setStatus('error');
53
+ onResult?.(false);
54
+ window.setTimeout(() => setStatus('idle'), 1800);
55
+ }
56
+ };
57
+ const isDisabled = disabled || !hasValue;
58
+ const button = (_jsx(IconButton, { "aria-label": ariaLabel, icon: CopyIcon, size: size, variant: variant, disabled: isDisabled, onClick: handleClick }));
59
+ /*
60
+ * A disabled button wears no tooltip.
61
+ *
62
+ * `Tooltip` only takes interactive content — `button:not([disabled])` — and
63
+ * throws on anything else, which is what a disabled button is. Nothing is
64
+ * lost by leaving it out: a disabled element fires no pointer event, so the
65
+ * tooltip would never have shown. `IconButton` does the same with the one
66
+ * it renders of its own.
67
+ */
68
+ if (isDisabled) {
69
+ return button;
70
+ }
71
+ return (_jsx(Tooltip, { text: tooltip, direction: direction, children: button }));
72
+ }
73
+ export default CopyToClipboardButton;
@@ -1,3 +1,4 @@
1
+ export * from './CopyToClipboardButton';
1
2
  export * from './DownloadCSVButton';
2
3
  export * from './DownloadJsonButton';
3
4
  export * from './LongActionButton';
@@ -2,6 +2,7 @@
2
2
  * Copyright (c) 2023-2025 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ export * from './CopyToClipboardButton.js';
5
6
  export * from './DownloadCSVButton.js';
6
7
  export * from './DownloadJsonButton.js';
7
8
  export * from './LongActionButton.js';
@@ -0,0 +1,2 @@
1
+ /** The value a deck's `model_s` holds, or `undefined` when it holds none. */
2
+ export declare const parseDeckSpec: (text: unknown) => unknown;
@@ -0,0 +1,22 @@
1
+ /*
2
+ * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * A deck's specification, as Spacer serves it: YAML.
7
+ *
8
+ * `GET /decks/{id}` inlines the specification as `model_s`, the text Spacer
9
+ * stored — YAML, or JSON for a deck stored before the specification was
10
+ * YAML, which the same parser reads (JSON is YAML). Saving sends the parsed
11
+ * value back (`PUT /decks/{id}/model`), and Spacer writes it as YAML.
12
+ *
13
+ * @module hooks/deckSpec
14
+ */
15
+ import { parse } from 'yaml';
16
+ /** The value a deck's `model_s` holds, or `undefined` when it holds none. */
17
+ export const parseDeckSpec = (text) => {
18
+ if (typeof text !== 'string' || !text.trim()) {
19
+ return undefined;
20
+ }
21
+ return parse(text);
22
+ };
@@ -38,6 +38,27 @@ type IAnySpace = any;
38
38
  export type CacheProps = {
39
39
  loginRoute?: string;
40
40
  };
41
+ /** A deck's video, as Spacer keeps it with the deck (`GET /decks/{id}/video`). */
42
+ export type SavedDeckVideo = {
43
+ /** Signed: plays it for `expiresIn` seconds. */
44
+ url: string;
45
+ /** Signed: downloads it as `<deck name>.mp4`. */
46
+ downloadUrl: string;
47
+ /** Signed: its first frame, a PNG. */
48
+ posterUrl: string;
49
+ /** Its captions, as WebVTT text. */
50
+ captions: string;
51
+ width: number;
52
+ height: number;
53
+ bytes: number;
54
+ madeAt: string;
55
+ madeBy: string;
56
+ /** When the deck was last edited, if it says. */
57
+ editedAt: string;
58
+ /** The deck has been edited since the video was made. */
59
+ stale: boolean;
60
+ expiresIn: number;
61
+ };
41
62
  export type ISearchOpts = {
42
63
  q: string;
43
64
  types: string[];
@@ -282,6 +303,8 @@ export declare const queryKeys: {
282
303
  readonly detail: (id: string) => readonly ["decks", "detail", string];
283
304
  readonly bySpace: (spaceId: string) => readonly ["decks", "space", string];
284
305
  readonly model: (deckId: string) => readonly ["decks", "detail", string, "model"];
306
+ /** Under the detail: saving the deck refreshes whether it is stale. */
307
+ readonly video: (deckId: string) => readonly ["decks", "detail", string, "video"];
285
308
  };
286
309
  readonly cells: {
287
310
  readonly all: () => readonly ["cells"];
@@ -803,6 +826,15 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
803
826
  spec: unknown;
804
827
  }, unknown>;
805
828
  useCloneDeck: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
829
+ useSavedDeckVideo: (deckId: string) => import("@tanstack/react-query").UseQueryResult<SavedDeckVideo | null, Error>;
830
+ useSaveDeckVideo: () => import("@tanstack/react-query").UseMutationResult<SavedDeckVideo, Error, {
831
+ deckId: string;
832
+ video: Blob;
833
+ captions: string;
834
+ poster: Blob;
835
+ spec: unknown;
836
+ }, unknown>;
837
+ useDeleteDeckVideo: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
806
838
  useDocument: (documentId: string) => import("@tanstack/react-query").UseQueryResult<any, Error>;
807
839
  useDocumentsBySpace: (spaceId: string) => import("@tanstack/react-query").UseQueryResult<any, Error>;
808
840
  useCreateDocument: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
@@ -958,6 +990,12 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
958
990
  data?: string;
959
991
  }, unknown>;
960
992
  useResetItemImage: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
993
+ useSetItemVideo: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
994
+ itemId: string;
995
+ video: Blob;
996
+ captions: string;
997
+ poster: Blob;
998
+ }, unknown>;
961
999
  useCountItemReuse: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
962
1000
  itemId: string;
963
1001
  kind: "clone" | "instantiate";
@@ -975,6 +1013,14 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
975
1013
  ref?: string;
976
1014
  data?: string;
977
1015
  };
1016
+ /**
1017
+ * Publish a deck *as a video*: the video saved with it in its space
1018
+ * becomes the published deck's video — its page plays it and its
1019
+ * shared link announces it (`og:video`). The library copies it in
1020
+ * the same request that publishes, and refuses before publishing a
1021
+ * deck that has none.
1022
+ */
1023
+ video?: boolean;
978
1024
  /**
979
1025
  * The publisher's own words for their work, which is what somebody
980
1026
  * else will search for.
@@ -1415,6 +1461,8 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
1415
1461
  readonly detail: (id: string) => readonly ["decks", "detail", string];
1416
1462
  readonly bySpace: (spaceId: string) => readonly ["decks", "space", string];
1417
1463
  readonly model: (deckId: string) => readonly ["decks", "detail", string, "model"];
1464
+ /** Under the detail: saving the deck refreshes whether it is stale. */
1465
+ readonly video: (deckId: string) => readonly ["decks", "detail", string, "video"];
1418
1466
  };
1419
1467
  readonly cells: {
1420
1468
  readonly all: () => readonly ["cells"];
@@ -39,6 +39,7 @@
39
39
  import { useQuery, useQueries, useMutation, useQueryClient, } from '@tanstack/react-query';
40
40
  import { BOOTSTRAP_USER_ONBOARDING, asContact, asInvite, asOrganization, asSecret, asTeam, asToken, asUser, } from '../models/index.js';
41
41
  import { asSpace } from './cacheConverters.js';
42
+ import { parseDeckSpec } from './deckSpec.js';
42
43
  import { libraryAppFieldsOf } from './libraryApp.js';
43
44
  import { unlinkIAMProvider } from './unlinkIAMProvider.js';
44
45
  import { useCoreStore, useIAMStore, profileStore } from '../state/index.js';
@@ -368,6 +369,8 @@ export const queryKeys = {
368
369
  detail: (id) => [...queryKeys.decks.details(), id],
369
370
  bySpace: (spaceId) => [...queryKeys.decks.all(), 'space', spaceId],
370
371
  model: (deckId) => [...queryKeys.decks.detail(deckId), 'model'],
372
+ /** Under the detail: saving the deck refreshes whether it is stale. */
373
+ video: (deckId) => [...queryKeys.decks.detail(deckId), 'video'],
371
374
  },
372
375
  // Cells
373
376
  cells: {
@@ -721,6 +724,10 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
721
724
  meta.publisher = raw.publisher;
722
725
  if (raw.image !== undefined)
723
726
  meta.image = raw.image;
727
+ // A deck's video, made by its owner: `url`, `captionsUrl`, `posterUrl`,
728
+ // `width`, `height`. Absent when it has none.
729
+ if (raw.video)
730
+ meta.video = raw.video;
724
731
  /*
725
732
  * Where the artifact can be opened for editing.
726
733
  *
@@ -1074,7 +1081,8 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1074
1081
  name: raw.name_t,
1075
1082
  description: raw.description_t,
1076
1083
  tags: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
1077
- spec: raw.model_s ? JSON.parse(raw.model_s) : undefined,
1084
+ // YAML, as Spacer stores it (a deck stored as JSON reads the same).
1085
+ spec: parseDeckSpec(raw.model_s),
1078
1086
  public: raw.is_public_b ?? false,
1079
1087
  creationDate: new Date(raw.creation_ts_dt),
1080
1088
  lastUpdateDate: raw.last_update_ts_dt
@@ -2628,6 +2636,77 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2628
2636
  },
2629
2637
  });
2630
2638
  };
2639
+ /**
2640
+ * The video saved with a deck in its space, or `null` when it has none.
2641
+ *
2642
+ * Signed URLs that read it for a few hours (`url`, `posterUrl`, and
2643
+ * `downloadUrl`, an attachment named after the deck), its captions inline
2644
+ * (WebVTT), its size, when it was made, and `stale` — the deck has been
2645
+ * edited since: its spec is not the one the video was made from.
2646
+ */
2647
+ const useSavedDeckVideo = (deckId) => {
2648
+ return useQuery({
2649
+ queryKey: queryKeys.decks.video(deckId),
2650
+ queryFn: async () => {
2651
+ const resp = await requestDatalayer({
2652
+ url: `${configuration.spacerUrl}/api/spacer/v1/decks/${encodeURIComponent(deckId)}/video`,
2653
+ method: 'GET',
2654
+ });
2655
+ if (!resp.success) {
2656
+ throw new Error(resp.message || 'Failed to read the deck video');
2657
+ }
2658
+ return (resp.video ?? null);
2659
+ },
2660
+ ...DEFAULT_QUERY_OPTIONS,
2661
+ enabled: !!deckId,
2662
+ });
2663
+ };
2664
+ /**
2665
+ * Keep the video the deck engine made of a deck with the deck, in its
2666
+ * space, replacing the one it had: the MP4, its WebVTT captions, its
2667
+ * poster, and the spec it was made from.
2668
+ */
2669
+ const useSaveDeckVideo = () => {
2670
+ return useMutation({
2671
+ mutationFn: async ({ deckId, video, captions, poster, spec, }) => {
2672
+ const body = new FormData();
2673
+ body.append('video', video, 'video.mp4');
2674
+ body.append('captions', new Blob([captions], { type: 'text/vtt' }), 'captions.vtt');
2675
+ body.append('poster', poster, 'poster.png');
2676
+ body.append('spec', JSON.stringify(spec));
2677
+ const resp = await requestDatalayer({
2678
+ url: `${configuration.spacerUrl}/api/spacer/v1/decks/${encodeURIComponent(deckId)}/video`,
2679
+ method: 'PUT',
2680
+ body,
2681
+ });
2682
+ if (!resp.success) {
2683
+ throw new Error(resp.message || 'Failed to save the video');
2684
+ }
2685
+ return resp.video;
2686
+ },
2687
+ onSuccess: (video, { deckId }) => {
2688
+ queryClient.setQueryData(queryKeys.decks.video(deckId), video);
2689
+ },
2690
+ });
2691
+ };
2692
+ /** The deck without its saved video. */
2693
+ const useDeleteDeckVideo = () => {
2694
+ return useMutation({
2695
+ mutationFn: async (deckId) => {
2696
+ const resp = await requestDatalayer({
2697
+ url: `${configuration.spacerUrl}/api/spacer/v1/decks/${encodeURIComponent(deckId)}/video`,
2698
+ method: 'DELETE',
2699
+ });
2700
+ if (!resp.success) {
2701
+ throw new Error(resp.message || 'Failed to remove the video');
2702
+ }
2703
+ return resp;
2704
+ },
2705
+ onSuccess: (_, deckId) => {
2706
+ queryClient.setQueryData(queryKeys.decks.video(deckId), null);
2707
+ },
2708
+ });
2709
+ };
2631
2710
  /**
2632
2711
  * Clone notebook
2633
2712
  */
@@ -6915,6 +6994,31 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
6915
6994
  onSuccess: invalidateArtifactVisibility,
6916
6995
  });
6917
6996
  };
6997
+ /**
6998
+ * A published deck's video, made in its owner's browser by the deck
6999
+ * engine: the MP4, its WebVTT captions and its poster (the first slide).
7000
+ * Making it again replaces it.
7001
+ */
7002
+ const useSetItemVideo = () => {
7003
+ return useMutation({
7004
+ mutationFn: async ({ itemId, video, captions, poster, }) => {
7005
+ const body = new FormData();
7006
+ body.append('video', video, 'video.mp4');
7007
+ body.append('captions', new Blob([captions], { type: 'text/vtt' }), 'captions.vtt');
7008
+ body.append('poster', poster, 'poster.png');
7009
+ const resp = await requestDatalayer({
7010
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/video`,
7011
+ method: 'PUT',
7012
+ body,
7013
+ });
7014
+ if (!resp.success) {
7015
+ throw new Error(resp.message || 'Failed to set the video');
7016
+ }
7017
+ return resp.video;
7018
+ },
7019
+ onSuccess: invalidateArtifactVisibility,
7020
+ });
7021
+ };
6918
7022
  /** Publish a public artifact natively to LinkedIn through Library's saga. */
6919
7023
  const usePublishLinkedInShare = () => {
6920
7024
  return useMutation({
@@ -7131,7 +7235,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
7131
7235
  const usePublishArtifact = () => {
7132
7236
  const setImage = useSetItemImage();
7133
7237
  return useMutation({
7134
- mutationFn: async ({ itemId, image, tags, description, }) => {
7238
+ mutationFn: async ({ itemId, image, tags, description, video, }) => {
7135
7239
  /*
7136
7240
  * Publish first, then dress it.
7137
7241
  *
@@ -7156,6 +7260,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
7156
7260
  // words must not be able to clear them by omission.
7157
7261
  ...(tags === undefined ? {} : { tags }),
7158
7262
  ...(description === undefined ? {} : { description }),
7263
+ ...(video ? { video: true } : {}),
7159
7264
  },
7160
7265
  });
7161
7266
  if (!resp.success) {
@@ -8584,6 +8689,9 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
8584
8689
  useUpdateDeck,
8585
8690
  useUpdateDeckModel,
8586
8691
  useCloneDeck,
8692
+ useSavedDeckVideo,
8693
+ useSaveDeckVideo,
8694
+ useDeleteDeckVideo,
8587
8695
  // Documents
8588
8696
  useDocument,
8589
8697
  useDocumentsBySpace,
@@ -8655,6 +8763,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
8655
8763
  useSetFeaturedRank,
8656
8764
  useSetItemImage,
8657
8765
  useResetItemImage,
8766
+ useSetItemVideo,
8658
8767
  useCountItemReuse,
8659
8768
  useSyncArtifactCatalogue,
8660
8769
  usePublishArtifact,
@@ -301,8 +301,22 @@ export function viewForRoute(route, current) {
301
301
  if (path === '/apps' || path.startsWith('/apps/')) {
302
302
  return 'apps';
303
303
  }
304
+ // A space's decks and teaching pages are drawn beside the Agentify sidebar:
305
+ // filed under the Platform, the Platform button came back to a deck with
306
+ // the Agentify sidebar beside it (reported 23 October 2026).
307
+ if (AGENTIFY_SPACE_PAGE.test(path) || AGENTIFY_ACCOUNT_PAGE.test(path)) {
308
+ return 'agentify';
309
+ }
304
310
  return 'home';
305
311
  }
312
+ /**
313
+ * `/<account>/<space>/<kind>[/…]` for the kinds a space shows beside the
314
+ * Agentify sidebar — decks, courses and what a course is made of. The
315
+ * documentation (`/docs/…`) is never an account.
316
+ */
317
+ const AGENTIFY_SPACE_PAGE = /^\/(?!docs\/)[^/]+\/[^/]+\/(?:decks|courses|course|lessons|exercises|assignments)(?:\/|$)/;
318
+ /** `/<account>/courses`: an account's courses, beside the Agentify sidebar. */
319
+ const AGENTIFY_ACCOUNT_PAGE = /^\/(?!docs\/)[^/]+\/courses$/;
306
320
  /** Whether a page may be the one a shell returns to. */
307
321
  function isRouteOfView(route, view) {
308
322
  return (typeof route === 'string' &&
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Who an account is, to be read: its avatar, then its username, display
3
+ * name, initials, origin and identifier, the identifier with a button that
4
+ * copies it.
5
+ *
6
+ * It is given the identity and fetches nothing, so any host draws it from
7
+ * what it already holds: the web's profile page beside its form, VS Code's
8
+ * Profile view from its signed-in user.
9
+ *
10
+ * @module views/profile/UserIdentity
11
+ */
12
+ import type { JSX } from 'react';
13
+ /** What {@link UserIdentity} shows of an account. */
14
+ export interface UserIdentityProfile {
15
+ /** The account's identifier. */
16
+ id?: string;
17
+ username: string;
18
+ display_name: string;
19
+ initials: string;
20
+ /** Its photograph, when it has one. */
21
+ avatar_url?: string;
22
+ /** Its chosen picture, which wins over the photograph. */
23
+ avatar_icon?: string;
24
+ /** Where the account comes from (the identity provider). */
25
+ origin?: string;
26
+ }
27
+ export interface UserIdentityProps {
28
+ identity: UserIdentityProfile;
29
+ /** Whether the identifier is shown. @default true */
30
+ displayId?: boolean;
31
+ /**
32
+ * Whether the display name and the initials are shown. They are computed
33
+ * from the first and the last name, so a host showing the form that edits
34
+ * those states them there instead. @default true
35
+ */
36
+ displayComputed?: boolean;
37
+ /** Rendered under the avatar, typically the button changing it. */
38
+ avatarAction?: JSX.Element;
39
+ }
40
+ export declare const UserIdentity: ({ identity, displayId, displayComputed, avatarAction, }: UserIdentityProps) => JSX.Element;
41
+ export default UserIdentity;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text } from '@primer/react';
3
+ import { Box } from '@datalayer/primer-addons';
4
+ import { UserAvatar } from '../../components/avatars/index.js';
5
+ import { CopyToClipboardButton } from '../../components/buttons/index.js';
6
+ /** One row of the list: its label, then its value. */
7
+ const Row = ({ label, children, }) => (_jsxs(_Fragment, { children: [_jsx(Box, { p: 1, children: _jsx(Text, { style: { fontWeight: 'bold' }, children: label }) }), _jsx(Box, { p: 1, display: "flex", alignItems: "center", gap: 1, children: typeof children === 'string' ? _jsx(Text, { children: children }) : children })] }));
8
+ export const UserIdentity = ({ identity, displayId = true, displayComputed = true, avatarAction, }) => (_jsxs(Box, { children: [_jsxs(Box, { display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 2, children: [_jsx(UserAvatar, { avatarUrl: identity.avatar_url, avatar: identity.avatar_icon, size: 100 }), avatarAction] }), _jsxs(Box, { display: "grid", gridTemplateColumns: "1fr 2fr", rowGap: 3, columnGap: 3, maxWidth: 500, mt: 3, children: [_jsx(Row, { label: "Username", children: identity.username }), displayComputed && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Display name", children: identity.display_name }), _jsx(Row, { label: "Initials", children: identity.initials })] })), identity.origin && _jsx(Row, { label: "Origin", children: identity.origin }), displayId && identity.id && (_jsx(Row, { label: "UID", children: _jsxs(_Fragment, { children: [_jsx(Text, { children: identity.id }), _jsx(CopyToClipboardButton, { value: identity.id, ariaLabel: "Copy the identifier of the account" })] }) }))] })] }));
9
+ export default UserIdentity;
@@ -1,2 +1,4 @@
1
1
  export { UserBadge } from './UserBadge';
2
2
  export type { UserBadgeProps } from './UserBadge';
3
+ export { UserIdentity } from './UserIdentity';
4
+ export type { UserIdentityProfile, UserIdentityProps } from './UserIdentity';
@@ -3,3 +3,4 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  export { UserBadge } from './UserBadge.js';
6
+ export { UserIdentity } from './UserIdentity.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/core",
3
- "version": "1.2.23",
3
+ "version": "1.2.24",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",
@@ -177,6 +177,7 @@
177
177
  "usehooks-ts": "3.1.1",
178
178
  "uuid": "^13.0.0",
179
179
  "validator": "^13.7.0",
180
+ "yaml": "^2.9.1",
180
181
  "zod": "^4.1.13",
181
182
  "zod-to-json-schema": "^3.25.0",
182
183
  "zustand": "^4.4.1"