@fuzdev/fuz_app 0.113.0 → 0.115.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.
Files changed (129) hide show
  1. package/dist/actions/CLAUDE.md +46 -20
  2. package/dist/actions/action_dispatcher.d.ts +1 -1
  3. package/dist/actions/action_dispatcher.d.ts.map +1 -1
  4. package/dist/actions/action_dispatcher.js +22 -16
  5. package/dist/actions/action_event.d.ts +18 -2
  6. package/dist/actions/action_event.d.ts.map +1 -1
  7. package/dist/actions/action_event.js +63 -19
  8. package/dist/actions/action_event_types.d.ts +22 -0
  9. package/dist/actions/action_event_types.d.ts.map +1 -1
  10. package/dist/actions/action_event_types.js +19 -0
  11. package/dist/actions/compile_action_registry.d.ts +4 -2
  12. package/dist/actions/compile_action_registry.d.ts.map +1 -1
  13. package/dist/actions/compile_action_registry.js +8 -3
  14. package/dist/actions/frontend_rpc_client.d.ts +6 -0
  15. package/dist/actions/frontend_rpc_client.d.ts.map +1 -1
  16. package/dist/actions/frontend_rpc_client.js +2 -1
  17. package/dist/actions/heartbeat.d.ts +8 -4
  18. package/dist/actions/heartbeat.d.ts.map +1 -1
  19. package/dist/actions/heartbeat.js +8 -4
  20. package/dist/actions/peer_ping.d.ts.map +1 -1
  21. package/dist/actions/peer_ping.js +3 -1
  22. package/dist/actions/perform_action.d.ts +1 -1
  23. package/dist/actions/perform_action.d.ts.map +1 -1
  24. package/dist/actions/perform_action.js +6 -4
  25. package/dist/actions/register_action_ws.d.ts +13 -0
  26. package/dist/actions/register_action_ws.d.ts.map +1 -1
  27. package/dist/actions/register_action_ws.js +23 -2
  28. package/dist/actions/rpc_client.d.ts +1 -1
  29. package/dist/actions/rpc_client.d.ts.map +1 -1
  30. package/dist/actions/rpc_client.js +2 -1
  31. package/dist/actions/socket.svelte.d.ts +53 -10
  32. package/dist/actions/socket.svelte.d.ts.map +1 -1
  33. package/dist/actions/socket.svelte.js +84 -23
  34. package/dist/actions/transports.d.ts +24 -0
  35. package/dist/actions/transports.d.ts.map +1 -1
  36. package/dist/actions/transports.js +30 -0
  37. package/dist/actions/transports_http.d.ts +11 -4
  38. package/dist/actions/transports_http.d.ts.map +1 -1
  39. package/dist/actions/transports_http.js +63 -18
  40. package/dist/actions/transports_ws.d.ts +28 -3
  41. package/dist/actions/transports_ws.d.ts.map +1 -1
  42. package/dist/actions/transports_ws.js +33 -4
  43. package/dist/actions/ws_endpoint_spec.d.ts +7 -0
  44. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  45. package/dist/auth/CLAUDE.md +52 -8
  46. package/dist/auth/admin_actions.d.ts.map +1 -1
  47. package/dist/auth/admin_actions.js +4 -3
  48. package/dist/auth/audit_log_schema.d.ts +3 -1
  49. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  50. package/dist/auth/audit_log_schema.js +13 -3
  51. package/dist/auth/cell_action_specs.js +1 -1
  52. package/dist/auth/cell_actions.d.ts.map +1 -1
  53. package/dist/auth/cell_actions.js +33 -4
  54. package/dist/auth/cell_authorize.d.ts +3 -0
  55. package/dist/auth/cell_authorize.d.ts.map +1 -1
  56. package/dist/auth/cell_authorize.js +7 -4
  57. package/dist/auth/request_context.d.ts +12 -3
  58. package/dist/auth/request_context.d.ts.map +1 -1
  59. package/dist/auth/request_context.js +12 -3
  60. package/dist/auth/role_grant_offer_action_specs.d.ts +30 -5
  61. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  62. package/dist/auth/role_grant_offer_action_specs.js +53 -17
  63. package/dist/auth/role_grant_offer_actions.d.ts +23 -12
  64. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  65. package/dist/auth/role_grant_offer_actions.js +132 -38
  66. package/dist/auth/role_grant_offer_queries.d.ts +36 -0
  67. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  68. package/dist/auth/role_grant_offer_queries.js +76 -0
  69. package/dist/auth/role_schema.d.ts +12 -0
  70. package/dist/auth/role_schema.d.ts.map +1 -1
  71. package/dist/auth/role_schema.js +12 -0
  72. package/dist/auth/self_service_role_actions.d.ts +14 -0
  73. package/dist/auth/self_service_role_actions.d.ts.map +1 -1
  74. package/dist/auth/self_service_role_actions.js +25 -1
  75. package/dist/http/CLAUDE.md +7 -3
  76. package/dist/http/jsonrpc_helpers.d.ts +0 -5
  77. package/dist/http/jsonrpc_helpers.d.ts.map +1 -1
  78. package/dist/http/jsonrpc_helpers.js +0 -13
  79. package/dist/http/schema_helpers.d.ts +56 -0
  80. package/dist/http/schema_helpers.d.ts.map +1 -1
  81. package/dist/http/schema_helpers.js +91 -0
  82. package/dist/server/app_server.d.ts.map +1 -1
  83. package/dist/server/app_server.js +1 -0
  84. package/dist/server/serve_fact_route.d.ts +3 -1
  85. package/dist/server/serve_fact_route.d.ts.map +1 -1
  86. package/dist/server/serve_fact_route.js +7 -4
  87. package/dist/testing/CLAUDE.md +29 -16
  88. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  89. package/dist/testing/cross_backend/cell_gated_create.js +23 -1
  90. package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
  91. package/dist/testing/cross_backend/peer_ping_ws.js +18 -0
  92. package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
  93. package/dist/testing/cross_backend/role_grant_participation.js +34 -0
  94. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +12 -1
  95. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  96. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +13 -1
  97. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
  98. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +9 -7
  99. package/dist/testing/cross_backend/ws_round_trip.d.ts +11 -3
  100. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  101. package/dist/testing/cross_backend/ws_round_trip.js +74 -12
  102. package/dist/testing/entities.d.ts +6 -3
  103. package/dist/testing/entities.d.ts.map +1 -1
  104. package/dist/testing/entities.js +4 -1
  105. package/dist/testing/transports/ws_client.d.ts +6 -0
  106. package/dist/testing/transports/ws_client.d.ts.map +1 -1
  107. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  108. package/dist/testing/transports/ws_transport.js +6 -1
  109. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  110. package/dist/testing/ws_round_trip.js +5 -0
  111. package/dist/ui/BootstrapForm.svelte +14 -10
  112. package/dist/ui/BootstrapForm.svelte.d.ts.map +1 -1
  113. package/dist/ui/CLAUDE.md +19 -7
  114. package/dist/ui/LoginForm.svelte +8 -4
  115. package/dist/ui/LoginForm.svelte.d.ts.map +1 -1
  116. package/dist/ui/RoleGrantOfferForm.svelte +29 -14
  117. package/dist/ui/RoleGrantOfferForm.svelte.d.ts +2 -3
  118. package/dist/ui/RoleGrantOfferForm.svelte.d.ts.map +1 -1
  119. package/dist/ui/SignupForm.svelte +14 -13
  120. package/dist/ui/form_state.svelte.d.ts +24 -11
  121. package/dist/ui/form_state.svelte.d.ts.map +1 -1
  122. package/dist/ui/form_state.svelte.js +38 -10
  123. package/dist/ui/role_grant_offers_state.svelte.d.ts +25 -15
  124. package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -1
  125. package/dist/ui/role_grant_offers_state.svelte.js +7 -1
  126. package/package.json +2 -22
  127. package/dist/actions/request_tracker.svelte.d.ts +0 -77
  128. package/dist/actions/request_tracker.svelte.d.ts.map +0 -1
  129. package/dist/actions/request_tracker.svelte.js +0 -169
@@ -69,7 +69,9 @@ export const create_ws_transport = async (options) => {
69
69
  socket.send(JSON.stringify(message));
70
70
  });
71
71
  });
72
- socket.on('close', () => {
72
+ let close_code = null;
73
+ socket.on('close', (code) => {
74
+ close_code = code;
73
75
  for (const resolve of close_resolvers)
74
76
  resolve();
75
77
  close_resolvers = [];
@@ -140,6 +142,9 @@ export const create_ws_transport = async (options) => {
140
142
  get messages() {
141
143
  return received;
142
144
  },
145
+ get close_code() {
146
+ return close_code;
147
+ },
143
148
  send: send_impl,
144
149
  async request(id, method, params, timeout_ms) {
145
150
  await send_impl(create_jsonrpc_request(method, params, id));
@@ -1 +1 @@
1
- {"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAQ,MAAM,MAAM,CAAC;AAC1C,OAAO,EACN,SAAS,EAET,KAAK,gBAAgB,EAErB,KAAK,QAAQ,EACb,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAE/E,OAAO,EAAsB,KAAK,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAEpG,OAAO,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAChF,OAAO,EAAuB,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEtF,OAAO,EAKN,KAAK,cAAc,EACnB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAMN,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,MAAM,2BAA2B,CAAC;AAMnC;;;GAGG;AACH,MAAM,WAAW,MAAM;IACtB,EAAE,EAAE,SAAS,CAAC;IACd,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,QAAO,MAajC,CAAC;AAEF,8CAA8C;AAC9C,MAAM,WAAW,sBAAsB;IACtC,eAAe,EAAE,cAAc,CAAC;IAChC,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;OAGG;IACH,eAAe,CAAC,EAAE,cAAc,CAAC;CACjC;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GAAI,MAAM,sBAAsB,KAAG,OAavE,CAAC;AAEF,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC3B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,iBAAiB,EAAE,MAAM,CAAC,CAAC,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACtE;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,QAAO,WAatC,CAAC;AAEF;;;;GAIG;AACH,qBAAa,wBAAyB,YAAW,sBAAsB;;IACtE,QAAQ,EAAE,UAAU,GAAG,SAAS,CAAa;gBAEjC,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC;IAGjD,qBAAqB,IAAI,SAAS;IAGlC,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;CAG/D;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC/B,YAAY,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,EAC9C,OAAO,YAAY,EACnB,IAAI,SAAS,KACX,OAAO,CAAC,IAAI,CAId,CAAC;AAMF,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IACjC,wEAAwE;IACxE,UAAU,CAAC,EAAE,IAAI,CAAC;IAClB,yFAAyF;IACzF,eAAe,CAAC,EAAE,cAAc,CAAC;IACjC,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,sFAAsF;IACtF,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED,4CAA4C;AAC5C,MAAM,WAAW,0BAA0B;IAC1C;;;;;OAKG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B,kEAAkE;IAClE,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;OAIG;IACH,SAAS,CAAC,EAAE,uBAAuB,CAAC,WAAW,CAAC,CAAC;IACjD,gEAAgE;IAChE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,cAAc,CAAC,EAAE,uBAAuB,CAAC,gBAAgB,CAAC,CAAC;IAC3D,yDAAyD;IACzD,eAAe,CAAC,EAAE,uBAAuB,CAAC,iBAAiB,CAAC,CAAC;IAC7D;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,kBAAkB,CAAC;CAChC;AAED,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,yBAAyB,CAAC;IACrC;;;;;;;;;;OAUG;IACH,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,iBAAiB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC7D;AAiED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,0BAA0B,KAAG,aA6M5E,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,eAAe,QAAO,iBAGjC,CAAC;AAaH;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,mBAAmB,GAAI,IAAI,SAAS,MAAM,EAAE,SAAS;IACjE,OAAO,EAAE,aAAa,CAAC;IACvB,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;CACtC,KAAG,IAIH,CAAC"}
1
+ {"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"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"}
@@ -223,6 +223,7 @@ export const create_ws_test_harness = (options) => {
223
223
  // test closed the socket.
224
224
  const close_waiters = [];
225
225
  let is_closed = false;
226
+ let close_code = null;
226
227
  // Captured in `ws.close` below; `client.close(...)` returns it so
227
228
  // tests can await async `on_socket_close` cleanup.
228
229
  let close_pending = null;
@@ -240,6 +241,7 @@ export const create_ws_test_harness = (options) => {
240
241
  if (is_closed)
241
242
  return;
242
243
  is_closed = true;
244
+ close_code = code ?? 1000;
243
245
  for (const resolve of close_waiters.splice(0))
244
246
  resolve();
245
247
  const close_event = new Event('close');
@@ -326,6 +328,9 @@ export const create_ws_test_harness = (options) => {
326
328
  if (close_pending)
327
329
  await close_pending;
328
330
  },
331
+ get close_code() {
332
+ return close_code;
333
+ },
329
334
  wait_for: wait_for_impl,
330
335
  wait_for_close: (timeout_ms = WS_CLIENT_DEFAULT_TIMEOUT_MS) => {
331
336
  if (is_closed)
@@ -17,7 +17,7 @@
17
17
  import PendingButton from '@fuzdev/fuz_ui/PendingButton.svelte';
18
18
  import { autofocus } from '@fuzdev/fuz_ui/autofocus.svelte.ts';
19
19
 
20
- import { Username } from '../primitive_schemas.js';
20
+ import { Username, USERNAME_LENGTH_MAX, USERNAME_LENGTH_MIN } from '../primitive_schemas.js';
21
21
  import { PASSWORD_LENGTH_MIN } from '../auth/password.js';
22
22
  import { auth_state_context } from './auth_state.svelte.js';
23
23
  import { FormState } from './form_state.svelte.js';
@@ -33,7 +33,7 @@
33
33
  } = $props();
34
34
 
35
35
  const auth_state = auth_state_context.get();
36
- const form_state = new FormState();
36
+ const form_state = new FormState<'token' | 'username' | 'password' | 'password_confirm'>();
37
37
 
38
38
  let token = $state.raw('');
39
39
  let username = $state.raw('');
@@ -47,11 +47,13 @@
47
47
  username.trim() &&
48
48
  username_valid &&
49
49
  password.length >= PASSWORD_LENGTH_MIN &&
50
- passwords_match &&
51
- !auth_state.verifying
50
+ passwords_match
52
51
  );
53
52
 
54
53
  const handle_bootstrap = async (): Promise<void> => {
54
+ // while verifying the fields are `readonly` and the button `aria-disabled` —
55
+ // `disabled` would drop focus to `<body>` — so Enter and clicks still land here
56
+ if (auth_state.verifying) return;
55
57
  form_state.attempt();
56
58
  if (!can_submit) {
57
59
  if (!token.trim()) form_state.focus('token');
@@ -86,7 +88,7 @@
86
88
  type="password"
87
89
  bind:value={token}
88
90
  placeholder="paste token"
89
- disabled={auth_state.verifying}
91
+ readonly={auth_state.verifying}
90
92
  {@attach autofocus()}
91
93
  />
92
94
  </label>
@@ -98,12 +100,13 @@
98
100
  bind:value={username}
99
101
  placeholder="admin"
100
102
  autocomplete="username"
101
- disabled={auth_state.verifying}
103
+ readonly={auth_state.verifying}
102
104
  />
103
105
  </label>
104
106
  {#if form_state.show('username') && username && !username_valid}
105
107
  <p class="color_c_50 font_size_sm mt_0 mb_xs">
106
- 3-39 chars, starts with a letter, ends with letter/number, middle allows dash/underscore
108
+ {USERNAME_LENGTH_MIN}-{USERNAME_LENGTH_MAX} chars, starts with a letter, ends with
109
+ letter/number, middle allows dash/underscore
107
110
  </p>
108
111
  {/if}
109
112
  <fieldset>
@@ -115,7 +118,7 @@
115
118
  bind:value={password}
116
119
  placeholder="password"
117
120
  autocomplete="new-password"
118
- disabled={auth_state.verifying}
121
+ readonly={auth_state.verifying}
119
122
  />
120
123
  </label>
121
124
  {#if form_state.show('password') && password && password.length < PASSWORD_LENGTH_MIN}
@@ -131,7 +134,7 @@
131
134
  bind:value={password_confirm}
132
135
  placeholder="confirm password"
133
136
  autocomplete="new-password"
134
- disabled={auth_state.verifying}
137
+ readonly={auth_state.verifying}
135
138
  />
136
139
  </label>
137
140
  {#if form_state.show('password_confirm') && password && password_confirm && !passwords_match}
@@ -141,7 +144,8 @@
141
144
  <div class="row gap_sm">
142
145
  <PendingButton
143
146
  pending={auth_state.verifying}
144
- disabled={auth_state.verifying}
147
+ disabled={false}
148
+ aria-disabled={auth_state.verifying}
145
149
  onclick={handle_bootstrap}
146
150
  class={auth_state.verify_error ? 'color_c' : ''}
147
151
  >
@@ -1 +1 @@
1
- {"version":3,"file":"BootstrapForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/BootstrapForm.svelte"],"names":[],"mappings":"AAyBC,KAAK,gBAAgB,GAAI;IACxB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAuGH,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"BootstrapForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/BootstrapForm.svelte"],"names":[],"mappings":"AAyBC,KAAK,gBAAgB,GAAI;IACxB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AA0GH,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
package/dist/ui/CLAUDE.md CHANGED
@@ -93,7 +93,12 @@ pattern rather than reintroducing prop-drilling.
93
93
  ## Auth forms
94
94
 
95
95
  All four consume `auth_state_context.get()`; all three form-driven ones
96
- attach a `FormState` for Enter-advance + blur-touched validation.
96
+ attach a `FormState` for Enter-advance + blur-touched validation. While a
97
+ submit is in flight their fields are `readonly` and the `PendingButton` is
98
+ `aria-disabled` (with `disabled={false}` overriding its disable-while-pending
99
+ default), never `disabled` — disabling the focused element drops focus to
100
+ `<body>`. So the handler guards re-entry itself (`if (auth_state.verifying)
101
+ return;`), since Enter and clicks still reach it.
97
102
 
98
103
  - `LoginForm.svelte` — props `username_label = 'username or email'`,
99
104
  `redirect_on_login`. Clears `auth_state.verify_error` on input.
@@ -170,13 +175,18 @@ destructive actions.
170
175
  to a specific actor on the recipient account; default account-grain),
171
176
  `roles: Array<string>` (pre-filtered upstream by admin-grant-path —
172
177
  `RoleSpec.grant_paths` includes `'admin'`),
173
- `scope_id = null`, `on_created?`, `format_role?`. Surfaces five
174
- reason codes with friendly copy: `ERROR_ROLE_GRANT_OFFER_SELF_TARGET`,
178
+ `scope_kind` + `scope_id` (`RoleGrantOfferScope` — together, or neither
179
+ for a global offer; the prop types refuse one alone), `on_created?`,
180
+ `format_role?`. Surfaces the RPC error reasons with friendly copy:
181
+ `ERROR_ROLE_GRANT_OFFER_SELF_TARGET`,
175
182
  `ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE`, `ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED`,
183
+ `ERROR_ROLE_GRANT_BUILTIN_SCOPED`,
176
184
  `ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH`, `ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH`
177
- — imported from `auth/role_grant_offer_action_specs.js` (see
185
+ — imported from `auth/role_grant_offer_action_specs.ts` (see
178
186
  `auth/CLAUDE.md` for `role_grant_offer_action_specs.ts` +
179
- `role_grant_offer_actions.ts`).
187
+ `role_grant_offer_actions.ts`). Submits like the auth forms (`readonly`
188
+ textarea, `aria-disabled` button, re-entry guard); its select stays
189
+ `disabled`, having no `readonly` — safe, since the button has focus.
180
190
  - `RoleGrantOfferHistory.svelte` — both-directions history (recipient +
181
191
  grantor, including terminal). Props: `current_actor_id: string | null`
182
192
  (classifies row as "sent" vs "received"), `format_actor?`,
@@ -259,8 +269,10 @@ destructive actions.
259
269
  Methods: `form()` (returns a Svelte `Attachment` for the form
260
270
  element), `show(field)` (touched OR attempted), `is_touched(field)`,
261
271
  `touch(field)` (programmatic), `focus(field)` (queries by `name`),
262
- `attempt()`, `reset()`. In DEV throws if an input loses focus
263
- without a `name` attribute — all tracked inputs must be named.
272
+ `attempt()`, `reset()`. Generic over its field names —
273
+ `new FormState<'username' | 'password'>()` makes a misspelled field a
274
+ compile error (defaults to `string`). In DEV throws if an input loses
275
+ focus without a `name` attribute — all tracked inputs must be named.
264
276
  - `sidebar_state.svelte.ts` — see Shell + layout above.
265
277
 
266
278
  ## Per-domain state modules
@@ -34,12 +34,15 @@
34
34
  } = $props();
35
35
 
36
36
  const auth_state = auth_state_context.get();
37
- const form_state = new FormState();
37
+ const form_state = new FormState<'username' | 'password'>();
38
38
 
39
39
  let username = $state.raw('');
40
40
  let password = $state.raw('');
41
41
 
42
42
  const handle_login = async (): Promise<void> => {
43
+ // while verifying the fields are `readonly` and the button `aria-disabled` —
44
+ // `disabled` would drop focus to `<body>` — so Enter and clicks still land here
45
+ if (auth_state.verifying) return;
43
46
  const u = username.trim();
44
47
  const p = password;
45
48
  if (!u) {
@@ -79,7 +82,7 @@
79
82
  bind:value={username}
80
83
  placeholder={username_label}
81
84
  autocomplete="username"
82
- disabled={auth_state.verifying}
85
+ readonly={auth_state.verifying}
83
86
  {@attach autofocus()}
84
87
  />
85
88
  </label>
@@ -91,13 +94,14 @@
91
94
  bind:value={password}
92
95
  placeholder="password"
93
96
  autocomplete="current-password"
94
- disabled={auth_state.verifying}
97
+ readonly={auth_state.verifying}
95
98
  />
96
99
  </label>
97
100
  <div class="row gap_sm">
98
101
  <PendingButton
99
102
  pending={auth_state.verifying}
100
- disabled={auth_state.verifying}
103
+ disabled={false}
104
+ aria-disabled={auth_state.verifying}
101
105
  onclick={handle_login}
102
106
  class={auth_state.verify_error ? 'color_c' : ''}
103
107
  >
@@ -1 +1 @@
1
- {"version":3,"file":"LoginForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/LoginForm.svelte"],"names":[],"mappings":"AAmBC,KAAK,gBAAgB,GAAI;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAsEH,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"LoginForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/LoginForm.svelte"],"names":[],"mappings":"AAmBC,KAAK,gBAAgB,GAAI;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAyEH,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -4,22 +4,29 @@
4
4
  *
5
5
  * Caller supplies `to_account_id`, the subset of roles the grantor may
6
6
  * offer (typically filtered by admin-grant-path — `RoleSpec.grant_paths`
7
- * includes `'admin'`), an optional `scope_id`,
8
- * and an optional `on_created` callback for post-submit UX. Errors from
9
- * the RPC surface the three distinct reason codes — self-target,
10
- * role-not-grantable, not-authorized — so consumers can render them
7
+ * includes `'admin'`), an optional scope — `scope_kind` and `scope_id`
8
+ * together, or neither for a global offer — and an optional `on_created`
9
+ * callback for post-submit UX. Errors from the RPC surface their distinct
10
+ * reason codes (self-target, role-not-grantable, not-authorized, a scoped
11
+ * builtin role, actor mismatches) so consumers can render them
11
12
  * appropriately.
13
+ *
14
+ * @module
12
15
  */
13
16
 
14
17
  import PendingButton from '@fuzdev/fuz_ui/PendingButton.svelte';
15
18
 
16
- import { role_grant_offers_state_context } from './role_grant_offers_state.svelte.js';
19
+ import {
20
+ role_grant_offers_state_context,
21
+ type RoleGrantOfferScope
22
+ } from './role_grant_offers_state.svelte.js';
17
23
  import { FormState } from './form_state.svelte.js';
18
24
  import {
19
25
  ROLE_GRANT_OFFER_MESSAGE_LENGTH_MAX,
20
26
  type RoleGrantOfferJson
21
27
  } from '../auth/role_grant_offer_schema.js';
22
28
  import {
29
+ ERROR_ROLE_GRANT_BUILTIN_SCOPED,
23
30
  ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH,
24
31
  ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH,
25
32
  ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED,
@@ -27,13 +34,15 @@
27
34
  ERROR_ROLE_GRANT_OFFER_SELF_TARGET
28
35
  } from '../auth/role_grant_offer_action_specs.js';
29
36
 
37
+ // `scope` is the offer's resource scope, `RoleGrantOfferScope`: `scope_kind`
38
+ // and `scope_id` together, or neither (the default) for a global offer
30
39
  const {
31
40
  to_account_id,
32
41
  to_actor_id = null,
33
42
  roles,
34
- scope_id = null,
35
43
  on_created,
36
- format_role = (role: string) => role
44
+ format_role = (role: string) => role,
45
+ ...scope
37
46
  }: {
38
47
  to_account_id: string;
39
48
  /**
@@ -44,14 +53,12 @@
44
53
  to_actor_id?: string | null;
45
54
  /** Roles the caller may offer — caller filters upstream (default: admin-grant-path). */
46
55
  roles: Array<string>;
47
- /** Resource scope for the offer; `null` (default) yields a global offer. */
48
- scope_id?: string | null;
49
56
  on_created?: (offer: RoleGrantOfferJson) => void;
50
57
  format_role?: (role: string) => string;
51
- } = $props();
58
+ } & RoleGrantOfferScope = $props();
52
59
 
53
60
  const role_grant_offers = role_grant_offers_state_context.get();
54
- const form_state = new FormState();
61
+ const form_state = new FormState<'role' | 'message'>();
55
62
 
56
63
  let role: string | undefined = $state.raw();
57
64
  const selected_role = $derived(role ?? roles[0] ?? '');
@@ -68,6 +75,8 @@
68
75
  return 'That role cannot be offered through this form.';
69
76
  case ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED:
70
77
  return 'You are not authorized to offer that role.';
78
+ case ERROR_ROLE_GRANT_BUILTIN_SCOPED:
79
+ return 'That role can only be offered globally, not within a scope.';
71
80
  case ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH:
72
81
  return 'That actor is not on the recipient account.';
73
82
  case ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH:
@@ -78,6 +87,9 @@
78
87
  };
79
88
 
80
89
  const handle_submit = async (): Promise<void> => {
90
+ // while submitting the textarea is `readonly` and the button `aria-disabled` —
91
+ // `disabled` would drop focus to `<body>` — so clicks still land here
92
+ if (submitting) return;
81
93
  form_state.attempt();
82
94
  local_error = null;
83
95
  if (!selected_role) {
@@ -88,7 +100,7 @@
88
100
  to_account_id,
89
101
  to_actor_id,
90
102
  role: selected_role,
91
- scope_id,
103
+ ...scope,
92
104
  message: message.trim() || null
93
105
  });
94
106
  if (offer) {
@@ -115,6 +127,8 @@
115
127
  >
116
128
  <label>
117
129
  <div class="title">role</div>
130
+ <!-- a select has no `readonly`, but disabling it can't drop focus: the only
131
+ submit is the button, which has focus when the select disables -->
118
132
  <select
119
133
  name="role"
120
134
  value={selected_role}
@@ -134,13 +148,14 @@
134
148
  bind:value={message}
135
149
  maxlength={ROLE_GRANT_OFFER_MESSAGE_LENGTH_MAX}
136
150
  placeholder="optional note for the recipient"
137
- disabled={submitting}></textarea>
151
+ readonly={submitting}></textarea>
138
152
  </label>
139
153
 
140
154
  <div class="row gap_sm">
141
155
  <PendingButton
142
156
  pending={submitting}
143
- disabled={submitting || !selected_role}
157
+ disabled={!selected_role}
158
+ aria-disabled={submitting}
144
159
  onclick={handle_submit}
145
160
  >
146
161
  send offer
@@ -1,3 +1,4 @@
1
+ import { type RoleGrantOfferScope } from './role_grant_offers_state.svelte.js';
1
2
  import { type RoleGrantOfferJson } from '../auth/role_grant_offer_schema.js';
2
3
  type $$ComponentProps = {
3
4
  to_account_id: string;
@@ -9,11 +10,9 @@ type $$ComponentProps = {
9
10
  to_actor_id?: string | null;
10
11
  /** Roles the caller may offer — caller filters upstream (default: admin-grant-path). */
11
12
  roles: Array<string>;
12
- /** Resource scope for the offer; `null` (default) yields a global offer. */
13
- scope_id?: string | null;
14
13
  on_created?: (offer: RoleGrantOfferJson) => void;
15
14
  format_role?: (role: string) => string;
16
- };
15
+ } & RoleGrantOfferScope;
17
16
  declare const RoleGrantOfferForm: import("svelte").Component<$$ComponentProps, {}, "">;
18
17
  type RoleGrantOfferForm = ReturnType<typeof RoleGrantOfferForm>;
19
18
  export default RoleGrantOfferForm;
@@ -1 +1 @@
1
- {"version":3,"file":"RoleGrantOfferForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/RoleGrantOfferForm.svelte"],"names":[],"mappings":"AAkBA,OAAO,EAEL,KAAK,kBAAkB,EACvB,MAAM,oCAAoC,CAAC;AAS5C,KAAK,gBAAgB,GAAI;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,wFAAwF;IACxF,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACjD,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC,CAAC;AA0GH,QAAA,MAAM,kBAAkB,sDAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"RoleGrantOfferForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/RoleGrantOfferForm.svelte"],"names":[],"mappings":"AAmBA,OAAO,EAEL,KAAK,mBAAmB,EACxB,MAAM,qCAAqC,CAAC;AAE9C,OAAO,EAEL,KAAK,kBAAkB,EACvB,MAAM,oCAAoC,CAAC;AAU5C,KAAK,gBAAgB,GAAI;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,wFAAwF;IACxF,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACjD,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC,GAAG,mBAAmB,CAAC;AAkHzB,QAAA,MAAM,kBAAkB,sDAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}
@@ -16,7 +16,7 @@
16
16
  import PendingButton from '@fuzdev/fuz_ui/PendingButton.svelte';
17
17
  import { autofocus } from '@fuzdev/fuz_ui/autofocus.svelte.ts';
18
18
 
19
- import { Username } from '../primitive_schemas.js';
19
+ import { Username, USERNAME_LENGTH_MAX, USERNAME_LENGTH_MIN } from '../primitive_schemas.js';
20
20
  import { PASSWORD_LENGTH_MIN } from '../auth/password.js';
21
21
  import { auth_state_context } from './auth_state.svelte.js';
22
22
  import { FormState } from './form_state.svelte.js';
@@ -32,7 +32,7 @@
32
32
  } = $props();
33
33
 
34
34
  const auth_state = auth_state_context.get();
35
- const form_state = new FormState();
35
+ const form_state = new FormState<'username' | 'email' | 'password' | 'password_confirm'>();
36
36
 
37
37
  let username = $state.raw('');
38
38
  let email = $state.raw('');
@@ -42,14 +42,13 @@
42
42
  const username_valid = $derived(Username.safeParse(username).success);
43
43
  const passwords_match = $derived(password === password_confirm);
44
44
  const can_submit = $derived(
45
- username.trim() &&
46
- username_valid &&
47
- password.length >= PASSWORD_LENGTH_MIN &&
48
- passwords_match &&
49
- !auth_state.verifying
45
+ username.trim() && username_valid && password.length >= PASSWORD_LENGTH_MIN && passwords_match
50
46
  );
51
47
 
52
48
  const handle_signup = async (): Promise<void> => {
49
+ // while verifying the fields are `readonly` and the button `aria-disabled` —
50
+ // `disabled` would drop focus to `<body>` — so Enter and clicks still land here
51
+ if (auth_state.verifying) return;
53
52
  form_state.attempt();
54
53
  if (!can_submit) {
55
54
  if (!username.trim() || !username_valid) form_state.focus('username');
@@ -84,13 +83,14 @@
84
83
  bind:value={username}
85
84
  placeholder="username"
86
85
  autocomplete="username"
87
- disabled={auth_state.verifying}
86
+ readonly={auth_state.verifying}
88
87
  {@attach autofocus()}
89
88
  />
90
89
  </label>
91
90
  {#if form_state.show('username') && username && !username_valid}
92
91
  <p class="color_c_50 font_size_sm mt_0 mb_xs">
93
- 3-39 chars, starts with a letter, ends with letter/number, middle allows dash/underscore
92
+ {USERNAME_LENGTH_MIN}-{USERNAME_LENGTH_MAX} chars, starts with a letter, ends with
93
+ letter/number, middle allows dash/underscore
94
94
  </p>
95
95
  {/if}
96
96
  <label>
@@ -101,7 +101,7 @@
101
101
  bind:value={email}
102
102
  placeholder="email"
103
103
  autocomplete="email"
104
- disabled={auth_state.verifying}
104
+ readonly={auth_state.verifying}
105
105
  />
106
106
  </label>
107
107
  <fieldset>
@@ -113,7 +113,7 @@
113
113
  bind:value={password}
114
114
  placeholder="password"
115
115
  autocomplete="new-password"
116
- disabled={auth_state.verifying}
116
+ readonly={auth_state.verifying}
117
117
  />
118
118
  </label>
119
119
  {#if form_state.show('password') && password && password.length < PASSWORD_LENGTH_MIN}
@@ -129,7 +129,7 @@
129
129
  bind:value={password_confirm}
130
130
  placeholder="confirm password"
131
131
  autocomplete="new-password"
132
- disabled={auth_state.verifying}
132
+ readonly={auth_state.verifying}
133
133
  />
134
134
  </label>
135
135
  {#if form_state.show('password_confirm') && password && password_confirm && !passwords_match}
@@ -139,7 +139,8 @@
139
139
  <div class="row gap_sm">
140
140
  <PendingButton
141
141
  pending={auth_state.verifying}
142
- disabled={auth_state.verifying}
142
+ disabled={false}
143
+ aria-disabled={auth_state.verifying}
143
144
  onclick={handle_signup}
144
145
  class={auth_state.verify_error ? 'color_c' : ''}
145
146
  >
@@ -5,16 +5,28 @@
5
5
  * `attempted` state (set on submit attempt). Errors show after a field is blurred or
6
6
  * after a submit attempt, avoiding premature validation while the user is still typing.
7
7
  *
8
- * The `FormState.form` attachment also handles Enter key advancing
9
- * between focusable elements.
8
+ * The `FormState.form` attachment also handles the Enter key: in an input
9
+ * followed by another input it moves focus to that input (past any button
10
+ * between), and in the last input it submits the form (`requestSubmit`) —
11
+ * so Enter in a login form's password field logs in, even when the form's
12
+ * button is `type="button"`, as `PendingButton` is. Hidden inputs don't count, and an Enter that commits
13
+ * IME composition is left alone.
14
+ *
15
+ * While a submit is in flight, make the fields `readonly` rather than `disabled`
16
+ * (and the button `aria-disabled`, guarding the handler against re-entry) —
17
+ * disabling the focused element drops focus to `<body>`. Enter still advances
18
+ * through and submits from `readonly` inputs.
19
+ *
20
+ * The type parameter names the form's fields, so `focus`, `show`, `touch`,
21
+ * and `is_touched` reject a misspelled field at compile time.
10
22
  *
11
23
  * All trackable inputs must have a `name` attribute — an error is thrown in dev
12
24
  * if an input without `name` loses focus.
13
25
  *
14
26
  * @example
15
27
  * ```svelte
16
- * <script>
17
- * const form_state = new FormState();
28
+ * <script lang="ts">
29
+ * const form_state = new FormState<'username'>();
18
30
  * let username = $state.raw('');
19
31
  * const username_valid = $derived(Username.safeParse(username).success);
20
32
  * const can_submit = $derived(username.trim() && username_valid);
@@ -41,15 +53,16 @@
41
53
  * @module
42
54
  */
43
55
  import type { Attachment } from 'svelte/attachments';
44
- export declare class FormState {
56
+ export declare class FormState<TField extends string = string> {
45
57
  #private;
46
58
  /**
47
59
  * Whether a submit attempt has been made.
48
60
  */
49
61
  get attempted(): boolean;
50
62
  /**
51
- * Creates a form attachment that handles Enter key advancing between
52
- * focusable elements and tracks field touched state via delegated `focusout`.
63
+ * Creates a form attachment that handles the Enter key (advance focus to
64
+ * the next input, or submit from the last one) and tracks field touched
65
+ * state via delegated `focusout`.
53
66
  *
54
67
  * Fields are identified by their `name` attribute.
55
68
  *
@@ -60,22 +73,22 @@ export declare class FormState {
60
73
  /**
61
74
  * Whether a field has been blurred at least once.
62
75
  */
63
- is_touched(field: string): boolean;
76
+ is_touched(field: TField): boolean;
64
77
  /**
65
78
  * Whether to show validation errors for a field.
66
79
  * Returns `true` if the field has been blurred or a submit attempt was made.
67
80
  */
68
- show(field: string): boolean;
81
+ show(field: TField): boolean;
69
82
  /**
70
83
  * Programmatically marks a field as touched without requiring a blur event.
71
84
  *
72
85
  * @mutates `this`
73
86
  */
74
- touch(field: string): void;
87
+ touch(field: TField): void;
75
88
  /**
76
89
  * Focuses the named input within the form.
77
90
  */
78
- focus(field: string): void;
91
+ focus(field: TField): void;
79
92
  /**
80
93
  * Marks the form as having been submitted, causing all field errors to show.
81
94
  *
@@ -1 +1 @@
1
- {"version":3,"file":"form_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/form_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AASrD,qBAAa,SAAS;;IAKrB;;OAEG;IACH,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;;;;;;OAQG;IACH,IAAI,IAAI,UAAU,CAAC,eAAe,CAAC;IAuCnC;;OAEG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIlC;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAI5B;;;;OAIG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1B;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAiB1B;;;;OAIG;IACH,OAAO,IAAI,IAAI;IAIf;;;;OAIG;IACH,KAAK,IAAI,IAAI;CAIb"}
1
+ {"version":3,"file":"form_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/form_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAUrD,qBAAa,SAAS,CAAC,MAAM,SAAS,MAAM,GAAG,MAAM;;IAKpD;;OAEG;IACH,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;;;;;;;OASG;IACH,IAAI,IAAI,UAAU,CAAC,eAAe,CAAC;IAmDnC;;OAEG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIlC;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAI5B;;;;OAIG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1B;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAiB1B;;;;OAIG;IACH,OAAO,IAAI,IAAI;IAIf;;;;OAIG;IACH,KAAK,IAAI,IAAI;CAIb"}