@dbcdk/react-components 0.0.171 → 0.0.173

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.
@@ -39,14 +39,15 @@ function Grid({
39
39
  );
40
40
  }
41
41
  function GridItem({ children, base = 12, md, lg, className, style }) {
42
+ const toTrackSpan = (span) => span * 2;
42
43
  return /* @__PURE__ */ jsxRuntime.jsx(
43
44
  "div",
44
45
  {
45
46
  className: [styles__default.default.item, className].filter(Boolean).join(" "),
46
47
  style: {
47
- "--span-base": base,
48
- ...md != null ? { "--span-md": md } : {},
49
- ...lg != null ? { "--span-lg": lg } : {},
48
+ "--span-base": toTrackSpan(base),
49
+ ...md != null ? { "--span-md": toTrackSpan(md) } : {},
50
+ ...lg != null ? { "--span-lg": toTrackSpan(lg) } : {},
50
51
  ...style
51
52
  },
52
53
  children
@@ -17,11 +17,11 @@ export interface GridProps {
17
17
  export declare function Grid({ children, gap, equalHeight, maxWidth, className, style, }: GridProps): import("react/jsx-runtime").JSX.Element;
18
18
  export interface GridItemProps {
19
19
  children?: ReactNode;
20
- /** Default span: <768px */
20
+ /** Default span: <768px. Supports 0.5 increments, e.g. 4.5 */
21
21
  base?: number;
22
- /** Medium span: ≥768px */
22
+ /** Medium span: ≥768px. Supports 0.5 increments, e.g. 4.5 */
23
23
  md?: number;
24
- /** Large span: ≥1200px */
24
+ /** Large span: ≥1200px. Supports 0.5 increments, e.g. 4.5 */
25
25
  lg?: number;
26
26
  className?: string;
27
27
  style?: CSSProperties;
@@ -33,14 +33,15 @@ function Grid({
33
33
  );
34
34
  }
35
35
  function GridItem({ children, base = 12, md, lg, className, style }) {
36
+ const toTrackSpan = (span) => span * 2;
36
37
  return /* @__PURE__ */ jsx(
37
38
  "div",
38
39
  {
39
40
  className: [styles.item, className].filter(Boolean).join(" "),
40
41
  style: {
41
- "--span-base": base,
42
- ...md != null ? { "--span-md": md } : {},
43
- ...lg != null ? { "--span-lg": lg } : {},
42
+ "--span-base": toTrackSpan(base),
43
+ ...md != null ? { "--span-md": toTrackSpan(md) } : {},
44
+ ...lg != null ? { "--span-lg": toTrackSpan(lg) } : {},
44
45
  ...style
45
46
  },
46
47
  children
@@ -1,6 +1,6 @@
1
1
  .grid {
2
2
  display: grid;
3
- grid-template-columns: repeat(12, minmax(0, 1fr));
3
+ grid-template-columns: repeat(24, minmax(0, 1fr));
4
4
  align-items: start;
5
5
  max-width: var(--max-width-base);
6
6
  }
@@ -42,10 +42,6 @@
42
42
  min-width: 0; /* required for truncation inside flex */
43
43
  }
44
44
 
45
- h2.headline {
46
- min-block-size: var(--component-size-md);
47
- }
48
-
49
45
  /* CollapsibleHeadline wrapper */
50
46
  .collapsibleRoot {
51
47
  display: flex;
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
+ var react = require('react');
4
5
  var client = require('../../client');
5
6
  var styles = require('./MetricTile.module.css');
6
7
 
@@ -11,23 +12,91 @@ var styles__default = /*#__PURE__*/_interopDefault(styles);
11
12
  function toSize(v) {
12
13
  return typeof v === "number" ? `${v}px` : v;
13
14
  }
14
- function MetricTile({
15
+ function getSlotName(el) {
16
+ var _a;
17
+ const t = el.type;
18
+ return (_a = t.__METRIC_TILE_SLOT__) != null ? _a : null;
19
+ }
20
+ function splitSlots(children) {
21
+ const slots = {};
22
+ const rest = [];
23
+ react.Children.forEach(children, (child) => {
24
+ if (!react.isValidElement(child)) {
25
+ if (child != null) rest.push(child);
26
+ return;
27
+ }
28
+ const slot = getSlotName(child);
29
+ if (!slot) {
30
+ rest.push(child);
31
+ return;
32
+ }
33
+ slots[slot] = child;
34
+ });
35
+ return { slots, rest };
36
+ }
37
+ const MetricTileValue = ({
38
+ children
39
+ }) => {
40
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
41
+ };
42
+ MetricTileValue.__METRIC_TILE_SLOT__ = "Value";
43
+ MetricTileValue.displayName = "MetricTile.Value";
44
+ const MetricTileAdornment = ({ children }) => {
45
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
46
+ };
47
+ MetricTileAdornment.__METRIC_TILE_SLOT__ = "Adornment";
48
+ MetricTileAdornment.displayName = "MetricTile.Adornment";
49
+ const MetricTileBase = ({
15
50
  severity,
16
51
  label,
52
+ icon,
17
53
  value,
18
54
  tooltip,
19
55
  size = "md",
56
+ variant = "filled",
20
57
  minWidth,
21
- maxWidth
22
- }) {
58
+ maxWidth,
59
+ secondaryLabel,
60
+ secondaryValue,
61
+ children
62
+ }) => {
63
+ var _a, _b, _c;
23
64
  const style = minWidth != null || maxWidth != null ? {
24
65
  ...minWidth != null ? { minWidth: toSize(minWidth) } : {},
25
66
  ...maxWidth != null ? { maxWidth: toSize(maxWidth) } : {}
26
67
  } : void 0;
27
- return /* @__PURE__ */ jsxRuntime.jsx(client.Tooltip, { content: tooltip, placement: "bottom", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles__default.default.tile, "data-severity": severity, "data-size": size, style, children: [
28
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.label, children: label }),
29
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.value, children: value })
30
- ] }) });
31
- }
68
+ const { slots } = splitSlots(children);
69
+ const resolvedValue = (_b = (_a = slots.Value) == null ? void 0 : _a.props.children) != null ? _b : value;
70
+ const resolvedAdornment = (_c = slots.Adornment) == null ? void 0 : _c.props.children;
71
+ const showValueRow = resolvedValue != null || resolvedAdornment != null;
72
+ return /* @__PURE__ */ jsxRuntime.jsx(client.Tooltip, { content: tooltip, placement: "bottom", children: /* @__PURE__ */ jsxRuntime.jsxs(
73
+ "span",
74
+ {
75
+ className: styles__default.default.tile,
76
+ "data-severity": severity,
77
+ "data-size": size,
78
+ "data-variant": variant,
79
+ style,
80
+ children: [
81
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles__default.default.labelRow, children: [
82
+ icon != null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.icon, children: icon }) : null,
83
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.label, children: label })
84
+ ] }),
85
+ showValueRow ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles__default.default.valueRow, children: [
86
+ resolvedValue != null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.value, children: resolvedValue }) : null,
87
+ resolvedAdornment != null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.adornment, children: resolvedAdornment }) : null
88
+ ] }) : null,
89
+ secondaryLabel != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles__default.default.secondary, children: [
90
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.secondaryLabel, children: secondaryLabel }),
91
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.secondaryValue, children: secondaryValue })
92
+ ] })
93
+ ]
94
+ }
95
+ ) });
96
+ };
97
+ const MetricTile = Object.assign(MetricTileBase, {
98
+ Value: MetricTileValue,
99
+ Adornment: MetricTileAdornment
100
+ });
32
101
 
33
102
  exports.MetricTile = MetricTile;
@@ -1,15 +1,28 @@
1
- import type { ReactNode, JSX } from 'react';
1
+ import type { FC, JSX, PropsWithChildren, ReactNode } from 'react';
2
2
  import { Severity } from '../../constants/severity.types';
3
3
  type MetricTileSize = 'sm' | 'md' | 'lg';
4
+ type MetricTileVariant = 'filled' | 'outlined';
4
5
  type SizeValue = number | string;
5
- interface MetricTileProps {
6
+ interface MetricTileSlotProps {
7
+ children: ReactNode;
8
+ }
9
+ interface MetricTileProps extends PropsWithChildren {
6
10
  severity: Severity;
7
11
  label: string;
8
- value: string | number;
12
+ icon?: JSX.Element;
13
+ value?: string | number;
9
14
  tooltip?: ReactNode;
10
15
  size?: MetricTileSize;
16
+ variant?: MetricTileVariant;
11
17
  minWidth?: SizeValue;
12
18
  maxWidth?: SizeValue;
19
+ /** Smaller secondary label shown below the main value, e.g. a second time period. */
20
+ secondaryLabel?: string;
21
+ /** Smaller secondary value shown alongside secondaryLabel. Ignored if secondaryLabel is omitted. */
22
+ secondaryValue?: string | number;
13
23
  }
14
- export declare function MetricTile({ severity, label, value, tooltip, size, minWidth, maxWidth, }: MetricTileProps): JSX.Element;
24
+ export declare const MetricTile: FC<MetricTileProps> & {
25
+ Value: FC<MetricTileSlotProps>;
26
+ Adornment: FC<MetricTileSlotProps>;
27
+ };
15
28
  export {};
@@ -1,27 +1,96 @@
1
- import { jsx, jsxs } from 'react/jsx-runtime';
1
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
2
+ import { Children, isValidElement } from 'react';
2
3
  import { Tooltip } from '../../client';
3
4
  import styles from './MetricTile.module.css';
4
5
 
5
6
  function toSize(v) {
6
7
  return typeof v === "number" ? `${v}px` : v;
7
8
  }
8
- function MetricTile({
9
+ function getSlotName(el) {
10
+ var _a;
11
+ const t = el.type;
12
+ return (_a = t.__METRIC_TILE_SLOT__) != null ? _a : null;
13
+ }
14
+ function splitSlots(children) {
15
+ const slots = {};
16
+ const rest = [];
17
+ Children.forEach(children, (child) => {
18
+ if (!isValidElement(child)) {
19
+ if (child != null) rest.push(child);
20
+ return;
21
+ }
22
+ const slot = getSlotName(child);
23
+ if (!slot) {
24
+ rest.push(child);
25
+ return;
26
+ }
27
+ slots[slot] = child;
28
+ });
29
+ return { slots, rest };
30
+ }
31
+ const MetricTileValue = ({
32
+ children
33
+ }) => {
34
+ return /* @__PURE__ */ jsx(Fragment, { children });
35
+ };
36
+ MetricTileValue.__METRIC_TILE_SLOT__ = "Value";
37
+ MetricTileValue.displayName = "MetricTile.Value";
38
+ const MetricTileAdornment = ({ children }) => {
39
+ return /* @__PURE__ */ jsx(Fragment, { children });
40
+ };
41
+ MetricTileAdornment.__METRIC_TILE_SLOT__ = "Adornment";
42
+ MetricTileAdornment.displayName = "MetricTile.Adornment";
43
+ const MetricTileBase = ({
9
44
  severity,
10
45
  label,
46
+ icon,
11
47
  value,
12
48
  tooltip,
13
49
  size = "md",
50
+ variant = "filled",
14
51
  minWidth,
15
- maxWidth
16
- }) {
52
+ maxWidth,
53
+ secondaryLabel,
54
+ secondaryValue,
55
+ children
56
+ }) => {
57
+ var _a, _b, _c;
17
58
  const style = minWidth != null || maxWidth != null ? {
18
59
  ...minWidth != null ? { minWidth: toSize(minWidth) } : {},
19
60
  ...maxWidth != null ? { maxWidth: toSize(maxWidth) } : {}
20
61
  } : void 0;
21
- return /* @__PURE__ */ jsx(Tooltip, { content: tooltip, placement: "bottom", children: /* @__PURE__ */ jsxs("span", { className: styles.tile, "data-severity": severity, "data-size": size, style, children: [
22
- /* @__PURE__ */ jsx("span", { className: styles.label, children: label }),
23
- /* @__PURE__ */ jsx("span", { className: styles.value, children: value })
24
- ] }) });
25
- }
62
+ const { slots } = splitSlots(children);
63
+ const resolvedValue = (_b = (_a = slots.Value) == null ? void 0 : _a.props.children) != null ? _b : value;
64
+ const resolvedAdornment = (_c = slots.Adornment) == null ? void 0 : _c.props.children;
65
+ const showValueRow = resolvedValue != null || resolvedAdornment != null;
66
+ return /* @__PURE__ */ jsx(Tooltip, { content: tooltip, placement: "bottom", children: /* @__PURE__ */ jsxs(
67
+ "span",
68
+ {
69
+ className: styles.tile,
70
+ "data-severity": severity,
71
+ "data-size": size,
72
+ "data-variant": variant,
73
+ style,
74
+ children: [
75
+ /* @__PURE__ */ jsxs("span", { className: styles.labelRow, children: [
76
+ icon != null ? /* @__PURE__ */ jsx("span", { className: styles.icon, children: icon }) : null,
77
+ /* @__PURE__ */ jsx("span", { className: styles.label, children: label })
78
+ ] }),
79
+ showValueRow ? /* @__PURE__ */ jsxs("span", { className: styles.valueRow, children: [
80
+ resolvedValue != null ? /* @__PURE__ */ jsx("span", { className: styles.value, children: resolvedValue }) : null,
81
+ resolvedAdornment != null ? /* @__PURE__ */ jsx("span", { className: styles.adornment, children: resolvedAdornment }) : null
82
+ ] }) : null,
83
+ secondaryLabel != null && /* @__PURE__ */ jsxs("span", { className: styles.secondary, children: [
84
+ /* @__PURE__ */ jsx("span", { className: styles.secondaryLabel, children: secondaryLabel }),
85
+ /* @__PURE__ */ jsx("span", { className: styles.secondaryValue, children: secondaryValue })
86
+ ] })
87
+ ]
88
+ }
89
+ ) });
90
+ };
91
+ const MetricTile = Object.assign(MetricTileBase, {
92
+ Value: MetricTileValue,
93
+ Adornment: MetricTileAdornment
94
+ });
26
95
 
27
96
  export { MetricTile };
@@ -1,4 +1,7 @@
1
1
  .tile {
2
+ --metric-tile-bg: var(--color-bg-surface);
3
+ --metric-tile-fg: var(--color-fg-default);
4
+ --metric-tile-outline: var(--color-border-default);
2
5
  display: inline-flex;
3
6
  flex-direction: column;
4
7
  align-items: center;
@@ -9,6 +12,9 @@
9
12
  overflow: hidden;
10
13
  box-sizing: border-box;
11
14
  border-radius: var(--border-radius-md);
15
+ border: var(--border-width-thin) solid transparent;
16
+ background-color: var(--metric-tile-bg);
17
+ color: var(--metric-tile-fg);
12
18
  font-family: var(--font-family);
13
19
  cursor: default;
14
20
  }
@@ -20,6 +26,10 @@
20
26
  border-radius: var(--border-radius-sm);
21
27
  }
22
28
 
29
+ .tile[data-size='md'] {
30
+ gap: 8px;
31
+ }
32
+
23
33
  .tile[data-size='lg'] {
24
34
  gap: var(--spacing-2xs);
25
35
  padding: var(--spacing-md) var(--spacing-lg);
@@ -28,55 +38,86 @@
28
38
  }
29
39
 
30
40
  .tile[data-severity='success'] {
31
- background-color: var(--color-status-success-bg-soft);
32
- color: var(--color-status-success-fg-soft);
41
+ --metric-tile-bg: var(--color-status-success-bg-soft);
42
+ --metric-tile-fg: var(--color-status-success-fg-soft);
43
+ --metric-tile-outline: var(--color-status-success-fg-soft);
33
44
  }
34
45
 
35
46
  .tile[data-severity='error'] {
36
- background-color: var(--color-status-error-bg-soft);
37
- color: var(--color-status-error-fg-soft);
47
+ --metric-tile-bg: var(--color-status-error-bg-soft);
48
+ --metric-tile-fg: var(--color-status-error-fg-soft);
49
+ --metric-tile-outline: var(--color-status-error-fg-soft);
38
50
  }
39
51
 
40
52
  .tile[data-severity='warning'] {
41
- background-color: var(--color-status-warning-bg-soft);
42
- color: var(--color-status-warning-fg-soft);
53
+ --metric-tile-bg: var(--color-status-warning-bg-soft);
54
+ --metric-tile-fg: var(--color-status-warning-fg-soft);
55
+ --metric-tile-outline: var(--color-status-warning-fg-soft);
43
56
  }
44
57
 
45
58
  .tile[data-severity='info'] {
46
- background-color: var(--color-status-info-bg-soft);
47
- color: var(--color-status-info-fg-soft);
59
+ --metric-tile-bg: var(--color-status-info-bg-soft);
60
+ --metric-tile-fg: var(--color-status-info-fg-soft);
61
+ --metric-tile-outline: var(--color-status-info-fg-soft);
48
62
  }
49
63
 
50
64
  .tile[data-severity='neutral'] {
51
- background-color: var(--color-bg-toolbar);
52
- color: var(--color-fg-default);
65
+ --metric-tile-bg: var(--color-bg-toolbar);
66
+ --metric-tile-fg: var(--color-fg-default);
67
+ --metric-tile-outline: var(--color-border-default);
53
68
  }
54
69
 
55
70
  .tile[data-severity='brand'] {
56
- background-color: var(--color-brand);
57
- color: var(--color-fg-on-brand);
71
+ --metric-tile-bg: var(--color-brand);
72
+ --metric-tile-fg: var(--color-fg-on-brand);
73
+ --metric-tile-outline: var(--color-brand);
58
74
  }
59
75
 
60
76
  .tile[data-severity='accent'] {
61
- background-color: var(--color-accent-bg-soft);
62
- color: var(--color-accent-fg-soft);
77
+ --metric-tile-bg: var(--color-accent-bg-soft);
78
+ --metric-tile-fg: var(--color-accent-fg-soft);
79
+ --metric-tile-outline: var(--color-accent-fg-soft);
80
+ }
81
+
82
+ .tile[data-variant='outlined'] {
83
+ background-color: var(--color-bg-surface);
84
+ color: var(--color-fg-default);
85
+ border-color: var(--metric-tile-outline);
86
+ }
87
+
88
+ .labelRow {
89
+ display: inline-flex;
90
+ align-items: center;
91
+ gap: var(--spacing-xxs);
92
+ max-width: 100%;
93
+ min-width: 0;
63
94
  }
64
95
 
65
96
  .label {
66
97
  font-size: var(--font-size-xs);
67
- font-weight: var(--font-weight-semibold, 600);
68
- text-transform: uppercase;
69
- letter-spacing: var(--letter-spacing-wide);
98
+ font-weight: 400;
99
+ color: var(--color-fg-subtle);
70
100
  line-height: 1;
71
- opacity: 0.85;
72
101
  max-width: 100%;
73
102
  overflow: hidden;
74
103
  white-space: nowrap;
75
104
  text-overflow: ellipsis;
76
105
  }
77
106
 
107
+ .icon {
108
+ display: inline-flex;
109
+ align-items: center;
110
+ flex: 0 0 auto;
111
+ }
112
+
113
+ .icon :global(svg) {
114
+ width: 0.9em;
115
+ height: 0.9em;
116
+ }
117
+
78
118
  .value {
79
119
  font-size: var(--font-size-md);
120
+ font-weight: 500;
80
121
  line-height: 1;
81
122
  font-variant-numeric: tabular-nums;
82
123
  max-width: 100%;
@@ -85,15 +126,51 @@
85
126
  text-overflow: ellipsis;
86
127
  }
87
128
 
129
+ .valueRow {
130
+ display: inline-flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ gap: var(--spacing-xxs);
134
+ max-width: 100%;
135
+ min-width: 0;
136
+ }
137
+
138
+ .adornment {
139
+ display: inline-flex;
140
+ align-items: center;
141
+ gap: var(--spacing-xxs);
142
+ flex: 0 0 auto;
143
+ font-size: var(--font-size-xs);
144
+ line-height: 1;
145
+ opacity: 0.9;
146
+ }
147
+
148
+ .adornment :global(svg) {
149
+ width: 0.9em;
150
+ height: 0.9em;
151
+ }
152
+
88
153
  .tile[data-size='sm'] .label {
89
154
  font-size: var(--font-size-xs);
90
- letter-spacing: normal;
155
+ }
156
+
157
+ .tile[data-size='sm'] .labelRow {
158
+ gap: 2px;
91
159
  }
92
160
 
93
161
  .tile[data-size='sm'] .value {
94
162
  font-size: var(--font-size-xs);
95
163
  }
96
164
 
165
+ .tile[data-size='sm'] .valueRow {
166
+ gap: 2px;
167
+ }
168
+
169
+ .tile[data-size='sm'] .adornment {
170
+ gap: 2px;
171
+ font-size: 10px;
172
+ }
173
+
97
174
  .tile[data-size='lg'] .label {
98
175
  font-size: var(--font-size-sm);
99
176
  }
@@ -101,3 +178,29 @@
101
178
  .tile[data-size='lg'] .value {
102
179
  font-size: var(--font-size-xl);
103
180
  }
181
+
182
+ .secondary {
183
+ display: flex;
184
+ align-items: baseline;
185
+ gap: var(--spacing-2xs, 4px);
186
+ font-size: var(--font-size-xs);
187
+ opacity: 0.75;
188
+ max-width: 100%;
189
+ overflow: hidden;
190
+ white-space: nowrap;
191
+ }
192
+
193
+ .secondaryLabel {
194
+ text-transform: none;
195
+ letter-spacing: normal;
196
+ }
197
+
198
+ .secondaryValue {
199
+ font-variant-numeric: tabular-nums;
200
+ overflow: hidden;
201
+ text-overflow: ellipsis;
202
+ }
203
+
204
+ .tile[data-size='sm'] .secondary {
205
+ font-size: 10px;
206
+ }
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var theme_constants = require('../../utils/theme.constants');
5
+
6
+ function buildThemeInitScript(defaultTheme) {
7
+ return `(function(){try{
8
+ var KEY=${JSON.stringify(theme_constants.THEME_STORAGE_KEY)};
9
+ var VARIANTS=${JSON.stringify(theme_constants.THEME_VARIANTS)};
10
+ function isVariant(x){return !!x && VARIANTS.indexOf(x)!==-1}
11
+ function getCookie(name){var m=document.cookie.match(new RegExp('(?:^|; )'+name+'=([^;]*)'));return m?decodeURIComponent(m[1]):null}
12
+ var resolved=${JSON.stringify(defaultTheme)};
13
+ var fromCookie=getCookie(KEY);
14
+ if(isVariant(fromCookie)){resolved=fromCookie}else{try{var fromStorage=localStorage.getItem(KEY);if(isVariant(fromStorage))resolved=fromStorage}catch(e){}}
15
+ document.documentElement.dataset.theme=resolved;
16
+ }catch(e){}})();`;
17
+ }
18
+ function ThemeScript({ defaultTheme = "system" }) {
19
+ return /* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: { __html: buildThemeInitScript(defaultTheme) } });
20
+ }
21
+
22
+ exports.ThemeScript = ThemeScript;
@@ -0,0 +1,25 @@
1
+ import type { ThemeVariant } from '../../utils/theme.constants';
2
+ export interface ThemeScriptProps {
3
+ /** Theme applied when nothing is stored yet. Defaults to 'system'. */
4
+ defaultTheme?: ThemeVariant;
5
+ }
6
+ /**
7
+ * Renders a blocking inline <script> that reads the persisted theme (cookie,
8
+ * falling back to localStorage) and sets `data-theme` on <html> synchronously
9
+ * — before the page paints and before React hydrates.
10
+ *
11
+ * `useTheme()` alone cannot prevent a flash of the wrong theme on load: its
12
+ * resolution runs inside a `useEffect`, which by definition only fires after
13
+ * the first render has already been committed (and, server-side, there is no
14
+ * `document` to read a cookie from at all). Closing that gap requires code
15
+ * that runs outside React's render lifecycle — this component is that code.
16
+ *
17
+ * Place it as early as possible in <head>, e.g. in Next.js's `_document.js`:
18
+ *
19
+ * ```tsx
20
+ * <Head>
21
+ * <ThemeScript />
22
+ * </Head>
23
+ * ```
24
+ */
25
+ export declare function ThemeScript({ defaultTheme }: ThemeScriptProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { THEME_STORAGE_KEY, THEME_VARIANTS } from '../../utils/theme.constants';
3
+
4
+ function buildThemeInitScript(defaultTheme) {
5
+ return `(function(){try{
6
+ var KEY=${JSON.stringify(THEME_STORAGE_KEY)};
7
+ var VARIANTS=${JSON.stringify(THEME_VARIANTS)};
8
+ function isVariant(x){return !!x && VARIANTS.indexOf(x)!==-1}
9
+ function getCookie(name){var m=document.cookie.match(new RegExp('(?:^|; )'+name+'=([^;]*)'));return m?decodeURIComponent(m[1]):null}
10
+ var resolved=${JSON.stringify(defaultTheme)};
11
+ var fromCookie=getCookie(KEY);
12
+ if(isVariant(fromCookie)){resolved=fromCookie}else{try{var fromStorage=localStorage.getItem(KEY);if(isVariant(fromStorage))resolved=fromStorage}catch(e){}}
13
+ document.documentElement.dataset.theme=resolved;
14
+ }catch(e){}})();`;
15
+ }
16
+ function ThemeScript({ defaultTheme = "system" }) {
17
+ return /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: buildThemeInitScript(defaultTheme) } });
18
+ }
19
+
20
+ export { ThemeScript };
@@ -2,62 +2,88 @@
2
2
  'use strict';
3
3
 
4
4
  var react = require('react');
5
+ var theme_constants = require('../utils/theme.constants');
5
6
 
6
- const THEME_VARIANTS = ["light", "dark", "system"];
7
- const STORAGE_KEY = "dbc_theme";
8
- function isThemeVariant(x) {
9
- return !!x && THEME_VARIANTS.includes(x);
10
- }
11
7
  function getCookie(name) {
12
8
  const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
13
9
  return match ? decodeURIComponent(match[1]) : null;
14
10
  }
15
11
  function persistTheme(id) {
16
12
  try {
17
- localStorage.setItem(STORAGE_KEY, id);
13
+ localStorage.setItem(theme_constants.THEME_STORAGE_KEY, id);
18
14
  } catch {
19
15
  console.error("Failed to access localStorage");
20
16
  }
21
17
  try {
22
- document.cookie = `${STORAGE_KEY}=${encodeURIComponent(
18
+ document.cookie = `${theme_constants.THEME_STORAGE_KEY}=${encodeURIComponent(
23
19
  id
24
20
  )}; Path=/; Max-Age=${60 * 60 * 24 * 365}`;
25
21
  } catch {
26
22
  console.error("Failed to set theme cookie");
27
23
  }
28
24
  }
29
- function getTheme() {
25
+ function getDomTheme() {
30
26
  return document.documentElement.dataset.theme;
31
27
  }
32
28
  function applyTheme(id) {
33
29
  document.documentElement.dataset.theme = id;
34
30
  }
31
+ let currentTheme = null;
32
+ let resolved = false;
33
+ const listeners = /* @__PURE__ */ new Set();
34
+ function notify() {
35
+ listeners.forEach((listener) => listener());
36
+ }
37
+ function subscribe(listener) {
38
+ listeners.add(listener);
39
+ return () => {
40
+ listeners.delete(listener);
41
+ };
42
+ }
43
+ function getSnapshot() {
44
+ return currentTheme;
45
+ }
46
+ function getServerSnapshot() {
47
+ return null;
48
+ }
49
+ function resolveInitialTheme(initialTheme) {
50
+ const themeFromDataAttributes = getDomTheme();
51
+ let resolvedTheme = theme_constants.isThemeVariant(themeFromDataAttributes) ? themeFromDataAttributes : initialTheme;
52
+ const fromCookie = getCookie(theme_constants.THEME_STORAGE_KEY);
53
+ if (theme_constants.isThemeVariant(fromCookie)) {
54
+ resolvedTheme = fromCookie;
55
+ } else {
56
+ try {
57
+ const fromStorage = localStorage.getItem(theme_constants.THEME_STORAGE_KEY);
58
+ if (theme_constants.isThemeVariant(fromStorage)) resolvedTheme = fromStorage;
59
+ } catch {
60
+ console.error("Failed to access localStorage");
61
+ }
62
+ }
63
+ return resolvedTheme;
64
+ }
65
+ function ensureResolved(initialTheme) {
66
+ if (resolved) return;
67
+ resolved = true;
68
+ const value = resolveInitialTheme(initialTheme);
69
+ applyTheme(value);
70
+ persistTheme(value);
71
+ currentTheme = value;
72
+ notify();
73
+ }
74
+ function switchTheme(id) {
75
+ applyTheme(id);
76
+ persistTheme(id);
77
+ currentTheme = id;
78
+ resolved = true;
79
+ notify();
80
+ return id;
81
+ }
35
82
  function useTheme(initialTheme = "system") {
36
- const [theme, setTheme] = react.useState(null);
37
83
  react.useEffect(() => {
38
- const themeFromDataAttributes = getTheme();
39
- let resolved = isThemeVariant(themeFromDataAttributes) ? themeFromDataAttributes : initialTheme;
40
- const fromCookie = getCookie(STORAGE_KEY);
41
- if (isThemeVariant(fromCookie)) {
42
- resolved = fromCookie;
43
- } else {
44
- try {
45
- const fromStorage = localStorage.getItem(STORAGE_KEY);
46
- if (isThemeVariant(fromStorage)) resolved = fromStorage;
47
- } catch {
48
- console.error("Failed to access localStorage");
49
- }
50
- }
51
- applyTheme(resolved);
52
- setTheme(resolved);
53
- persistTheme(resolved);
84
+ ensureResolved(initialTheme);
54
85
  }, [initialTheme]);
55
- const switchTheme = react.useCallback((id) => {
56
- applyTheme(id);
57
- setTheme(id);
58
- persistTheme(id);
59
- return id;
60
- }, []);
86
+ const theme = react.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
61
87
  return { theme, switchTheme };
62
88
  }
63
89
 
@@ -1,7 +1,6 @@
1
- declare const THEME_VARIANTS: readonly ["light", "dark", "system"];
2
- export type ThemeVariant = (typeof THEME_VARIANTS)[number];
1
+ import type { ThemeVariant } from '../utils/theme.constants';
2
+ export type { ThemeVariant };
3
3
  export declare function useTheme(initialTheme?: ThemeVariant): {
4
4
  theme: ThemeVariant | null;
5
5
  switchTheme: (id: ThemeVariant) => ThemeVariant;
6
6
  };
7
- export {};
@@ -1,61 +1,87 @@
1
1
  'use client';
2
- import { useState, useEffect, useCallback } from 'react';
2
+ import { useEffect, useSyncExternalStore } from 'react';
3
+ import { isThemeVariant, THEME_STORAGE_KEY } from '../utils/theme.constants';
3
4
 
4
- const THEME_VARIANTS = ["light", "dark", "system"];
5
- const STORAGE_KEY = "dbc_theme";
6
- function isThemeVariant(x) {
7
- return !!x && THEME_VARIANTS.includes(x);
8
- }
9
5
  function getCookie(name) {
10
6
  const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
11
7
  return match ? decodeURIComponent(match[1]) : null;
12
8
  }
13
9
  function persistTheme(id) {
14
10
  try {
15
- localStorage.setItem(STORAGE_KEY, id);
11
+ localStorage.setItem(THEME_STORAGE_KEY, id);
16
12
  } catch {
17
13
  console.error("Failed to access localStorage");
18
14
  }
19
15
  try {
20
- document.cookie = `${STORAGE_KEY}=${encodeURIComponent(
16
+ document.cookie = `${THEME_STORAGE_KEY}=${encodeURIComponent(
21
17
  id
22
18
  )}; Path=/; Max-Age=${60 * 60 * 24 * 365}`;
23
19
  } catch {
24
20
  console.error("Failed to set theme cookie");
25
21
  }
26
22
  }
27
- function getTheme() {
23
+ function getDomTheme() {
28
24
  return document.documentElement.dataset.theme;
29
25
  }
30
26
  function applyTheme(id) {
31
27
  document.documentElement.dataset.theme = id;
32
28
  }
29
+ let currentTheme = null;
30
+ let resolved = false;
31
+ const listeners = /* @__PURE__ */ new Set();
32
+ function notify() {
33
+ listeners.forEach((listener) => listener());
34
+ }
35
+ function subscribe(listener) {
36
+ listeners.add(listener);
37
+ return () => {
38
+ listeners.delete(listener);
39
+ };
40
+ }
41
+ function getSnapshot() {
42
+ return currentTheme;
43
+ }
44
+ function getServerSnapshot() {
45
+ return null;
46
+ }
47
+ function resolveInitialTheme(initialTheme) {
48
+ const themeFromDataAttributes = getDomTheme();
49
+ let resolvedTheme = isThemeVariant(themeFromDataAttributes) ? themeFromDataAttributes : initialTheme;
50
+ const fromCookie = getCookie(THEME_STORAGE_KEY);
51
+ if (isThemeVariant(fromCookie)) {
52
+ resolvedTheme = fromCookie;
53
+ } else {
54
+ try {
55
+ const fromStorage = localStorage.getItem(THEME_STORAGE_KEY);
56
+ if (isThemeVariant(fromStorage)) resolvedTheme = fromStorage;
57
+ } catch {
58
+ console.error("Failed to access localStorage");
59
+ }
60
+ }
61
+ return resolvedTheme;
62
+ }
63
+ function ensureResolved(initialTheme) {
64
+ if (resolved) return;
65
+ resolved = true;
66
+ const value = resolveInitialTheme(initialTheme);
67
+ applyTheme(value);
68
+ persistTheme(value);
69
+ currentTheme = value;
70
+ notify();
71
+ }
72
+ function switchTheme(id) {
73
+ applyTheme(id);
74
+ persistTheme(id);
75
+ currentTheme = id;
76
+ resolved = true;
77
+ notify();
78
+ return id;
79
+ }
33
80
  function useTheme(initialTheme = "system") {
34
- const [theme, setTheme] = useState(null);
35
81
  useEffect(() => {
36
- const themeFromDataAttributes = getTheme();
37
- let resolved = isThemeVariant(themeFromDataAttributes) ? themeFromDataAttributes : initialTheme;
38
- const fromCookie = getCookie(STORAGE_KEY);
39
- if (isThemeVariant(fromCookie)) {
40
- resolved = fromCookie;
41
- } else {
42
- try {
43
- const fromStorage = localStorage.getItem(STORAGE_KEY);
44
- if (isThemeVariant(fromStorage)) resolved = fromStorage;
45
- } catch {
46
- console.error("Failed to access localStorage");
47
- }
48
- }
49
- applyTheme(resolved);
50
- setTheme(resolved);
51
- persistTheme(resolved);
82
+ ensureResolved(initialTheme);
52
83
  }, [initialTheme]);
53
- const switchTheme = useCallback((id) => {
54
- applyTheme(id);
55
- setTheme(id);
56
- persistTheme(id);
57
- return id;
58
- }, []);
84
+ const theme = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
59
85
  return { theme, switchTheme };
60
86
  }
61
87
 
package/dist/index.cjs CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ var ThemeScript = require('./components/theme-script/ThemeScript');
4
+ var theme_constants = require('./utils/theme.constants');
3
5
  var Icon = require('./components/icon/Icon');
4
6
  var UserDisplay = require('./components/user-display/UserDisplay');
5
7
  var Headline = require('./components/headline/Headline');
@@ -44,6 +46,18 @@ var CheckboxGroup = require('./components/forms/checkbox-group/CheckboxGroup');
44
46
 
45
47
 
46
48
 
49
+ Object.keys(ThemeScript).forEach(function (k) {
50
+ if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
51
+ enumerable: true,
52
+ get: function () { return ThemeScript[k]; }
53
+ });
54
+ });
55
+ Object.keys(theme_constants).forEach(function (k) {
56
+ if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
57
+ enumerable: true,
58
+ get: function () { return theme_constants[k]; }
59
+ });
60
+ });
47
61
  Object.keys(Icon).forEach(function (k) {
48
62
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
49
63
  enumerable: true,
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export * from './components/theme-script/ThemeScript';
2
+ export * from './utils/theme.constants';
1
3
  export * from './components/icon/Icon';
2
4
  export * from './components/user-display/UserDisplay';
3
5
  export * from './components/headline/Headline';
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ export * from './components/theme-script/ThemeScript';
2
+ export * from './utils/theme.constants';
1
3
  export * from './components/icon/Icon';
2
4
  export * from './components/user-display/UserDisplay';
3
5
  export * from './components/headline/Headline';
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ const THEME_VARIANTS = ["light", "dark", "system"];
4
+ const THEME_STORAGE_KEY = "dbc_theme";
5
+ function isThemeVariant(x) {
6
+ return !!x && THEME_VARIANTS.includes(x);
7
+ }
8
+
9
+ exports.THEME_STORAGE_KEY = THEME_STORAGE_KEY;
10
+ exports.THEME_VARIANTS = THEME_VARIANTS;
11
+ exports.isThemeVariant = isThemeVariant;
@@ -0,0 +1,4 @@
1
+ export declare const THEME_VARIANTS: readonly ["light", "dark", "system"];
2
+ export type ThemeVariant = (typeof THEME_VARIANTS)[number];
3
+ export declare const THEME_STORAGE_KEY = "dbc_theme";
4
+ export declare function isThemeVariant(x: string | null | undefined): x is ThemeVariant;
@@ -0,0 +1,7 @@
1
+ const THEME_VARIANTS = ["light", "dark", "system"];
2
+ const THEME_STORAGE_KEY = "dbc_theme";
3
+ function isThemeVariant(x) {
4
+ return !!x && THEME_VARIANTS.includes(x);
5
+ }
6
+
7
+ export { THEME_STORAGE_KEY, THEME_VARIANTS, isThemeVariant };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.171",
3
+ "version": "0.0.173",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",