@danxbot/ui 7.4.2 → 7.5.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.
@@ -25,6 +25,77 @@ export declare function write(payload: RecordPayload, options: WriteOptions): Wr
25
25
  * in one call coalesces into a single notification.
26
26
  */
27
27
  export declare function writeAll(payloads: readonly RecordPayload[], options: WriteOptions): WriteAllResult;
28
+ /**
29
+ * One optimistic write, held ON TOP of a record's base rather than merged into it.
30
+ *
31
+ * ==== WHY A LAYER AND NOT A WRITE ====
32
+ *
33
+ * The first optimistic writes built on this store merged the belief into the base with a
34
+ * client-invented `__timestamp`, and each one then had to work around what that did:
35
+ *
36
+ * - the rollback had to be stamped NEWER than the belief, or the out-of-order guard dropped
37
+ * it — and a same-millisecond apply-then-revert is not newer, so a local monotonic clock
38
+ * was needed to get it through;
39
+ * - once a client timestamp was stored, the SERVER's own later answer could lose to it, since
40
+ * a browser clock and a database column are not the same clock;
41
+ * - the rollback snapshot came from a read, which strips the envelope, so `__type` had to be
42
+ * patched back in before the store would take it.
43
+ *
44
+ * A layer has none of those problems by construction. The base — the only thing the ordering
45
+ * guard adjudicates — is never touched by the belief, so the belief can never outrank a server
46
+ * answer and removing it is a removal, not a write that has to win anything.
47
+ *
48
+ * ==== ORDER ====
49
+ *
50
+ * Layers apply oldest first, so the newest belief about a field is the one a reader sees — a
51
+ * set, clear, set, clear on one field shows the last clear the moment it is made, whatever
52
+ * order the four answers come back in. Removing a layer from the middle leaves every later one
53
+ * standing: undoing write 2 of 4 does not undo 3 and 4.
54
+ */
55
+ export interface OptimisticLayer {
56
+ /** Global and strictly increasing — also the issue order of the writes that made them. */
57
+ readonly id: number;
58
+ /** Data fields, stored as given — never normalized, so a patch cannot mint records. */
59
+ fields: Record<string, unknown>;
60
+ /** An ISO time when this layer is a delete. */
61
+ readonly tombstone: string | undefined;
62
+ /** True when this layer IS the record — an optimistic create with no server row yet. */
63
+ readonly created: boolean;
64
+ }
65
+ /**
66
+ * Put an optimistic layer on top of `Type:id`, creating a base-less record for a create.
67
+ *
68
+ * Notification is QUEUED here, like every other store change; the optimistic-write primitive
69
+ * flushes synchronously afterwards, because a user-initiated write must never wait on an
70
+ * animation frame (a hidden page never gets one).
71
+ *
72
+ * @returns the layer id, for `removeOptimisticLayer` / `trimOptimisticLayers`
73
+ */
74
+ export declare function pushOptimisticLayer(type: string, id: string | number, layer: {
75
+ fields?: Record<string, unknown>;
76
+ tombstone?: string;
77
+ created?: boolean;
78
+ }): number;
79
+ /**
80
+ * Take one layer off. A record that only ever existed as an optimistic create disappears with
81
+ * its last layer — it reads `unknown` again, exactly as before the create.
82
+ */
83
+ export declare function removeOptimisticLayer(key: string, layerId: number): void;
84
+ /**
85
+ * A later write's answer has landed: every OLDER layer's belief about the same fields is
86
+ * superseded, whether or not that older request has answered yet. A layer left with nothing to
87
+ * say is dropped. Without this, a set whose answer is still out would keep overlaying a field
88
+ * the server has already confirmed was cleared after it.
89
+ */
90
+ export declare function trimOptimisticLayers(key: string, beforeLayerId: number, fields: readonly string[]): void;
91
+ /**
92
+ * Whether the BASE is tombstoned — what a list slot prunes on. An optimistic delete hides the
93
+ * row from every read but must not cost the slot its key, or a rollback would bring the record
94
+ * back with no list left holding it.
95
+ */
96
+ export declare function isBaseTombstoned(key: string): boolean;
97
+ /** Does `key` carry any optimistic layer right now? */
98
+ export declare function hasOptimisticLayers(key: string): boolean;
28
99
  /**
29
100
  * The `unknown` state, as ONE module-level frozen constant.
30
101
  *
@@ -54,7 +125,9 @@ export declare function peekRecord(type: string, id: string | number, needs: str
54
125
  *
55
126
  * Exported so a caller that must observe the effect of a write synchronously —
56
127
  * a test, or a measurement taken immediately after a write — can flush rather
57
- * than wait for a frame.
128
+ * than wait for a frame. The optimistic write (`reads/optimistic.ts`) flushes
129
+ * after every step: a reader's own action is one change, not a burst, and it
130
+ * must reach the screen in the same tick even on a page that gets no frames.
58
131
  */
59
132
  export declare function flushRecordNotifications(): void;
60
133
  export declare function subscribeToKey(key: string, listener: () => void): () => void;
@@ -2,5 +2,11 @@
2
2
  * Whether `value` reads as a plausible email address. An empty (or
3
3
  * whitespace-only) value is treated as VALID — clearing the field is not a
4
4
  * typo, and a required-field rule belongs to the caller, not this check.
5
+ *
6
+ * SG-909 review fix: the previous regex (`/^[^\s@]+@[^\s@]+\.[^\s@]+$/`)
7
+ * matched the domain half non-specifically, so `"a@b..com"` (an empty label
8
+ * between two dots) passed, and it never looked at trailing punctuation at
9
+ * all, so `"jane@example.com."` and `"jane@example.com,"` — the shape a typo
10
+ * or a sentence's own full stop produces — both passed too.
5
11
  */
6
12
  export declare function isValidEmail(value: string): boolean;
@@ -6,6 +6,12 @@
6
6
  * rule `formatDate` documents. A stored value should already be `parsePhone`'s
7
7
  * output; if it is not, that is a data problem worth seeing in the row, not
8
8
  * a blank field that looks like nobody ever entered a number.
9
+ *
10
+ * Reuses `formatPhonePartial` for the ten-digit case (SG-909 review) rather
11
+ * than repeating the same three-slice punctuation a second time — passing it
12
+ * exactly ten digits always produces the identical `"(XXX) XXX-XXXX"` string,
13
+ * since `formatPhonePartial` only ever appends a raw, unformatted tail past
14
+ * the tenth digit and there is no tail here.
9
15
  */
10
16
  export declare function formatPhone(value: string | null | undefined): string;
11
17
  /**
@@ -19,22 +25,44 @@ export declare function formatPhone(value: string | null | undefined): string;
19
25
  * too many, or an 11-digit number NOT starting with `1` (a foreign number
20
26
  * mis-typed into a US field, not a US number with an extra digit) — returns
21
27
  * `null` rather than truncating or padding a guess into place.
28
+ *
29
+ * THIS is the one place in the file the 11-digit-leading-`1` rule is written
30
+ * (SG-909 review — it used to also live in `digitsOfPhoneInProgress`, which
31
+ * no longer transforms digits at all; see that function's own doc for why).
22
32
  */
23
33
  export declare function parsePhone(input: string): string | null;
24
34
  /**
25
- * Every non-digit stripped, an 11-digit leading US country code dropped (the
26
- * same rule `parsePhone` applies), and the rest capped at ten digits — the
27
- * normalised digit sequence `PhoneField` (SG-909) keeps as ITS state while a
28
- * person is still typing, before there are enough digits for `parsePhone` to
29
- * accept.
35
+ * Every non-digit stripped — nothing else. `PhoneField` (SG-909) keeps this
36
+ * as ITS state while a person is still typing.
37
+ *
38
+ * Deliberately NOT capped at ten and NOT country-code-aware, unlike an
39
+ * earlier revision of this function. That revision silently dropped every
40
+ * digit past the tenth and stripped a leading `"1"` off an 11-digit run —
41
+ * which meant pasting `"555-123-4567 x12"` or `"44 20 7946 0958"` (a UK
42
+ * number) landed at commit as `parsePhone` accepting whatever ten-or-eleven
43
+ * digits happened to survive the cap, saving a DIFFERENT phone number than
44
+ * the one actually typed, with no error and no visible sign anything had
45
+ * been thrown away. `PhoneField` now keeps the full digit sequence — see its
46
+ * own module comment for how it is FORMATTED (first ten punctuated, any rest
47
+ * appended raw) and how `parsePhone` alone decides, at commit, whether the
48
+ * full sequence resolves to a real ten-digit number.
30
49
  */
31
50
  export declare function digitsOfPhoneInProgress(input: string): string;
32
51
  /**
33
- * `digitsOfPhoneInProgress`'s output → what the box shows WHILE TYPING —
34
- * progressively punctuated as each digit lands, not only once ten are
35
- * present. `formatPhone` above is the too-late-to-use-here sibling: it takes
36
- * the STORED ten-digit form and either renders it or hands back a non-ten-
37
- * digit value unchanged, which would show a partial number completely
38
- * unpunctuated until the last digit arrived.
52
+ * A digit sequence of any length → what the box shows WHILE TYPING —
53
+ * progressively punctuated as each of the first ten digits lands, with any
54
+ * digit past the tenth appended after a space, UNFORMATTED, rather than
55
+ * silently dropped (SG-909 review). `formatPhone` above is the too-late-to-
56
+ * use-here sibling: it takes the STORED ten-digit form and either renders it
57
+ * or hands back a non-ten-digit value unchanged, which would show a partial
58
+ * number completely unpunctuated until the last digit arrived, and would
59
+ * have nowhere to put an eleventh digit at all.
60
+ *
61
+ * The trailing digits past ten are shown, not hidden, specifically so a
62
+ * too-long paste or a stray extra keystroke stays visible for the person to
63
+ * see and correct — `parsePhone` will refuse anything that is not exactly
64
+ * ten digits (or eleven with a leading `1`) at commit, and a refusal against
65
+ * a box that visibly still has extra digits in it is legible; a refusal
66
+ * against a box that silently ate them is not.
39
67
  */
40
68
  export declare function formatPhonePartial(digits: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danxbot/ui",
3
- "version": "7.4.2",
3
+ "version": "7.5.0",
4
4
  "type": "module",
5
5
  "description": "Danxbot — a domain-agnostic React design system with motion as a first-class primitive.",
6
6
  "license": "MIT",