@glimstone/core 4.1.1 → 4.2.2

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/README.md CHANGED
@@ -15,9 +15,11 @@ npm install @glimstone/core
15
15
  onto Tailwind v4 utilities for an app that uses Tailwind.
16
16
  - `glyphs.json` and `glyphs.md` are the glyph assortment: which mark means
17
17
  what, where each comes from, and the sizing rules that make them one set.
18
- - The engines: `appearance` (shape, accent, rainbow, motion), `controls` (the
19
- label engine and its width stages) and `setStoragePrefix`, which keeps an
20
- app's own names for what it stores.
18
+ - The engines: `appearance` (shape, accent, rainbow, motion), `theme` (OLED
19
+ and whether dark is showing), `contrast` (the inks that hold 4.5:1),
20
+ `browserAccent` (the browser as a source for the system accent), `controls`
21
+ (the label engine and its width stages) and `setStoragePrefix`, which keeps
22
+ an app's own names for what it stores.
21
23
  - What a component shows, as functions: `buttonView`, `badgeClassName`, the
22
24
  select's list model, the Info tile's version links and update check, the
23
25
  README button's classes, where a bubble goes, a dialog's keys and the toast
@@ -26,6 +28,10 @@ npm install @glimstone/core
26
28
  say how a field looks and how wide it is, `pressRowControl` does what a click
27
29
  on a row's label does, `treeRows` and `treeStep` hold a tree's rows and keys, and
28
30
  `tableCellClass` says what a table's cell wears.
31
+ - The sign-in card's flow: `createSignInFlow` keeps what was typed and which
32
+ form shows while the app answers each step, `signInTextEn` holds the English
33
+ words, and `showRefusal` shakes the button of a refused try and selects the
34
+ refused field.
29
35
  - What windows and menus share: `windowFoot` adds the way out, `stackWindow`
30
36
  gives Escape and Tab to the window in front, and `menu.ts` holds the menu
31
37
  model with `tidyMenu`, `menuRowView`, `placeMenu`, `menuMove`,
@@ -1,3 +1,5 @@
1
+ import { contrastOn } from './contrast.ts';
2
+ export { contrastOn };
1
3
  /**
2
4
  * The corner shapes. Like the motion levels these strings are a wire format:
3
5
  * they go into `data-shape`, the stylesheet and storage, and what the user
@@ -108,15 +110,42 @@ export declare function stormTap(state: {
108
110
  taps: number;
109
111
  }, tapped: string, current: string): Motion | undefined;
110
112
  /**
111
- * Overrides the accent tokens, or clears the override so the theme's gold comes
112
- * back. The contrast colour is computed, so a light accent never gets white
113
- * text.
113
+ * The accent mode that takes its colour from the device: Material You on
114
+ * Android, the desktop's accent colour, or the browser's `AccentColor`. It is
115
+ * stored as this word, never as the colour, so a synced setting does not carry
116
+ * one device's wallpaper to the next.
117
+ */
118
+ export declare const SYSTEM_ACCENT = "system";
119
+ /**
120
+ * The colour an accent setting stands for: a hex as it is, the system mode as
121
+ * the colour the source reported, and undefined for the default, which is also
122
+ * what the system mode gives on a device without a source.
123
+ */
124
+ export declare function resolveAccent(accent: string | undefined, system: string | undefined): string | undefined;
125
+ /**
126
+ * Overrides the accent tokens with a hex or SYSTEM_ACCENT, or clears the
127
+ * override so the theme's gold comes back. The ink on the fill is computed, so
128
+ * a light accent never gets white text.
129
+ *
130
+ * `--accent-ink`, `--accent-glyph` and `--accent-hover` are derived in
131
+ * tokens.css with color-mix, so they also follow a later light or dark switch.
132
+ * Where that mix would fall short of 4.5:1 on a ramp, the ramp's ink is set
133
+ * here instead, as `--accent-ink-light` or `--accent-ink-dark`.
134
+ */
135
+ export declare function applyAccent(accent: string | undefined): void;
136
+ /**
137
+ * Feeds the colour the device reports, or undefined where it has none, and
138
+ * repaints when the accent is in the system mode. Call it again whenever the
139
+ * source changes; `watchBrowserAccent(setSystemAccent)` does that for the
140
+ * browser.
114
141
  *
115
- * `--accent-ink`, `--accent-glyph` and `--accent-hover` are not set here:
116
- * tokens.css derives them from --accent with color-mix, so they also follow a
117
- * later light or dark switch.
142
+ * The colour is remembered on this device alone, so the first frame after a
143
+ * reload is already in it.
118
144
  */
119
- export declare function applyAccent(hex: string | undefined): void;
145
+ export declare function setSystemAccent(hex: string | undefined): void;
146
+ /** The device's colour, or undefined where there is none, so the wand swatch stays hidden. */
147
+ export declare function systemAccent(): string | undefined;
148
+ export declare function subscribeSystemAccent(fn: () => void): () => void;
120
149
  /** The current snapshot, with a stable identity between changes. */
121
150
  export declare function rainbowState(): RainbowState;
122
151
  export declare function subscribeRainbow(fn: () => void): () => void;
@@ -169,8 +198,7 @@ export declare function rainbowFromSettings(s: {
169
198
  rainbowSeed?: number;
170
199
  rainbowPalette?: string[] | null;
171
200
  }): RainbowState;
172
- /** Black or white, whichever is readable on the given colour. */
173
- export declare function contrastOn(hex: string): string;
201
+ /** `accent` is the setting, so the system mode is cached as SYSTEM_ACCENT. */
174
202
  export declare function cacheAppearance(shape: string, accent: string, rainbow?: RainbowState): void;
175
203
  /** Applied at boot, before the app renders anything. */
176
204
  export declare function applyCachedAppearance(): void;
@@ -4,7 +4,11 @@
4
4
  // so every component picks it up through the tokens it already reads.
5
5
  //
6
6
  // It has no framework dependency; the React and Vue packages wrap it.
7
+ import { accentInks, contrastOn, validHex as valid } from './contrast.js';
7
8
  import { storageKey } from './storage.js';
9
+ import { applyOled, storedOled } from './theme.js';
10
+ // Also reachable through the @glimstone/core/appearance subpath.
11
+ export { contrastOn };
8
12
  /** The shapes a picker shows. */
9
13
  export const SHAPES = ['round', 'soft', 'square'];
10
14
  /**
@@ -140,26 +144,94 @@ export function stormTap(state, tapped, current) {
140
144
  * animations: `.glim-live`'s pulse keeps its stop at every level.
141
145
  */
142
146
  /**
143
- * Overrides the accent tokens, or clears the override so the theme's gold comes
144
- * back. The contrast colour is computed, so a light accent never gets white
145
- * text.
147
+ * The accent mode that takes its colour from the device: Material You on
148
+ * Android, the desktop's accent colour, or the browser's `AccentColor`. It is
149
+ * stored as this word, never as the colour, so a synced setting does not carry
150
+ * one device's wallpaper to the next.
151
+ */
152
+ export const SYSTEM_ACCENT = 'system';
153
+ /**
154
+ * The colour an accent setting stands for: a hex as it is, the system mode as
155
+ * the colour the source reported, and undefined for the default, which is also
156
+ * what the system mode gives on a device without a source.
157
+ */
158
+ export function resolveAccent(accent, system) {
159
+ if (accent === SYSTEM_ACCENT)
160
+ return valid(system) ? system : undefined;
161
+ return valid(accent) ? accent : undefined;
162
+ }
163
+ let accentSetting;
164
+ let systemColour;
165
+ const systemListeners = new Set();
166
+ /**
167
+ * Overrides the accent tokens with a hex or SYSTEM_ACCENT, or clears the
168
+ * override so the theme's gold comes back. The ink on the fill is computed, so
169
+ * a light accent never gets white text.
146
170
  *
147
- * `--accent-ink`, `--accent-glyph` and `--accent-hover` are not set here:
148
- * tokens.css derives them from --accent with color-mix, so they also follow a
149
- * later light or dark switch.
171
+ * `--accent-ink`, `--accent-glyph` and `--accent-hover` are derived in
172
+ * tokens.css with color-mix, so they also follow a later light or dark switch.
173
+ * Where that mix would fall short of 4.5:1 on a ramp, the ramp's ink is set
174
+ * here instead, as `--accent-ink-light` or `--accent-ink-dark`.
150
175
  */
151
- export function applyAccent(hex) {
176
+ export function applyAccent(accent) {
177
+ accentSetting = accent;
178
+ paintAccent(resolveAccent(accent, systemColour));
179
+ }
180
+ function paintAccent(hex) {
152
181
  const root = document.documentElement.style;
182
+ const inks = valid(hex) ? accentInks(hex) : { light: undefined, dark: undefined };
183
+ for (const [ramp, ink] of Object.entries(inks)) {
184
+ if (ink)
185
+ root.setProperty(`--accent-ink-${ramp}`, ink);
186
+ else
187
+ root.removeProperty(`--accent-ink-${ramp}`);
188
+ }
153
189
  if (!valid(hex)) {
154
190
  root.removeProperty('--accent');
155
191
  root.removeProperty('--accent-contrast');
156
192
  root.removeProperty('--accent-soft');
157
193
  return;
158
194
  }
159
- const { r, g, b } = parse(hex);
195
+ const n = parseInt(hex.slice(1), 16);
160
196
  root.setProperty('--accent', hex);
161
197
  root.setProperty('--accent-contrast', contrastOn(hex));
162
- root.setProperty('--accent-soft', `rgba(${r}, ${g}, ${b}, 0.14)`);
198
+ root.setProperty('--accent-soft', `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, 0.14)`);
199
+ }
200
+ /**
201
+ * Feeds the colour the device reports, or undefined where it has none, and
202
+ * repaints when the accent is in the system mode. Call it again whenever the
203
+ * source changes; `watchBrowserAccent(setSystemAccent)` does that for the
204
+ * browser.
205
+ *
206
+ * The colour is remembered on this device alone, so the first frame after a
207
+ * reload is already in it.
208
+ */
209
+ export function setSystemAccent(hex) {
210
+ const next = valid(hex) ? hex : undefined;
211
+ if (next === systemColour)
212
+ return;
213
+ systemColour = next;
214
+ try {
215
+ if (next)
216
+ localStorage.setItem(storageKey('system-accent'), next);
217
+ else
218
+ localStorage.removeItem(storageKey('system-accent'));
219
+ }
220
+ catch {
221
+ // Without storage the default shows until the source answers.
222
+ }
223
+ if (accentSetting === SYSTEM_ACCENT)
224
+ paintAccent(next);
225
+ for (const fn of systemListeners)
226
+ fn();
227
+ }
228
+ /** The device's colour, or undefined where there is none, so the wand swatch stays hidden. */
229
+ export function systemAccent() {
230
+ return systemColour;
231
+ }
232
+ export function subscribeSystemAccent(fn) {
233
+ systemListeners.add(fn);
234
+ return () => systemListeners.delete(fn);
163
235
  }
164
236
  // The rainbow state belongs to the document, not a component: the sidebar and a
165
237
  // list far away in the tree must agree on which colour position three is, so
@@ -285,32 +357,7 @@ function usablePalette(p) {
285
357
  return RAINBOW;
286
358
  return p;
287
359
  }
288
- /** Black or white, whichever is readable on the given colour. */
289
- export function contrastOn(hex) {
290
- if (!valid(hex))
291
- return '#FFFFFF';
292
- const { r, g, b } = parse(hex);
293
- // Carbon's own ink: on a yellow accent a brown-tinted black reads as a smudge.
294
- return luminance(r, g, b) > 0.55 ? '#161616' : '#FFFFFF';
295
- }
296
- function valid(hex) {
297
- return !!hex && /^#[0-9a-fA-F]{6}$/.test(hex);
298
- }
299
- function parse(hex) {
300
- const n = parseInt(hex.slice(1), 16);
301
- return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
302
- }
303
- /**
304
- * Relative luminance, used to pick black or white on top. The channels are
305
- * linearised first because raw sRGB overstates blue and understates green.
306
- */
307
- function luminance(r, g, b) {
308
- const lin = (c) => {
309
- const v = c / 255;
310
- return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
311
- };
312
- return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
313
- }
360
+ /** `accent` is the setting, so the system mode is cached as SYSTEM_ACCENT. */
314
361
  export function cacheAppearance(shape, accent, rainbow) {
315
362
  try {
316
363
  // The conditional spread omits the key under exactOptionalPropertyTypes
@@ -324,7 +371,11 @@ export function cacheAppearance(shape, accent, rainbow) {
324
371
  }
325
372
  /** Applied at boot, before the app renders anything. */
326
373
  export function applyCachedAppearance() {
374
+ applyOled(storedOled());
327
375
  try {
376
+ const system = localStorage.getItem(storageKey('system-accent')) ?? undefined;
377
+ if (valid(system))
378
+ systemColour = system;
328
379
  const raw = localStorage.getItem(storageKey('appearance'));
329
380
  if (!raw) {
330
381
  applyShape(DEFAULT_SHAPE);
@@ -0,0 +1,12 @@
1
+ /** Whether `AccentColor` carries the system's colour in this browser. */
2
+ export declare function browserAccentWorks(): boolean;
3
+ /** The system's accent as the browser reports it, or undefined where it is no source. */
4
+ export declare function browserAccent(): string | undefined;
5
+ /** A computed `rgb()` or `rgba()` colour as `#RRGGBB`, or undefined for anything else. */
6
+ export declare function cssColourToHex(colour: string): string | undefined;
7
+ /**
8
+ * Calls `onChange` with the browser's accent now and whenever it has changed
9
+ * on the window's return, so `watchBrowserAccent(setSystemAccent)` keeps the
10
+ * system mode current. Returns the function that stops watching.
11
+ */
12
+ export declare function watchBrowserAccent(onChange: (hex: string | undefined) => void): () => void;
@@ -0,0 +1,66 @@
1
+ // The browser as a source for the system accent: the CSS system colour
2
+ // `AccentColor`, read off a probe element. An app inside a native shell feeds
3
+ // setSystemAccent from the shell instead (Material You on Android, the desktop
4
+ // accent in a desktop shell); this is for the plain web.
5
+ //
6
+ // Limits, all of them the browser's:
7
+ // · Firefox and Safari resolve AccentColor to the system's colour. Chromium
8
+ // resolves it to a fixed colour of its own, whatever the system uses, so
9
+ // Chrome, Edge, Opera and every other Chromium browser count as no source.
10
+ // · Where the system has no accent of its own, as on Linux without a theme
11
+ // that names one or on iOS, the browser answers with its default.
12
+ // · Firefox with resistFingerprinting answers with a fixed colour too, and
13
+ // nothing on the page can tell.
14
+ // · No event fires when the system's accent changes, so the colour is read
15
+ // again when the window comes back to the front, which is when somebody
16
+ // returns from the system's settings.
17
+ /** Whether `AccentColor` carries the system's colour in this browser. */
18
+ export function browserAccentWorks() {
19
+ if (typeof CSS === 'undefined' || !CSS.supports('color', 'AccentColor'))
20
+ return false;
21
+ // No word boundary, so a headless Chrome's `HeadlessChrome/` counts as well.
22
+ return !/Chrom(e|ium)\//.test(navigator.userAgent);
23
+ }
24
+ /** The system's accent as the browser reports it, or undefined where it is no source. */
25
+ export function browserAccent() {
26
+ if (!browserAccentWorks())
27
+ return undefined;
28
+ const probe = document.createElement('span');
29
+ probe.style.color = 'AccentColor';
30
+ probe.style.display = 'none';
31
+ document.documentElement.append(probe);
32
+ const colour = getComputedStyle(probe).color;
33
+ probe.remove();
34
+ return cssColourToHex(colour);
35
+ }
36
+ /** A computed `rgb()` or `rgba()` colour as `#RRGGBB`, or undefined for anything else. */
37
+ export function cssColourToHex(colour) {
38
+ const m = colour.match(/^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
39
+ if (!m)
40
+ return undefined;
41
+ return '#' + m.slice(1, 4).map((v) => Number(v).toString(16).padStart(2, '0')).join('').toUpperCase();
42
+ }
43
+ /**
44
+ * Calls `onChange` with the browser's accent now and whenever it has changed
45
+ * on the window's return, so `watchBrowserAccent(setSystemAccent)` keeps the
46
+ * system mode current. Returns the function that stops watching.
47
+ */
48
+ export function watchBrowserAccent(onChange) {
49
+ let last = browserAccent();
50
+ onChange(last);
51
+ const check = () => {
52
+ if (document.visibilityState === 'hidden')
53
+ return;
54
+ const next = browserAccent();
55
+ if (next === last)
56
+ return;
57
+ last = next;
58
+ onChange(next);
59
+ };
60
+ addEventListener('focus', check);
61
+ document.addEventListener('visibilitychange', check);
62
+ return () => {
63
+ removeEventListener('focus', check);
64
+ document.removeEventListener('visibilitychange', check);
65
+ };
66
+ }
@@ -0,0 +1,26 @@
1
+ /** The ratio body text needs, and the floor of every ink computed here. */
2
+ export declare const MIN_TEXT_CONTRAST = 4.5;
3
+ export declare function validHex(hex: string | undefined): hex is string;
4
+ /** The WCAG contrast ratio of two `#rrggbb` colours, from 1 to 21. */
5
+ export declare function contrastRatio(a: string, b: string): number;
6
+ /**
7
+ * The ink for text or a glyph on a fill of the given colour: white or Carbon's
8
+ * near-black, whichever reads better. On the mid-tones where neither reaches
9
+ * 4.5:1, the near-black goes on toward black until it does, which it always
10
+ * can.
11
+ */
12
+ export declare function contrastOn(hex: string): string;
13
+ /**
14
+ * The accent used as ink on each ramp, where the theme's own derivation in
15
+ * tokens.css falls short of 4.5:1 on its card. Undefined for a ramp means the
16
+ * stylesheet's mix holds and nothing needs setting, which is the case for
17
+ * every preset.
18
+ *
19
+ * The light ink starts from the stylesheet's mix toward black and goes on
20
+ * toward black; the dark ink starts from the accent itself and goes toward
21
+ * white.
22
+ */
23
+ export declare function accentInks(hex: string): {
24
+ light: string | undefined;
25
+ dark: string | undefined;
26
+ };
@@ -0,0 +1,107 @@
1
+ // The contrast arithmetic of WCAG 2, and the inks the colour engine computes
2
+ // with it. Every ink it hands out reaches 4.5:1, the minimum for body text,
3
+ // whatever colour somebody picked or the system supplied.
4
+ /** The ratio body text needs, and the floor of every ink computed here. */
5
+ export const MIN_TEXT_CONTRAST = 4.5;
6
+ // Carbon's own ink: on a yellow accent a brown-tinted black reads as a smudge.
7
+ const DARK_INK = '#161616';
8
+ const WHITE = '#FFFFFF';
9
+ const BLACK = '#000000';
10
+ // The card surface of each ramp in tokens.css, where accent text mostly sits.
11
+ // The presets were measured against these, and OLED's darker card only adds
12
+ // to the dark figure, so the dark ink holds there as well.
13
+ const LIGHT_CARD = '#FFFFFF';
14
+ const DARK_CARD = '#262626';
15
+ // --ink-mix of the light ramp in tokens.css.
16
+ const LIGHT_INK_MIX = 0.55;
17
+ export function validHex(hex) {
18
+ return !!hex && /^#[0-9a-fA-F]{6}$/.test(hex);
19
+ }
20
+ function rgb(hex) {
21
+ const n = parseInt(hex.slice(1), 16);
22
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
23
+ }
24
+ function hexOf(c) {
25
+ return '#' + c.map((v) => Math.round(v).toString(16).padStart(2, '0')).join('').toUpperCase();
26
+ }
27
+ /** `amount` of `to` in `from`, the way color-mix() in srgb mixes. */
28
+ function mix(from, to, amount) {
29
+ return [0, 1, 2].map((i) => from[i] + (to[i] - from[i]) * amount);
30
+ }
31
+ /**
32
+ * Relative luminance. The channels are linearised first because raw sRGB
33
+ * overstates blue and understates green.
34
+ */
35
+ function luminance(c) {
36
+ const lin = (v) => {
37
+ const s = v / 255;
38
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
39
+ };
40
+ return 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);
41
+ }
42
+ function ratio(a, b) {
43
+ const x = luminance(a);
44
+ const y = luminance(b);
45
+ return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
46
+ }
47
+ /** The WCAG contrast ratio of two `#rrggbb` colours, from 1 to 21. */
48
+ export function contrastRatio(a, b) {
49
+ return ratio(rgb(a), rgb(b));
50
+ }
51
+ /**
52
+ * `from` moved toward `to` just far enough to reach the minimum against
53
+ * `ground`, as a hex colour. The ratio only grows on the way, as long as `to`
54
+ * lies on the far side of `from` from the ground, so halving finds the least
55
+ * amount that does it.
56
+ */
57
+ function pushed(from, to, ground) {
58
+ const reaches = (t) => ratio(mix(from, to, t).map(Math.round), ground) >= MIN_TEXT_CONTRAST;
59
+ let lo = 0;
60
+ let hi = 1;
61
+ for (let i = 0; i < 16; i++) {
62
+ const mid = (lo + hi) / 2;
63
+ if (reaches(mid))
64
+ hi = mid;
65
+ else
66
+ lo = mid;
67
+ }
68
+ return hexOf(mix(from, to, hi));
69
+ }
70
+ /**
71
+ * The ink for text or a glyph on a fill of the given colour: white or Carbon's
72
+ * near-black, whichever reads better. On the mid-tones where neither reaches
73
+ * 4.5:1, the near-black goes on toward black until it does, which it always
74
+ * can.
75
+ */
76
+ export function contrastOn(hex) {
77
+ if (!validHex(hex))
78
+ return WHITE;
79
+ const fill = rgb(hex);
80
+ const onWhite = ratio(fill, rgb(WHITE));
81
+ const onDark = ratio(fill, rgb(DARK_INK));
82
+ if (onWhite > onDark && onWhite >= MIN_TEXT_CONTRAST)
83
+ return WHITE;
84
+ if (onDark >= MIN_TEXT_CONTRAST)
85
+ return DARK_INK;
86
+ return pushed(rgb(DARK_INK), rgb(BLACK), fill);
87
+ }
88
+ /**
89
+ * The accent used as ink on each ramp, where the theme's own derivation in
90
+ * tokens.css falls short of 4.5:1 on its card. Undefined for a ramp means the
91
+ * stylesheet's mix holds and nothing needs setting, which is the case for
92
+ * every preset.
93
+ *
94
+ * The light ink starts from the stylesheet's mix toward black and goes on
95
+ * toward black; the dark ink starts from the accent itself and goes toward
96
+ * white.
97
+ */
98
+ export function accentInks(hex) {
99
+ if (!validHex(hex))
100
+ return { light: undefined, dark: undefined };
101
+ const accent = rgb(hex);
102
+ const light = mix(accent, rgb(BLACK), 1 - LIGHT_INK_MIX);
103
+ return {
104
+ light: ratio(light, rgb(LIGHT_CARD)) >= MIN_TEXT_CONTRAST ? undefined : pushed(light, rgb(BLACK), rgb(LIGHT_CARD)),
105
+ dark: ratio(accent, rgb(DARK_CARD)) >= MIN_TEXT_CONTRAST ? undefined : pushed(accent, rgb(WHITE), rgb(DARK_CARD)),
106
+ };
107
+ }
package/dist/disco.js CHANGED
@@ -10,7 +10,8 @@
10
10
  // hidden mode somebody found on purpose is not a default anybody inherited.
11
11
  // The glide itself follows the motion engine: with reduced motion, or at the
12
12
  // "off" level, disco steps one colour at a time instead.
13
- import { applyRainbow, contrastOn, rainbowState, RAINBOW } from './appearance.js';
13
+ import { applyRainbow, rainbowState, RAINBOW } from './appearance.js';
14
+ import { contrastOn } from './contrast.js';
14
15
  import { buildLoop, colourAt } from './discoLoop.js';
15
16
  /** The walk covers one palette colour's worth of loop every 2.4 seconds, so a
16
17
  * full turn of eight colours takes 19.2 seconds. Stepping, it moves one
package/dist/index.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  export * from "./appearance.ts";
2
2
  export * from "./appMarks.ts";
3
3
  export * from "./badge.ts";
4
+ export * from "./browserAccent.ts";
4
5
  export * from "./bubblePosition.ts";
5
6
  export * from "./button.ts";
6
7
  export * from "./clipboard.ts";
7
8
  export * from "./colorNames.ts";
8
9
  export * from "./colorPicker.ts";
10
+ export * from "./contrast.ts";
9
11
  export * from "./controls.ts";
10
12
  export * from "./dialog.ts";
11
13
  export * from "./disco.ts";
@@ -30,9 +32,11 @@ export * from "./selector.ts";
30
32
  export * from "./selectModel.ts";
31
33
  export * from "./selectScroll.ts";
32
34
  export * from "./sidebarRow.ts";
35
+ export * from "./signIn.ts";
33
36
  export * from "./storage.ts";
34
37
  export * from "./tipBubble.ts";
35
38
  export * from "./table.ts";
39
+ export * from "./theme.ts";
36
40
  export * from "./toastEngine.ts";
37
41
  export * from "./tooltip.ts";
38
42
  export * from "./tree.ts";
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  export * from "./appearance.js";
2
2
  export * from "./appMarks.js";
3
3
  export * from "./badge.js";
4
+ export * from "./browserAccent.js";
4
5
  export * from "./bubblePosition.js";
5
6
  export * from "./button.js";
6
7
  export * from "./clipboard.js";
7
8
  export * from "./colorNames.js";
8
9
  export * from "./colorPicker.js";
10
+ export * from "./contrast.js";
9
11
  export * from "./controls.js";
10
12
  export * from "./dialog.js";
11
13
  export * from "./disco.js";
@@ -30,9 +32,11 @@ export * from "./selector.js";
30
32
  export * from "./selectModel.js";
31
33
  export * from "./selectScroll.js";
32
34
  export * from "./sidebarRow.js";
35
+ export * from "./signIn.js";
33
36
  export * from "./storage.js";
34
37
  export * from "./tipBubble.js";
35
38
  export * from "./table.js";
39
+ export * from "./theme.js";
36
40
  export * from "./toastEngine.js";
37
41
  export * from "./tooltip.js";
38
42
  export * from "./tree.js";
@@ -0,0 +1,136 @@
1
+ /** The card's words, translated by the app. */
2
+ export interface SignInText {
3
+ password: string;
4
+ signIn: string;
5
+ /** The bubble on Sign in while it works. */
6
+ signingIn: string;
7
+ passkey: string;
8
+ /** "Forgot password?", with the question mark. */
9
+ forgot: string;
10
+ /** The label of the second step's code field. */
11
+ code: string;
12
+ /** The line under the code field, such as where the code comes from. */
13
+ codeHint: string;
14
+ /** Said when the app refuses a code without a reason of its own. */
15
+ codeInvalid: string;
16
+ back: string;
17
+ /** The label of the reset form's code field. */
18
+ resetCode: string;
19
+ /** The (i) beside it: where the log is, how long the code lasts, what a reset removes. */
20
+ resetCodeTip: string;
21
+ newPassword: string;
22
+ confirmPassword: string;
23
+ setPassword: string;
24
+ /** The bubble on Set password while it works. */
25
+ saving: string;
26
+ mismatch: string;
27
+ /** Says how many characters a new password needs, with the number filled in. */
28
+ tooShort: string;
29
+ /** Said when the app refuses the code from the log. */
30
+ resetRejected: string;
31
+ /** Said when a callback throws or answers without a reason. */
32
+ failed: string;
33
+ /** The reveal eye's name while the secret is hidden, and once it shows. */
34
+ show: string;
35
+ hide: string;
36
+ }
37
+ /** English words, for an app in English and for the demos. */
38
+ export declare const signInTextEn: SignInText;
39
+ /**
40
+ * What the app answers to a sign-in or a passkey. `ok` means the card's part is
41
+ * done: the app signs in or shows a step of its own. `needCode` asks for the
42
+ * second step, and `error` is said above Sign in.
43
+ */
44
+ export interface SignInAnswer {
45
+ ok?: boolean;
46
+ needCode?: boolean;
47
+ error?: string;
48
+ }
49
+ /** What the app answers to asking for a reset code or to setting the password. `rejected` means the code from the log was refused. */
50
+ export interface ResetAnswer {
51
+ ok?: boolean;
52
+ rejected?: boolean;
53
+ error?: string;
54
+ }
55
+ export interface SignInHandlers {
56
+ /** `code` is set once the app asked for one, and `passkey` when a passkey started the sign-in. */
57
+ onSignIn: (input: {
58
+ password: string;
59
+ code?: string;
60
+ passkey: boolean;
61
+ }) => Promise<SignInAnswer>;
62
+ /** An empty answer leaves the card as it was, for a prompt the person closed. */
63
+ onPasskey?: () => Promise<SignInAnswer>;
64
+ /** Writes a code to the log. The reset form shows only once this answers ok. */
65
+ onAskCode?: () => Promise<ResetAnswer>;
66
+ onSetPassword?: (input: {
67
+ code: string;
68
+ password: string;
69
+ }) => Promise<ResetAnswer>;
70
+ }
71
+ export type SignInField = "password" | "code" | "resetCode" | "newPassword" | "confirm";
72
+ /** The card's buttons, by their `data-signin` hook. */
73
+ export type SignInButton = "submit" | "set-password" | "passkey" | "forgot";
74
+ /**
75
+ * The last refused step: the button that shakes and the field that takes the
76
+ * focus with its content selected. `count` grows with every refusal, so the
77
+ * same refusal twice still shakes twice.
78
+ */
79
+ export interface SignInRefusal {
80
+ button: SignInButton;
81
+ field: SignInField | null;
82
+ count: number;
83
+ }
84
+ export interface SignInState {
85
+ view: "signIn" | "reset";
86
+ password: string;
87
+ code: string;
88
+ needCode: boolean;
89
+ /** A passkey started this sign-in, so the code goes without the password. */
90
+ viaPasskey: boolean;
91
+ resetCode: string;
92
+ newPassword: string;
93
+ confirm: string;
94
+ error: string | null;
95
+ refusal: SignInRefusal | null;
96
+ busy: "signIn" | "passkey" | "askCode" | "setPassword" | null;
97
+ }
98
+ export interface SignInOptions extends SignInHandlers {
99
+ text: SignInText;
100
+ /** The shortest new password the reset form takes. */
101
+ minPassword: number;
102
+ }
103
+ export interface SignInFlow {
104
+ state(): SignInState;
105
+ subscribe(listener: () => void): () => void;
106
+ set(field: SignInField, value: string): void;
107
+ submit(): Promise<void>;
108
+ passkey(): Promise<void>;
109
+ forgot(): Promise<void>;
110
+ back(): void;
111
+ }
112
+ /**
113
+ * The flow behind one card. `options` is read at every step, so a framework
114
+ * hands in its current props rather than the ones the card was made with.
115
+ */
116
+ export declare function createSignInFlow(options: () => SignInOptions): SignInFlow;
117
+ /** Sign in waits for the password, and in the second step for the code. */
118
+ export declare function signInBlocked(s: SignInState): boolean;
119
+ /** Set password waits for the code from the log and a new password. */
120
+ export declare function resetBlocked(s: SignInState): boolean;
121
+ /**
122
+ * Which form shows, as a key that changes whenever the focus should move to
123
+ * the first empty field of the new form.
124
+ */
125
+ export declare function signInStage(s: SignInState): string;
126
+ /**
127
+ * Puts the focus in the first empty field inside `root`, or in its first field
128
+ * when all hold something, such as a name the app filled in.
129
+ */
130
+ export declare function focusFirstField(root: HTMLElement | null): void;
131
+ /**
132
+ * Answers a refusal on screen: the button shakes once (`glim-shake`, which the
133
+ * motion engine stills) and the field takes the focus with its content
134
+ * selected, so the next try starts by typing. Returns the shake's cleanup.
135
+ */
136
+ export declare function showRefusal(root: HTMLElement | null, refusal: SignInRefusal): () => void;