@dloizides/auth-web 1.13.0 → 1.14.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/CHANGELOG.md CHANGED
@@ -1,6 +1,76 @@
1
1
  # Changelog
2
2
 
3
- ## 1.13.0 (2026-07-19)
3
+ ## 1.14.0 (2026-07-19)
4
+
5
+ Three defects found by measuring the DEPLOYED portals rather than reading the
6
+ source. All three had passing tests over them.
7
+
8
+ ### Fixed
9
+
10
+ - **The auth card no longer vertically centres, so expanding anything stops
11
+ moving the inputs.** Measured on `app.zygos.dloizides.com/login` at 1.13.0:
12
+ expanding the demo disclosure grew the card 445.6 → 589.6px and pulled the
13
+ username input from `y=275.2` to `y=203.2` and the password from `y=368.8` to
14
+ `y=296.8` — **72px UP each**, mid-interaction. `<DemoCredentialsHint>` was
15
+ deliberately mounted BELOW the form to avoid exactly this; centring defeated
16
+ the placement, because a centred card re-centres as it grows and drags
17
+ everything above the growth upward.
18
+
19
+ `useAuthScreenStyles.screen` and `useAuthStyles.screen` are now top-anchored
20
+ (`justifyContent: 'flex-start'` + a 64dp `paddingTop`) instead of centred.
21
+ ⚠️ **This is a visible change on every portal**: the login card now sits below
22
+ a fixed gap rather than floating in the middle of tall viewports.
23
+
24
+ Chosen over the alternatives because the defect is not specific to the
25
+ disclosure — ANY growth shifted the fields (a validation error under an input,
26
+ a passkey button arriving after the config fetch, an OTP tab swap), and only
27
+ top-anchoring fixes the whole class. Reserving the disclosure's height would
28
+ have left ~144px of permanent dead space on every login; absolutely
29
+ positioning the expanded panel would have removed it from the scrollable area,
30
+ making the credentials unreachable on a short viewport. Top-anchoring is also
31
+ strictly safer at small viewports than what it replaces: overflow now runs
32
+ downward only, where centring pushed the top of the card past the top edge
33
+ where no scroll could reach it.
34
+
35
+ - **Selection and expansion states are now announced on the web.** Three
36
+ components declared `accessibilityState` and announced nothing:
37
+ `<DemoCredentialsHint>` (`expanded`), `<DevicePinLengthPicker>` (`selected`)
38
+ and `<PreferredMethodSettingsCard>` (`selected`). Each now passes the ARIA
39
+ attribute **explicitly** (`aria-expanded` / `aria-checked`) alongside the RN
40
+ prop, which is kept for native.
41
+
42
+ Root cause is systemic, not a typo. Measured against react-native-web 0.21.2:
43
+ a `<TouchableOpacity>` given `accessibilityLabel`, `accessibilityRole`,
44
+ `accessibilityHint` and `accessibilityState={{ expanded: true, selected: true }}`
45
+ renders `<button role="button" aria-label="…">` — **no `aria-expanded`, no
46
+ `aria-selected`, and no trace of the hint at all**. `accessibilityState` is not
47
+ read by react-native-web; `accessibilityHint` produces zero DOM output.
48
+
49
+ For `<DevicePinLengthPicker>` the practical effect was WCAG 1.4.1: the chosen
50
+ PIN length was distinguishable only by the pill's fill colour. Its options also
51
+ move from `accessibilityRole="button"` to `"radio"` inside a `radiogroup`,
52
+ since `aria-checked` is only meaningful on a checkable role.
53
+
54
+ - **Field labels now speak the shared contract's `field` voice — 13/600, not
55
+ 14/600.** The forms contract (mirrored in `@dloizides/ui-forms`'
56
+ `labelVariants.ts`) defines exactly two label voices: `field` (13/600, sentence
57
+ case) and `control` (11/700, uppercase). `defaultAuthTheme.typography.label`
58
+ was 14, which is neither — drift, inherited uniformly by every portal, since
59
+ they all reuse `defaultAuthTheme.typography` verbatim. It read against the 13px
60
+ `Field` labels those same apps use on every non-auth form.
61
+
62
+ ### Tests
63
+
64
+ - **New DOM-level guards** (`*.dom.test.tsx`), rendered through the real
65
+ `react-native-web` in a `jsdom` environment, asserting the rendered
66
+ **attribute** rather than the React prop. This is the point: the pre-existing
67
+ suite mocked `react-native` down to host strings and asserted
68
+ `props.accessibilityState` — so it passed, unchanged, against a button that
69
+ announced nothing in every browser. A prop assertion cannot catch this class of
70
+ bug, and did not.
71
+ - `authVisualContract.test.ts` guards the two visual contracts by naming the
72
+ defect they forbid (`justifyContent` must NOT be `'center'`; labels must be
73
+ 13/600 written as a literal, not read back from the token under test).
4
74
 
5
75
  `<DemoCredentialsHint>` gets a **discreet** presentation, and it is now the
6
76
  default. The demo stops competing with the sign-in form on login pages that real
package/dist/index.d.mts CHANGED
@@ -62,7 +62,21 @@ interface AuthThemeTypography {
62
62
  title: number;
63
63
  /** Form subtitle. */
64
64
  subtitle: number;
65
- /** Field labels. */
65
+ /**
66
+ * Field labels.
67
+ *
68
+ * The shared forms contract ("The TWO legitimate label voices", mirrored in
69
+ * `@dloizides/ui-forms`' `labelVariants.ts`) defines exactly two: `field` —
70
+ * 13 / 600 / sentence case, a label that is READ — and `control` — 11 / 700 /
71
+ * uppercase, a label that is SCANNED. Auth field labels are prose addressed to
72
+ * the person signing in, so they are the `field` voice: **13**.
73
+ *
74
+ * Through 1.13.0 this default was 14, which is neither voice — drift. Every
75
+ * portal that renders `<LoginForm>` inherits this token (all of them reuse
76
+ * `defaultAuthTheme.typography` verbatim), so the drift was uniform across
77
+ * zygos, agora, erevna, katalogos and ichnos, and it read against the 13px
78
+ * `Field` labels those same apps use on every non-auth form.
79
+ */
66
80
  label: number;
67
81
  /** Input text and button text. */
68
82
  body: number;
@@ -1359,9 +1373,15 @@ declare const enum DemoCredentialsHintVariant {
1359
1373
  * the same AA-held token as every other piece of helper copy. Discretion is
1360
1374
  * carried by size, placement and the absence of chrome.
1361
1375
  * - **It does not hide from assistive tech.** The trigger is a real focusable
1362
- * button carrying `accessibilityState.expanded`, so a screen reader
1363
- * announces both that it exists and whether it is open. Visually quiet must
1364
- * not mean absent — a demo nobody can find is a demo nobody uses.
1376
+ * button carrying an explicit `aria-expanded` (plus `accessibilityState` for
1377
+ * native), so a screen reader announces both that it exists and whether it is
1378
+ * open. Visually quiet must not mean absent — a demo nobody can find is a demo
1379
+ * nobody uses.
1380
+ *
1381
+ * ⚠️ Through 1.13.0 this carried ONLY `accessibilityState={{ expanded }}` and
1382
+ * therefore announced nothing on web: react-native-web 0.21.2 does not read
1383
+ * `accessibilityState`, so the rendered button had no `aria-expanded` at all.
1384
+ * The old unit test asserted the React *prop*, so it passed against the bug.
1365
1385
  *
1366
1386
  * ## Layout stability
1367
1387
  *
package/dist/index.d.ts CHANGED
@@ -62,7 +62,21 @@ interface AuthThemeTypography {
62
62
  title: number;
63
63
  /** Form subtitle. */
64
64
  subtitle: number;
65
- /** Field labels. */
65
+ /**
66
+ * Field labels.
67
+ *
68
+ * The shared forms contract ("The TWO legitimate label voices", mirrored in
69
+ * `@dloizides/ui-forms`' `labelVariants.ts`) defines exactly two: `field` —
70
+ * 13 / 600 / sentence case, a label that is READ — and `control` — 11 / 700 /
71
+ * uppercase, a label that is SCANNED. Auth field labels are prose addressed to
72
+ * the person signing in, so they are the `field` voice: **13**.
73
+ *
74
+ * Through 1.13.0 this default was 14, which is neither voice — drift. Every
75
+ * portal that renders `<LoginForm>` inherits this token (all of them reuse
76
+ * `defaultAuthTheme.typography` verbatim), so the drift was uniform across
77
+ * zygos, agora, erevna, katalogos and ichnos, and it read against the 13px
78
+ * `Field` labels those same apps use on every non-auth form.
79
+ */
66
80
  label: number;
67
81
  /** Input text and button text. */
68
82
  body: number;
@@ -1359,9 +1373,15 @@ declare const enum DemoCredentialsHintVariant {
1359
1373
  * the same AA-held token as every other piece of helper copy. Discretion is
1360
1374
  * carried by size, placement and the absence of chrome.
1361
1375
  * - **It does not hide from assistive tech.** The trigger is a real focusable
1362
- * button carrying `accessibilityState.expanded`, so a screen reader
1363
- * announces both that it exists and whether it is open. Visually quiet must
1364
- * not mean absent — a demo nobody can find is a demo nobody uses.
1376
+ * button carrying an explicit `aria-expanded` (plus `accessibilityState` for
1377
+ * native), so a screen reader announces both that it exists and whether it is
1378
+ * open. Visually quiet must not mean absent — a demo nobody can find is a demo
1379
+ * nobody uses.
1380
+ *
1381
+ * ⚠️ Through 1.13.0 this carried ONLY `accessibilityState={{ expanded }}` and
1382
+ * therefore announced nothing on web: react-native-web 0.21.2 does not read
1383
+ * `accessibilityState`, so the rendered button had no `aria-expanded` at all.
1384
+ * The old unit test asserted the React *prop*, so it passed against the bug.
1365
1385
  *
1366
1386
  * ## Layout stability
1367
1387
  *
package/dist/index.js CHANGED
@@ -9,15 +9,18 @@ var authClient = require('@dloizides/auth-client');
9
9
  // src/components/AuthScreen.tsx
10
10
  var FULL_WIDTH = "100%";
11
11
  var CARD_BORDER_WIDTH = 1;
12
+ var CARD_TOP_OFFSET = 64;
12
13
  function useAuthScreenStyles(theme) {
13
14
  return react.useMemo(() => {
14
15
  const { colors, radii, spacing, typography } = theme;
15
16
  return reactNative.StyleSheet.create({
16
17
  screen: {
17
18
  flex: 1,
18
- justifyContent: "center",
19
+ // Deliberately NOT `justifyContent: 'center'` — see CARD_TOP_OFFSET.
20
+ justifyContent: "flex-start",
19
21
  alignItems: "center",
20
22
  padding: spacing.md,
23
+ paddingTop: CARD_TOP_OFFSET,
21
24
  backgroundColor: colors.background
22
25
  },
23
26
  card: {
@@ -75,7 +78,8 @@ var defaultAuthTheme = {
75
78
  typography: {
76
79
  title: 28,
77
80
  subtitle: 16,
78
- label: 14,
81
+ // 13, not 14 — the shared contract's `field` voice. See AuthThemeTypography.label.
82
+ label: 13,
79
83
  body: 16,
80
84
  caption: 12
81
85
  }
@@ -391,15 +395,21 @@ function withTestIdPrefix(baseId, prefix) {
391
395
  var FULL_WIDTH2 = "100%";
392
396
  var CARD_MAX_WIDTH = 420;
393
397
  var BUTTON_VERTICAL_PADDING = 14;
398
+ var SCREEN_TOP_OFFSET = 64;
394
399
  function useAuthStyles(theme) {
395
400
  return react.useMemo(() => {
396
401
  const { colors, radii, spacing, typography } = theme;
397
402
  return reactNative.StyleSheet.create({
398
403
  screen: {
399
404
  flex: 1,
400
- justifyContent: "center",
405
+ // Top-anchored, NOT centred — a centred card re-centres as it grows and
406
+ // drags the inputs up under the cursor. Same reasoning (and the same
407
+ // measurement) as `useAuthScreenStyles`' CARD_TOP_OFFSET; kept in step so
408
+ // a standalone `<LoginForm>` and an `<AuthScreen>`-composed one agree.
409
+ justifyContent: "flex-start",
401
410
  alignItems: "center",
402
411
  padding: spacing.md,
412
+ paddingTop: SCREEN_TOP_OFFSET,
403
413
  backgroundColor: colors.background
404
414
  },
405
415
  card: {
@@ -2013,14 +2023,15 @@ function DevicePinLengthPicker({
2013
2023
  const theme = useAuthTheme(themeProp);
2014
2024
  const dynamic = usePickerStyles(theme);
2015
2025
  const baseId = withTestIdPrefix(AuthTestIds.devicePinEnrollLength, testIdPrefix);
2016
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.row, children: DEVICE_PIN_ALLOWED_DIGITS.map((option) => {
2026
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { accessibilityRole: "radiogroup", role: "radiogroup", style: styles2.row, children: DEVICE_PIN_ALLOWED_DIGITS.map((option) => {
2017
2027
  const active = option === selected;
2018
2028
  return /* @__PURE__ */ jsxRuntime.jsx(
2019
2029
  reactNative.Pressable,
2020
2030
  {
2031
+ "aria-checked": active,
2021
2032
  accessibilityHint: interpolate(optionHint, { count: option }),
2022
2033
  accessibilityLabel: String(option),
2023
- accessibilityRole: "button",
2034
+ accessibilityRole: "radio",
2024
2035
  accessibilityState: { selected: active },
2025
2036
  disabled,
2026
2037
  style: [styles2.pill, dynamic.pill, active ? dynamic.active : null],
@@ -3164,6 +3175,7 @@ function PreferredMethodSettingsCard({
3164
3175
  return /* @__PURE__ */ jsxRuntime.jsx(
3165
3176
  reactNative.TouchableOpacity,
3166
3177
  {
3178
+ "aria-checked": isSelected,
3167
3179
  accessibilityHint: labels.description,
3168
3180
  accessibilityLabel: option.label,
3169
3181
  accessibilityRole: "radio",
@@ -3450,6 +3462,7 @@ function DemoCredentialsHint({
3450
3462
  /* @__PURE__ */ jsxRuntime.jsx(
3451
3463
  reactNative.TouchableOpacity,
3452
3464
  {
3465
+ "aria-expanded": expanded,
3453
3466
  accessibilityHint: labels.toggleHint,
3454
3467
  accessibilityLabel: labels.toggle,
3455
3468
  accessibilityRole: "button",