@enigmax/primitives 0.11.0 → 0.12.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/dist/index.d.ts CHANGED
@@ -309,4 +309,216 @@ interface EstimateOptions {
309
309
  */
310
310
  declare function estimatePasswordStrength(password: string, options?: EstimateOptions): PasswordStrengthReport;
311
311
 
312
- export { type ButtonCooldown, type ButtonElement, type ButtonInstance, type ButtonOptions, type ButtonState, type EstimateOptions, type GeneratePasswordOptions, INPUT_ICON_PATHS, type InputAction, type InputActionState, type InputIcon, type InputInstance, type InputOptions, type MarqueeHover, type MarqueeInstance, type MarqueeOptions, type PasswordAlphabet, type PasswordScore, type PasswordStrengthReport, createButton, createInput, createMarquee, estimatePasswordStrength, generatePassword, iconMarkup };
312
+ /**
313
+ * Notification queue: ordering, timers, dedupe and pause. No rendering, no styles.
314
+ *
315
+ * The timer is the part that is always wrong when hand-rolled. A dismiss timer
316
+ * that keeps counting while the tab is hidden fires the moment the visitor comes
317
+ * back and they never see the message, so the remaining time is held, not run.
318
+ */
319
+ type NotificationTone = "info" | "success" | "warning" | "error" | "loading";
320
+ /** One button on a notification. Anything more belongs on the page, not in a toast. */
321
+ interface NotificationAction {
322
+ label: string;
323
+ onSelect: () => void;
324
+ /** Dismiss once it has been pressed. On by default. */
325
+ dismiss?: boolean;
326
+ }
327
+ interface NotificationInput {
328
+ /** Reuses the slot of a live notification with the same key instead of stacking. */
329
+ key?: string;
330
+ title: string;
331
+ body?: string;
332
+ tone?: NotificationTone;
333
+ /** ms before it dismisses itself. 0 or Infinity keeps it until dismissed. */
334
+ duration?: number;
335
+ action?: NotificationAction;
336
+ /** Arbitrary payload for the renderer: an href, an icon name. */
337
+ data?: unknown;
338
+ }
339
+ interface Notification extends Required<Omit<NotificationInput, "body" | "data" | "key" | "action">> {
340
+ id: string;
341
+ key?: string;
342
+ body?: string;
343
+ action?: NotificationAction;
344
+ data?: unknown;
345
+ createdAt: number;
346
+ }
347
+ /** What `promise()` shows at each stage. A function receives the resolved value or the error. */
348
+ interface PromiseMessages<T> {
349
+ loading: string | NotificationInput;
350
+ success: string | ((value: T) => string | NotificationInput);
351
+ error: string | ((error: unknown) => string | NotificationInput);
352
+ }
353
+ interface NotificationsOptions {
354
+ /** Live notifications kept at once. The oldest dismissable one makes room. */
355
+ max?: number;
356
+ /** Default ms before self-dismissal. */
357
+ duration?: number;
358
+ /** Errors default to staying until dismissed. */
359
+ stickyTones?: NotificationTone[];
360
+ }
361
+ interface Notifications {
362
+ readonly items: readonly Notification[];
363
+ notify(input: NotificationInput): string;
364
+ /** Change a live notification in place, keeping its slot and restarting its timer. */
365
+ update(id: string, patch: Partial<NotificationInput>): void;
366
+ /**
367
+ * One notification that follows an async operation from loading to its outcome, in the
368
+ * same slot. The alternative is three toasts stacking up for one action.
369
+ */
370
+ promise<T>(work: Promise<T>, messages: PromiseMessages<T>): Promise<T>;
371
+ dismiss(id: string): void;
372
+ dismissAll(): void;
373
+ /** Hold every timer, e.g. while a pointer rests on the stack. */
374
+ pause(): void;
375
+ resume(): void;
376
+ subscribe(listener: (items: readonly Notification[]) => void): () => void;
377
+ destroy(): void;
378
+ }
379
+ declare function createNotifications(options?: NotificationsOptions): Notifications;
380
+
381
+ /**
382
+ * Connection state: online/offline plus whatever the Network Information API will say.
383
+ *
384
+ * The part worth not rewriting is the TRANSITION. "You are back online" needs to know the
385
+ * connection had dropped, and every implementation of that ends up as a stray `wasOffline`
386
+ * boolean next to the effect, re-derived per screen and wrong the first time the page
387
+ * loads offline. It is tracked here instead.
388
+ */
389
+ interface NetworkState {
390
+ online: boolean;
391
+ /** Estimated downlink in Mbps. Null where the browser does not report it. */
392
+ downlink: number | null;
393
+ downlinkMax: number | null;
394
+ /** "slow-2g" | "2g" | "3g" | "4g", or null. */
395
+ effectiveType: string | null;
396
+ /** Estimated round-trip time in ms. */
397
+ rtt: number | null;
398
+ /** The visitor asked for reduced data use. */
399
+ saveData: boolean | null;
400
+ /** "wifi" | "cellular" | "ethernet" | ..., or null. */
401
+ type: string | null;
402
+ /**
403
+ * The connection dropped earlier and is back. False on a first load, so a page that
404
+ * opens online never announces a recovery that did not happen.
405
+ */
406
+ recovered: boolean;
407
+ /** On a connection worth degrading for: 2g or slower, or data saver on. */
408
+ slow: boolean;
409
+ }
410
+ interface NetworkMonitor {
411
+ readonly state: NetworkState;
412
+ subscribe(listener: (state: NetworkState) => void): () => void;
413
+ destroy(): void;
414
+ }
415
+ /** What a server render sees. Assuming offline would flash a warning on every page. */
416
+ declare const SERVER_NETWORK_STATE: NetworkState;
417
+ declare function createNetworkMonitor(): NetworkMonitor;
418
+
419
+ /**
420
+ * Relative time: "3 hours ago", and everything that has to be right around it.
421
+ *
422
+ * The rendering itself is `<relative-time>` (@github/relative-time-element), which already
423
+ * owns the hard parts - Intl.RelativeTimeFormat per locale, and re-rendering on a schedule
424
+ * that gets slower as the date gets older. What is here is the part every wrapper
425
+ * re-implements and usually gets wrong: parsing a timestamp that does not declare its zone,
426
+ * knowing when the date has aged past the relative threshold, and producing an absolute
427
+ * label to show while the element is still loading, or forever if scripting is off.
428
+ *
429
+ * Nothing in this file touches the DOM, so it renders on a server.
430
+ */
431
+ /** How the element should phrase it. `auto` is relative until the threshold, then a date. */
432
+ type RelativeTimeFormat = "auto" | "relative" | "duration" | "datetime" | "micro" | "elapsed";
433
+ type RelativeTimeTense = "auto" | "past" | "future";
434
+ type RelativeTimePrecision = "year" | "month" | "day" | "hour" | "minute" | "second";
435
+ type RelativeTimeStyle = "long" | "short" | "narrow";
436
+ type NumericStyle = "numeric" | "2-digit";
437
+ interface RelativeTimeOptions {
438
+ format?: RelativeTimeFormat;
439
+ tense?: RelativeTimeTense;
440
+ precision?: RelativeTimePrecision;
441
+ /** ISO 8601 duration. Past this age `auto` stops being relative. Default `P30D`. */
442
+ threshold?: string;
443
+ /** Word before an absolute date, e.g. "on 5 May". Empty string removes it. */
444
+ prefix?: string;
445
+ formatStyle?: RelativeTimeStyle;
446
+ /**
447
+ * BCP 47 tag. Left undefined the element reads the closest `lang` in the document,
448
+ * which is what a translated page wants - hardcoding one is how a Spanish page ends
449
+ * up saying "3 hours ago".
450
+ */
451
+ locale?: string;
452
+ /** IANA zone for the absolute rendering. Undefined means the reader's own. */
453
+ timeZone?: string;
454
+ second?: NumericStyle;
455
+ minute?: NumericStyle;
456
+ hour?: NumericStyle;
457
+ weekday?: RelativeTimeStyle;
458
+ day?: NumericStyle;
459
+ month?: NumericStyle | "short" | "long" | "narrow";
460
+ year?: NumericStyle;
461
+ timeZoneName?: "long" | "short" | "shortOffset" | "longOffset" | "shortGeneric" | "longGeneric";
462
+ /** Drop the exact timestamp the element otherwise puts in `title`. */
463
+ noTitle?: boolean;
464
+ /**
465
+ * Once the date is older than `threshold`, render it as digits (05/05/2026) instead of
466
+ * a prefixed month name. The cutoff is the threshold itself, not a guess at it.
467
+ */
468
+ numericBeyondThreshold?: boolean;
469
+ }
470
+ /** Everything derived from a date, in one pass, for whichever adapter is rendering it. */
471
+ interface RelativeTimeView {
472
+ /** Null when the input could not be parsed - nothing else here is meaningful then. */
473
+ date: Date | null;
474
+ /** `datetime` attribute value: always UTC, always ISO. */
475
+ iso: string;
476
+ /** Absolute text. Shown until the element upgrades, and forever without scripting. */
477
+ label: string;
478
+ /** Full timestamp for `title` / `aria-label`. */
479
+ exact: string;
480
+ /** The date is older (or further ahead) than `threshold`. */
481
+ beyondThreshold: boolean;
482
+ /** Render `label` in a plain `<time>` and skip the element entirely. */
483
+ absoluteOnly: boolean;
484
+ }
485
+ /**
486
+ * A timestamp with no zone is UTC.
487
+ *
488
+ * This is the single most common defect in a date column: `2026-08-13 22:41:00` comes back
489
+ * from the database with no offset, `new Date()` reads it as LOCAL time, and every reader
490
+ * east or west of the server sees a time that is hours out - silently, because the wrong
491
+ * time is still a valid one.
492
+ *
493
+ * A date with no clock is left exactly as it is. The spec already reads a bare `YYYY-MM-DD`
494
+ * as UTC, so there is nothing to add - and `YYYY-MM-DDZ` is not in the spec's grammar at
495
+ * all, which drops it into each engine's own legacy parser. That is a portability coin
496
+ * flip on a value that was already correct.
497
+ */
498
+ declare function ensureZone(text: string): string;
499
+ /** Parse anything a date column or an API hands over. Null rather than an Invalid Date. */
500
+ declare function normalizeDate(value: string | number | Date | null | undefined): Date | null;
501
+ /**
502
+ * ISO 8601 duration to milliseconds, for comparing an age against `threshold`.
503
+ *
504
+ * Years and months are approximated the way the element approximates them, because the
505
+ * threshold is a rough "old enough to stop counting", not an anniversary.
506
+ */
507
+ declare function parseDuration(value: string): number;
508
+ /**
509
+ * Everything an adapter needs to render one date.
510
+ *
511
+ * `now` is a parameter so a test can pin it, and so a server render can pass the same
512
+ * instant it used elsewhere on the page.
513
+ */
514
+ declare function relativeTimeView(value: string | number | Date | null | undefined, options?: RelativeTimeOptions, now?: Date): RelativeTimeView;
515
+ /**
516
+ * The element's attributes, kebab-cased, with anything undefined left out.
517
+ *
518
+ * Written as attributes rather than properties because that is the half of a custom
519
+ * element's API that works before it is defined - the markup is already correct when the
520
+ * definition arrives late, or never.
521
+ */
522
+ declare function relativeTimeAttributes(view: RelativeTimeView, options?: RelativeTimeOptions): Record<string, string>;
523
+
524
+ export { type ButtonCooldown, type ButtonElement, type ButtonInstance, type ButtonOptions, type ButtonState, type EstimateOptions, type GeneratePasswordOptions, INPUT_ICON_PATHS, type InputAction, type InputActionState, type InputIcon, type InputInstance, type InputOptions, type MarqueeHover, type MarqueeInstance, type MarqueeOptions, type NetworkMonitor, type NetworkState, type Notification, type NotificationAction, type NotificationInput, type NotificationTone, type Notifications, type NotificationsOptions, type PasswordAlphabet, type PasswordScore, type PasswordStrengthReport, type PromiseMessages, type RelativeTimeFormat, type RelativeTimeOptions, type RelativeTimePrecision, type RelativeTimeStyle, type RelativeTimeTense, type RelativeTimeView, SERVER_NETWORK_STATE, createButton, createInput, createMarquee, createNetworkMonitor, createNotifications, ensureZone, estimatePasswordStrength, generatePassword, iconMarkup, normalizeDate, parseDuration, relativeTimeAttributes, relativeTimeView };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { INPUT_ICON_PATHS, createButton, createInput, createMarquee, estimatePasswordStrength, generatePassword, iconMarkup } from './chunk-42Y7OOJM.js';
1
+ export { INPUT_ICON_PATHS, SERVER_NETWORK_STATE, createButton, createInput, createMarquee, createNetworkMonitor, createNotifications, ensureZone, estimatePasswordStrength, generatePassword, iconMarkup, normalizeDate, parseDuration, relativeTimeAttributes, relativeTimeView } from './chunk-DD2X5B2J.js';
2
2
  export { createSearch } from './chunk-UZFEEFMF.js';
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ButtonProps } from '../react/index.js';
3
- export { BreachChecker, BreachState, BreachStatus, FieldAction, Input, InputProps, PasswordStrength, PasswordStrengthProps, UseButtonResult, UseInputResult, UseMarqueeResult, UseSearchResult, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useSearch } from '../react/index.js';
4
- export { ButtonOptions, ButtonState, EstimateOptions, GeneratePasswordOptions, InputAction, InputIcon, InputOptions, MarqueeHover, MarqueeInstance, MarqueeOptions, PasswordScore, PasswordStrengthReport, estimatePasswordStrength, generatePassword } from '../index.js';
3
+ export { BreachChecker, BreachState, BreachStatus, FieldAction, Input, InputProps, PasswordStrength, PasswordStrengthProps, RelativeTime, RelativeTimeProps, ToastControls, ToastPosition, Toaster, ToasterProps, UseButtonResult, UseInputResult, UseMarqueeResult, UseNotificationsResult, UseSearchResult, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../react/index.js';
4
+ export { ButtonOptions, ButtonState, EstimateOptions, GeneratePasswordOptions, InputAction, InputIcon, InputOptions, MarqueeHover, MarqueeInstance, MarqueeOptions, NetworkState, PasswordScore, PasswordStrengthReport, estimatePasswordStrength, generatePassword } from '../index.js';
5
5
  export { F as FuseConstructor, b as SearchMatch, c as SearchOptions } from '../search-CsO3L1Lw.js';
6
6
 
7
7
  /**
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { Button } from '../chunk-L4KA342C.js';
3
- export { Input, PasswordStrength, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useSearch } from '../chunk-L4KA342C.js';
4
- export { estimatePasswordStrength, generatePassword } from '../chunk-42Y7OOJM.js';
2
+ import { Button } from '../chunk-YFM2FWHQ.js';
3
+ export { Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../chunk-YFM2FWHQ.js';
4
+ export { estimatePasswordStrength, generatePassword } from '../chunk-DD2X5B2J.js';
5
5
  import '../chunk-UZFEEFMF.js';
6
6
  import Link from 'next/link';
7
7
  import { forwardRef, createElement } from 'react';
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
- import { ComponentPropsWithoutRef, ElementType, ReactNode, RefObject } from 'react';
3
- import { ButtonOptions, ButtonState, MarqueeOptions, InputOptions, GeneratePasswordOptions, EstimateOptions, PasswordStrengthReport } from '../index.js';
2
+ import { ComponentPropsWithoutRef, ElementType, ReactNode, RefObject, CSSProperties, HTMLAttributes } from 'react';
3
+ import { ButtonOptions, ButtonState, MarqueeOptions, InputOptions, GeneratePasswordOptions, EstimateOptions, PasswordStrengthReport, Notifications, Notification, NotificationsOptions, NetworkState, RelativeTimeOptions } from '../index.js';
4
4
  export { InputAction, InputIcon, MarqueeHover, MarqueeInstance, PasswordScore, estimatePasswordStrength, generatePassword } from '../index.js';
5
5
  import { b as SearchMatch, c as SearchOptions } from '../search-CsO3L1Lw.js';
6
6
  export { F as FuseConstructor } from '../search-CsO3L1Lw.js';
@@ -18,6 +18,16 @@ export { F as FuseConstructor } from '../search-CsO3L1Lw.js';
18
18
  * ordinary one means writing the same six lines at every call site, which is how they drift.
19
19
  */
20
20
  interface ButtonProps extends ButtonOptions, Omit<ComponentPropsWithoutRef<"button">, "onClick" | "onChange" | "children" | "type"> {
21
+ /**
22
+ * What to run when it is pressed. The same thing as `onPress`, under the name React
23
+ * already uses - write whichever reads better.
24
+ *
25
+ * It is an ALIAS, not a second handler: the component owns the element's real onClick,
26
+ * because that is where a press is refused while the button is loading, disabled or
27
+ * cooling down. Passing one through would replace that and silently take the behaviour
28
+ * with it. Returning a promise drives `loading` for its duration, same as `onPress`.
29
+ */
30
+ onClick?: ButtonOptions["onPress"];
21
31
  /**
22
32
  * Override what to render. You rarely need it: an href already renders whatever
23
33
  * `setLinkComponent` registered - your router's Link - and a button otherwise.
@@ -133,7 +143,7 @@ declare function useSearch<T>(options?: SearchOptions<T>): UseSearchResult<T>;
133
143
  interface UseButtonResult extends ButtonState {
134
144
  /** Spread onto the element named by `element`. */
135
145
  props: {
136
- onClick: (event: {
146
+ onClick: (event: Event | {
137
147
  preventDefault(): void;
138
148
  }) => void;
139
149
  "aria-disabled": boolean;
@@ -279,4 +289,88 @@ interface PasswordStrengthProps extends Omit<ComponentPropsWithoutRef<"div">, "c
279
289
  */
280
290
  declare function PasswordStrength({ value, report, userInputs, segments, labels, showWarning, classNames, ...props }: PasswordStrengthProps): ReactNode;
281
291
 
282
- export { type BreachChecker, type BreachState, type BreachStatus, Button, ButtonOptions, type ButtonProps, ButtonState, EstimateOptions, type FieldAction, GeneratePasswordOptions, Input, InputOptions, type InputProps, MarqueeOptions, PasswordStrength, type PasswordStrengthProps, PasswordStrengthReport, SearchMatch, SearchOptions, type UseButtonResult, type UseInputResult, type UseMarqueeResult, type UseSearchResult, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useSearch };
292
+ /** One queue per app by default; a second one would race the first for the corner. */
293
+ declare const defaultQueue: Notifications;
294
+ interface UseNotificationsResult extends Pick<Notifications, "notify" | "dismiss" | "dismissAll" | "pause" | "resume"> {
295
+ items: readonly Notification[];
296
+ }
297
+ /**
298
+ * Subscribe to a notification queue.
299
+ *
300
+ * Rendering is yours: this returns the list, the ordering and the timers, and
301
+ * nothing about how a notification looks.
302
+ */
303
+ declare function useNotifications(queue?: Notifications): UseNotificationsResult;
304
+ declare function createNotificationQueue(options?: NotificationsOptions): Notifications;
305
+
306
+ /**
307
+ * `<Toaster />` - the stack, the movement and the gestures. No colours.
308
+ *
309
+ * The queue underneath already owns ordering, dedupe by key, sticky errors and timers that
310
+ * HOLD while the tab is hidden. What this adds is everything you cannot express in a queue:
311
+ * a toast that is on its way out is still on screen, a pointer resting on the stack must
312
+ * stop the clock, and a swipe has to follow the finger before it decides anything.
313
+ *
314
+ * Every part carries a data attribute, so the theme is a stylesheet you own. `enigma add
315
+ * toast --copy` writes one.
316
+ */
317
+ type ToastPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
318
+ interface ToasterProps {
319
+ /** Which queue to render. The shared one by default. */
320
+ queue?: Notifications;
321
+ position?: ToastPosition;
322
+ /**
323
+ * How long the exit animation lasts, in ms. The node stays mounted for exactly this
324
+ * long after it leaves the queue - a toast that vanishes the instant it is dismissed
325
+ * cannot animate out, and that is the only reason this component keeps its own list.
326
+ */
327
+ exitDuration?: number;
328
+ /** Px a toast must travel before the release dismisses it. */
329
+ swipeThreshold?: number;
330
+ /** Render a toast yourself. The default renders title, body and the action. */
331
+ children?: (notification: Notification, controls: ToastControls) => ReactNode;
332
+ /** Accessible name for the region. */
333
+ label?: string;
334
+ className?: string;
335
+ style?: CSSProperties;
336
+ }
337
+ interface ToastControls {
338
+ dismiss: () => void;
339
+ }
340
+ declare function Toaster({ queue, position, exitDuration, swipeThreshold, children, label, className, style }: ToasterProps): ReactNode;
341
+
342
+ /**
343
+ * Connection state, kept in sync through useSyncExternalStore.
344
+ *
345
+ * ```tsx
346
+ * const { online, recovered, slow } = useNetworkState();
347
+ *
348
+ * useEffect(() => {
349
+ * if (!online) notify({ key: "net", title: "No connection", tone: "error" });
350
+ * else if (recovered) notify({ key: "net", title: "Back online", tone: "success" });
351
+ * }, [online, recovered]);
352
+ * ```
353
+ *
354
+ * `recovered` is false on a first load, so a page that opens online never announces a
355
+ * recovery that did not happen - the bug every hand-rolled `wasOffline` flag starts with.
356
+ */
357
+ declare function useNetworkState(): NetworkState;
358
+
359
+ interface RelativeTimeProps extends RelativeTimeOptions, Omit<HTMLAttributes<HTMLElement>, "prefix" | "children"> {
360
+ /** ISO string, epoch milliseconds, or a Date. A string with no zone is read as UTC. */
361
+ date: string | number | Date | null | undefined;
362
+ /** Rendered when the date cannot be parsed. Nothing, by default. */
363
+ fallback?: ReactNode;
364
+ /** Title Case The Whole Phrase. Rarely what you want; `capitalizeFirst` usually is. */
365
+ capitalize?: boolean;
366
+ /** Uppercase the first letter, so a standalone "yesterday" reads as "Yesterday". */
367
+ capitalizeFirst?: boolean;
368
+ /**
369
+ * Pin the instant the age is measured from. Pass the request's timestamp to make a
370
+ * server render and its hydration agree exactly.
371
+ */
372
+ now?: Date;
373
+ }
374
+ declare function RelativeTime({ date, fallback, capitalize, capitalizeFirst, now, format, tense, precision, threshold, prefix, formatStyle, locale, timeZone, second, minute, hour, weekday, day, month, year, timeZoneName, noTitle, numericBeyondThreshold, ...rest }: RelativeTimeProps): ReactNode;
375
+
376
+ export { type BreachChecker, type BreachState, type BreachStatus, Button, ButtonOptions, type ButtonProps, ButtonState, EstimateOptions, type FieldAction, GeneratePasswordOptions, Input, InputOptions, type InputProps, MarqueeOptions, NetworkState, PasswordStrength, type PasswordStrengthProps, PasswordStrengthReport, RelativeTime, type RelativeTimeProps, SearchMatch, SearchOptions, type ToastControls, type ToastPosition, Toaster, type ToasterProps, type UseButtonResult, type UseInputResult, type UseMarqueeResult, type UseNotificationsResult, type UseSearchResult, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch };
@@ -1,4 +1,4 @@
1
1
  "use client";
2
- export { Button, Input, PasswordStrength, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useSearch } from '../chunk-L4KA342C.js';
3
- export { estimatePasswordStrength, generatePassword } from '../chunk-42Y7OOJM.js';
2
+ export { Button, Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../chunk-YFM2FWHQ.js';
3
+ export { estimatePasswordStrength, generatePassword } from '../chunk-DD2X5B2J.js';
4
4
  import '../chunk-UZFEEFMF.js';
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ButtonProps } from '../react/index.js';
3
- export { BreachChecker, BreachState, BreachStatus, FieldAction, Input, InputProps, PasswordStrength, PasswordStrengthProps, UseButtonResult, UseInputResult, UseMarqueeResult, UseSearchResult, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useSearch } from '../react/index.js';
4
- export { ButtonOptions, ButtonState, EstimateOptions, GeneratePasswordOptions, InputAction, InputIcon, InputOptions, MarqueeHover, MarqueeInstance, MarqueeOptions, PasswordScore, PasswordStrengthReport, estimatePasswordStrength, generatePassword } from '../index.js';
3
+ export { BreachChecker, BreachState, BreachStatus, FieldAction, Input, InputProps, PasswordStrength, PasswordStrengthProps, RelativeTime, RelativeTimeProps, ToastControls, ToastPosition, Toaster, ToasterProps, UseButtonResult, UseInputResult, UseMarqueeResult, UseNotificationsResult, UseSearchResult, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../react/index.js';
4
+ export { ButtonOptions, ButtonState, EstimateOptions, GeneratePasswordOptions, InputAction, InputIcon, InputOptions, MarqueeHover, MarqueeInstance, MarqueeOptions, NetworkState, PasswordScore, PasswordStrengthReport, estimatePasswordStrength, generatePassword } from '../index.js';
5
5
  export { F as FuseConstructor, b as SearchMatch, c as SearchOptions } from '../search-CsO3L1Lw.js';
6
6
 
7
7
  declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLElement>>;
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { Button } from '../chunk-L4KA342C.js';
3
- export { Input, PasswordStrength, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useSearch } from '../chunk-L4KA342C.js';
4
- export { estimatePasswordStrength, generatePassword } from '../chunk-42Y7OOJM.js';
2
+ import { Button } from '../chunk-YFM2FWHQ.js';
3
+ export { Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../chunk-YFM2FWHQ.js';
4
+ export { estimatePasswordStrength, generatePassword } from '../chunk-DD2X5B2J.js';
5
5
  import '../chunk-UZFEEFMF.js';
6
6
  import { Link } from 'react-router';
7
7
  import { forwardRef, createElement } from 'react';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@enigmax/primitives",
3
- "version": "0.11.0",
4
- "description": "Headless interaction primitives: the behaviour, the timing and the accessibility of components like a draggable infinite marquee, with no styles of their own. Framework-agnostic core plus thin adapters.",
3
+ "version": "0.12.0",
4
+ "description": "Headless components: the behaviour, the timing and the accessibility, with no styles of their own. Framework-agnostic core plus thin adapters.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -26,12 +26,14 @@
26
26
  "./react-router": {
27
27
  "types": "./dist/react-router/index.d.ts",
28
28
  "import": "./dist/react-router/index.js"
29
- }
29
+ },
30
+ "./relative-time.css": "./recipes/relative-time/styles.css",
31
+ "./toast.css": "./recipes/toast/styles.css"
30
32
  },
31
33
  "scripts": {
32
34
  "build": "tsup",
33
35
  "typecheck": "tsc --noEmit",
34
- "test": "npm run test:fixtures && playwright test",
36
+ "test": "npm run test:fixtures && node --test test/*.test.mjs && playwright test",
35
37
  "test:fixtures": "esbuild test/fixture/react.tsx --bundle --format=esm --jsx=automatic --outfile=test/fixture/build/react.js --tsconfig=tsconfig.json --alias:next/link=./test/fixture/next-link-stub.tsx --alias:react-router=./test/fixture/react-router-stub.tsx",
36
38
  "test:install": "playwright install chromium",
37
39
  "prepublishOnly": "npm run typecheck && npm run build"
@@ -45,6 +47,7 @@
45
47
  "LICENSE"
46
48
  ],
47
49
  "peerDependencies": {
50
+ "@github/relative-time-element": ">=4",
48
51
  "fuse.js": ">=6",
49
52
  "next": ">=13",
50
53
  "react": ">=18",
@@ -62,9 +65,13 @@
62
65
  },
63
66
  "react-router": {
64
67
  "optional": true
68
+ },
69
+ "@github/relative-time-element": {
70
+ "optional": true
65
71
  }
66
72
  },
67
73
  "devDependencies": {
74
+ "@github/relative-time-element": "^4.5.1",
68
75
  "@playwright/test": "^1.48.0",
69
76
  "@types/react": "^19.2.0",
70
77
  "@types/react-dom": "^19.2.4",
@@ -80,18 +87,22 @@
80
87
  "node": ">=18"
81
88
  },
82
89
  "keywords": [
83
- "headless",
84
- "primitives",
85
- "marquee",
86
90
  "carousel",
87
- "unstyled",
88
- "react",
89
- "vanilla",
90
91
  "enigma",
92
+ "fuse",
93
+ "headless",
91
94
  "input",
95
+ "marquee",
96
+ "network",
97
+ "notifications",
92
98
  "password",
99
+ "primitives",
100
+ "react",
101
+ "relative-time",
93
102
  "search",
94
- "fuse"
103
+ "toast",
104
+ "unstyled",
105
+ "vanilla"
95
106
  ],
96
107
  "license": "Apache-2.0",
97
108
  "author": "FJRG2007 <fjrg2007@tpeoficial.com>",
@@ -0,0 +1,60 @@
1
+ import { useNotifications } from "@enigmax/primitives/react";
2
+
3
+ /**
4
+ * A styled notification stack, yours to edit.
5
+ *
6
+ * Ordering, dedupe by key, sticky errors and the dismiss timers all come from the
7
+ * queue. Hovering pauses those timers, which is the whole reason `pause`/`resume` are
8
+ * wired to the pointer here.
9
+ *
10
+ * Fire one from anywhere:
11
+ * import { defaultQueue } from "@enigmax/primitives/react";
12
+ * defaultQueue.notify({ title: "Saved", tone: "success" });
13
+ */
14
+ const TONE = {
15
+ info: "border-l-neutral-500",
16
+ success: "border-l-emerald-500",
17
+ warning: "border-l-amber-500",
18
+ error: "border-l-red-500"
19
+ } as const;
20
+
21
+ export function Notifications() {
22
+ const { items, dismiss, pause, resume } = useNotifications();
23
+
24
+ if (!items.length) return null;
25
+
26
+ return (
27
+ <div
28
+ className="fixed bottom-4 right-4 z-50 grid w-[min(22rem,calc(100vw-2rem))] gap-2"
29
+ role="region"
30
+ aria-label="Notifications"
31
+ onPointerEnter={pause}
32
+ onPointerLeave={resume}
33
+ >
34
+ {items.map((item) => (
35
+ <div
36
+ key={item.id}
37
+ role={item.tone === "error" ? "alert" : "status"}
38
+ className={`
39
+ flex items-start gap-3 rounded-xl border border-l-[3px] border-neutral-800
40
+ bg-neutral-900 px-3.5 py-3 shadow-lg ${TONE[item.tone]}
41
+ `}
42
+ >
43
+ <div className="min-w-0 flex-1">
44
+ <p className="text-sm text-neutral-100">{item.title}</p>
45
+ {item.body && <p className="mt-0.5 text-xs text-neutral-400">{item.body}</p>}
46
+ </div>
47
+ <button
48
+ type="button"
49
+ onClick={() => dismiss(item.id)}
50
+ aria-label={`Dismiss ${item.title}`}
51
+ title="Dismiss"
52
+ className="rounded-md px-1 text-neutral-500 hover:text-neutral-100"
53
+ >
54
+ &times;
55
+ </button>
56
+ </div>
57
+ ))}
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,24 @@
1
+ /*
2
+ * The two hooks <RelativeTime> exposes. Import this once, anywhere:
3
+ *
4
+ * import "@enigmax/utils/relative-time.css";
5
+ *
6
+ * Both are opt-in per timestamp - the attributes only appear when you ask for them - and
7
+ * both are marked !important on purpose: the element writes its own text content, so a
8
+ * `text-transform` from a reset or a typography plugin lands on the same node and would
9
+ * otherwise win on cascade order alone.
10
+ */
11
+
12
+ /* Title Case Every Word. Reads oddly on "2 Days Ago"; usually you want the rule below. */
13
+ [data-relative-time-capitalize="true"],
14
+ [data-relative-time-capitalize="true"] * {
15
+ text-transform: capitalize !important;
16
+ }
17
+
18
+ /* Just the first letter, so a standalone "yesterday" starts a line properly. The nested
19
+ selectors cover the element wrapping its text in a child, which it does for some formats. */
20
+ [data-relative-time-capitalize-first-letter="true"]::first-letter,
21
+ [data-relative-time-capitalize-first-letter="true"] *::first-letter,
22
+ [data-relative-time-capitalize-first-letter="true"] * *::first-letter {
23
+ text-transform: uppercase !important;
24
+ }