@reykjavik/webtools 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@
4
4
 
5
5
  - ... <!-- Add new lines here. -->
6
6
 
7
+ ## 0.3.7
8
+
9
+ _2026-03-16_
10
+
11
+ - feat: Add `@reykjavik/webtools/alertsStore` for toasts and other global UI
12
+ feedback messages along with `@reykjavik/webtools/alertsStore/react` helpers
13
+
7
14
  ## 0.3.6
8
15
 
9
16
  _2026-02-24_
package/README.md CHANGED
@@ -49,6 +49,13 @@ bun add @reykjavik/webtools
49
49
  - [`Result.throw`](#resultthrow)
50
50
  - [Type `Result.PayloadOf`](#type-resultpayloadof)
51
51
  - [Type `Result.ErrorOf`](#type-resulterrorof)
52
+ - [`@reykjavik/webtools/alertsStore`](#reykjavikwebtoolsalertsstore)
53
+ - [`createAlerterStore`](#createalerterstore)
54
+ - [type `AlerterConfig`](#type-alerterconfig)
55
+ - [`@reykjavik/webtools/alertsStore/react`](#reykjavikwebtoolsalertsstorereact)
56
+ - [`makeReactSubscription`](#makereactsubscription)
57
+ - [`renderAlertMessage`](#renderalertmessage)
58
+ - [`renderAlertMessage.withLinkRenderer`](#renderalertmessagewithlinkrenderer)
52
59
  - [`@reykjavik/webtools/SiteImprove`](#reykjavikwebtoolssiteimprove)
53
60
  - [`SiteImprove` component](#siteimprove-component)
54
61
  - [`pingSiteImprove` helper](#pingsiteimprove-helper)
@@ -962,6 +969,274 @@ it's a subtype of `ResultTuple`.
962
969
 
963
970
  ---
964
971
 
972
+ ## `@reykjavik/webtools/alertsStore`
973
+
974
+ A small JS alerts store for toasts and other global UI feedback messages.
975
+
976
+ Persists alerts to `sessionStorage` to survive browser reloads, and provides a
977
+ simple pub/sub API for components to subscribe to alert changes.
978
+
979
+ ### `createAlerterStore`
980
+
981
+ **Syntax:**
982
+ `createAlerterStore(cfg?: AlerterConfig): { alerter: Record<Level, (payload: AlertPayload>) => void, subscribe: (callback: (alerts: Array<AlertInfo>, meta: { type: EventType, ids: Array<string> }) => void) => unsubscribe() => void; }`
983
+
984
+ Factory function that instantiates a new alerter store and returns a strongly
985
+ typed object with the following properties:
986
+
987
+ - `alerter`: A singleton object with methods for dispatching new alerts of
988
+ different levels. Pass a payload object to the method of the level you want
989
+ to dispatch, and the alert will be added to the store.
990
+ - `subscribe`: A function for subscribing to alert changes. It accepts a
991
+ callback that gets called with the current list of alerts and some metadata
992
+ whenever an alert is added or cleared.
993
+ The callback is called immediately upon subscription if there are already
994
+ active alerts.
995
+ It returns an unsubscribe function to stop receiving updates.
996
+
997
+ Simple useage with default settings:
998
+
999
+ ```ts
1000
+ // ---------------------------------------------------------------------------
1001
+ // alerterStore.ts
1002
+ // ---------------------------------------------------------------------------
1003
+
1004
+ import { createAlerterStore } from '@reykjavik/webtools/alertsStore';
1005
+ import type { InferSubscriberAlerts, InferAlerterPayload } from '@reykjavik/webtools/alertsStore';
1006
+
1007
+ const { alerter, subscribe } = createAlerterStore();
1008
+
1009
+ export { alerter, subscribe };
1010
+ export type AlertPayload = InferAlerterPayload(typeof alerter);
1011
+ export type AlertInfo = InferSubscriberAlerts<typeof subscribe>;
1012
+
1013
+ // ---------------------------------------------------------------------------
1014
+ // appRoot.ts
1015
+ // ---------------------------------------------------------------------------
1016
+
1017
+ import { subscribe } from '../alerterStore';
1018
+
1019
+ const unsubscribe = subscribe((alerts, meta) => {
1020
+ console.log('Current alerts:', alerts);
1021
+ console.log('Change type:', meta.type);
1022
+ console.log('Affected alert IDs:', meta.ids);
1023
+ });
1024
+
1025
+ // Stop receiving updates after 1 hour
1026
+ setTimeout(unsubscribe, 3_600_000);
1027
+
1028
+ // ---------------------------------------------------------------------------
1029
+ // someOtherModule.ts
1030
+ // ---------------------------------------------------------------------------
1031
+
1032
+ import { alerter } from '../alerterStore';
1033
+ alerter.success({
1034
+ message: 'All is good',
1035
+ // type: 'something',
1036
+ // flags: ['pristine'],
1037
+ duration: 'MEDIUM',
1038
+ delay: 500, // Optional delay
1039
+ });
1040
+ // after 500ms the above alert is added to the store, and all subscribers
1041
+ // are notified. The subscriber in `appRoot.ts` will log the following;
1042
+ /*
1043
+ Current alerts: [
1044
+ {
1045
+ id: '_234566-27_', // autugenerated
1046
+ level: 'success',
1047
+ message: 'All is good',
1048
+ duration: 5000, // ms
1049
+ dismiss: <Function>,
1050
+ setFalgs: <Function>,
1051
+ }
1052
+ ]
1053
+ Change type: 'add'
1054
+ Affected alert IDs: ['_234566-27_']
1055
+ */
1056
+ ```
1057
+
1058
+ Note how the `AlertPayload` and `AlertInfo` types are inferred from the
1059
+ generated `alerter` and the `subscribe` functions, respectively, using the
1060
+ provided `InferAlerterPayload` and `InferSubscriberAlerts` utility types.
1061
+
1062
+ #### type `AlerterConfig`
1063
+
1064
+ The `createAlerter` function accepts an optional configuration object that
1065
+ allows the customization of all of the accepted alert values and durations.
1066
+
1067
+ The configuration values affect the type signatures of the generated `alerter`
1068
+ and the `subscribe` functions. (See `InferAlerterPayload` and
1069
+ `InferSubscriberAlerts` below)
1070
+
1071
+ The configuration options are as follows:
1072
+
1073
+ - **`key?: string`**
1074
+ Identifier for the alerts store, used to create the key to persist alerts in
1075
+ `sessionStorage` (or other provided storage).
1076
+ Required if you want to have multiple independent alert stores in the same
1077
+ application.
1078
+ Default: `'app-alerts'`.
1079
+
1080
+ - **`levels?: Array<string>`**
1081
+ The accepted alert levels. The returned `alerter` object has a named
1082
+ dispatcher method for each level.
1083
+ Default: `['success', 'info', 'warning', 'error']`.
1084
+
1085
+ - **`types?: Array<string>`**
1086
+ The allowed alert "types", which can be used to, for example, dispatch both
1087
+ "toasts" vs. "static alert banners" via the same store.
1088
+ This can also be used for more basic styling or categorization purposes.
1089
+ Default: no restrictions, any string value is allowed.
1090
+
1091
+ - **`flags?: Array<string>`**
1092
+ The allowed alert "flags", which can be changed during the lifetime of an
1093
+ alert using the `setFlags` function on the `AlertInfo` object.
1094
+ This can be used for styling or any other purpose you like.
1095
+ Default: no restriction, any string value is allowed.
1096
+
1097
+ - **`durations?: Record<string, number>`**
1098
+ The allowed alert "duration" names and their lengths in milliseconds.
1099
+ Default:
1100
+ `{ BLINK: 2_000, SHORT: 4_000, MEDIUM: 8_000, LONG: 16_000, XLONG: 32_000, INDEFINITE: 0 }`.
1101
+
1102
+ - **`defaultDuration?: string`**
1103
+ The duration to use for alerts if no duration is specified when
1104
+ dispatching.
1105
+ Default: `SHORT` if using the default durations, otherwise the default is
1106
+ `0` (indefinite)
1107
+
1108
+ - **`storage?: Pick<Storage, 'getItem' | 'setItem'>`**
1109
+ The storage object to use instead of `sessionStorage` (the default) for
1110
+ persisting alerts across page reloads, etc.
1111
+
1112
+ ### `@reykjavik/webtools/alertsStore/react`
1113
+
1114
+ #### `makeReactSubscription`
1115
+
1116
+ **Syntax:**
1117
+ `makeReactSubscription(): { useAlerter: () => Array<AlertInfo>, AlertsContainer: (props: { children: (alerts: Array<alertInfo>) => ReactNode }) => ReactNode }`
1118
+
1119
+ Factory function that creates a React subscription hook and a container
1120
+ component linked to a specific alerter store subscibe function.
1121
+
1122
+ The returned `useAlerter` hook can be used in any React component to get the
1123
+ current list of alerts from the store
1124
+
1125
+ Meanwhile the `AlertsContainer` is a sugar component that calls `useAlerter()`
1126
+ internally and provides the current alerts list to its child as a render prop.
1127
+
1128
+ The returned list and its items and their properties are all immutable/stable
1129
+ so you can safely use them as dependencies in React hooks, etc.
1130
+
1131
+ ```ts
1132
+ // ---------------------------------------------------------------------------
1133
+ // alerterStore.ts
1134
+ // ---------------------------------------------------------------------------
1135
+
1136
+ import { createAlerterStore } from '@reykjavik/webtools/alertsStore';
1137
+ import { makeReactSubscription } from '@reykjavik/webtools/alertsStore/react';
1138
+
1139
+ const { alerter, subscribe } = createAlerterStore();
1140
+
1141
+ export { alerter };
1142
+ export const { useAlerter, AlertsContainer } =
1143
+ makeReactSubscription(subscribe);
1144
+
1145
+ // ---------------------------------------------------------------------------
1146
+ // app.tsx
1147
+ // ---------------------------------------------------------------------------
1148
+
1149
+ import { AlertsContainer } from '../alerterStore';
1150
+ import { Toast } from '../components/Toast';
1151
+
1152
+ // In your App JSX
1153
+ <AlertsContainer>
1154
+ {(alerts) => (
1155
+ <div class="toastcontainer">
1156
+ {alerts.map((alert) => (
1157
+ <Toast key={alert.id} {...alert} />
1158
+ ))}
1159
+ </div>
1160
+ )}
1161
+ </AlertsContainer>;
1162
+ ```
1163
+
1164
+ #### `renderAlertMessage`
1165
+
1166
+ **Syntax:**
1167
+ `renderAlertMessage(message: AlertInfo['message'], linkComponent?: renderAlertMessage.LinkRenderer): ReactNode`
1168
+
1169
+ Helper to render an alerter alert message, which can be a simple string or a
1170
+ more complex array of strings and objects representing links and rich (bold)
1171
+ text formatting.
1172
+
1173
+ It renders link objects as simple `<a href="" />` elements, by default, but
1174
+ you can optionally provide a custom `linkComponent` as a second parameter.
1175
+
1176
+ Third
1177
+
1178
+ ```ts
1179
+ import { renderAlertMessage } from '@reykjavik/webtools/alertsStore/react';
1180
+ import Link from 'next/link';
1181
+
1182
+ import { AlertInfo } from '../alertsStore';
1183
+
1184
+ export const Toast = (props: AlertInfo) => {
1185
+ const dismissOnLinkClick = (e: React.MouseEvent) => {
1186
+ if ((e.target as HTMLElement).closest('a')) {
1187
+ props.dismiss(); // Dismiss the alert when a link is clicked
1188
+ }
1189
+ };
1190
+ return (
1191
+ <div class="toast" onClick={dismissOnLinkClick}>
1192
+ {renderAlertMessage(props.message, Link)}
1193
+ </div>
1194
+ );
1195
+ };
1196
+ ```
1197
+
1198
+ To build your own custom `LinkComponent`, you can use the
1199
+ `renderAlertMessage.LinkRenderer` type for the function signature.
1200
+
1201
+ ```ts
1202
+ import { renderAlertMessage } from '@reykjavik/webtools/alertsStore/react';
1203
+ import { Link } from 'react-router';
1204
+
1205
+ const MyWrappedLink: renderAlertMessage.LinkRenderer = (props) => {
1206
+ const { href, ...linkProps } = props;
1207
+ return <Link to={href} {...linkProps} />;
1208
+ };
1209
+
1210
+ // Then elsewhere in your Alert/Toast component
1211
+ <div class="toast__message">
1212
+ {renderAlertMessage(props.message, MyWrappedLink)};
1213
+ </div>;
1214
+ ```
1215
+
1216
+ Alternatively, if you want to avoid passing the `LinkComponent` every time you
1217
+ call `renderAlertMessage`, you can use the
1218
+ `renderAlertMessage.withLinkRenderer` helper
1219
+
1220
+ #### `renderAlertMessage.withLinkRenderer`
1221
+
1222
+ **Syntax:**
1223
+ `renderAlertMessage.withLinkRenderer(LinkComponent: renderAlertMessage.LinkRenderer): (message: AlertInfo['message']):ReactNode`
1224
+
1225
+ It returns a curried version of [`renderAlertMessage`](#renderAlertMessage)
1226
+ that uses the passed `LinkComponent` for rendering links in alert messages.
1227
+
1228
+ ```ts
1229
+ const curriedRenderAlertMessage =
1230
+ renderAlertMessage.withLinkRenderer(MyWrappedLink);
1231
+
1232
+ // Then elsewhere in your Alert/Toast component
1233
+ <div class="toast__message">
1234
+ {renderAlertMessage(props.message, MyWrappedLink)};
1235
+ </div>;
1236
+ ```
1237
+
1238
+ ---
1239
+
965
1240
  ## `@reykjavik/webtools/SiteImprove`
966
1241
 
967
1242
  Contains React helpers for loading SiteImprove's analytics scripts, and
@@ -0,0 +1,201 @@
1
+ import * as v from 'valibot';
2
+ declare const messageSchema: v.UnionSchema<[v.StringSchema<undefined>, v.ArraySchema<v.UnionSchema<[v.StringSchema<undefined>, v.ObjectSchema<{
3
+ readonly tag: v.LiteralSchema<"a", undefined>;
4
+ readonly text: v.StringSchema<undefined>;
5
+ readonly href: v.StringSchema<undefined>;
6
+ readonly target: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
7
+ readonly hrefLang: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
8
+ readonly lang: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
9
+ }, undefined>, v.ObjectSchema<{
10
+ readonly tag: v.LiteralSchema<"strong", undefined>;
11
+ readonly text: v.StringSchema<undefined>;
12
+ }, undefined>], undefined>, undefined>], undefined>;
13
+ export type AlertMessage = v.InferOutput<typeof messageSchema>;
14
+ type _AlertNotification<Level, Type, Flag> = {
15
+ level: Level;
16
+ message: AlertMessage;
17
+ type?: Type;
18
+ flags?: Array<Flag>;
19
+ duration?: number;
20
+ id: string;
21
+ };
22
+ declare const defaultAlertLevels: readonly ["info", "warning", "success", "error"];
23
+ declare const defaultDurations: {
24
+ BLINK: number;
25
+ SHORT: number;
26
+ MEDIUM: number;
27
+ LONG: number;
28
+ XLONG: number;
29
+ INDEFINITE: number;
30
+ };
31
+ /**
32
+ * A configuration object for the `createAlerter` factory function, that allows
33
+ * the customization of all of the accepted alert values and durations.
34
+ *
35
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#type-alerterconfig
36
+ */
37
+ export type AlerterConfig<Level extends string = (typeof defaultAlertLevels)[number], Type extends string = string, Flag extends string = string, Duration extends string = keyof typeof defaultDurations, Durations extends Record<Duration, number> = Record<Duration, number>> = {
38
+ /**
39
+ * Identifier for the alerts store, used to create the key to persist alerts
40
+ * in `sessionStorage` (or other provided storage).
41
+ *
42
+ * Only required if you need to create multiple independent alert stores in
43
+ * the same app.
44
+ *
45
+ * Default: `"app~alerts"`
46
+ */
47
+ key?: string;
48
+ /**
49
+ * The allowed alert "levels". The returned `alerter` object will have a
50
+ * named dispatcher method for each level.
51
+ *
52
+ * Default: `['info', 'warning', 'success', 'error']`
53
+ */
54
+ levels?: Array<Level>;
55
+ /**
56
+ * The allowed alert "types", which can be used to, for example, to dispatch
57
+ * both "toasts" vs. "static alert banners" via the same store.
58
+ *
59
+ * This can also be used for more basic styling or categorization purposes.
60
+ *
61
+ * Default: no restrictions, any string value is allowed.
62
+ */
63
+ types?: Array<Type>;
64
+ /**
65
+ * The allowed alert "flags", which can be changed during the lifetime of
66
+ * an alert using the `setFlags` function on the `AlertInfo` object.
67
+ *
68
+ * This can be used for styling or any other purpose you like.
69
+ *
70
+ * Default: no restriction, any string value is allowed.
71
+ */
72
+ flags?: Array<Flag>;
73
+ /**
74
+ * Optionally controls the allowed alert "duration" names and their lengths
75
+ * in milliseconds.
76
+ *
77
+ * Default:
78
+ * ```ts
79
+ * {
80
+ BLINK: 2_000,
81
+ SHORT: 4_000,
82
+ MEDIUM: 8_000,
83
+ LONG: 16_000,
84
+ XLONG: 32_000,
85
+ INDEFINITE: 0,
86
+ }
87
+ * ```
88
+ */
89
+ durations?: Durations;
90
+ /**
91
+ * Default duration to use for alerts if no duration is specified when
92
+ * dispatching.
93
+ *
94
+ * Default: `SHORT` if using the default durations, otherwise the default
95
+ * is `0` (indefinite)
96
+ */
97
+ defaultDuration?: Durations extends Record<infer D, number> ? D : never;
98
+ /**
99
+ * Optional custom storage object to use instead of `sessionStorage` (the
100
+ * default) for persisting alerts across page reloads, etc.
101
+ */
102
+ storage?: {
103
+ getItem: (key: string) => string | undefined | null;
104
+ setItem: (key: string, value: string) => void;
105
+ };
106
+ };
107
+ /**
108
+ * Factory function that creates an alerter store singleton with optional
109
+ * configuration for the genarated alerts.
110
+ *
111
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
112
+ */
113
+ export declare const createAlerterStore: <Level extends string = (typeof defaultAlertLevels)[number], Type extends string = string, Flag extends string = string, Duration extends string = keyof typeof defaultDurations>(cfg?: AlerterConfig<Level, Type, Flag, Duration>) => {
114
+ /**
115
+ * Singleton object with methods for showing alerts of different levels.
116
+ * Pass a payload object to the method of the level you want to dispatch,
117
+ * and the alert will be added to the store.
118
+ *
119
+ * Use `subscribeToAlerts` elsewhere in the app to subscribe to alert
120
+ * notifications and display them.
121
+ *
122
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
123
+ */
124
+ alerter: Record<Level, (payload: {
125
+ /**
126
+ * A simple string containing the alert message.
127
+ *
128
+ * For sightly more complex alert messages pass an array of strings
129
+ * (representing text nodes), objects with `tag: 'a'` and hyperlink-related
130
+ * props, or `tag: 'strong'` objects for minimal rich formatting.
131
+ *
132
+ * (Assume that such array itmes will be rendered with whitespace between
133
+ * them.)
134
+ */
135
+ message: AlertMessage;
136
+ /**
137
+ * Allows distinguishing between different "types" of alerts, for example,
138
+ * to dispatch both "toasts" vs. "static alert banners" via the same store.
139
+ *
140
+ * May also be used for more basic styling or categorization purposes.
141
+ */
142
+ type?: Type;
143
+ /**
144
+ * Flag values can be changed during the lifetime of
145
+ * an alert using the `setFlags` function on each `AlertInfo` object.
146
+ *
147
+ * Flags may be used for styling or any other purpose you like.
148
+ */
149
+ flags?: Array<Flag>;
150
+ /**
151
+ * Hint for how long the notification should remain displayed before
152
+ * auto-dismissing.
153
+ *
154
+ * **NOTE:** The alerter store does not implement auto-dismissing. However,
155
+ * this value can be used by UI component that actually render the alert,
156
+ * by calling each `AlertInfo`'s `dismiss` method.
157
+ */
158
+ duration?: Duration;
159
+ delay?: number;
160
+ }) => void>;
161
+ /**
162
+ * Subscribes to alert events. The provided callback will be called whenever a
163
+ * alert is added or cleared.
164
+ *
165
+ * The callback is called immediately upon subscription if there are already
166
+ * active alerts.
167
+ *
168
+ * Returns an unsubscribe function that can be called to stop receiving alert
169
+ * events.
170
+ *
171
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
172
+ */
173
+ subscribe: (callback: (notifications: Array<_AlertNotification<Level, Type, Flag> & {
174
+ /** Dispatcher function that dismisses/hides/removes the callback */
175
+ dismiss: () => void;
176
+ /**
177
+ * Dispatcher function that can be used to set a simple "flag" on the alert,
178
+ * which can be used for styling or other purposes.
179
+ */
180
+ setFlags: (value: Flag | Array<Flag> | ((flags: Array<Flag> | undefined) => Array<Flag> | undefined)) => void;
181
+ }>, meta: {
182
+ type: "clear" | "add" | "change";
183
+ /** IDs of the alerts that were added or cleared in this event. */
184
+ ids: Array<string>;
185
+ }) => void) => () => void;
186
+ };
187
+ /**
188
+ * Utility type for inferring the payload shape of the dispatching methods of a
189
+ * specific `alerter` singleton.
190
+ *
191
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
192
+ */
193
+ export type InferAlerterPayload<F extends Record<string, (...args: Array<any>) => void>> = F extends Record<string, (payload: infer P) => void> ? P : never;
194
+ /**
195
+ * Utility type for inferring the alert info object shape received by the
196
+ * callbacks of a specific alerter `subscribe` function.
197
+ *
198
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
199
+ */
200
+ export type InferSubscriberAlerts<F extends (callback: (alerts: Array<unknown>, meta: unknown) => void) => () => void> = F extends (callback: (alerts: Array<infer A>) => void) => () => void ? A : never;
201
+ export {};