@reykjavik/hanna-react 0.10.169 → 0.10.171

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/Alert.d.ts CHANGED
@@ -11,7 +11,7 @@ type AlertType = 'info' | 'success' | 'warning' | 'error' | 'critical';
11
11
  export declare const alertTypes: Record<string, 1>;
12
12
  export type AlertProps = {
13
13
  type: AlertType;
14
- /** Defaults to `true` if an `onClose` handler or a `closeUrl` is passaed */
14
+ /** Defaults to `true` if an `onClose` or `closeUrl` handlers are passaed */
15
15
  closable?: boolean;
16
16
  /** The alert content */
17
17
  children?: ReactNode;
@@ -25,7 +25,7 @@ export type AlertProps = {
25
25
  instantShow?: boolean;
26
26
  } & SSRSupportProps & EitherObj<{
27
27
  /**
28
- * Seconds until the Alert auto-closes.
28
+ * Number of **seconds** until the Alert auto-closes.
29
29
  * Mosueover and keyboard focus resets the timer. \
30
30
  * NOTE: An `onClosed` handler is required when using this option.
31
31
  */
package/Alert.js CHANGED
@@ -10,25 +10,42 @@ const env_js_1 = require("./utils/env.js");
10
10
  const utils_js_1 = require("./utils.js");
11
11
  // FIXME: Eventually import from @reykjavik/hanna-css
12
12
  const AlertCloseTransitionDuration = 400;
13
+ /** How much to extend the remaining time when the auto-closing is interrupted (frozen) by mouseover or keyboard focus */
14
+ const FREEZE_EXTENSION_MS = 750;
13
15
  const useAutoClosing = (autoClose, props) => {
14
- const [temp, setTemp] = (0, react_1.useState)(0);
16
+ const startTime = (0, react_1.useRef)(Date.now());
17
+ const remainingMs = (0, react_1.useRef)(autoClose);
18
+ const [frost, setFrost] = (0, react_1.useState)(0);
15
19
  if (!autoClose) {
16
- return { autoClosing: false };
20
+ return { autoCloseIn: undefined };
17
21
  }
18
22
  const thaw = (e) => {
19
- setTemp((temp) => temp + 1);
23
+ // When it's about to thaw, make set a new start time to base future
24
+ // "remaining time" calculations on.
25
+ if (frost === 1) {
26
+ startTime.current = Date.now();
27
+ }
28
+ setFrost((frost) => frost - 1);
20
29
  const handler = props[e.type.startsWith('blur') ? 'onBlur' : 'onMouseLeave'];
21
- // @ts-expect-error (Proper fix ends up as too much code for this extreme edge case)
22
30
  handler && handler(e);
23
31
  };
24
32
  const freeze = (e) => {
25
- setTemp((temp) => temp - 1);
33
+ // When there's no frost and it's about to freeze, make note
34
+ // of the remaining time. Furhter freezes should not update this.
35
+ if (!frost) {
36
+ console.log(remainingMs.current);
37
+ // Calculate the currently remaining time based on the last start time.
38
+ const newRemainingMs = remainingMs.current - (Date.now() - startTime.current);
39
+ // Extend the remainint time a bit to give usees some leeway,
40
+ // but never extend it beyond the original autoClose value.
41
+ remainingMs.current = Math.min(newRemainingMs + FREEZE_EXTENSION_MS, autoClose);
42
+ }
43
+ setFrost((frost) => frost + 1);
26
44
  const handler = props[e.type.startsWith('focus') ? 'onFocus' : 'onMouseEnter'];
27
- // @ts-expect-error (Resolving ends up as too much code for this extreme edge case)
28
45
  handler && handler(e);
29
46
  };
30
47
  return {
31
- autoClosing: temp === 0,
48
+ autoCloseIn: frost === 0 ? remainingMs.current : undefined,
32
49
  autoClosingProps: Object.assign({ onMouseEnter: freeze, onMouseLeave: thaw, onFocus: freeze, onBlur: thaw }, (env_js_1.isPreact && {
33
50
  onfocusin: (e) => e.currentTarget !== e.target && freeze(e),
34
51
  onfocusout: (e) => e.currentTarget !== e.target && thaw(e),
@@ -51,7 +68,7 @@ exports.alertTypes = {
51
68
  const Alert = (props) => {
52
69
  var _a;
53
70
  const { type, childrenHTML, children, onClose, closeUrl, ssr, onClosed, instantShow, wrapperProps, } = props;
54
- const autoClose = Math.max(props.autoClose || 0, 0);
71
+ const autoClose = Math.max(props.autoClose || 0, 0) * 1000;
55
72
  const closable = (_a = props.closable) !== null && _a !== void 0 ? _a : !!(onClose || (onClosed && !autoClose) || closeUrl != null);
56
73
  const closing = (0, react_1.useRef)();
57
74
  const isBrowser = (0, utils_js_1.useIsBrowserSide)(ssr);
@@ -76,13 +93,13 @@ const Alert = (props) => {
76
93
  }, AlertCloseTransitionDuration);
77
94
  }
78
95
  }, [onClose, onClosed]);
79
- const { autoClosing, autoClosingProps } = useAutoClosing(autoClose, props);
96
+ const { autoCloseIn, autoClosingProps } = useAutoClosing(autoClose, props);
80
97
  (0, react_1.useEffect)(() => {
81
- if (autoClosing) {
98
+ if (autoCloseIn) {
82
99
  let autoCloseTimeout;
83
100
  autoCloseTimeout = setTimeout(() => {
84
101
  closeAlert();
85
- }, autoClose * 1000);
102
+ }, autoCloseIn);
86
103
  return () => {
87
104
  if (autoCloseTimeout) {
88
105
  clearTimeout(autoCloseTimeout);
@@ -94,7 +111,7 @@ const Alert = (props) => {
94
111
  }
95
112
  };
96
113
  }
97
- }, [closeAlert, autoClosing, autoClose]);
114
+ }, [closeAlert, autoCloseIn]);
98
115
  return (react_1.default.createElement("div", Object.assign({}, wrapperProps, { className: (0, hanna_utils_1.modifiedClass)('Alert', [!!exports.alertTypes[type] && type, closable && 'closable'], (wrapperProps || {}).className), role: "alert", hidden: !open || undefined }, autoClosingProps),
99
116
  childrenHTML ? (react_1.default.createElement("div", { dangerouslySetInnerHTML: { __html: childrenHTML } })) : (children),
100
117
  ' ',
package/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@
4
4
 
5
5
  - ... <!-- Add new lines here. -->
6
6
 
7
+ ## 0.10.171
8
+
9
+ _2026-04-02_
10
+
11
+ - feat: Add component `StatusTag`
12
+ - feat: Add component `Timeline`
13
+ - `utils`:
14
+ - feat: Deprecate `domid` in favor of `dumbId` from `@reykjavik/hanna-utils`
15
+ - fix: Update className selector for `MobileMenuToggler` button in
16
+ `useHannaUIState()`
17
+
18
+ ## 0.10.170
19
+
20
+ _2026-03-10_
21
+
22
+ - `Alert`:
23
+ - fix: On freeze only extend auto-closing alerts' remaining time by a bit
24
+
7
25
  ## 0.10.169
8
26
 
9
27
  _2026-02-24_
package/Datepicker.d.ts CHANGED
@@ -12,7 +12,7 @@ export type DatepickerProps = {
12
12
  * the `<input/>` element is still `type="text"` and it's `.value` is
13
13
  * the human-readable (parsed) date `string`.
14
14
  *
15
- * Use this incombination with the `isoMode` prop to submit ISO-8601
15
+ * Use this in combination with the `isoMode` prop to submit ISO-8601
16
16
  * formatted input values
17
17
  */
18
18
  defaultValue?: Date;
@@ -10,7 +10,7 @@ const htmlClass = (className, add) => {
10
10
  const noop = () => undefined;
11
11
  const HamburgerMedias = { phone: 1, phablet: 1, tablet: 1 };
12
12
  const useMobileMenuToggling = (opts) => {
13
- const { doInitialize, togglerElm = '.MainMenuToggler' } = typeof opts === 'boolean'
13
+ const { doInitialize, togglerElm = '.MainMenuToggler, .MobileMenuToggler' } = typeof opts === 'boolean'
14
14
  ? ({ doInitialize: opts })
15
15
  : !opts
16
16
  ? ({ doInitialize: true })
package/StatusTag.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { ReactNode } from 'react';
2
+ import { WrapperElmProps } from './utils.js';
3
+ declare const colors: {
4
+ readonly none: undefined;
5
+ readonly blue: "color--blue";
6
+ readonly green: "color--green";
7
+ readonly yellow: "color--yellow";
8
+ readonly red: "color--red";
9
+ };
10
+ export type StatusTagColor = keyof typeof colors;
11
+ export type StatusTagProps = {
12
+ /** Label takes preference over `children` */
13
+ label?: ReactNode;
14
+ /** Default: 'none' (grey) */
15
+ color?: StatusTagColor;
16
+ /** Hides/removes the colored indicator light ball/bulp. */
17
+ light?: 'off';
18
+ /** Make the status tag larger. Defaults to false. */
19
+ large?: boolean;
20
+ children?: ReactNode;
21
+ } & WrapperElmProps<'span', 'children'>;
22
+ export declare const StatusTag: (props: StatusTagProps) => JSX.Element;
23
+ export {};
package/StatusTag.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StatusTag = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const react_1 = tslib_1.__importDefault(require("react"));
6
+ const hanna_utils_1 = require("@reykjavik/hanna-utils");
7
+ const colors = {
8
+ none: undefined,
9
+ blue: 'color--blue',
10
+ green: 'color--green',
11
+ yellow: 'color--yellow',
12
+ red: 'color--red',
13
+ };
14
+ const StatusTag = (props) => {
15
+ const { children, label, color, light, large, wrapperProps } = props;
16
+ return (react_1.default.createElement("span", Object.assign({}, wrapperProps, { className: (0, hanna_utils_1.modifiedClass)('StatusTag', [colors[color || 'none'], large && 'large', light === 'off' && 'light--off'], (wrapperProps || {}).className) }),
17
+ react_1.default.createElement("span", { className: "StatusTag__label" }, label || children)));
18
+ };
19
+ exports.StatusTag = StatusTag;
package/Timeline.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { TagPillColor } from '@reykjavik/hanna-react/TagPill';
2
+ import { HannaLang } from '@reykjavik/hanna-utils/i18n';
3
+ export type TimeLineItem = {
4
+ /** The main timeline item value */
5
+ title: string;
6
+ /** Flexible categorization field. Could for example contain a person/user's name */
7
+ category?: string;
8
+ /** More details about the */
9
+ description?: string;
10
+ /**
11
+ * The date/time of the timeline event.
12
+ *
13
+ * `Date` and `number` (timestamp) values are auto formatted according to the
14
+ * currently active `HannaLang`. If you need a non-default format pass it as a
15
+ * preformatted string.
16
+ */
17
+ date?: Date | number | string;
18
+ /** Displays a status tag next to the timeline item. */
19
+ status?: {
20
+ label: string;
21
+ color?: TagPillColor;
22
+ };
23
+ /**
24
+ * If no item is marked current, then the first item in the array is impilicitly the current one.
25
+ *
26
+ * If multiple items are marked current, then the first one is the current one.
27
+ */
28
+ current?: boolean;
29
+ } | 'loading';
30
+ export type TimelineProps = {
31
+ /** Optional title to show above the timeline. */
32
+ title?: string;
33
+ /** The items to display in the timeline. */
34
+ items: Array<TimeLineItem>;
35
+ /**
36
+ * If true, Item `Dates` will be formatted year, month day only, without the
37
+ * time of day (hours and minutes) visible.
38
+ *
39
+ * Default: `false`
40
+ */
41
+ hideTime?: boolean;
42
+ /** If true, the timeline will be sorted with the oldest item first. By default, the newest item is first. */
43
+ oldestFirst?: boolean;
44
+ /** Defaults to the current global `DEFAULT_LANG` */
45
+ lang?: HannaLang;
46
+ };
47
+ export declare const Timeline: (props: TimelineProps) => JSX.Element;
package/Timeline.js ADDED
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Timeline = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const react_1 = tslib_1.__importDefault(require("react"));
6
+ const Skeleton_1 = require("@reykjavik/hanna-react/Skeleton");
7
+ const TagPill_1 = require("@reykjavik/hanna-react/TagPill");
8
+ const hanna_utils_1 = require("@reykjavik/hanna-utils");
9
+ const i18n_1 = require("@reykjavik/hanna-utils/i18n");
10
+ const dateFormatters = {};
11
+ const dateTimeFormatters = {};
12
+ const getDateFormatter = (lang, hideTime) => hideTime
13
+ ? dateFormatters[lang] ||
14
+ (dateFormatters[lang] = new Intl.DateTimeFormat(lang, {
15
+ year: 'numeric',
16
+ month: 'long',
17
+ day: 'numeric',
18
+ }))
19
+ : dateTimeFormatters[lang] ||
20
+ (dateTimeFormatters[lang] = new Intl.DateTimeFormat(lang, {
21
+ year: 'numeric',
22
+ month: 'long',
23
+ day: 'numeric',
24
+ hour: '2-digit',
25
+ minute: '2-digit',
26
+ }));
27
+ const Timeline = (props) => {
28
+ const { title, items, oldestFirst, lang = i18n_1.DEFAULT_LANG, hideTime } = props;
29
+ const dateFmt = getDateFormatter(lang, hideTime);
30
+ const currentIdx = Math.max(items.findIndex((item) => item !== 'loading' && !!item.current), 0);
31
+ return (react_1.default.createElement("div", { className: (0, hanna_utils_1.modifiedClass)('Timeline', [oldestFirst && 'oldestFirst']) },
32
+ title && react_1.default.createElement("h3", { className: "Timeline__title" }, title),
33
+ react_1.default.createElement("ul", { className: "Timeline__items" }, items.map((item, i) => {
34
+ if (item === 'loading') {
35
+ return (react_1.default.createElement("li", { key: i, className: "Timeline__item Timeline__item--loading" },
36
+ react_1.default.createElement(Skeleton_1.Skeleton, { height: 1, wrapperProps: { className: 'Skeleton__circle' } }),
37
+ react_1.default.createElement(Skeleton_1.Skeleton, { items: 1, height: 3, text: true })));
38
+ }
39
+ const { title, category: author, description, date, status } = item;
40
+ return (react_1.default.createElement("li", { key: i, className: (0, hanna_utils_1.modifiedClass)('Timeline__item', [
41
+ i < currentIdx && 'future',
42
+ i > currentIdx && 'past',
43
+ ]), "aria-current": currentIdx === i ? 'step' : undefined },
44
+ react_1.default.createElement("strong", { className: "Timeline__item__title" }, title),
45
+ status && react_1.default.createElement(TagPill_1.TagPill, { color: status.color }, status.label),
46
+ author && react_1.default.createElement("div", { className: "Timeline__item__category" }, author),
47
+ description && (react_1.default.createElement("div", { className: "Timeline__item__description" }, description)),
48
+ date && (react_1.default.createElement("div", { className: "Timeline__item__date" }, typeof date === 'string' ? date : dateFmt.format(date)))));
49
+ }))));
50
+ };
51
+ exports.Timeline = Timeline;
package/esm/Alert.d.ts CHANGED
@@ -11,7 +11,7 @@ type AlertType = 'info' | 'success' | 'warning' | 'error' | 'critical';
11
11
  export declare const alertTypes: Record<string, 1>;
12
12
  export type AlertProps = {
13
13
  type: AlertType;
14
- /** Defaults to `true` if an `onClose` handler or a `closeUrl` is passaed */
14
+ /** Defaults to `true` if an `onClose` or `closeUrl` handlers are passaed */
15
15
  closable?: boolean;
16
16
  /** The alert content */
17
17
  children?: ReactNode;
@@ -25,7 +25,7 @@ export type AlertProps = {
25
25
  instantShow?: boolean;
26
26
  } & SSRSupportProps & EitherObj<{
27
27
  /**
28
- * Seconds until the Alert auto-closes.
28
+ * Number of **seconds** until the Alert auto-closes.
29
29
  * Mosueover and keyboard focus resets the timer. \
30
30
  * NOTE: An `onClosed` handler is required when using this option.
31
31
  */
package/esm/Alert.js CHANGED
@@ -6,25 +6,42 @@ import { isPreact } from './utils/env.js';
6
6
  import { useIsBrowserSide, } from './utils.js';
7
7
  // FIXME: Eventually import from @reykjavik/hanna-css
8
8
  const AlertCloseTransitionDuration = 400;
9
+ /** How much to extend the remaining time when the auto-closing is interrupted (frozen) by mouseover or keyboard focus */
10
+ const FREEZE_EXTENSION_MS = 750;
9
11
  const useAutoClosing = (autoClose, props) => {
10
- const [temp, setTemp] = useState(0);
12
+ const startTime = useRef(Date.now());
13
+ const remainingMs = useRef(autoClose);
14
+ const [frost, setFrost] = useState(0);
11
15
  if (!autoClose) {
12
- return { autoClosing: false };
16
+ return { autoCloseIn: undefined };
13
17
  }
14
18
  const thaw = (e) => {
15
- setTemp((temp) => temp + 1);
19
+ // When it's about to thaw, make set a new start time to base future
20
+ // "remaining time" calculations on.
21
+ if (frost === 1) {
22
+ startTime.current = Date.now();
23
+ }
24
+ setFrost((frost) => frost - 1);
16
25
  const handler = props[e.type.startsWith('blur') ? 'onBlur' : 'onMouseLeave'];
17
- // @ts-expect-error (Proper fix ends up as too much code for this extreme edge case)
18
26
  handler && handler(e);
19
27
  };
20
28
  const freeze = (e) => {
21
- setTemp((temp) => temp - 1);
29
+ // When there's no frost and it's about to freeze, make note
30
+ // of the remaining time. Furhter freezes should not update this.
31
+ if (!frost) {
32
+ console.log(remainingMs.current);
33
+ // Calculate the currently remaining time based on the last start time.
34
+ const newRemainingMs = remainingMs.current - (Date.now() - startTime.current);
35
+ // Extend the remainint time a bit to give usees some leeway,
36
+ // but never extend it beyond the original autoClose value.
37
+ remainingMs.current = Math.min(newRemainingMs + FREEZE_EXTENSION_MS, autoClose);
38
+ }
39
+ setFrost((frost) => frost + 1);
22
40
  const handler = props[e.type.startsWith('focus') ? 'onFocus' : 'onMouseEnter'];
23
- // @ts-expect-error (Resolving ends up as too much code for this extreme edge case)
24
41
  handler && handler(e);
25
42
  };
26
43
  return {
27
- autoClosing: temp === 0,
44
+ autoCloseIn: frost === 0 ? remainingMs.current : undefined,
28
45
  autoClosingProps: Object.assign({ onMouseEnter: freeze, onMouseLeave: thaw, onFocus: freeze, onBlur: thaw }, (isPreact && {
29
46
  onfocusin: (e) => e.currentTarget !== e.target && freeze(e),
30
47
  onfocusout: (e) => e.currentTarget !== e.target && thaw(e),
@@ -47,7 +64,7 @@ export const alertTypes = {
47
64
  export const Alert = (props) => {
48
65
  var _a;
49
66
  const { type, childrenHTML, children, onClose, closeUrl, ssr, onClosed, instantShow, wrapperProps, } = props;
50
- const autoClose = Math.max(props.autoClose || 0, 0);
67
+ const autoClose = Math.max(props.autoClose || 0, 0) * 1000;
51
68
  const closable = (_a = props.closable) !== null && _a !== void 0 ? _a : !!(onClose || (onClosed && !autoClose) || closeUrl != null);
52
69
  const closing = useRef();
53
70
  const isBrowser = useIsBrowserSide(ssr);
@@ -72,13 +89,13 @@ export const Alert = (props) => {
72
89
  }, AlertCloseTransitionDuration);
73
90
  }
74
91
  }, [onClose, onClosed]);
75
- const { autoClosing, autoClosingProps } = useAutoClosing(autoClose, props);
92
+ const { autoCloseIn, autoClosingProps } = useAutoClosing(autoClose, props);
76
93
  useEffect(() => {
77
- if (autoClosing) {
94
+ if (autoCloseIn) {
78
95
  let autoCloseTimeout;
79
96
  autoCloseTimeout = setTimeout(() => {
80
97
  closeAlert();
81
- }, autoClose * 1000);
98
+ }, autoCloseIn);
82
99
  return () => {
83
100
  if (autoCloseTimeout) {
84
101
  clearTimeout(autoCloseTimeout);
@@ -90,7 +107,7 @@ export const Alert = (props) => {
90
107
  }
91
108
  };
92
109
  }
93
- }, [closeAlert, autoClosing, autoClose]);
110
+ }, [closeAlert, autoCloseIn]);
94
111
  return (React.createElement("div", Object.assign({}, wrapperProps, { className: modifiedClass('Alert', [!!alertTypes[type] && type, closable && 'closable'], (wrapperProps || {}).className), role: "alert", hidden: !open || undefined }, autoClosingProps),
95
112
  childrenHTML ? (React.createElement("div", { dangerouslySetInnerHTML: { __html: childrenHTML } })) : (children),
96
113
  ' ',
@@ -12,7 +12,7 @@ export type DatepickerProps = {
12
12
  * the `<input/>` element is still `type="text"` and it's `.value` is
13
13
  * the human-readable (parsed) date `string`.
14
14
  *
15
- * Use this incombination with the `isoMode` prop to submit ISO-8601
15
+ * Use this in combination with the `isoMode` prop to submit ISO-8601
16
16
  * formatted input values
17
17
  */
18
18
  defaultValue?: Date;
@@ -7,7 +7,7 @@ const htmlClass = (className, add) => {
7
7
  const noop = () => undefined;
8
8
  const HamburgerMedias = { phone: 1, phablet: 1, tablet: 1 };
9
9
  export const useMobileMenuToggling = (opts) => {
10
- const { doInitialize, togglerElm = '.MainMenuToggler' } = typeof opts === 'boolean'
10
+ const { doInitialize, togglerElm = '.MainMenuToggler, .MobileMenuToggler' } = typeof opts === 'boolean'
11
11
  ? ({ doInitialize: opts })
12
12
  : !opts
13
13
  ? ({ doInitialize: true })
@@ -0,0 +1,23 @@
1
+ import { ReactNode } from 'react';
2
+ import { WrapperElmProps } from './utils.js';
3
+ declare const colors: {
4
+ readonly none: undefined;
5
+ readonly blue: "color--blue";
6
+ readonly green: "color--green";
7
+ readonly yellow: "color--yellow";
8
+ readonly red: "color--red";
9
+ };
10
+ export type StatusTagColor = keyof typeof colors;
11
+ export type StatusTagProps = {
12
+ /** Label takes preference over `children` */
13
+ label?: ReactNode;
14
+ /** Default: 'none' (grey) */
15
+ color?: StatusTagColor;
16
+ /** Hides/removes the colored indicator light ball/bulp. */
17
+ light?: 'off';
18
+ /** Make the status tag larger. Defaults to false. */
19
+ large?: boolean;
20
+ children?: ReactNode;
21
+ } & WrapperElmProps<'span', 'children'>;
22
+ export declare const StatusTag: (props: StatusTagProps) => JSX.Element;
23
+ export {};
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { modifiedClass } from '@reykjavik/hanna-utils';
3
+ const colors = {
4
+ none: undefined,
5
+ blue: 'color--blue',
6
+ green: 'color--green',
7
+ yellow: 'color--yellow',
8
+ red: 'color--red',
9
+ };
10
+ export const StatusTag = (props) => {
11
+ const { children, label, color, light, large, wrapperProps } = props;
12
+ return (React.createElement("span", Object.assign({}, wrapperProps, { className: modifiedClass('StatusTag', [colors[color || 'none'], large && 'large', light === 'off' && 'light--off'], (wrapperProps || {}).className) }),
13
+ React.createElement("span", { className: "StatusTag__label" }, label || children)));
14
+ };
@@ -0,0 +1,47 @@
1
+ import { TagPillColor } from '@reykjavik/hanna-react/TagPill';
2
+ import { HannaLang } from '@reykjavik/hanna-utils/i18n';
3
+ export type TimeLineItem = {
4
+ /** The main timeline item value */
5
+ title: string;
6
+ /** Flexible categorization field. Could for example contain a person/user's name */
7
+ category?: string;
8
+ /** More details about the */
9
+ description?: string;
10
+ /**
11
+ * The date/time of the timeline event.
12
+ *
13
+ * `Date` and `number` (timestamp) values are auto formatted according to the
14
+ * currently active `HannaLang`. If you need a non-default format pass it as a
15
+ * preformatted string.
16
+ */
17
+ date?: Date | number | string;
18
+ /** Displays a status tag next to the timeline item. */
19
+ status?: {
20
+ label: string;
21
+ color?: TagPillColor;
22
+ };
23
+ /**
24
+ * If no item is marked current, then the first item in the array is impilicitly the current one.
25
+ *
26
+ * If multiple items are marked current, then the first one is the current one.
27
+ */
28
+ current?: boolean;
29
+ } | 'loading';
30
+ export type TimelineProps = {
31
+ /** Optional title to show above the timeline. */
32
+ title?: string;
33
+ /** The items to display in the timeline. */
34
+ items: Array<TimeLineItem>;
35
+ /**
36
+ * If true, Item `Dates` will be formatted year, month day only, without the
37
+ * time of day (hours and minutes) visible.
38
+ *
39
+ * Default: `false`
40
+ */
41
+ hideTime?: boolean;
42
+ /** If true, the timeline will be sorted with the oldest item first. By default, the newest item is first. */
43
+ oldestFirst?: boolean;
44
+ /** Defaults to the current global `DEFAULT_LANG` */
45
+ lang?: HannaLang;
46
+ };
47
+ export declare const Timeline: (props: TimelineProps) => JSX.Element;
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { Skeleton } from '@reykjavik/hanna-react/Skeleton';
3
+ import { TagPill } from '@reykjavik/hanna-react/TagPill';
4
+ import { modifiedClass } from '@reykjavik/hanna-utils';
5
+ import { DEFAULT_LANG } from '@reykjavik/hanna-utils/i18n';
6
+ const dateFormatters = {};
7
+ const dateTimeFormatters = {};
8
+ const getDateFormatter = (lang, hideTime) => hideTime
9
+ ? dateFormatters[lang] ||
10
+ (dateFormatters[lang] = new Intl.DateTimeFormat(lang, {
11
+ year: 'numeric',
12
+ month: 'long',
13
+ day: 'numeric',
14
+ }))
15
+ : dateTimeFormatters[lang] ||
16
+ (dateTimeFormatters[lang] = new Intl.DateTimeFormat(lang, {
17
+ year: 'numeric',
18
+ month: 'long',
19
+ day: 'numeric',
20
+ hour: '2-digit',
21
+ minute: '2-digit',
22
+ }));
23
+ export const Timeline = (props) => {
24
+ const { title, items, oldestFirst, lang = DEFAULT_LANG, hideTime } = props;
25
+ const dateFmt = getDateFormatter(lang, hideTime);
26
+ const currentIdx = Math.max(items.findIndex((item) => item !== 'loading' && !!item.current), 0);
27
+ return (React.createElement("div", { className: modifiedClass('Timeline', [oldestFirst && 'oldestFirst']) },
28
+ title && React.createElement("h3", { className: "Timeline__title" }, title),
29
+ React.createElement("ul", { className: "Timeline__items" }, items.map((item, i) => {
30
+ if (item === 'loading') {
31
+ return (React.createElement("li", { key: i, className: "Timeline__item Timeline__item--loading" },
32
+ React.createElement(Skeleton, { height: 1, wrapperProps: { className: 'Skeleton__circle' } }),
33
+ React.createElement(Skeleton, { items: 1, height: 3, text: true })));
34
+ }
35
+ const { title, category: author, description, date, status } = item;
36
+ return (React.createElement("li", { key: i, className: modifiedClass('Timeline__item', [
37
+ i < currentIdx && 'future',
38
+ i > currentIdx && 'past',
39
+ ]), "aria-current": currentIdx === i ? 'step' : undefined },
40
+ React.createElement("strong", { className: "Timeline__item__title" }, title),
41
+ status && React.createElement(TagPill, { color: status.color }, status.label),
42
+ author && React.createElement("div", { className: "Timeline__item__category" }, author),
43
+ description && (React.createElement("div", { className: "Timeline__item__description" }, description)),
44
+ date && (React.createElement("div", { className: "Timeline__item__date" }, typeof date === 'string' ? date : dateFmt.format(date)))));
45
+ }))));
46
+ };
package/esm/index.d.ts CHANGED
@@ -8,12 +8,14 @@
8
8
  /// <reference path="./VerticalTabsTOC.d.tsx" />
9
9
  /// <reference path="./VSpacer.d.tsx" />
10
10
  /// <reference path="./Tooltip.d.tsx" />
11
+ /// <reference path="./Timeline.d.tsx" />
11
12
  /// <reference path="./TextInput.d.tsx" />
12
13
  /// <reference path="./TextButton.d.tsx" />
13
14
  /// <reference path="./TextBlock.d.tsx" />
14
15
  /// <reference path="./TagPill.d.tsx" />
15
16
  /// <reference path="./Tabs.d.tsx" />
16
17
  /// <reference path="./SubHeading.d.tsx" />
18
+ /// <reference path="./StatusTag.d.tsx" />
17
19
  /// <reference path="./Skeleton.d.tsx" />
18
20
  /// <reference path="./SiteSearchInput.d.tsx" />
19
21
  /// <reference path="./SiteSearchCurtain.d.tsx" />
@@ -1,7 +1,3 @@
1
- /**
2
- * Returns a short locally-unique ID string.
3
- */
4
- export declare const domid: () => string;
5
1
  /**
6
2
  * Returns a stable, unique ID string.
7
3
  *
@@ -14,3 +10,5 @@ export declare const domid: () => string;
14
10
  * mode and there's nothing we can do about it, except to ignore them.)
15
11
  */
16
12
  export declare const useDomid: (staticId?: string) => string;
13
+ /** @deprecated Use `import { dumbId } from '@reykjavik/hanna-utils'` instead (Will be removed in v0.3) */
14
+ export declare const domid: () => string;
@@ -1,12 +1,7 @@
1
1
  import React from 'react';
2
+ import { dumbId } from '@reykjavik/hanna-utils';
2
3
  // @ts-expect-error (transparently feature-detect useId hook, which is introduced in React@18)
3
4
  const useId = React.useId;
4
- const domid_prefix = `_${ /*@__PURE__*/`${Date.now()}-`.slice(6)}`;
5
- let domid_incr = 0;
6
- /**
7
- * Returns a short locally-unique ID string.
8
- */
9
- export const domid = () => domid_prefix + domid_incr++;
10
5
  /**
11
6
  * Returns a stable, unique ID string.
12
7
  *
@@ -26,7 +21,9 @@ export const useDomid = useId
26
21
  : (staticId) => {
27
22
  const idRef = React.useRef();
28
23
  if (!idRef.current) {
29
- idRef.current = staticId || domid();
24
+ idRef.current = staticId || dumbId();
30
25
  }
31
26
  return idRef.current;
32
27
  };
28
+ /** @deprecated Use `import { dumbId } from '@reykjavik/hanna-utils'` instead (Will be removed in v0.3) */
29
+ export const domid = dumbId;
package/index.d.ts CHANGED
@@ -8,12 +8,14 @@
8
8
  /// <reference path="./VerticalTabsTOC.d.tsx" />
9
9
  /// <reference path="./VSpacer.d.tsx" />
10
10
  /// <reference path="./Tooltip.d.tsx" />
11
+ /// <reference path="./Timeline.d.tsx" />
11
12
  /// <reference path="./TextInput.d.tsx" />
12
13
  /// <reference path="./TextButton.d.tsx" />
13
14
  /// <reference path="./TextBlock.d.tsx" />
14
15
  /// <reference path="./TagPill.d.tsx" />
15
16
  /// <reference path="./Tabs.d.tsx" />
16
17
  /// <reference path="./SubHeading.d.tsx" />
18
+ /// <reference path="./StatusTag.d.tsx" />
17
19
  /// <reference path="./Skeleton.d.tsx" />
18
20
  /// <reference path="./SiteSearchInput.d.tsx" />
19
21
  /// <reference path="./SiteSearchCurtain.d.tsx" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reykjavik/hanna-react",
3
- "version": "0.10.169",
3
+ "version": "0.10.171",
4
4
  "author": "Reykjavík (http://www.reykjavik.is)",
5
5
  "contributors": [
6
6
  "Hugsmiðjan ehf (http://www.hugsmidjan.is)",
@@ -17,8 +17,8 @@
17
17
  "@floating-ui/react": "^0.19.2",
18
18
  "@hugsmidjan/qj": "^4.22.1",
19
19
  "@hugsmidjan/react": "^0.4.32",
20
- "@reykjavik/hanna-css": "^0.4.26",
21
- "@reykjavik/hanna-utils": "^0.2.21",
20
+ "@reykjavik/hanna-css": "^0.4.27",
21
+ "@reykjavik/hanna-utils": "^0.2.22",
22
22
  "@types/react-autosuggest": "^10.1.0",
23
23
  "@types/react-datepicker": "^4.8.0",
24
24
  "@types/react-transition-group": "^4.4.0",
@@ -85,6 +85,10 @@
85
85
  "import": "./esm/Tooltip.js",
86
86
  "require": "./Tooltip.js"
87
87
  },
88
+ "./Timeline": {
89
+ "import": "./esm/Timeline.js",
90
+ "require": "./Timeline.js"
91
+ },
88
92
  "./TextInput": {
89
93
  "import": "./esm/TextInput.js",
90
94
  "require": "./TextInput.js"
@@ -109,6 +113,10 @@
109
113
  "import": "./esm/SubHeading.js",
110
114
  "require": "./SubHeading.js"
111
115
  },
116
+ "./StatusTag": {
117
+ "import": "./esm/StatusTag.js",
118
+ "require": "./StatusTag.js"
119
+ },
112
120
  "./Skeleton": {
113
121
  "import": "./esm/Skeleton.js",
114
122
  "require": "./Skeleton.js"
@@ -1,7 +1,3 @@
1
- /**
2
- * Returns a short locally-unique ID string.
3
- */
4
- export declare const domid: () => string;
5
1
  /**
6
2
  * Returns a stable, unique ID string.
7
3
  *
@@ -14,3 +10,5 @@ export declare const domid: () => string;
14
10
  * mode and there's nothing we can do about it, except to ignore them.)
15
11
  */
16
12
  export declare const useDomid: (staticId?: string) => string;
13
+ /** @deprecated Use `import { dumbId } from '@reykjavik/hanna-utils'` instead (Will be removed in v0.3) */
14
+ export declare const domid: () => string;
package/utils/useDomid.js CHANGED
@@ -1,17 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useDomid = exports.domid = void 0;
3
+ exports.domid = exports.useDomid = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const react_1 = tslib_1.__importDefault(require("react"));
6
+ const hanna_utils_1 = require("@reykjavik/hanna-utils");
6
7
  // @ts-expect-error (transparently feature-detect useId hook, which is introduced in React@18)
7
8
  const useId = react_1.default.useId;
8
- const domid_prefix = `_${ /*@__PURE__*/`${Date.now()}-`.slice(6)}`;
9
- let domid_incr = 0;
10
- /**
11
- * Returns a short locally-unique ID string.
12
- */
13
- const domid = () => domid_prefix + domid_incr++;
14
- exports.domid = domid;
15
9
  /**
16
10
  * Returns a stable, unique ID string.
17
11
  *
@@ -31,7 +25,9 @@ exports.useDomid = useId
31
25
  : (staticId) => {
32
26
  const idRef = react_1.default.useRef();
33
27
  if (!idRef.current) {
34
- idRef.current = staticId || (0, exports.domid)();
28
+ idRef.current = staticId || (0, hanna_utils_1.dumbId)();
35
29
  }
36
30
  return idRef.current;
37
31
  };
32
+ /** @deprecated Use `import { dumbId } from '@reykjavik/hanna-utils'` instead (Will be removed in v0.3) */
33
+ exports.domid = hanna_utils_1.dumbId;