@intility/bifrost-experimental 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,7 +1,23 @@
1
1
  # @intility/bifrost-experimental
2
2
 
3
- ## 0.1.0
3
+ ## 0.3.0
4
4
 
5
5
  ### Minor Changes
6
6
 
7
- - 2b88573: New package for experimental, AI-generated components. Adds `TwinkleBackground`, an animated dot-grid background in the Bifrost theme color, `Backdrop`, a soft shaded area that keeps text readable on a busy background, and `FrontpageHeader`, a logo, title and subtitle for the front page of a Platform Manager module.
7
+ - 7ed86c6: `TwinkleBackground` now covers the top 35% of the viewport height instead of the top 40% of its container, so it looks the same on every front page, however tall the page is, but at least 240px. If the container is shorter than that, the dots cover the whole container. The first row of dots now always sits 8px from the top, where it before moved between 8px and 16px depending on the height. `fill` is unchanged. You can remove any CSS that overrides the height of the canvas.
8
+
9
+ ## 0.2.0 - 2026-10-28
10
+
11
+ ### Minor Changes
12
+
13
+ - d272c33: `TwinkleBackground` has a new `fill` prop for using it inside a `Box` or a card. The dots then cover the whole container, evenly, with no fade. The dot grid is now also centered with a margin to the edges, so the edge of the container no longer cuts a dot in half.
14
+
15
+ ### Patch Changes
16
+
17
+ - 75f2bf7: `Backdrop` no longer uses `filter: blur()` for its soft edges. Safari on some Macs drew a dark or light halo around the shade when switching between light and dark mode. The edges now fade with a gradient mask instead.
18
+
19
+ ## 0.1.0 - 2026-10-27
20
+
21
+ ### First release
22
+
23
+ - 2b88573: New package for experimental, AI-generated components. Adds `TwinkleBackground`, an animated dot-grid background in the Bifrost theme color, `Backdrop`, a soft shaded area that keeps text readable on a busy background, and `FrontpageHeader`, a logo, title and subtitle for the front page of a Platform Manager module. #998
@@ -17,14 +17,48 @@ span.bfx-backdrop {
17
17
  z-index: -1;
18
18
  pointer-events: none;
19
19
 
20
- /* Larger than the content without changing the layout, but no wider than the
21
- page padding, so it can't reach past the screen edge on small screens */
22
- inset: calc(-1 * var(--bfs12))
23
- calc(-1 * min(var(--bfs24), var(--bfl-page-padding)));
24
- opacity: 0.85;
20
+ /* How far the shade reaches past the content, which is also the width of its
21
+ fade. The horizontal reach is no wider than the page padding, so the shade
22
+ can't reach past the screen edge on small screens. */
23
+ --bfx-backdrop-x: min(var(--bfs32), var(--bfl-page-padding));
24
+ --bfx-backdrop-y: var(--bfs24);
25
25
 
26
- /* Soft edges, so it doesn't look like a box */
27
- filter: blur(10px);
26
+ /* Larger than the content without changing the layout */
27
+ inset: calc(-1 * var(--bfx-backdrop-y)) calc(-1 * var(--bfx-backdrop-x));
28
+ opacity: 0.7;
29
+
30
+ /* Soft edges, so it doesn't look like a box. Each edge eases out over the
31
+ reach, like the tail of a blur, and the two masks are multiplied so the
32
+ corners fade too. A mask rather than filter: blur(), which Safari renders
33
+ with a dark or light halo on some Macs. */
34
+ mask-image:
35
+ linear-gradient(
36
+ to bottom,
37
+ transparent,
38
+ rgba(0, 0, 0, 0.1) calc(var(--bfx-backdrop-y) * 0.25),
39
+ rgba(0, 0, 0, 0.5) calc(var(--bfx-backdrop-y) * 0.5),
40
+ rgba(0, 0, 0, 0.9) calc(var(--bfx-backdrop-y) * 0.75),
41
+ #000 var(--bfx-backdrop-y),
42
+ #000 calc(100% - var(--bfx-backdrop-y)),
43
+ rgba(0, 0, 0, 0.9) calc(100% - var(--bfx-backdrop-y) * 0.75),
44
+ rgba(0, 0, 0, 0.5) calc(100% - var(--bfx-backdrop-y) * 0.5),
45
+ rgba(0, 0, 0, 0.1) calc(100% - var(--bfx-backdrop-y) * 0.25),
46
+ transparent
47
+ ),
48
+ linear-gradient(
49
+ to right,
50
+ transparent,
51
+ rgba(0, 0, 0, 0.1) calc(var(--bfx-backdrop-x) * 0.25),
52
+ rgba(0, 0, 0, 0.5) calc(var(--bfx-backdrop-x) * 0.5),
53
+ rgba(0, 0, 0, 0.9) calc(var(--bfx-backdrop-x) * 0.75),
54
+ #000 var(--bfx-backdrop-x),
55
+ #000 calc(100% - var(--bfx-backdrop-x)),
56
+ rgba(0, 0, 0, 0.9) calc(100% - var(--bfx-backdrop-x) * 0.75),
57
+ rgba(0, 0, 0, 0.5) calc(100% - var(--bfx-backdrop-x) * 0.5),
58
+ rgba(0, 0, 0, 0.1) calc(100% - var(--bfx-backdrop-x) * 0.25),
59
+ transparent
60
+ );
61
+ mask-composite: intersect;
28
62
  }
29
63
 
30
64
  /* A stacking context, so the title's and subtitle's backdrops sit behind both
@@ -63,7 +97,13 @@ overlap the title. */
63
97
 
64
98
  /* A canvas is a replaced element, so inset alone doesn't stretch it */
65
99
  width: 100%;
66
- height: 40%;
100
+
101
+ /* 35% of the viewport, not of the container, so every front page looks the
102
+ same however tall it is. svh and not dvh: the dvh changes while a mobile
103
+ browser's toolbar shows and hides, which would lay the dots out again while
104
+ scrolling. At least 240px, so a short viewport still gets a fade that reads
105
+ as one. 100% keeps it inside a container shorter than that. */
106
+ height: min(max(35svh, 240px), 100%);
67
107
  z-index: -1;
68
108
  pointer-events: none;
69
109
 
@@ -76,6 +116,10 @@ overlap the title. */
76
116
  );
77
117
  }
78
118
 
119
+ .bfx-twinkle-background-fill {
120
+ height: 100%;
121
+ }
122
+
79
123
  /* Red and green aren't themes. The theme colors have a lightness of 46% to
80
124
  55% in light mode and 71% to 90% in dark mode, so the lightness is moved
81
125
  into that range where alert (55% dark) and success (85% light) are far off. */
@@ -5,13 +5,23 @@ export type TwinkleBackgroundProps = {
5
5
  * @default "theme"
6
6
  */
7
7
  color?: "theme" | "teal" | "purple" | "pink" | "yellow" | "red" | "green";
8
+ /**
9
+ * Fills the whole container with evenly spread dots, with no fade, for a
10
+ * background inside a card or a `Box`. By default the dots cover the top 35%
11
+ * of the viewport height, at least 240px, or the whole container if that is
12
+ * shorter, and fade out down the page.
13
+ * @default false
14
+ */
15
+ fill?: boolean;
8
16
  };
9
17
  /**
10
18
  * Animated dot grid in the Bifrost theme color, for the front page of a
11
19
  * Platform Manager module. It fades out down the page and reacts to hover and
12
20
  * clicks. Render it as the first child of a container with
13
- * `position: relative` and `isolation: isolate`; it fills the top 40% of that
14
- * container, behind its content. Experimental: AI-generated, not reviewed line
21
+ * `position: relative` and `isolation: isolate`; it covers the top 35% of the
22
+ * viewport height, at least 240px (the whole container if that is shorter),
23
+ * behind its content, or all of the container with `fill`. No dot is cut off by the edge
24
+ * of the container. Experimental: AI-generated, not reviewed line
15
25
  * by line.
16
26
  *
17
27
  * @see https://bifrost.intility.com/react/experimental/twinkle-background
@@ -24,5 +34,5 @@ export type TwinkleBackgroundProps = {
24
34
  * </Backdrop>
25
35
  * </div>
26
36
  */
27
- declare const TwinkleBackground: ({ color }: TwinkleBackgroundProps) => import("react").JSX.Element;
37
+ declare const TwinkleBackground: ({ color, fill, }: TwinkleBackgroundProps) => import("react").JSX.Element;
28
38
  export default TwinkleBackground;
@@ -47,8 +47,10 @@ const within = (rect, x, y) => x >= 0 && x <= rect.width && y >= 0 && y <= rect.
47
47
  * Animated dot grid in the Bifrost theme color, for the front page of a
48
48
  * Platform Manager module. It fades out down the page and reacts to hover and
49
49
  * clicks. Render it as the first child of a container with
50
- * `position: relative` and `isolation: isolate`; it fills the top 40% of that
51
- * container, behind its content. Experimental: AI-generated, not reviewed line
50
+ * `position: relative` and `isolation: isolate`; it covers the top 35% of the
51
+ * viewport height, at least 240px (the whole container if that is shorter),
52
+ * behind its content, or all of the container with `fill`. No dot is cut off by the edge
53
+ * of the container. Experimental: AI-generated, not reviewed line
52
54
  * by line.
53
55
  *
54
56
  * @see https://bifrost.intility.com/react/experimental/twinkle-background
@@ -62,18 +64,20 @@ const within = (rect, x, y) => x >= 0 && x <= rect.width && y >= 0 && y <= rect.
62
64
  * </div>
63
65
  */
64
66
  const TwinkleBackground = t0 => {
65
- const $ = _c(5);
67
+ const $ = _c(6);
66
68
  const {
67
- color: t1
69
+ color: t1,
70
+ fill: t2
68
71
  } = t0;
69
72
  const color = t1 === undefined ? "theme" : t1;
73
+ const fill = t2 === undefined ? false : t2;
70
74
  const canvasRef = useRef(null);
71
75
  const still = useReducedMotion();
72
76
  const colorClass = color === "red" || color === "green" ? ` bfx-twinkle-background-${color}` : color === "theme" ? "" : ` bf-theme-${color}`;
73
- let t2;
74
77
  let t3;
75
- if ($[0] !== still) {
76
- t2 = () => {
78
+ let t4;
79
+ if ($[0] !== fill || $[1] !== still) {
80
+ t3 = () => {
77
81
  const canvas = canvasRef.current;
78
82
  const ctx = canvas?.getContext("2d");
79
83
  if (!canvas || !ctx) {
@@ -93,7 +97,10 @@ const TwinkleBackground = t0 => {
93
97
  const clicks = [];
94
98
  let w = 0;
95
99
  let h = 0;
96
- let cols = 0;
100
+ let cols = 1;
101
+ let offsetX = 0;
102
+ let offsetY = 0;
103
+ let span = 0;
97
104
  let rand = new Float32Array(0);
98
105
  const seed = Math.random() * 4294967296 | 0;
99
106
  const noise = (col, row, k) => {
@@ -106,12 +113,12 @@ const TwinkleBackground = t0 => {
106
113
  let last = 0;
107
114
  let raf = 0;
108
115
  let visible = false;
109
- let fill = "";
116
+ let dotColor = "";
110
117
  let base = OPACITY.light;
111
118
  let styleAt = -Infinity;
112
119
  const readStyle = () => {
113
120
  const style = getComputedStyle(canvas);
114
- fill = style.color;
121
+ dotColor = style.color;
115
122
  base = style.colorScheme.includes("dark") ? OPACITY.dark : OPACITY.light;
116
123
  };
117
124
  const batches = Array.from({
@@ -122,12 +129,12 @@ const TwinkleBackground = t0 => {
122
129
  if (forcedColors.matches || h <= 0) {
123
130
  return;
124
131
  }
125
- ctx.fillStyle = fill;
132
+ ctx.fillStyle = dotColor;
126
133
  for (let i = 0; i < rand.length; i = i + 2, i) {
127
134
  const r1 = rand[i];
128
135
  const r2 = rand[i + 1];
129
- const x = i / 2 % cols * SPACING;
130
- const y = Math.floor(i / 2 / cols) * SPACING;
136
+ const x = offsetX + i / 2 % cols * SPACING;
137
+ const y = offsetY + Math.floor(i / 2 / cols) * SPACING;
131
138
  const tw = 0.5 + 0.5 * Math.sin(clock * 0.48 * (1 + r1 * 3) + r2 * TAU);
132
139
  let alpha = MIN_ALPHA + (1 - MIN_ALPHA) * tw * tw;
133
140
  let size = 1.4 * (1 - 0.3 * r2) * (0.8 + 0.4 * tw);
@@ -144,7 +151,7 @@ const TwinkleBackground = t0 => {
144
151
  alpha = alpha + ring * 2.6;
145
152
  size = size + ring * 1.4;
146
153
  }
147
- const fade = y / h;
154
+ const fade = fill || span <= 0 ? 0 : (y - offsetY) / span;
148
155
  alpha = alpha * (1 - fade * fade) * base;
149
156
  const step = Math.round(Math.min(1, alpha) * ALPHA_STEPS);
150
157
  if (step < 1) {
@@ -221,9 +228,13 @@ const TwinkleBackground = t0 => {
221
228
  canvas.width = Math.round(w * dpr);
222
229
  canvas.height = Math.round(h * dpr);
223
230
  ctx.setTransform(w ? canvas.width / w : 1, 0, 0, h ? canvas.height / h : 1, 0, 0);
224
- cols = Math.ceil(w / SPACING) + 1;
231
+ cols = Math.max(1, Math.floor(w / SPACING));
232
+ const rows = Math.max(1, Math.floor(h / SPACING));
233
+ span = (rows - 1) * SPACING;
234
+ offsetX = (w - (cols - 1) * SPACING) / 2;
235
+ offsetY = fill ? (h - span) / 2 : SPACING / 2;
225
236
  rand = Float32Array.from({
226
- length: cols * (Math.ceil(h / SPACING) + 1) * 2
237
+ length: cols * rows * 2
227
238
  }, (_, i_1) => noise((i_1 >> 1) % cols, Math.floor(i_1 / 2 / cols), i_1 & 1));
228
239
  redraw();
229
240
  };
@@ -299,8 +310,8 @@ const TwinkleBackground = t0 => {
299
310
  });
300
311
  }
301
312
  }
302
- const visibility = new IntersectionObserver(t4 => {
303
- const [entry] = t4;
313
+ const visibility = new IntersectionObserver(t5 => {
314
+ const [entry] = t5;
304
315
  visible = entry.isIntersecting;
305
316
  if (visible) {
306
317
  play();
@@ -341,29 +352,30 @@ const TwinkleBackground = t0 => {
341
352
  window.removeEventListener("blur", onLeave);
342
353
  };
343
354
  };
344
- t3 = [still];
345
- $[0] = still;
346
- $[1] = t2;
355
+ t4 = [still, fill];
356
+ $[0] = fill;
357
+ $[1] = still;
347
358
  $[2] = t3;
359
+ $[3] = t4;
348
360
  } else {
349
- t2 = $[1];
350
361
  t3 = $[2];
362
+ t4 = $[3];
351
363
  }
352
- useEffect(t2, t3);
353
- const t4 = `bfx-twinkle-background${colorClass}`;
354
- let t5;
355
- if ($[3] !== t4) {
356
- t5 = /*#__PURE__*/_jsx("canvas", {
364
+ useEffect(t3, t4);
365
+ const t5 = `bfx-twinkle-background${fill ? " bfx-twinkle-background-fill" : ""}${colorClass}`;
366
+ let t6;
367
+ if ($[4] !== t5) {
368
+ t6 = /*#__PURE__*/_jsx("canvas", {
357
369
  ref: canvasRef,
358
370
  "aria-hidden": true,
359
- className: t4
371
+ className: t5
360
372
  });
361
- $[3] = t4;
362
373
  $[4] = t5;
374
+ $[5] = t6;
363
375
  } else {
364
- t5 = $[4];
376
+ t6 = $[5];
365
377
  }
366
- return t5;
378
+ return t6;
367
379
  };
368
380
  export default TwinkleBackground;
369
381
  function _temp() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intility/bifrost-experimental",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Experimental, AI-generated React components for Intility's design system, Bifrost.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",