@datalayer/primer-addons 1.0.21 → 1.0.23

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.
Files changed (100) hide show
  1. package/README.md +14 -0
  2. package/lib/components/appearance/AppearanceMenuWithStore.d.ts +26 -0
  3. package/lib/components/appearance/AppearanceMenuWithStore.js +21 -0
  4. package/lib/components/appearance/CollaboratorPalette.d.ts +65 -0
  5. package/lib/components/appearance/CollaboratorPalette.js +160 -0
  6. package/lib/components/appearance/index.d.ts +2 -0
  7. package/lib/components/appearance/index.js +2 -0
  8. package/lib/components/box/Box.d.ts +17 -5
  9. package/lib/components/box/Box.js +4 -7
  10. package/lib/components/box/sx.js +1 -2
  11. package/lib/components/calendar-picker/CalendarPicker.d.ts +35 -0
  12. package/lib/components/calendar-picker/CalendarPicker.js +146 -0
  13. package/lib/components/calendar-picker/index.d.ts +1 -0
  14. package/lib/components/calendar-picker/index.js +5 -0
  15. package/lib/components/card/Card.d.ts +6 -1
  16. package/lib/components/card/Card.js +1 -1
  17. package/lib/components/closeable-flash/CloseableFlash.d.ts +23 -1
  18. package/lib/components/closeable-flash/CloseableFlash.js +52 -4
  19. package/lib/components/color/ColorSwatch.d.ts +1 -1
  20. package/lib/components/color-picker/ColorPicker.d.ts +97 -0
  21. package/lib/components/color-picker/ColorPicker.js +358 -0
  22. package/lib/components/color-picker/index.d.ts +1 -0
  23. package/lib/components/color-picker/index.js +5 -0
  24. package/lib/components/content-loader/ContentLoader.d.ts +1 -1
  25. package/lib/components/content-loader/ContentLoader.js +4 -0
  26. package/lib/components/content-loader/ContentLoader.stories.d.ts +1 -1
  27. package/lib/components/date-picker/DatePicker.d.ts +81 -0
  28. package/lib/components/date-picker/DatePicker.js +204 -0
  29. package/lib/components/date-picker/TimeColumns.d.ts +23 -0
  30. package/lib/components/date-picker/TimeColumns.js +88 -0
  31. package/lib/components/date-picker/index.d.ts +1 -0
  32. package/lib/components/date-picker/index.js +5 -0
  33. package/lib/components/icons/CircleIcon.d.ts +1 -1
  34. package/lib/components/icons/CircleIcon.js +4 -0
  35. package/lib/components/icons/CircleIcon.stories.d.ts +1 -1
  36. package/lib/components/index.d.ts +5 -0
  37. package/lib/components/index.js +9 -0
  38. package/lib/components/logo/DatalayerText.d.ts +1 -1
  39. package/lib/components/logo/index.d.ts +0 -1
  40. package/lib/components/logo/index.js +4 -1
  41. package/lib/components/side-overlay/SideOverlay.d.ts +5 -5
  42. package/lib/components/side-overlay/SideOverlay.js +4 -0
  43. package/lib/components/side-overlay/SideOverlay.stories.d.ts +1 -1
  44. package/lib/components/side-overlay/SideOverlay.stories.js +4 -0
  45. package/lib/components/slider/Slider.d.ts +1 -1
  46. package/lib/components/slider/Slider.js +4 -0
  47. package/lib/components/slider/Slider.stories.d.ts +1 -1
  48. package/lib/components/slider/Slider.stories.js +4 -0
  49. package/lib/components/sliding-panel/SlidingPanel.d.ts +1 -1
  50. package/lib/components/sliding-panel/SlidingPanel.js +17 -1
  51. package/lib/components/sliding-panel/SlidingPanel.stories.d.ts +1 -1
  52. package/lib/components/sliding-panel/SlidingPanel.stories.js +4 -0
  53. package/lib/components/toolbar/Toolbar.d.ts +2 -2
  54. package/lib/components/toolbar/Toolbar.js +0 -0
  55. package/lib/components/toolbar/ToolbarButton.d.ts +3 -1
  56. package/lib/components/toolbar/ToolbarButton.js +54 -22
  57. package/lib/components/toolbar/ToolbarDivider.d.ts +1 -1
  58. package/lib/components/toolbar/ToolbarDropdown.d.ts +3 -3
  59. package/lib/components/toolbar/ToolbarDropdown.js +66 -35
  60. package/lib/components/toolbar/ToolbarRenderer.d.ts +20 -3
  61. package/lib/components/toolbar/ToolbarRenderer.js +16 -16
  62. package/lib/components/toolbar/types.d.ts +8 -8
  63. package/lib/index.js +4 -0
  64. package/lib/reactor/AppearancePlugin.d.ts +31 -0
  65. package/lib/reactor/AppearancePlugin.js +63 -0
  66. package/lib/reactor/ThemePlugin.d.ts +5 -0
  67. package/lib/reactor/ThemePlugin.js +98 -0
  68. package/lib/reactor/index.d.ts +30 -0
  69. package/lib/reactor/index.js +34 -0
  70. package/lib/reactor/page-layout/PageLayout.d.ts +148 -0
  71. package/lib/reactor/page-layout/PageLayout.js +326 -0
  72. package/lib/reactor/page-layout/PageLayoutPlugin.d.ts +59 -0
  73. package/lib/reactor/page-layout/PageLayoutPlugin.js +105 -0
  74. package/lib/reactor/page-layout/PagePanelToggle.d.ts +21 -0
  75. package/lib/reactor/page-layout/PagePanelToggle.js +29 -0
  76. package/lib/reactor/page-layout/SlotPageLayout.d.ts +36 -0
  77. package/lib/reactor/page-layout/SlotPageLayout.js +25 -0
  78. package/lib/reactor/page-layout/panelState.d.ts +28 -0
  79. package/lib/reactor/page-layout/panelState.js +49 -0
  80. package/lib/story-helpers.d.ts +2 -2
  81. package/lib/theme/DatalayerBrandThemeProvider.d.ts +1 -1
  82. package/lib/theme/DatalayerThemeProvider.d.ts +5 -5
  83. package/lib/theme/DatalayerThemeProvider.js +119 -58
  84. package/lib/theme/css/createThemeCSSVars.d.ts +1 -0
  85. package/lib/theme/css/createThemeCSSVars.js +9 -0
  86. package/lib/theme/fontStacks.d.ts +2 -0
  87. package/lib/theme/fontStacks.js +6 -0
  88. package/lib/theme/index.d.ts +2 -0
  89. package/lib/theme/index.js +2 -0
  90. package/lib/theme/palettes/ColorPalette.d.ts +3 -3
  91. package/lib/theme/palettes/ColorPalette.js +1 -2
  92. package/lib/theme/palettes/index.js +1 -2
  93. package/lib/theme/portableTheme.d.ts +32 -0
  94. package/lib/theme/portableTheme.js +45 -0
  95. package/lib/utils/Helper.d.ts +2 -2
  96. package/lib/utils/Portals.js +3 -1
  97. package/lib/utils/Styles.d.ts +1 -1
  98. package/package.json +21 -14
  99. package/lib/components/logo/DI.d.ts +0 -19
  100. package/lib/components/logo/DI.js +0 -20
@@ -0,0 +1,105 @@
1
+ /*
2
+ * Copyright (c) 2023-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * `@datalayer/primer-page-layout` — the page layout, as a reactor plugin.
7
+ *
8
+ * A host lists this plugin and renders one slot, `main` by default; the
9
+ * plugin fills it with {@link PageLayout} and opens four slots of its own for
10
+ * the other plugins to fill: `page` for what lies on the sheet, `page-band`
11
+ * for the band docked above it (a composer, a toolbar), `page-panel` for the
12
+ * side panel, `page-chips` for the chips under the band. A button that opens
13
+ * and closes the panel goes in the header — only while something fills the
14
+ * panel slot, so a host with no panel gets no dead button.
15
+ *
16
+ * This is the Loop's page layout with the Loop taken out: there the chat
17
+ * view hands the layout its parts directly through a contribution point of
18
+ * its own, and the Loop keeps that adapter. Here the parts are slots, which
19
+ * is what a plain Reactor host has.
20
+ *
21
+ * @module reactor/page-layout/PageLayoutPlugin
22
+ */
23
+ import { createElement } from "react";
24
+ import { definePlugin } from "@datalayer/reactor";
25
+ import { SlotPageLayout, SlotPanelToggle } from "./SlotPageLayout";
26
+ import { pageLayoutPanelOpen } from "./panelState";
27
+ export const PAGE_LAYOUT_PLUGIN_NAME = "@datalayer/primer-page-layout";
28
+ /** The slots the plugin opens, by their default names. */
29
+ export const PageLayoutSlots = {
30
+ page: "page",
31
+ band: "page-band",
32
+ panel: "page-panel",
33
+ chips: "page-chips",
34
+ };
35
+ export const PageLayoutPlugin = definePlugin({
36
+ name: PAGE_LAYOUT_PLUGIN_NAME,
37
+ displayName: "Page layout",
38
+ description: "The page on a centred sheet, a band docked above it at the same width or floating over it, and a side panel that opens from the header.",
39
+ octicon: "file",
40
+ emoji: "\u{1F4C4}",
41
+ config: {
42
+ slot: "main",
43
+ order: 0,
44
+ headerSlot: "header",
45
+ toggleOrder: 50,
46
+ panelName: "panel",
47
+ toggleIcon: undefined,
48
+ pageSlot: PageLayoutSlots.page,
49
+ bandSlot: PageLayoutSlots.band,
50
+ panelSlot: PageLayoutSlots.panel,
51
+ chipsSlot: PageLayoutSlots.chips,
52
+ band: "docked",
53
+ panel: "docked",
54
+ panelSide: "right",
55
+ panelOpen: false,
56
+ sheetWidth: undefined,
57
+ pageSize: undefined,
58
+ panelWidth: undefined,
59
+ },
60
+ build: ({ config }) => {
61
+ // A host whose panel is a navigation sidebar wants it open on arrival,
62
+ // not behind the header's toggle. Set once, at build: after that the
63
+ // signal is the reader's.
64
+ if (config.panelOpen) {
65
+ pageLayoutPanelOpen.value = true;
66
+ }
67
+ return {
68
+ components: [
69
+ {
70
+ id: "page-layout",
71
+ slot: config.slot,
72
+ order: config.order,
73
+ Component: (context) => createElement(SlotPageLayout, {
74
+ pageSlot: config.pageSlot,
75
+ bandSlot: config.bandSlot,
76
+ panelSlot: config.panelSlot,
77
+ chipsSlot: config.chipsSlot,
78
+ bandMode: config.band,
79
+ panelMode: config.panel,
80
+ panelSide: config.panelSide,
81
+ sheetWidth: config.sheetWidth,
82
+ pageSize: config.pageSize,
83
+ panelWidth: config.panelWidth,
84
+ context,
85
+ }),
86
+ },
87
+ ...(config.headerSlot
88
+ ? [
89
+ {
90
+ id: "page-layout-panel-toggle",
91
+ slot: config.headerSlot,
92
+ order: config.toggleOrder,
93
+ Component: () => createElement(SlotPanelToggle, {
94
+ panelSlot: config.panelSlot,
95
+ panelName: config.panelName,
96
+ icon: config.toggleIcon,
97
+ }),
98
+ },
99
+ ]
100
+ : []),
101
+ ],
102
+ };
103
+ },
104
+ });
105
+ export default PageLayoutPlugin;
@@ -0,0 +1,21 @@
1
+ import type { JSX } from "react";
2
+ import { type SxProp } from "@primer/react";
3
+ import { type Icon } from "@primer/octicons-react";
4
+ export type PagePanelToggleProps = {
5
+ /** What the panel holds, for the label: "Show the conversation". */
6
+ panelName?: string;
7
+ /** The button's glyph. */
8
+ icon?: Icon;
9
+ /** Extra styles — an `order` among a header's items, say. */
10
+ sx?: SxProp["sx"];
11
+ };
12
+ /**
13
+ * The header button that opens and closes the page layout's side panel.
14
+ *
15
+ * It shares the layout's signals, so a host that opens the panel itself —
16
+ * `openPagePanel()` when something lands in it — sees the button follow.
17
+ * With the panel's content on the sheet there is nothing to open, and the
18
+ * button draws nothing.
19
+ */
20
+ export declare function PagePanelToggle({ panelName, icon, sx, }: PagePanelToggleProps): JSX.Element | null;
21
+ export default PagePanelToggle;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { IconButton } from "@primer/react";
3
+ import { SidebarExpandIcon } from "@primer/octicons-react";
4
+ import { useSignalValue } from "@datalayer/reactor/react";
5
+ import { openPagePanel, pageLayoutPanelOpen, pageLayoutSheet, } from "./panelState";
6
+ /**
7
+ * The header button that opens and closes the page layout's side panel.
8
+ *
9
+ * It shares the layout's signals, so a host that opens the panel itself —
10
+ * `openPagePanel()` when something lands in it — sees the button follow.
11
+ * With the panel's content on the sheet there is nothing to open, and the
12
+ * button draws nothing.
13
+ */
14
+ export function PagePanelToggle({ panelName = "panel", icon = SidebarExpandIcon, sx, }) {
15
+ const open = useSignalValue(pageLayoutPanelOpen);
16
+ const sheet = useSignalValue(pageLayoutSheet);
17
+ if (sheet === "panel") {
18
+ return null;
19
+ }
20
+ return (_jsx(IconButton, { icon: icon, size: "small", variant: open ? "default" : "invisible", "aria-label": open ? `Hide the ${panelName}` : `Show the ${panelName}`, "aria-pressed": open, onClick: () => {
21
+ if (open) {
22
+ pageLayoutPanelOpen.value = false;
23
+ }
24
+ else {
25
+ openPagePanel();
26
+ }
27
+ }, sx: { color: open ? "accent.fg" : "fg.muted", ...sx } }));
28
+ }
29
+ export default PagePanelToggle;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The page layout fed from slots, for a host whose parts arrive as plugins.
3
+ *
4
+ * The layout component takes elements; a Reactor host has slot components.
5
+ * This reads four slots — the page, the band, the panel, the chips — and
6
+ * hands each to the layout as a `ReactorSlot`, or as nothing when no plugin
7
+ * filled it, so an empty band draws no band and an empty panel offers no
8
+ * toggle. Whatever the host passed to the layout's own slot goes on to every
9
+ * sub-slot, the way a slot's props always reach its components.
10
+ *
11
+ * @module reactor/page-layout/SlotPageLayout
12
+ */
13
+ import type { JSX } from "react";
14
+ import type { Icon } from "@primer/octicons-react";
15
+ import { type PageLayoutProps } from "./PageLayout";
16
+ export type SlotPageLayoutProps = Pick<PageLayoutProps, "bandMode" | "panelMode" | "panelSide" | "sheetWidth" | "pageSize" | "panelWidth"> & {
17
+ pageSlot: string;
18
+ bandSlot: string;
19
+ panelSlot: string;
20
+ chipsSlot: string;
21
+ /** What the host handed the layout's slot; passed on to every sub-slot. */
22
+ context?: Record<string, unknown>;
23
+ };
24
+ export declare function SlotPageLayout({ pageSlot, bandSlot, panelSlot, chipsSlot, context, ...layout }: SlotPageLayoutProps): JSX.Element;
25
+ /**
26
+ * The panel toggle, drawn only while some plugin fills the panel slot.
27
+ *
28
+ * Its place in the header is the slot component's `order`, which the slot
29
+ * applies; nothing here sets a CSS order, which would reshuffle the row.
30
+ */
31
+ export declare function SlotPanelToggle({ panelSlot, panelName, icon, }: {
32
+ panelSlot: string;
33
+ panelName?: string;
34
+ icon?: Icon;
35
+ }): JSX.Element | null;
36
+ export default SlotPageLayout;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { ReactorSlot, useOptionalSlotComponents, } from "@datalayer/reactor/react";
3
+ import { PageLayout } from "./PageLayout";
4
+ import { PagePanelToggle } from "./PagePanelToggle";
5
+ export function SlotPageLayout({ pageSlot, bandSlot, panelSlot, chipsSlot, context, ...layout }) {
6
+ const hasPage = useOptionalSlotComponents(pageSlot).length > 0;
7
+ const hasBand = useOptionalSlotComponents(bandSlot).length > 0;
8
+ const hasPanel = useOptionalSlotComponents(panelSlot).length > 0;
9
+ const hasChips = useOptionalSlotComponents(chipsSlot).length > 0;
10
+ return (_jsx(PageLayout, { ...layout, page: hasPage ? _jsx(ReactorSlot, { slot: pageSlot, props: context }) : null, hasPage: hasPage, band: hasBand ? _jsx(ReactorSlot, { slot: bandSlot, props: context }) : null, panel: hasPanel ? _jsx(ReactorSlot, { slot: panelSlot, props: context }) : null, chips: hasChips ? _jsx(ReactorSlot, { slot: chipsSlot, props: context }) : null }));
11
+ }
12
+ /**
13
+ * The panel toggle, drawn only while some plugin fills the panel slot.
14
+ *
15
+ * Its place in the header is the slot component's `order`, which the slot
16
+ * applies; nothing here sets a CSS order, which would reshuffle the row.
17
+ */
18
+ export function SlotPanelToggle({ panelSlot, panelName, icon, }) {
19
+ const hasPanel = useOptionalSlotComponents(panelSlot).length > 0;
20
+ if (!hasPanel) {
21
+ return null;
22
+ }
23
+ return _jsx(PagePanelToggle, { panelName: panelName, icon: icon });
24
+ }
25
+ export default SlotPageLayout;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Whether the side panel is open.
3
+ *
4
+ * Closed to start: the page is what a reader came for, and a panel with
5
+ * nothing in it yet is a column of nothing.
6
+ */
7
+ export declare const pageLayoutPanelOpen: import("@datalayer/reactor").Signal<boolean>;
8
+ /**
9
+ * What lies on the sheet: the page, or — when there is no page to show — the
10
+ * panel's own content.
11
+ *
12
+ * Written by the layout as it renders, read by the parts that only make
13
+ * sense beside a page: the toggle would open a second copy of what is
14
+ * already on the sheet, and anything hung under a band to echo the panel
15
+ * would repeat it.
16
+ */
17
+ export declare const pageLayoutSheet: import("@datalayer/reactor").Signal<"page" | "panel">;
18
+ /**
19
+ * What is happening in the page, in a short line — "Analyst is adding a
20
+ * cell…" — or nothing. The layout pins it to the top of the sheet while it
21
+ * is set. A host or another plugin writes it; a layout given an `activity`
22
+ * prop shows that instead.
23
+ */
24
+ export declare const pageLayoutActivity: import("@datalayer/reactor").Signal<string | undefined>;
25
+ /** Open the panel — for a host that wants its content seen the moment it lands. */
26
+ export declare function openPagePanel(): void;
27
+ export declare function closePagePanel(): void;
28
+ export declare function togglePagePanel(): void;
@@ -0,0 +1,49 @@
1
+ /*
2
+ * Copyright (c) 2023-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The page layout's shared state: whether the side panel is open, what the
7
+ * sheet holds, and what is happening in the page.
8
+ *
9
+ * Signals, in a module of their own so the layout, the header toggle, the
10
+ * panel's own contents and a host can all read and write them without any
11
+ * of them importing the others.
12
+ *
13
+ * @module reactor/page-layout/panelState
14
+ */
15
+ import { signal } from "@datalayer/reactor";
16
+ /**
17
+ * Whether the side panel is open.
18
+ *
19
+ * Closed to start: the page is what a reader came for, and a panel with
20
+ * nothing in it yet is a column of nothing.
21
+ */
22
+ export const pageLayoutPanelOpen = signal(false);
23
+ /**
24
+ * What lies on the sheet: the page, or — when there is no page to show — the
25
+ * panel's own content.
26
+ *
27
+ * Written by the layout as it renders, read by the parts that only make
28
+ * sense beside a page: the toggle would open a second copy of what is
29
+ * already on the sheet, and anything hung under a band to echo the panel
30
+ * would repeat it.
31
+ */
32
+ export const pageLayoutSheet = signal("page");
33
+ /**
34
+ * What is happening in the page, in a short line — "Analyst is adding a
35
+ * cell…" — or nothing. The layout pins it to the top of the sheet while it
36
+ * is set. A host or another plugin writes it; a layout given an `activity`
37
+ * prop shows that instead.
38
+ */
39
+ export const pageLayoutActivity = signal(undefined);
40
+ /** Open the panel — for a host that wants its content seen the moment it lands. */
41
+ export function openPagePanel() {
42
+ pageLayoutPanelOpen.value = true;
43
+ }
44
+ export function closePagePanel() {
45
+ pageLayoutPanelOpen.value = false;
46
+ }
47
+ export function togglePagePanel() {
48
+ pageLayoutPanelOpen.value = !pageLayoutPanelOpen.value;
49
+ }
@@ -7,8 +7,8 @@ type StoryContext = Record<string, unknown> & {
7
7
  };
8
8
  parameters: Record<string, unknown>;
9
9
  };
10
- export declare const colormodeFromScheme: (colorScheme: string) => "dark" | "light";
11
- export declare const withThemeProvider: (Story: React.FC<React.PropsWithChildren<StoryContext>>, context: StoryContext) => string | number | boolean | Iterable<React.ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
10
+ export declare const colormodeFromScheme: (colorScheme: string) => "light" | "dark";
11
+ export declare const withThemeProvider: (Story: React.FC<React.PropsWithChildren<StoryContext>>, context: StoryContext) => string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<React.ReactNode> | React.JSX.Element | null | undefined;
12
12
  export declare const toolbarTypes: {
13
13
  colorScheme: {
14
14
  name: string;
@@ -38,4 +38,4 @@ export interface IDatalayerBrandThemeProviderProps {
38
38
  * </DatalayerBrandThemeProvider>
39
39
  * ```
40
40
  */
41
- export declare function DatalayerBrandThemeProvider({ colorMode, brandTheme, style, children, }: IDatalayerBrandThemeProviderProps): JSX.Element;
41
+ export declare function DatalayerBrandThemeProvider({ colorMode, brandTheme, style, children, }: IDatalayerBrandThemeProviderProps): React.JSX.Element;
@@ -1,13 +1,13 @@
1
- import { type CSSProperties } from 'react';
2
- import { ThemeProviderProps } from '@primer/react';
3
- export interface IDatalayerThemeProviderProps extends Omit<ThemeProviderProps, 'theme' | 'colorMode'> {
1
+ import { type CSSProperties } from "react";
2
+ import { ThemeProviderProps } from "@primer/react";
3
+ export interface IDatalayerThemeProviderProps extends Omit<ThemeProviderProps, "theme" | "colorMode"> {
4
4
  /**
5
5
  * Color mode to use.
6
6
  * - `'light'` / `'dark'` — explicit override
7
7
  * - `'auto'` — follow the operating system preference (prefers-color-scheme)
8
8
  * - Primer's `'day'` / `'night'` are still accepted.
9
9
  */
10
- colorMode?: 'light' | 'dark' | 'auto' | 'day' | 'night';
10
+ colorMode?: "light" | "dark" | "auto" | "day" | "night";
11
11
  /**
12
12
  * Additional base styles merged on top of theme defaults.
13
13
  */
@@ -33,4 +33,4 @@ export interface IDatalayerThemeProviderProps extends Omit<ThemeProviderProps, '
33
33
  dark: CSSProperties;
34
34
  };
35
35
  }
36
- export declare function DatalayerThemeProvider(props: React.PropsWithChildren<IDatalayerThemeProviderProps>): JSX.Element;
36
+ export declare function DatalayerThemeProvider(props: React.PropsWithChildren<IDatalayerThemeProviderProps>): React.JSX.Element;
@@ -1,20 +1,20 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /*
3
3
  * Copyright (c) 2023-2025 Datalayer, Inc.
4
4
  * Distributed under the terms of the Modified BSD License.
5
5
  */
6
- import { useEffect, useMemo } from 'react';
7
- import { BaseStyles, ThemeProvider } from '@primer/react';
8
- import { useSystemColorMode } from './useSystemColorMode';
9
- import { datalayerTheme, datalayerThemeStyles } from './themes/datalayerTheme';
10
- import { setupPrimerPortals, syncPortalThemeStyles } from '../utils/Portals';
6
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
7
+ import { BaseStyles, ThemeProvider } from "@primer/react";
8
+ import { useSystemColorMode } from "./useSystemColorMode";
9
+ import { datalayerTheme, datalayerThemeStyles } from "./themes/datalayerTheme";
10
+ import { systemFontStack } from "./fontStacks";
11
+ import { setupPrimerPortals, syncPortalThemeStyles } from "../utils/Portals";
11
12
  /**
12
13
  * System sans-serif font stack — shared between `fontFamily` and
13
14
  * the Primer `--fontStack-*` CSS custom properties so that
14
15
  * components using the CSS `font` shorthand (e.g. `Blankslate`)
15
16
  * also pick up the themed typeface.
16
17
  */
17
- const SYSTEM_FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"';
18
18
  /**
19
19
  * Shared typographic rhythm — clean, spacious feel inspired by
20
20
  * modern developer-blog aesthetics (generous line-height, open
@@ -31,70 +31,114 @@ const SYSTEM_FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, A
31
31
  */
32
32
  const typographyVars = {
33
33
  /* ── Font stacks ───────────────────────────────────────────────── */
34
- '--fontStack-monospace': 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace',
35
- '--fontStack-sansSerif': SYSTEM_FONT,
36
- '--fontStack-sansSerifDisplay': SYSTEM_FONT,
37
- '--fontStack-system': SYSTEM_FONT,
34
+ "--fontStack-monospace": "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace",
35
+ "--fontStack-sansSerif": systemFontStack,
36
+ "--fontStack-sansSerifDisplay": systemFontStack,
37
+ "--fontStack-system": systemFontStack,
38
38
  /* ── Base text weights (from @primer/primitives) ───────────────── */
39
- '--base-text-weight-light': '300',
40
- '--base-text-weight-normal': '400',
41
- '--base-text-weight-medium': '500',
42
- '--base-text-weight-semibold': '600',
39
+ "--base-text-weight-light": "300",
40
+ "--base-text-weight-normal": "400",
41
+ "--base-text-weight-medium": "500",
42
+ "--base-text-weight-semibold": "600",
43
43
  /* ── Body sizing ───────────────────────────────────────────────── */
44
- '--text-body-size-large': '1rem',
45
- '--text-body-size-medium': '0.875rem',
46
- '--text-body-size-small': '0.75rem',
47
- '--text-body-lineHeight-large': '1.5',
48
- '--text-body-lineHeight-medium': '1.4285',
49
- '--text-body-lineHeight-small': '1.6666',
44
+ "--text-body-size-large": "1rem",
45
+ "--text-body-size-medium": "0.875rem",
46
+ "--text-body-size-small": "0.75rem",
47
+ "--text-body-lineHeight-large": "1.5",
48
+ "--text-body-lineHeight-medium": "1.4285",
49
+ "--text-body-lineHeight-small": "1.6666",
50
50
  /* ── Title sizing ──────────────────────────────────────────────── */
51
- '--text-title-size-large': '2rem',
52
- '--text-title-size-medium': '1.25rem',
53
- '--text-title-size-small': '1rem',
54
- '--text-title-lineHeight-large': '1.5',
55
- '--text-title-lineHeight-medium': '1.6',
56
- '--text-title-lineHeight-small': '1.5',
51
+ "--text-title-size-large": "2rem",
52
+ "--text-title-size-medium": "1.25rem",
53
+ "--text-title-size-small": "1rem",
54
+ "--text-title-lineHeight-large": "1.5",
55
+ "--text-title-lineHeight-medium": "1.6",
56
+ "--text-title-lineHeight-small": "1.5",
57
57
  /* ── Caption / subtitle / display ──────────────────────────────── */
58
- '--text-caption-size': '0.75rem',
59
- '--text-caption-lineHeight': '1.3333',
60
- '--text-subtitle-size': '1.25rem',
61
- '--text-subtitle-lineHeight': '1.6',
62
- '--text-display-size': '2.5rem',
63
- '--text-display-lineHeight': '1.4',
58
+ "--text-caption-size": "0.75rem",
59
+ "--text-caption-lineHeight": "1.3333",
60
+ "--text-subtitle-size": "1.25rem",
61
+ "--text-subtitle-lineHeight": "1.6",
62
+ "--text-display-size": "2.5rem",
63
+ "--text-display-lineHeight": "1.4",
64
64
  /* ── Font shorthand tokens ─────────────────────────────────────── *
65
65
  * These are the tokens consumed by Primer components (Blankslate,
66
66
  * etc.) via `font: var(--text-title-shorthand-medium, <fallback>)`.
67
67
  * Defining them here prevents the hard-coded fallback from firing.
68
68
  * Each one references `var(--fontStack-sansSerif)` or
69
- * `SYSTEM_FONT` directly (no nested `var()`) so that the CSS
69
+ * the system font stack directly (no nested `var()`) so that the CSS
70
70
  * `font` shorthand parses reliably. Themes that override fonts
71
71
  * (e.g. Matrix monospace) replace these shorthand tokens via
72
72
  * `buildThemeStyles({ fontFamily })` in their `themeStyles`.
73
73
  * ─────────────────────────────────────────────────────────────── */
74
- '--text-body-shorthand-large': `400 1rem/1.5 ${SYSTEM_FONT}`,
75
- '--text-body-shorthand-medium': `400 0.875rem/1.4285 ${SYSTEM_FONT}`,
76
- '--text-body-shorthand-small': `400 0.75rem/1.6666 ${SYSTEM_FONT}`,
77
- '--text-title-shorthand-large': `600 2rem/1.5 ${SYSTEM_FONT}`,
78
- '--text-title-shorthand-medium': `600 1.25rem/1.6 ${SYSTEM_FONT}`,
79
- '--text-title-shorthand-small': `600 1rem/1.5 ${SYSTEM_FONT}`,
80
- '--text-caption-shorthand': `400 0.75rem/1.3333 ${SYSTEM_FONT}`,
81
- '--text-subtitle-shorthand': `400 1.25rem/1.6 ${SYSTEM_FONT}`,
82
- '--text-display-shorthand': `500 2.5rem/1.4 ${SYSTEM_FONT}`,
74
+ "--text-body-shorthand-large": `400 1rem/1.5 ${systemFontStack}`,
75
+ "--text-body-shorthand-medium": `400 0.875rem/1.4285 ${systemFontStack}`,
76
+ "--text-body-shorthand-small": `400 0.75rem/1.6666 ${systemFontStack}`,
77
+ "--text-title-shorthand-large": `600 2rem/1.5 ${systemFontStack}`,
78
+ "--text-title-shorthand-medium": `600 1.25rem/1.6 ${systemFontStack}`,
79
+ "--text-title-shorthand-small": `600 1rem/1.5 ${systemFontStack}`,
80
+ "--text-caption-shorthand": `400 0.75rem/1.3333 ${systemFontStack}`,
81
+ "--text-subtitle-shorthand": `400 1.25rem/1.6 ${systemFontStack}`,
82
+ "--text-display-shorthand": `500 2.5rem/1.4 ${systemFontStack}`,
83
83
  /* ── Custom overrides ──────────────────────────────────────────── */
84
- '--text-body-lineHeight': '1.7',
85
- '--text-title-lineHeight': '1.2',
86
- '--text-title-letterSpacing': '-0.02em',
87
- fontFamily: SYSTEM_FONT,
88
- WebkitFontSmoothing: 'antialiased',
89
- MozOsxFontSmoothing: 'grayscale',
90
- textRendering: 'optimizeLegibility',
84
+ "--text-body-lineHeight": "1.7",
85
+ "--text-title-lineHeight": "1.2",
86
+ "--text-title-letterSpacing": "-0.02em",
87
+ fontFamily: systemFontStack,
88
+ WebkitFontSmoothing: "antialiased",
89
+ MozOsxFontSmoothing: "grayscale",
90
+ textRendering: "optimizeLegibility",
91
91
  };
92
92
  export function DatalayerThemeProvider(props) {
93
93
  const { children, colorMode, baseStyles, theme, themeStyles, ...rest } = props;
94
94
  // Resolve 'auto' → actual system preference ('light' or 'dark').
95
95
  const systemMode = useSystemColorMode();
96
- const resolvedColorMode = colorMode === 'auto' ? systemMode : (colorMode ?? 'light');
97
- const isDark = resolvedColorMode === 'dark' || resolvedColorMode === 'night';
96
+ /*
97
+ * Whether this provider sits inside another themed element, and if so what
98
+ * that element says.
99
+ *
100
+ * The portal root is one element shared by the whole page, and every
101
+ * provider used to restamp it from its own mode — so a provider nested in
102
+ * the page (a deck in its own theme, a widget rendered into a notebook cell
103
+ * as a React root of its own) wrote its mode over the application's
104
+ * whenever it mounted, and the next overlay came up in the wrong colour.
105
+ * Nested providers leave the portal root alone. And a nested provider given
106
+ * no `colorMode` wears the mode above it rather than a default of light: a
107
+ * widget inside a dark page has no business being light. Read from the DOM
108
+ * rather than from React context, because those widget roots are separate
109
+ * React trees; what they share with the application is the document.
110
+ */
111
+ const sentinel = useRef(null);
112
+ const [themedAncestor, setThemedAncestor] = useState(null);
113
+ const [ancestorMode, setAncestorMode] = useState(null);
114
+ useLayoutEffect(() => {
115
+ // The sentinel's parent is this provider's own element; anything themed
116
+ // above *that* is another provider's.
117
+ const own = sentinel.current?.parentElement;
118
+ const ancestor = own?.parentElement?.closest("[data-color-mode]") ?? null;
119
+ setThemedAncestor(ancestor);
120
+ if (!ancestor) {
121
+ setAncestorMode(null);
122
+ return undefined;
123
+ }
124
+ const read = () => {
125
+ const mode = ancestor.getAttribute("data-color-mode");
126
+ setAncestorMode(mode === "dark" || mode === "night" ? "dark" : "light");
127
+ };
128
+ read();
129
+ // Rewritten on every toggle; follow it.
130
+ const observer = new MutationObserver(read);
131
+ observer.observe(ancestor, {
132
+ attributes: true,
133
+ attributeFilter: ["data-color-mode"],
134
+ });
135
+ return () => observer.disconnect();
136
+ }, []);
137
+ const nested = themedAncestor !== null;
138
+ const resolvedColorMode = colorMode === "auto"
139
+ ? systemMode
140
+ : (colorMode ?? (nested ? (ancestorMode ?? "light") : "light"));
141
+ const isDark = resolvedColorMode === "dark" || resolvedColorMode === "night";
98
142
  const resolvedTheme = theme ?? datalayerTheme;
99
143
  const styles = themeStyles ?? datalayerThemeStyles;
100
144
  const resolvedStyles = isDark ? styles.dark : styles.light;
@@ -109,15 +153,32 @@ export function DatalayerThemeProvider(props) {
109
153
  // Keep document.body portal-root attributes AND theme styles in sync
110
154
  // so that Primer portals (modals, dialogs, overlays) inherit the
111
155
  // correct color mode **and** theme tokens (fonts, colors, CSS vars).
156
+ //
157
+ // Only the outermost provider does this. The portal root is one element
158
+ // shared by the whole page, and every provider used to restamp it from its
159
+ // own mode — so a provider nested in the page (a deck in its own theme, a
160
+ // widget jupyter-react renders into a notebook cell as a React root of its
161
+ // own) wrote its mode over the application's whenever it mounted, and the
162
+ // next overlay came up in the wrong colour. Nesting is read from the DOM
163
+ // rather than from React context, because those widget roots are separate
164
+ // React trees: what they share with the application is the document.
112
165
  useEffect(() => {
113
- setupPrimerPortals(resolvedColorMode === 'night' ? 'dark' : resolvedColorMode === 'day' ? 'light' : resolvedColorMode);
166
+ if (nested) {
167
+ // Another provider owns the page's portal root; see above.
168
+ return;
169
+ }
170
+ setupPrimerPortals(resolvedColorMode === "night"
171
+ ? "dark"
172
+ : resolvedColorMode === "day"
173
+ ? "light"
174
+ : resolvedColorMode);
114
175
  syncPortalThemeStyles(portalStyles);
115
- }, [resolvedColorMode, portalStyles]);
116
- return (_jsx(ThemeProvider, { colorMode: resolvedColorMode, theme: resolvedTheme, ...rest, children: _jsx(BaseStyles, { style: {
117
- lineHeight: '1.7',
118
- transition: 'background-color 0.25s ease, color 0.25s ease',
176
+ }, [nested, resolvedColorMode, portalStyles]);
177
+ return (_jsx(ThemeProvider, { colorMode: resolvedColorMode, theme: resolvedTheme, ...rest, children: _jsxs(BaseStyles, { style: {
178
+ lineHeight: "1.7",
179
+ transition: "background-color 0.25s ease, color 0.25s ease",
119
180
  ...typographyVars,
120
181
  ...resolvedStyles,
121
182
  ...baseStyles,
122
- }, children: children }) }));
183
+ }, children: [_jsx("span", { ref: sentinel, hidden: true, "data-datalayer-theme-root": "" }), children] }) }));
123
184
  }
@@ -135,6 +135,7 @@ export interface ThemeColorDefs {
135
135
  * | Background | `--bgColor-default` |
136
136
  * | Foreground | `--fgColor-default` |
137
137
  * | Border | `--borderColor-default` |
138
+ * | Field (rest) | `--control-borderColor-rest` |
138
139
  * | Link | `--fgColor-link` |
139
140
  * | Accent / Success | `--bgColor-accent-emphasis` |
140
141
  * | Default button | `--button-default-bgColor-rest` |
@@ -19,6 +19,7 @@
19
19
  * | Background | `--bgColor-default` |
20
20
  * | Foreground | `--fgColor-default` |
21
21
  * | Border | `--borderColor-default` |
22
+ * | Field (rest) | `--control-borderColor-rest` |
22
23
  * | Link | `--fgColor-link` |
23
24
  * | Accent / Success | `--bgColor-accent-emphasis` |
24
25
  * | Default button | `--button-default-bgColor-rest` |
@@ -219,6 +220,14 @@ export function colorDefsToCSS(defs, mode = 'light') {
219
220
  '--control-transparent-bgColor-selected': defs.btn.selectedBg,
220
221
  '--control-transparent-borderColor-hover': 'transparent',
221
222
  '--control-transparent-borderColor-active': 'transparent',
223
+ /* ── Fields (rest state) ──────────────────────────────────────
224
+ * `TextInput` / `Textarea` / `Select` paint their inactive border
225
+ * with `var(--control-borderColor-rest, var(--color-btn-border))`
226
+ * — both legacy fallbacks apps that skip @primer/primitives CSS
227
+ * never get — so without this the border shorthand is invalid and
228
+ * the field shows no border at all until it is focused. */
229
+ '--control-borderColor-rest': defs.border?.default ?? defs.btn.border,
230
+ '--control-fgColor-rest': defs.fg.default,
222
231
  /* ── Disabled controls / fields ─────────────────────────────── */
223
232
  '--control-bgColor-disabled': defs.canvas.subtle ?? defs.btn.bg,
224
233
  '--control-borderColor-disabled': defs.border?.default ?? defs.btn.border,
@@ -0,0 +1,2 @@
1
+ /** Default typeface used when a Datalayer theme does not override typography. */
2
+ export declare const systemFontStack = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\"";
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright (c) 2023-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** Default typeface used when a Datalayer theme does not override typography. */
6
+ export const systemFontStack = '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"';
@@ -13,6 +13,8 @@ export * from './themes/ivoryTheme';
13
13
  export * from './themes/sunTheme';
14
14
  export * from './themes-brand/spatialBrandTheme';
15
15
  export * from './themeRegistry';
16
+ export * from './portableTheme';
17
+ export * from './fontStacks';
16
18
  export * from './palettes';
17
19
  export * from './useSystemColorMode';
18
20
  export * from './useThemeStore';
@@ -17,6 +17,8 @@ export * from './themes/ivoryTheme';
17
17
  export * from './themes/sunTheme';
18
18
  export * from './themes-brand/spatialBrandTheme';
19
19
  export * from './themeRegistry';
20
+ export * from './portableTheme';
21
+ export * from './fontStacks';
20
22
  export * from './palettes';
21
23
  export * from './useSystemColorMode';
22
24
  export * from './useThemeStore';