@vitavision/ui 0.7.0 → 0.9.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.
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Numbers as a field shows them and as a person types them. Internal to this package (used by
3
+ * `VectorInput` and `PoseInput`); not exported.
4
+ *
5
+ * A field bound straight to a number re-formats under the cursor: typing `0.` into a field
6
+ * showing `0.000` becomes `0` before the next digit arrives. So the multi-number fields keep
7
+ * the typed text while they have focus and show the formatted value otherwise; these are the
8
+ * two conversions between the two.
9
+ */
10
+
11
+ /**
12
+ * A number at a fixed number of decimals, without a negative zero (`-0.000` reads as a sign
13
+ * error in a coordinate readout).
14
+ *
15
+ * @param value - The number. Non-finite values print as `NaN`, `Infinity`, `-Infinity`.
16
+ * @param precision - Decimals, 0–20.
17
+ * @returns The text.
18
+ */
19
+ export function formatNumber(value: number, precision: number): string {
20
+ if (!Number.isFinite(value)) return String(value);
21
+ const text = value.toFixed(precision);
22
+ return /^-0(?:\.0*)?$/.test(text) ? text.slice(1) : text;
23
+ }
24
+
25
+ /**
26
+ * The number a field's text means, if it means one.
27
+ *
28
+ * @param text - What is in the field.
29
+ * @returns The number, or `null` for empty, partial (`-`, `1e`) or non-numeric text.
30
+ */
31
+ export function parseNumber(text: string): number | null {
32
+ const trimmed = text.trim();
33
+ if (trimmed === "") return null;
34
+ const value = Number(trimmed);
35
+ return Number.isFinite(value) ? value : null;
36
+ }
@@ -0,0 +1,128 @@
1
+ /*
2
+ * Notifications, as a store any module can write to.
3
+ *
4
+ * A toast is raised from wherever the news is — a file loader, a worker's error handler, a
5
+ * bake that finished — which is rarely a component with a context in reach. So `toast()` is
6
+ * a plain function over a module-level store, and `<Toaster />`, mounted once, is its only
7
+ * reader. The store holds no timers and touches no DOM: auto-dismissal belongs to the
8
+ * `Toaster`, which is what knows whether a reader is hovering it.
9
+ */
10
+
11
+ /** A toast's kind: a fact (`info`), a completed action (`success`), a caveat (`warn`), a failure (`error`). */
12
+ export type ToastTone = "info" | "success" | "warn" | "error";
13
+
14
+ /** What `toast()` takes. */
15
+ export interface ToastOptions {
16
+ /** One line: what happened. */
17
+ title: string;
18
+ /** More detail, and what to do about it. */
19
+ description?: string | undefined;
20
+ /** Defaults to `info`. */
21
+ tone?: ToastTone | undefined;
22
+ /**
23
+ * How long it stays, in milliseconds, while not hovered or focused. Defaults to 5000
24
+ * (8000 for `error`); `Infinity` keeps it until dismissed.
25
+ */
26
+ duration?: number | undefined;
27
+ /** Replace the toast with this id instead of adding one — for progress that updates in place. */
28
+ id?: string | undefined;
29
+ }
30
+
31
+ /** A toast in the store. */
32
+ export interface ToastRecord {
33
+ /** Identifies the toast to `dismiss` and to `id` replacement. */
34
+ id: string;
35
+ /** One line: what happened. */
36
+ title: string;
37
+ /** More detail, if any. */
38
+ description: string | undefined;
39
+ /** Its kind. */
40
+ tone: ToastTone;
41
+ /** Milliseconds before it dismisses itself (`Infinity`: never). */
42
+ duration: number;
43
+ }
44
+
45
+ /**
46
+ * A toast store: what `toast()` writes to and `<Toaster />` reads. Every member may be called
47
+ * detached.
48
+ */
49
+ export interface ToastStore {
50
+ /** Add a toast (or replace the one with `options.id`), returning its id. */
51
+ readonly toast: (options: ToastOptions) => string;
52
+ /** Remove one toast, or every toast when `id` is omitted. */
53
+ readonly dismiss: (id?: string) => void;
54
+ /** The toasts, oldest first. The same array until the store changes. */
55
+ readonly getSnapshot: () => readonly ToastRecord[];
56
+ /** Be told when the toasts change; returns the unsubscribe function. */
57
+ readonly subscribe: (listener: () => void) => () => void;
58
+ }
59
+
60
+ const DEFAULT_DURATION = 5000;
61
+ const ERROR_DURATION = 8000;
62
+
63
+ /**
64
+ * A new, empty toast store. The package keeps one (behind `toast()` and the default
65
+ * `<Toaster />`); create another only to isolate a region or a test.
66
+ *
67
+ * @returns The store.
68
+ */
69
+ export function createToastStore(): ToastStore {
70
+ let toasts: readonly ToastRecord[] = [];
71
+ let sequence = 0;
72
+ const listeners = new Set<() => void>();
73
+ const commit = (next: readonly ToastRecord[]) => {
74
+ toasts = next;
75
+ for (const listener of [...listeners]) listener();
76
+ };
77
+
78
+ return {
79
+ toast: (options) => {
80
+ const tone = options.tone ?? "info";
81
+ sequence += 1;
82
+ const record: ToastRecord = {
83
+ id: options.id ?? `toast-${sequence}`,
84
+ title: options.title,
85
+ description: options.description,
86
+ tone,
87
+ duration: options.duration ?? (tone === "error" ? ERROR_DURATION : DEFAULT_DURATION),
88
+ };
89
+ const existing = toasts.findIndex((t) => t.id === record.id);
90
+ commit(existing === -1 ? [...toasts, record] : toasts.map((t, i) => (i === existing ? record : t)));
91
+ return record.id;
92
+ },
93
+ dismiss: (id) => {
94
+ if (id === undefined) {
95
+ if (toasts.length > 0) commit([]);
96
+ return;
97
+ }
98
+ if (toasts.some((t) => t.id === id)) commit(toasts.filter((t) => t.id !== id));
99
+ },
100
+ getSnapshot: () => toasts,
101
+ subscribe: (listener) => {
102
+ listeners.add(listener);
103
+ return () => {
104
+ listeners.delete(listener);
105
+ };
106
+ },
107
+ };
108
+ }
109
+
110
+ /** The store behind `toast()` and a `<Toaster />` given no `store`. */
111
+ export const defaultToastStore: ToastStore = createToastStore();
112
+
113
+ /**
114
+ * Raise a notification, from anywhere — shown by the mounted `<Toaster />`.
115
+ *
116
+ * `toast({ title: "Scenario loaded", tone: "success" })`. Pass `id` to update a toast in
117
+ * place (a progress message that becomes a result). `toast.dismiss(id)` removes one early,
118
+ * `toast.dismiss()` all of them.
119
+ *
120
+ * @param options - The message, its tone and its duration.
121
+ * @returns The toast's id.
122
+ */
123
+ export const toast: ((options: ToastOptions) => string) & {
124
+ /** Remove one toast, or every toast when `id` is omitted. */
125
+ dismiss: (id?: string) => void;
126
+ } = Object.assign((options: ToastOptions) => defaultToastStore.toast(options), {
127
+ dismiss: (id?: string) => defaultToastStore.dismiss(id),
128
+ });
package/src/styles.css CHANGED
@@ -159,13 +159,30 @@
159
159
  list-style: none;
160
160
  }
161
161
 
162
+ /*
163
+ * Reduced motion makes motion synchronous, not merely short.
164
+ *
165
+ * A transition of any non-zero duration -- 0.01ms included -- is still a transition: the
166
+ * computed style keeps its old value until the next frame, so code that sets a style and
167
+ * measures in the same tick (mermaid's layout) reads stale numbers wherever an element
168
+ * has `transition-property: all`. A 0s transition is never started, so the new value is
169
+ * visible at once. Nothing in this repo waits for `transitionend`.
170
+ *
171
+ * Animations keep a name with a 0s duration rather than `animation: none`: the animation
172
+ * still runs to its end state and fires `animationend` immediately, which is what Radix
173
+ * `Presence` waits for before unmounting an overlay whose consumer gave it an exit
174
+ * animation (`data-[state=closed]:animate-out`). `animation: none` would unmount it at
175
+ * once too, but would also freeze `animate-spin` / `animate-pulse` in their first frame.
176
+ */
162
177
  @media (prefers-reduced-motion: reduce) {
163
178
  *,
164
179
  *::before,
165
180
  *::after {
166
- animation-duration: 0.01ms !important;
181
+ animation-duration: 0s !important;
182
+ animation-delay: 0s !important;
167
183
  animation-iteration-count: 1 !important;
168
- transition-duration: 0.01ms !important;
184
+ transition-duration: 0s !important;
185
+ transition-delay: 0s !important;
169
186
  }
170
187
  }
171
188
  }