@phis/ui 0.4.3 → 0.4.4

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 (132) hide show
  1. package/components/controls/phi-anchor-nav-control.js +4 -1
  2. package/components/controls/phi-background-control.js +21 -6
  3. package/components/controls/phi-bullet-list-control.d.ts +7 -2
  4. package/components/controls/phi-bullet-list-control.js +4 -3
  5. package/components/controls/phi-color-control.js +4 -2
  6. package/components/controls/phi-drawer-control-adapter.js +3 -2
  7. package/components/controls/phi-inline-text-editor.js +7 -1
  8. package/components/controls/phi-length-control.js +1 -1
  9. package/components/controls/phi-ribbon-control.d.ts +13 -4
  10. package/components/controls/phi-ribbon-control.js +10 -6
  11. package/components/controls/phi-ribbon-wrap-control.d.ts +26 -0
  12. package/components/controls/phi-ribbon-wrap-control.js +25 -0
  13. package/components/controls/phi-select-control.js +5 -2
  14. package/components/controls/phi-series-chart-control.d.ts +48 -0
  15. package/components/controls/phi-series-chart-control.js +82 -0
  16. package/components/controls/phi-text-control.js +57 -3
  17. package/components/controls/phi-tree-control.js +1 -1
  18. package/components/controls/phi-truncated-hover-text.d.ts +15 -0
  19. package/components/controls/phi-truncated-hover-text.js +25 -0
  20. package/components/forms/link-target-form-control.d.ts +1 -1
  21. package/components/forms/link-target-form-control.js +2 -45
  22. package/components/forms/phi-form-widget-frame.js +4 -2
  23. package/components/layouts/clients/phi-three-column-layout-client.js +10 -1
  24. package/components/navigation/phi-link.js +6 -1
  25. package/components/regions/clients/cms-region-container-client.js +6 -1
  26. package/components/regions/phi-cms-region-container.js +3 -0
  27. package/components/regions/phi-cms-region-shell.d.ts +12 -1
  28. package/components/regions/phi-cms-region-shell.js +81 -19
  29. package/components/root/phi-config-provider.d.ts +13 -0
  30. package/components/root/phi-config-provider.js +12 -1
  31. package/components/root/phi-root-background.d.ts +7 -0
  32. package/components/root/phi-root-background.js +20 -1
  33. package/components/root/phi-root-theme-resolver.js +3 -0
  34. package/components/root/phi-shell-chrome-overlay.d.ts +12 -1
  35. package/components/root/phi-shell-chrome-overlay.js +19 -3
  36. package/components/shell/phi-antd-icon.d.ts +1 -0
  37. package/components/shell/phi-antd-icon.js +3 -0
  38. package/components/shell/use-phi-shell-sticky-top.js +61 -16
  39. package/components/surface/phi-surface-tone.d.ts +13 -0
  40. package/components/surface/phi-surface-tone.js +21 -0
  41. package/components/widgets/client/bullet-list-entries-editor.js +7 -1
  42. package/components/widgets/client/html-editor.js +1 -1
  43. package/components/widgets/client/shared/use-page-reference-options.d.ts +17 -0
  44. package/components/widgets/client/shared/use-page-reference-options.js +57 -0
  45. package/components/widgets/config/background.d.ts +32 -1
  46. package/components/widgets/config/background.js +41 -2
  47. package/components/widgets/label-sets/background.js +2 -0
  48. package/components/widgets/label-types/background.d.ts +1 -0
  49. package/components/widgets/label-types/background.js +1 -0
  50. package/components/widgets/shared/bullet-list-vocabulary.d.ts +10 -0
  51. package/components/widgets/shared/bullet-list-vocabulary.js +5 -2
  52. package/components/widgets/shared/card-body-client.d.ts +5 -3
  53. package/components/widgets/shared/card-body-client.js +49 -6
  54. package/components/widgets/shared/card-vocabulary.d.ts +12 -2
  55. package/components/widgets/shared/card-vocabulary.js +7 -0
  56. package/components/widgets/shared/markdown-body-client.js +39 -10
  57. package/helpers/cms-container-chrome.js +6 -1
  58. package/helpers/locale.d.ts +8 -0
  59. package/helpers/locale.js +18 -0
  60. package/helpers/shell-region-style.js +2 -1
  61. package/helpers/surface-style.d.ts +12 -1
  62. package/helpers/surface-style.js +28 -2
  63. package/module-dashboard-cards.d.ts +1 -1
  64. package/module-dashboard-cards.js +1 -1
  65. package/package.json +1 -1
  66. package/plugins/runtime-modules/area-definitions.d.ts +0 -42
  67. package/plugins/runtime-modules/area-definitions.js +3 -8
  68. package/plugins/runtime-modules/builder/clients/inspector-config-field.js +20 -1
  69. package/plugins/runtime-modules/builder/clients/layout-scaffold-overlays.js +10 -5
  70. package/plugins/runtime-modules/builder/edit-scaffold-drawer.js +3 -2
  71. package/plugins/runtime-modules/builder/render-root-node-scaffold.js +25 -15
  72. package/plugins/runtime-modules/builder/trees/phi-default-builder-area-preset-tree.js +4 -0
  73. package/plugins/runtime-modules/builder/widgets/structure-region/built-in.js +15 -4
  74. package/plugins/runtime-modules/client-authoring-module.d.ts +8 -1
  75. package/plugins/runtime-modules/client-authoring-module.js +13 -3
  76. package/plugins/runtime-modules/core/widgets/brand/client.d.ts +12 -3
  77. package/plugins/runtime-modules/core/widgets/brand/client.js +3 -11
  78. package/plugins/runtime-modules/core/widgets/brand/home.d.ts +15 -0
  79. package/plugins/runtime-modules/core/widgets/brand/home.js +18 -0
  80. package/plugins/runtime-modules/core/widgets/brand/plugin.js +3 -2
  81. package/plugins/runtime-modules/core/widgets/brand/server.d.ts +2 -2
  82. package/plugins/runtime-modules/core/widgets/brand/server.js +2 -2
  83. package/plugins/runtime-modules/core/widgets/card/config.d.ts +6 -1
  84. package/plugins/runtime-modules/core/widgets/card/config.js +19 -3
  85. package/plugins/runtime-modules/core/widgets/card/editor.js +13 -3
  86. package/plugins/runtime-modules/core/widgets/card/server.js +1 -0
  87. package/plugins/runtime-modules/core/widgets/description/client.js +3 -3
  88. package/plugins/runtime-modules/core/widgets/description/config.d.ts +1 -0
  89. package/plugins/runtime-modules/core/widgets/description/editor.js +3 -2
  90. package/plugins/runtime-modules/core/widgets/description/server.js +3 -2
  91. package/plugins/runtime-modules/core/widgets/entry-list/config.d.ts +1 -0
  92. package/plugins/runtime-modules/core/widgets/gallery/config.d.ts +1 -0
  93. package/plugins/runtime-modules/core/widgets/header-navigation/client.js +2 -2
  94. package/plugins/runtime-modules/core/widgets/html/client.js +1 -1
  95. package/plugins/runtime-modules/core/widgets/markdown/config.d.ts +320 -2
  96. package/plugins/runtime-modules/core/widgets/markdown/config.js +18 -4
  97. package/plugins/runtime-modules/core/widgets/markdown/editor.js +9 -1
  98. package/plugins/runtime-modules/core/widgets/markdown-toc/client.js +6 -1
  99. package/plugins/runtime-modules/core/widgets/markdown-toc/config.d.ts +14 -0
  100. package/plugins/runtime-modules/core/widgets/markdown-toc/config.js +14 -2
  101. package/plugins/runtime-modules/core/widgets/quick-links/authoring.js +7 -2
  102. package/plugins/runtime-modules/core/widgets/quick-links/client.js +2 -2
  103. package/plugins/runtime-modules/core/widgets/quick-links/items.d.ts +0 -2
  104. package/plugins/runtime-modules/core/widgets/quick-links/items.js +0 -15
  105. package/plugins/runtime-modules/core/widgets/result/config.d.ts +1 -0
  106. package/plugins/runtime-modules/core/widgets/sidebar-navigation/authoring.js +1 -1
  107. package/plugins/runtime-modules/core/widgets/sidebar-navigation/client.js +1 -1
  108. package/plugins/runtime-modules/core/widgets/simple-text/client.js +8 -3
  109. package/plugins/runtime-modules/core/widgets/simple-text/config.d.ts +1 -0
  110. package/plugins/runtime-modules/core/widgets/simple-text/editor.js +2 -2
  111. package/plugins/runtime-modules/core/widgets/table/config.d.ts +3 -0
  112. package/plugins/runtime-modules/dashboard/card-rows.d.ts +3 -1
  113. package/plugins/runtime-modules/dashboard/card-rows.js +30 -2
  114. package/plugins/runtime-modules/dashboard/clients/dashboard-card-view.js +29 -7
  115. package/plugins/runtime-modules/dashboard/trees/phi-default-builder-dashboard-page-tree.js +5 -40
  116. package/plugins/runtime-modules/theme/widgets/brand-controls/client.js +19 -15
  117. package/plugins/runtime-modules/theme/widgets/brand-controls/config.js +5 -9
  118. package/server-helpers/cms-link-targets.js +6 -0
  119. package/styles/controls.css +64 -2
  120. package/styles/layout.css +45 -2
  121. package/styles/shell.css +73 -0
  122. package/theme/phi-antd-token-resolver.js +20 -1
  123. package/theme/phi-theme-tokens.d.ts +1 -1
  124. package/theme/phi-theme-tokens.js +1 -0
  125. package/theme/phi-theme.d.ts +5 -0
  126. package/theme/phi-theme.js +21 -0
  127. package/theme/phi-tokens.d.ts +16 -15
  128. package/theme/phi-tokens.js +16 -15
  129. package/types/cms-plugins.d.ts +2 -0
  130. package/types/dashboard-cards.d.ts +39 -4
  131. package/types/dashboard-cards.js +6 -0
  132. package/types/site-theme.d.ts +10 -2
@@ -8,5 +8,8 @@ import { Anchor } from "antd";
8
8
  * page, the shape a table of contents takes. It stays where it is drawn and never pins itself.
9
9
  */
10
10
  export function PhiAnchorNavControl({ items, offsetTop = 0, style }) {
11
- return _jsx(Anchor, { affix: false, offsetTop: offsetTop, items: [...items], style: style });
11
+ return (_jsx(Anchor, { affix: false, offsetTop: offsetTop, items: [...items], className: "phi-anchor-nav-control",
12
+ // As wide as its slot and no wider: Ant Design sizes the list to its longest title, which in a
13
+ // narrow sider ran past the edge. Within the slot a long title ends in an ellipsis, Ant Design's own.
14
+ style: { maxWidth: "100%", minWidth: 0, ...style } }));
12
15
  }
@@ -50,7 +50,7 @@ const BACKGROUND_IMAGE_VARIANT_OPTIONS = [
50
50
  ];
51
51
  function toPickerValue(background) {
52
52
  const normalized = normalizePhiBackgroundWidgetConfig(background ?? null);
53
- if (normalized.base.kind === "none") {
53
+ if (normalized.base.kind === "none" || normalized.base.kind === "theme") {
54
54
  return "rgba(255, 255, 255, 0)";
55
55
  }
56
56
  if (normalized.base.kind === "image") {
@@ -126,6 +126,7 @@ export function PhiBackgroundControl({ value, config, disabled = false, mode = "
126
126
  { value: "color", label: labels.base.color },
127
127
  { value: "gradient", label: labels.base.gradient },
128
128
  { value: "image", label: labels.base.image },
129
+ { value: "theme", label: labels.base.theme },
129
130
  ];
130
131
  const baseKindOptions = baseKindCatalog.filter((option) => baseKinds.includes(option.value));
131
132
  const backgroundPositionOptions = [
@@ -223,7 +224,13 @@ export function PhiBackgroundControl({ value, config, disabled = false, mode = "
223
224
  const isOfferedFilter = (filter) => filters.includes(filter) && phiBackgroundSupportsFilter(currentValue, filter);
224
225
  const filterKindOptions = filterKindCatalog.filter((option) => option.value === "none" || isOfferedFilter(option.value));
225
226
  const activeFilterKind = currentValue.filter && isOfferedFilter(currentValue.filter) ? currentValue.filter : "none";
226
- const canOpenPreview = currentValue.base.kind !== "none";
227
+ // The Theme's Overlay is edited in the Theme workspace; here it has nothing to open.
228
+ const canOpenPreview = currentValue.base.kind !== "none" && currentValue.base.kind !== "theme";
229
+ /*
230
+ * A `theme` Base is the Theme's Overlay whole -- its own Overlay and pane included -- so the Background
231
+ * offers nothing to lay over it, and the Filter segment has nothing to offer either.
232
+ */
233
+ const offersOverlay = currentValue.base.kind !== "theme";
227
234
  useEffect(() => {
228
235
  if (currentValue.base.kind === "color") {
229
236
  lastColorBaseRef.current = currentValue.base;
@@ -298,6 +305,11 @@ export function PhiBackgroundControl({ value, config, disabled = false, mode = "
298
305
  if (kind === currentValue.base.kind) {
299
306
  return;
300
307
  }
308
+ // The last colour, gradient and picture stay remembered (the effect above), so switching back finds them.
309
+ if (kind === "theme") {
310
+ emit({ base: { kind: "theme" }, overlay: null, filter: null, motion: null });
311
+ return;
312
+ }
301
313
  if (kind === "none") {
302
314
  emit({
303
315
  ...currentValue,
@@ -650,7 +662,10 @@ export function PhiBackgroundControl({ value, config, disabled = false, mode = "
650
662
  return (_jsx(ConfigPreviewShell, { expanded: canOpenPreview, children: renderPreviewPicker() }));
651
663
  }
652
664
  const showPreview = mode !== "config";
653
- return (_jsxs(PhiFlexControl, { vertical: true, gap: 12, style: { width: "100%" }, children: [showPreview ? (_jsx(ConfigPreviewShell, { expanded: canOpenPreview, children: renderPreviewPicker() })) : null, _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: token.paddingXS, width: "100%" }, children: [_jsx(Typography.Text, { children: labels.sections.base }), _jsx(PhiSegmentedControl, { block: true, disabled: isDisabled, value: currentValue.base.kind, options: baseKindOptions, onChange: (next) => updateBaseKind(next) })] }), currentValue.base.kind === "image" ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: token.paddingSM, width: "100%" }, children: [_jsxs(Flex, { align: "center", justify: "space-between", gap: token.paddingXS, wrap: false, style: { width: "100%" }, children: [_jsx(Typography.Text, { children: labels.sections.image }), currentValue.base.sourceKind === "asset" && currentValue.base.assetId != null ? (_jsx(Typography.Text, { type: "secondary", style: { fontSize: token.fontSizeSM, textAlign: "right" }, children: currentValue.base.assetId })) : null] }), _jsxs("div", { style: {
665
+ return (_jsxs(PhiFlexControl, { vertical: true, gap: 12, style: { width: "100%" }, children: [showPreview ? (_jsx(ConfigPreviewShell, { expanded: canOpenPreview, children: renderPreviewPicker() })) : null, _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: token.paddingXS, width: "100%" }, children: [_jsx(Typography.Text, { children: labels.sections.base }), _jsx(PhiSelectControl, { ariaLabel: labels.sections.base, disabled: isDisabled, value: currentValue.base.kind, options: baseKindOptions, style: { width: "100%" }, onChange: (next) => {
666
+ if (next != null)
667
+ updateBaseKind(next);
668
+ } })] }), currentValue.base.kind === "image" ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: token.paddingSM, width: "100%" }, children: [_jsxs(Flex, { align: "center", justify: "space-between", gap: token.paddingXS, wrap: false, style: { width: "100%" }, children: [_jsx(Typography.Text, { children: labels.sections.image }), currentValue.base.sourceKind === "asset" && currentValue.base.assetId != null ? (_jsx(Typography.Text, { type: "secondary", style: { fontSize: token.fontSizeSM, textAlign: "right" }, children: currentValue.base.assetId })) : null] }), _jsxs("div", { style: {
654
669
  display: "grid",
655
670
  gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
656
671
  gap: token.paddingSM,
@@ -676,13 +691,13 @@ export function PhiBackgroundControl({ value, config, disabled = false, mode = "
676
691
  }, children: [_jsx(Select, { value: currentValue.base.position ?? "center", disabled: isDisabled, style: { width: "100%" }, options: backgroundPositionOptions, onChange: (next) => updateImageField("position", next) }), _jsx(Select, { value: currentValue.base.size ?? "cover", disabled: isDisabled, style: { width: "100%" }, options: backgroundSizeOptions, onChange: (next) => updateImageField("size", next) }), _jsx(Select, { value: currentValue.base.repeat ?? "no-repeat", disabled: isDisabled, style: { width: "100%" }, options: backgroundRepeatOptions, onChange: (next) => updateImageField("repeat", next) })] }), backgroundMotionOptions.length > 1 ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: token.paddingXS, width: "100%" }, children: [_jsx(Typography.Text, { children: labels.sections.motion }), _jsx(PhiSegmentedControl, { block: true, value: activeMotionMode, options: backgroundMotionOptions, disabled: isDisabled, onChange: updateMotionMode }), activeParallaxMotion ? (_jsxs(_Fragment, { children: [_jsx(Typography.Text, { children: labels.motion.strength }), _jsx(PhiSliderControl, { min: 0, max: 1, step: 0.05, ariaLabel: labels.motion.strength, value: activeParallaxMotion.strength ?? resolvePhiBackgroundParallaxDefaultStrength(activeParallaxMotion.travel), disabled: isDisabled, tooltipSuffix: "\u00D7", style: { width: "100%" }, onChange: (strength) => {
677
692
  beginSliderEdit();
678
693
  updateParallaxMotion({ strength });
679
- }, onChangeComplete: commitSliderEdit }), _jsx(Typography.Text, { children: labels.motion.direction }), _jsx(PhiSegmentedControl, { block: true, value: activeParallaxMotion.direction ?? "natural", options: backgroundMotionDirectionOptions, disabled: isDisabled, onChange: (direction) => updateParallaxMotion({ direction }) }), _jsx(Typography.Text, { children: labels.motion.travel }), _jsx(PhiSegmentedControl, { block: true, value: activeParallaxMotion.travel ?? "rate", options: backgroundMotionTravelOptions, disabled: isDisabled, onChange: (travel) => updateParallaxMotion({ travel }) })] })) : null] })) : null] })) : currentValue.base.kind !== "none" ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: token.paddingSM, width: "100%" }, children: [currentValue.base.kind === "color" ? (_jsxs(Flex, { align: "center", justify: "space-between", gap: token.paddingSM, wrap: "wrap", children: [_jsx(Typography.Text, { children: labels.base.color }), _jsx(PhiColorControl, { ...colorPickerTransaction, value: currentValue.base.color, disabled: isDisabled, presets: colorPickerPresets, placement: colorPickerPlacement, onChange: (value) => {
694
+ }, onChangeComplete: commitSliderEdit }), _jsx(Typography.Text, { children: labels.motion.direction }), _jsx(PhiSegmentedControl, { block: true, value: activeParallaxMotion.direction ?? "natural", options: backgroundMotionDirectionOptions, disabled: isDisabled, onChange: (direction) => updateParallaxMotion({ direction }) }), _jsx(Typography.Text, { children: labels.motion.travel }), _jsx(PhiSegmentedControl, { block: true, value: activeParallaxMotion.travel ?? "rate", options: backgroundMotionTravelOptions, disabled: isDisabled, onChange: (travel) => updateParallaxMotion({ travel }) })] })) : null] })) : null] })) : currentValue.base.kind === "color" || currentValue.base.kind === "gradient" ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: token.paddingSM, width: "100%" }, children: [currentValue.base.kind === "color" ? (_jsxs(Flex, { align: "center", justify: "space-between", gap: token.paddingSM, wrap: "wrap", children: [_jsx(Typography.Text, { children: labels.base.color }), _jsx(PhiColorControl, { ...colorPickerTransaction, value: currentValue.base.color, disabled: isDisabled, presets: colorPickerPresets, placement: colorPickerPlacement, onChange: (value) => {
680
695
  if (value != null)
681
696
  updateBaseFromCss(value);
682
697
  } })] })) : (_jsxs(Flex, { align: "center", justify: "space-between", gap: token.paddingSM, wrap: "wrap", children: [_jsx(Typography.Text, { children: labels.base.gradient }), _jsx(PhiColorControl, { ...colorPickerTransaction, mode: "gradient", value: derivedPickerValue, disabled: isDisabled, presets: colorPickerPresets, placement: colorPickerPlacement, onChange: (value) => {
683
698
  if (value != null)
684
699
  updateBaseFromCss(value);
685
- } })] })), currentValue.base.kind === "gradient" ? (renderDirectionEditor(labels.gradient.direction, currentValue.base.direction, updateGradientDirection)) : null] })) : null, _jsx(Divider, { dashed: true, size: "small" }), _jsxs(PhiFlexControl, { vertical: true, gap: 8, style: { width: "100%" }, children: [_jsx(Typography.Text, { children: labels.sections.overlay }), _jsx(PhiSegmentedControl, { block: true, disabled: isDisabled, value: currentValue.overlay?.kind ?? "none", options: overlayKindOptions, onChange: (next) => updateOverlayKind(next) }), currentValue.overlay ? (_jsxs(PhiFlexControl, { vertical: true, gap: 8, style: { width: "100%" }, children: [currentValue.overlay.kind === "pattern" ? (_jsxs(Flex, { align: "center", justify: "space-between", gap: token.paddingSM, wrap: "wrap", children: [_jsx(Typography.Text, { children: labels.overlay.pattern }), _jsx(PhiSelectControl, { value: currentValue.overlay.patternKey, options: resolvedPatternOptions, disabled: isDisabled, style: { width: fieldControlWidth }, onChange: (patternKey) => {
700
+ } })] })), currentValue.base.kind === "gradient" ? (renderDirectionEditor(labels.gradient.direction, currentValue.base.direction, updateGradientDirection)) : null] })) : null, offersOverlay ? _jsx(Divider, { dashed: true, size: "small" }) : null, offersOverlay ? _jsxs(PhiFlexControl, { vertical: true, gap: 8, style: { width: "100%" }, children: [_jsx(Typography.Text, { children: labels.sections.overlay }), _jsx(PhiSegmentedControl, { block: true, disabled: isDisabled, value: currentValue.overlay?.kind ?? "none", options: overlayKindOptions, onChange: (next) => updateOverlayKind(next) }), currentValue.overlay ? (_jsxs(PhiFlexControl, { vertical: true, gap: 8, style: { width: "100%" }, children: [currentValue.overlay.kind === "pattern" ? (_jsxs(Flex, { align: "center", justify: "space-between", gap: token.paddingSM, wrap: "wrap", children: [_jsx(Typography.Text, { children: labels.overlay.pattern }), _jsx(PhiSelectControl, { value: currentValue.overlay.patternKey, options: resolvedPatternOptions, disabled: isDisabled, style: { width: fieldControlWidth }, onChange: (patternKey) => {
686
701
  const provider = resolvePhiBackgroundPatternProvider(patternKey);
687
702
  if (!provider)
688
703
  return;
@@ -762,5 +777,5 @@ export function PhiBackgroundControl({ value, config, disabled = false, mode = "
762
777
  });
763
778
  }, style: { width: fieldControlWidth } })] }, field.key));
764
779
  })
765
- : null] })) : null] }), filterKindOptions.length > 1 ? (_jsxs(_Fragment, { children: [_jsx(Divider, { dashed: true, size: "small" }), _jsxs(PhiFlexControl, { vertical: true, gap: 8, style: { width: "100%" }, children: [_jsx(Typography.Text, { children: labels.sections.filter }), _jsx(PhiSegmentedControl, { block: true, disabled: isDisabled, value: activeFilterKind, options: filterKindOptions, onChange: (next) => updateFilterKind(next) })] })] })) : null] }));
780
+ : null] })) : null] }) : null, filterKindOptions.length > 1 ? (_jsxs(_Fragment, { children: [_jsx(Divider, { dashed: true, size: "small" }), _jsxs(PhiFlexControl, { vertical: true, gap: 8, style: { width: "100%" }, children: [_jsx(Typography.Text, { children: labels.sections.filter }), _jsx(PhiSegmentedControl, { block: true, disabled: isDisabled, value: activeFilterKind, options: filterKindOptions, onChange: (next) => updateFilterKind(next) })] })] })) : null] }));
766
781
  }
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from "react";
1
+ import type { CSSProperties, ReactNode } from "react";
2
2
  import { type PhiBulletListNode, type PhiResolvedBulletListStyle } from "../widgets/shared/bullet-list-vocabulary";
3
3
  export type PhiBulletListControlProps = {
4
4
  items: readonly PhiBulletListNode<ReactNode>[];
@@ -15,6 +15,11 @@ export type PhiBulletListControlProps = {
15
15
  * an entry that is a text field has the field's height instead.
16
16
  */
17
17
  markerLineHeight?: string;
18
+ /**
19
+ * The space the list keeps around it as a block among blocks -- a Markdown text's list takes its
20
+ * paragraphs' spacing, so the block after the last entry stands as far off as after a paragraph.
21
+ */
22
+ blockSpacing?: Pick<CSSProperties, "marginBlockStart" | "marginBlockEnd">;
18
23
  };
19
24
  /**
20
25
  * A list, as the house draws one: a marker per level, entries with a box of their own or none, lines
@@ -29,4 +34,4 @@ export type PhiBulletListControlProps = {
29
34
  * blocks. An entry's children are the list one level below it, drawn inside the entry under its content,
30
35
  * so they are indented by the marker and keep to the entry's box.
31
36
  */
32
- export declare function PhiBulletListControl({ items, listStyle, depth, ordered, start, markerLineHeight, }: PhiBulletListControlProps): import("react").JSX.Element;
37
+ export declare function PhiBulletListControl({ items, listStyle, depth, ordered, start, markerLineHeight, blockSpacing, }: PhiBulletListControlProps): import("react").JSX.Element;
@@ -66,7 +66,7 @@ function renderMarker(marker, icon, number) {
66
66
  * blocks. An entry's children are the list one level below it, drawn inside the entry under its content,
67
67
  * so they are indented by the marker and keep to the entry's box.
68
68
  */
69
- export function PhiBulletListControl({ items, listStyle = resolvePhiBulletListStyle(null), depth = 1, ordered = false, start = 1, markerLineHeight = "1lh", }) {
69
+ export function PhiBulletListControl({ items, listStyle = resolvePhiBulletListStyle(null), depth = 1, ordered = false, start = 1, markerLineHeight = "1lh", blockSpacing, }) {
70
70
  const level = resolvePhiBulletListLevel(depth);
71
71
  const marker = ordered ? "number" : listStyle.markers[level];
72
72
  const gap = resolvePhiSpacingScaleValue(listStyle.gap);
@@ -75,11 +75,12 @@ export function PhiBulletListControl({ items, listStyle = resolvePhiBulletListSt
75
75
  // A line between entries that have edges of their own would only double them.
76
76
  const divided = !boxed && hasPhiFlag(listStyle.flags, PhiBulletListFlag.Dividers);
77
77
  const ListTag = marker === "number" ? "ol" : "ul";
78
- const listCss = { display: "grid", gap, margin: 0, padding: 0, listStyle: "none", width: "100%", minWidth: 0 };
78
+ const listCss = { display: "grid", gap, margin: 0, padding: 0, listStyle: "none", width: "100%", minWidth: 0, ...blockSpacing };
79
79
  return (_jsx(ListTag, { style: listCss, ...(ListTag === "ol" && start !== 1 ? { start } : {}), children: items.map((item, index) => (_jsxs("li", { className: surface.className, style: {
80
80
  display: "flex",
81
81
  alignItems: "flex-start",
82
- gap: PHI_SPACE.sm,
82
+ // A marker belongs to its words: one small step between them, not the space between blocks.
83
+ gap: PHI_SPACE.xs,
83
84
  minWidth: 0,
84
85
  ...surface.style,
85
86
  ...(boxed ? { padding: `${PHI_SPACE.sm} ${PHI_SPACE.md}` } : {}),
@@ -82,8 +82,10 @@ export function PhiColorControl({ value = null, defaultValue = "#f0f0f0", disabl
82
82
  */
83
83
  ...(children == null ? { width: "fit-content" } : {}),
84
84
  },
85
- popupOverlayInner: {
86
- padding: 12,
85
+ popup: {
86
+ container: {
87
+ padding: 12,
88
+ },
87
89
  },
88
90
  }, panelRender: (_, extra) => {
89
91
  const { Picker } = extra.components;
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  */
7
7
  import { useSyncExternalStore } from "react";
8
8
  import { Drawer } from "antd";
9
- import { PHI_SPACE } from "../../theme/antd-css-var-contract";
9
+ import { PHI_FONT_SIZE, PHI_SPACE } from "../../theme/antd-css-var-contract";
10
10
  import { PHI_OVERLAY_DEFAULT_MASK, resolvePhiOverlayDismissSource, resolvePhiOverlayMaskPresentation, } from "./phi-overlay-control-contract";
11
11
  const subscribeHydration = () => () => undefined;
12
12
  const PHI_DRAWER_HEADER_MIN_BLOCK_SIZE = `calc(var(--ant-control-height) + ${PHI_SPACE.xs} + ${PHI_SPACE.xs})`;
@@ -51,7 +51,8 @@ export function PhiDrawerControlAdapter({ open, title, header, body, footer, clo
51
51
  padding: 0,
52
52
  background: "transparent",
53
53
  },
54
- title: { width: "100%", minWidth: 0 },
54
+ // The title in the body's size, as a Modal's is (theme/phi-theme.ts); Ant Design sets a Drawer's at `lg`.
55
+ title: { width: "100%", minWidth: 0, fontSize: PHI_FONT_SIZE.base, lineHeight: "var(--ant-line-height)" },
55
56
  close: {
56
57
  position: "absolute",
57
58
  top: "50%",
@@ -46,7 +46,13 @@ export function PhiInlineTextEditor({ value, onChange, onCommit, onCancel, onFoc
46
46
  * At one column it is the smallest thing in the box and the text is the widest, which is the
47
47
  * order this field is built on; it is also the width an empty field keeps.
48
48
  */
49
- cols: fitContent ? 1 : undefined, style: fitContent ? { width: "100%", overflow: "hidden" } : { width: "100%" }, inputStyle: inputStyle, onChange: (nextValue) => onChange(nextValue ?? ""), onFocus: onFocus, onBlur: (event) => {
49
+ cols: fitContent ? 1 : undefined,
50
+ /*
51
+ * `overflowX` and `overflowY`, not the `overflow` shorthand: while it measures, the text area's
52
+ * `autoSize` sets both longhands and takes them away again, and React warns when a longhand
53
+ * leaves beside its shorthand -- the shorthand would not be written back.
54
+ */
55
+ style: fitContent ? { width: "100%", overflowX: "hidden", overflowY: "hidden" } : { width: "100%" }, inputStyle: inputStyle, onChange: (nextValue) => onChange(nextValue ?? ""), onFocus: onFocus, onBlur: (event) => {
50
56
  const shouldCommit = !cancelPendingRef.current;
51
57
  cancelPendingRef.current = false;
52
58
  if (shouldCommit) {
@@ -20,7 +20,7 @@ export function PhiLengthControl({ value, label, placeholder, min, max, step, pr
20
20
  * is kept shut and its change ignored instead.
21
21
  */
22
22
  const controlDisabled = disabled || (!onChange && !readOnly);
23
- const control = (_jsxs(PhiCompactGroupControl, { block: true, size: size, style: { width: "100%", minWidth: 0, ...style }, children: [_jsx(InputNumber, { "aria-label": placeholder ?? label ?? "Length", controls: false, disabled: controlDisabled, readOnly: readOnly, min: min, max: max, step: step, precision: precision, placeholder: placeholder, value: part?.value ?? null, style: { flex: "1 1 0", minWidth: 0 }, onChange: (nextValue) => onChange?.(typeof nextValue === "number" ? serializePhiCssLength(nextValue, unit) : null) }), _jsx(Select, { "aria-label": `${placeholder ?? label ?? "Length"} unit`, disabled: controlDisabled, open: readOnly ? false : undefined, options: PHI_CSS_LENGTH_UNITS.map((option) => ({ value: option, label: option })), popupMatchSelectWidth: false, getPopupContainer: getPopupContainer, showSearch: false, suffixIcon: null, variant: "filled", value: unit, style: { flex: "0 0 38px", width: 38 }, styles: {
23
+ const control = (_jsxs(PhiCompactGroupControl, { block: true, size: size, style: { width: "100%", minWidth: 0, ...style }, children: [_jsx(InputNumber, { "aria-label": placeholder ?? label ?? "Length", controls: false, disabled: controlDisabled, readOnly: readOnly, min: min, max: max, step: step, precision: precision, placeholder: placeholder, value: part?.value ?? null, style: { flex: "1 1 0", minWidth: 0 }, onChange: (nextValue) => onChange?.(typeof nextValue === "number" ? serializePhiCssLength(nextValue, unit) : null) }), _jsx(Select, { "aria-label": `${placeholder ?? label ?? "Length"} unit`, disabled: controlDisabled, open: readOnly ? false : undefined, options: PHI_CSS_LENGTH_UNITS.map((option) => ({ value: option, label: option })), popupMatchSelectWidth: false, getPopupContainer: getPopupContainer, showSearch: false, suffix: null, variant: "filled", value: unit, style: { flex: "0 0 38px", width: 38 }, styles: {
24
24
  root: { paddingInline: 4 },
25
25
  content: { justifyContent: "center", textAlign: "center" },
26
26
  }, onChange: (nextUnit) => {
@@ -1,4 +1,4 @@
1
- import type { CSSProperties, ReactNode } from "react";
1
+ import type { CSSProperties, MouseEventHandler, ReactNode } from "react";
2
2
  export type PhiRibbonControlProps = {
3
3
  children: ReactNode;
4
4
  /** The band's ground; absent, the Theme's primary. Its words are the light text on any ground. */
@@ -9,6 +9,14 @@ export type PhiRibbonControlProps = {
9
9
  */
10
10
  placement?: "corner" | "edge";
11
11
  style?: CSSProperties;
12
+ /**
13
+ * The band's whole words, shown on hover when its ellipsis cut them short (`PhiTruncatedHoverText`).
14
+ * A band with a hover text takes the pointer, so it needs `onClick` to hand a click on to what lies
15
+ * under it.
16
+ */
17
+ fullText?: string;
18
+ /** What a click on the band does, for a band that takes the pointer: the box's own click, passed on. */
19
+ onClick?: MouseEventHandler<HTMLSpanElement>;
12
20
  };
13
21
  /**
14
22
  * A band of words at the end edge of a box: "Popular", "New", "-20 %".
@@ -23,7 +31,8 @@ export type PhiRibbonControlProps = {
23
31
  * box's top inset it was too thin to read. In the corner it stands over the inset and a little into the
24
32
  * first line's end, which a long eyebrow in a narrow box can reach.
25
33
  *
26
- * It sits above what the box draws, the highlight's ring included, and takes no clicks, so a card that
27
- * is a link stays one under its band.
34
+ * It sits above what the box draws, the highlight's ring included. Without a hover text it takes no
35
+ * clicks, so a card that is a link stays one under its band; with one it has to take the pointer to be
36
+ * pointed at, and hands a click on through `onClick`.
28
37
  */
29
- export declare function PhiRibbonControl({ children, color, placement, style }: PhiRibbonControlProps): import("react").JSX.Element;
38
+ export declare function PhiRibbonControl({ children, color, placement, style, fullText, onClick }: PhiRibbonControlProps): import("react").JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { PHI_COLOR, PHI_FONT_SIZE, PHI_SPACE } from "../../theme/antd-css-var-contract";
3
+ import { PhiTruncatedHoverText } from "./phi-truncated-hover-text";
3
4
  /**
4
5
  * A band of words at the end edge of a box: "Popular", "New", "-20 %".
5
6
  *
@@ -13,10 +14,11 @@ import { PHI_COLOR, PHI_FONT_SIZE, PHI_SPACE } from "../../theme/antd-css-var-co
13
14
  * box's top inset it was too thin to read. In the corner it stands over the inset and a little into the
14
15
  * first line's end, which a long eyebrow in a narrow box can reach.
15
16
  *
16
- * It sits above what the box draws, the highlight's ring included, and takes no clicks, so a card that
17
- * is a link stays one under its band.
17
+ * It sits above what the box draws, the highlight's ring included. Without a hover text it takes no
18
+ * clicks, so a card that is a link stays one under its band; with one it has to take the pointer to be
19
+ * pointed at, and hands a click on through `onClick`.
18
20
  */
19
- export function PhiRibbonControl({ children, color, placement = "edge", style }) {
21
+ export function PhiRibbonControl({ children, color, placement = "edge", style, fullText, onClick }) {
20
22
  const radius = "var(--phi-control-radius-sm)";
21
23
  const position = placement === "corner"
22
24
  ? {
@@ -31,7 +33,7 @@ export function PhiRibbonControl({ children, color, placement = "edge", style })
31
33
  borderStartStartRadius: radius,
32
34
  borderEndStartRadius: radius,
33
35
  };
34
- return (_jsx("span", { className: "phi-ribbon-control", style: {
36
+ const band = (_jsx("span", { className: "phi-ribbon-control", style: {
35
37
  position: "absolute",
36
38
  insetInlineEnd: 0,
37
39
  zIndex: 4,
@@ -46,8 +48,10 @@ export function PhiRibbonControl({ children, color, placement = "edge", style })
46
48
  whiteSpace: "nowrap",
47
49
  overflow: "hidden",
48
50
  textOverflow: "ellipsis",
49
- pointerEvents: "none",
51
+ pointerEvents: fullText ? "auto" : "none",
52
+ ...(fullText && onClick ? { cursor: "pointer" } : {}),
50
53
  ...position,
51
54
  ...style,
52
- }, children: children }));
55
+ }, ...(onClick ? { onClick } : {}), children: children }));
56
+ return fullText ? _jsx(PhiTruncatedHoverText, { title: fullText, children: band }) : band;
53
57
  }
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from "react";
2
+ export type PhiRibbonWrapControlProps = {
3
+ /** The box the ribbon hangs on. */
4
+ children: ReactNode;
5
+ /** The ribbon's words. */
6
+ text: ReactNode;
7
+ /** The ribbon's ground; absent, the Theme's primary. The fold under it is a darker shade of it. */
8
+ color?: string;
9
+ /** Takes the pointer, for an editor that writes in the ribbon; otherwise clicks pass to the box. */
10
+ interactive?: boolean;
11
+ className?: string;
12
+ };
13
+ /**
14
+ * A ribbon that hangs over a box's end edge, with a fold behind it: Ant Design's `Badge.Ribbon`.
15
+ *
16
+ * The other band, `PhiRibbonControl`, stands inside the box and needs nothing from it. This one reaches
17
+ * past the box's edge, which a box that clips -- a card does, for its picture's corners and its zoom --
18
+ * cannot give a child of its own. So it wraps the box: the wrapper does not clip, the box keeps clipping
19
+ * what is its own, and the ribbon is the box's sibling. The wrapper is a column that passes the slot's
20
+ * width and height on to the box. Whatever stands around the wrapper must leave the overhang room: a
21
+ * Layout that clips at the box's edge cuts the ribbon off as it would any overhang.
22
+ *
23
+ * Imported from its own module, not from `antd`'s Badge: the Badge brings its scroll-number motion along,
24
+ * which a ribbon never uses.
25
+ */
26
+ export declare function PhiRibbonWrapControl({ children, text, color, interactive, className }: PhiRibbonWrapControlProps): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import Ribbon from "antd/es/badge/Ribbon";
4
+ /**
5
+ * A ribbon that hangs over a box's end edge, with a fold behind it: Ant Design's `Badge.Ribbon`.
6
+ *
7
+ * The other band, `PhiRibbonControl`, stands inside the box and needs nothing from it. This one reaches
8
+ * past the box's edge, which a box that clips -- a card does, for its picture's corners and its zoom --
9
+ * cannot give a child of its own. So it wraps the box: the wrapper does not clip, the box keeps clipping
10
+ * what is its own, and the ribbon is the box's sibling. The wrapper is a column that passes the slot's
11
+ * width and height on to the box. Whatever stands around the wrapper must leave the overhang room: a
12
+ * Layout that clips at the box's edge cuts the ribbon off as it would any overhang.
13
+ *
14
+ * Imported from its own module, not from `antd`'s Badge: the Badge brings its scroll-number motion along,
15
+ * which a ribbon never uses.
16
+ */
17
+ export function PhiRibbonWrapControl({ children, text, color, interactive = false, className }) {
18
+ const indicator = interactive ? { zIndex: 4 } : { zIndex: 4, pointerEvents: "none" };
19
+ /*
20
+ * An editor's field in the ribbon measures itself against the ribbon, which is as wide as its words:
21
+ * empty, both were nothing, and only the fold showed. Room for a word, as the inner band keeps it.
22
+ */
23
+ const content = interactive ? { display: "inline-block", minWidth: "6em" } : undefined;
24
+ return (_jsx(Ribbon, { text: text, ...(color ? { color } : {}), rootClassName: ["phi-ribbon-wrap-control", className].filter(Boolean).join(" "), styles: { indicator, ...(content ? { content } : {}) }, children: children }));
25
+ }
@@ -60,6 +60,9 @@ function groupPhiSelectOptions(resolved) {
60
60
  }
61
61
  export function PhiSelectControl({ id, value, label, description, ariaLabel, placeholder, disabled, readOnly, presentation = "select", allowCustom, allowClear, options, size, variant, popupMatchSelectWidth = false, getPopupContainer, popupRootClassName, popupZIndex, style, onFocus, onBlur, onOpenChange, onSearch, filterOptionsLocally = true, onChange, }) {
62
62
  const { labelId, labelledBy } = usePhiControlLabel(label, ariaLabel);
63
+ // A compact field opens a compact list: its options write small, as the field does (controls.css).
64
+ const popupClassName = [popupRootClassName, size === "small" ? "phi-select-popup--sm" : null]
65
+ .filter(Boolean).join(" ") || undefined;
63
66
  const resolvedOptions = groupPhiSelectOptions(separatePhiSelectOptions(options.map((option) => ({
64
67
  value: option.value,
65
68
  // rc-select synthesizes native HTML title attributes from primitive labels.
@@ -74,7 +77,7 @@ export function PhiSelectControl({ id, value, label, description, ariaLabel, pla
74
77
  (value === undefined || typeof value === "string") &&
75
78
  options.every((option) => typeof option.value === "string");
76
79
  if (canUseTextEntry) {
77
- const control = (_jsx(AutoComplete, { "aria-label": ariaLabel, "aria-labelledby": labelledBy, value: value, placeholder: placeholder, disabled: controlDisabled, allowClear: allowClear, options: resolvedOptions, optionRender: (resolvedOption) => isPhiSelectSeparatorEntry(resolvedOption.data) ? renderPhiSelectSeparator() : (_jsx(PhiControlOptionContent, { option: resolvedOption.data.option, presentation: "dropdown" })), size: size, variant: variant, popupMatchSelectWidth: popupMatchSelectWidth, getPopupContainer: getPopupContainer, classNames: popupRootClassName ? { popup: { root: popupRootClassName } } : undefined, styles: popupZIndex == null ? undefined : { popup: { root: { zIndex: popupZIndex } } }, filterOption: !filterOptionsLocally
80
+ const control = (_jsx(AutoComplete, { "aria-label": ariaLabel, "aria-labelledby": labelledBy, value: value, placeholder: placeholder, disabled: controlDisabled, allowClear: allowClear, options: resolvedOptions, optionRender: (resolvedOption) => isPhiSelectSeparatorEntry(resolvedOption.data) ? renderPhiSelectSeparator() : (_jsx(PhiControlOptionContent, { option: resolvedOption.data.option, presentation: "dropdown" })), size: size, variant: variant, popupMatchSelectWidth: popupMatchSelectWidth, getPopupContainer: getPopupContainer, classNames: popupClassName ? { popup: { root: popupClassName } } : undefined, styles: popupZIndex == null ? undefined : { popup: { root: { zIndex: popupZIndex } } }, filterOption: !filterOptionsLocally
78
81
  ? false
79
82
  : (inputValue, option) => {
80
83
  // A group heading has no search label of its own; antd filters the options inside it.
@@ -90,6 +93,6 @@ export function PhiSelectControl({ id, value, label, description, ariaLabel, pla
90
93
  return selected
91
94
  ? _jsx(PhiControlOptionContent, { option: selected, presentation: "selection" })
92
95
  : selectedOption.label;
93
- }, size: size, variant: variant, popupMatchSelectWidth: popupMatchSelectWidth, getPopupContainer: getPopupContainer, classNames: popupRootClassName ? { popup: { root: popupRootClassName } } : undefined, styles: popupZIndex == null ? undefined : { popup: { root: { zIndex: popupZIndex } } }, showSearch: true, optionFilterProp: ["searchLabel", "value"], filterOption: filterOptionsLocally ? undefined : false, onSearch: onSearch, onFocus: onFocus, onBlur: onBlur, onOpenChange: onOpenChange, onChange: onChange, style: style }));
96
+ }, size: size, variant: variant, popupMatchSelectWidth: popupMatchSelectWidth, getPopupContainer: getPopupContainer, classNames: popupClassName ? { popup: { root: popupClassName } } : undefined, styles: popupZIndex == null ? undefined : { popup: { root: { zIndex: popupZIndex } } }, showSearch: true, optionFilterProp: ["searchLabel", "value"], filterOption: filterOptionsLocally ? undefined : false, onSearch: onSearch, onFocus: onFocus, onBlur: onBlur, onOpenChange: onOpenChange, onChange: onChange, style: style }));
94
97
  return _jsx(PhiLabeledControl, { label: label, labelId: labelId, description: description, fill: style?.width === "100%", children: control });
95
98
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Figures over time: a line per series across the width it is given.
3
+ *
4
+ * Drawn as SVG by hand rather than through a chart library. What a card asks of it is a shape -- rising,
5
+ * falling, a spike last Tuesday -- and its latest figure, not axes to read values off; a library would
6
+ * bring a second rendering model and a dependency to every Site for that. No hooks and no Ant Design, so
7
+ * a server render draws it as well as a client one.
8
+ *
9
+ * The drawing is stretched to its box (`preserveAspectRatio="none"`) with strokes that do not stretch
10
+ * with it, so the chart takes its container's width at any size. The last point of each series is the
11
+ * emphasized one, drawn over the SVG rather than in it: a circle in a stretched drawing would be an ellipse.
12
+ *
13
+ * Every colour is the Theme's variable (`PHI_COLOR`), never a resolved value, so a Surface `tone` reaches
14
+ * the lines. The first series is the Theme's primary, with an area under it; the second and third are the
15
+ * text colours, the third dashed, so two lines differ in more than hue.
16
+ *
17
+ * The scale starts at zero for figures that are never negative, which counts are: a line that starts at
18
+ * its own minimum turns a day with one more visitor into a cliff.
19
+ */
20
+ export type PhiSeriesChartControlSeries = {
21
+ label: string;
22
+ /** One value per key, in the keys' order. */
23
+ values: readonly number[];
24
+ };
25
+ export type PhiSeriesChartControlProps = {
26
+ /** What each point is called, already in words -- its day -- oldest first. */
27
+ keys: readonly string[];
28
+ series: readonly PhiSeriesChartControlSeries[];
29
+ /** A word for what is counted, already translated; said after each latest figure. */
30
+ unit?: string;
31
+ /** The page's locale, for the figures in the spoken label. */
32
+ locale: string;
33
+ /** What the chart is of, said before its latest figures. */
34
+ title?: string;
35
+ /** The drawing's height in pixels; its width is the container's. */
36
+ height?: number;
37
+ };
38
+ /** Where each series' points stand in the drawing's own coordinates. */
39
+ export declare function computePhiSeriesChartGeometry(series: readonly PhiSeriesChartControlSeries[], pointCount: number): {
40
+ baseline: number;
41
+ top: number;
42
+ series: {
43
+ x: number;
44
+ y: number;
45
+ value: number;
46
+ }[][];
47
+ };
48
+ export declare function PhiSeriesChartControl({ keys, series, unit, locale, title, height, }: PhiSeriesChartControlProps): import("react").JSX.Element;
@@ -0,0 +1,82 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PHI_COLOR, PHI_FONT_SIZE, PHI_SPACE } from "../../theme/antd-css-var-contract";
3
+ const VIEW_WIDTH = 100;
4
+ const VIEW_HEIGHT = 100;
5
+ /* Room above the highest point and below the baseline, so the emphasized dot is not clipped. */
6
+ const VIEW_INSET = 8;
7
+ const SERIES_STROKES = [
8
+ { color: PHI_COLOR.primary },
9
+ { color: PHI_COLOR.textSecondary },
10
+ { color: PHI_COLOR.textTertiary, dash: "4 3" },
11
+ ];
12
+ function strokeOf(index) {
13
+ return SERIES_STROKES[index % SERIES_STROKES.length];
14
+ }
15
+ function finite(values) {
16
+ return values.filter((value) => Number.isFinite(value));
17
+ }
18
+ /** Where each series' points stand in the drawing's own coordinates. */
19
+ export function computePhiSeriesChartGeometry(series, pointCount) {
20
+ const all = series.flatMap((entry) => finite(entry.values.slice(0, pointCount)));
21
+ const low = Math.min(0, ...all);
22
+ const high = Math.max(0, ...all);
23
+ const range = high - low || 1;
24
+ const xOf = (index) => (pointCount <= 1 ? VIEW_WIDTH / 2 : (index / (pointCount - 1)) * VIEW_WIDTH);
25
+ const yOf = (value) => VIEW_INSET + (1 - (value - low) / range) * (VIEW_HEIGHT - 2 * VIEW_INSET);
26
+ return {
27
+ baseline: yOf(Math.max(low, 0)),
28
+ top: yOf(high),
29
+ series: series.map((entry) => entry.values.slice(0, pointCount).flatMap((value, index) => Number.isFinite(value) ? [{ x: xOf(index), y: yOf(value), value }] : [])),
30
+ };
31
+ }
32
+ function round(value) {
33
+ return Math.round(value * 100) / 100;
34
+ }
35
+ function linePath(points) {
36
+ return points.map((point, index) => `${index === 0 ? "M" : "L"}${round(point.x)} ${round(point.y)}`).join(" ");
37
+ }
38
+ export function PhiSeriesChartControl({ keys, series, unit, locale, title, height = 64, }) {
39
+ const pointCount = keys.length;
40
+ const geometry = computePhiSeriesChartGeometry(series, pointCount);
41
+ const numbers = new Intl.NumberFormat(locale);
42
+ const lastKey = keys[pointCount - 1];
43
+ const latest = series.flatMap((entry) => {
44
+ const value = entry.values[pointCount - 1];
45
+ return value !== undefined && Number.isFinite(value)
46
+ ? [`${entry.label} ${numbers.format(value)}${unit ? ` ${unit}` : ""}`]
47
+ : [];
48
+ });
49
+ /* What a reader who cannot see the lines is told: what it is, the latest day, each series' figure on it. */
50
+ const spoken = [title, lastKey && latest.length > 0 ? `${lastKey}: ${latest.join(", ")}` : null]
51
+ .filter(Boolean)
52
+ .join(" -- ");
53
+ const first = geometry.series[0] ?? [];
54
+ const area = first.length > 1
55
+ ? `${linePath(first)} L${round(first[first.length - 1].x)} ${round(geometry.baseline)} L${round(first[0].x)} ${round(geometry.baseline)} Z`
56
+ : null;
57
+ return (_jsxs("figure", { style: { margin: 0, display: "grid", gap: PHI_SPACE.xxs, minWidth: 0 }, "data-phi-series-chart": "", children: [_jsxs("div", { style: { position: "relative", height, minWidth: 0 }, children: [_jsxs("svg", { role: "img", "aria-label": spoken || undefined, viewBox: `0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`, preserveAspectRatio: "none", width: "100%", height: height, style: { display: "block", overflow: "visible" }, children: [spoken ? _jsx("title", { children: spoken }) : null, _jsx("line", { x1: 0, x2: VIEW_WIDTH, y1: round(geometry.top), y2: round(geometry.top), vectorEffect: "non-scaling-stroke", strokeWidth: 1, strokeDasharray: "2 3", style: { stroke: PHI_COLOR.split } }), _jsx("line", { x1: 0, x2: VIEW_WIDTH, y1: round(geometry.baseline), y2: round(geometry.baseline), vectorEffect: "non-scaling-stroke", strokeWidth: 1, style: { stroke: PHI_COLOR.border } }), area ? (_jsx("path", { d: area, style: { fill: `color-mix(in srgb, ${PHI_COLOR.primary} 14%, transparent)`, stroke: "none" } })) : null, geometry.series.map((points, index) => points.length > 1 ? (_jsx("path", { d: linePath(points), fill: "none", vectorEffect: "non-scaling-stroke", strokeWidth: index === 0 ? 2 : 1.5, strokeLinejoin: "round", strokeLinecap: "round", ...(strokeOf(index).dash ? { strokeDasharray: strokeOf(index).dash } : {}), style: { stroke: strokeOf(index).color } }, index)) : null)] }), geometry.series.map((points, index) => {
58
+ /* The latest day's point, where the series has a figure for that day. */
59
+ const last = points[points.length - 1];
60
+ return last && Number.isFinite(series[index].values[pointCount - 1]) ? (_jsx("span", { "aria-hidden": "true", "data-phi-series-chart-last": "", style: {
61
+ position: "absolute",
62
+ left: `${round((last.x / VIEW_WIDTH) * 100)}%`,
63
+ top: `${round((last.y / VIEW_HEIGHT) * 100)}%`,
64
+ width: 8,
65
+ height: 8,
66
+ borderRadius: "50%",
67
+ transform: "translate(-50%, -50%)",
68
+ background: strokeOf(index).color,
69
+ boxShadow: `0 0 0 2px ${PHI_COLOR.bgContainer}`,
70
+ } }, index)) : null;
71
+ })] }), series.length > 1 || pointCount > 1 ? (_jsxs("figcaption", { "aria-hidden": "true", style: {
72
+ display: "grid",
73
+ gap: PHI_SPACE.xxs,
74
+ fontSize: PHI_FONT_SIZE.sm,
75
+ color: PHI_COLOR.textTertiary,
76
+ minWidth: 0,
77
+ }, children: [pointCount > 1 ? (_jsxs("span", { style: { display: "flex", justifyContent: "space-between", columnGap: PHI_SPACE.sm, minWidth: 0 }, children: [_jsx("span", { children: keys[0] }), _jsx("span", { children: lastKey })] })) : null, series.length > 1 ? (_jsx("span", { style: { display: "flex", flexWrap: "wrap", columnGap: PHI_SPACE.sm, minWidth: 0 }, children: series.map((entry, index) => (_jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: PHI_SPACE.xxs }, children: [_jsx("span", { style: {
78
+ display: "inline-block",
79
+ width: 10,
80
+ borderTop: `2px ${strokeOf(index).dash ? "dashed" : "solid"} ${strokeOf(index).color}`,
81
+ } }), entry.label] }, index))) })) : null] })) : null] }));
82
+ }
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { Input } from "antd";
3
+ import { Input, theme } from "antd";
4
4
  import { PhiLabeledControl, usePhiControlLabel } from "./phi-labeled-control";
5
5
  import { PhiIcon } from "../shell/phi-icon";
6
6
  function resolveTextInputKind(inputType) {
@@ -22,12 +22,63 @@ function resolveTextInputKind(inputType) {
22
22
  return { type: "text", inputMode: "text" };
23
23
  }
24
24
  }
25
+ /*
26
+ * The first line of a text area, where its clear button belongs.
27
+ *
28
+ * Ant Design hangs the button absolutely from the box's top at `paddingXS`, which is the middle of the
29
+ * first line only for a text area at its own padding. A field restyled to stand in its text -- no
30
+ * padding, the text's own type, as the inline editors are -- had the cross half a line below its line.
31
+ * Where the field states its own top padding, the button is given one line of the field from there,
32
+ * with the cross in its middle: `1lh` in the field's type. A field at Ant Design's padding keeps
33
+ * Ant Design's place.
34
+ */
35
+ function readBlockStartPadding(style) {
36
+ const value = style?.paddingBlockStart ?? style?.paddingTop ?? style?.paddingBlock ?? style?.padding;
37
+ if (value == null)
38
+ return null;
39
+ if (typeof value === "number")
40
+ return `${value}px`;
41
+ // The first value of a shorthand, read past the spaces inside a `calc()`.
42
+ const text = String(value).trim();
43
+ let depth = 0;
44
+ for (let index = 0; index < text.length; index += 1) {
45
+ const character = text[index];
46
+ if (character === "(")
47
+ depth += 1;
48
+ else if (character === ")")
49
+ depth -= 1;
50
+ else if (depth === 0 && /\s/u.test(character ?? ""))
51
+ return text.slice(0, index);
52
+ }
53
+ return text || null;
54
+ }
55
+ function resolveTextAreaClearStyle(style, inputStyle) {
56
+ const fieldPadding = readBlockStartPadding(inputStyle);
57
+ if (fieldPadding == null)
58
+ return null;
59
+ const boxPadding = readBlockStartPadding(style) ?? "0px";
60
+ const type = inputStyle?.font != null
61
+ ? { font: inputStyle.font }
62
+ : {
63
+ fontSize: inputStyle?.fontSize ?? "inherit",
64
+ lineHeight: inputStyle?.lineHeight ?? "inherit",
65
+ fontFamily: inputStyle?.fontFamily ?? "inherit",
66
+ };
67
+ return {
68
+ ...type,
69
+ insetBlockStart: `calc(${boxPadding} + ${fieldPadding})`,
70
+ height: "1lh",
71
+ display: "flex",
72
+ alignItems: "center",
73
+ };
74
+ }
25
75
  export function PhiTextControl({ id, value, label, description, inputType = "text", presentation = "input", disabled = false, readOnly = false, allowClear = true, clearLabel, placeholder, ariaLabel, prefix, autoFocus, autoComplete, minLength, maxLength, rows, cols, autoSize, tabIndex, size, variant, inputRef, textareaRef, onChange, onPressEnter, onKeyDown, onFocus, onBlur, onClear, style, inputStyle, }) {
26
76
  const { labelId, labelledBy } = usePhiControlLabel(label, ariaLabel);
27
77
  const resolvedKind = resolveTextInputKind(inputType);
78
+ const { token } = theme.useToken();
28
79
  const stableAllowClear = {
29
80
  disabled: !allowClear,
30
- clearIcon: (_jsx(PhiIcon, { name: "close", size: "inherit", ariaLabel: clearLabel })),
81
+ clearIcon: (_jsx(PhiIcon, { name: "close", size: token.fontSizeIcon, ariaLabel: clearLabel })),
31
82
  };
32
83
  const commonProps = {
33
84
  ref: inputRef,
@@ -56,7 +107,10 @@ export function PhiTextControl({ id, value, label, description, inputType = "tex
56
107
  }
57
108
  let control;
58
109
  if (presentation === "textarea") {
59
- control = (_jsx(Input.TextArea, { ...commonProps, styles: inputStyle ? { textarea: inputStyle } : undefined, ref: textareaRef, allowClear: stableAllowClear, onClear: onClear, autoSize: autoSize, rows: rows, cols: cols, onPressEnter: onPressEnter }));
110
+ control = (_jsx(Input.TextArea, { ...commonProps, styles: {
111
+ ...(inputStyle ? { textarea: inputStyle } : {}),
112
+ ...(allowClear ? { clear: resolveTextAreaClearStyle(style, inputStyle) ?? undefined } : {}),
113
+ }, ref: textareaRef, allowClear: stableAllowClear, onClear: onClear, autoSize: autoSize, rows: rows, cols: cols, onPressEnter: onPressEnter }));
60
114
  }
61
115
  else if (presentation === "password") {
62
116
  control = (_jsx(Input.Password, { ...commonProps, styles: inputStyle ? { input: inputStyle } : undefined, allowClear: stableAllowClear, onClear: onClear, prefix: prefix, onPressEnter: onPressEnter }));
@@ -102,7 +102,7 @@ function PhiTreeNodeTitle({ node, presentation, editing, actions, controlSize, o
102
102
  beforeNodeIdentity: dropMode === "before" ? node.identity : null,
103
103
  afterNodeIdentity: dropMode === "after" ? node.identity : null,
104
104
  });
105
- }, children: [iconEditable ? (_jsx(PhiIconPickerControl, { value: typeof icon === "string" ? icon : null, buttonSize: controlSize, buttonType: "text", onChange: (value) => void onCommitField?.(node.identity, iconFieldKey, value) })) : presentation.showIcon !== false && typeof icon === "string" ? _jsx(PhiIcon, { name: icon }) : null, _jsxs("div", { style: { display: "grid", flex: 1, minWidth: 0 }, children: [titleEditable ? (_jsx(PhiTextControl, { value: draftTitle, size: controlSize, variant: presentation.node.titleEditor?.variant ?? "underlined", onChange: (value) => setDraftTitle(value ?? ""), onPressEnter: commitTitle, onBlur: commitTitle })) : _jsx("span", { children: title }), description != null && description !== "" ? (_jsx(Typography.Text, { type: "secondary", ellipsis: true, children: String(description) })) : null] }), actions.length ? (_jsx(Flex, { align: "center", gap: 2, onClick: (event) => event.stopPropagation(), children: actions.map((action) => (_jsx(PhiTreeActionButton, { action: action.execution === "link" && action.hrefPath
105
+ }, children: [iconEditable ? (_jsx(PhiIconPickerControl, { value: typeof icon === "string" ? icon : null, buttonSize: controlSize, buttonType: "text", onChange: (value) => void onCommitField?.(node.identity, iconFieldKey, value) })) : presentation.showIcon !== false && typeof icon === "string" ? _jsx(PhiIcon, { name: icon }) : null, _jsxs("div", { style: { display: "grid", flex: 1, minWidth: 0 }, children: [titleEditable ? (_jsx(PhiTextControl, { value: draftTitle, size: controlSize, variant: presentation.node.titleEditor?.variant ?? "underlined", onChange: (value) => setDraftTitle(value ?? ""), onPressEnter: commitTitle, onBlur: commitTitle })) : _jsx("span", { children: title }), description != null && description !== "" ? (_jsx(Typography.Text, { type: "secondary", ellipsis: true, style: { fontSize: "inherit" }, children: String(description) })) : null] }), actions.length ? (_jsx(Flex, { align: "center", gap: 2, onClick: (event) => event.stopPropagation(), children: actions.map((action) => (_jsx(PhiTreeActionButton, { action: action.execution === "link" && action.hrefPath
106
106
  ? { ...action, href: String(readPath(node.record, action.hrefPath) ?? "") }
107
107
  : action, size: controlSize, onActivate: () => onAction?.(action, node.identity) }, action.key))) })) : null] }));
108
108
  }