@chelone/core 0.0.0-stage → 0.1.1

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 (212) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +8 -2
  3. package/dist/auth/access-control.d.ts +9 -0
  4. package/dist/auth/access-control.js +27 -0
  5. package/dist/auth/auth-options.d.ts +57 -0
  6. package/dist/auth/auth-options.js +60 -0
  7. package/dist/auth/types.d.ts +32 -0
  8. package/dist/command-menu/hotkey.d.ts +33 -0
  9. package/dist/command-menu/hotkey.js +60 -0
  10. package/dist/command-menu/recents.d.ts +14 -0
  11. package/dist/command-menu/recents.js +15 -0
  12. package/dist/command-menu/registry.d.ts +34 -0
  13. package/dist/command-menu/registry.js +53 -0
  14. package/dist/controller/calendar-dates.d.ts +27 -0
  15. package/dist/controller/calendar-dates.js +123 -0
  16. package/dist/controller/calendar-events.d.ts +54 -0
  17. package/dist/controller/calendar-events.js +109 -0
  18. package/dist/controller/client-list-controller.d.ts +67 -0
  19. package/dist/controller/client-list-controller.js +275 -0
  20. package/dist/controller/data-table-columns.d.ts +23 -0
  21. package/dist/controller/data-table-columns.js +73 -0
  22. package/dist/controller/filter-toggle.d.ts +17 -0
  23. package/dist/controller/filter-toggle.js +52 -0
  24. package/dist/controller/infinite-list-controller.d.ts +31 -0
  25. package/dist/controller/infinite-list-controller.js +193 -0
  26. package/dist/controller/list-controller.d.ts +161 -0
  27. package/dist/controller/list-controller.js +425 -0
  28. package/dist/controller/list-params.d.ts +72 -0
  29. package/dist/controller/list-params.js +103 -0
  30. package/dist/controller/prev-next.d.ts +61 -0
  31. package/dist/controller/prev-next.js +62 -0
  32. package/dist/controller/record-controllers.d.ts +161 -0
  33. package/dist/controller/record-controllers.js +274 -0
  34. package/dist/controller/record-tree.d.ts +20 -0
  35. package/dist/controller/record-tree.js +53 -0
  36. package/dist/controller/record-workflow.d.ts +54 -0
  37. package/dist/controller/record-workflow.js +70 -0
  38. package/dist/controller/reference-many-controller.d.ts +43 -0
  39. package/dist/controller/reference-many-controller.js +196 -0
  40. package/dist/controller/reference-one-controller.d.ts +39 -0
  41. package/dist/controller/reference-one-controller.js +60 -0
  42. package/dist/controller/saved-queries.d.ts +43 -0
  43. package/dist/controller/saved-queries.js +104 -0
  44. package/dist/controller/translate-fallback.d.ts +6 -0
  45. package/dist/controller/translate-fallback.js +8 -0
  46. package/dist/csv/export.d.ts +45 -0
  47. package/dist/csv/export.js +47 -0
  48. package/dist/csv/import-controller.d.ts +21 -0
  49. package/dist/csv/import-controller.js +175 -0
  50. package/dist/csv/match-headers.d.ts +6 -0
  51. package/dist/csv/match-headers.js +24 -0
  52. package/dist/csv/parse-with.d.ts +2 -0
  53. package/dist/csv/parse-with.js +58 -0
  54. package/dist/csv/run-import.d.ts +23 -0
  55. package/dist/csv/run-import.js +76 -0
  56. package/dist/csv/types.d.ts +98 -0
  57. package/dist/csv/validate-rows.d.ts +22 -0
  58. package/dist/csv/validate-rows.js +58 -0
  59. package/dist/data/cache-ops.d.ts +25 -0
  60. package/dist/data/cache-ops.js +132 -0
  61. package/dist/data/combine-data-providers.d.ts +10 -0
  62. package/dist/data/combine-data-providers.js +25 -0
  63. package/dist/data/errors.d.ts +10 -0
  64. package/dist/data/errors.js +11 -0
  65. package/dist/data/fake-provider.d.ts +6 -0
  66. package/dist/data/fake-provider.js +122 -0
  67. package/dist/data/fetch-utils.d.ts +38 -0
  68. package/dist/data/fetch-utils.js +56 -0
  69. package/dist/data/get-many-aggregate.d.ts +17 -0
  70. package/dist/data/get-many-aggregate.js +85 -0
  71. package/dist/data/http-error.d.ts +12 -0
  72. package/dist/data/http-error.js +13 -0
  73. package/dist/data/keys.d.ts +42 -0
  74. package/dist/data/keys.js +55 -0
  75. package/dist/data/mutation-options.d.ts +64 -0
  76. package/dist/data/mutation-options.js +95 -0
  77. package/dist/data/query-options.d.ts +58 -0
  78. package/dist/data/query-options.js +81 -0
  79. package/dist/data/types.d.ts +127 -0
  80. package/dist/data/undoable.d.ts +121 -0
  81. package/dist/data/undoable.js +196 -0
  82. package/dist/data/with-lifecycle-callbacks.d.ts +50 -0
  83. package/dist/data/with-lifecycle-callbacks.js +54 -0
  84. package/dist/environment/application-update-store.d.ts +23 -0
  85. package/dist/environment/application-update-store.js +60 -0
  86. package/dist/environment/offline-store.d.ts +11 -0
  87. package/dist/environment/offline-store.js +24 -0
  88. package/dist/form/array-binding.d.ts +131 -0
  89. package/dist/form/array-binding.js +170 -0
  90. package/dist/form/choices-controller.d.ts +95 -0
  91. package/dist/form/choices-controller.js +281 -0
  92. package/dist/form/dirty-values.d.ts +14 -0
  93. package/dist/form/dirty-values.js +13 -0
  94. package/dist/form/edit-create-form.d.ts +100 -0
  95. package/dist/form/edit-create-form.js +201 -0
  96. package/dist/form/filter-form-values.d.ts +25 -0
  97. package/dist/form/filter-form-values.js +56 -0
  98. package/dist/form/form-controller.d.ts +154 -0
  99. package/dist/form/form-controller.js +168 -0
  100. package/dist/form/form-groups.d.ts +74 -0
  101. package/dist/form/form-groups.js +83 -0
  102. package/dist/form/input-binding.d.ts +141 -0
  103. package/dist/form/input-binding.js +125 -0
  104. package/dist/form/json-text.d.ts +8 -0
  105. package/dist/form/json-text.js +47 -0
  106. package/dist/form/save-pipeline.d.ts +62 -0
  107. package/dist/form/save-pipeline.js +88 -0
  108. package/dist/form/source.d.ts +50 -0
  109. package/dist/form/source.js +46 -0
  110. package/dist/form/validators.d.ts +83 -0
  111. package/dist/form/validators.js +99 -0
  112. package/dist/geo/bbox.d.ts +15 -0
  113. package/dist/geo/bbox.js +56 -0
  114. package/dist/geo/bounds.d.ts +20 -0
  115. package/dist/geo/bounds.js +74 -0
  116. package/dist/geo/constraints.d.ts +14 -0
  117. package/dist/geo/constraints.js +121 -0
  118. package/dist/geo/coordinates.d.ts +7 -0
  119. package/dist/geo/coordinates.js +32 -0
  120. package/dist/geo/editor.d.ts +59 -0
  121. package/dist/geo/editor.js +168 -0
  122. package/dist/geo/geocoding.d.ts +62 -0
  123. package/dist/geo/geocoding.js +45 -0
  124. package/dist/geo/geojson.d.ts +75 -0
  125. package/dist/geo/geojson.js +57 -0
  126. package/dist/geo/geometry-math.d.ts +22 -0
  127. package/dist/geo/geometry-math.js +120 -0
  128. package/dist/geo/index.d.ts +28 -0
  129. package/dist/geo/map-layers.d.ts +37 -0
  130. package/dist/geo/map-layers.js +67 -0
  131. package/dist/geo/map-style.d.ts +32 -0
  132. package/dist/geo/map-style.js +39 -0
  133. package/dist/geo/messages.d.ts +47 -0
  134. package/dist/geo/messages.js +47 -0
  135. package/dist/geo/nominatim.d.ts +18 -0
  136. package/dist/geo/nominatim.js +87 -0
  137. package/dist/geo/osm-presets.d.ts +86 -0
  138. package/dist/geo/osm-presets.js +98 -0
  139. package/dist/geo/osm-tags.d.ts +14 -0
  140. package/dist/geo/osm-tags.js +14 -0
  141. package/dist/geo/overpass.d.ts +49 -0
  142. package/dist/geo/overpass.js +97 -0
  143. package/dist/geo/shape-value.d.ts +36 -0
  144. package/dist/geo/shape-value.js +172 -0
  145. package/dist/geo/snap-to-roads.d.ts +19 -0
  146. package/dist/geo/snap-to-roads.js +52 -0
  147. package/dist/geo/throttle.d.ts +17 -0
  148. package/dist/geo/throttle.js +45 -0
  149. package/dist/i18n/default-messages.d.ts +33 -0
  150. package/dist/i18n/default-messages.js +31 -0
  151. package/dist/i18n/i18n.d.ts +42 -0
  152. package/dist/i18n/i18n.js +56 -0
  153. package/dist/i18n/messages.d.ts +24 -0
  154. package/dist/i18n/messages.js +6 -0
  155. package/dist/i18n/translatable.d.ts +32 -0
  156. package/dist/i18n/translatable.js +56 -0
  157. package/dist/index.d.ts +172 -0
  158. package/dist/index.js +343 -0
  159. package/dist/json/strip-json-comments.d.ts +6 -0
  160. package/dist/json/strip-json-comments.js +50 -0
  161. package/dist/notification/notification-store.d.ts +45 -0
  162. package/dist/notification/notification-store.js +47 -0
  163. package/dist/realtime/add-events-for-mutations.d.ts +13 -0
  164. package/dist/realtime/add-events-for-mutations.js +85 -0
  165. package/dist/realtime/copy-members.d.ts +5 -0
  166. package/dist/realtime/copy-members.js +16 -0
  167. package/dist/realtime/in-memory-lock-provider.d.ts +7 -0
  168. package/dist/realtime/in-memory-lock-provider.js +62 -0
  169. package/dist/realtime/live.d.ts +23 -0
  170. package/dist/realtime/live.js +118 -0
  171. package/dist/realtime/lock-options.d.ts +19 -0
  172. package/dist/realtime/lock-options.js +22 -0
  173. package/dist/realtime/lock-session.d.ts +34 -0
  174. package/dist/realtime/lock-session.js +82 -0
  175. package/dist/realtime/memory-transport.d.ts +19 -0
  176. package/dist/realtime/memory-transport.js +68 -0
  177. package/dist/realtime/presence.d.ts +43 -0
  178. package/dist/realtime/presence.js +128 -0
  179. package/dist/realtime/realtime-data-provider.d.ts +14 -0
  180. package/dist/realtime/realtime-data-provider.js +49 -0
  181. package/dist/realtime/status-store.d.ts +11 -0
  182. package/dist/realtime/status-store.js +33 -0
  183. package/dist/realtime/topics.d.ts +9 -0
  184. package/dist/realtime/topics.js +12 -0
  185. package/dist/realtime/types.d.ts +81 -0
  186. package/dist/realtime/types.js +13 -0
  187. package/dist/realtime/unread-count.d.ts +20 -0
  188. package/dist/realtime/unread-count.js +27 -0
  189. package/dist/resource/describe-resource.d.ts +37 -0
  190. package/dist/resource/describe-resource.js +246 -0
  191. package/dist/resource/identifier.d.ts +1 -0
  192. package/dist/resource/record.d.ts +9 -0
  193. package/dist/resource/registry-store.d.ts +83 -0
  194. package/dist/resource/registry-store.js +68 -0
  195. package/dist/resource/registry.d.ts +30 -0
  196. package/dist/resource/registry.js +6 -0
  197. package/dist/router/location.d.ts +37 -0
  198. package/dist/router/location.js +20 -0
  199. package/dist/router/match.d.ts +22 -0
  200. package/dist/router/match.js +45 -0
  201. package/dist/router/paths.d.ts +37 -0
  202. package/dist/router/paths.js +40 -0
  203. package/dist/router/port.d.ts +38 -0
  204. package/dist/router/record-from-location.d.ts +18 -0
  205. package/dist/router/record-from-location.js +19 -0
  206. package/dist/store/adapters.d.ts +5 -0
  207. package/dist/store/adapters.js +61 -0
  208. package/dist/store/micro-store.d.ts +22 -0
  209. package/dist/store/micro-store.js +61 -0
  210. package/dist/store/store.d.ts +21 -0
  211. package/dist/store/store.js +59 -0
  212. package/package.json +49 -3
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rin
4
+ Copyright (c) 2025 marmelab (the react-admin / ra-core foundation)
5
+ Copyright (c) 2026 Shadmin contributors
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,9 @@
1
- # Temporary Holding Version
1
+ # @chelone/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The framework-free core of Chelone, a headless admin engine: resource registry, data, list, record and form controllers, auth and i18n.
4
+
5
+ ```bash
6
+ pnpm add @chelone/core @tanstack/form-core @tanstack/query-core
7
+ ```
8
+
9
+ Docs: https://chelone.dev
@@ -0,0 +1,9 @@
1
+ import type { CanAccessParams } from "./types.js";
2
+ /** One grant. `record` predicate (when present) must pass against a provided record. */
3
+ export interface Permission {
4
+ action: string | string[];
5
+ resource: string;
6
+ record?: (record: unknown) => boolean;
7
+ }
8
+ /** Deny-by-default permission matcher (OR across permissions). */
9
+ export declare function canAccessWithPermissions(permissions: Permission[], params: CanAccessParams): boolean;
@@ -0,0 +1,27 @@
1
+ function matchesAction(granted, action) {
2
+ const list = Array.isArray(granted) ? granted : [granted];
3
+ return list.includes("*") || list.includes(action);
4
+ }
5
+ function matchesResource(granted, resource) {
6
+ if (granted === "*") return true;
7
+ if (granted.endsWith(".*")) return resource.startsWith(granted.slice(0, -1));
8
+ return granted === resource;
9
+ }
10
+ function canAccessWithPermissions(permissions, params) {
11
+ return permissions.some((permission) => {
12
+ if (!matchesAction(permission.action, params.action)) return false;
13
+ if (!matchesResource(permission.resource, params.resource)) return false;
14
+ if (permission.record) {
15
+ if (params.record == null) return false;
16
+ try {
17
+ return permission.record(params.record);
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+ return true;
23
+ });
24
+ }
25
+ export {
26
+ canAccessWithPermissions
27
+ };
@@ -0,0 +1,57 @@
1
+ import type { AuthProvider, CanAccessParams, UserIdentity } from "./types.js";
2
+ /** Plain option objects, same convention as the data factories (P2). */
3
+ export declare function identityQueryOptions(authProvider: AuthProvider): {
4
+ queryKey: readonly ["auth", "getIdentity"];
5
+ queryFn: () => Promise<UserIdentity>;
6
+ };
7
+ export declare function canAccessQueryOptions(authProvider: AuthProvider, params: CanAccessParams): {
8
+ queryKey: readonly ["auth", "canAccess", CanAccessParams];
9
+ queryFn: () => Promise<boolean>;
10
+ };
11
+ export declare function authStateQueryOptions(authProvider: AuthProvider): {
12
+ queryKey: readonly ["auth", "checkAuth"];
13
+ queryFn: () => Promise<{
14
+ authenticated: boolean;
15
+ }>;
16
+ };
17
+ /**
18
+ * Fetch the current user permissions via `authProvider.getPermissions`.
19
+ * No `getPermissions` on the provider = permissions not configured =
20
+ * resolves `null` (undefined-tolerant; never rejects for that reason). `null`
21
+ * rather than `undefined` is the sentinel because react-query/vue-query forbid
22
+ * a queryFn resolving `undefined`. Mirrors {@link canAccessQueryOptions}: keyed
23
+ * by the passed `params` so distinct param objects cache independently.
24
+ */
25
+ export declare function permissionsQueryOptions(authProvider: AuthProvider, params?: unknown): {
26
+ queryKey: readonly ["auth", "getPermissions", unknown];
27
+ queryFn: () => Promise<unknown>;
28
+ };
29
+ /**
30
+ * The triage result of {@link checkError}. Vanilla (react/vue-free) so both
31
+ * adapters share the reject-payload parsing before doing logout/notify/redirect.
32
+ */
33
+ export interface CheckErrorOutcome {
34
+ /** The session is invalid (`authProvider.checkError` rejected) — act on it. */
35
+ logout: boolean;
36
+ /**
37
+ * Whether to fully log the user out vs. merely signal "not authorized".
38
+ * Honors a `logoutUser: false` hint on the reject payload; defaults to
39
+ * `logout` otherwise. `false` when `logout` is `false`.
40
+ */
41
+ logoutUser: boolean;
42
+ /** Redirect target from the reject payload, falling back to the original error. */
43
+ redirectTo?: string;
44
+ /** Notification message hint; `false` asks the adapter to suppress the notice. */
45
+ message?: string | false;
46
+ }
47
+ /**
48
+ * Error-triage helper for the data error path. Fail-safe: runs
49
+ * `authProvider.checkError(error)` and NEVER throws.
50
+ *
51
+ * - checkError RESOLVES -> ordinary data error -> `{ logout: false }`.
52
+ * - checkError REJECTS -> invalid session -> `{ logout: true, ... }` carrying
53
+ * `redirectTo` / `logoutUser` / `message` hints from the reject payload (or,
54
+ * for `redirectTo`, the original error). Adapters do the logout + notify +
55
+ * redirect; only an auth rejection ever logs the user out.
56
+ */
57
+ export declare function checkError(authProvider: AuthProvider, error: unknown): Promise<CheckErrorOutcome>;
@@ -0,0 +1,60 @@
1
+ function identityQueryOptions(authProvider) {
2
+ return {
3
+ queryKey: ["auth", "getIdentity"],
4
+ queryFn: async () => {
5
+ if (!authProvider.getIdentity) {
6
+ throw new Error(
7
+ "identityQueryOptions: the authProvider does not implement getIdentity."
8
+ );
9
+ }
10
+ return authProvider.getIdentity();
11
+ }
12
+ };
13
+ }
14
+ function canAccessQueryOptions(authProvider, params) {
15
+ return {
16
+ queryKey: ["auth", "canAccess", params],
17
+ // No canAccess on the provider = access control not configured = allow.
18
+ queryFn: async () => authProvider.canAccess?.(params) ?? true
19
+ };
20
+ }
21
+ function authStateQueryOptions(authProvider) {
22
+ return {
23
+ queryKey: ["auth", "checkAuth"],
24
+ queryFn: async () => {
25
+ try {
26
+ await authProvider.checkAuth();
27
+ return { authenticated: true };
28
+ } catch {
29
+ return { authenticated: false };
30
+ }
31
+ }
32
+ };
33
+ }
34
+ function permissionsQueryOptions(authProvider, params) {
35
+ return {
36
+ queryKey: ["auth", "getPermissions", params],
37
+ queryFn: async () => await authProvider.getPermissions?.(params) ?? null
38
+ };
39
+ }
40
+ function readProp(source, key) {
41
+ return source != null && typeof source === "object" && key in source ? source[key] : void 0;
42
+ }
43
+ async function checkError(authProvider, error) {
44
+ try {
45
+ await authProvider.checkError(error);
46
+ return { logout: false, logoutUser: false };
47
+ } catch (rejection) {
48
+ const logoutUser = readProp(rejection, "logoutUser") ?? true;
49
+ const redirectTo = readProp(rejection, "redirectTo") ?? readProp(error, "redirectTo");
50
+ const message = readProp(rejection, "message");
51
+ return { logout: true, logoutUser, redirectTo, message };
52
+ }
53
+ }
54
+ export {
55
+ authStateQueryOptions,
56
+ canAccessQueryOptions,
57
+ checkError,
58
+ identityQueryOptions,
59
+ permissionsQueryOptions
60
+ };
@@ -0,0 +1,32 @@
1
+ import type { Identifier } from "../resource/identifier.js";
2
+ export interface UserIdentity {
3
+ id: Identifier;
4
+ fullName?: string;
5
+ avatar?: string;
6
+ [key: string]: unknown;
7
+ }
8
+ export interface CanAccessParams {
9
+ action: string;
10
+ resource: string;
11
+ record?: unknown;
12
+ }
13
+ /**
14
+ * The auth contract. All methods return promises; reject = not authenticated /
15
+ * not permitted. `refreshAuth` is the token-refresh hook point (charter §11);
16
+ * the P6 shell builds the no-flash SSR gate on `checkAuth`.
17
+ */
18
+ export interface AuthProvider {
19
+ login(params: Record<string, unknown>): Promise<void | {
20
+ redirectTo?: string;
21
+ }>;
22
+ logout(params?: unknown): Promise<void | string>;
23
+ checkAuth(params?: unknown): Promise<void>;
24
+ checkError(error: unknown): Promise<void>;
25
+ getIdentity?(): Promise<UserIdentity>;
26
+ getPermissions?(params?: unknown): Promise<unknown>;
27
+ canAccess?(params: CanAccessParams): Promise<boolean>;
28
+ handleCallback?(): Promise<void | {
29
+ redirectTo?: string;
30
+ }>;
31
+ refreshAuth?(): Promise<void>;
32
+ }
@@ -0,0 +1,33 @@
1
+ /** The slice of a `KeyboardEvent` the matcher reads, so core needs no DOM types. */
2
+ export interface HotkeyEvent {
3
+ key: string;
4
+ /**
5
+ * The physical key (`KeyboardEvent.code`). Optional. When present it settles
6
+ * chords whose `key` the modifier rewrote: Option+/ is `"÷"` on macOS and
7
+ * Shift+1 is `"!"` on a US layout.
8
+ */
9
+ code?: string;
10
+ metaKey: boolean;
11
+ ctrlKey: boolean;
12
+ shiftKey: boolean;
13
+ altKey: boolean;
14
+ isComposing?: boolean;
15
+ }
16
+ /** The slice of an `EventTarget` that says whether the user is typing in it. */
17
+ export interface HotkeyTarget {
18
+ tagName?: string;
19
+ isContentEditable?: boolean;
20
+ }
21
+ /**
22
+ * Reads bindings such as `"mod+k"`, `"mod+shift+p"`, `"alt+/"` and `"f1"`.
23
+ * `mod` is Meta or Ctrl. Modifiers match exactly: a binding without `mod`
24
+ * does not fire while Ctrl or Meta is down.
25
+ */
26
+ export declare function matchesHotkey(event: HotkeyEvent, binding: string): boolean;
27
+ export declare function isTypingTarget(target: HotkeyTarget | null | undefined): boolean;
28
+ /**
29
+ * `matchesHotkey` plus two guards: nothing fires during IME composition, and a
30
+ * binding with neither `mod` nor `alt` is ignored while `target` is typing, so
31
+ * `/` never opens the menu from a text field.
32
+ */
33
+ export declare function shouldHandleHotkey(event: HotkeyEvent, binding: string, target: HotkeyTarget | null | undefined): boolean;
@@ -0,0 +1,60 @@
1
+ function parseBinding(binding) {
2
+ const lower = binding.toLowerCase();
3
+ const plus = lower === "+" || lower.endsWith("++");
4
+ const parts = (plus ? lower.slice(0, -1) : lower).split("+");
5
+ const key = plus ? "+" : parts.pop() ?? "";
6
+ return {
7
+ key,
8
+ mod: parts.includes("mod"),
9
+ shift: parts.includes("shift"),
10
+ alt: parts.includes("alt")
11
+ };
12
+ }
13
+ const punctuationCodes = {
14
+ "/": "Slash",
15
+ ".": "Period",
16
+ ",": "Comma",
17
+ ";": "Semicolon",
18
+ "'": "Quote",
19
+ "[": "BracketLeft",
20
+ "]": "BracketRight",
21
+ "\\": "Backslash",
22
+ "-": "Minus",
23
+ "=": "Equal",
24
+ "`": "Backquote"
25
+ };
26
+ function codeFor(key) {
27
+ if (/^[a-z]$/.test(key)) return `Key${key.toUpperCase()}`;
28
+ if (/^[0-9]$/.test(key)) return `Digit${key}`;
29
+ return punctuationCodes[key];
30
+ }
31
+ function keyMatches(event, parsed, eventKey) {
32
+ if (eventKey === parsed.key) return true;
33
+ if (!(parsed.alt || parsed.shift) || !event.code) return false;
34
+ if (/^[a-z0-9]$/i.test(event.key)) return false;
35
+ return event.code === codeFor(parsed.key);
36
+ }
37
+ function matchesHotkey(event, binding) {
38
+ const parsed = parseBinding(binding);
39
+ if (!keyMatches(event, parsed, event.key.toLowerCase())) return false;
40
+ const modDown = event.metaKey || event.ctrlKey;
41
+ return modDown === parsed.mod && event.shiftKey === parsed.shift && event.altKey === parsed.alt;
42
+ }
43
+ function isTypingTarget(target) {
44
+ if (!target) return false;
45
+ if (target.isContentEditable) return true;
46
+ const tag = target.tagName?.toLowerCase();
47
+ return tag === "input" || tag === "textarea" || tag === "select";
48
+ }
49
+ function shouldHandleHotkey(event, binding, target) {
50
+ if (event.isComposing) return false;
51
+ if (!matchesHotkey(event, binding)) return false;
52
+ const { mod, alt } = parseBinding(binding);
53
+ if (!mod && !alt && isTypingTarget(target)) return false;
54
+ return true;
55
+ }
56
+ export {
57
+ isTypingTarget,
58
+ matchesHotkey,
59
+ shouldHandleHotkey
60
+ };
@@ -0,0 +1,14 @@
1
+ /** Where the command menu keeps its recents in a `CheloneStore`. */
2
+ export declare const recentsKey = "command-menu.recents";
3
+ /**
4
+ * A page the user visited. It holds no `path`: the link is rebuilt from the
5
+ * resource and id, so a basename change cannot strand a saved entry.
6
+ */
7
+ export interface RecentEntry {
8
+ type: "resource" | "record";
9
+ resource: string;
10
+ id?: string | number;
11
+ label: string;
12
+ }
13
+ /** Puts `entry` first, drops an earlier copy, and cuts to `limit`. */
14
+ export declare function rememberRecent(recents: readonly RecentEntry[], entry: RecentEntry, limit: number): RecentEntry[];
@@ -0,0 +1,15 @@
1
+ const recentsKey = "command-menu.recents";
2
+ function sameEntry(a, b) {
3
+ return a.type === b.type && a.resource === b.resource && (a.id === void 0 ? void 0 : String(a.id)) === (b.id === void 0 ? void 0 : String(b.id));
4
+ }
5
+ function rememberRecent(recents, entry, limit) {
6
+ if (limit < 1) return [];
7
+ return [entry, ...recents.filter((other) => !sameEntry(other, entry))].slice(
8
+ 0,
9
+ limit
10
+ );
11
+ }
12
+ export {
13
+ recentsKey,
14
+ rememberRecent
15
+ };
@@ -0,0 +1,34 @@
1
+ import { type MicroStore } from "../store/micro-store.js";
2
+ /**
3
+ * An extra command in the menu. `label` and `icon` are generic so each adapter
4
+ * uses its own types (a React node, a Vue component).
5
+ */
6
+ export interface CommandAction<TLabel = string, TIcon = unknown> {
7
+ id: string;
8
+ label: TLabel;
9
+ icon?: TIcon;
10
+ /** Heading the command is listed under. Read as `"actions"` when missing. */
11
+ group?: string;
12
+ keywords?: string[];
13
+ shortcut?: string;
14
+ /** Hides the command when it returns false or throws. */
15
+ when?: () => boolean;
16
+ onSelect: () => void;
17
+ }
18
+ export interface CommandRegistry<TLabel = string, TIcon = unknown> {
19
+ store: MicroStore<{
20
+ commands: CommandAction<TLabel, TIcon>[];
21
+ }>;
22
+ /**
23
+ * Adds the action, or replaces one with the same id. The returned function
24
+ * removes it only while it is still the registered one, so a stale cleanup
25
+ * cannot remove a newer registration.
26
+ */
27
+ register(action: CommandAction<TLabel, TIcon>): () => void;
28
+ unregister(id: string): void;
29
+ }
30
+ export declare const defaultCommandGroup = "actions";
31
+ export declare function createCommandRegistry<TLabel = string, TIcon = unknown>(): CommandRegistry<TLabel, TIcon>;
32
+ export declare function getVisibleCommands<T extends Pick<CommandAction, "when">>(commands: readonly T[]): T[];
33
+ /** Groups in first-seen order. A missing `group` reads as `"actions"`. */
34
+ export declare function groupCommands<T extends Pick<CommandAction, "group">>(commands: readonly T[]): [string, T[]][];
@@ -0,0 +1,53 @@
1
+ import { createMicroStore } from "../store/micro-store.js";
2
+ const defaultCommandGroup = "actions";
3
+ function createCommandRegistry() {
4
+ const store = createMicroStore({
5
+ commands: []
6
+ });
7
+ const unregister = (id) => {
8
+ store.setState((prev) => ({
9
+ commands: prev.commands.filter((command) => command.id !== id)
10
+ }));
11
+ };
12
+ const register = (action) => {
13
+ store.setState((prev) => {
14
+ const exists = prev.commands.some((command) => command.id === action.id);
15
+ return {
16
+ commands: exists ? prev.commands.map(
17
+ (command) => command.id === action.id ? action : command
18
+ ) : [...prev.commands, action]
19
+ };
20
+ });
21
+ return () => {
22
+ store.setState((prev) => ({
23
+ commands: prev.commands.filter((command) => command !== action)
24
+ }));
25
+ };
26
+ };
27
+ return { store, register, unregister };
28
+ }
29
+ function getVisibleCommands(commands) {
30
+ return commands.filter((command) => {
31
+ try {
32
+ return command.when ? command.when() !== false : true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ });
37
+ }
38
+ function groupCommands(commands) {
39
+ const groups = /* @__PURE__ */ new Map();
40
+ for (const command of commands) {
41
+ const group = command.group ?? defaultCommandGroup;
42
+ const members = groups.get(group);
43
+ if (members) members.push(command);
44
+ else groups.set(group, [command]);
45
+ }
46
+ return [...groups];
47
+ }
48
+ export {
49
+ createCommandRegistry,
50
+ defaultCommandGroup,
51
+ getVisibleCommands,
52
+ groupCommands
53
+ };
@@ -0,0 +1,27 @@
1
+ export type CalendarView = "month" | "week" | "agenda";
2
+ /** 0 is Sunday, as in `Date#getDay`. */
3
+ export type CalendarWeekStart = 0 | 1 | 2 | 3 | 4 | 5 | 6;
4
+ export interface CalendarRange {
5
+ /** Local midnight of the first day. */
6
+ start: Date;
7
+ /** The last millisecond of the last day. */
8
+ end: Date;
9
+ }
10
+ /**
11
+ * A date-only string is local midnight: `new Date("2026-05-01")` reads it as
12
+ * UTC, which is the previous day west of Greenwich.
13
+ */
14
+ export declare function parseCalendarDate(value: unknown): Date | undefined;
15
+ export declare function calendarRange(view: CalendarView, anchor: Date, weekStartsOn: CalendarWeekStart): CalendarRange;
16
+ /** One local midnight per day: adding to the day of the month never skips or repeats a day when the clocks change. */
17
+ export declare function calendarDays(range: CalendarRange): Date[];
18
+ export declare function shiftCalendarAnchor(view: CalendarView, anchor: Date, direction: 1 | -1): Date;
19
+ export declare const calendarDayKey: (date: Date) => string;
20
+ export interface CalendarLabels {
21
+ title(view: CalendarView, anchor: Date, range: CalendarRange): string;
22
+ weekday(date: Date): string;
23
+ dayNumber(date: Date): string;
24
+ hour(hour: number): string;
25
+ agendaDay(date: Date): string;
26
+ }
27
+ export declare function calendarLabels(locale: string): CalendarLabels;
@@ -0,0 +1,123 @@
1
+ const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
2
+ const AGENDA_DAYS = 14;
3
+ const atMidnight = (date, dayOffset = 0) => new Date(date.getFullYear(), date.getMonth(), date.getDate() + dayOffset);
4
+ const atEndOfDay = (date, dayOffset = 0) => new Date(
5
+ date.getFullYear(),
6
+ date.getMonth(),
7
+ date.getDate() + dayOffset,
8
+ 23,
9
+ 59,
10
+ 59,
11
+ 999
12
+ );
13
+ const daysFromWeekStart = (date, weekStartsOn) => (date.getDay() - weekStartsOn + 7) % 7;
14
+ function parseCalendarDate(value) {
15
+ let date;
16
+ if (value instanceof Date) {
17
+ date = new Date(value.getTime());
18
+ } else if (typeof value === "number") {
19
+ date = new Date(value);
20
+ } else if (typeof value === "string") {
21
+ const dateOnly = DATE_ONLY.exec(value);
22
+ if (dateOnly) {
23
+ const [year, month, day] = [
24
+ Number(dateOnly[1]),
25
+ Number(dateOnly[2]),
26
+ Number(dateOnly[3])
27
+ ];
28
+ date = new Date(2e3, 0, 1);
29
+ date.setFullYear(year, month - 1, day);
30
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
31
+ return void 0;
32
+ }
33
+ } else {
34
+ date = new Date(value);
35
+ }
36
+ } else {
37
+ return void 0;
38
+ }
39
+ return Number.isNaN(date.getTime()) ? void 0 : date;
40
+ }
41
+ function calendarRange(view, anchor, weekStartsOn) {
42
+ if (view === "agenda") {
43
+ return {
44
+ start: atMidnight(anchor, -AGENDA_DAYS),
45
+ end: atEndOfDay(anchor, AGENDA_DAYS)
46
+ };
47
+ }
48
+ const first = view === "month" ? new Date(anchor.getFullYear(), anchor.getMonth(), 1) : anchor;
49
+ const last = view === "month" ? new Date(anchor.getFullYear(), anchor.getMonth() + 1, 0) : anchor;
50
+ return {
51
+ start: atMidnight(first, -daysFromWeekStart(first, weekStartsOn)),
52
+ end: atEndOfDay(last, 6 - daysFromWeekStart(last, weekStartsOn))
53
+ };
54
+ }
55
+ function calendarDays(range) {
56
+ const days = [];
57
+ const first = atMidnight(range.start);
58
+ for (let day = first; day.getTime() <= range.end.getTime(); day = atMidnight(first, days.length)) {
59
+ days.push(day);
60
+ }
61
+ return days;
62
+ }
63
+ function shiftCalendarAnchor(view, anchor, direction) {
64
+ const time = [
65
+ anchor.getHours(),
66
+ anchor.getMinutes(),
67
+ anchor.getSeconds(),
68
+ anchor.getMilliseconds()
69
+ ];
70
+ if (view === "month") {
71
+ const month = anchor.getMonth() + direction;
72
+ const lastDay = new Date(anchor.getFullYear(), month + 1, 0).getDate();
73
+ return new Date(
74
+ anchor.getFullYear(),
75
+ month,
76
+ Math.min(anchor.getDate(), lastDay),
77
+ ...time
78
+ );
79
+ }
80
+ const step = view === "week" ? 7 : AGENDA_DAYS;
81
+ return new Date(
82
+ anchor.getFullYear(),
83
+ anchor.getMonth(),
84
+ anchor.getDate() + direction * step,
85
+ ...time
86
+ );
87
+ }
88
+ const pad = (value) => String(value).padStart(2, "0");
89
+ const calendarDayKey = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
90
+ function calendarLabels(locale) {
91
+ const month = new Intl.DateTimeFormat(locale, {
92
+ month: "long",
93
+ year: "numeric"
94
+ });
95
+ const span = new Intl.DateTimeFormat(locale, {
96
+ month: "short",
97
+ day: "numeric",
98
+ year: "numeric"
99
+ });
100
+ const weekday = new Intl.DateTimeFormat(locale, { weekday: "short" });
101
+ const dayNumber = new Intl.DateTimeFormat(locale, { day: "numeric" });
102
+ const hour = new Intl.DateTimeFormat(locale, { hour: "numeric" });
103
+ const agendaDay = new Intl.DateTimeFormat(locale, {
104
+ weekday: "long",
105
+ month: "long",
106
+ day: "numeric"
107
+ });
108
+ return {
109
+ title: (view, anchor, range) => view === "month" ? month.format(anchor) : span.formatRange(range.start, range.end),
110
+ weekday: (date) => weekday.format(date),
111
+ dayNumber: (date) => dayNumber.format(date),
112
+ hour: (value) => hour.format(new Date(2e3, 0, 1, value)),
113
+ agendaDay: (date) => agendaDay.format(date)
114
+ };
115
+ }
116
+ export {
117
+ calendarDayKey,
118
+ calendarDays,
119
+ calendarLabels,
120
+ calendarRange,
121
+ parseCalendarDate,
122
+ shiftCalendarAnchor
123
+ };
@@ -0,0 +1,54 @@
1
+ import { type CalendarRange } from "./calendar-dates.js";
2
+ export interface CalendarEvent<R> {
3
+ record: R;
4
+ start: Date;
5
+ end?: Date;
6
+ title: string;
7
+ color?: string;
8
+ }
9
+ export interface ToCalendarEventsOptions<R> {
10
+ startSource: string;
11
+ endSource?: string;
12
+ titleSource?: string;
13
+ colorSource?: string;
14
+ /** Maps the value at `colorSource` to a color. */
15
+ colorMap?: Record<string, string>;
16
+ /** The title when `titleSource` is unset or empty: the adapter fills it from the record representation. */
17
+ getTitle: (record: R) => string;
18
+ }
19
+ /** Records with no usable start are skipped, and an end before the start is dropped. */
20
+ export declare function toCalendarEvents<R>(records: readonly R[], options: ToCalendarEventsOptions<R>): CalendarEvent<R>[];
21
+ /** An event runs from its start day to its end day; an end at midnight leaves its own day out. */
22
+ export declare function calendarEventsOnDay<R>(events: readonly CalendarEvent<R>[], day: Date): CalendarEvent<R>[];
23
+ export declare function calendarEventsInHour<R>(events: readonly CalendarEvent<R>[], day: Date, hour: number): CalendarEvent<R>[];
24
+ export interface CalendarAgendaGroup<R> {
25
+ day: Date;
26
+ key: string;
27
+ events: CalendarEvent<R>[];
28
+ }
29
+ /** Equal starts keep list order. */
30
+ export declare function groupCalendarAgenda<R>(events: readonly CalendarEvent<R>[], range: CalendarRange): CalendarAgendaGroup<R>[];
31
+ /**
32
+ * Shifts the start and the end by the whole days between the two days, each
33
+ * keeping its time of day. Adding the duration in milliseconds instead would
34
+ * move an all-day event's end off midnight when the move crosses a daylight
35
+ * saving change.
36
+ */
37
+ export declare function moveCalendarEvent<R>(event: CalendarEvent<R>, fromDay: Date, toDay: Date): {
38
+ start: Date;
39
+ end?: Date;
40
+ };
41
+ /**
42
+ * The list filter that asks for a range. With an `endSource`, an event is kept
43
+ * when it starts before the range ends and ends after it starts, so one that
44
+ * began earlier and runs into the range is fetched; a record with an empty end
45
+ * is then left out. Without one, only the start is bounded, on both sides.
46
+ */
47
+ export declare function calendarRangeFilter(startSource: string, range: CalendarRange, endSource?: string): Record<string, string>;
48
+ /**
49
+ * Keys an earlier default filter wrote that the current one no longer does: a
50
+ * list URL or store can still hold the start's lower bound, which would hide
51
+ * the event that starts before the range.
52
+ */
53
+ export declare function staleCalendarRangeKeys(startSource: string, endSource?: string): string[];
54
+ export declare function omitCalendarRangeFilter(filter: Record<string, unknown>, startSource: string, endSource?: string): Record<string, unknown>;