@galaxy-io/dls 2.9.0 → 2.10.1

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 (103) hide show
  1. package/dist/beacons/Beacon.d.ts +1 -1
  2. package/dist/beacons/Beacon.js +12 -28
  3. package/dist/brand/GalaxyAlchemyWordmark.d.ts +4 -1
  4. package/dist/brand/GalaxyAlchemyWordmark.js +7 -1
  5. package/dist/brand/GalaxyFilamentWordmark.d.ts +5 -2
  6. package/dist/brand/GalaxyFilamentWordmark.js +9 -2
  7. package/dist/brand/GalaxyLogomark.js +3 -0
  8. package/dist/brand/PoweredBy.d.ts +27 -0
  9. package/dist/brand/PoweredBy.js +96 -0
  10. package/dist/brand/internal/svg/GalaxyFilamentWordmarkSvg.d.ts +1 -1
  11. package/dist/brand/internal/svg/GalaxyFilamentWordmarkSvg.js +2 -2
  12. package/dist/charts/Heatmap.js +25 -9
  13. package/dist/charts/Sparkline.d.ts +1 -1
  14. package/dist/charts/Sparkline.js +10 -26
  15. package/dist/charts/chartFormat.d.ts +1 -1
  16. package/dist/charts/chartFormat.js +2 -2
  17. package/dist/charts/heatmapGeometry.d.ts +14 -0
  18. package/dist/charts/heatmapGeometry.js +17 -1
  19. package/dist/chips/Chip.d.ts +2 -2
  20. package/dist/chips/Chip.js +15 -31
  21. package/dist/drawer/Drawer.d.ts +2 -2
  22. package/dist/drawer/Drawer.js +1 -1
  23. package/dist/editor/CodeEditor.d.ts +1 -1
  24. package/dist/editor/CodeEditor.js +2 -1
  25. package/dist/feedback/Alert.d.ts +36 -9
  26. package/dist/feedback/Alert.js +26 -8
  27. package/dist/feedback/ProgressBar.d.ts +4 -1
  28. package/dist/feedback/ProgressBar.js +17 -30
  29. package/dist/feedback/ProgressCircle.d.ts +1 -1
  30. package/dist/feedback/ProgressCircle.js +14 -30
  31. package/dist/hooks/useEndReached.d.ts +40 -0
  32. package/dist/hooks/useEndReached.js +69 -0
  33. package/dist/icons/Icon.d.ts +36 -8
  34. package/dist/icons/Icon.js +14 -7
  35. package/dist/inputs/CronInput.js +114 -78
  36. package/dist/inputs/DateInput.d.ts +1 -1
  37. package/dist/inputs/DateInput.js +10 -10
  38. package/dist/inputs/MultiSelectInput.d.ts +3 -1
  39. package/dist/inputs/MultiSelectInput.js +8 -3
  40. package/dist/inputs/SelectInput.d.ts +8 -1
  41. package/dist/inputs/SelectInput.js +52 -25
  42. package/dist/inputs/internal/selectAll.d.ts +11 -0
  43. package/dist/inputs/internal/selectAll.js +28 -0
  44. package/dist/json/JsonViewer.js +7 -6
  45. package/dist/layout/AppFrame.d.ts +20 -0
  46. package/dist/layout/AppFrame.js +71 -0
  47. package/dist/layout/EmptyLayout.d.ts +53 -0
  48. package/dist/layout/EmptyLayout.js +59 -0
  49. package/dist/layout/ErrorLayout.d.ts +42 -0
  50. package/dist/layout/ErrorLayout.js +60 -0
  51. package/dist/layout/PageLayout.d.ts +26 -0
  52. package/dist/layout/PageLayout.js +116 -0
  53. package/dist/layout/PendingLayout.d.ts +30 -0
  54. package/dist/layout/PendingLayout.js +57 -0
  55. package/dist/layout/internal/errorState.d.ts +14 -0
  56. package/dist/layout/internal/errorState.js +49 -0
  57. package/dist/layout/internal/region.d.ts +10 -0
  58. package/dist/layout/internal/region.js +24 -0
  59. package/dist/lists/DescriptionList.js +1 -0
  60. package/dist/menu/Menu.d.ts +2 -2
  61. package/dist/menu/Menu.js +6 -13
  62. package/dist/modal/Modal.d.ts +2 -2
  63. package/dist/modal/Modal.js +1 -1
  64. package/dist/navigation/CommandPalette.js +4 -11
  65. package/dist/navigation/SidebarNav.d.ts +5 -0
  66. package/dist/navigation/SidebarNav.js +41 -22
  67. package/dist/navigation/Tabs.d.ts +4 -2
  68. package/dist/navigation/Tabs.js +52 -32
  69. package/dist/navigation/Topbar.js +4 -0
  70. package/dist/shapes/Circle.d.ts +1 -1
  71. package/dist/shapes/Circle.js +10 -26
  72. package/dist/shapes/Square.d.ts +1 -1
  73. package/dist/shapes/Square.js +10 -26
  74. package/dist/styles/internal/roleColor.d.ts +3 -0
  75. package/dist/styles/internal/roleColor.js +42 -0
  76. package/dist/styles/mixins.js +1 -1
  77. package/dist/styles.css +1 -1
  78. package/dist/table/InfiniteSpreadsheet.js +6 -6
  79. package/dist/table/InfiniteTable.d.ts +52 -20
  80. package/dist/table/InfiniteTable.js +3 -2
  81. package/dist/table/TableCore.d.ts +7 -1
  82. package/dist/table/TableCore.js +93 -83
  83. package/dist/table/VirtualizedInfiniteTable.d.ts +53 -21
  84. package/dist/table/VirtualizedInfiniteTable.js +7 -5
  85. package/dist/table/useInfiniteScrollSentinel.d.ts +13 -13
  86. package/dist/table/useInfiniteScrollSentinel.js +7 -51
  87. package/dist/theme/tokens/dark.js +1 -1
  88. package/dist/theme/tokens/light.js +4 -3
  89. package/dist/theme/tokens/types.d.ts +7 -7
  90. package/dist/timeline/Timeline.d.ts +1 -1
  91. package/dist/timeline/Timeline.js +6 -24
  92. package/dist/tokens.css +1 -1
  93. package/dist/utils/format.d.ts +35 -10
  94. package/dist/utils/format.js +60 -12
  95. package/dist/utils/search.d.ts +15 -0
  96. package/dist/utils/search.js +24 -0
  97. package/dist/vite/index.d.ts +16 -2
  98. package/dist/vite/index.js +28 -19
  99. package/dist/vite/internal/themeBoot.d.ts +25 -0
  100. package/dist/vite/internal/themeBoot.js +44 -0
  101. package/dist/vite/internal/wywOptions.d.ts +46 -0
  102. package/dist/vite/internal/wywOptions.js +44 -0
  103. package/package.json +34 -1
@@ -1,17 +1,14 @@
1
1
  /**
2
- * @internal Shared by the table components; apps use InfiniteTable, VirtualizedInfiniteTable or
3
- * InfiniteSpreadsheet.
4
- *
5
- * The `onEndReached` sentinel: a 1px element after the table, observed with an
6
- * `IntersectionObserver` whose bottom margin is `marginPx` (`TABLE_END_REACHED_MARGIN_PX`). The root is the
7
- * scroll container when it overflows, otherwise the viewport, so a table in an unbounded parent
8
- * pages on page scroll. Armed only while enabled (not loading, no error), re-armed when the row
9
- * count changes, and calls `onEndReached` at most once per row count: an app with no next page
10
- * adds no rows, so it is not called again. Without `IntersectionObserver` it falls back to a
11
- * passive `scroll` listener. Nothing runs at module scope.
2
+ * @internal
3
+ * @deprecated Use `@galaxy-io/dls/hooks/useEndReached`, which this wraps (the same observer, with
4
+ * `rowCount` named `itemCount` and `scrollRef` and `marginPx` optional). Kept so the published
5
+ * subpath keeps working; it goes in 3.0.
6
+ */
7
+ import type { RefObject } from "react";
8
+ /**
9
+ * @internal Options for `useInfiniteScrollSentinel`.
10
+ * @deprecated Use `UseEndReachedOptions` from `@galaxy-io/dls/hooks/useEndReached`.
12
11
  */
13
- import { type RefObject } from "react";
14
- /** @internal Options for `useInfiniteScrollSentinel`. */
15
12
  export interface UseInfiniteScrollSentinelOptions {
16
13
  /** The table's scroll container. */
17
14
  scrollRef: RefObject<HTMLElement | null>;
@@ -24,5 +21,8 @@ export interface UseInfiniteScrollSentinelOptions {
24
21
  /** How far below the visible area the end counts as reached, in pixels. */
25
22
  marginPx: number;
26
23
  }
27
- /** @internal Observes the sentinel and calls `onEndReached`; returns the sentinel's ref. */
24
+ /**
25
+ * @internal Observes the zero-size sentinel and calls `onEndReached`; returns the sentinel's ref.
26
+ * @deprecated Use `useEndReached` from `@galaxy-io/dls/hooks/useEndReached`.
27
+ */
28
28
  export declare function useInfiniteScrollSentinel({ scrollRef, onEndReached, isEnabled, rowCount, marginPx, }: UseInfiniteScrollSentinelOptions): RefObject<HTMLDivElement | null>;
@@ -1,61 +1,17 @@
1
- import { useEffect, useRef } from "react";
1
+ import { useEndReached } from "../hooks/useEndReached.js";
2
2
  //#region src/table/useInfiniteScrollSentinel.ts
3
3
  /**
4
- * @internal Shared by the table components; apps use InfiniteTable, VirtualizedInfiniteTable or
5
- * InfiniteSpreadsheet.
6
- *
7
- * The `onEndReached` sentinel: a 1px element after the table, observed with an
8
- * `IntersectionObserver` whose bottom margin is `marginPx` (`TABLE_END_REACHED_MARGIN_PX`). The root is the
9
- * scroll container when it overflows, otherwise the viewport, so a table in an unbounded parent
10
- * pages on page scroll. Armed only while enabled (not loading, no error), re-armed when the row
11
- * count changes, and calls `onEndReached` at most once per row count: an app with no next page
12
- * adds no rows, so it is not called again. Without `IntersectionObserver` it falls back to a
13
- * passive `scroll` listener. Nothing runs at module scope.
4
+ * @internal Observes the zero-size sentinel and calls `onEndReached`; returns the sentinel's ref.
5
+ * @deprecated Use `useEndReached` from `@galaxy-io/dls/hooks/useEndReached`.
14
6
  */
15
- /** @internal Observes the sentinel and calls `onEndReached`; returns the sentinel's ref. */
16
7
  function useInfiniteScrollSentinel({ scrollRef, onEndReached, isEnabled, rowCount, marginPx }) {
17
- const sentinelRef = useRef(null);
18
- const onEndReachedRef = useRef(onEndReached);
19
- onEndReachedRef.current = onEndReached;
20
- const calledForCountRef = useRef(null);
21
- const hasHandler = onEndReached !== void 0;
22
- useEffect(() => {
23
- const scroll = scrollRef.current;
24
- const sentinel = sentinelRef.current;
25
- if (!hasHandler || !isEnabled || scroll === null || sentinel === null) return;
26
- if (calledForCountRef.current === rowCount) return;
27
- const reach = () => {
28
- if (calledForCountRef.current === rowCount) return;
29
- calledForCountRef.current = rowCount;
30
- onEndReachedRef.current?.();
31
- };
32
- const isScrollRoot = scroll.scrollHeight > scroll.clientHeight;
33
- if (typeof IntersectionObserver === "undefined") {
34
- const target = isScrollRoot ? scroll : window;
35
- const check = () => {
36
- const bottom = isScrollRoot ? scroll.getBoundingClientRect().bottom : window.innerHeight;
37
- if (sentinel.getBoundingClientRect().top - bottom < marginPx) reach();
38
- };
39
- check();
40
- target.addEventListener("scroll", check, { passive: true });
41
- return () => target.removeEventListener("scroll", check);
42
- }
43
- const observer = new IntersectionObserver((entries) => {
44
- if (entries.some((entry) => entry.isIntersecting)) reach();
45
- }, {
46
- root: isScrollRoot ? scroll : null,
47
- rootMargin: `0px 0px ${marginPx}px 0px`
48
- });
49
- observer.observe(sentinel);
50
- return () => observer.disconnect();
51
- }, [
8
+ return useEndReached({
52
9
  scrollRef,
53
- hasHandler,
10
+ onEndReached,
54
11
  isEnabled,
55
- rowCount,
12
+ itemCount: rowCount,
56
13
  marginPx
57
- ]);
58
- return sentinelRef;
14
+ });
59
15
  }
60
16
  //#endregion
61
17
  export { useInfiniteScrollSentinel };
@@ -3,7 +3,7 @@ import { TYPE_SCALE } from "./scales.js";
3
3
  /**
4
4
  * Dark theme, resolved hex. Everything is Galaxy monochrome: `solid.primary` is `text.primary`
5
5
  * with `background.base` on it, `background.selected` is `text.primary` at 12% over
6
- * `background.base`.
6
+ * `background.base` (Light uses 10%).
7
7
  *
8
8
  * Families (`<family>` below is any of the 17; the palette steps are `PALETTE`):
9
9
  * - `text`: brighter and more vivid than Tailwind's 400: OKLCH lightness ~0.03 above it, at the
@@ -2,8 +2,9 @@ import { TYPE_SCALE } from "./scales.js";
2
2
  //#region src/theme/tokens/light.ts
3
3
  /**
4
4
  * Light theme, resolved hex. Everything is Galaxy monochrome: `solid.primary` is `text.primary`
5
- * with `background.base` on it, `background.selected` is `text.primary` at 12% over
6
- * `background.base`.
5
+ * with `background.base` on it, `background.selected` is `text.primary` at 10% over
6
+ * `background.base` (Dark uses 12%: a lift off near-black reads quieter than a darkening of white,
7
+ * and at 12% Light's selected row jumped well past `pressed`).
7
8
  *
8
9
  * Families (`<family>` below is any of the 17; the palette steps are `PALETTE`):
9
10
  * - `text`: the lightest OKLCH lightness, at the largest in-gamut chroma for the hue, that reaches
@@ -37,7 +38,7 @@ var LIGHT_THEME = {
37
38
  tertiary: "#efeff0",
38
39
  hovered: "#f5f5f5",
39
40
  pressed: "#eeeeee",
40
- selected: "#e2e2e2",
41
+ selected: "#e7e7e7",
41
42
  disabled: "#f3f3f4",
42
43
  success: "#ecfcf0",
43
44
  warning: "#fff8eb",
@@ -42,7 +42,7 @@ export type SolidColor = {
42
42
  /** The fill while pressed. */
43
43
  pressed: string;
44
44
  };
45
- /** Type-scale steps (Constitution III-A.1). */
45
+ /** Type-scale steps (AGENTS.md "Type scale"). */
46
46
  export type TypeStep = "caption" | "body_sm" | "body_md" | "body_lg" | "heading_sm" | "heading_md" | "heading_lg" | "display_sm" | "display_lg";
47
47
  /** Serif (Cardo) is editorial only: it has no `caption`, `body_sm` or `body_md` step. */
48
48
  type SerifStep = Exclude<TypeStep, "caption" | "body_sm" | "body_md">;
@@ -50,7 +50,7 @@ type SerifStep = Exclude<TypeStep, "caption" | "body_sm" | "body_md">;
50
50
  export type FontScale<S extends TypeStep = TypeStep> = {
51
51
  /** CSS `font-family` stack. */
52
52
  family: string;
53
- /** The only two weights (Constitution II.4). */
53
+ /** The only two weights (AGENTS.md "Type"). */
54
54
  weight: {
55
55
  regular: number;
56
56
  medium: number;
@@ -82,7 +82,7 @@ export type Theme = {
82
82
  hovered: string;
83
83
  /** A neutral row or control while pressed. */
84
84
  pressed: string;
85
- /** The chosen item (menus, trees, tabs, nav, tables): `text.primary` at 12% over `base`. */
85
+ /** The chosen item (menus, trees, tabs, nav, tables): `text.primary` over `base`, 12% in Dark and 10% in Light. */
86
86
  selected: string;
87
87
  /** Disabled controls. */
88
88
  disabled: string;
@@ -161,16 +161,16 @@ export type Theme = {
161
161
  };
162
162
  /** Theme-independent scales, emitted once on `:root`. */
163
163
  export type Scales = {
164
- /** Gap and padding (Constitution II.5). */
164
+ /** Gap and padding (AGENTS.md "Spacing"). */
165
165
  space: Record<Space, string>;
166
- /** Corner radii (Constitution II.6). */
166
+ /** Corner radii (AGENTS.md "Radius"). */
167
167
  radius: {
168
168
  sm: string;
169
169
  md: string;
170
170
  lg: string;
171
171
  pill: string;
172
172
  };
173
- /** Transition durations (Constitution II.7). */
173
+ /** Transition durations (AGENTS.md "Motion"). */
174
174
  duration: {
175
175
  fast: string;
176
176
  base: string;
@@ -183,7 +183,7 @@ export type Scales = {
183
183
  toast: number;
184
184
  tooltip: number;
185
185
  };
186
- /** Control heights: the canonical size rungs (Constitution III-A.4). */
186
+ /** Control heights: the canonical size rungs (AGENTS.md "Control rungs"). */
187
187
  size: {
188
188
  control: {
189
189
  xSmall: string;
@@ -19,7 +19,7 @@ export declare enum TimelineMarkerShape {
19
19
  /** No box: the dot or icon sits on the rail, 4px clear of the line above and below it. */
20
20
  NONE = "NONE"
21
21
  }
22
- /** Marker color of one item (Constitution V-A color set; the Timeline item is listed in that column). */
22
+ /** Marker color of one item (AGENTS.md "Variant": color set). */
23
23
  export declare enum TimelineVariant {
24
24
  /** `text.primary` marker in a `border.primary` box. */
25
25
  PRIMARY = "PRIMARY",
@@ -1,4 +1,5 @@
1
1
  import Text, { TextSize, TextVariant } from "../text/Text.js";
2
+ import { ROLE_COLOR_TO_CSS_MAP } from "../styles/internal/roleColor.js";
2
3
  import Beacon, { BeaconSize, BeaconVariant } from "../beacons/Beacon.js";
3
4
  import Icon, { IconVariant } from "../icons/Icon.js";
4
5
  /* empty css */
@@ -41,7 +42,7 @@ var TimelineMarkerShape = /* @__PURE__ */ function(TimelineMarkerShape) {
41
42
  TimelineMarkerShape["NONE"] = "NONE";
42
43
  return TimelineMarkerShape;
43
44
  }({});
44
- /** Marker color of one item (Constitution V-A color set; the Timeline item is listed in that column). */
45
+ /** Marker color of one item (AGENTS.md "Variant": color set). */
45
46
  var TimelineVariant = /* @__PURE__ */ function(TimelineVariant) {
46
47
  /** `text.primary` marker in a `border.primary` box. */
47
48
  TimelineVariant["PRIMARY"] = "PRIMARY";
@@ -211,29 +212,10 @@ var TIMELINE_VARIANT_TO_CSS_MAP = {
211
212
  /**
212
213
  * Categorical `color` on the marker: `text.<family>`, which an icon marker (`IconVariant.INHERIT`)
213
214
  * takes, and `border.<family>` and the `background.<family>` fill on the box.
215
+ * Reads the shared `--gx-role-*` variables that `ROLE_COLOR_TO_CSS_MAP` sets per family, so the
216
+ * twenty families are written once (`styles/internal/roleColor`).
214
217
  */
215
- var TIMELINE_COLOR_TO_CSS_MAP = {
216
- success: "gx-Timeline-success-s1khc5e9",
217
- warning: "gx-Timeline-warning-wob0jun",
218
- error: "gx-Timeline-error-ecpz10x",
219
- red: "gx-Timeline-red-r1arc1uv",
220
- orange: "gx-Timeline-orange-o1x7dvj6",
221
- amber: "gx-Timeline-amber-a1fx1m3c",
222
- yellow: "gx-Timeline-yellow-y1gy5gqm",
223
- lime: "gx-Timeline-lime-ltyv4re",
224
- green: "gx-Timeline-green-g1506u15",
225
- emerald: "gx-Timeline-emerald-e2f8n55",
226
- teal: "gx-Timeline-teal-t1bza0tc",
227
- cyan: "gx-Timeline-cyan-c1vbt6lr",
228
- sky: "gx-Timeline-sky-sjtm0e",
229
- blue: "gx-Timeline-blue-bek3kdh",
230
- indigo: "gx-Timeline-indigo-i13hn3pe",
231
- violet: "gx-Timeline-violet-vdf3rdi",
232
- purple: "gx-Timeline-purple-pjv99mk",
233
- fuchsia: "gx-Timeline-fuchsia-f4wvpbo",
234
- pink: "gx-Timeline-pink-p1qqjpue",
235
- rose: "gx-Timeline-rose-r183jhdd"
236
- };
218
+ var TIMELINE_COLOR_CSS = "gx-Timeline-TIMELINE_COLOR_CSS-t1khc5e9";
237
219
  /** A vertical list of events with dot or icon markers (boxed or bare) on a hairline rail. */
238
220
  var Timeline = forwardRef(function Timeline({ items, size = "MEDIUM", markerShape = "SQUARE", isFilled = false, className, style, ...rest }, ref) {
239
221
  const textSize = TIMELINE_SIZE_TO_TEXT_SIZE_MAP[size];
@@ -252,7 +234,7 @@ var Timeline = forwardRef(function Timeline({ items, size = "MEDIUM", markerShap
252
234
  children: [/* @__PURE__ */ jsxs(Rail, {
253
235
  "aria-hidden": true,
254
236
  children: [/* @__PURE__ */ jsx(Marker, {
255
- className: cx(color !== void 0 ? TIMELINE_COLOR_TO_CSS_MAP[color] : TIMELINE_VARIANT_TO_CSS_MAP[variant], TIMELINE_MARKER_SHAPE_TO_CSS_MAP[markerShape], isFilled && markerShape !== "NONE" && FILLED_CSS),
237
+ className: cx(color !== void 0 ? cx(ROLE_COLOR_TO_CSS_MAP[color], TIMELINE_COLOR_CSS) : TIMELINE_VARIANT_TO_CSS_MAP[variant], TIMELINE_MARKER_SHAPE_TO_CSS_MAP[markerShape], isFilled && markerShape !== "NONE" && FILLED_CSS),
256
238
  children: item.icon === void 0 ? /* @__PURE__ */ jsx(Beacon, {
257
239
  size: TIMELINE_SIZE_TO_BEACON_SIZE_MAP[size],
258
240
  ...color !== void 0 ? { color } : { variant: TIMELINE_VARIANT_TO_BEACON_VARIANT_MAP[variant] },
package/dist/tokens.css CHANGED
@@ -410,7 +410,7 @@
410
410
  --gx-color-background-tertiary: #efeff0;
411
411
  --gx-color-background-hovered: #f5f5f5;
412
412
  --gx-color-background-pressed: #eeeeee;
413
- --gx-color-background-selected: #e2e2e2;
413
+ --gx-color-background-selected: #e7e7e7;
414
414
  --gx-color-background-disabled: #f3f3f4;
415
415
  --gx-color-background-success: #ecfcf0;
416
416
  --gx-color-background-warning: #fff8eb;
@@ -1,19 +1,31 @@
1
1
  /**
2
- * Display formatting for numbers, byte sizes, durations, relative times and percentages
3
- * (Constitution VIII-A.2). Pure functions on `Intl`, no dependencies, safe at module scope and
4
- * under SSR. Number parts default to the `en-US` locale (not the runtime's) so server and client
5
- * render the same string.
2
+ * Display formatting for numbers, byte sizes, durations, relative times, dates and percentages
3
+ * (`docs/components.md`, Hooks and utils). Pure functions on `Intl`, no dependencies, safe at
4
+ * module scope and under SSR. Number parts default to the `en-US` locale (not the runtime's) so
5
+ * server and client render the same string.
6
6
  *
7
7
  * `precision` is always the maximum number of fraction digits; trailing zeros are dropped
8
8
  * (`formatPercent(0.5)` → `"50%"`, not `"50.0%"`). Non-finite input (`NaN`, `±Infinity`) is
9
9
  * returned as `formatNumber` prints it (`"NaN"`, `"∞"`, `"-∞"`).
10
+ *
11
+ * Every formatter also takes a `bigint` (a database count, a unix-millisecond timestamp), so call
12
+ * sites never wrap one in `Number()`. `formatNumber` prints a bigint exactly (`Intl` formats it
13
+ * natively); compact numbers, byte sizes, durations and relative times scale and round anyway, so
14
+ * they convert it to a number first (exact up to 2^53).
15
+ */
16
+ /**
17
+ * What a missing value prints as: an em dash, in a table cell, a description list or a chart
18
+ * tooltip. One constant so every "no value" in an app reads the same. Render it in
19
+ * `TextVariant.TERTIARY`.
10
20
  */
21
+ export declare const EMPTY_VALUE = "\u2014";
11
22
  /**
12
23
  * Formats a number with grouping. `1234` → `"1,234"`; `{ compact: true }` → `"1.2k"`
13
24
  * (suffixes `k M B T`). `precision` is the maximum fraction digits (default `1` when compact,
14
- * otherwise the `Intl` default of `3`). `locale` defaults to `"en-US"`.
25
+ * otherwise the `Intl` default of `3`). `locale` defaults to `"en-US"`. A `bigint` prints exactly
26
+ * (`12345678901234567890n` → `"12,345,678,901,234,567,890"`).
15
27
  */
16
- export declare function formatNumber(n: number, opts?: {
28
+ export declare function formatNumber(input: number | bigint, opts?: {
17
29
  compact?: boolean;
18
30
  precision?: number;
19
31
  locale?: string;
@@ -23,7 +35,7 @@ export declare function formatNumber(n: number, opts?: {
23
35
  * `{ binary: true }` uses IEC units (`KiB`, base 1024). Whole bytes have no fraction digits.
24
36
  * `precision` is the maximum fraction digits (default `1`).
25
37
  */
26
- export declare function formatBytes(bytes: number, opts?: {
38
+ export declare function formatBytes(input: number | bigint, opts?: {
27
39
  precision?: number;
28
40
  binary?: boolean;
29
41
  }): string;
@@ -34,21 +46,34 @@ export declare function formatBytes(bytes: number, opts?: {
34
46
  * finite); zero units are omitted and the
35
47
  * remainder is truncated. Zero is `"0s"` / `"0 seconds"`. `style` defaults to `"compact"`.
36
48
  */
37
- export declare function formatDuration(ms: number, opts?: {
49
+ export declare function formatDuration(input: number | bigint, opts?: {
38
50
  style?: "compact" | "long";
39
51
  maxUnits?: number;
40
52
  }): string;
41
53
  /**
42
- * Formats a date relative to `now` (default: the current time, read at call time).
54
+ * Formats a date (or a unix-millisecond timestamp, `number` or `bigint`) relative to `now`
55
+ * (default: the current time, read at call time).
43
56
  * `{ style: "short" }` (default) → `"3m ago"`, `"in 2d"`, `"now"` (units `s m h d w mo y`);
44
57
  * `{ style: "long" }` → `Intl.RelativeTimeFormat` with `numeric: "auto"` (`"3 minutes ago"`,
45
58
  * `"in 2 days"`, `"yesterday"`, `"now"`). Uses the largest unit that fits, truncated;
46
59
  * months are 30 days, years 365 days. Under 1 second is `"now"`.
47
60
  */
48
- export declare function formatRelativeTime(date: Date | number, opts?: {
61
+ export declare function formatRelativeTime(date: Date | number | bigint, opts?: {
49
62
  now?: Date;
50
63
  style?: "short" | "long";
51
64
  }): string;
65
+ /**
66
+ * Formats a date (or a unix-millisecond timestamp, `number` or `bigint`). `{ style: "date" }`
67
+ * (default) → `"Oct 3, 2026"`, the DateInput and table default; `{ style: "dateTime" }` →
68
+ * `"Oct 10, 3:04:05 PM"`, an event in the recent past. `locale` defaults to `"en-US"`;
69
+ * `timeZone` defaults to the runtime's (pass `"UTC"` when server and client must agree). An
70
+ * invalid date is returned as `formatNumber` prints `NaN`.
71
+ */
72
+ export declare function formatDate(date: Date | number | bigint, opts?: {
73
+ style?: "date" | "dateTime";
74
+ locale?: string;
75
+ timeZone?: string;
76
+ }): string;
52
77
  /**
53
78
  * Formats a ratio as a percentage. `0.1234` → `"12.3%"`. `precision` is the maximum fraction
54
79
  * digits (default `1`).
@@ -1,15 +1,26 @@
1
1
  //#region src/utils/format.ts
2
2
  /**
3
- * Display formatting for numbers, byte sizes, durations, relative times and percentages
4
- * (Constitution VIII-A.2). Pure functions on `Intl`, no dependencies, safe at module scope and
5
- * under SSR. Number parts default to the `en-US` locale (not the runtime's) so server and client
6
- * render the same string.
3
+ * Display formatting for numbers, byte sizes, durations, relative times, dates and percentages
4
+ * (`docs/components.md`, Hooks and utils). Pure functions on `Intl`, no dependencies, safe at
5
+ * module scope and under SSR. Number parts default to the `en-US` locale (not the runtime's) so
6
+ * server and client render the same string.
7
7
  *
8
8
  * `precision` is always the maximum number of fraction digits; trailing zeros are dropped
9
9
  * (`formatPercent(0.5)` → `"50%"`, not `"50.0%"`). Non-finite input (`NaN`, `±Infinity`) is
10
10
  * returned as `formatNumber` prints it (`"NaN"`, `"∞"`, `"-∞"`).
11
+ *
12
+ * Every formatter also takes a `bigint` (a database count, a unix-millisecond timestamp), so call
13
+ * sites never wrap one in `Number()`. `formatNumber` prints a bigint exactly (`Intl` formats it
14
+ * natively); compact numbers, byte sizes, durations and relative times scale and round anyway, so
15
+ * they convert it to a number first (exact up to 2^53).
11
16
  */
12
17
  var DEFAULT_LOCALE = "en-US";
18
+ /**
19
+ * What a missing value prints as: an em dash, in a table cell, a description list or a chart
20
+ * tooltip. One constant so every "no value" in an app reads the same. Render it in
21
+ * `TextVariant.TERTIARY`.
22
+ */
23
+ var EMPTY_VALUE = "—";
13
24
  var COMPACT_SUFFIXES = [
14
25
  "",
15
26
  "k",
@@ -111,6 +122,8 @@ var RELATIVE_UNITS = [
111
122
  intl: "second"
112
123
  }
113
124
  ];
125
+ /** A bigint as a number, for the formatters that scale or round (exact up to 2^53). */
126
+ var toNumber = (value) => typeof value === "bigint" ? Number(value) : value;
114
127
  var decimal = (value, maxFractionDigits, locale = DEFAULT_LOCALE) => new Intl.NumberFormat(locale, {
115
128
  signDisplay: "negative",
116
129
  ...maxFractionDigits === void 0 ? {} : { maximumFractionDigits: maxFractionDigits }
@@ -136,11 +149,13 @@ function scale(abs, base, steps, precisionAt) {
136
149
  /**
137
150
  * Formats a number with grouping. `1234` → `"1,234"`; `{ compact: true }` → `"1.2k"`
138
151
  * (suffixes `k M B T`). `precision` is the maximum fraction digits (default `1` when compact,
139
- * otherwise the `Intl` default of `3`). `locale` defaults to `"en-US"`.
152
+ * otherwise the `Intl` default of `3`). `locale` defaults to `"en-US"`. A `bigint` prints exactly
153
+ * (`12345678901234567890n` → `"12,345,678,901,234,567,890"`).
140
154
  */
141
- function formatNumber(n, opts) {
155
+ function formatNumber(input, opts) {
142
156
  const locale = opts?.locale ?? DEFAULT_LOCALE;
143
- if (!opts?.compact || !Number.isFinite(n)) return decimal(n, opts?.precision, locale);
157
+ if (!opts?.compact || typeof input === "number" && !Number.isFinite(input)) return decimal(input, opts?.precision, locale);
158
+ const n = toNumber(input);
144
159
  const precision = opts.precision ?? 1;
145
160
  const [value, step] = scale(Math.abs(n), 1e3, COMPACT_SUFFIXES.length, () => precision);
146
161
  return `${n < 0 && decimal(value, precision, locale) !== "0" ? "-" : ""}${decimal(value, precision, locale)}${COMPACT_SUFFIXES[step] ?? ""}`;
@@ -150,7 +165,8 @@ function formatNumber(n, opts) {
150
165
  * `{ binary: true }` uses IEC units (`KiB`, base 1024). Whole bytes have no fraction digits.
151
166
  * `precision` is the maximum fraction digits (default `1`).
152
167
  */
153
- function formatBytes(bytes, opts) {
168
+ function formatBytes(input, opts) {
169
+ const bytes = toNumber(input);
154
170
  if (!Number.isFinite(bytes)) return formatNumber(bytes);
155
171
  const precision = opts?.precision ?? 1;
156
172
  const units = opts?.binary ? BINARY_BYTE_UNITS : DECIMAL_BYTE_UNITS;
@@ -166,7 +182,8 @@ function formatBytes(bytes, opts) {
166
182
  * finite); zero units are omitted and the
167
183
  * remainder is truncated. Zero is `"0s"` / `"0 seconds"`. `style` defaults to `"compact"`.
168
184
  */
169
- function formatDuration(ms, opts) {
185
+ function formatDuration(input, opts) {
186
+ const ms = toNumber(input);
170
187
  if (!Number.isFinite(ms)) return formatNumber(ms);
171
188
  const isLong = opts?.style === "long";
172
189
  const requestedUnits = opts?.maxUnits ?? 2;
@@ -186,7 +203,8 @@ function formatDuration(ms, opts) {
186
203
  return `${ms < 0 ? "-" : ""}${parts.join(" ")}`;
187
204
  }
188
205
  /**
189
- * Formats a date relative to `now` (default: the current time, read at call time).
206
+ * Formats a date (or a unix-millisecond timestamp, `number` or `bigint`) relative to `now`
207
+ * (default: the current time, read at call time).
190
208
  * `{ style: "short" }` (default) → `"3m ago"`, `"in 2d"`, `"now"` (units `s m h d w mo y`);
191
209
  * `{ style: "long" }` → `Intl.RelativeTimeFormat` with `numeric: "auto"` (`"3 minutes ago"`,
192
210
  * `"in 2 days"`, `"yesterday"`, `"now"`). Uses the largest unit that fits, truncated;
@@ -194,7 +212,7 @@ function formatDuration(ms, opts) {
194
212
  */
195
213
  function formatRelativeTime(date, opts) {
196
214
  const now = opts?.now ?? /* @__PURE__ */ new Date();
197
- const diff = (typeof date === "number" ? date : date.getTime()) - now.getTime();
215
+ const diff = (date instanceof Date ? date.getTime() : toNumber(date)) - now.getTime();
198
216
  if (!Number.isFinite(diff)) return formatNumber(diff);
199
217
  const isLong = opts?.style === "long";
200
218
  const unit = RELATIVE_UNITS.find((candidate) => Math.abs(diff) >= candidate.ms);
@@ -204,6 +222,36 @@ function formatRelativeTime(date, opts) {
204
222
  const amount = `${decimal(count, 0)}${unit.short}`;
205
223
  return diff < 0 ? `${amount} ago` : `in ${amount}`;
206
224
  }
225
+ /** The `Intl` fields of each `formatDate` style. */
226
+ var DATE_STYLE_TO_FORMAT_OPTIONS_MAP = {
227
+ date: {
228
+ month: "short",
229
+ day: "numeric",
230
+ year: "numeric"
231
+ },
232
+ dateTime: {
233
+ month: "short",
234
+ day: "numeric",
235
+ hour: "numeric",
236
+ minute: "2-digit",
237
+ second: "2-digit"
238
+ }
239
+ };
240
+ /**
241
+ * Formats a date (or a unix-millisecond timestamp, `number` or `bigint`). `{ style: "date" }`
242
+ * (default) → `"Oct 3, 2026"`, the DateInput and table default; `{ style: "dateTime" }` →
243
+ * `"Oct 10, 3:04:05 PM"`, an event in the recent past. `locale` defaults to `"en-US"`;
244
+ * `timeZone` defaults to the runtime's (pass `"UTC"` when server and client must agree). An
245
+ * invalid date is returned as `formatNumber` prints `NaN`.
246
+ */
247
+ function formatDate(date, opts) {
248
+ const time = date instanceof Date ? date.getTime() : toNumber(date);
249
+ if (!Number.isFinite(time)) return formatNumber(time);
250
+ return new Intl.DateTimeFormat(opts?.locale ?? DEFAULT_LOCALE, {
251
+ ...DATE_STYLE_TO_FORMAT_OPTIONS_MAP[opts?.style ?? "date"],
252
+ ...opts?.timeZone === void 0 ? {} : { timeZone: opts.timeZone }
253
+ }).format(time);
254
+ }
207
255
  /**
208
256
  * Formats a ratio as a percentage. `0.1234` → `"12.3%"`. `precision` is the maximum fraction
209
257
  * digits (default `1`).
@@ -217,4 +265,4 @@ function formatPercent(ratio, opts) {
217
265
  }).format(ratio);
218
266
  }
219
267
  //#endregion
220
- export { formatBytes, formatDuration, formatNumber, formatPercent, formatRelativeTime };
268
+ export { EMPTY_VALUE, formatBytes, formatDate, formatDuration, formatNumber, formatPercent, formatRelativeTime };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Text search for lists that filter as the user types: CommandPalette, Menu, SelectInput,
3
+ * MultiSelectInput and JsonViewer use it, so one term matches the same rows everywhere. The rule:
4
+ * trim the term, lower-case it with `toLocaleLowerCase`, and match when any field contains it (an
5
+ * empty term matches everything). Pure functions, safe at module scope and under SSR.
6
+ */
7
+ /** The term as it is compared: trimmed and lower-cased (`" Orders "` → `"orders"`). */
8
+ export declare function normalizeSearchTerm(term: string): string;
9
+ /**
10
+ * Whether any field contains the term, case-insensitively. An empty (or blank) term matches;
11
+ * `null` and `undefined` fields are skipped, so optional fields pass straight through:
12
+ * `isSearchMatch(term, item.label, item.description, ...item.keywords)`. The term may already be
13
+ * normalized (normalizing is idempotent).
14
+ */
15
+ export declare function isSearchMatch(term: string, ...fields: readonly (string | null | undefined)[]): boolean;
@@ -0,0 +1,24 @@
1
+ //#region src/utils/search.ts
2
+ /**
3
+ * Text search for lists that filter as the user types: CommandPalette, Menu, SelectInput,
4
+ * MultiSelectInput and JsonViewer use it, so one term matches the same rows everywhere. The rule:
5
+ * trim the term, lower-case it with `toLocaleLowerCase`, and match when any field contains it (an
6
+ * empty term matches everything). Pure functions, safe at module scope and under SSR.
7
+ */
8
+ /** The term as it is compared: trimmed and lower-cased (`" Orders "` → `"orders"`). */
9
+ function normalizeSearchTerm(term) {
10
+ return term.trim().toLocaleLowerCase();
11
+ }
12
+ /**
13
+ * Whether any field contains the term, case-insensitively. An empty (or blank) term matches;
14
+ * `null` and `undefined` fields are skipped, so optional fields pass straight through:
15
+ * `isSearchMatch(term, item.label, item.description, ...item.keywords)`. The term may already be
16
+ * normalized (normalizing is idempotent).
17
+ */
18
+ function isSearchMatch(term, ...fields) {
19
+ const normalized = normalizeSearchTerm(term);
20
+ if (normalized === "") return true;
21
+ return fields.some((field) => field?.toLocaleLowerCase().includes(normalized) ?? false);
22
+ }
23
+ //#endregion
24
+ export { isSearchMatch, normalizeSearchTerm };
@@ -1,4 +1,5 @@
1
1
  import { type Plugin } from "@wyw-in-js/vite";
2
+ import { GalaxyTheme } from "../theme/enums.js";
2
3
  /** Options for {@link galaxyDls}. */
3
4
  export type GalaxyDlsOptions = {
4
5
  /**
@@ -7,10 +8,23 @@ export type GalaxyDlsOptions = {
7
8
  * @default "gx"
8
9
  */
9
10
  prefix?: string;
11
+ /**
12
+ * The theme the page boots in before `GalaxyProvider` mounts. Pass the provider's own `theme`; a
13
+ * choice stored under `storageKey` still wins, as it does in the provider.
14
+ * @default GalaxyTheme.SYSTEM
15
+ */
16
+ theme?: GalaxyTheme;
17
+ /**
18
+ * Where the provider stores the user's choice; the boot script reads it and never writes it.
19
+ * Pass the provider's `storageKey` if you changed it.
20
+ * @default "galaxy:theme"
21
+ */
22
+ storageKey?: string;
10
23
  };
11
24
  /**
12
25
  * Returns the configured `@wyw-in-js/vite` plugin: the DLS Babel presets (React automatic
13
- * runtime + TypeScript), `include`/`exclude`, React `importOverrides`, and the DLS
14
- * `classNameSlug` (`gx-<Component>-<Part>-<hash>`) with `gx` replaced by `prefix`.
26
+ * runtime + TypeScript), `include`/`exclude`, and the DLS's own class-name config (the slug
27
+ * `gx-<Component>-<Part>-<hash>` with `gx` replaced by `prefix`, no vendor prefixes, React
28
+ * `importOverrides`), so app CSS is named and emitted like DLS CSS.
15
29
  */
16
30
  export declare function galaxyDls(options?: GalaxyDlsOptions): Plugin;
@@ -1,3 +1,6 @@
1
+ import { GalaxyTheme } from "../theme/enums.js";
2
+ import { BOOT_THEMES, DEFAULT_THEME_STORAGE_KEY, themeBackgroundStyle, themeBootScript } from "./internal/themeBoot.js";
3
+ import { sharedWywOptions } from "./internal/wywOptions.js";
1
4
  import { createRequire } from "node:module";
2
5
  import wyw from "@wyw-in-js/vite";
3
6
  //#region src/vite/index.ts
@@ -12,41 +15,47 @@ import wyw from "@wyw-in-js/vite";
12
15
  *
13
16
  * The DLS itself ships pre-extracted CSS (`@galaxy-io/dls/styles.css`); this preset only
14
17
  * compiles the app's own `styled` / `css` code, which reads DLS tokens through `t`.
18
+ *
19
+ * It also boots the theme in `index.html`: one inline script sets `data-gx-theme` on `<html>`
20
+ * before the first paint (the stored choice, else `theme`, resolving `SYSTEM` from the OS), and one
21
+ * `<style>` paints the canvas per theme, so a page never flashes the wrong theme before
22
+ * `GalaxyProvider` mounts. Pass the same `theme` and `storageKey` as the app's provider. A strict
23
+ * Content Security Policy needs the script's hash.
15
24
  */
16
- /** The DLS class prefix; also the default app prefix. */
17
- var DEFAULT_PREFIX = "gx";
18
25
  /** A valid start for a CSS class name: a letter, then letters, digits or `-`. */
19
26
  var PREFIX_PATTERN = /^[A-Za-z][A-Za-z0-9-]*$/;
20
- /** The styled root is named `Root`; its class drops the part segment. */
21
- var ROOT_PART = "Root";
22
27
  /** Files the plugin compiles. The DLS (and every other package) ships already compiled. */
23
28
  var INCLUDE = ["**/*.{js,jsx,ts,tsx}"];
24
29
  var EXCLUDE = ["**/node_modules/**"];
25
30
  /**
26
- * React is reached while wyw evaluates `styled` components at build time; loading it as-is is
27
- * expected, so the "unknown import reached during eval" warning is silenced for it.
28
- */
29
- var IMPORT_OVERRIDES = {
30
- react: { unknown: "allow" },
31
- "react/jsx-runtime": { unknown: "allow" },
32
- "react/jsx-dev-runtime": { unknown: "allow" }
33
- };
34
- /**
35
31
  * Returns the configured `@wyw-in-js/vite` plugin: the DLS Babel presets (React automatic
36
- * runtime + TypeScript), `include`/`exclude`, React `importOverrides`, and the DLS
37
- * `classNameSlug` (`gx-<Component>-<Part>-<hash>`) with `gx` replaced by `prefix`.
32
+ * runtime + TypeScript), `include`/`exclude`, and the DLS's own class-name config (the slug
33
+ * `gx-<Component>-<Part>-<hash>` with `gx` replaced by `prefix`, no vendor prefixes, React
34
+ * `importOverrides`), so app CSS is named and emitted like DLS CSS.
38
35
  */
39
36
  function galaxyDls(options = {}) {
40
- const { prefix = DEFAULT_PREFIX } = options;
37
+ const { prefix = "gx", theme = GalaxyTheme.SYSTEM, storageKey = DEFAULT_THEME_STORAGE_KEY } = options;
41
38
  if (!PREFIX_PATTERN.test(prefix)) throw new Error(`[@galaxy-io/dls/vite] prefix "${prefix}" is not a valid class-name start: use letters, digits and "-", starting with a letter.`);
39
+ if (!BOOT_THEMES.includes(theme)) throw new Error(`[@galaxy-io/dls/vite] theme "${theme}" is not a GalaxyTheme: use GalaxyTheme.SYSTEM, DARK or LIGHT.`);
42
40
  const require = createRequire(import.meta.url);
43
- return wyw({
41
+ const plugin = wyw({
44
42
  include: INCLUDE,
45
43
  exclude: EXCLUDE,
46
44
  babelOptions: { presets: [[require.resolve("@babel/preset-react"), { runtime: "automatic" }], require.resolve("@babel/preset-typescript")] },
47
- importOverrides: IMPORT_OVERRIDES,
48
- classNameSlug: (hash, title, { name }) => `${prefix}-${name}${title === ROOT_PART ? "" : `-${title}`}-${hash}`
45
+ ...sharedWywOptions(prefix)
49
46
  });
47
+ const bootScript = themeBootScript(theme, storageKey);
48
+ const backgroundStyle = themeBackgroundStyle();
49
+ plugin.transformIndexHtml = () => [{
50
+ tag: "script",
51
+ children: bootScript,
52
+ injectTo: "head-prepend"
53
+ }, {
54
+ tag: "style",
55
+ children: backgroundStyle,
56
+ injectTo: "head-prepend"
57
+ }];
58
+ return plugin;
50
59
  }
51
60
  //#endregion
52
61
  export { galaxyDls };