@chelone/core 0.6.0 → 0.7.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 (242) hide show
  1. package/dist/assistant/assistant-session.d.ts +6 -0
  2. package/dist/assistant/assistant-session.js +204 -181
  3. package/dist/assistant/echo-transport.js +26 -29
  4. package/dist/assistant/http-transport.d.ts +10 -0
  5. package/dist/assistant/http-transport.js +98 -0
  6. package/dist/assistant/types.d.ts +1 -7
  7. package/dist/auth/access-control.d.ts +13 -5
  8. package/dist/auth/access-control.js +25 -21
  9. package/dist/auth/auth-options.d.ts +21 -17
  10. package/dist/auth/auth-options.js +80 -48
  11. package/dist/auth/permission-matrix.d.ts +12 -0
  12. package/dist/auth/permission-matrix.js +67 -0
  13. package/dist/auth/types.d.ts +14 -4
  14. package/dist/block-editor/block-document.d.ts +60 -0
  15. package/dist/block-editor/block-document.js +143 -0
  16. package/dist/block-editor/data-blocks.d.ts +63 -0
  17. package/dist/block-editor/data-blocks.js +117 -0
  18. package/dist/chart/chart-data.js +46 -44
  19. package/dist/command-menu/hotkey.js +58 -47
  20. package/dist/command-menu/recents.js +8 -11
  21. package/dist/command-menu/registry.d.ts +9 -5
  22. package/dist/command-menu/registry.js +36 -46
  23. package/dist/controller/calendar-dates.js +82 -113
  24. package/dist/controller/calendar-events.js +93 -88
  25. package/dist/controller/client-list-controller.d.ts +2 -2
  26. package/dist/controller/client-list-controller.js +251 -254
  27. package/dist/controller/data-table-columns.d.ts +2 -0
  28. package/dist/controller/data-table-columns.js +50 -56
  29. package/dist/controller/deep-equal.d.ts +1 -0
  30. package/dist/controller/deep-equal.js +14 -0
  31. package/dist/controller/filter-toggle.js +36 -44
  32. package/dist/controller/in-place-editor.d.ts +42 -0
  33. package/dist/controller/in-place-editor.js +42 -0
  34. package/dist/controller/infinite-list-controller.js +219 -189
  35. package/dist/controller/inspector.d.ts +49 -0
  36. package/dist/controller/inspector.js +64 -0
  37. package/dist/controller/job-monitor.d.ts +3 -4
  38. package/dist/controller/job-monitor.js +37 -47
  39. package/dist/controller/kanban-board.d.ts +2 -3
  40. package/dist/controller/kanban-board.js +43 -24
  41. package/dist/controller/list-controller.d.ts +5 -5
  42. package/dist/controller/list-controller.js +444 -400
  43. package/dist/controller/list-params.js +96 -87
  44. package/dist/controller/pivot-grid.js +89 -95
  45. package/dist/controller/prev-next.d.ts +2 -2
  46. package/dist/controller/prev-next.js +74 -54
  47. package/dist/controller/record-controllers.d.ts +14 -6
  48. package/dist/controller/record-controllers.js +288 -257
  49. package/dist/controller/record-diff.d.ts +21 -0
  50. package/dist/controller/record-diff.js +50 -0
  51. package/dist/controller/record-timeline.js +42 -30
  52. package/dist/controller/record-tree.js +57 -47
  53. package/dist/controller/record-workflow.d.ts +38 -16
  54. package/dist/controller/record-workflow.js +85 -64
  55. package/dist/controller/reference-many-controller.js +198 -192
  56. package/dist/controller/reference-one-controller.d.ts +1 -1
  57. package/dist/controller/reference-one-controller.js +63 -56
  58. package/dist/controller/saved-queries.js +104 -91
  59. package/dist/controller/translate-fallback.js +10 -6
  60. package/dist/csv/export.d.ts +2 -1
  61. package/dist/csv/export.js +43 -40
  62. package/dist/csv/import-controller.js +185 -169
  63. package/dist/csv/match-headers.js +29 -22
  64. package/dist/csv/parse-with.js +60 -54
  65. package/dist/csv/run-import.js +69 -72
  66. package/dist/csv/validate-rows.d.ts +1 -1
  67. package/dist/csv/validate-rows.js +65 -52
  68. package/dist/data/cache-ops.d.ts +5 -4
  69. package/dist/data/cache-ops.js +208 -188
  70. package/dist/data/combine-data-providers.js +25 -23
  71. package/dist/data/data-provider-log.d.ts +45 -0
  72. package/dist/data/data-provider-log.js +93 -0
  73. package/dist/data/errors.js +14 -10
  74. package/dist/data/fake-provider.d.ts +6 -3
  75. package/dist/data/fake-provider.js +129 -114
  76. package/dist/data/fetch-utils.js +61 -44
  77. package/dist/data/filter-branch.d.ts +5 -0
  78. package/dist/data/filter-branch.js +10 -0
  79. package/dist/data/get-many-aggregate.js +84 -76
  80. package/dist/data/http-error.js +17 -12
  81. package/dist/data/keys.js +50 -36
  82. package/dist/data/mutation-options.d.ts +14 -9
  83. package/dist/data/mutation-options.js +93 -109
  84. package/dist/data/query-options.js +73 -68
  85. package/dist/data/remove-empty.d.ts +3 -0
  86. package/dist/data/remove-empty.js +11 -0
  87. package/dist/data/undoable.d.ts +2 -2
  88. package/dist/data/undoable.js +190 -174
  89. package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
  90. package/dist/data/with-lifecycle-callbacks.js +66 -52
  91. package/dist/environment/application-update-store.js +58 -55
  92. package/dist/environment/offline-store.js +21 -21
  93. package/dist/form/array-binding.d.ts +30 -11
  94. package/dist/form/array-binding.js +184 -166
  95. package/dist/form/choice-ids.d.ts +7 -0
  96. package/dist/form/choice-ids.js +11 -0
  97. package/dist/form/choices-controller.d.ts +3 -2
  98. package/dist/form/choices-controller.js +302 -275
  99. package/dist/form/date-format.d.ts +38 -0
  100. package/dist/form/date-format.js +89 -0
  101. package/dist/form/dirty-values.d.ts +2 -2
  102. package/dist/form/dirty-values.js +22 -11
  103. package/dist/form/duration.d.ts +22 -0
  104. package/dist/form/duration.js +49 -0
  105. package/dist/form/edit-create-form.d.ts +12 -5
  106. package/dist/form/edit-create-form.js +243 -188
  107. package/dist/form/event-value.d.ts +6 -0
  108. package/dist/form/event-value.js +17 -0
  109. package/dist/form/field-error.d.ts +14 -0
  110. package/dist/form/field-error.js +28 -0
  111. package/dist/form/field-name.d.ts +20 -0
  112. package/dist/form/field-name.js +6 -0
  113. package/dist/form/filter-form-values.js +63 -49
  114. package/dist/form/form-controller.d.ts +9 -1
  115. package/dist/form/form-controller.js +165 -160
  116. package/dist/form/form-groups.d.ts +4 -3
  117. package/dist/form/form-groups.js +79 -79
  118. package/dist/form/form-handle.d.ts +175 -0
  119. package/dist/form/form-handle.js +68 -0
  120. package/dist/form/input-binding.d.ts +56 -31
  121. package/dist/form/input-binding.js +169 -118
  122. package/dist/form/input-values.d.ts +130 -0
  123. package/dist/form/input-values.js +177 -0
  124. package/dist/form/json-text.js +51 -37
  125. package/dist/form/save-pipeline.d.ts +6 -8
  126. package/dist/form/save-pipeline.js +91 -82
  127. package/dist/form/set-form-values.d.ts +6 -0
  128. package/dist/form/set-form-values.js +10 -0
  129. package/dist/form/source.js +97 -65
  130. package/dist/form/text-of.d.ts +2 -0
  131. package/dist/form/text-of.js +7 -0
  132. package/dist/form/validators.d.ts +58 -19
  133. package/dist/form/validators.js +97 -71
  134. package/dist/geo/bbox.d.ts +5 -4
  135. package/dist/geo/bbox.js +69 -48
  136. package/dist/geo/bounds.d.ts +14 -3
  137. package/dist/geo/bounds.js +98 -68
  138. package/dist/geo/constraints.js +88 -93
  139. package/dist/geo/coordinates.js +31 -23
  140. package/dist/geo/editor.d.ts +13 -13
  141. package/dist/geo/editor.js +176 -153
  142. package/dist/geo/geocoding.d.ts +4 -4
  143. package/dist/geo/geocoding.js +38 -41
  144. package/dist/geo/geojson.d.ts +12 -74
  145. package/dist/geo/geojson.js +52 -40
  146. package/dist/geo/geometry-math.d.ts +4 -4
  147. package/dist/geo/geometry-math.js +100 -93
  148. package/dist/geo/index.d.ts +7 -6
  149. package/dist/geo/map-layers.d.ts +42 -7
  150. package/dist/geo/map-layers.js +88 -62
  151. package/dist/geo/map-style.js +31 -32
  152. package/dist/geo/messages.js +48 -45
  153. package/dist/geo/nominatim.js +88 -80
  154. package/dist/geo/osm-presets.d.ts +4 -3
  155. package/dist/geo/osm-presets.js +108 -86
  156. package/dist/geo/osm-tags.js +14 -12
  157. package/dist/geo/overpass.d.ts +14 -2
  158. package/dist/geo/overpass.js +83 -90
  159. package/dist/geo/shape-value.d.ts +9 -5
  160. package/dist/geo/shape-value.js +169 -146
  161. package/dist/geo/snap-to-roads.d.ts +3 -3
  162. package/dist/geo/snap-to-roads.js +55 -50
  163. package/dist/geo/throttle.js +36 -39
  164. package/dist/guards.d.ts +37 -0
  165. package/dist/guards.js +62 -0
  166. package/dist/i18n/default-messages.js +34 -29
  167. package/dist/i18n/english-messages.js +187 -189
  168. package/dist/i18n/i18n.d.ts +2 -2
  169. package/dist/i18n/i18n.js +47 -60
  170. package/dist/i18n/inflection.d.ts +34 -0
  171. package/dist/i18n/inflection.js +82 -0
  172. package/dist/i18n/messages.d.ts +10 -2
  173. package/dist/i18n/messages.js +17 -4
  174. package/dist/i18n/missing-keys.js +66 -62
  175. package/dist/i18n/translatable.d.ts +16 -1
  176. package/dist/i18n/translatable.js +40 -47
  177. package/dist/index.d.ts +71 -24
  178. package/dist/index.js +40 -288
  179. package/dist/json/deep-equal.d.ts +8 -0
  180. package/dist/json/deep-equal.js +24 -0
  181. package/dist/json/strip-json-comments.js +38 -42
  182. package/dist/noop.d.ts +2 -0
  183. package/dist/noop.js +4 -0
  184. package/dist/notification/error-message.d.ts +2 -0
  185. package/dist/notification/error-message.js +10 -0
  186. package/dist/notification/notification-store.d.ts +3 -2
  187. package/dist/notification/notification-store.js +41 -45
  188. package/dist/onboarding-tour/placement.js +63 -78
  189. package/dist/onboarding-tour/tour-controller.js +46 -54
  190. package/dist/realtime/add-events-for-mutations.js +75 -81
  191. package/dist/realtime/announce-lock.js +16 -13
  192. package/dist/realtime/broadcast-channel-transport.js +65 -59
  193. package/dist/realtime/copy-members.js +22 -14
  194. package/dist/realtime/in-memory-lock-provider.js +40 -48
  195. package/dist/realtime/live.js +95 -101
  196. package/dist/realtime/lock-options.js +16 -16
  197. package/dist/realtime/lock-session.js +91 -78
  198. package/dist/realtime/memory-transport.js +25 -25
  199. package/dist/realtime/presence.js +120 -122
  200. package/dist/realtime/realtime-data-provider.d.ts +10 -0
  201. package/dist/realtime/realtime-data-provider.js +45 -42
  202. package/dist/realtime/reconnect-backoff.js +17 -0
  203. package/dist/realtime/sse-transport.d.ts +31 -0
  204. package/dist/realtime/sse-transport.js +235 -0
  205. package/dist/realtime/status-store.js +23 -28
  206. package/dist/realtime/topic-hub.js +62 -71
  207. package/dist/realtime/topics.js +10 -12
  208. package/dist/realtime/types.js +10 -12
  209. package/dist/realtime/unread-count.js +21 -21
  210. package/dist/realtime/websocket-transport.d.ts +32 -0
  211. package/dist/realtime/websocket-transport.js +306 -0
  212. package/dist/resource/describe-resource.d.ts +2 -0
  213. package/dist/resource/describe-resource.js +232 -219
  214. package/dist/resource/get-at-path.js +29 -14
  215. package/dist/resource/identifier.d.ts +18 -0
  216. package/dist/resource/identifier.js +27 -0
  217. package/dist/resource/record.d.ts +4 -0
  218. package/dist/resource/record.js +10 -0
  219. package/dist/resource/register.d.ts +7 -0
  220. package/dist/resource/registry-store.d.ts +45 -15
  221. package/dist/resource/registry-store.js +67 -60
  222. package/dist/resource/registry.d.ts +10 -0
  223. package/dist/resource/registry.js +8 -4
  224. package/dist/resource/source-path.d.ts +16 -1
  225. package/dist/rich-text/rich-text-extras.d.ts +132 -0
  226. package/dist/rich-text/rich-text-extras.js +179 -0
  227. package/dist/rich-text/rich-text-image.d.ts +124 -0
  228. package/dist/rich-text/rich-text-image.js +163 -0
  229. package/dist/rich-text/rich-text.d.ts +6 -0
  230. package/dist/rich-text/rich-text.js +125 -88
  231. package/dist/router/location.d.ts +22 -4
  232. package/dist/router/location.js +41 -18
  233. package/dist/router/match.js +47 -39
  234. package/dist/router/paths.js +40 -34
  235. package/dist/router/record-from-location.d.ts +1 -1
  236. package/dist/router/record-from-location.js +25 -17
  237. package/dist/store/adapters.js +51 -55
  238. package/dist/store/micro-store.js +57 -59
  239. package/dist/store/shallow-equal.d.ts +12 -0
  240. package/dist/store/shallow-equal.js +20 -0
  241. package/dist/store/store.js +54 -57
  242. package/package.json +13 -11
@@ -10,12 +10,44 @@
10
10
  * a plain keyed store with a whole-map snapshot, mirroring the store /
11
11
  * MicroStore notification pattern.
12
12
  */
13
+ /**
14
+ * The augmentation point for the types a resource definition carries that
15
+ * depend on the UI framework. An adapter names them once, as `Register` does
16
+ * for resources:
17
+ *
18
+ * ```ts
19
+ * declare module "@chelone/core" {
20
+ * interface ResourceSlots {
21
+ * icon: ComponentType;
22
+ * representation: RecordRepresentation;
23
+ * component: ComponentType;
24
+ * }
25
+ * }
26
+ * ```
27
+ *
28
+ * Augmenting is global to a TypeScript program, so a program that includes
29
+ * both adapters would see both. Without an augmentation each type is
30
+ * `unknown`.
31
+ */
32
+ export interface ResourceSlots {
33
+ }
34
+ /** A resource's menu icon: the adapter's icon type, or `unknown` when none is declared. */
35
+ export type ResourceIcon = ResourceSlots extends {
36
+ icon: infer TIcon;
37
+ } ? TIcon : unknown;
38
+ /** How a record renders as a label: the adapter's type, or `unknown` when none is declared. */
39
+ export type ResourceRepresentation = ResourceSlots extends {
40
+ representation: infer TRepresentation;
41
+ } ? TRepresentation : unknown;
42
+ /** A resource's list, create, edit or show view: the adapter's component type, or `unknown` when none is declared. */
43
+ export type ResourceComponent = ResourceSlots extends {
44
+ component: infer TComponent;
45
+ } ? TComponent : unknown;
13
46
  /**
14
47
  * What a single mounted resource exposes to the shell. `hasList/hasCreate/…`
15
- * drive the routing engine and the menu; `icon` / `recordRepresentation` are
16
- * framework-neutral here (`unknown`) — the React layer narrows them to a
17
- * component / render function. `options` is an open extension bag (D9: the
18
- * shadcn menu reads `options.group`).
48
+ * drive the routing engine and the menu; `icon` / `recordRepresentation` take
49
+ * the adapter's types through {@link ResourceSlots}. `options` is an open
50
+ * extension bag (D9: the shadcn menu reads `options.group`).
19
51
  */
20
52
  export interface ResourceDefinition {
21
53
  name: string;
@@ -23,10 +55,8 @@ export interface ResourceDefinition {
23
55
  hasCreate: boolean;
24
56
  hasEdit: boolean;
25
57
  hasShow: boolean;
26
- /** Framework-neutral: a React component in `@chelone/react`. */
27
- icon?: unknown;
28
- /** Framework-neutral: `string | function | ReactNode` in `@chelone/react`. */
29
- recordRepresentation?: unknown;
58
+ icon?: ResourceIcon;
59
+ recordRepresentation?: ResourceRepresentation;
30
60
  /** Open extension bag — e.g. `{ group: "content" }` for menu grouping (D9). */
31
61
  options?: Record<string, unknown>;
32
62
  }
@@ -60,18 +90,18 @@ export interface ResourceRegistry {
60
90
  /** Framework-neutral props `computeDefinition` derives a definition from. */
61
91
  export interface ComputeDefinitionProps {
62
92
  name: string;
63
- /** A list component/route (any value); presence sets `hasList`. */
64
- list?: unknown;
65
- create?: unknown;
66
- edit?: unknown;
67
- show?: unknown;
93
+ /** A list component; presence sets `hasList`. */
94
+ list?: ResourceComponent;
95
+ create?: ResourceComponent;
96
+ edit?: ResourceComponent;
97
+ show?: ResourceComponent;
68
98
  /** Explicit overrides OR-ed with component presence (`!!list || !!hasList`). */
69
99
  hasList?: boolean;
70
100
  hasCreate?: boolean;
71
101
  hasEdit?: boolean;
72
102
  hasShow?: boolean;
73
- icon?: unknown;
74
- recordRepresentation?: unknown;
103
+ icon?: ResourceIcon;
104
+ recordRepresentation?: ResourceRepresentation;
75
105
  options?: Record<string, unknown>;
76
106
  }
77
107
  /**
@@ -1,68 +1,75 @@
1
+ // src/resource/registry-store.ts
2
+ /**
3
+ * Derive a `ResourceDefinition` from resource props: a route flag is true when
4
+ * EITHER the component is present OR the explicit `has*` flag is set
5
+ * (`hasList = !!list || !!hasList`), matching ra-core's Resource registration.
6
+ */
1
7
  function computeDefinition(props) {
2
- return {
3
- name: props.name,
4
- hasList: !!props.list || !!props.hasList,
5
- hasCreate: !!props.create || !!props.hasCreate,
6
- hasEdit: !!props.edit || !!props.hasEdit,
7
- hasShow: !!props.show || !!props.hasShow,
8
- icon: props.icon,
9
- recordRepresentation: props.recordRepresentation,
10
- options: props.options
11
- };
8
+ return {
9
+ name: props.name,
10
+ hasList: !!props.list || !!props.hasList,
11
+ hasCreate: !!props.create || !!props.hasCreate,
12
+ hasEdit: !!props.edit || !!props.hasEdit,
13
+ hasShow: !!props.show || !!props.hasShow,
14
+ icon: props.icon,
15
+ recordRepresentation: props.recordRepresentation,
16
+ options: props.options
17
+ };
12
18
  }
19
+ /** Shallow-compare two `options` bags (own enumerable keys, `Object.is` values). */
13
20
  function optionsEqual(a, b) {
14
- if (a === b) return true;
15
- if (a == null || b == null) return false;
16
- const aKeys = Object.keys(a);
17
- const bKeys = Object.keys(b);
18
- if (aKeys.length !== bKeys.length) return false;
19
- for (const key of aKeys) {
20
- if (!Object.is(a[key], b[key])) return false;
21
- }
22
- return true;
21
+ if (a === b) return true;
22
+ if (a == null || b == null) return false;
23
+ const aKeys = Object.keys(a);
24
+ const bKeys = Object.keys(b);
25
+ if (aKeys.length !== bKeys.length) return false;
26
+ for (const key of aKeys) if (!Object.is(a[key], b[key])) return false;
27
+ return true;
23
28
  }
29
+ /**
30
+ * Whether two definitions are observably equal: scalar fields by `Object.is`,
31
+ * `options` shallow-compared. Used to no-op needless re-registrations so the
32
+ * React register-on-render loop doesn't churn subscribers (parity §0a-8).
33
+ */
24
34
  function definitionEqual(a, b) {
25
- return a.name === b.name && a.hasList === b.hasList && a.hasCreate === b.hasCreate && a.hasEdit === b.hasEdit && a.hasShow === b.hasShow && Object.is(a.icon, b.icon) && Object.is(a.recordRepresentation, b.recordRepresentation) && optionsEqual(a.options, b.options);
35
+ return a.name === b.name && a.hasList === b.hasList && a.hasCreate === b.hasCreate && a.hasEdit === b.hasEdit && a.hasShow === b.hasShow && Object.is(a.icon, b.icon) && Object.is(a.recordRepresentation, b.recordRepresentation) && optionsEqual(a.options, b.options);
26
36
  }
27
37
  function createResourceRegistry() {
28
- const definitions = /* @__PURE__ */ new Map();
29
- const listeners = /* @__PURE__ */ new Set();
30
- let snapshot = null;
31
- const notify = () => {
32
- snapshot = null;
33
- for (const listener of listeners) listener();
34
- };
35
- return {
36
- register(definition) {
37
- const current = definitions.get(definition.name);
38
- if (current && definitionEqual(current, definition)) return;
39
- definitions.set(definition.name, definition);
40
- notify();
41
- },
42
- unregister(name) {
43
- if (!definitions.delete(name)) return;
44
- notify();
45
- },
46
- get(name) {
47
- return definitions.get(name);
48
- },
49
- getAll() {
50
- if (snapshot === null) {
51
- const next = {};
52
- for (const [name, definition] of definitions) next[name] = definition;
53
- snapshot = next;
54
- }
55
- return snapshot;
56
- },
57
- subscribe(listener) {
58
- listeners.add(listener);
59
- return () => {
60
- listeners.delete(listener);
61
- };
62
- }
63
- };
38
+ const definitions = /* @__PURE__ */ new Map();
39
+ const listeners = /* @__PURE__ */ new Set();
40
+ let snapshot = null;
41
+ const notify = () => {
42
+ snapshot = null;
43
+ for (const listener of listeners) listener();
44
+ };
45
+ return {
46
+ register(definition) {
47
+ const current = definitions.get(definition.name);
48
+ if (current && definitionEqual(current, definition)) return;
49
+ definitions.set(definition.name, definition);
50
+ notify();
51
+ },
52
+ unregister(name) {
53
+ if (!definitions.delete(name)) return;
54
+ notify();
55
+ },
56
+ get(name) {
57
+ return definitions.get(name);
58
+ },
59
+ getAll() {
60
+ if (snapshot === null) {
61
+ const next = {};
62
+ for (const [name, definition] of definitions) next[name] = definition;
63
+ snapshot = next;
64
+ }
65
+ return snapshot;
66
+ },
67
+ subscribe(listener) {
68
+ listeners.add(listener);
69
+ return () => {
70
+ listeners.delete(listener);
71
+ };
72
+ }
73
+ };
64
74
  }
65
- export {
66
- computeDefinition,
67
- createResourceRegistry
68
- };
75
+ export { computeDefinition, createResourceRegistry };
@@ -35,6 +35,16 @@ type Checked<T extends ResourceMap> = {
35
35
  };
36
36
  };
37
37
  };
38
+ /**
39
+ * A resource map whose every resource has `TRecord` as its record. A view
40
+ * generic over its record passes it to a hook that is generic over a map, so
41
+ * `RecordOf<RecordResources<TRecord>, string>` is `TRecord`.
42
+ */
43
+ export type RecordResources<TRecord> = {
44
+ [name: string]: {
45
+ schema: StandardSchemaV1<unknown, TRecord>;
46
+ };
47
+ };
38
48
  /**
39
49
  * The record type for a given resource name in a registry:
40
50
  * the schema's Standard Schema output type when a schema is present, else `LooseRecord`.
@@ -1,6 +1,10 @@
1
+ // src/resource/registry.ts
2
+ /**
3
+ * Identity helper that pins literal resource names + schema types via `const`.
4
+ * `defineResources({ posts: { schema: postSchema } })` keeps `"posts"` literal
5
+ * and `typeof postSchema` exact, enabling end-to-end inference.
6
+ */
1
7
  function defineResources(resources) {
2
- return resources;
8
+ return resources;
3
9
  }
4
- export {
5
- defineResources
6
- };
10
+ export { defineResources };
@@ -56,11 +56,26 @@ export type SourcePath<TRecord> = unknown extends TRecord ? string : Paths<TReco
56
56
  /**
57
57
  * The value type at `TPath`, including the `null`/`undefined` of every parent
58
58
  * along it — what reading `record.author?.name` gives. `unknown` for a loose
59
- * record.
59
+ * record, and for a path typed as a bare `string` (no path to walk).
60
+ *
61
+ * The guard is load-bearing even though `Get` makes the same check: without it
62
+ * here, `Get` over a field's open `TRecord` hangs `vue-tsc`.
60
63
  */
61
64
  export type SourceValue<TRecord, TPath extends string> = unknown extends TRecord ? unknown : Get<TRecord, TPath, {
62
65
  nullish: "path";
63
66
  }>;
67
+ /**
68
+ * The value a `format`/`parse` transform carries at `TPath`.
69
+ *
70
+ * {@link SourceValue} walks from the record root, so a source relative to a row
71
+ * (`qty` under `items[]`) or a filter key (`published_at_gte`) walks to `never`.
72
+ * Both are legal {@link SourceProp}s and their value is simply unknown to the
73
+ * type, so a transform over one stays loose instead of demanding a `never` no
74
+ * callback can return.
75
+ */
76
+ export type SourceTransformValue<TRecord, TPath extends string> = [
77
+ SourceValue<TRecord, TPath>
78
+ ] extends [never] ? unknown : SourceValue<TRecord, TPath>;
64
79
  /**
65
80
  * The top-level keys of `TRecord`, for props that read or write one flat key
66
81
  * (a filter's field, a reference's target). `string` for a loose record.
@@ -0,0 +1,132 @@
1
+ import { type RichTextToolbarGroup } from "./rich-text.js";
2
+ export interface RichTextImageRules {
3
+ /** Exact types, or `type/*` for any subtype. SVG never passes. */
4
+ allowedMimeTypes: readonly string[];
5
+ /** Bytes. */
6
+ maxFileSize: number;
7
+ allowBase64: boolean;
8
+ }
9
+ /** shadmin's image settings: any image, 5 MB, base64 allowed. */
10
+ export declare const defaultRichTextImageRules: Readonly<RichTextImageRules>;
11
+ /** Why an image file is refused, or `null` when it is accepted. */
12
+ export declare function checkRichTextImageFile(file: {
13
+ type: string;
14
+ size: number;
15
+ }, rules: RichTextImageRules): "type" | "size" | null;
16
+ export interface RichTextUpload {
17
+ id: string | null;
18
+ src: string;
19
+ }
20
+ /** What an upload handler may return: a URL, or a URL with the stored file's id. */
21
+ export declare function normalizeRichTextUpload(result: string | {
22
+ id: string | number;
23
+ src: string;
24
+ }, rules?: Pick<RichTextImageRules, "allowBase64">): RichTextUpload | null;
25
+ /** `width` rounded and held in `min`..`max`. `max` wins when it is below `min`. */
26
+ export declare function clampRichTextImageWidth(width: number, { min, max }: {
27
+ min: number;
28
+ max: number;
29
+ }): number;
30
+ /** `<name>.<extension>` for downloading an image, from its alt text and mime type. */
31
+ export declare function richTextImageFileName(alt: string, mimeType: string): string;
32
+ export type RichTextImageActionId = "view" | "download" | "copyImage" | "copyLink" | "remove";
33
+ export interface RichTextImageAction {
34
+ id: RichTextImageActionId;
35
+ /** Translation key, always `chelone.rich_text.image.<id>`. */
36
+ labelKey: `chelone.rich_text.image.${RichTextImageActionId}`;
37
+ defaultLabel: string;
38
+ }
39
+ /** The image popover's actions, in display order. */
40
+ export declare const richTextImageActions: readonly Readonly<RichTextImageAction>[];
41
+ export type RichTextColorPalette = "default" | "bright" | "soft" | "muted";
42
+ export interface RichTextColor {
43
+ id: string;
44
+ palette: RichTextColorPalette;
45
+ /** Translation key, always `chelone.rich_text.color.<id>`. */
46
+ labelKey: `chelone.rich_text.color.${string}`;
47
+ defaultLabel: string;
48
+ /** A `var(--rich-text-color-<id>)` reference, or `null` for the entry that clears the color. */
49
+ value: `var(--rich-text-color-${string})` | null;
50
+ }
51
+ /**
52
+ * The color picker's entries: `default` (clears the color), then three
53
+ * palettes of seven. The adapter's stylesheet defines each
54
+ * `--rich-text-color-<id>`.
55
+ */
56
+ export declare const richTextColors: readonly Readonly<RichTextColor>[];
57
+ /**
58
+ * `color: <value>` when `style` is one `color` declaration with a palette
59
+ * `var(...)`, hex, `rgb()`, `rgba()`, `hsl()` or `hsla()` value, else `null`.
60
+ * Everything else a style attribute can carry is dropped.
61
+ */
62
+ export declare function sanitizeRichTextStyle(style: string): string | null;
63
+ /**
64
+ * `richTextSanitizeOptions` with `style` allowed through, for adapters that
65
+ * run `sanitizeRichTextStyle` on every `style` attribute.
66
+ */
67
+ export declare const richTextStyledSanitizeOptions: Readonly<{
68
+ FORBID_TAGS: readonly ["style", "form", "input", "button", "textarea", "select", "iframe", "object", "embed"];
69
+ FORBID_ATTR: readonly string[];
70
+ }>;
71
+ /** Options for tiptap's `CodeBlockLowlight.configure`, as shadmin sets them. */
72
+ export declare const richTextCodeBlockOptions: Readonly<{
73
+ languageClassPrefix: "language-";
74
+ defaultLanguage: null;
75
+ exitOnTripleEnter: true;
76
+ exitOnArrowDown: true;
77
+ enableTabIndentation: false;
78
+ tabSize: 4;
79
+ }>;
80
+ export type RichTextExtraToolbarItemId = "image" | "color";
81
+ export interface RichTextExtraToolbarItem {
82
+ id: RichTextExtraToolbarItemId;
83
+ group: RichTextToolbarGroup;
84
+ /** Translation key, always `chelone.rich_text.<id>`. */
85
+ labelKey: `chelone.rich_text.${RichTextExtraToolbarItemId}`;
86
+ defaultLabel: string;
87
+ /** A binding in the syntax of `matchesHotkey`, e.g. `"mod+b"`. */
88
+ shortcut?: string;
89
+ }
90
+ /** The toolbar buttons an image or color extension adds, shaped like `richTextToolbarItems`. */
91
+ export declare const richTextExtraToolbarItems: readonly Readonly<RichTextExtraToolbarItem>[];
92
+ interface NamedMark {
93
+ type: {
94
+ name: string;
95
+ };
96
+ }
97
+ /** The slice of a ProseMirror state `endColorOnEnter` reads, so core names no ProseMirror type. */
98
+ interface EnterState<Mark extends NamedMark, Transaction> {
99
+ selection: {
100
+ $from: {
101
+ marks(): readonly Mark[];
102
+ nodeAfter: {
103
+ marks: readonly Mark[];
104
+ } | null;
105
+ };
106
+ };
107
+ storedMarks: readonly Mark[] | null;
108
+ tr: {
109
+ setStoredMarks(marks: readonly Mark[] | null): Transaction;
110
+ };
111
+ }
112
+ /**
113
+ * The two ProseMirror plugin hooks that end a text color with its line.
114
+ * Splitting a block would carry the color into the next one, so after Enter
115
+ * the color's `textStyle` mark is dropped from the stored marks, unless colored
116
+ * text follows the cursor. An adapter wires them into
117
+ * `new Plugin({ props: { handleKeyDown }, appendTransaction })` and runs that
118
+ * plugin before the keymap, which handles Enter.
119
+ */
120
+ export declare function endColorOnEnter(): {
121
+ handleKeyDown<View>(_view: View, event: {
122
+ key: string;
123
+ shiftKey: boolean;
124
+ ctrlKey: boolean;
125
+ metaKey: boolean;
126
+ altKey: boolean;
127
+ }): false;
128
+ appendTransaction<Mark extends NamedMark, Transaction, PreviousState>(transactions: readonly {
129
+ docChanged: boolean;
130
+ }[], _previous: PreviousState, state: EnterState<Mark, Transaction>): Transaction | null;
131
+ };
132
+ export {};
@@ -0,0 +1,179 @@
1
+ import { isSafeImageSrc, richTextSanitizeOptions } from "./rich-text.js";
2
+ // src/rich-text/rich-text-extras.ts
3
+ var CONTROL = /[\u0000-\u001f\u007f]/g;
4
+ /** shadmin's image settings: any image, 5 MB, base64 allowed. */
5
+ var defaultRichTextImageRules = Object.freeze({
6
+ allowedMimeTypes: Object.freeze(["image/*"]),
7
+ maxFileSize: 5242880,
8
+ allowBase64: true
9
+ });
10
+ /** The `type/subtype` of a MIME type, lowercased, without parameters such as `;charset=utf-8`. */
11
+ function baseMimeType(mimeType) {
12
+ return (mimeType.split(";")[0] ?? "").trim().toLowerCase();
13
+ }
14
+ function mimeMatches(pattern, type) {
15
+ const wanted = pattern.toLowerCase();
16
+ if (wanted.endsWith("/*")) return type.startsWith(wanted.slice(0, -1));
17
+ return type === wanted;
18
+ }
19
+ /** Why an image file is refused, or `null` when it is accepted. */
20
+ function checkRichTextImageFile(file, rules) {
21
+ const type = baseMimeType(file.type);
22
+ if (type === "image/svg+xml") return "type";
23
+ if (!rules.allowedMimeTypes.some((pattern) => mimeMatches(pattern, type))) return "type";
24
+ return file.size > rules.maxFileSize ? "size" : null;
25
+ }
26
+ /** What an upload handler may return: a URL, or a URL with the stored file's id. */
27
+ function normalizeRichTextUpload(result, rules = defaultRichTextImageRules) {
28
+ const upload = typeof result === "string" ? {
29
+ id: null,
30
+ src: result
31
+ } : {
32
+ id: String(result.id),
33
+ src: result.src
34
+ };
35
+ return isSafeImageSrc(upload.src, { allowDataImages: rules.allowBase64 }) ? upload : null;
36
+ }
37
+ /** `width` rounded and held in `min`..`max`. `max` wins when it is below `min`. */
38
+ function clampRichTextImageWidth(width, { min, max }) {
39
+ return Math.min(max, Math.max(min, Number.isNaN(width) ? min : Math.round(width)));
40
+ }
41
+ var EXTENSIONS = /* @__PURE__ */ new Map([
42
+ ["jpeg", "jpg"],
43
+ ["svg+xml", "svg"],
44
+ ["x-icon", "ico"],
45
+ ["vnd.microsoft.icon", "ico"]
46
+ ]);
47
+ /** `<name>.<extension>` for downloading an image, from its alt text and mime type. */
48
+ function richTextImageFileName(alt, mimeType) {
49
+ const name = alt.replace(CONTROL, "").replace(/[/\\]/g, "").trim().replace(/^\.+/, "").trim();
50
+ const subtype = baseMimeType(mimeType).split("/")[1] ?? "";
51
+ const extension = EXTENSIONS.get(subtype) || subtype.replace(/[^a-z0-9]/g, "") || "png";
52
+ return `${name || "image"}.${extension}`;
53
+ }
54
+ function imageAction(id, defaultLabel) {
55
+ return Object.freeze({
56
+ id,
57
+ labelKey: `chelone.rich_text.image.${id}`,
58
+ defaultLabel
59
+ });
60
+ }
61
+ /** The image popover's actions, in display order. */
62
+ var richTextImageActions = Object.freeze([
63
+ imageAction("view", "View image"),
64
+ imageAction("download", "Download image"),
65
+ imageAction("copyImage", "Copy image"),
66
+ imageAction("copyLink", "Copy link"),
67
+ imageAction("remove", "Remove image")
68
+ ]);
69
+ var COLOR_NAMES = [
70
+ "red",
71
+ "orange",
72
+ "yellow",
73
+ "green",
74
+ "blue",
75
+ "purple",
76
+ "pink"
77
+ ];
78
+ function color(id, palette, defaultLabel, value) {
79
+ return Object.freeze({
80
+ id,
81
+ palette,
82
+ labelKey: `chelone.rich_text.color.${id}`,
83
+ defaultLabel,
84
+ value
85
+ });
86
+ }
87
+ function palette(name, label) {
88
+ return COLOR_NAMES.map((base) => {
89
+ const id = `${name}-${base}`;
90
+ return color(id, name, `${label} ${base}`, `var(--rich-text-color-${id})`);
91
+ });
92
+ }
93
+ /**
94
+ * The color picker's entries: `default` (clears the color), then three
95
+ * palettes of seven. The adapter's stylesheet defines each
96
+ * `--rich-text-color-<id>`.
97
+ */
98
+ var richTextColors = Object.freeze([
99
+ color("default", "default", "Default", null),
100
+ ...palette("bright", "Bright"),
101
+ ...palette("soft", "Soft"),
102
+ ...palette("muted", "Muted")
103
+ ]);
104
+ var NUMBER = String.raw`[+-]?(?:\d+\.?\d*|\.\d+)%?`;
105
+ var SEPARATOR = String.raw`(?:\s*[,/]\s*|\s+)`;
106
+ var HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
107
+ var FUNCTION_COLOR = new RegExp(String.raw`^(?:rgba?|hsla?)\(\s*${NUMBER}(?:${SEPARATOR}${NUMBER}){2,3}\s*\)$`, "i");
108
+ var PALETTE_VALUES = new Set(richTextColors.flatMap((entry) => entry.value ? [entry.value] : []));
109
+ function isSafeColorValue(value) {
110
+ return PALETTE_VALUES.has(value) || HEX_COLOR.test(value) || FUNCTION_COLOR.test(value);
111
+ }
112
+ /**
113
+ * `color: <value>` when `style` is one `color` declaration with a palette
114
+ * `var(...)`, hex, `rgb()`, `rgba()`, `hsl()` or `hsla()` value, else `null`.
115
+ * Everything else a style attribute can carry is dropped.
116
+ */
117
+ function sanitizeRichTextStyle(style) {
118
+ const [declaration, ...rest] = style.split(";").map((declaration) => declaration.trim()).filter((declaration) => declaration !== "");
119
+ if (declaration === void 0 || rest.length > 0) return null;
120
+ const colon = declaration.indexOf(":");
121
+ if (colon === -1) return null;
122
+ if (declaration.slice(0, colon).trim().toLowerCase() !== "color") return null;
123
+ const value = declaration.slice(colon + 1).trim();
124
+ return isSafeColorValue(value) ? `color: ${value}` : null;
125
+ }
126
+ /**
127
+ * `richTextSanitizeOptions` with `style` allowed through, for adapters that
128
+ * run `sanitizeRichTextStyle` on every `style` attribute.
129
+ */
130
+ var richTextStyledSanitizeOptions = Object.freeze({
131
+ FORBID_TAGS: richTextSanitizeOptions.FORBID_TAGS,
132
+ FORBID_ATTR: Object.freeze(richTextSanitizeOptions.FORBID_ATTR.filter((attribute) => attribute !== "style"))
133
+ });
134
+ /** Options for tiptap's `CodeBlockLowlight.configure`, as shadmin sets them. */
135
+ var richTextCodeBlockOptions = Object.freeze({
136
+ languageClassPrefix: "language-",
137
+ defaultLanguage: null,
138
+ exitOnTripleEnter: true,
139
+ exitOnArrowDown: true,
140
+ enableTabIndentation: false,
141
+ tabSize: 4
142
+ });
143
+ function extraItem(id, group, defaultLabel) {
144
+ return Object.freeze({
145
+ id,
146
+ group,
147
+ labelKey: `chelone.rich_text.${id}`,
148
+ defaultLabel
149
+ });
150
+ }
151
+ /** The toolbar buttons an image or color extension adds, shaped like `richTextToolbarItems`. */
152
+ var richTextExtraToolbarItems = Object.freeze([extraItem("image", "insert", "Image"), extraItem("color", "mark", "Text color")]);
153
+ /**
154
+ * The two ProseMirror plugin hooks that end a text color with its line.
155
+ * Splitting a block would carry the color into the next one, so after Enter
156
+ * the color's `textStyle` mark is dropped from the stored marks, unless colored
157
+ * text follows the cursor. An adapter wires them into
158
+ * `new Plugin({ props: { handleKeyDown }, appendTransaction })` and runs that
159
+ * plugin before the keymap, which handles Enter.
160
+ */
161
+ function endColorOnEnter() {
162
+ let enterPressed = false;
163
+ return {
164
+ handleKeyDown(_view, event) {
165
+ enterPressed = event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey;
166
+ return false;
167
+ },
168
+ appendTransaction(transactions, _previous, state) {
169
+ if (!enterPressed || !transactions.some((tr) => tr.docChanged)) return null;
170
+ enterPressed = false;
171
+ const { $from } = state.selection;
172
+ if (($from.nodeAfter?.marks ?? []).some((mark) => mark.type.name === "textStyle")) return null;
173
+ const marks = state.storedMarks ?? $from.marks();
174
+ const kept = marks.filter((mark) => mark.type.name !== "textStyle");
175
+ return kept.length === marks.length ? null : state.tr.setStoredMarks(kept);
176
+ }
177
+ };
178
+ }
179
+ export { checkRichTextImageFile, clampRichTextImageWidth, defaultRichTextImageRules, endColorOnEnter, normalizeRichTextUpload, richTextCodeBlockOptions, richTextColors, richTextExtraToolbarItems, richTextImageActions, richTextImageFileName, richTextStyledSanitizeOptions, sanitizeRichTextStyle };