@intility/bifrost-experimental 0.0.0 → 0.2.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 ADDED
@@ -0,0 +1,17 @@
1
+ # @intility/bifrost-experimental
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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.
8
+
9
+ ### Patch Changes
10
+
11
+ - 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.
12
+
13
+ ## 0.1.0 - 2026-10-27
14
+
15
+ ### First release
16
+
17
+ - 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
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @intility/bifrost-experimental
2
+
3
+ Experimental, AI-generated React components for Intility's design system,
4
+ Bifrost. See [Experimental](https://bifrost.intility.com/react/experimental)
5
+ in the style guide.
6
+
7
+ ## What experimental means
8
+
9
+ The components are built with AI and aren't reviewed line by line. They are
10
+ front end only: they depend on nothing but React and Bifrost, and make no
11
+ network calls. Lint rules and tests catch common mistakes, but they aren't a
12
+ guarantee. Use the components in your apps, but expect them to change more
13
+ often than the Bifrost packages.
14
+
15
+ ## Install
16
+
17
+ ```shell
18
+ npm install @intility/bifrost-experimental
19
+ ```
20
+
21
+ It needs `react`, `react-dom` and `@intility/bifrost-react`. Import the CSS
22
+ once, after the Bifrost CSS:
23
+
24
+ ```tsx
25
+ import "@intility/bifrost-react/bifrost-app.css";
26
+ import "@intility/bifrost-experimental/bifrost-experimental.css";
27
+ ```
28
+
29
+ Import each component from its own path:
30
+
31
+ ```tsx
32
+ import TwinkleBackground from "@intility/bifrost-experimental/TwinkleBackground";
33
+ ```
34
+
35
+ ## Components
36
+
37
+ - [`Backdrop`](https://bifrost.intility.com/react/experimental/backdrop):
38
+ a soft shaded area that keeps text readable on a busy background
39
+ - [`FrontpageHeader`](https://bifrost.intility.com/react/experimental/frontpage-header):
40
+ logo, title and subtitle for the front page of a Platform Manager module
41
+ - [`TwinkleBackground`](https://bifrost.intility.com/react/experimental/twinkle-background):
42
+ an animated dot grid in the theme color, for the front page of a Platform
43
+ Manager module
44
+
45
+ ## Contributing
46
+
47
+ See
48
+ [AGENTS.md](https://github.com/intility/bifrost/blob/main/packages/bifrost-experimental/AGENTS.md)
49
+ for the rules and commands, whether you write the component yourself or with
50
+ an AI agent.
51
+
52
+ Members of the `bifrost-experimenters` team in the `intility` GitHub
53
+ organization can approve changes to the components, their docs pages
54
+ (`docs/app/react/experimental/`) and the Experimental sidebar group
55
+ (`docs/utils/experimentalNavigation.ts`). Changes to the package configs, the
56
+ guardrails and the rest of the docs need a Bifrost developer, as
57
+ [CODEOWNERS](https://github.com/intility/bifrost/blob/main/CODEOWNERS) defines.
@@ -0,0 +1,132 @@
1
+ /* No z-index or isolation: the shade has to join the stacking context around
2
+ it, so it sits above what is behind it but below all the text */
3
+ .bfx-backdrop {
4
+ display: block;
5
+ position: relative;
6
+ width: fit-content;
7
+ }
8
+
9
+ /* The wrapper only takes the text color from its bfc-*-bg class. The type
10
+ selector outranks the class, whatever order the stylesheets load in. */
11
+ span.bfx-backdrop {
12
+ background-color: transparent;
13
+ }
14
+
15
+ .bfx-backdrop-shade {
16
+ position: absolute;
17
+ z-index: -1;
18
+ pointer-events: none;
19
+
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
+
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;
62
+ }
63
+
64
+ /* A stacking context, so the title's and subtitle's backdrops sit behind both
65
+ lines and neither covers the other line */
66
+ .bfx-frontpage-header {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: var(--bfs16);
70
+ isolation: isolate;
71
+ }
72
+
73
+ .bfx-frontpage-header-logo {
74
+ display: flex;
75
+ flex-shrink: 0;
76
+ height: 72px;
77
+ }
78
+
79
+ /* Stretching from the slot isn't enough in Safari, which sizes the slot from
80
+ the image's own width. An SVG with only a viewBox has none, so the logo would
81
+ overlap the title. */
82
+ .bfx-frontpage-header-logo > * {
83
+ height: 100%;
84
+ width: auto;
85
+ }
86
+
87
+ /* flex-start keeps each line only as wide as its text */
88
+ .bfx-frontpage-header-text {
89
+ display: flex;
90
+ flex-direction: column;
91
+ align-items: flex-start;
92
+ gap: var(--bfs8);
93
+ }
94
+ .bfx-twinkle-background {
95
+ position: absolute;
96
+ inset: 0 0 auto;
97
+
98
+ /* A canvas is a replaced element, so inset alone doesn't stretch it */
99
+ width: 100%;
100
+ height: 40%;
101
+ z-index: -1;
102
+ pointer-events: none;
103
+
104
+ /* The component reads this color back for the canvas. Light mode cuts the
105
+ chroma to a quarter, so the dots are only a hint of color. Browsers without
106
+ relative colors fall back to the text color. */
107
+ color: light-dark(
108
+ oklch(from var(--bfc-theme) l calc(c * 0.25) h),
109
+ var(--bfc-theme)
110
+ );
111
+ }
112
+
113
+ .bfx-twinkle-background-fill {
114
+ height: 100%;
115
+ }
116
+
117
+ /* Red and green aren't themes. The theme colors have a lightness of 46% to
118
+ 55% in light mode and 71% to 90% in dark mode, so the lightness is moved
119
+ into that range where alert (55% dark) and success (85% light) are far off. */
120
+ .bfx-twinkle-background-red {
121
+ color: light-dark(
122
+ oklch(from var(--bfc-alert) l calc(c * 0.25) h),
123
+ oklch(from var(--bfc-alert) 66% c h)
124
+ );
125
+ }
126
+
127
+ .bfx-twinkle-background-green {
128
+ color: light-dark(
129
+ oklch(from var(--bfc-success) 52% calc(c * 0.25) h),
130
+ var(--bfc-success)
131
+ );
132
+ }
@@ -0,0 +1,31 @@
1
+ import type { BoxProps } from "@intility/bifrost-react/Box";
2
+ import type { ReactNode } from "react";
3
+ export type BackdropProps = {
4
+ children: ReactNode;
5
+ /**
6
+ * The background the backdrop sits on, which is the color it fades out to.
7
+ * Takes the same values as the `background` prop of Bifrost's Box, and also
8
+ * sets the matching text color.
9
+ * @example
10
+ * `"base"` is the page background, and the default.
11
+ * `true` and `"base-3"` match `.bfc-base-3-bg`.
12
+ * `"theme-fade"` matches `.bfc-theme-fade-bg`.
13
+ */
14
+ background?: BoxProps["background"];
15
+ };
16
+ /**
17
+ * Soft shaded area behind one line of text or one see-through control, so it
18
+ * stays readable on a busy background like the twinkle background. Wrap each
19
+ * line on its own, not a group of lines. Experimental: AI-generated, not
20
+ * reviewed line by line.
21
+ *
22
+ * @see https://bifrost.intility.com/react/experimental/backdrop
23
+ *
24
+ * @example
25
+ * <h1 className="bf-h1">
26
+ * <Backdrop>Platform Manager</Backdrop>
27
+ * </h1>
28
+ * <Backdrop background="base-3">On a base-3 background</Backdrop>
29
+ */
30
+ declare const Backdrop: ({ children, background }: BackdropProps) => import("react").JSX.Element;
31
+ export default Backdrop;
@@ -0,0 +1,53 @@
1
+ import { c as _c } from "react-compiler-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Soft shaded area behind one line of text or one see-through control, so it
5
+ * stays readable on a busy background like the twinkle background. Wrap each
6
+ * line on its own, not a group of lines. Experimental: AI-generated, not
7
+ * reviewed line by line.
8
+ *
9
+ * @see https://bifrost.intility.com/react/experimental/backdrop
10
+ *
11
+ * @example
12
+ * <h1 className="bf-h1">
13
+ * <Backdrop>Platform Manager</Backdrop>
14
+ * </h1>
15
+ * <Backdrop background="base-3">On a base-3 background</Backdrop>
16
+ */
17
+ const Backdrop = t0 => {
18
+ const $ = _c(6);
19
+ const {
20
+ children,
21
+ background: t1
22
+ } = t0;
23
+ const background = t1 === undefined ? "base" : t1;
24
+ const bg = `bfc-${background === true ? "base-3" : background || "base"}-bg`;
25
+ const t2 = `bfx-backdrop ${bg}`;
26
+ const t3 = `bfx-backdrop-shade ${bg}`;
27
+ let t4;
28
+ if ($[0] !== t3) {
29
+ t4 = /*#__PURE__*/_jsx("span", {
30
+ "aria-hidden": true,
31
+ className: t3
32
+ });
33
+ $[0] = t3;
34
+ $[1] = t4;
35
+ } else {
36
+ t4 = $[1];
37
+ }
38
+ let t5;
39
+ if ($[2] !== children || $[3] !== t2 || $[4] !== t4) {
40
+ t5 = /*#__PURE__*/_jsxs("span", {
41
+ className: t2,
42
+ children: [t4, children]
43
+ });
44
+ $[2] = children;
45
+ $[3] = t2;
46
+ $[4] = t4;
47
+ $[5] = t5;
48
+ } else {
49
+ t5 = $[5];
50
+ }
51
+ return t5;
52
+ };
53
+ export default Backdrop;
@@ -0,0 +1,2 @@
1
+ export type * from "./Backdrop.js";
2
+ export { default } from "./Backdrop.js";
@@ -0,0 +1 @@
1
+ export { default } from "./Backdrop.js";
@@ -0,0 +1,33 @@
1
+ import type { ReactNode } from "react";
2
+ export type FrontpageHeaderProps = {
3
+ /**
4
+ * The name of the app or module. Rendered as the page's `h1`, so use the
5
+ * component once per page.
6
+ */
7
+ title: ReactNode;
8
+ /** One line under the title */
9
+ subtitle?: ReactNode;
10
+ /**
11
+ * Image shown to the left of the title, like `<img src="/logo.svg" alt="" />`.
12
+ * It is shown 72px high and hidden on small screens. Use an empty `alt` when
13
+ * the title already names the app.
14
+ */
15
+ logo?: ReactNode;
16
+ };
17
+ /**
18
+ * Header for the front page of a Platform Manager module: logo, title and
19
+ * subtitle. The title is the page's `h1`. To keep the title and subtitle
20
+ * readable on the twinkle background, wrap each in a `Backdrop`.
21
+ * Experimental: AI-generated, not reviewed line by line.
22
+ *
23
+ * @see https://bifrost.intility.com/react/experimental/frontpage-header
24
+ *
25
+ * @example
26
+ * <FrontpageHeader
27
+ * logo={<img src="/logo.svg" alt="" />}
28
+ * title={<Backdrop>Platform Manager</Backdrop>}
29
+ * subtitle={<Backdrop>An operational control plane</Backdrop>}
30
+ * />
31
+ */
32
+ declare const FrontpageHeader: ({ title, subtitle, logo }: FrontpageHeaderProps) => import("react").JSX.Element;
33
+ export default FrontpageHeader;
@@ -0,0 +1,84 @@
1
+ import { c as _c } from "react-compiler-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Header for the front page of a Platform Manager module: logo, title and
5
+ * subtitle. The title is the page's `h1`. To keep the title and subtitle
6
+ * readable on the twinkle background, wrap each in a `Backdrop`.
7
+ * Experimental: AI-generated, not reviewed line by line.
8
+ *
9
+ * @see https://bifrost.intility.com/react/experimental/frontpage-header
10
+ *
11
+ * @example
12
+ * <FrontpageHeader
13
+ * logo={<img src="/logo.svg" alt="" />}
14
+ * title={<Backdrop>Platform Manager</Backdrop>}
15
+ * subtitle={<Backdrop>An operational control plane</Backdrop>}
16
+ * />
17
+ */
18
+ const FrontpageHeader = t0 => {
19
+ const $ = _c(12);
20
+ const {
21
+ title,
22
+ subtitle,
23
+ logo
24
+ } = t0;
25
+ let t1;
26
+ if ($[0] !== logo) {
27
+ t1 = logo && /*#__PURE__*/_jsx("span", {
28
+ className: "bfx-frontpage-header-logo from-small",
29
+ children: logo
30
+ });
31
+ $[0] = logo;
32
+ $[1] = t1;
33
+ } else {
34
+ t1 = $[1];
35
+ }
36
+ let t2;
37
+ if ($[2] !== title) {
38
+ t2 = /*#__PURE__*/_jsx("h1", {
39
+ className: "bf-h1",
40
+ children: title
41
+ });
42
+ $[2] = title;
43
+ $[3] = t2;
44
+ } else {
45
+ t2 = $[3];
46
+ }
47
+ let t3;
48
+ if ($[4] !== subtitle) {
49
+ t3 = subtitle && /*#__PURE__*/_jsx("div", {
50
+ className: "bf-large",
51
+ children: subtitle
52
+ });
53
+ $[4] = subtitle;
54
+ $[5] = t3;
55
+ } else {
56
+ t3 = $[5];
57
+ }
58
+ let t4;
59
+ if ($[6] !== t2 || $[7] !== t3) {
60
+ t4 = /*#__PURE__*/_jsxs("div", {
61
+ className: "bfx-frontpage-header-text",
62
+ children: [t2, t3]
63
+ });
64
+ $[6] = t2;
65
+ $[7] = t3;
66
+ $[8] = t4;
67
+ } else {
68
+ t4 = $[8];
69
+ }
70
+ let t5;
71
+ if ($[9] !== t1 || $[10] !== t4) {
72
+ t5 = /*#__PURE__*/_jsxs("div", {
73
+ className: "bfx-frontpage-header",
74
+ children: [t1, t4]
75
+ });
76
+ $[9] = t1;
77
+ $[10] = t4;
78
+ $[11] = t5;
79
+ } else {
80
+ t5 = $[11];
81
+ }
82
+ return t5;
83
+ };
84
+ export default FrontpageHeader;
@@ -0,0 +1,2 @@
1
+ export type * from "./FrontpageHeader.js";
2
+ export { default } from "./FrontpageHeader.js";
@@ -0,0 +1 @@
1
+ export { default } from "./FrontpageHeader.js";
@@ -0,0 +1,36 @@
1
+ export type TwinkleBackgroundProps = {
2
+ /**
3
+ * The color of the dots. `"theme"` follows the Bifrost theme of the app, and
4
+ * the other values override it.
5
+ * @default "theme"
6
+ */
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 40%
11
+ * of the container and fade out down the page.
12
+ * @default false
13
+ */
14
+ fill?: boolean;
15
+ };
16
+ /**
17
+ * Animated dot grid in the Bifrost theme color, for the front page of a
18
+ * Platform Manager module. It fades out down the page and reacts to hover and
19
+ * clicks. Render it as the first child of a container with
20
+ * `position: relative` and `isolation: isolate`; it fills the top 40% of that
21
+ * container, behind its content, or all of it with `fill`. No dot is cut off
22
+ * by the edge of the container. Experimental: AI-generated, not reviewed line
23
+ * by line.
24
+ *
25
+ * @see https://bifrost.intility.com/react/experimental/twinkle-background
26
+ *
27
+ * @example
28
+ * <div style={{ position: "relative", isolation: "isolate" }}>
29
+ * <TwinkleBackground />
30
+ * <Backdrop>
31
+ * <h1 className="bf-h1">Platform Manager</h1>
32
+ * </Backdrop>
33
+ * </div>
34
+ */
35
+ declare const TwinkleBackground: ({ color, fill, }: TwinkleBackgroundProps) => import("react").JSX.Element;
36
+ export default TwinkleBackground;
@@ -0,0 +1,389 @@
1
+ "use client";
2
+
3
+ import { c as _c } from "react-compiler-runtime";
4
+ import { useEffect, useRef, useSyncExternalStore } from "react";
5
+ import { jsx as _jsx } from "react/jsx-runtime";
6
+ // Not props, so every front page looks the same
7
+ const SPACING = 16;
8
+ // Opacity at the dimmest point of a twinkle
9
+ const MIN_ALPHA = 0.2;
10
+ // Hover glow reach in CSS pixels, ripple speed in CSS pixels per second and
11
+ // ripple life in seconds
12
+ const RADIUS = 240;
13
+ const RIPPLE_SPEED = 620;
14
+ const RIPPLE_LIFE = 1.8;
15
+ const CLICK_STRENGTH = 0.5;
16
+ // Resting opacity. Hover and ripples lift dots above it, up to fully opaque.
17
+ const OPACITY = {
18
+ light: 0.4,
19
+ dark: 0.8
20
+ };
21
+ // Dots are filled in batches of equal opacity, one path per batch instead of
22
+ // one per dot. The steps are too small to see.
23
+ const ALPHA_STEPS = 48;
24
+ // Reading the computed color forces a style recalculation, so not every frame
25
+ const STYLE_INTERVAL = 250;
26
+ // Caps the animation at about 60fps on high refresh rate screens
27
+ const FRAME_INTERVAL = 1000 / 60 - 2;
28
+ // Clicks on these don't start a ripple. tabindex="-1" is often set on a whole
29
+ // container like <main>, so it doesn't count.
30
+ const INTERACTIVE = 'a, button, input, select, textarea, label, summary, [tabindex]:not([tabindex="-1"]), [contenteditable],' + "[role=button], [role=link], [role=tab], [role=checkbox], [role=radio], [role=switch]," + "[role=menuitem], [role=option], [role=combobox], [role=slider]";
31
+ const TAU = Math.PI * 2;
32
+
33
+ // Reduced motion is the only way to stop the twinkle (WCAG 2.2.2), so it is
34
+ // followed live
35
+ const REDUCED_MOTION = "(prefers-reduced-motion: reduce)";
36
+ const subscribeReducedMotion = onChange => {
37
+ const query = window.matchMedia(REDUCED_MOTION);
38
+ query.addEventListener("change", onChange);
39
+ return () => query.removeEventListener("change", onChange);
40
+ };
41
+ const useReducedMotion = () => {
42
+ return useSyncExternalStore(subscribeReducedMotion, _temp, _temp2);
43
+ };
44
+ const within = (rect, x, y) => x >= 0 && x <= rect.width && y >= 0 && y <= rect.height;
45
+
46
+ /**
47
+ * Animated dot grid in the Bifrost theme color, for the front page of a
48
+ * Platform Manager module. It fades out down the page and reacts to hover and
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, or all of it with `fill`. No dot is cut off
52
+ * by the edge of the container. Experimental: AI-generated, not reviewed line
53
+ * by line.
54
+ *
55
+ * @see https://bifrost.intility.com/react/experimental/twinkle-background
56
+ *
57
+ * @example
58
+ * <div style={{ position: "relative", isolation: "isolate" }}>
59
+ * <TwinkleBackground />
60
+ * <Backdrop>
61
+ * <h1 className="bf-h1">Platform Manager</h1>
62
+ * </Backdrop>
63
+ * </div>
64
+ */
65
+ const TwinkleBackground = t0 => {
66
+ const $ = _c(6);
67
+ const {
68
+ color: t1,
69
+ fill: t2
70
+ } = t0;
71
+ const color = t1 === undefined ? "theme" : t1;
72
+ const fill = t2 === undefined ? false : t2;
73
+ const canvasRef = useRef(null);
74
+ const still = useReducedMotion();
75
+ const colorClass = color === "red" || color === "green" ? ` bfx-twinkle-background-${color}` : color === "theme" ? "" : ` bf-theme-${color}`;
76
+ let t3;
77
+ let t4;
78
+ if ($[0] !== fill || $[1] !== still) {
79
+ t3 = () => {
80
+ const canvas = canvasRef.current;
81
+ const ctx = canvas?.getContext("2d");
82
+ if (!canvas || !ctx) {
83
+ return;
84
+ }
85
+ const forcedColors = window.matchMedia("(forced-colors: active)");
86
+ const pointer = {
87
+ x: 0,
88
+ y: 0,
89
+ on: 0
90
+ };
91
+ const cursor = {
92
+ x: 0,
93
+ y: 0,
94
+ over: false
95
+ };
96
+ const clicks = [];
97
+ let w = 0;
98
+ let h = 0;
99
+ let cols = 1;
100
+ let offsetX = 0;
101
+ let offsetY = 0;
102
+ let span = 0;
103
+ let rand = new Float32Array(0);
104
+ const seed = Math.random() * 4294967296 | 0;
105
+ const noise = (col, row, k) => {
106
+ let n = Math.imul(col + 1, 2654435761) ^ Math.imul(row + 1, 2246822507) ^ Math.imul(k + 1, 3266489909) ^ seed;
107
+ n = Math.imul(n ^ n >>> 15, 739982445);
108
+ n = Math.imul(n ^ n >>> 12, 695872825);
109
+ return ((n ^ n >>> 15) >>> 0) / 4294967296;
110
+ };
111
+ let clock = 0;
112
+ let last = 0;
113
+ let raf = 0;
114
+ let visible = false;
115
+ let dotColor = "";
116
+ let base = OPACITY.light;
117
+ let styleAt = -Infinity;
118
+ const readStyle = () => {
119
+ const style = getComputedStyle(canvas);
120
+ dotColor = style.color;
121
+ base = style.colorScheme.includes("dark") ? OPACITY.dark : OPACITY.light;
122
+ };
123
+ const batches = Array.from({
124
+ length: ALPHA_STEPS
125
+ }, _temp3);
126
+ const draw = () => {
127
+ ctx.clearRect(0, 0, w, h);
128
+ if (forcedColors.matches || h <= 0) {
129
+ return;
130
+ }
131
+ ctx.fillStyle = dotColor;
132
+ for (let i = 0; i < rand.length; i = i + 2, i) {
133
+ const r1 = rand[i];
134
+ const r2 = rand[i + 1];
135
+ const x = offsetX + i / 2 % cols * SPACING;
136
+ const y = offsetY + Math.floor(i / 2 / cols) * SPACING;
137
+ const tw = 0.5 + 0.5 * Math.sin(clock * 0.48 * (1 + r1 * 3) + r2 * TAU);
138
+ let alpha = MIN_ALPHA + (1 - MIN_ALPHA) * tw * tw;
139
+ let size = 1.4 * (1 - 0.3 * r2) * (0.8 + 0.4 * tw);
140
+ if (pointer.on > 0.001) {
141
+ const d = Math.hypot(x - pointer.x, y - pointer.y);
142
+ const near = d < RADIUS ? 1 - d / RADIUS : 0;
143
+ alpha = alpha * (1 - 0.3 * pointer.on + near * near * 1.8 * pointer.on);
144
+ }
145
+ for (const c of clicks) {
146
+ const age = clock - c.t;
147
+ const life = 1 - age / RIPPLE_LIFE;
148
+ const d_0 = Math.hypot(x - c.x, y - c.y);
149
+ const ring = Math.exp(-((d_0 - age * RIPPLE_SPEED) ** 2) / 4232) * life * life * CLICK_STRENGTH;
150
+ alpha = alpha + ring * 2.6;
151
+ size = size + ring * 1.4;
152
+ }
153
+ const fade = fill || span <= 0 ? 0 : (y - offsetY) / span;
154
+ alpha = alpha * (1 - fade * fade) * base;
155
+ const step = Math.round(Math.min(1, alpha) * ALPHA_STEPS);
156
+ if (step < 1) {
157
+ continue;
158
+ }
159
+ batches[step - 1].push(x, y, size);
160
+ }
161
+ batches.forEach((dots, i_0) => {
162
+ if (!dots.length) {
163
+ return;
164
+ }
165
+ ctx.globalAlpha = (i_0 + 1) / ALPHA_STEPS;
166
+ ctx.beginPath();
167
+ for (let j = 0; j < dots.length; j = j + 3, j) {
168
+ ctx.moveTo(dots[j] + dots[j + 2], dots[j + 1]);
169
+ ctx.arc(dots[j], dots[j + 1], dots[j + 2], 0, TAU);
170
+ }
171
+ ctx.fill();
172
+ dots.length = 0;
173
+ });
174
+ };
175
+ const redraw = () => {
176
+ if (h <= 0) {
177
+ return;
178
+ }
179
+ readStyle();
180
+ draw();
181
+ };
182
+ const tick = now => {
183
+ if (forcedColors.matches) {
184
+ ctx.clearRect(0, 0, w, h);
185
+ return;
186
+ }
187
+ raf = requestAnimationFrame(tick);
188
+ if (last && now - last < FRAME_INTERVAL) {
189
+ return;
190
+ }
191
+ if (now - styleAt > STYLE_INTERVAL) {
192
+ readStyle();
193
+ styleAt = now;
194
+ }
195
+ clock = clock + (last ? Math.min(0.05, (now - last) / 1000) : 0);
196
+ clock;
197
+ last = now;
198
+ while (clicks.length && clock - clicks[0].t > RIPPLE_LIFE) {
199
+ clicks.shift();
200
+ }
201
+ let inside = false;
202
+ if (cursor.over) {
203
+ const rect = canvas.getBoundingClientRect();
204
+ const x_0 = cursor.x - rect.left;
205
+ const y_0 = cursor.y - rect.top;
206
+ inside = within(rect, x_0, y_0);
207
+ if (pointer.on < 0.01) {
208
+ pointer.x = x_0;
209
+ pointer.y = y_0;
210
+ }
211
+ pointer.x = pointer.x + (x_0 - pointer.x) * 0.18;
212
+ pointer.y = pointer.y + (y_0 - pointer.y) * 0.18;
213
+ }
214
+ pointer.on = pointer.on + ((inside ? 1 : 0) - pointer.on) * 0.08;
215
+ draw();
216
+ };
217
+ const play = () => {
218
+ cancelAnimationFrame(raf);
219
+ last = 0;
220
+ raf = requestAnimationFrame(tick);
221
+ };
222
+ const resize = () => {
223
+ const rect_0 = canvas.getBoundingClientRect();
224
+ const dpr = window.devicePixelRatio || 1;
225
+ w = rect_0.width;
226
+ h = rect_0.height;
227
+ canvas.width = Math.round(w * dpr);
228
+ canvas.height = Math.round(h * dpr);
229
+ ctx.setTransform(w ? canvas.width / w : 1, 0, 0, h ? canvas.height / h : 1, 0, 0);
230
+ cols = Math.max(1, Math.floor(w / SPACING));
231
+ const rows = Math.max(1, Math.floor(h / SPACING));
232
+ span = (rows - 1) * SPACING;
233
+ offsetX = (w - (cols - 1) * SPACING) / 2;
234
+ offsetY = (h - span) / 2;
235
+ rand = Float32Array.from({
236
+ length: cols * rows * 2
237
+ }, (_, i_1) => noise((i_1 >> 1) % cols, Math.floor(i_1 / 2 / cols), i_1 & 1));
238
+ redraw();
239
+ };
240
+ let unwatchDensity = _temp4;
241
+ const watchDensity = () => {
242
+ const query = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
243
+ const onChange = () => {
244
+ resize();
245
+ watchDensity();
246
+ };
247
+ query.addEventListener("change", onChange, {
248
+ once: true
249
+ });
250
+ unwatchDensity = () => query.removeEventListener("change", onChange);
251
+ };
252
+ const onMove = e => {
253
+ cursor.x = e.clientX;
254
+ cursor.y = e.clientY;
255
+ cursor.over = true;
256
+ };
257
+ const onLeave = () => {
258
+ cursor.over = false;
259
+ };
260
+ const onRelease = e_0 => {
261
+ if (e_0.pointerType !== "mouse") {
262
+ onLeave();
263
+ }
264
+ };
265
+ const onDown = e_1 => {
266
+ const el = e_1.target;
267
+ if (e_1.button !== 0 || !(el instanceof Element)) {
268
+ return;
269
+ }
270
+ if (!canvas.parentElement?.contains(el) || el.closest(INTERACTIVE)) {
271
+ return;
272
+ }
273
+ const rect_1 = canvas.getBoundingClientRect();
274
+ const x_1 = e_1.clientX - rect_1.left;
275
+ const y_1 = e_1.clientY - rect_1.top;
276
+ if (!within(rect_1, x_1, y_1)) {
277
+ return;
278
+ }
279
+ if (clicks.length >= 8) {
280
+ clicks.shift();
281
+ }
282
+ clicks.push({
283
+ x: x_1,
284
+ y: y_1,
285
+ t: clock
286
+ });
287
+ };
288
+ const resizeObserver = new ResizeObserver(resize);
289
+ resizeObserver.observe(canvas);
290
+ watchDensity();
291
+ const systemMode = window.matchMedia("(prefers-color-scheme: dark)");
292
+ const attributes = new MutationObserver(redraw);
293
+ const onForcedColors = () => {
294
+ if (still) {
295
+ redraw();
296
+ } else {
297
+ if (visible) {
298
+ play();
299
+ }
300
+ }
301
+ };
302
+ forcedColors.addEventListener("change", onForcedColors);
303
+ if (still) {
304
+ systemMode.addEventListener("change", redraw);
305
+ for (let el_0 = canvas; el_0; el_0 = el_0.parentElement) {
306
+ attributes.observe(el_0, {
307
+ attributes: true,
308
+ attributeFilter: ["class", "style", "data-bf-color-mode"]
309
+ });
310
+ }
311
+ }
312
+ const visibility = new IntersectionObserver(t5 => {
313
+ const [entry] = t5;
314
+ visible = entry.isIntersecting;
315
+ if (visible) {
316
+ play();
317
+ } else {
318
+ cancelAnimationFrame(raf);
319
+ }
320
+ });
321
+ if (!still) {
322
+ visibility.observe(canvas);
323
+ window.addEventListener("pointermove", onMove, {
324
+ passive: true
325
+ });
326
+ window.addEventListener("pointerdown", onDown, {
327
+ passive: true
328
+ });
329
+ window.addEventListener("pointerup", onRelease, {
330
+ passive: true
331
+ });
332
+ window.addEventListener("pointercancel", onLeave, {
333
+ passive: true
334
+ });
335
+ document.addEventListener("mouseleave", onLeave);
336
+ window.addEventListener("blur", onLeave);
337
+ }
338
+ return () => {
339
+ cancelAnimationFrame(raf);
340
+ resizeObserver.disconnect();
341
+ unwatchDensity();
342
+ systemMode.removeEventListener("change", redraw);
343
+ forcedColors.removeEventListener("change", onForcedColors);
344
+ attributes.disconnect();
345
+ visibility.disconnect();
346
+ window.removeEventListener("pointermove", onMove);
347
+ window.removeEventListener("pointerdown", onDown);
348
+ window.removeEventListener("pointerup", onRelease);
349
+ window.removeEventListener("pointercancel", onLeave);
350
+ document.removeEventListener("mouseleave", onLeave);
351
+ window.removeEventListener("blur", onLeave);
352
+ };
353
+ };
354
+ t4 = [still, fill];
355
+ $[0] = fill;
356
+ $[1] = still;
357
+ $[2] = t3;
358
+ $[3] = t4;
359
+ } else {
360
+ t3 = $[2];
361
+ t4 = $[3];
362
+ }
363
+ useEffect(t3, t4);
364
+ const t5 = `bfx-twinkle-background${fill ? " bfx-twinkle-background-fill" : ""}${colorClass}`;
365
+ let t6;
366
+ if ($[4] !== t5) {
367
+ t6 = /*#__PURE__*/_jsx("canvas", {
368
+ ref: canvasRef,
369
+ "aria-hidden": true,
370
+ className: t5
371
+ });
372
+ $[4] = t5;
373
+ $[5] = t6;
374
+ } else {
375
+ t6 = $[5];
376
+ }
377
+ return t6;
378
+ };
379
+ export default TwinkleBackground;
380
+ function _temp() {
381
+ return window.matchMedia(REDUCED_MOTION).matches;
382
+ }
383
+ function _temp2() {
384
+ return false;
385
+ }
386
+ function _temp3() {
387
+ return [];
388
+ }
389
+ function _temp4() {}
@@ -0,0 +1,2 @@
1
+ export type * from "./TwinkleBackground.js";
2
+ export { default } from "./TwinkleBackground.js";
@@ -0,0 +1 @@
1
+ export { default } from "./TwinkleBackground.js";
@@ -0,0 +1,3 @@
1
+ export { default as Backdrop } from "./components/Backdrop/index.js";
2
+ export { default as FrontpageHeader } from "./components/FrontpageHeader/index.js";
3
+ export { default as TwinkleBackground } from "./components/TwinkleBackground/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { default as Backdrop } from "./components/Backdrop/index.js";
2
+ export { default as FrontpageHeader } from "./components/FrontpageHeader/index.js";
3
+ export { default as TwinkleBackground } from "./components/TwinkleBackground/index.js";
package/package.json CHANGED
@@ -1,11 +1,61 @@
1
1
  {
2
2
  "name": "@intility/bifrost-experimental",
3
- "version": "0.0.0",
4
- "description": "",
5
- "license": "MIT",
6
- "author": "Intility AS",
3
+ "version": "0.2.0",
4
+ "description": "Experimental, AI-generated React components for Intility's design system, Bifrost.",
7
5
  "type": "module",
8
- "main": "index.js",
6
+ "main": "dist/index.js",
9
7
  "scripts": {
10
- }
8
+ "build:css": "postcss src/bifrost-experimental.css --dir dist --use postcss-import --no-map",
9
+ "build:module": "babel ./src -d dist --extensions .ts,.tsx --ignore '**/*.test.*,**/*.d.ts' --root-mode upward",
10
+ "build:types": "tsc --project tsconfig.build.json",
11
+ "watch:css": "npm run build:css -- --watch",
12
+ "watch:module": "npm run build:module -- --watch",
13
+ "watch:types": "npm run build:types -- --watch --preserveWatchOutput",
14
+ "clean": "rimraf dist",
15
+ "lint": "eslint \"./src/**/*.{ts,tsx}\" && stylelint \"src/**/*.css\"",
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "exports": {
19
+ "./package.json": "./package.json",
20
+ "./bifrost-experimental.css": "./dist/bifrost-experimental.css",
21
+ ".": "./dist/index.js",
22
+ "./*": "./dist/components/*/index.js"
23
+ },
24
+ "homepage": "https://bifrost.intility.com/react/experimental",
25
+ "keywords": [
26
+ "react",
27
+ "typescript",
28
+ "intility",
29
+ "design system",
30
+ "bifrost",
31
+ "experimental"
32
+ ],
33
+ "author": "Intility AS",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/intility/bifrost.git",
38
+ "directory": "packages/bifrost-experimental"
39
+ },
40
+ "publishConfig": {
41
+ "registry": "https://registry.npmjs.org/",
42
+ "access": "public"
43
+ },
44
+ "peerDependencies": {
45
+ "@intility/bifrost-react": "^6",
46
+ "react": "^17 || ^18 || ^19",
47
+ "react-dom": "^17 || ^18 || ^19"
48
+ },
49
+ "devDependencies": {
50
+ "@intility/bifrost-react": "^6.23.0"
51
+ },
52
+ "dependencies": {
53
+ "react-compiler-runtime": "^1.0.0"
54
+ },
55
+ "files": [
56
+ "CHANGELOG.md",
57
+ "README.md",
58
+ "dist/**/*",
59
+ "!dist/**/*.test.*"
60
+ ]
11
61
  }
package/index.js DELETED
@@ -1 +0,0 @@
1
- console.log("hey there");