@tristan2828/ui-foundation 4.21.0 → 4.23.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/conventions/docs/add-an-entity.md +23 -0
- package/conventions/docs/timed-screens.md +21 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/audio-cues.d.ts +7 -0
- package/dist/lib/audio-cues.js +10 -3
- package/package.json +1 -1
|
@@ -642,3 +642,26 @@ soft one; copy it.
|
|
|
642
642
|
- **Hard-deleted:** `DELETE` removes the row. Give the entity no `delete`
|
|
643
643
|
in `mcp_entities.py`: a chat that read planted instructions would
|
|
644
644
|
destroy data that only a backup returns.
|
|
645
|
+
|
|
646
|
+
## Fields only a script writes
|
|
647
|
+
|
|
648
|
+
Some fields the API takes on create and update are for a script, not for
|
|
649
|
+
the people or chats using the app: an importer that runs again and again
|
|
650
|
+
stores each record's id in the source it came from, so a re-run finds the
|
|
651
|
+
record even after it's renamed. The API has to accept that id, for the
|
|
652
|
+
importer. A chat that changed or cleared it would break the matching, and
|
|
653
|
+
the next run would create the record a second time.
|
|
654
|
+
|
|
655
|
+
- *MCP:* list the field's wire name in the entity's `McpEntity`
|
|
656
|
+
`script_only` (`script_only=("sourceId",)`). Chats still read it
|
|
657
|
+
(`fetch` shows it), but `create` and `update` refuse a call that names
|
|
658
|
+
it, by its wire name or its snake_case one, before the entity's own
|
|
659
|
+
function runs: `Nothing was saved.` and `- sourceId: set by a script,
|
|
660
|
+
not through MCP`, with no audit row. It's left out of the two tools'
|
|
661
|
+
descriptions, so a chat isn't invited to send it. The backend refuses
|
|
662
|
+
to start if a name isn't a field of the create or update schema, or if
|
|
663
|
+
create requires it (a chat could then never create one).
|
|
664
|
+
- *Screens:* the form leaves the field out, for the same reason.
|
|
665
|
+
- Don't use it for a field the server works out itself (`computed`) or
|
|
666
|
+
one that's never on the request: those aren't in the create or update
|
|
667
|
+
schema to begin with.
|
|
@@ -10,6 +10,7 @@ package has the parts, so a screen like this is assembled, not rebuilt:
|
|
|
10
10
|
| `useSessionClock` | A clock that doesn't drift, with pause and resume: `idle`, `running`, `done` |
|
|
11
11
|
| `useWakeLock` | Keeps the screen on while a session runs (`SessionView`'s `keepAwake` uses it) |
|
|
12
12
|
| `unlockAudio`, `chime`, `sequence`, `say`, `hush`, `vibrate` | Sound cues with no audio files |
|
|
13
|
+
| `canVibrate` | Whether this browser can vibrate, to offer a Vibrate setting only where it works |
|
|
13
14
|
| `useMuted` | A mute toggle remembered on this device |
|
|
14
15
|
| `usePrefersReducedMotion` | Whether to swap movement for a fade |
|
|
15
16
|
|
|
@@ -95,6 +96,13 @@ Browsers block sound until the person has clicked or tapped. So:
|
|
|
95
96
|
line still being spoken. `hush()` stops it. Call `hush()` when the
|
|
96
97
|
session stops or the view closes.
|
|
97
98
|
- `vibrate(pattern)` only works on Android. Elsewhere it does nothing.
|
|
99
|
+
- **A Vibrate setting is offered only where `canVibrate()` is true.**
|
|
100
|
+
iPhone browsers can't vibrate, so a toggle there would set something
|
|
101
|
+
that does nothing. `canVibrate()` is the same test `vibrate()` makes, so
|
|
102
|
+
the two never disagree. Read it once, since it doesn't change during a
|
|
103
|
+
session: `const [vibrates] = useState(canVibrate)`. Show the toggle (a
|
|
104
|
+
pressed button named "Vibrate", like Mute) only when `vibrates` is
|
|
105
|
+
true, and check the setting before each `vibrate()` call.
|
|
98
106
|
- Every one of them is a no-op where the browser can't make the sound, so
|
|
99
107
|
none needs a check around it.
|
|
100
108
|
- **Mute** is the app's: `const [muted, setMuted] = useMuted('my-app:routine:muted')`,
|
|
@@ -159,18 +167,18 @@ test('running, paused and done', async ({ page }) => {
|
|
|
159
167
|
steps, so every reading picks up however long the machine took since the
|
|
160
168
|
last `fastForward`. A count-up with no end (a stopwatch, a hold that
|
|
161
169
|
ends on a tap) has nothing to stop at, so under load its readings are
|
|
162
|
-
off by a second or more. Pause the clock
|
|
163
|
-
move it only with `fastForward`:
|
|
170
|
+
off by a second or more. Pause the clock before the session starts, at a
|
|
171
|
+
time fixed from the install, and move it only with `fastForward`:
|
|
164
172
|
|
|
165
173
|
```ts
|
|
166
174
|
test('the hold counts up until the tap', async ({ page }) => {
|
|
167
|
-
|
|
175
|
+
const t0 = Date.now()
|
|
176
|
+
await page.clock.install({ time: t0 })
|
|
168
177
|
await page.goto('/routine')
|
|
178
|
+
// The page's clock runs no faster than real time, so a minute on is still
|
|
179
|
+
// ahead for the whole test timeout; nothing on the start screen is timed.
|
|
180
|
+
await page.clock.pauseAt(t0 + 60_000)
|
|
169
181
|
await page.getByRole('button', { name: 'Start' }).click()
|
|
170
|
-
// Slightly ahead, never Date.now() itself: the page's clock has already
|
|
171
|
-
// moved past the test's, and pauseAt() refuses an earlier time with
|
|
172
|
-
// "Cannot fast-forward to the past".
|
|
173
|
-
await page.clock.pauseAt(Date.now() + 50)
|
|
174
182
|
|
|
175
183
|
await page.clock.fastForward('00:42')
|
|
176
184
|
await expect(page.getByRole('timer', { name: 'Held for' })).toHaveText('0:42')
|
|
@@ -183,10 +191,12 @@ test('the hold counts up until the tap', async ({ page }) => {
|
|
|
183
191
|
})
|
|
184
192
|
```
|
|
185
193
|
|
|
186
|
-
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
194
|
+
- **Pause at a time fixed from `install`, before the session starts.**
|
|
195
|
+
Never at one read after it (`pauseAt(Date.now() + 50)` after Start):
|
|
196
|
+
until the pause lands the page's fake clock runs in real time, so a
|
|
197
|
+
slow round trip or a busy first frame puts it past that time, and
|
|
198
|
+
`pauseAt` fails with "Cannot fast-forward to the past". A time a minute
|
|
199
|
+
on from `install` is still ahead however slow the machine is.
|
|
190
200
|
- **`clock.resume()` before axe or closing the view.** With the clock
|
|
191
201
|
paused, axe's scan never finishes and the dialog's close animation
|
|
192
202
|
never ends, so the test times out with nothing pointing at the clock.
|
package/dist/index.d.ts
CHANGED
|
@@ -55,4 +55,4 @@ export { usePrefersReducedMotion } from './hooks/use-prefers-reduced-motion.js';
|
|
|
55
55
|
export { useMuted } from './hooks/use-muted.js';
|
|
56
56
|
export { createPushController, usePushPermission, type PushController, type PushControllerOptions, type PushEnvironment, type PushPermission, type PushStatus, } from './hooks/use-push-permission.js';
|
|
57
57
|
export { cn } from './lib/utils.js';
|
|
58
|
-
export { chime, hush, say, sequence, unlockAudio, vibrate, type ChimeOptions, type SayOptions, type SequenceOptions, } from './lib/audio-cues.js';
|
|
58
|
+
export { canVibrate, chime, hush, say, sequence, unlockAudio, vibrate, type ChimeOptions, type SayOptions, type SequenceOptions, } from './lib/audio-cues.js';
|
package/dist/index.js
CHANGED
|
@@ -61,4 +61,4 @@ export { useMuted } from './hooks/use-muted.js';
|
|
|
61
61
|
export { createPushController, usePushPermission, } from './hooks/use-push-permission.js';
|
|
62
62
|
// Utilities
|
|
63
63
|
export { cn } from './lib/utils.js';
|
|
64
|
-
export { chime, hush, say, sequence, unlockAudio, vibrate, } from './lib/audio-cues.js';
|
|
64
|
+
export { canVibrate, chime, hush, say, sequence, unlockAudio, vibrate, } from './lib/audio-cues.js';
|
package/dist/lib/audio-cues.d.ts
CHANGED
|
@@ -38,6 +38,13 @@ export type AudioCues = {
|
|
|
38
38
|
vibrate: (pattern: number | number[]) => void;
|
|
39
39
|
};
|
|
40
40
|
export declare function createAudioCues(env: AudioCuesEnvironment): AudioCues;
|
|
41
|
+
/**
|
|
42
|
+
* Whether this browser can vibrate: Android browsers can, iPhone browsers
|
|
43
|
+
* can't. The same test vibrate() makes, so a Vibrate setting offered only
|
|
44
|
+
* where this is true never sets something that does nothing. Read it once;
|
|
45
|
+
* it doesn't change during a session.
|
|
46
|
+
*/
|
|
47
|
+
export declare function canVibrate(): boolean;
|
|
41
48
|
/** Call from the click or tap that starts a session; sound is allowed from then on. */
|
|
42
49
|
export declare const unlockAudio: AudioCues['unlockAudio'];
|
|
43
50
|
/** A soft sine chime; dropped before unlockAudio() or where there's no Web Audio. */
|
package/dist/lib/audio-cues.js
CHANGED
|
@@ -109,6 +109,15 @@ export function createAudioCues(env) {
|
|
|
109
109
|
},
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Whether this browser can vibrate: Android browsers can, iPhone browsers
|
|
114
|
+
* can't. The same test vibrate() makes, so a Vibrate setting offered only
|
|
115
|
+
* where this is true never sets something that does nothing. Read it once;
|
|
116
|
+
* it doesn't change during a session.
|
|
117
|
+
*/
|
|
118
|
+
export function canVibrate() {
|
|
119
|
+
return typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function';
|
|
120
|
+
}
|
|
112
121
|
let shared = null;
|
|
113
122
|
function browserCues() {
|
|
114
123
|
if (shared)
|
|
@@ -118,9 +127,7 @@ function browserCues() {
|
|
|
118
127
|
AudioContext: win?.AudioContext ?? win?.webkitAudioContext,
|
|
119
128
|
speechSynthesis: win?.speechSynthesis,
|
|
120
129
|
SpeechSynthesisUtterance: win?.SpeechSynthesisUtterance,
|
|
121
|
-
vibrate:
|
|
122
|
-
? (pattern) => navigator.vibrate(pattern)
|
|
123
|
-
: undefined,
|
|
130
|
+
vibrate: canVibrate() ? (pattern) => navigator.vibrate(pattern) : undefined,
|
|
124
131
|
});
|
|
125
132
|
return shared;
|
|
126
133
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.23.0",
|
|
4
4
|
"description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|