@dataverse-kit/surface-kit 0.2.1 → 0.4.0

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/dist/index.d.ts CHANGED
@@ -2,64 +2,16 @@ import { DialogSizePreset } from './core.js';
2
2
  export { CALLOUT_DEFAULT_WIDTH, DIALOG_SIZE_PX, MODAL_AUTOSIZE_MIN_WIDTH, PANEL_DEFAULT_WIDTH, PanelKind, calloutFormDefaults, calloutWidthPx, dialogHeightCss, dialogWidthCss, dialogWidthPx, panelCustomWidthPx, panelKindFromWidth } from './core.js';
3
3
  import { C as ColumnCount } from './columns-BlpFO5VD.js';
4
4
  export { c as clampSpan, g as gridTemplateForColumns } from './columns-BlpFO5VD.js';
5
- import { Breakpoint, ResponsiveValue } from './responsive.js';
6
- export { BREAKPOINTS, CARD_REFLOW_WIDTH, ResponsiveMode, StyleObject, fieldGridStyle, fieldRowStyle, resolveBreakpoint, resolveResponsiveValue, sectionStackingStyle } from './responsive.js';
5
+ import { Breakpoint } from './responsive.js';
6
+ export { BREAKPOINTS, CARD_REFLOW_WIDTH, ResponsiveMode, ResponsiveValue, StyleObject, fieldGridStyle, fieldRowStyle, resolveBreakpoint, resolveResponsiveValue, sectionStackingStyle } from './responsive.js';
7
7
  export { colors, layout, radius, spacing, typography } from '@dataverse-kit/design-tokens';
8
8
  import { Theme, ICommandBarItemProps } from '@fluentui/react';
9
- import * as React from 'react';
9
+ import { D as Density } from './useDensity-BePwtUzY.js';
10
+ export { C as ContainerSize, f as DensityContext, d as densityScale, s as scaleSpacing, b as useContainerBreakpoint, u as useContainerSize, a as useContainerWidth, g as useDensity, e as useMediaQuery, c as useResponsiveValue } from './useDensity-BePwtUzY.js';
10
11
  import React__default, { CSSProperties, ReactNode, RefObject, ReactElement } from 'react';
11
12
 
12
13
  declare function createDynamicsTheme(): Theme;
13
14
 
14
- type Density = 'comfortable' | 'compact';
15
- declare const densityScale: {
16
- readonly comfortable: 1;
17
- readonly compact: 0.75;
18
- };
19
- /** Scale a base spacing value (px) by the active density. */
20
- declare function scaleSpacing(value: number, density?: Density): number;
21
-
22
- interface ContainerSize {
23
- width: number;
24
- height: number;
25
- inlineSize: number;
26
- blockSize: number;
27
- }
28
- /** Observe an element's size via ResizeObserver. Pass a ref; returns {width,height,inlineSize,blockSize}. */
29
- declare function useContainerSize<T extends HTMLElement = HTMLElement>(ref: React.RefObject<T>): ContainerSize;
30
-
31
- /**
32
- * Create-and-return a ref plus the observed width of the element it is attached to.
33
- * `const [ref, width] = useContainerWidth(); return <div ref={ref}>…</div>`.
34
- */
35
- declare function useContainerWidth<T extends HTMLElement = HTMLDivElement>(): [React.RefObject<T>, number];
36
-
37
- /**
38
- * Resolve the *container's* current breakpoint (not the viewport's) — the correctness
39
- * fix for a surface inside a PCF, where width comes from `context.allocatedWidth`.
40
- * Returns `[ref, breakpoint, width]`.
41
- */
42
- declare function useContainerBreakpoint<T extends HTMLElement = HTMLDivElement>(): [React.RefObject<T>, Breakpoint, number];
43
-
44
- /**
45
- * Resolve a responsive value against the *container's* current breakpoint.
46
- * `const [ref, cols] = useResponsiveValue({ xs: 1, md: 2, lg: 3 });`
47
- */
48
- declare function useResponsiveValue<T, E extends HTMLElement = HTMLDivElement>(value: ResponsiveValue<T>): [React.RefObject<E>, T | undefined];
49
-
50
- /** Viewport media-query match — the SSR-safe / no-ResizeObserver fallback to the container hooks. */
51
- declare function useMediaQuery(query: string): boolean;
52
-
53
- /**
54
- * Form-level spacing density. `FormSurface` provides it; `Section` (and any future
55
- * container) reads it via `useDensity()`, so a single `density` switch on the form
56
- * cascades to every section instead of being set per-section. Defaults to
57
- * `'comfortable'` (the Dynamics default) — `'compact'` scales spacing by 0.75.
58
- */
59
- declare const DensityContext: React.Context<Density>;
60
- /** Read the ambient spacing density (defaults to `'comfortable'` outside a provider). */
61
- declare const useDensity: () => Density;
62
-
63
15
  type SurfaceVariant = 'card' | 'flat' | 'placeholder';
64
16
  type Alignment = 'left' | 'center' | 'right';
65
17
  interface SurfaceActionButton {
@@ -174,6 +126,11 @@ interface SectionHeaderProps {
174
126
  collapsed?: boolean;
175
127
  onToggle?: () => void;
176
128
  }
129
+ /** A Dynamics form-header field: a small label above its (usually bolder) value. */
130
+ interface HeaderField {
131
+ label: ReactNode;
132
+ value: ReactNode;
133
+ }
177
134
  interface FormHeaderProps {
178
135
  title?: ReactNode;
179
136
  subtitle?: ReactNode;
@@ -182,7 +139,10 @@ interface FormHeaderProps {
182
139
  subtitleColor?: string;
183
140
  /** Avatar/persona shown to the left of the title. */
184
141
  persona?: ReactNode;
185
- /** Right side of the header (e.g. KPI header fields). */
142
+ /** Dynamics-style header fields (label-over-value KPI pairs, separated) shown on the
143
+ * right. Renders before `actions`. */
144
+ headerFields?: HeaderField[];
145
+ /** Right side of the header (e.g. custom controls) — rendered after `headerFields`. */
186
146
  actions?: ReactNode;
187
147
  className?: string;
188
148
  }
@@ -191,6 +151,8 @@ interface SurfaceTab {
191
151
  label: string;
192
152
  content: ReactNode;
193
153
  iconName?: string;
154
+ /** Hover tooltip for the tab (shows on the label + icon via Fluent TooltipHost). */
155
+ tooltip?: string;
194
156
  disabled?: boolean;
195
157
  }
196
158
  interface PivotSurfaceProps {
@@ -210,8 +172,12 @@ interface CommandSurfaceProps {
210
172
  }
211
173
  interface ActionBarProps {
212
174
  buttons: SurfaceActionButton[];
213
- /** Default `'right'`. */
175
+ /** Alignment of the right button cluster (left-positioned buttons always pin left).
176
+ * Default `'right'`. */
214
177
  alignment?: Alignment;
178
+ /** Reverse the right cluster's order so the primary action sits first (leftmost) —
179
+ * the "inverted" Dynamics footer order. Default false. */
180
+ inverted?: boolean;
215
181
  /** Stick to the bottom of the scroll container. Default true. */
216
182
  sticky?: boolean;
217
183
  className?: string;
@@ -259,7 +225,9 @@ interface FormDialogSurfaceProps {
259
225
  current: number;
260
226
  total: number;
261
227
  };
262
- /** Footer content (e.g. an ActionBar). */
228
+ /** Footer content. Rendered **bare** (no wrapper border/padding), so pass a component
229
+ * that supplies its own top divider + padding + alignment — use `ActionBar` or `Footer`.
230
+ * Raw content (e.g. a lone `<button>`) renders flush against the body. */
263
231
  footer?: ReactNode;
264
232
  className?: string;
265
233
  children?: ReactNode;
@@ -274,6 +242,9 @@ interface ModalSurfaceProps {
274
242
  isBlocking?: boolean;
275
243
  /** Auto width with a 1000px min (Track-and-Ship pattern) instead of a fixed preset. */
276
244
  autoSize?: boolean;
245
+ /** Footer content. Rendered **bare** (no wrapper border/padding), so pass a component
246
+ * that supplies its own top divider + padding + alignment — use `ActionBar` or `Footer`.
247
+ * Raw content (e.g. a lone `<button>`) renders flush against the body. */
277
248
  footer?: ReactNode;
278
249
  className?: string;
279
250
  children?: ReactNode;
@@ -414,4 +385,4 @@ type ResolvedSurface = {
414
385
  /** Resolve a (duck-typed) form-runtime FormDefinition to the right surface + sizing props. */
415
386
  declare function fromFormDefinition(def: FormDefinitionLike): ResolvedSurface;
416
387
 
417
- export { ActionBar, type ActionBarProps, type Alignment, Breakpoint, type CalloutDirection, CalloutSurface, type CalloutSurfaceProps, ColumnCount, ColumnLayout, type ColumnLayoutProps, CommandSurface, type CommandSurfaceProps, type ContainerSize, DashboardSurface, type DashboardSurfaceProps, type Density, DensityContext, DialogSizePreset, FieldRow, type FieldRowProps, type FluentPanelType, Footer, type FooterProps, type FormDefinitionLike, FormDialogSurface, type FormDialogSurfaceProps, FormHeader, type FormHeaderProps, type FormSettingsLike, FormSurface, type FormSurfaceProps, MasterDetailSurface, type MasterDetailSurfaceProps, ModalSurface, type ModalSurfaceProps, PageSurface, type PageSurfaceProps, PanelSurface, type PanelSurfaceProps, PivotSurface, type PivotSurfaceProps, type ResolvedSurface, ResponsiveValue, Section, SectionCard, type SectionCardProps, SectionHeader, type SectionHeaderProps, type SectionProps, type SurfaceActionButton, type SurfaceFormType, type SurfaceTab, type SurfaceVariant, TooltipSurface, type TooltipSurfaceProps, TwoColumnLayout, type TwoColumnLayoutProps, createDynamicsTheme, densityScale, fromFormDefinition, scaleSpacing, useContainerBreakpoint, useContainerSize, useContainerWidth, useDensity, useMediaQuery, useResponsiveValue };
388
+ export { ActionBar, type ActionBarProps, type Alignment, Breakpoint, type CalloutDirection, CalloutSurface, type CalloutSurfaceProps, ColumnCount, ColumnLayout, type ColumnLayoutProps, CommandSurface, type CommandSurfaceProps, DashboardSurface, type DashboardSurfaceProps, Density, DialogSizePreset, FieldRow, type FieldRowProps, type FluentPanelType, Footer, type FooterProps, type FormDefinitionLike, FormDialogSurface, type FormDialogSurfaceProps, FormHeader, type FormHeaderProps, type FormSettingsLike, FormSurface, type FormSurfaceProps, type HeaderField, MasterDetailSurface, type MasterDetailSurfaceProps, ModalSurface, type ModalSurfaceProps, PageSurface, type PageSurfaceProps, PanelSurface, type PanelSurfaceProps, PivotSurface, type PivotSurfaceProps, type ResolvedSurface, Section, SectionCard, type SectionCardProps, SectionHeader, type SectionHeaderProps, type SectionProps, type SurfaceActionButton, type SurfaceFormType, type SurfaceTab, type SurfaceVariant, TooltipSurface, type TooltipSurfaceProps, TwoColumnLayout, type TwoColumnLayoutProps, createDynamicsTheme, fromFormDefinition };
package/dist/index.mjs CHANGED
@@ -494,18 +494,23 @@ var Section = ({
494
494
  ...style
495
495
  };
496
496
  const styles = mergeStyleSets3({
497
- card: { ...dynamicsCardStyle, boxShadow: cardShadow, padding: cardPad, minWidth: 300, width: "100%" },
497
+ // box-sizing:border-box so the 20px padding + 1px border are contained within
498
+ // `width:100%` — otherwise the card renders 42px wider than its grid cell and
499
+ // spills off the right edge (dashboards, page sections, fields inside dialogs).
500
+ card: { ...dynamicsCardStyle, boxSizing: "border-box", boxShadow: cardShadow, padding: cardPad, minWidth: 0, width: "100%" },
498
501
  flat: {
499
502
  background: colors.surface,
500
503
  borderRadius: radius.card,
504
+ boxSizing: "border-box",
501
505
  padding: cardPad,
502
- minWidth: 300,
506
+ minWidth: 0,
503
507
  width: "100%",
504
508
  overflow: "hidden"
505
509
  },
506
510
  placeholder: {
507
511
  border: `1px dashed ${colors.borderNeutral}`,
508
512
  borderRadius: radius.card,
513
+ boxSizing: "border-box",
509
514
  minHeight,
510
515
  width: "100%",
511
516
  background: "transparent"
@@ -561,6 +566,7 @@ var SectionCard = ({
561
566
  );
562
567
 
563
568
  // src/containers/FormHeader.tsx
569
+ import React2 from "react";
564
570
  import { mergeStyleSets as mergeStyleSets4 } from "@fluentui/react";
565
571
  import { jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
566
572
  var FormHeader = ({
@@ -570,6 +576,7 @@ var FormHeader = ({
570
576
  titleColor,
571
577
  subtitleColor,
572
578
  persona,
579
+ headerFields,
573
580
  actions,
574
581
  className
575
582
  }) => {
@@ -593,8 +600,29 @@ var FormHeader = ({
593
600
  lineHeight: "1.3"
594
601
  },
595
602
  status: { fontSize: typography.fontSize.smPlus, color: colors.textSecondary },
596
- subtitle: { fontSize: typography.fontSize.smPlus, color: subtitleColor || colors.textSecondary, marginTop: 2 }
603
+ subtitle: { fontSize: typography.fontSize.smPlus, color: subtitleColor || colors.textSecondary, marginTop: 2 },
604
+ // Right cluster: Dynamics header-field summary tiles (value over label) separated by
605
+ // hairline dividers — mirrors form-runtime HeaderShell's SummaryTile.
606
+ right: { display: "flex", alignItems: "center", justifyContent: "flex-end", flexWrap: "wrap", minWidth: 0 },
607
+ field: { display: "flex", flexDirection: "column", gap: 2, maxWidth: 200, minWidth: 0 },
608
+ fieldValue: {
609
+ fontSize: typography.fontSize.md,
610
+ fontWeight: typography.fontWeight.semibold,
611
+ color: colors.textPrimary,
612
+ whiteSpace: "nowrap",
613
+ overflow: "hidden",
614
+ textOverflow: "ellipsis"
615
+ },
616
+ fieldLabel: {
617
+ fontSize: typography.fontSize.sm,
618
+ color: colors.textSecondary,
619
+ whiteSpace: "nowrap",
620
+ overflow: "hidden",
621
+ textOverflow: "ellipsis"
622
+ },
623
+ divider: { width: 1, height: 24, background: colors.borderNeutral, margin: "0 12px", flexShrink: 0 }
597
624
  });
625
+ const hasFields = headerFields != null && headerFields.length > 0;
598
626
  return /* @__PURE__ */ jsxs4("div", { className: [styles.root, className].filter(Boolean).join(" "), children: [
599
627
  /* @__PURE__ */ jsxs4("div", { className: styles.main, children: [
600
628
  persona,
@@ -606,13 +634,29 @@ var FormHeader = ({
606
634
  subtitle != null && /* @__PURE__ */ jsx9("div", { className: styles.subtitle, children: subtitle })
607
635
  ] })
608
636
  ] }),
609
- actions != null && /* @__PURE__ */ jsx9("div", { children: actions })
637
+ (hasFields || actions != null) && /* @__PURE__ */ jsxs4("div", { className: styles.right, children: [
638
+ hasFields && headerFields.map((f, i) => /* @__PURE__ */ jsxs4(React2.Fragment, { children: [
639
+ i > 0 && /* @__PURE__ */ jsx9("div", { className: styles.divider, "aria-hidden": true }),
640
+ /* @__PURE__ */ jsxs4("div", { className: styles.field, children: [
641
+ /* @__PURE__ */ jsx9(
642
+ "div",
643
+ {
644
+ className: styles.fieldValue,
645
+ title: typeof f.value === "string" ? f.value : void 0,
646
+ children: f.value
647
+ }
648
+ ),
649
+ /* @__PURE__ */ jsx9("div", { className: styles.fieldLabel, children: f.label })
650
+ ] })
651
+ ] }, i)),
652
+ actions != null && /* @__PURE__ */ jsx9("div", { style: { marginLeft: hasFields ? 12 : 0 }, children: actions })
653
+ ] })
610
654
  ] });
611
655
  };
612
656
 
613
657
  // src/containers/PivotSurface.tsx
614
658
  import { useState as useState4 } from "react";
615
- import { Pivot, PivotItem } from "@fluentui/react";
659
+ import { Pivot, PivotItem, TooltipHost } from "@fluentui/react";
616
660
  import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
617
661
  var PivotSurface = ({
618
662
  tabs,
@@ -628,10 +672,12 @@ var PivotSurface = ({
628
672
  const handleLinkClick = (item) => {
629
673
  const id = item?.props.itemKey;
630
674
  if (!id) return;
675
+ if (tabs.find((t) => t.id === id)?.disabled) return;
631
676
  if (selectedId == null) setInternal(id);
632
677
  onTabChange?.(id);
633
678
  };
634
679
  const active = tabs.find((t) => t.id === selected) ?? tabs[0];
680
+ const renderItemLink = (tab) => tab.tooltip ? (props, defaultRender) => /* @__PURE__ */ jsx10(TooltipHost, { content: tab.tooltip, children: defaultRender?.(props) }) : void 0;
635
681
  return /* @__PURE__ */ jsxs5("div", { className, children: [
636
682
  /* @__PURE__ */ jsx10(
637
683
  Pivot,
@@ -646,7 +692,8 @@ var PivotSurface = ({
646
692
  itemKey: t.id,
647
693
  headerText: t.label,
648
694
  itemIcon: t.iconName,
649
- headerButtonProps: t.disabled ? { disabled: true } : void 0
695
+ onRenderItemLink: renderItemLink(t),
696
+ headerButtonProps: t.disabled ? { disabled: true, style: { opacity: 0.4, cursor: "not-allowed", pointerEvents: "none" } } : void 0
650
697
  },
651
698
  t.id
652
699
  ))
@@ -675,16 +722,22 @@ function renderButton(b) {
675
722
  if (b.appearance === "subtle") return /* @__PURE__ */ jsx12(ActionButton, { ...common }, b.id);
676
723
  return /* @__PURE__ */ jsx12(DefaultButton, { ...common }, b.id);
677
724
  }
678
- var ActionBar = ({ buttons, alignment = "right", sticky = true, className }) => {
725
+ var ActionBar = ({
726
+ buttons,
727
+ alignment = "right",
728
+ inverted = false,
729
+ sticky = true,
730
+ className
731
+ }) => {
679
732
  const justify = alignment === "left" ? "flex-start" : alignment === "center" ? "center" : "flex-end";
680
733
  const visible = buttons.filter((b) => !b.hidden);
681
734
  const left = visible.filter((b) => b.position === "left");
682
735
  const right = visible.filter((b) => b.position !== "left");
736
+ const rightOrdered = inverted ? [...right].reverse() : right;
683
737
  const cls = mergeStyles4(
684
738
  {
685
739
  display: "flex",
686
740
  alignItems: "center",
687
- justifyContent: left.length ? "space-between" : justify,
688
741
  gap: 8,
689
742
  padding: "12px 24px",
690
743
  borderTop: `1px solid ${colors.borderNeutral}`,
@@ -695,7 +748,7 @@ var ActionBar = ({ buttons, alignment = "right", sticky = true, className }) =>
695
748
  );
696
749
  return /* @__PURE__ */ jsxs6("div", { className: cls, children: [
697
750
  left.length > 0 && /* @__PURE__ */ jsx12("div", { style: { display: "flex", gap: 8 }, children: left.map(renderButton) }),
698
- /* @__PURE__ */ jsx12("div", { style: { display: "flex", gap: 8, justifyContent: justify }, children: right.map(renderButton) })
751
+ /* @__PURE__ */ jsx12("div", { style: { display: "flex", flex: 1, gap: 8, justifyContent: justify }, children: rightOrdered.map(renderButton) })
699
752
  ] });
700
753
  };
701
754
 
@@ -758,8 +811,7 @@ var FormDialogSurface = ({
758
811
  subheader: { fontSize: typography.fontSize.smPlus, color: colors.textSecondary, marginTop: 2 },
759
812
  headerRight: { display: "flex", alignItems: "center", gap: 8 },
760
813
  counter: { fontSize: typography.fontSize.smPlus, color: colors.textSecondary },
761
- content: { flex: 1, minHeight: 0, overflow: "auto", padding: 20 },
762
- footer: { borderTop: `1px solid ${colors.borderNeutral}`, padding: "12px 20px" }
814
+ content: { flex: 1, minHeight: 0, overflow: "auto", padding: 20 }
763
815
  });
764
816
  const showHeader = Boolean(title) || Boolean(recordCounter) || Boolean(onDismiss);
765
817
  return /* @__PURE__ */ jsxs7(
@@ -810,7 +862,7 @@ var FormDialogSurface = ({
810
862
  ] })
811
863
  ] }),
812
864
  /* @__PURE__ */ jsx14("div", { className: styles.content, children }),
813
- footer && /* @__PURE__ */ jsx14("div", { className: styles.footer, children: footer })
865
+ footer
814
866
  ]
815
867
  }
816
868
  );
@@ -840,19 +892,12 @@ var ModalSurface = ({
840
892
  color: colors.textPrimary,
841
893
  padding: "16px 20px 0"
842
894
  },
843
- content: { padding: 20 },
844
- footer: {
845
- borderTop: `1px solid ${colors.borderNeutral}`,
846
- padding: "12px 20px",
847
- display: "flex",
848
- justifyContent: "flex-end",
849
- gap: 8
850
- }
895
+ content: { padding: 20 }
851
896
  });
852
897
  return /* @__PURE__ */ jsxs8(Modal2, { isOpen, onDismiss, isBlocking, className, styles: { main }, children: [
853
898
  title && /* @__PURE__ */ jsx15("div", { className: styles.title, children: title }),
854
899
  /* @__PURE__ */ jsx15("div", { className: styles.content, children }),
855
- footer && /* @__PURE__ */ jsx15("div", { className: styles.footer, children: footer })
900
+ footer
856
901
  ] });
857
902
  };
858
903
 
@@ -926,7 +971,7 @@ var CalloutSurface = ({
926
971
  };
927
972
 
928
973
  // src/overlays/TooltipSurface.tsx
929
- import { TooltipHost, DirectionalHint as DirectionalHint2, TooltipDelay } from "@fluentui/react";
974
+ import { TooltipHost as TooltipHost2, DirectionalHint as DirectionalHint2, TooltipDelay } from "@fluentui/react";
930
975
  import { jsx as jsx18 } from "react/jsx-runtime";
931
976
  var DELAY_MAP = {
932
977
  zero: TooltipDelay.zero,
@@ -938,7 +983,7 @@ var TooltipSurface = ({
938
983
  directionalHint = "bottomCenter",
939
984
  delay = "medium",
940
985
  children
941
- }) => /* @__PURE__ */ jsx18(TooltipHost, { content, directionalHint: DirectionalHint2[directionalHint], delay: DELAY_MAP[delay], children });
986
+ }) => /* @__PURE__ */ jsx18(TooltipHost2, { content, directionalHint: DirectionalHint2[directionalHint], delay: DELAY_MAP[delay], children });
942
987
 
943
988
  // src/composites/MasterDetailSurface.tsx
944
989
  import { mergeStyles as mergeStyles6 } from "@fluentui/react";