@parallelworks/ui 0.57.0 → 0.59.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.
Files changed (60) hide show
  1. package/README.md +60 -4
  2. package/dist/access/PrincipalPicker.js +2 -1
  3. package/dist/ai/agent/diff.d.ts +1 -0
  4. package/dist/ai/agent/diff.js +50 -3
  5. package/dist/ai/ai.css +38 -0
  6. package/dist/ai/components/AttachmentUpload.d.ts +3 -0
  7. package/dist/ai/components/AttachmentUpload.js +51 -43
  8. package/dist/ai/components/agent/AgentMessageParts.js +33 -6
  9. package/dist/ai/strings.d.ts +2 -0
  10. package/dist/ai/strings.js +2 -1
  11. package/dist/components/AppLink.js +8 -3
  12. package/dist/components/Avatar.d.ts +5 -1
  13. package/dist/components/Avatar.js +13 -5
  14. package/dist/components/ExternalLink.d.ts +3 -2
  15. package/dist/components/ExternalLink.js +5 -2
  16. package/dist/components/Field.d.ts +7 -1
  17. package/dist/components/Field.js +83 -56
  18. package/dist/components/HoverCard.js +12 -0
  19. package/dist/components/Markdown.d.ts +3 -0
  20. package/dist/components/Markdown.js +2 -1
  21. package/dist/components/MarkdownImpl.d.ts +1 -1
  22. package/dist/components/MarkdownImpl.js +5 -3
  23. package/dist/components/Provider.d.ts +16 -1
  24. package/dist/components/Provider.js +17 -3
  25. package/dist/components/StickyBar.d.ts +14 -0
  26. package/dist/components/StickyBar.js +22 -0
  27. package/dist/components/Table.d.ts +7 -5
  28. package/dist/components/Table.js +6 -2
  29. package/dist/components/Tabs.d.ts +2 -2
  30. package/dist/components/Tabs.js +5 -2
  31. package/dist/components/category.d.ts +9 -0
  32. package/dist/components/category.js +30 -0
  33. package/dist/components/httpsImages.d.ts +12 -0
  34. package/dist/components/httpsImages.js +27 -0
  35. package/dist/components/stickyTop.d.ts +14 -0
  36. package/dist/components/stickyTop.js +93 -0
  37. package/dist/file-explorer/FileExplorer.js +21 -15
  38. package/dist/graph/DependencyGraph.d.ts +14 -1
  39. package/dist/graph/DependencyGraph.js +169 -77
  40. package/dist/graph/JobSummary.js +13 -3
  41. package/dist/graph/StepSummary.d.ts +4 -0
  42. package/dist/graph/StepSummary.js +31 -0
  43. package/dist/graph/index.d.ts +1 -0
  44. package/dist/graph/index.js +2 -1
  45. package/dist/graph/types.d.ts +3 -0
  46. package/dist/graph/util.d.ts +1 -1
  47. package/dist/icons.d.ts +1 -0
  48. package/dist/icons.js +3 -2
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.js +3 -1
  51. package/dist/list/ActionToolbar.d.ts +2 -3
  52. package/dist/list/ActionToolbar.js +3 -3
  53. package/dist/list/index.d.ts +1 -1
  54. package/dist/list/index.js +2 -2
  55. package/dist/logviewer/LogViewer.js +3 -3
  56. package/dist/styles/theme.css +27 -0
  57. package/dist/styles.css +1 -1
  58. package/dist/theme/derive.d.ts +2 -1
  59. package/dist/theme/derive.js +45 -1
  60. package/package.json +1 -1
@@ -37,12 +37,14 @@ var labelSizes = {
37
37
  sm: {
38
38
  text: "text-xs text-(--theme-muted-text-color)",
39
39
  inlineHeight: "min-h-7",
40
- stackGap: "gap-1"
40
+ stackGap: "gap-1",
41
+ inlineLine: "py-1.5"
41
42
  },
42
43
  md: {
43
44
  text: "text-sm text-(--theme-app)",
44
45
  inlineHeight: "min-h-9",
45
- stackGap: "gap-1.5"
46
+ stackGap: "gap-1.5",
47
+ inlineLine: "py-2"
46
48
  }
47
49
  };
48
50
  var labelClassName = "flex items-center gap-1 font-medium";
@@ -64,7 +66,7 @@ function ErrorText({ id, children }) {
64
66
  }
65
67
  /** A control with its label, help and error, tied to it for assistive technology. One frame
66
68
  * for every field, in a form or standing alone. */
67
- function Field$1({ label, hint, help, actions, description, error, invalid, required = false, disabled = false, layout = "stacked", compact = false, size = "md", group = false, id: ownId, className, children }) {
69
+ function Field$1({ label, hint, help, actions, description, error, invalid, required = false, disabled = false, layout = "stacked", compact = false, size = "md", group = false, controlFirst = false, id: ownId, className, children }) {
68
70
  const uid = useId();
69
71
  const id = ownId ?? `${uid}control`;
70
72
  const labelId = `${uid}label`;
@@ -125,8 +127,8 @@ function Field$1({ label, hint, help, actions, description, error, invalid, requ
125
127
  }
126
128
  } : {};
127
129
  const labelBox = cx(labelClassName, labelSize.text, inline && cx(labelSize.inlineHeight, "shrink-0", compact ? "mr-2 w-fit" : "w-52 pr-3"), inRow && "row-start-1 self-end");
128
- const labelContent = /* @__PURE__ */ jsxs(Fragment, { children: [
129
- /* @__PURE__ */ jsxs("span", { children: [label, required && /* @__PURE__ */ jsx(RequiredMark, {})] }),
130
+ const nameContent = /* @__PURE__ */ jsxs(Fragment, { children: [label, required && /* @__PURE__ */ jsx(RequiredMark, {})] });
131
+ const labelExtras = /* @__PURE__ */ jsxs(Fragment, { children: [
130
132
  helpTip,
131
133
  hint,
132
134
  actions && /* @__PURE__ */ jsx("span", {
@@ -135,42 +137,83 @@ function Field$1({ label, hint, help, actions, description, error, invalid, requ
135
137
  })
136
138
  ] });
137
139
  const placed = typeof children === "function" ? children(control) : children;
140
+ const controlInContext = /* @__PURE__ */ jsx(FieldRowContext, {
141
+ value: false,
142
+ children: /* @__PURE__ */ jsx(ControlContext, {
143
+ value: controlContext,
144
+ children: placed
145
+ })
146
+ });
147
+ const messages = /* @__PURE__ */ jsxs(Fragment, { children: [
148
+ description && /* @__PURE__ */ jsx(Field.Description, {
149
+ id: descriptionId,
150
+ className: "text-xs leading-relaxed text-(--theme-muted-text-color)",
151
+ children: description
152
+ }),
153
+ help && /* @__PURE__ */ jsx(Field.Description, {
154
+ id: helpId,
155
+ hidden: true,
156
+ children: formatInfoTipContent(help)
157
+ }),
158
+ error && /* @__PURE__ */ jsxs(Field.Error, {
159
+ id: errorId,
160
+ match: true,
161
+ role: "alert",
162
+ className: errorTextClassName,
163
+ children: [/* @__PURE__ */ jsx(WarningIcon, {
164
+ "aria-hidden": "true",
165
+ className: "size-3.5 shrink-0"
166
+ }), error]
167
+ })
168
+ ] });
169
+ const fieldLabel = (className) => /* @__PURE__ */ jsx(Field.Label, {
170
+ ...labelledBy > 0 ? {
171
+ htmlFor: void 0,
172
+ onClick: () => document.getElementById(id)?.focus()
173
+ } : typeof children === "function" || ownId !== void 0 ? { htmlFor: id } : {},
174
+ id: labelId,
175
+ className,
176
+ "data-field-label": true,
177
+ children: nameContent
178
+ });
179
+ if (controlFirst && !group) {
180
+ const lined = inline || inRow;
181
+ return /* @__PURE__ */ jsxs(Field.Root, {
182
+ invalid: isInvalid,
183
+ disabled,
184
+ className: inRow ? cx("contents", className) : cx("flex w-full items-start", className),
185
+ "data-field-row": inRow || void 0,
186
+ children: [inline && !compact && /* @__PURE__ */ jsx("div", {
187
+ "aria-hidden": "true",
188
+ className: "w-52 shrink-0"
189
+ }), /* @__PURE__ */ jsxs("div", {
190
+ ref: help ? setControlBox : void 0,
191
+ className: cx("flex w-fit max-w-full min-w-0 items-start gap-2", lined && labelSize.inlineLine, inRow && "row-start-2"),
192
+ children: [/* @__PURE__ */ jsx("div", {
193
+ className: cx("flex h-lh shrink-0 items-center", labelSize.text),
194
+ ...helpFocus,
195
+ children: controlInContext
196
+ }), /* @__PURE__ */ jsxs("div", {
197
+ className: "flex min-w-0 flex-col gap-1",
198
+ children: [/* @__PURE__ */ jsxs("div", {
199
+ className: cx("font-medium", labelSize.text, disabled && "opacity-60"),
200
+ children: [fieldLabel(disabled ? "cursor-not-allowed" : "cursor-pointer"), (help || hint || actions) && /* @__PURE__ */ jsx("span", {
201
+ className: "ms-1 inline-flex items-center gap-1 align-[-0.125em]",
202
+ children: labelExtras
203
+ })]
204
+ }), messages]
205
+ })]
206
+ })]
207
+ });
208
+ }
138
209
  const body = /* @__PURE__ */ jsxs("div", {
139
210
  ref: help ? setControlBox : void 0,
140
211
  className: cx("flex w-full min-w-0 flex-col gap-1", inRow && "row-start-2"),
141
212
  ...helpFocus,
142
- children: [
143
- /* @__PURE__ */ jsx(FieldRowContext, {
144
- value: false,
145
- children: /* @__PURE__ */ jsx(ControlContext, {
146
- value: controlContext,
147
- children: inline ? /* @__PURE__ */ jsx("div", {
148
- className: cx("grid items-center", labelSize.inlineHeight),
149
- children: placed
150
- }) : placed
151
- })
152
- }),
153
- description && /* @__PURE__ */ jsx(Field.Description, {
154
- id: descriptionId,
155
- className: "text-xs leading-relaxed text-(--theme-muted-text-color)",
156
- children: description
157
- }),
158
- help && /* @__PURE__ */ jsx(Field.Description, {
159
- id: helpId,
160
- hidden: true,
161
- children: formatInfoTipContent(help)
162
- }),
163
- error && /* @__PURE__ */ jsxs(Field.Error, {
164
- id: errorId,
165
- match: true,
166
- role: "alert",
167
- className: errorTextClassName,
168
- children: [/* @__PURE__ */ jsx(WarningIcon, {
169
- "aria-hidden": "true",
170
- className: "size-3.5 shrink-0"
171
- }), error]
172
- })
173
- ]
213
+ children: [inline ? /* @__PURE__ */ jsx("div", {
214
+ className: cx("grid items-center", labelSize.inlineHeight),
215
+ children: controlInContext
216
+ }) : controlInContext, messages]
174
217
  });
175
218
  const frame = inRow ? cx("contents", className) : cx("flex w-full", inline ? "items-start" : cx("flex-col", labelSize.stackGap), className);
176
219
  if (group) return /* @__PURE__ */ jsx(Field.Root, {
@@ -182,10 +225,10 @@ function Field$1({ label, hint, help, actions, description, error, invalid, requ
182
225
  }),
183
226
  children: /* @__PURE__ */ jsxs("div", {
184
227
  className: frame,
185
- children: [/* @__PURE__ */ jsx(Fieldset.Legend, {
228
+ children: [/* @__PURE__ */ jsxs(Fieldset.Legend, {
186
229
  id: labelId,
187
230
  className: labelBox,
188
- children: labelContent
231
+ children: [/* @__PURE__ */ jsx("span", { children: nameContent }), labelExtras]
189
232
  }), body]
190
233
  })
191
234
  });
@@ -196,23 +239,7 @@ function Field$1({ label, hint, help, actions, description, error, invalid, requ
196
239
  "data-field-row": inRow || void 0,
197
240
  children: [/* @__PURE__ */ jsxs("div", {
198
241
  className: labelBox,
199
- children: [
200
- /* @__PURE__ */ jsxs(Field.Label, {
201
- ...labelledBy > 0 ? {
202
- htmlFor: void 0,
203
- onClick: () => document.getElementById(id)?.focus()
204
- } : typeof children === "function" || ownId !== void 0 ? { htmlFor: id } : {},
205
- id: labelId,
206
- "data-field-label": true,
207
- children: [label, required && /* @__PURE__ */ jsx(RequiredMark, {})]
208
- }),
209
- helpTip,
210
- hint,
211
- actions && /* @__PURE__ */ jsx("span", {
212
- className: actionsClassName,
213
- children: actions
214
- })
215
- ]
242
+ children: [fieldLabel(), labelExtras]
216
243
  }), body]
217
244
  });
218
245
  }
@@ -3,6 +3,17 @@ import { useFullscreenContainer } from "./fullscreen.js";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
  import { PreviewCard } from "@base-ui/react/preview-card";
5
5
  //#region src/components/HoverCard.tsx
6
+ var stop = (event) => event.stopPropagation();
7
+ var pressHandlers = {
8
+ onClick: stop,
9
+ onAuxClick: stop,
10
+ onDoubleClick: stop,
11
+ onContextMenu: stop,
12
+ onPointerDown: stop,
13
+ onPointerUp: stop,
14
+ onMouseDown: stop,
15
+ onMouseUp: stop
16
+ };
6
17
  /** Detail about its anchor, such as a person's profile, after a short hover or
7
18
  * on focus. A visual enhancement: whatever the card shows must also be
8
19
  * reachable another way, such as the page the anchor links to. The card
@@ -25,6 +36,7 @@ function HoverCard({ card, children, defaultOpen = false }) {
25
36
  className: positionerClassName,
26
37
  children: /* @__PURE__ */ jsx(PreviewCard.Popup, {
27
38
  className: cardClassName,
39
+ ...pressHandlers,
28
40
  children: card
29
41
  })
30
42
  })
@@ -7,6 +7,9 @@ export interface MarkdownProps {
7
7
  controls?: ControlsConfig;
8
8
  /** Per-tag overrides of the rendered elements. */
9
9
  components?: Components;
10
+ httpsImagesOnly?: boolean;
11
+ /** Sets it at the page's text size, for Markdown inside other UI rather than a document of its own. */
12
+ compact?: boolean;
10
13
  }
11
14
  /** Markdown, with highlighted code, math and mermaid diagrams. Links leave in a
12
15
  * new tab, and inline code breaks after path separators. */
@@ -1,3 +1,4 @@
1
+ import { cx } from "../cx.js";
1
2
  import { jsx } from "react/jsx-runtime";
2
3
  import { Suspense, lazy } from "react";
3
4
  //#region src/components/Markdown.tsx
@@ -7,7 +8,7 @@ var MarkdownImpl = lazy(() => import("./MarkdownImpl.js"));
7
8
  function Markdown(props) {
8
9
  return /* @__PURE__ */ jsx(Suspense, {
9
10
  fallback: /* @__PURE__ */ jsx("div", {
10
- className: "markdown whitespace-pre-wrap text-start w-full",
11
+ className: cx("markdown whitespace-pre-wrap text-start w-full", props.compact && "markdown-compact"),
11
12
  children: props.children
12
13
  }),
13
14
  children: /* @__PURE__ */ jsx(MarkdownImpl, { ...props })
@@ -1,3 +1,3 @@
1
1
  import 'katex/dist/katex.min.css';
2
2
  import type { MarkdownProps } from './Markdown.js';
3
- export default function MarkdownImpl({ children, isStreaming, controls, components, }: MarkdownProps): import("react").JSX.Element;
3
+ export default function MarkdownImpl({ children, isStreaming, controls, components, httpsImagesOnly, compact, }: MarkdownProps): import("react").JSX.Element;
@@ -2,6 +2,7 @@ import { cx } from "../cx.js";
2
2
  import { ExternalLink } from "./ExternalLink.js";
3
3
  import { readIsDark } from "./useCssIsDark.js";
4
4
  import { CODE_THEMES } from "./codeTheme.js";
5
+ import { rehypeHttpsImagesOnly } from "./httpsImages.js";
5
6
  import { rehypeCodeBreaks } from "./markdownCodeBreaks.js";
6
7
  import { jsx } from "react/jsx-runtime";
7
8
  import { useEffect, useState } from "react";
@@ -33,7 +34,8 @@ function loadMermaid() {
33
34
  return mermaidLoad;
34
35
  }
35
36
  var rehypePlugins = [...Object.values(defaultRehypePlugins), rehypeCodeBreaks];
36
- function MarkdownImpl({ children, isStreaming, controls, components }) {
37
+ var httpsImagesRehypePlugins = [...Object.entries(defaultRehypePlugins).flatMap(([name, plugin]) => name === "harden" ? [rehypeHttpsImagesOnly, plugin] : [plugin]), rehypeCodeBreaks];
38
+ function MarkdownImpl({ children, isStreaming, controls, components, httpsImagesOnly, compact }) {
37
39
  const needsMermaid = !!children && children.includes("```mermaid");
38
40
  const [, setMermaidReady] = useState(() => !!pluginsWithMermaid);
39
41
  useEffect(() => {
@@ -48,7 +50,7 @@ function MarkdownImpl({ children, isStreaming, controls, components }) {
48
50
  }, [needsMermaid]);
49
51
  const plugins = needsMermaid && pluginsWithMermaid ? pluginsWithMermaid : basePlugins;
50
52
  return /* @__PURE__ */ jsx("div", {
51
- className: "markdown whitespace-normal text-start w-full",
53
+ className: cx("markdown whitespace-normal text-start w-full", compact && "markdown-compact"),
52
54
  children: /* @__PURE__ */ jsx(Streamdown, {
53
55
  mode: isStreaming ? "streaming" : "static",
54
56
  isAnimating: isStreaming ?? false,
@@ -58,7 +60,7 @@ function MarkdownImpl({ children, isStreaming, controls, components }) {
58
60
  } : {},
59
61
  ...controls ? { controls } : {},
60
62
  plugins,
61
- rehypePlugins,
63
+ rehypePlugins: httpsImagesOnly ? httpsImagesRehypePlugins : rehypePlugins,
62
64
  shikiTheme: CODE_THEMES,
63
65
  components: {
64
66
  a: ({ node, ...props }) => /* @__PURE__ */ jsx(ExternalLink, {
@@ -227,6 +227,14 @@ export interface UIStrings {
227
227
  loadingLogs: string;
228
228
  noLogFound: string;
229
229
  logLoadFailed: string;
230
+ loadingSummary: string;
231
+ hasSummary: string;
232
+ stepOf: (position: number, count: number) => string;
233
+ summaryLoadFailed: string;
234
+ summaryTab: string;
235
+ logsTab: string;
236
+ scriptTab: string;
237
+ openSubworkflow: string;
230
238
  statusLabel: (status: string) => string;
231
239
  statusReason: (reason: string | undefined | null) => string | undefined;
232
240
  /** A run's general log, beside its summary. */
@@ -277,6 +285,8 @@ export interface UIStrings {
277
285
  openFolder: string;
278
286
  /** A path's page in the app, as in "Copy link". */
279
287
  link: string;
288
+ /** An object's key within its storage, as in "Copy path". */
289
+ path: string;
280
290
  detailsTab: string;
281
291
  previewTab: string;
282
292
  preview: string;
@@ -734,7 +744,12 @@ export interface UIProviderProps {
734
744
  /** The engine, or a loader that resolves to it. A loader is called once, the first one
735
745
  * passed, so it may be written inline. */
736
746
  engine?: WorkflowEngineSource | undefined;
747
+ /** The nonce the page's Content Security Policy allows inline scripts by,
748
+ * for a host that renders on the server: a few components emit a script
749
+ * that places them before hydration. The components render no inline
750
+ * `<style>`, so a policy without 'unsafe-inline' styles needs none. */
751
+ nonce?: string | undefined;
737
752
  children: React.ReactNode;
738
753
  }
739
- export declare function UIProvider({ strings, navigation, data, slots, engine: engineProp, children, }: UIProviderProps): import("react").JSX.Element;
754
+ export declare function UIProvider({ strings, navigation, data, slots, engine: engineProp, nonce, children, }: UIProviderProps): import("react").JSX.Element;
740
755
  export {};
@@ -4,6 +4,7 @@ import { Toaster } from "./Toast.js";
4
4
  import { TooltipProvider } from "./Tooltip.js";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  import { createContext, use, useCallback, useContext, useEffect, useRef, useState } from "react";
7
+ import { CSPProvider } from "@base-ui/react/csp-provider";
7
8
  import { dequal } from "dequal";
8
9
  //#region src/components/Provider.tsx
9
10
  var isGroup = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -236,6 +237,14 @@ var DEFAULTS = {
236
237
  loadingLogs: "Loading logs",
237
238
  noLogFound: "No log found",
238
239
  logLoadFailed: "Log could not be loaded",
240
+ loadingSummary: "Loading summary",
241
+ hasSummary: "Has a summary",
242
+ stepOf: (position, count) => `Step ${position} of ${count}`,
243
+ summaryLoadFailed: "Summary could not be loaded",
244
+ summaryTab: "Summary",
245
+ logsTab: "Logs",
246
+ scriptTab: "Script",
247
+ openSubworkflow: "Open Subworkflow",
239
248
  statusLabel: (status) => status,
240
249
  statusReason: () => void 0,
241
250
  generalLog: "General log",
@@ -289,6 +298,7 @@ var DEFAULTS = {
289
298
  viewDetails: "View details",
290
299
  openFolder: "Open folder",
291
300
  link: "link",
301
+ path: "path",
292
302
  detailsTab: "Details",
293
303
  previewTab: "Preview",
294
304
  preview: "Preview",
@@ -685,7 +695,7 @@ function useMerged(compute, input) {
685
695
  };
686
696
  return ref.current.value;
687
697
  }
688
- function UIProvider({ strings, navigation, data, slots, engine: engineProp, children }) {
698
+ function UIProvider({ strings, navigation, data, slots, engine: engineProp, nonce, children }) {
689
699
  const [firstLoader] = useState(() => typeof engineProp === "function" ? engineProp : void 0);
690
700
  const engine = typeof engineProp === "function" ? firstLoader ?? engineProp : engineProp;
691
701
  useEffect(() => {
@@ -712,9 +722,13 @@ function UIProvider({ strings, navigation, data, slots, engine: engineProp, chil
712
722
  value: mergedData,
713
723
  children: /* @__PURE__ */ jsx(SlotsContext, {
714
724
  value: stableSlots,
715
- children: /* @__PURE__ */ jsxs(EngineContext, {
725
+ children: /* @__PURE__ */ jsx(EngineContext, {
716
726
  value: engine,
717
- children: [/* @__PURE__ */ jsx(TooltipProvider, { children }), /* @__PURE__ */ jsx(Toaster, {})]
727
+ children: /* @__PURE__ */ jsxs(CSPProvider, {
728
+ nonce,
729
+ disableStyleElements: true,
730
+ children: [/* @__PURE__ */ jsx(TooltipProvider, { children }), /* @__PURE__ */ jsx(Toaster, {})]
731
+ })
718
732
  })
719
733
  })
720
734
  })
@@ -0,0 +1,14 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface StickyBarProps {
3
+ /** Padding, borders and layout; the bar sets its own position, `top`,
4
+ * layer and background. */
5
+ className?: string | undefined;
6
+ testId?: string | undefined;
7
+ children?: ReactNode;
8
+ }
9
+ /** A strip of a page's own controls, such as a row of filters or a page's
10
+ * actions, pinned as the page scrolls. It stops just below whatever its
11
+ * scroller already pins at the top, such as the page header, and moves with
12
+ * it as that wraps or changes height. For an `ActionToolbar` or `Tabs` alone,
13
+ * give them `sticky` instead. */
14
+ export declare function StickyBar({ className, testId, children }: StickyBarProps): import("react").JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { cx } from "../cx.js";
2
+ import { stickyClasses, useStickyTop } from "./stickyTop.js";
3
+ import { jsx } from "react/jsx-runtime";
4
+ import { useState } from "react";
5
+ //#region src/components/StickyBar.tsx
6
+ /** A strip of a page's own controls, such as a row of filters or a page's
7
+ * actions, pinned as the page scrolls. It stops just below whatever its
8
+ * scroller already pins at the top, such as the page header, and moves with
9
+ * it as that wraps or changes height. For an `ActionToolbar` or `Tabs` alone,
10
+ * give them `sticky` instead. */
11
+ function StickyBar({ className, testId, children }) {
12
+ const [el, setEl] = useState(null);
13
+ useStickyTop(el, true);
14
+ return /* @__PURE__ */ jsx("div", {
15
+ ref: setEl,
16
+ "data-testid": testId,
17
+ className: cx(stickyClasses, className),
18
+ children
19
+ });
20
+ }
21
+ //#endregion
22
+ export { StickyBar };
@@ -37,11 +37,13 @@ interface TableProps {
37
37
  roundedBottom?: boolean;
38
38
  isLoading?: boolean;
39
39
  /** The header row stays in view while the rows scroll under it. It sticks
40
- * to the nearest scrolling ancestor, `--table-header-top` below its top
41
- * edge (0 unless an ancestor sets it), so the table no longer scrolls or
42
- * clips on its own: a box that scrolls itself is all a sticky header can
43
- * stick to. Its ancestor scrolls it sideways as well as down. Rows pass
44
- * under the header, so a `theadClassName` of your own needs a background. */
40
+ * to the nearest scrolling ancestor, just below whatever that ancestor
41
+ * already pins at its top, such as a page header, a toolbar or tabs, which
42
+ * it finds and follows by itself; in a box that scrolls on its own it pins
43
+ * at the box's top. So the table no longer scrolls or clips on its own: a
44
+ * box that scrolls itself is all a sticky header can stick to. Its ancestor
45
+ * scrolls it sideways as well as down. Rows pass under the header, so a
46
+ * `theadClassName` of your own needs a background. */
45
47
  stickyHeader?: boolean | undefined;
46
48
  /** Forwarded to the `<table>`, for attributes that must sit on the element owning
47
49
  * the rows — `role='grid'`, aria-activedescendant, aria-rowcount. `className` is
@@ -1,8 +1,9 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { SortDownIcon, SortUnsortedIcon, SortUpIcon } from "../icons.js";
3
3
  import { Loader } from "./Loader.js";
4
+ import { useStickyTop } from "./stickyTop.js";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
5
- import React, { createContext, useContext } from "react";
6
+ import React, { createContext, useContext, useState } from "react";
6
7
  //#region src/components/Table.tsx
7
8
  var DensityContext = createContext("comfortable");
8
9
  var cellX = "px-3";
@@ -47,6 +48,8 @@ var isHeaderChild = (child) => {
47
48
  * the children, then `<tr>`s of `Table.Item`s. */
48
49
  function Table({ density = "comfortable", borderless = false, panel = true, className, wrapperClassName = "", tbodyClassName = "divide-y", theadClassName = "theme-muted-panel", children, roundedTop = false, rounded = false, roundedBottom = false, isLoading = false, stickyHeader = false, tableProps }) {
49
50
  const childList = React.Children.toArray(children);
51
+ const [thead, setThead] = useState(null);
52
+ useStickyTop(thead, stickyHeader && !isLoading);
50
53
  const hasOverflow = wrapperClassName.split(" ").some((cls) => cls.startsWith("overflow-"));
51
54
  return /* @__PURE__ */ jsx(DensityContext, {
52
55
  value: density,
@@ -62,7 +65,8 @@ function Table({ density = "comfortable", borderless = false, panel = true, clas
62
65
  className: cx("min-w-full divide-y theme-border", className),
63
66
  ...tableProps,
64
67
  children: [/* @__PURE__ */ jsx("thead", {
65
- className: cx(stickyHeader && "sticky top-(--table-header-top,0px) z-(--layer-sticky) border-b-0 shadow-[inset_0_-1px_0_var(--theme-border)]", theadClassName),
68
+ ref: setThead,
69
+ className: cx(stickyHeader && "sticky top-0 z-(--layer-sticky) border-b-0 shadow-[inset_0_-1px_0_var(--theme-border)]", theadClassName),
66
70
  children: /* @__PURE__ */ jsx("tr", { children: childList.filter(isHeaderChild) })
67
71
  }), /* @__PURE__ */ jsx("tbody", {
68
72
  className: tbodyClassName,
@@ -25,8 +25,8 @@ export interface TabsProps<Value extends string> extends SegmentLayout {
25
25
  variant?: 'line' | 'segmented' | undefined;
26
26
  /** At the tab row's far end, such as a picker for what the tabs show. */
27
27
  end?: ReactNode;
28
- /** Pins the tab row to the top of the page's scroller as it scrolls; give it a
29
- * `top-*` in `className` to pin it under something else pinned there. */
28
+ /** Pins the tab row as the page scrolls, just below whatever the page's
29
+ * scroller already pins at the top, such as the page header and a toolbar. */
30
30
  sticky?: boolean | undefined;
31
31
  /** On the tab row. */
32
32
  className?: string | undefined;
@@ -2,11 +2,11 @@ import { cx } from "../cx.js";
2
2
  import { Tooltip } from "./Tooltip.js";
3
3
  import { AppLink } from "./AppLink.js";
4
4
  import { SegmentContent, segmentClassName, segmentGroupClassName } from "./segmented.js";
5
+ import { useStickyTop } from "./stickyTop.js";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  import { useState } from "react";
7
8
  import { Tabs } from "@base-ui/react/tabs";
8
9
  //#region src/components/Tabs.tsx
9
- var stickyClassName = "sticky top-0 z-(--layer-sticky) bg-(--theme-app-bg)";
10
10
  /** Views of one thing, one shown at a time: panels below the tabs, or pages
11
11
  * the tabs link to. A lone tab would only repeat the page's title, so the row
12
12
  * hides unless there are two or more, or something at its end. */
@@ -19,6 +19,8 @@ function Tabs$1({ label, items, value, defaultValue, onValueChange, variant = "l
19
19
  const [own, setOwn] = useState(defaultValue ?? items[0]?.value);
20
20
  const current = value ?? own;
21
21
  const showRow = items.length > 1 || !!end;
22
+ const [row, setRow] = useState(null);
23
+ useStickyTop(row, !!sticky);
22
24
  const tabs = items.map((item) => {
23
25
  const tab = /* @__PURE__ */ jsx(Tabs.Tab, {
24
26
  value: item.value,
@@ -50,7 +52,8 @@ function Tabs$1({ label, items, value, defaultValue, onValueChange, variant = "l
50
52
  "data-testid": testId,
51
53
  className: children ? void 0 : "contents",
52
54
  children: [showRow && /* @__PURE__ */ jsxs("div", {
53
- className: cx("flex items-center justify-between gap-2", !segmented && "border-b border-(--theme-border)", sticky && stickyClassName, className),
55
+ ref: setRow,
56
+ className: cx("flex items-center justify-between gap-2", !segmented && "border-b border-(--theme-border)", sticky && "sticky top-0 z-(--layer-sticky) bg-(--theme-app-bg)", className),
54
57
  children: [/* @__PURE__ */ jsxs(Tabs.List, {
55
58
  "aria-label": label,
56
59
  className: cx(segmented ? segmentGroupClassName(layout) : "relative flex min-w-0 overflow-x-auto"),
@@ -0,0 +1,9 @@
1
+ /** The fill and text of each palette entry, written out so the stylesheet build sees them. */
2
+ export declare const CATEGORY_CLASSES: readonly ['bg-(--theme-category-1) text-(--theme-category-1-text)', 'bg-(--theme-category-2) text-(--theme-category-2-text)', 'bg-(--theme-category-3) text-(--theme-category-3-text)', 'bg-(--theme-category-4) text-(--theme-category-4-text)', 'bg-(--theme-category-5) text-(--theme-category-5-text)', 'bg-(--theme-category-6) text-(--theme-category-6-text)', 'bg-(--theme-category-7) text-(--theme-category-7-text)', 'bg-(--theme-category-8) text-(--theme-category-8-text)'];
3
+ /** 32-bit FNV-1a over a string's UTF-8 bytes: cheap, well spread, and the same in any language. */
4
+ export declare function fnv1a(text: string): number;
5
+ /** The palette entry, 1–8, for a key. Case, surrounding space and Unicode composition don't
6
+ * change it, so "Ada Lovelace" and " ada lovelace" match. */
7
+ export declare function categoryOf(key: string): number;
8
+ /** The fill and text classes for a key. */
9
+ export declare function categoryClasses(key: string): string;
@@ -0,0 +1,30 @@
1
+ //#region src/components/category.ts
2
+ /** The fill and text of each palette entry, written out so the stylesheet build sees them. */
3
+ var CATEGORY_CLASSES = [
4
+ "bg-(--theme-category-1) text-(--theme-category-1-text)",
5
+ "bg-(--theme-category-2) text-(--theme-category-2-text)",
6
+ "bg-(--theme-category-3) text-(--theme-category-3-text)",
7
+ "bg-(--theme-category-4) text-(--theme-category-4-text)",
8
+ "bg-(--theme-category-5) text-(--theme-category-5-text)",
9
+ "bg-(--theme-category-6) text-(--theme-category-6-text)",
10
+ "bg-(--theme-category-7) text-(--theme-category-7-text)",
11
+ "bg-(--theme-category-8) text-(--theme-category-8-text)"
12
+ ];
13
+ var encoder = new TextEncoder();
14
+ /** 32-bit FNV-1a over a string's UTF-8 bytes: cheap, well spread, and the same in any language. */
15
+ function fnv1a(text) {
16
+ let hash = 2166136261;
17
+ for (const byte of encoder.encode(text)) hash = Math.imul(hash ^ byte, 16777619);
18
+ return hash >>> 0;
19
+ }
20
+ /** The palette entry, 1–8, for a key. Case, surrounding space and Unicode composition don't
21
+ * change it, so "Ada Lovelace" and " ada lovelace" match. */
22
+ function categoryOf(key) {
23
+ return fnv1a(key.trim().normalize("NFC").toLowerCase()) % CATEGORY_CLASSES.length + 1;
24
+ }
25
+ /** The fill and text classes for a key. */
26
+ function categoryClasses(key) {
27
+ return CATEGORY_CLASSES[categoryOf(key) - 1];
28
+ }
29
+ //#endregion
30
+ export { CATEGORY_CLASSES, categoryClasses, categoryOf, fnv1a };
@@ -0,0 +1,12 @@
1
+ interface HastElement {
2
+ type: 'element';
3
+ tagName: string;
4
+ properties?: Record<string, unknown>;
5
+ children: HastNode[];
6
+ }
7
+ type HastNode = HastElement | {
8
+ type: string;
9
+ children?: HastNode[];
10
+ };
11
+ export declare function rehypeHttpsImagesOnly(): (tree: HastNode) => void;
12
+ export {};
@@ -0,0 +1,27 @@
1
+ //#region src/components/httpsImages.ts
2
+ function isHttpsUrl(url) {
3
+ if (typeof url !== "string") return false;
4
+ try {
5
+ return new URL(url).protocol === "https:";
6
+ } catch {
7
+ return false;
8
+ }
9
+ }
10
+ function isHttpsSrcSet(srcSet) {
11
+ return (Array.isArray(srcSet) ? srcSet : [srcSet]).every((entry) => typeof entry === "string" && entry.split(",").every((candidate) => isHttpsUrl(candidate.trim().split(/\s+/)[0])));
12
+ }
13
+ function walk(node) {
14
+ if (node.type === "element") {
15
+ const { tagName, properties } = node;
16
+ if (properties && tagName === "img" && !isHttpsUrl(properties["src"])) delete properties["src"];
17
+ if (properties && (tagName === "img" || tagName === "source") && !isHttpsSrcSet(properties["srcSet"])) delete properties["srcSet"];
18
+ }
19
+ for (const child of node.children ?? []) walk(child);
20
+ }
21
+ function rehypeHttpsImagesOnly() {
22
+ return (tree) => {
23
+ walk(tree);
24
+ };
25
+ }
26
+ //#endregion
27
+ export { rehypeHttpsImagesOnly };
@@ -0,0 +1,14 @@
1
+ /** Pins a bar to the top of its scroller, over what scrolls under it.
2
+ * `useStickyTop` moves it below whatever else is pinned there. */
3
+ export declare const stickyClasses = "sticky top-0 z-(--layer-sticky) bg-(--theme-app-bg)";
4
+ /**
5
+ * Pins a `sticky` element just below whatever its scroller already pins at
6
+ * the top, such as a page header, a toolbar and a tab row, rather than at
7
+ * `top: 0` behind them. It finds them in the page, so neither they nor the
8
+ * host declare anything, and it follows them as they change height (a toolbar
9
+ * wrapping on a narrow screen), move, or come and go, before the browser
10
+ * paints. Chrome that uses it stacks: each piece pins under the ones before
11
+ * it, and its own `top` moving is a change the pieces after it follow.
12
+ * In a box that scrolls on its own, nothing sits above, so it pins at 0.
13
+ */
14
+ export declare function useStickyTop(el: HTMLElement | null, enabled: boolean): void;