@datalayer/primer-addons 1.0.18 → 1.0.20

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 (80) hide show
  1. package/lib/components/appearance/AppearanceControls.d.ts +3 -2
  2. package/lib/components/appearance/AppearanceControls.js +71 -3
  3. package/lib/components/appearance/AppearanceMenu.d.ts +30 -0
  4. package/lib/components/appearance/AppearanceMenu.js +15 -0
  5. package/lib/components/appearance/ColorModeCircle.d.ts +28 -0
  6. package/lib/components/appearance/ColorModeCircle.js +132 -0
  7. package/lib/components/appearance/ThemeCircle.d.ts +21 -0
  8. package/lib/components/appearance/ThemeCircle.js +46 -0
  9. package/lib/components/appearance/ThemePreviewCard.d.ts +12 -0
  10. package/lib/components/appearance/ThemePreviewCard.js +83 -0
  11. package/lib/components/appearance/index.d.ts +4 -0
  12. package/lib/components/appearance/index.js +4 -0
  13. package/lib/components/box/Box.d.ts +2 -1
  14. package/lib/components/closeable-flash/CloseableFlash.js +58 -1
  15. package/lib/components/color/ColorSwatch.d.ts +23 -0
  16. package/lib/components/color/ColorSwatch.js +62 -0
  17. package/lib/components/color/index.d.ts +1 -0
  18. package/lib/components/color/index.js +5 -0
  19. package/lib/components/index.d.ts +6 -0
  20. package/lib/components/index.js +6 -0
  21. package/lib/components/logo/DatalayerLogo.js +49 -1
  22. package/lib/components/logo/DatalayerTextAI.d.ts +4 -4
  23. package/lib/components/logo/DatalayerTextAI.js +6 -6
  24. package/lib/components/logo/index.d.ts +5 -7
  25. package/lib/components/logo/index.js +5 -7
  26. package/lib/components/side-overlay/SideOverlay.js +1 -1
  27. package/lib/components/sliding-panel/SlidingPanel.d.ts +19 -0
  28. package/lib/components/sliding-panel/SlidingPanel.js +217 -0
  29. package/lib/components/sliding-panel/SlidingPanel.stories.d.ts +16 -0
  30. package/lib/components/sliding-panel/SlidingPanel.stories.js +68 -0
  31. package/lib/components/toolbar/Toolbar.js +18 -1
  32. package/lib/components/toolbar/ToolbarButton.js +12 -7
  33. package/lib/components/toolbar/ToolbarRenderer.js +3 -0
  34. package/lib/components/toolbar/types.d.ts +11 -2
  35. package/lib/theme/colors/datalayerColors.d.ts +12 -0
  36. package/lib/theme/colors/datalayerColors.js +13 -0
  37. package/lib/theme/colors/earthColors.d.ts +12 -0
  38. package/lib/theme/colors/earthColors.js +13 -0
  39. package/lib/theme/colors/index.d.ts +3 -0
  40. package/lib/theme/colors/index.js +9 -0
  41. package/lib/theme/colors/ivoryColors.d.ts +46 -0
  42. package/lib/theme/colors/ivoryColors.js +56 -0
  43. package/lib/theme/colors/lovelyColors.d.ts +12 -0
  44. package/lib/theme/colors/lovelyColors.js +13 -0
  45. package/lib/theme/colors/matrixColors.d.ts +12 -0
  46. package/lib/theme/colors/matrixColors.js +13 -0
  47. package/lib/theme/colors/sandColors.d.ts +45 -0
  48. package/lib/theme/colors/sandColors.js +55 -0
  49. package/lib/theme/colors/spatialColors.d.ts +12 -0
  50. package/lib/theme/colors/spatialColors.js +13 -0
  51. package/lib/theme/colors/sunColors.d.ts +45 -0
  52. package/lib/theme/colors/sunColors.js +55 -0
  53. package/lib/theme/css/createThemeCSSVars.d.ts +25 -1
  54. package/lib/theme/css/createThemeCSSVars.js +56 -3
  55. package/lib/theme/index.d.ts +3 -0
  56. package/lib/theme/index.js +3 -0
  57. package/lib/theme/palettes/ColorPalette.js +121 -1
  58. package/lib/theme/themeRegistry.d.ts +1 -1
  59. package/lib/theme/themeRegistry.js +151 -1
  60. package/lib/theme/themes/datalayerTheme.js +58 -7
  61. package/lib/theme/themes/earthTheme.js +50 -0
  62. package/lib/theme/themes/ivoryTheme.d.ts +1091 -0
  63. package/lib/theme/themes/ivoryTheme.js +257 -0
  64. package/lib/theme/themes/lovelyTheme.js +58 -7
  65. package/lib/theme/themes/matrixTheme.js +57 -8
  66. package/lib/theme/themes/sandTheme.d.ts +1091 -0
  67. package/lib/theme/themes/sandTheme.js +256 -0
  68. package/lib/theme/themes/spatialTheme.js +58 -7
  69. package/lib/theme/themes/sunTheme.d.ts +1091 -0
  70. package/lib/theme/themes/sunTheme.js +257 -0
  71. package/lib/utils/Helper.d.ts +1 -1
  72. package/lib/utils/Portals.d.ts +0 -1
  73. package/lib/utils/Portals.js +0 -1
  74. package/package.json +9 -5
  75. package/lib/App.d.ts +0 -13
  76. package/lib/App.js +0 -25
  77. package/lib/components/logo/AI.d.ts +0 -17
  78. package/lib/components/logo/AI.js +0 -18
  79. package/lib/components/logo/AI2.d.ts +0 -20
  80. package/lib/components/logo/AI2.js +0 -23
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2023-2025 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ import { useLayoutEffect, useRef, useState } from "react";
7
+ import { Box, Text } from "@primer/react";
8
+ import { useThemeStore } from "../../theme/useThemeStore";
9
+ /** Convert an `rgb()` / `rgba()` string to a `#rrggbb` hex value. */
10
+ function rgbToHex(rgb) {
11
+ const match = rgb.match(/rgba?\(([^)]+)\)/);
12
+ if (!match) {
13
+ return null;
14
+ }
15
+ const parts = match[1].split(",").map((part) => part.trim());
16
+ if (parts.length < 3) {
17
+ return null;
18
+ }
19
+ const [r, g, b] = parts.map((part) => parseInt(part, 10));
20
+ if ([r, g, b].some((channel) => Number.isNaN(channel))) {
21
+ return null;
22
+ }
23
+ const toHex = (channel) => channel.toString(16).padStart(2, "0");
24
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
25
+ }
26
+ /**
27
+ * Theme-aware color swatch. Renders a block filled with `color`, then reads
28
+ * the resolved value via `getComputedStyle` and displays it as a hex string.
29
+ * The resolved value recomputes whenever the active theme or color mode
30
+ * changes, so swatches backed by CSS custom properties stay in sync.
31
+ */
32
+ export function ColorSwatch({ color, label, showValue = true, height = 54, }) {
33
+ const ref = useRef(null);
34
+ const { theme, colorMode } = useThemeStore();
35
+ const [hex, setHex] = useState(null);
36
+ useLayoutEffect(() => {
37
+ const raf = requestAnimationFrame(() => {
38
+ if (!ref.current) {
39
+ return;
40
+ }
41
+ const rgb = getComputedStyle(ref.current).backgroundColor;
42
+ setHex(rgbToHex(rgb));
43
+ });
44
+ return () => cancelAnimationFrame(raf);
45
+ }, [color, theme, colorMode]);
46
+ return (_jsxs(Box, { sx: {
47
+ display: "grid",
48
+ gridTemplateRows: `${height}px auto`,
49
+ border: "1px solid",
50
+ borderColor: "var(--borderColor-default)",
51
+ borderRadius: 2,
52
+ overflow: "hidden",
53
+ minWidth: 0,
54
+ }, children: [_jsx(Box, { ref: ref, style: { backgroundColor: color } }), _jsxs(Box, { sx: { p: 2, display: "grid", gap: 1 }, children: [_jsx(Text, { sx: {
55
+ fontSize: 0,
56
+ color: "fg.default",
57
+ whiteSpace: "nowrap",
58
+ overflow: "hidden",
59
+ textOverflow: "ellipsis",
60
+ }, title: label ?? color, children: label ?? color }), showValue && hex ? (_jsx(Text, { sx: { fontSize: 0, color: "fg.muted", fontFamily: "mono" }, children: hex })) : null] })] }));
61
+ }
62
+ export default ColorSwatch;
@@ -0,0 +1 @@
1
+ export * from "./ColorSwatch";
@@ -0,0 +1,5 @@
1
+ /*
2
+ * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export * from "./ColorSwatch";
@@ -1,11 +1,17 @@
1
1
  export * from "./appearance/AppearanceControls";
2
2
  export * from "./appearance/AppearanceControlsWithStore";
3
+ export * from "./appearance/AppearanceMenu";
4
+ export * from "./appearance/ColorModeCircle";
5
+ export * from "./appearance/ThemePreviewCard";
6
+ export * from "./appearance/ThemeCircle";
3
7
  export * from "./box/Box";
4
8
  export * from "./card/Card";
9
+ export * from "./color";
5
10
  export * from "./content-loader/ContentLoader";
6
11
  export * from "./closeable-flash/CloseableFlash";
7
12
  export * from "./icons/CircleIcon";
8
13
  export * from "./side-overlay/SideOverlay";
14
+ export * from "./sliding-panel/SlidingPanel";
9
15
  export * from "./slider/Slider";
10
16
  export * from "./logo";
11
17
  export * from "./appearance";
@@ -1,11 +1,17 @@
1
1
  export * from "./appearance/AppearanceControls";
2
2
  export * from "./appearance/AppearanceControlsWithStore";
3
+ export * from "./appearance/AppearanceMenu";
4
+ export * from "./appearance/ColorModeCircle";
5
+ export * from "./appearance/ThemePreviewCard";
6
+ export * from "./appearance/ThemeCircle";
3
7
  export * from "./box/Box";
4
8
  export * from "./card/Card";
9
+ export * from "./color";
5
10
  export * from "./content-loader/ContentLoader";
6
11
  export * from "./closeable-flash/CloseableFlash";
7
12
  export * from "./icons/CircleIcon";
8
13
  export * from "./side-overlay/SideOverlay";
14
+ export * from "./sliding-panel/SlidingPanel";
9
15
  export * from "./slider/Slider";
10
16
  export * from "./logo";
11
17
  export * from "./appearance";
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  * Distributed under the terms of the Modified BSD License.
5
5
  */
6
6
  import { forwardRef, useId } from 'react';
7
- import { datalayerColors, spatialColors, lovelyColors, matrixColors, earthColors, } from '../../theme';
7
+ import { datalayerColors, spatialColors, lovelyColors, matrixColors, earthColors, sandColors, ivoryColors, sunColors, } from '../../theme';
8
8
  /**
9
9
  * Gradient pattern derived from the original brand logo SVG:
10
10
  * - Left bars: accent → brand (bright → mid)
@@ -93,6 +93,54 @@ export const THEME_LOGO_COLORS = {
93
93
  secondaryGradient: [earthColors.oceanHover, earthColors.oceanAccent],
94
94
  },
95
95
  },
96
+ sand: {
97
+ light: {
98
+ primary: sandColors.sandBrand,
99
+ secondary: sandColors.sandText,
100
+ textColor: sandColors.gray,
101
+ primaryGradient: [sandColors.sandAccent, sandColors.sandBrand],
102
+ secondaryGradient: [sandColors.sandHover, sandColors.sandText],
103
+ },
104
+ dark: {
105
+ primary: sandColors.sandBright,
106
+ secondary: sandColors.sandAccent,
107
+ textColor: sandColors.gray,
108
+ primaryGradient: [sandColors.sandBright, sandColors.sandBrand],
109
+ secondaryGradient: [sandColors.sandHover, sandColors.sandAccent],
110
+ },
111
+ },
112
+ ivory: {
113
+ light: {
114
+ primary: ivoryColors.ivoryBrand,
115
+ secondary: ivoryColors.ivoryText,
116
+ textColor: ivoryColors.gray,
117
+ primaryGradient: [ivoryColors.ivoryAccent, ivoryColors.ivoryBrand],
118
+ secondaryGradient: [ivoryColors.ivoryHover, ivoryColors.ivoryText],
119
+ },
120
+ dark: {
121
+ primary: ivoryColors.ivoryBright,
122
+ secondary: ivoryColors.ivoryAccent,
123
+ textColor: ivoryColors.gray,
124
+ primaryGradient: [ivoryColors.ivoryBright, ivoryColors.ivoryBrand],
125
+ secondaryGradient: [ivoryColors.ivoryHover, ivoryColors.ivoryAccent],
126
+ },
127
+ },
128
+ sun: {
129
+ light: {
130
+ primary: sunColors.sunBrand,
131
+ secondary: sunColors.sunText,
132
+ textColor: sunColors.gray,
133
+ primaryGradient: [sunColors.sunAccent, sunColors.sunBrand],
134
+ secondaryGradient: [sunColors.sunHover, sunColors.sunText],
135
+ },
136
+ dark: {
137
+ primary: sunColors.sunBright,
138
+ secondary: sunColors.sunAccent,
139
+ textColor: sunColors.gray,
140
+ primaryGradient: [sunColors.sunBright, sunColors.sunBrand],
141
+ secondaryGradient: [sunColors.sunHover, sunColors.sunAccent],
142
+ },
143
+ },
96
144
  };
97
145
  export function getLogoColors(variant = 'datalayer', colorMode = 'light') {
98
146
  const mode = colorMode === 'auto' ? 'light' : colorMode;
@@ -1,9 +1,9 @@
1
- import { type SVGProps } from 'react';
2
- import { type ThemeVariant } from '../../theme';
3
- export interface DatalayerTextAIProps extends Omit<SVGProps<SVGSVGElement>, 'ref'> {
1
+ import { type SVGProps } from "react";
2
+ import { type ThemeVariant } from "../../theme";
3
+ export interface DatalayerTextAIProps extends Omit<SVGProps<SVGSVGElement>, "ref"> {
4
4
  size?: number;
5
5
  scale?: number;
6
- colorMode?: 'light' | 'dark' | 'auto';
6
+ colorMode?: "light" | "dark" | "auto";
7
7
  variant?: ThemeVariant;
8
8
  /** Colour for the "LAYER" part of the wordmark. */
9
9
  primaryColor?: string;
@@ -3,10 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  * Copyright (c) 2023-2026 Datalayer, Inc.
4
4
  * Distributed under the terms of the Modified BSD License.
5
5
  */
6
- import { forwardRef } from 'react';
7
- import { AI } from './AI';
8
- import { DatalayerText } from './DatalayerText';
9
- import { getLogoColors } from './DatalayerLogo';
6
+ import { forwardRef } from "react";
7
+ import { DatalayerText } from "./DatalayerText";
8
+ import { getLogoColors } from "./DatalayerLogo";
10
9
  const TEXT_LEFT_TRIM = 12;
11
10
  const TEXT_END_X = 136.672 - TEXT_LEFT_TRIM;
12
11
  const AI_SIZE = 24;
@@ -15,13 +14,14 @@ const AI_VISIBLE_LEFT = 0.25;
15
14
  const AI_VISIBLE_HEIGHT = 16.136075;
16
15
  const AI_SCALE_TO_TEXT_HEIGHT = 25 / AI_VISIBLE_HEIGHT;
17
16
  const AI_RENDER_SIZE = AI_SIZE * AI_SCALE_TO_TEXT_HEIGHT;
17
+ const AI_RENDER_SCALE = AI_RENDER_SIZE / AI_SIZE;
18
18
  const AI_X_TRIM = AI_VISIBLE_LEFT * AI_SCALE_TO_TEXT_HEIGHT;
19
19
  const AI_Y_TRIM = AI_VISIBLE_TOP * AI_SCALE_TO_TEXT_HEIGHT;
20
20
  /**
21
21
  * Datalayer wordmark followed by AI glyph on one line.
22
22
  * Both elements are aligned to the same 25-unit cap height.
23
23
  */
24
- export const DatalayerTextAI = forwardRef(function DatalayerTextAI({ size = 25, scale = 1, colorMode = 'light', variant = 'datalayer', primaryColor, secondaryColor, textColor,
24
+ export const DatalayerTextAI = forwardRef(function DatalayerTextAI({ size = 25, scale = 1, colorMode = "light", variant = "datalayer", primaryColor, secondaryColor, textColor,
25
25
  // 2.380655 ~= 25 / (0.7 * 15.00187)
26
26
  textSizeMultiplier = 2.380655, aiPrimaryColor, aiSecondaryColor, aiGap = 6, ...svgProps }, ref) {
27
27
  const themed = getLogoColors(variant, colorMode);
@@ -35,6 +35,6 @@ textSizeMultiplier = 2.380655, aiPrimaryColor, aiSecondaryColor, aiGap = 6, ...s
35
35
  const baseWidth = (viewBoxWidth / 25) * size;
36
36
  const height = baseHeight * scale;
37
37
  const width = baseWidth * scale;
38
- return (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: `0 0 ${viewBoxWidth} 25`, width: width, height: height, role: "img", "aria-label": "Datalayer AI", ref: ref, ...svgProps, children: [_jsx(DatalayerText, { primaryColor: layerColor, secondaryColor: dataTextColor, sizeMultiplier: textSizeMultiplier, x: -TEXT_LEFT_TRIM }), _jsx(AI, { size: AI_RENDER_SIZE, x: TEXT_END_X + aiGap - AI_X_TRIM, y: -AI_Y_TRIM, variant: variant, colorMode: colorMode, primaryColor: resolvedAiPrimary, secondaryColor: resolvedAiSecondary })] }));
38
+ return (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: `0 0 ${viewBoxWidth} 25`, width: width, height: height, role: "img", "aria-label": "Datalayer AI", ref: ref, ...svgProps, children: [_jsx(DatalayerText, { primaryColor: layerColor, secondaryColor: dataTextColor, sizeMultiplier: textSizeMultiplier, x: -TEXT_LEFT_TRIM }), _jsxs("g", { transform: `translate(${TEXT_END_X + aiGap - AI_X_TRIM} ${-AI_Y_TRIM}) scale(${AI_RENDER_SCALE})`, children: [_jsx("path", { d: "m13.788825 3.932 6.43325 16.136075h3.5279L17.316725 3.932H13.788825Z", fill: resolvedAiPrimary, strokeWidth: 0.25 }), _jsx("path", { d: "m6.325375 13.682775 2.20125 -5.67065 2.201275 5.67065H6.325375ZM6.68225 3.932 0.25 20.068075h3.596525l1.3155 -3.3886h6.729425l1.315275 3.3886h3.59655L10.371 3.932H6.68225Z", fill: resolvedAiSecondary, fillRule: "evenodd", clipRule: "evenodd", strokeWidth: 0.25 })] })] }));
39
39
  });
40
40
  export default DatalayerTextAI;
@@ -1,7 +1,5 @@
1
- export * from './AI';
2
- export * from './AI2';
3
- export * from './DI';
4
- export * from './DatalayerLogo';
5
- export * from './DatalayerLogoText';
6
- export * from './DatalayerText';
7
- export * from './DatalayerTextAI';
1
+ export * from "./DI";
2
+ export * from "./DatalayerLogo";
3
+ export * from "./DatalayerLogoText";
4
+ export * from "./DatalayerText";
5
+ export * from "./DatalayerTextAI";
@@ -1,7 +1,5 @@
1
- export * from './AI';
2
- export * from './AI2';
3
- export * from './DI';
4
- export * from './DatalayerLogo';
5
- export * from './DatalayerLogoText';
6
- export * from './DatalayerText';
7
- export * from './DatalayerTextAI';
1
+ export * from "./DI";
2
+ export * from "./DatalayerLogo";
3
+ export * from "./DatalayerLogoText";
4
+ export * from "./DatalayerText";
5
+ export * from "./DatalayerTextAI";
@@ -37,7 +37,7 @@ const PrimerAddonOverlay = (props) => {
37
37
  display: 'flex',
38
38
  justifyContent: 'right',
39
39
  width: '100%'
40
- }, children: _jsx(IconButton, { variant: 'invisible', ref: closeButtonRef, onClick: closeOverlay, icon: XIcon, "aria-labelledby": "close" }) }), content] }) }));
40
+ }, children: _jsx(IconButton, { variant: 'invisible', ref: closeButtonRef, onClick: closeOverlay, icon: XIcon, "aria-label": "Close" }) }), content] }) }));
41
41
  return (closeButtonRef && openButtonRef ?
42
42
  _jsx(_Fragment, { children: isOpen ?
43
43
  direction === 'left' ?
@@ -0,0 +1,19 @@
1
+ import { type ReactNode } from 'react';
2
+ export type SlidingPanelPosition = 'north' | 'south' | 'west' | 'east';
3
+ export type SlidingPanelVariant = 'default' | 'info' | 'success' | 'warning' | 'error' | 'danger';
4
+ export interface SlidingPanelProps {
5
+ isOpen: boolean;
6
+ message: ReactNode;
7
+ details?: ReactNode;
8
+ onDismiss?: () => void;
9
+ position?: SlidingPanelPosition;
10
+ variant?: SlidingPanelVariant;
11
+ durationMs?: number;
12
+ transitionMs?: number;
13
+ zIndex?: number;
14
+ offset?: number;
15
+ panelSize?: number | string;
16
+ fullWidth?: boolean;
17
+ }
18
+ export declare const SlidingPanel: ({ isOpen, message, details, onDismiss, position, variant, durationMs, transitionMs, zIndex, offset, panelSize, fullWidth, }: SlidingPanelProps) => import("react/jsx-runtime").JSX.Element | null;
19
+ export default SlidingPanel;
@@ -0,0 +1,217 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { IconButton } from '@primer/react';
4
+ import { XIcon } from '@primer/octicons-react';
5
+ import { Box } from '../box/Box';
6
+ const variantStyles = {
7
+ default: {
8
+ bg: 'var(--bgColor-default, #ffffff)',
9
+ border: 'var(--borderColor-default, #d0d7de)',
10
+ fg: 'var(--fgColor-default, #1f2328)',
11
+ },
12
+ info: {
13
+ bg: 'var(--bgColor-accent-muted, #ddf4ff)',
14
+ border: 'var(--borderColor-accent-emphasis, #218bff)',
15
+ fg: 'var(--fgColor-default, #1f2328)',
16
+ },
17
+ success: {
18
+ bg: 'var(--bgColor-success-muted, #dafbe1)',
19
+ border: 'var(--borderColor-success-emphasis, #1a7f37)',
20
+ fg: 'var(--fgColor-success, #1a7f37)',
21
+ },
22
+ warning: {
23
+ bg: 'var(--bgColor-attention-muted, #fff8c5)',
24
+ border: 'var(--borderColor-attention-emphasis, #9a6700)',
25
+ fg: 'var(--fgColor-attention, #9a6700)',
26
+ },
27
+ error: {
28
+ bg: 'var(--bgColor-danger-muted, #ffebe9)',
29
+ border: 'var(--borderColor-danger-emphasis, #cf222e)',
30
+ fg: 'var(--fgColor-danger, #cf222e)',
31
+ },
32
+ danger: {
33
+ bg: 'var(--bgColor-danger-muted, #ffebe9)',
34
+ border: 'var(--borderColor-danger-emphasis, #cf222e)',
35
+ fg: 'var(--fgColor-danger, #cf222e)',
36
+ },
37
+ };
38
+ const closeButtonStyles = {
39
+ default: {
40
+ fg: 'var(--fgColor-default, #1f2328)',
41
+ hoverBg: 'rgba(31, 35, 40, 0.10)',
42
+ },
43
+ info: {
44
+ fg: 'var(--fgColor-accent, #0969da)',
45
+ hoverBg: 'rgba(9, 105, 218, 0.14)',
46
+ },
47
+ success: {
48
+ fg: 'var(--fgColor-success, #1a7f37)',
49
+ hoverBg: 'rgba(26, 127, 55, 0.14)',
50
+ },
51
+ warning: {
52
+ fg: 'var(--fgColor-attention, #9a6700)',
53
+ hoverBg: 'rgba(154, 103, 0, 0.16)',
54
+ },
55
+ error: {
56
+ fg: 'var(--fgColor-danger, #cf222e)',
57
+ hoverBg: 'rgba(207, 34, 46, 0.14)',
58
+ },
59
+ danger: {
60
+ fg: 'var(--fgColor-danger, #cf222e)',
61
+ hoverBg: 'rgba(207, 34, 46, 0.14)',
62
+ },
63
+ };
64
+ const closedTransforms = {
65
+ north: 'translateY(-100%)',
66
+ south: 'translateY(100%)',
67
+ west: 'translateX(-100%)',
68
+ east: 'translateX(100%)',
69
+ };
70
+ const edgeBorderStyles = {
71
+ north: {
72
+ borderTop: '0',
73
+ borderRight: '0',
74
+ borderBottom: '1px solid',
75
+ borderLeft: '0',
76
+ },
77
+ south: {
78
+ borderTop: '1px solid',
79
+ borderRight: '0',
80
+ borderBottom: '0',
81
+ borderLeft: '0',
82
+ },
83
+ west: {
84
+ borderTop: '0',
85
+ borderRight: '1px solid',
86
+ borderBottom: '0',
87
+ borderLeft: '0',
88
+ },
89
+ east: {
90
+ borderTop: '0',
91
+ borderRight: '0',
92
+ borderBottom: '0',
93
+ borderLeft: '1px solid',
94
+ },
95
+ };
96
+ export const SlidingPanel = ({ isOpen, message, details, onDismiss, position = 'north', variant = 'default', durationMs, transitionMs = 260, zIndex = 200, offset = 0, panelSize = 360, fullWidth = true, }) => {
97
+ const [isMounted, setIsMounted] = useState(isOpen);
98
+ const [isVisible, setIsVisible] = useState(isOpen);
99
+ useEffect(() => {
100
+ if (isOpen) {
101
+ setIsMounted(true);
102
+ const showTimer = window.setTimeout(() => setIsVisible(true), 10);
103
+ return () => window.clearTimeout(showTimer);
104
+ }
105
+ setIsVisible(false);
106
+ const hideTimer = window.setTimeout(() => setIsMounted(false), transitionMs);
107
+ return () => window.clearTimeout(hideTimer);
108
+ }, [isOpen, transitionMs]);
109
+ useEffect(() => {
110
+ if (!isOpen || !onDismiss) {
111
+ return;
112
+ }
113
+ if (durationMs === undefined || durationMs <= 0) {
114
+ return;
115
+ }
116
+ const timer = window.setTimeout(() => {
117
+ onDismiss();
118
+ }, durationMs);
119
+ return () => {
120
+ window.clearTimeout(timer);
121
+ };
122
+ }, [isOpen, durationMs, onDismiss]);
123
+ const anchorStyles = useMemo(() => {
124
+ if (position === 'north') {
125
+ return {
126
+ top: offset,
127
+ left: 0,
128
+ right: 0,
129
+ justifyContent: 'center',
130
+ alignItems: 'flex-start',
131
+ };
132
+ }
133
+ if (position === 'south') {
134
+ return {
135
+ bottom: offset,
136
+ left: 0,
137
+ right: 0,
138
+ justifyContent: 'center',
139
+ alignItems: 'flex-end',
140
+ };
141
+ }
142
+ if (position === 'west') {
143
+ return {
144
+ left: offset,
145
+ top: 0,
146
+ bottom: 0,
147
+ justifyContent: 'flex-start',
148
+ alignItems: 'stretch',
149
+ };
150
+ }
151
+ return {
152
+ right: offset,
153
+ top: 0,
154
+ bottom: 0,
155
+ justifyContent: 'flex-end',
156
+ alignItems: 'stretch',
157
+ };
158
+ }, [position, offset]);
159
+ if (!isMounted) {
160
+ return null;
161
+ }
162
+ const style = variantStyles[variant];
163
+ const closeStyle = closeButtonStyles[variant];
164
+ const horizontal = position === 'north' || position === 'south';
165
+ const edgeBorder = edgeBorderStyles[position];
166
+ return (_jsx(Box, { sx: {
167
+ position: 'fixed',
168
+ zIndex,
169
+ display: 'flex',
170
+ pointerEvents: 'none',
171
+ ...anchorStyles,
172
+ }, children: _jsxs(Box, { role: variant === 'error' || variant === 'danger' ? 'alert' : 'status', "aria-live": variant === 'error' || variant === 'danger' ? 'assertive' : 'polite', onClick: onDismiss, sx: {
173
+ pointerEvents: 'auto',
174
+ position: 'relative',
175
+ display: 'flex',
176
+ alignItems: 'center',
177
+ justifyContent: 'flex-start',
178
+ cursor: onDismiss ? 'pointer' : 'default',
179
+ width: horizontal
180
+ ? fullWidth
181
+ ? '100vw'
182
+ : 'min(960px, 96vw)'
183
+ : panelSize,
184
+ maxWidth: horizontal ? '100vw' : undefined,
185
+ height: horizontal ? undefined : '100vh',
186
+ maxHeight: horizontal ? undefined : '100vh',
187
+ borderColor: style.border,
188
+ borderTop: edgeBorder.borderTop,
189
+ borderRight: edgeBorder.borderRight,
190
+ borderBottom: edgeBorder.borderBottom,
191
+ borderLeft: edgeBorder.borderLeft,
192
+ backgroundColor: style.bg,
193
+ color: style.fg,
194
+ textAlign: 'left',
195
+ px: 4,
196
+ py: horizontal ? 3 : 4,
197
+ boxShadow: 'shadow.large',
198
+ transform: isVisible ? 'translate(0, 0)' : closedTransforms[position],
199
+ opacity: isVisible ? 1 : 0,
200
+ transition: `transform ${transitionMs}ms ease, opacity ${transitionMs}ms ease`,
201
+ overflow: 'auto',
202
+ }, children: [onDismiss ? (_jsx(Box, { sx: {
203
+ position: 'absolute',
204
+ top: 2,
205
+ right: 3,
206
+ }, children: _jsx(IconButton, { variant: "invisible", icon: XIcon, "aria-label": "Close panel", onClick: event => {
207
+ event.stopPropagation();
208
+ onDismiss();
209
+ }, sx: {
210
+ color: closeStyle.fg,
211
+ borderRadius: 2,
212
+ '&:hover': {
213
+ backgroundColor: closeStyle.hoverBg,
214
+ },
215
+ } }) })) : null, _jsxs(Box, { sx: { width: 'min(980px, 100%)', display: 'grid', gap: 1, pr: onDismiss ? 6 : 0 }, children: [_jsx(Box, { sx: { fontSize: 3, fontWeight: 500, lineHeight: 1.4 }, children: message }), details ? (_jsx(Box, { sx: { fontSize: 1, fontWeight: 400, lineHeight: 1.4, color: 'fg.muted' }, children: details })) : null] })] }) }));
216
+ };
217
+ export default SlidingPanel;
@@ -0,0 +1,16 @@
1
+ import type { StoryObj } from '@storybook/react';
2
+ import { type SlidingPanelProps } from './SlidingPanel';
3
+ declare const meta: {
4
+ title: string;
5
+ component: ({ isOpen, message, details, onDismiss, position, variant, durationMs, transitionMs, zIndex, offset, panelSize, fullWidth, }: SlidingPanelProps) => import("react/jsx-runtime").JSX.Element | null;
6
+ tags: string[];
7
+ parameters: {
8
+ layout: string;
9
+ };
10
+ };
11
+ export default meta;
12
+ type Story = StoryObj<typeof meta>;
13
+ export declare const NorthInfo: Story;
14
+ export declare const SouthWarning: Story;
15
+ export declare const EastSuccess: Story;
16
+ export declare const WestError: Story;
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState } from 'react';
3
+ import { Box, Button, Heading } from '@primer/react';
4
+ import { SlidingPanel } from './SlidingPanel';
5
+ const meta = {
6
+ title: 'Components/SlidingPanel',
7
+ component: SlidingPanel,
8
+ tags: ['autodocs'],
9
+ parameters: {
10
+ layout: 'fullscreen',
11
+ },
12
+ };
13
+ export default meta;
14
+ const Demo = (props) => {
15
+ const [isOpen, setIsOpen] = useState(false);
16
+ const openButtonRef = useRef(null);
17
+ return (_jsxs(Box, { sx: { p: 4, minHeight: '100vh', bg: 'canvas.default' }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 3, mb: 3 }, children: "Sliding Panel Demo" }), _jsx(Button, { ref: openButtonRef, onClick: () => setIsOpen(true), children: "Open panel" }), _jsx(SlidingPanel, { ...props, isOpen: isOpen, onDismiss: () => setIsOpen(false) }), isOpen ? (_jsx(Box, { sx: { mt: 3 }, children: _jsx(Button, { size: "small", onClick: () => setIsOpen(false), children: "Close" }) })) : null] }));
18
+ };
19
+ export const NorthInfo = {
20
+ args: {
21
+ isOpen: false,
22
+ message: 'Sliding panel message',
23
+ details: 'Additional details appear below the message in normal font.',
24
+ position: 'north',
25
+ variant: 'info',
26
+ durationMs: 4500,
27
+ fullWidth: true,
28
+ },
29
+ render: args => _jsx(Demo, { ...args }),
30
+ };
31
+ export const SouthWarning = {
32
+ args: {
33
+ isOpen: false,
34
+ message: 'Sliding panel message',
35
+ details: 'Additional details appear below the message in normal font.',
36
+ position: 'south',
37
+ variant: 'warning',
38
+ durationMs: 0,
39
+ fullWidth: true,
40
+ },
41
+ render: args => _jsx(Demo, { ...args }),
42
+ };
43
+ export const EastSuccess = {
44
+ args: {
45
+ isOpen: false,
46
+ message: 'Sliding panel message',
47
+ details: 'Additional details appear below the message in normal font.',
48
+ position: 'east',
49
+ variant: 'success',
50
+ durationMs: 0,
51
+ panelSize: 360,
52
+ fullWidth: false,
53
+ },
54
+ render: args => _jsx(Demo, { ...args }),
55
+ };
56
+ export const WestError = {
57
+ args: {
58
+ isOpen: false,
59
+ message: 'Sliding panel message',
60
+ details: 'Additional details appear below the message in normal font.',
61
+ position: 'west',
62
+ variant: 'error',
63
+ durationMs: 0,
64
+ panelSize: 360,
65
+ fullWidth: false,
66
+ },
67
+ render: args => _jsx(Demo, { ...args }),
68
+ };
@@ -30,7 +30,24 @@ export function Toolbar({ items, extraItems, className, disabled, ariaLabel = 'E
30
30
  const allItems = useMemo(() => {
31
31
  if (!extraItems?.length)
32
32
  return items;
33
- return [...items, ...extraItems];
33
+ /*
34
+ * An extra item takes the place of the item it shares its key with.
35
+ *
36
+ * A host that knows better what one of the items should do — how this
37
+ * document is saved, where it is run — says so with the key of that item
38
+ * rather than adding a second button next to it. What it leaves out, its
39
+ * order and its icon among them, stays as the toolbar had it.
40
+ */
41
+ const overrides = new Map(extraItems.map(item => [item.key, item]));
42
+ const merged = items.map(item => {
43
+ const override = overrides.get(item.key);
44
+ if (!override) {
45
+ return item;
46
+ }
47
+ overrides.delete(item.key);
48
+ return { ...item, ...override };
49
+ });
50
+ return [...merged, ...extraItems.filter(item => overrides.has(item.key))];
34
51
  }, [items, extraItems]);
35
52
  return (_jsx(Box, { className: className, role: "toolbar", "aria-label": ariaLabel, sx: {
36
53
  display: 'flex',
@@ -6,16 +6,19 @@ import { jsx as _jsx } from "react/jsx-runtime";
6
6
  /**
7
7
  * ToolbarButton - A single button in a Toolbar.
8
8
  *
9
- * Renders a native <button> with a direct Octicon icon.
10
- * Avoids Primer's IconButton and Tooltip components to prevent invariant
11
- * violations in Primer v37 (Tooltip requires a native interactive child,
12
- * and IconButton's internal Tooltip conflicts with external wrappers).
13
- *
14
- * Uses the native HTML `title` attribute for tooltip behaviour.
9
+ * Renders a native <button> with a direct Octicon icon, wrapped in Primer's
10
+ * Tooltip when the item carries a title. The v37 Tooltip demands a native
11
+ * interactive child — the reason IconButton is avoided — and a plain
12
+ * <button> is exactly that. A disabled item therefore wears `aria-disabled`
13
+ * rather than the `disabled` attribute: a truly disabled button is not
14
+ * interactive, which both silences its tooltip and breaks the invariant —
15
+ * and the tooltip of a disabled action is precisely where the why-disabled
16
+ * explanation lives.
15
17
  *
16
18
  * @module components/toolbar/ToolbarButton
17
19
  */
18
20
  import { isValidElement } from 'react';
21
+ import { Tooltip } from '@primer/react';
19
22
  export function ToolbarButton({ item, size = 'medium' }) {
20
23
  const { ariaLabel, title, icon, label, isActive, onClick, disabled } = item;
21
24
  // Resolve icon → React element.
@@ -38,7 +41,7 @@ export function ToolbarButton({ item, size = 'medium' }) {
38
41
  iconElement = _jsx("span", { style: { fontSize: 12, fontWeight: 600, lineHeight: 1 }, children: label });
39
42
  }
40
43
  const btnSize = size === 'small' ? 28 : 32;
41
- return (_jsx("button", { type: "button", "aria-label": ariaLabel, title: title, onMouseDown: (e) => e.preventDefault(), onClick: onClick, disabled: disabled, style: {
44
+ const button = (_jsx("button", { type: "button", "aria-label": ariaLabel, "aria-disabled": disabled || undefined, onMouseDown: (e) => e.preventDefault(), onClick: disabled ? undefined : onClick, style: {
42
45
  display: 'inline-flex',
43
46
  alignItems: 'center',
44
47
  justifyContent: 'center',
@@ -66,5 +69,7 @@ export function ToolbarButton({ item, size = 'medium' }) {
66
69
  ? 'var(--fgColor-accent, #0969da)'
67
70
  : 'var(--fgColor-muted, #656d76)';
68
71
  }, children: iconElement }));
72
+ // No title: nothing to say, no wrapper to say it with.
73
+ return title ? (_jsx(Tooltip, { text: title, direction: "s", children: button })) : (button);
69
74
  }
70
75
  export default ToolbarButton;