@supersoniks/concorde 4.5.2 → 4.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 (185) hide show
  1. package/.gitlab-ci.yml +23 -0
  2. package/README.md +106 -55
  3. package/ai/AGENTS.md +52 -0
  4. package/ai/README.md +30 -0
  5. package/ai/cursor/rules/concorde-menu.mdc +15 -0
  6. package/ai/cursor/rules/concorde-scope.mdc +14 -0
  7. package/ai/cursor/rules/concorde-theme.mdc +13 -0
  8. package/ai/cursor/rules/concorde.mdc +49 -0
  9. package/ai/jetbrains/rules/concorde.md +39 -0
  10. package/ai/skills/concorde/SKILL.md +220 -0
  11. package/ai/skills/concorde-get-set-dp/SKILL.md +194 -0
  12. package/ai/skills/concorde-imports/SKILL.md +78 -0
  13. package/ai/skills/concorde-menu/SKILL.md +74 -0
  14. package/ai/skills/concorde-scope/SKILL.md +70 -0
  15. package/ai/skills/concorde-theme/SKILL.md +46 -0
  16. package/build-infos.json +1 -1
  17. package/concorde-core.bundle.js +159 -159
  18. package/concorde-core.es.js +1915 -1809
  19. package/dist/altcha-widget.js +2662 -0
  20. package/dist/concorde-core.bundle.js +159 -159
  21. package/dist/concorde-core.es.js +1915 -1809
  22. package/dist/docs-mock-api-sw.js +589 -0
  23. package/dist/docs-mock-api-sw.js.map +7 -0
  24. package/docs/altcha-widget.js +2662 -0
  25. package/docs/assets/index-D9pxaQYK.js +7508 -0
  26. package/docs/assets/index-t0-i22oI.css +1 -0
  27. package/docs/docs-mock-api-sw.js +589 -0
  28. package/docs/docs-mock-api-sw.js.map +7 -0
  29. package/docs/index.html +2 -2
  30. package/docs/src/core/components/functional/fetch/fetch.md +13 -11
  31. package/docs/src/core/components/functional/if/if.md +4 -11
  32. package/docs/src/core/components/functional/list/list.md +60 -194
  33. package/docs/src/core/components/functional/queue/queue.md +70 -85
  34. package/docs/src/core/components/functional/router/router.md +62 -97
  35. package/docs/src/core/components/functional/states/states.md +2 -2
  36. package/docs/src/core/components/functional/submit/submit.md +86 -55
  37. package/docs/src/core/components/ui/captcha/captcha.md +2 -2
  38. package/docs/src/core/components/ui/card/card.md +1 -1
  39. package/docs/src/core/components/ui/form/checkbox/checkbox.md +5 -32
  40. package/docs/src/core/components/ui/form/input/input.md +5 -30
  41. package/docs/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +6 -4
  42. package/docs/src/core/components/ui/form/radio/radio.md +5 -32
  43. package/docs/src/core/components/ui/form/select/select.md +5 -31
  44. package/docs/src/core/components/ui/form/switch/switch.md +5 -32
  45. package/docs/src/core/components/ui/loader/loader.md +1 -13
  46. package/docs/src/core/components/ui/table/table.md +3 -3
  47. package/docs/src/docs/_core-concept/dataFlow.md +73 -0
  48. package/docs/src/docs/_core-concept/subscriber.md +9 -10
  49. package/docs/src/docs/_decorators/ancestor-attribute.md +4 -3
  50. package/docs/src/docs/_decorators/auto-subscribe.md +19 -16
  51. package/docs/src/docs/_decorators/bind.md +20 -17
  52. package/docs/src/docs/_decorators/get.md +7 -4
  53. package/docs/src/docs/_decorators/handle.md +171 -0
  54. package/docs/src/docs/_decorators/on-assign.md +99 -73
  55. package/docs/src/docs/_decorators/publish.md +2 -1
  56. package/docs/src/docs/_decorators/subscribe.md +70 -9
  57. package/docs/src/docs/_decorators/wait-for-ancestors.md +13 -10
  58. package/docs/src/docs/_directives/sub.md +91 -0
  59. package/docs/src/docs/_getting-started/ai-agents.md +56 -0
  60. package/docs/src/docs/_getting-started/concorde-manual-install.md +133 -0
  61. package/docs/src/docs/_getting-started/concorde-outside.md +13 -123
  62. package/docs/src/docs/_getting-started/create-a-component.md +2 -0
  63. package/docs/src/docs/_getting-started/my-first-component.md +236 -0
  64. package/docs/src/docs/_getting-started/my-first-subscriber.md +29 -83
  65. package/docs/src/docs/_getting-started/pubsub.md +21 -134
  66. package/docs/src/docs/_getting-started/start.md +26 -18
  67. package/docs/src/docs/_misc/api-configuration.md +79 -0
  68. package/docs/src/docs/_misc/dataProviderKey.md +38 -5
  69. package/docs/src/docs/_misc/docs-mock-api.md +60 -0
  70. package/docs/src/docs/_misc/endpoint.md +2 -1
  71. package/docs/src/docs/_misc/html-integration.md +13 -0
  72. package/docs/src/docs/search/docs-search.json +4163 -873
  73. package/docs/src/tsconfig.json +380 -317
  74. package/gitlab/job_tests.sh +55 -0
  75. package/package.json +37 -3
  76. package/public/altcha-widget.js +2662 -0
  77. package/public/docs-mock-api-sw.js +589 -0
  78. package/public/docs-mock-api-sw.js.map +7 -0
  79. package/scripts/ai-init.mjs +167 -0
  80. package/scripts/docs-mock-api-vite-plugin.ts +116 -0
  81. package/scripts/docs-open-in-editor-plugin.ts +130 -0
  82. package/scripts/pre-publish.mjs +2 -1
  83. package/src/core/components/functional/example/example.ts +1 -1
  84. package/src/core/components/functional/fetch/fetch.md +13 -11
  85. package/src/core/components/functional/if/if.md +4 -11
  86. package/src/core/components/functional/list/list.demo.ts +4 -4
  87. package/src/core/components/functional/list/list.md +60 -194
  88. package/src/core/components/functional/list/list.ts +8 -7
  89. package/src/core/components/functional/queue/queue.demo.ts +1 -1
  90. package/src/core/components/functional/queue/queue.md +70 -85
  91. package/src/core/components/functional/queue/queue.ts +4 -4
  92. package/src/core/components/functional/router/router.md +62 -97
  93. package/src/core/components/functional/router/router.ts +1 -1
  94. package/src/core/components/functional/states/states.md +2 -2
  95. package/src/core/components/functional/submit/submit.md +86 -55
  96. package/src/core/components/functional/submit/submit.ts +10 -3
  97. package/src/core/components/ui/captcha/captcha.md +2 -2
  98. package/src/core/components/ui/card/card.md +1 -1
  99. package/src/core/components/ui/form/checkbox/checkbox.md +5 -32
  100. package/src/core/components/ui/form/input/input.md +5 -30
  101. package/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +6 -4
  102. package/src/core/components/ui/form/radio/radio.md +5 -32
  103. package/src/core/components/ui/form/select/select.md +5 -31
  104. package/src/core/components/ui/form/switch/switch.md +5 -32
  105. package/src/core/components/ui/loader/loader.md +1 -13
  106. package/src/core/components/ui/table/table.md +3 -3
  107. package/src/core/decorators/Subscriber.ts +2 -0
  108. package/src/core/decorators/subscriber/handle.disambig.spec.ts +20 -0
  109. package/src/core/decorators/subscriber/handle.skip.spec.ts +37 -0
  110. package/src/core/decorators/subscriber/handle.ts +128 -0
  111. package/src/core/decorators/subscriber/onAssign.ts +94 -4
  112. package/src/core/directives/DataProvider.sub.spec.ts +96 -0
  113. package/src/core/directives/DataProvider.ts +109 -40
  114. package/src/core/utils/PublisherProxy.ts +33 -18
  115. package/src/core/utils/dataProviderKey.ts +23 -0
  116. package/src/core/utils/publisherPathKey.spec.ts +58 -0
  117. package/src/decorators.ts +6 -0
  118. package/src/docs/_core-concept/dataFlow.md +73 -0
  119. package/src/docs/_core-concept/subscriber.md +9 -10
  120. package/src/docs/_decorators/ancestor-attribute.md +4 -3
  121. package/src/docs/_decorators/auto-subscribe.md +19 -16
  122. package/src/docs/_decorators/bind.md +20 -17
  123. package/src/docs/_decorators/get.md +7 -4
  124. package/src/docs/_decorators/handle.md +171 -0
  125. package/src/docs/_decorators/on-assign.md +99 -47
  126. package/src/docs/_decorators/publish.md +2 -1
  127. package/src/docs/_decorators/subscribe.md +70 -9
  128. package/src/docs/_decorators/wait-for-ancestors.md +13 -10
  129. package/src/docs/_directives/sub.md +91 -0
  130. package/src/docs/_getting-started/ai-agents.md +56 -0
  131. package/src/docs/_getting-started/concorde-manual-install.md +133 -0
  132. package/src/docs/_getting-started/concorde-outside.md +13 -123
  133. package/src/docs/_getting-started/create-a-component.md +2 -0
  134. package/src/docs/_getting-started/my-first-component.md +236 -0
  135. package/src/docs/_getting-started/my-first-subscriber.md +29 -83
  136. package/src/docs/_getting-started/pubsub.md +21 -134
  137. package/src/docs/_getting-started/start.md +26 -18
  138. package/src/docs/_misc/api-configuration.md +79 -0
  139. package/src/docs/_misc/dataProviderKey.md +38 -5
  140. package/src/docs/_misc/docs-mock-api.md +60 -0
  141. package/src/docs/_misc/endpoint.md +2 -1
  142. package/src/docs/_misc/html-integration.md +13 -0
  143. package/src/docs/code.ts +58 -12
  144. package/src/docs/components/docs-demo-sources.ts +397 -0
  145. package/src/docs/components/docs-lit-demo-raw.ts +28 -0
  146. package/src/docs/components/docs-lit-demo.ts +166 -0
  147. package/src/docs/components/docs-source-link.ts +72 -0
  148. package/src/docs/docs-location.ts +54 -0
  149. package/src/docs/docs.ts +12 -0
  150. package/src/docs/example/decorators-demo-bind-demos.ts +41 -46
  151. package/src/docs/example/decorators-demo-geo.ts +16 -11
  152. package/src/docs/example/decorators-demo-init.ts +2 -228
  153. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +142 -12
  154. package/src/docs/example/decorators-demo.ts +71 -70
  155. package/src/docs/example/docs-api-config-demos.ts +234 -0
  156. package/src/docs/example/docs-joke-demos.ts +297 -0
  157. package/src/docs/example/docs-list-demos.ts +179 -0
  158. package/src/docs/example/docs-provider-keys.ts +315 -0
  159. package/src/docs/example/docs-queue-demos.ts +114 -0
  160. package/src/docs/example/docs-router-demos.ts +89 -0
  161. package/src/docs/example/docs-submit-demos.ts +455 -0
  162. package/src/docs/example/docs-toggle-demos.ts +73 -0
  163. package/src/docs/example/docs-user-two-scopes.ts +37 -0
  164. package/src/docs/example/docs-users-list.ts +71 -0
  165. package/src/docs/example/users.ts +41 -24
  166. package/src/docs/mock-api/api-config-mock.ts +152 -0
  167. package/src/docs/mock-api/fixtures.ts +377 -0
  168. package/src/docs/mock-api/register.ts +25 -0
  169. package/src/docs/mock-api/router.ts +234 -0
  170. package/src/docs/mock-api/service-worker.ts +23 -0
  171. package/src/docs/mock-api/urls.ts +11 -0
  172. package/src/docs/navigation/navigation.ts +43 -7
  173. package/src/docs/search/docs-search.json +4193 -858
  174. package/src/docs/search/markdown-renderer.ts +7 -3
  175. package/src/docs/search/page.ts +11 -14
  176. package/src/docs/search/sonic-code-markdown.spec.ts +29 -0
  177. package/src/docs/search/sonic-code-markdown.ts +28 -0
  178. package/src/docs.ts +4 -0
  179. package/src/tsconfig.json +96 -0
  180. package/src/tsconfig.tsbuildinfo +1 -1
  181. package/vite.config.mts +8 -0
  182. package/docs/assets/index-CaysOMFz.js +0 -5046
  183. package/docs/assets/index-D8mGoXzF.css +0 -1
  184. package/docs/src/docs/_misc/templates-demo.md +0 -19
  185. package/src/docs/_misc/templates-demo.md +0 -19
@@ -2,21 +2,32 @@ import { AsyncDirective, PartInfo } from "lit/async-directive.js";
2
2
  import { directive } from "lit/directive.js";
3
3
  import { noChange } from "lit";
4
4
  import { SearchableDomElement } from "../utils/HTML";
5
- import {
5
+ import {
6
+ extractDynamicDependencies,
7
+ hasPath,
8
+ resolveDynamicPath,
9
+ } from "../decorators/subscriber/dynamicPath";
10
+ import {
11
+ bindDynamicWatchKeys,
12
+ observeDynamicProperty,
13
+ } from "../decorators/subscriber/dynamicPropertyWatch";
14
+ import {
6
15
  getObservables,
7
- get as pubGet, set as pubSet, dataProvider as pubDataProvider ,
8
- dp as pubDp, DataProvider
16
+ get as pubGet,
17
+ set as pubSet,
18
+ dataProvider as pubDataProvider,
19
+ dp as pubDp,
20
+ DataProvider,
9
21
  } from "../utils/PublisherProxy";
10
22
 
23
+ export type SubPathInput = string | {path: string};
11
24
 
12
25
  class ObserveDirective extends AsyncDirective {
13
26
  observables: Set<DataProvider<any>> = new Set();
14
- unsubscribe(): void {
15
- this.observables.forEach((publisher: DataProvider<any>) =>
16
- publisher.offAssign(this.onAssign)
17
- );
18
- }
19
- observable?: string;
27
+ pathTemplate?: string;
28
+ /** Chemin résolu actuellement abonné */
29
+ resolvedPath?: string;
30
+ cleanupWatchers: Array<() => void> = [];
20
31
  node?: SearchableDomElement;
21
32
 
22
33
  /* eslint-disable @typescript-eslint/no-explicit-any*/
@@ -26,21 +37,87 @@ class ObserveDirective extends AsyncDirective {
26
37
  }
27
38
  /* eslint-enable @typescript-eslint/no-explicit-any*/
28
39
 
29
- render(observable: string) {
30
- if (this.observable !== observable) {
31
- this.observable = observable;
32
- if (this.isConnected) {
33
- this.subscribe(observable);
34
- }
40
+ private teardownWatchers() {
41
+ this.cleanupWatchers.forEach((cleanup) => cleanup());
42
+ this.cleanupWatchers = [];
43
+ }
44
+
45
+ private normalizeInput(input: SubPathInput): string {
46
+ return hasPath(input) ? input.path : input;
47
+ }
48
+
49
+ render(input: SubPathInput) {
50
+ const path = this.normalizeInput(input);
51
+ const pathChanged = this.pathTemplate !== path;
52
+ if (pathChanged) {
53
+ this.pathTemplate = path;
54
+ this.teardownWatchers();
55
+ }
56
+ if (pathChanged || !this.resolvedPath) {
57
+ queueMicrotask(() => {
58
+ if (!this.isConnected || !this.pathTemplate) return;
59
+ this.setupSubscription();
60
+ });
35
61
  }
36
62
  return noChange;
37
63
  }
38
64
 
65
+ private setupSubscription() {
66
+ const host = this.node;
67
+ const path = this.pathTemplate;
68
+ if (!host || !path) return;
69
+
70
+ this.teardownWatchers();
71
+ const dependencies = extractDynamicDependencies(path);
72
+ if (dependencies.length > 0) {
73
+ for (const dependency of dependencies) {
74
+ this.cleanupWatchers.push(
75
+ observeDynamicProperty(
76
+ bindDynamicWatchKeys.watcherStore,
77
+ bindDynamicWatchKeys.hooked,
78
+ host,
79
+ dependency,
80
+ () => this.refreshSubscription(),
81
+ ),
82
+ );
83
+ }
84
+ }
85
+ this.refreshSubscription();
86
+ }
87
+
88
+ private refreshSubscription() {
89
+ const host = this.node;
90
+ const path = this.pathTemplate;
91
+ if (!path) return;
92
+
93
+ const isDynamic = extractDynamicDependencies(path).length > 0;
94
+ let resolved: string | null = path;
95
+ if (isDynamic) {
96
+ if (!host) {
97
+ resolved = null;
98
+ } else {
99
+ const resolution = resolveDynamicPath(host, path);
100
+ resolved = resolution.ready ? resolution.path : null;
101
+ }
102
+ }
103
+
104
+ if (resolved === this.resolvedPath) return;
105
+
106
+ if (!resolved) {
107
+ this.unsubscribe();
108
+ this.resolvedPath = undefined;
109
+ this.setValue(undefined);
110
+ return;
111
+ }
112
+
113
+ this.resolvedPath = resolved;
114
+ this.subscribe(resolved);
115
+ }
116
+
39
117
  onAssign = (v: unknown) => {
40
118
  this.setValue(v);
41
119
  };
42
- // Subscribes to the observable, calling the directive's asynchronous
43
- // setValue API each time the value changes
120
+
44
121
  subscribe<T>(observable: string) {
45
122
  this.unsubscribe();
46
123
  this.onAssign = (v: unknown) => {
@@ -52,55 +129,47 @@ class ObserveDirective extends AsyncDirective {
52
129
  publisher.onAssign(this.onAssign);
53
130
  });
54
131
  }
55
- // When the directive is disconnected from the DOM, unsubscribe to ensure
56
- // the directive instance can be garbage collected
132
+
133
+ unsubscribe(): void {
134
+ this.observables.forEach((publisher: DataProvider<any>) =>
135
+ publisher.offAssign(this.onAssign),
136
+ );
137
+ this.observables.clear();
138
+ }
139
+
57
140
  disconnected() {
141
+ this.teardownWatchers();
58
142
  this.unsubscribe();
143
+ this.resolvedPath = undefined;
59
144
  }
60
- // If the subtree the directive is in was disconnected and subsequently
61
- // re-connected, re-subscribe to make the directive operable again
145
+
62
146
  reconnected() {
63
- if (!this.observable) return;
64
- this.subscribe(this.observable);
147
+ if (!this.pathTemplate) return;
148
+ this.setupSubscription();
65
149
  }
66
150
  }
151
+
67
152
  const dir = directive(ObserveDirective);
68
- //
69
- //autoUpdate directive
153
+
70
154
  export const subscribe = dir;
71
155
  export const sub = dir;
72
156
 
73
-
74
157
  /**
75
158
  * @deprecated @see {@link "/src/core/utils/PublisherProxy.ts#get"}
76
- * @param id Observable
77
- * @returns value of the observable
78
- *
79
159
  */
80
160
  export const get: <T = any>(id: string) => T = pubGet;
81
161
 
82
-
83
-
84
162
  /**
85
163
  * @deprecated @see {@link "/src/core/utils/PublisherProxy.ts#dataProvider"}
86
- * @param id Observable
87
- * @param defaultValue Optional default value
88
- * @returns Observable
89
164
  */
90
165
  export const dataProvider = pubDataProvider;
91
166
 
92
167
  /**
93
168
  * @deprecated @see {@link "/src/core/utils/PublisherProxy.ts#dp"}
94
- * @param id Observable
95
- * @param defaultValue Optional default value
96
- * @returns Observable
97
169
  */
98
170
  export const dp = pubDp;
99
171
 
100
172
  /**
101
173
  * @deprecated @see {@link "/src/core/utils/PublisherProxy.ts#set"}
102
- * @param id Observable
103
- * @param value value to set
104
- * @returns void
105
174
  */
106
175
  export const set: <T = any>(id: string, value: T) => void = pubSet;
@@ -9,6 +9,8 @@ import type {
9
9
  CoreJSType,
10
10
  PublisherContentType,
11
11
  } from "../_types/types";
12
+ import type { DataProviderKey } from "./dataProviderKey";
13
+ import { resolveStaticPublisherPath } from "./dataProviderKey";
12
14
  import HTML from "./HTML";
13
15
  import Objects from "./Objects";
14
16
  import { sonicClassPrefix } from "./Utils";
@@ -1058,13 +1060,17 @@ export const getObservables = <T = any>(
1058
1060
  return new Set<DataProvider<T>>([observable as DataProvider<T>]);
1059
1061
  };
1060
1062
 
1061
- // get value
1062
- export const get = <T = any>(id: string) => {
1063
- return getObservables<T>(id).values().next().value?.get() as T;
1064
- };
1063
+ // get value (snapshot at call time)
1064
+ export function get<T>(id: DataProviderKey<T>): T;
1065
+ export function get<T = any>(id: string): T;
1066
+ export function get<T>(id: string | DataProviderKey<T>): T {
1067
+ const path = resolveStaticPublisherPath(id);
1068
+ return getObservables<T>(path).values().next().value?.get() as T;
1069
+ }
1065
1070
 
1066
- const deepee = <T = any>(id: string) => {
1067
- const value = getObservables<T>(id).values().next().value as DataProvider<T>;
1071
+ function deepee<T>(id: string | DataProviderKey<T>, _defaultValue?: T) {
1072
+ const path = resolveStaticPublisherPath(id);
1073
+ const value = getObservables<T>(path).values().next().value as DataProvider<T>;
1068
1074
  // if (defaultValue !== undefined && value) {
1069
1075
  // const innerValue = value.get();
1070
1076
  // if (Objects.isEmpty(innerValue as Record<string, any>)) {
@@ -1073,20 +1079,29 @@ const deepee = <T = any>(id: string) => {
1073
1079
  // }
1074
1080
 
1075
1081
  return value;
1076
- };
1082
+ }
1077
1083
 
1078
- export const dataProvider: <T = any>(
1079
- id: string,
1084
+ export function dataProvider<T>(id: DataProviderKey<T>, defaultValue?: T): DataProvider<T>;
1085
+ export function dataProvider<T = any>(id: string, defaultValue?: T): DataProvider<T>;
1086
+ export function dataProvider<T>(
1087
+ id: string | DataProviderKey<T>,
1080
1088
  defaultValue?: T,
1081
- ) => DataProvider<T> = deepee;
1082
- export const dp = deepee;
1083
-
1084
- export const set: <T = any>(id: string, value: T) => void = <T>(
1085
- id: string,
1086
- value: T,
1087
- ) => {
1088
- getObservables(id).values().next().value?.set(value);
1089
- };
1089
+ ): DataProvider<T> {
1090
+ return deepee(id, defaultValue);
1091
+ }
1092
+
1093
+ export function dp<T>(id: DataProviderKey<T>, defaultValue?: T): DataProvider<T>;
1094
+ export function dp<T = any>(id: string, defaultValue?: T): DataProvider<T>;
1095
+ export function dp<T>(id: string | DataProviderKey<T>, defaultValue?: T): DataProvider<T> {
1096
+ return deepee(id, defaultValue);
1097
+ }
1098
+
1099
+ export function set<T>(id: DataProviderKey<T>, value: T): void;
1100
+ export function set<T = any>(id: string, value: T): void;
1101
+ export function set<T>(id: string | DataProviderKey<T>, value: T): void {
1102
+ const path = resolveStaticPublisherPath(id);
1103
+ getObservables(path).values().next().value?.set(value);
1104
+ }
1090
1105
 
1091
1106
  /**
1092
1107
  * next back handling data invalidation
@@ -93,6 +93,29 @@ export interface DataProviderKeyConstructor {
93
93
  new <T, U = any>(path: string): DataProviderKey<T, U>;
94
94
  }
95
95
 
96
+ /** Placeholders `${…}` / `{$…}` — resolved only by decorators (@subscribe, @publish, …). */
97
+ const DYNAMIC_PUBLISHER_PATH = /\$\{|\{\$/;
98
+
99
+ export function isDynamicPublisherPath(path: string): boolean {
100
+ return DYNAMIC_PUBLISHER_PATH.test(path);
101
+ }
102
+
103
+ export type PublisherPathInput = string | {path: string};
104
+
105
+ /**
106
+ * Resolves a static publisher path for get / set / dp.
107
+ * Rejects DataProviderKey templates with dynamic placeholders.
108
+ */
109
+ export function resolveStaticPublisherPath(input: PublisherPathInput): string {
110
+ const path = typeof input === "string" ? input : input.path;
111
+ if (isDynamicPublisherPath(path)) {
112
+ throw new Error(
113
+ "Static publisher path required for get/set/dp. Use @subscribe, @publish, or @handle for dynamic DataProviderKey paths.",
114
+ );
115
+ }
116
+ return path;
117
+ }
118
+
96
119
  /* eslint-disable @typescript-eslint/no-explicit-any */
97
120
  export const DataProviderKey: DataProviderKeyConstructor = function (
98
121
  this: any,
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ DataProviderKey,
4
+ isDynamicPublisherPath,
5
+ resolveStaticPublisherPath,
6
+ } from "./dataProviderKey";
7
+ import { dp, get, set } from "./PublisherProxy";
8
+
9
+ type Counter = { count: number };
10
+
11
+ describe("resolveStaticPublisherPath", () => {
12
+ it("accepte une string statique", () => {
13
+ expect(resolveStaticPublisherPath("myCounter")).toBe("myCounter");
14
+ });
15
+
16
+ it("accepte une DataProviderKey statique", () => {
17
+ const key = new DataProviderKey<Counter>("myCounter").count;
18
+ expect(resolveStaticPublisherPath(key)).toBe("myCounter.count");
19
+ });
20
+
21
+ it("rejette un chemin avec placeholder ${…}", () => {
22
+ expect(isDynamicPublisherPath("users.${userIndex}")).toBe(true);
23
+ expect(() =>
24
+ resolveStaticPublisherPath("users.${userIndex}"),
25
+ ).toThrow(/Static publisher path required/);
26
+ });
27
+
28
+ it("rejette une clé dynamique", () => {
29
+ const key = new DataProviderKey<Counter, { userIndex: number }>(
30
+ "users.${userIndex}",
31
+ );
32
+ expect(() => resolveStaticPublisherPath(key)).toThrow(
33
+ /Static publisher path required/,
34
+ );
35
+ });
36
+ });
37
+
38
+ describe("get / set / dp with DataProviderKey", () => {
39
+ const counterKey = new DataProviderKey<{ count: number }>(
40
+ "publisherPathKeySpecCounter",
41
+ );
42
+
43
+ it("set et get snapshot via clé racine", () => {
44
+ set(counterKey, { count: 0 });
45
+ expect(get(counterKey)).toEqual({ count: 0 });
46
+ });
47
+
48
+ it("dp sur sous-clé et écriture feuille", () => {
49
+ set(counterKey, { count: 0 });
50
+ dp(counterKey.count).set(3);
51
+ expect(get(counterKey)).toEqual({ count: 3 });
52
+ });
53
+
54
+ it("accepte encore un chemin string", () => {
55
+ set(`${counterKey.path}.legacy`, { count: 7 });
56
+ expect(get(`${counterKey.path}.legacy`)).toEqual({ count: 7 });
57
+ });
58
+ });
package/src/decorators.ts CHANGED
@@ -7,6 +7,11 @@ export const bind = mySubscriber.bind;
7
7
  export const publish = mySubscriber.publish;
8
8
  export const subscribe = mySubscriber.subscribe;
9
9
  export const onAssign = mySubscriber.onAssign;
10
+ export const handle = mySubscriber.handle;
11
+ export {
12
+ Skip,
13
+ type HandleOptions,
14
+ } from "@supersoniks/concorde/core/decorators/Subscriber";
10
15
  export const ancestorAttribute = mySubscriber.ancestorAttribute;
11
16
  export const autoSubscribe = mySubscriber.autoSubscribe;
12
17
  export const autoFill = mySubscriber.autoFill;
@@ -31,6 +36,7 @@ window["concorde-decorator-subscriber"] = {
31
36
  publish: mySubscriber.publish,
32
37
  subscribe: mySubscriber.subscribe,
33
38
  onAssing: mySubscriber.onAssign,
39
+ handle: mySubscriber.handle,
34
40
  ancestorAttribute: mySubscriber.ancestorAttribute,
35
41
  autoSubscribe: mySubscriber.autoSubscribe,
36
42
  autoFill: mySubscriber.autoFill,
@@ -0,0 +1,73 @@
1
+ # Data flow
2
+
3
+ Recommended patterns for new Concorde apps (Lit + TypeScript). Under the hood, data lives in a **DataProvider** store (legacy **Publisher** API: [Legacy: Sharing data](#docs/_getting-started/pubsub.md/pubsub)).
4
+
5
+ ## Quick map
6
+
7
+ | Need | Use |
8
+ |------|-----|
9
+ | Read/write in code | `get` / `set` / `dp` + `DataProviderKey` (static paths only) |
10
+ | Reactive Lit template | `sub(key)` or `@subscribe` |
11
+ | Read component state from store | `@subscribe` + `DataProviderKey<T, U>` + `@state` |
12
+ | Inherit ancestor attributes | `@ancestorAttribute` |
13
+ | Write from component state | `@publish` |
14
+ | React to assignments | `@handle` |
15
+ | HTTP GET | `@get` + `Endpoint`, or `sonic-list` / `sonic-queue` with `fetch` |
16
+ | Forms | `formDataProvider` + `name` on fields |
17
+ | Offline doc demos | `serviceURL="/docs-mock-api"` — [Local API demos](#docs/_misc/docs-mock-api.md/docs-mock-api) |
18
+
19
+ Skill: `concorde-get-set-dp` in the package `ai/` folder.
20
+
21
+ ## DataProviderKey
22
+
23
+ <sonic-code language="typescript">
24
+ <template>
25
+ import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey";
26
+ import { dp, get, set } from "@supersoniks/concorde/utils";
27
+
28
+ const cartKey = new DataProviderKey&lt;{ items: string[] }&gt;("cart");
29
+
30
+ set(cartKey, { items: [] });
31
+ dp(cartKey.items).set(["a", "b"]);
32
+ get(cartKey);
33
+ </template>
34
+ </sonic-code>
35
+
36
+ Dynamic paths (`users.${userId}`) → decorators or `sub()` — not `get("users.${id}")` in imperative code.
37
+
38
+ [DataProviderKey](#docs/_misc/dataProviderKey.md/dataProviderKey)
39
+
40
+ ## Decorators
41
+
42
+ | Decorator | Role |
43
+ |-----------|------|
44
+ | `@subscribe` | Read-only property from store — [data configuration](#docs/_getting-started/my-first-component.md/my-first-component) (type + key + scope) |
45
+ | `@publish` | Push property writes to store |
46
+ | `@handle` | Method called on assignment |
47
+ | `@ancestorAttribute` | Copy ancestor HTML attribute onto property |
48
+ | `@get` | HTTP GET into `ApiGetResult&lt;T&gt;` |
49
+
50
+ Walkthrough: [My first component](#docs/_getting-started/my-first-component.md/my-first-component)
51
+
52
+ ## HTTP and lists
53
+
54
+ - [@get](#docs/_decorators/get.md/get) — single request on a component
55
+ - [List](#core/components/functional/list/list.md/list) — `fetch` + `key="data"` + `/docs-mock-api/api/users`
56
+ - [Queue](#core/components/functional/queue/queue.md/queue) — lazy `offset=$offset&per_page=$limit` + optional `dataFilterProvider` (form → query)
57
+
58
+ ## Starter kit
59
+
60
+ ```bash
61
+ npx @supersoniks/create-concorde-ts-starter my-app
62
+ ```
63
+
64
+ Interactive routes mirror these patterns (`/concepts/*`, `/demo/*`).
65
+
66
+ ## Legacy integration
67
+
68
+ | Topic | Page |
69
+ |-------|------|
70
+ | `Subscriber` / `Fetcher` mixins on app code | [Legacy: Subscriber mixin](#docs/_core-concept/subscriber.md/subscriber), [Legacy: My first subscriber](#docs/_getting-started/my-first-subscriber.md/my-first-subscriber) |
71
+ | `data-bind` HTML (plain HTML hosts) | [HTML integration](#docs/_misc/html-integration.md/html-integration) — doc demos use Lit in `src/docs/example/` |
72
+ | `@onAssign` | [@onAssign](#docs/_decorators/on-assign.md/on-assign) (prefer `@handle`) |
73
+ | `sonic-fetch` alone | [Fetch](#core/components/functional/fetch/fetch.md/fetch) |
@@ -1,12 +1,14 @@
1
- # The subscriber mixin
1
+ # Legacy: Subscriber mixin
2
2
 
3
- This is a mixin that is commonly extended by Concorde core components and destination components. Pure UI components usually don't extend it, especially those outside of form components.
3
+ > **New app components:** [My first component](#docs/_getting-started/my-first-component.md/my-first-component) and [Data flow](#docs/_core-concept/dataFlow.md/dataFlow) extend `LitElement` with decorators (`@subscribe`, `@ancestorAttribute`, …). This mixin remains in **core** components (`sonic-list`, `sonic-fetch`, …) and [legacy tutorials](#docs/_getting-started/my-first-subscriber.md/my-first-subscriber).
4
+
5
+ The Subscriber mixin was commonly extended by Concorde core components and older destination components. Pure UI components usually don't extend it, especially those outside of form components.
4
6
 
5
7
  ## DataProvider Attribute: Automatic Filling of Subscriber Properties
6
8
 
7
9
  Upon being added to the DOM (connectedCallback), subscribers search for the first occurrence of the `dataProvider` attribute in their parent's HTML structure.
8
10
 
9
- The value of this attribute is used to obtain a publisher via the PublisherManager (see [🥨 Sharing Data](#docs/_getting-started/pubsub.md/pubsub)).
11
+ The value of this attribute is the DataProvider path (see [Data flow](#docs/_core-concept/dataFlow.md/dataFlow) and [DataProviderKey](#docs/_misc/dataProviderKey.md/dataProviderKey); legacy API: [Sharing Data](#docs/_getting-started/pubsub.md/pubsub)).
10
12
 
11
13
  The subscriber then subscribes to the publisher as a data template to be filled.
12
14
 
@@ -51,14 +53,11 @@ Suppose that:
51
53
  </sonic-code>
52
54
  - We want to keep the display of an image and its `title` attribute up to date, as well as a "contact" button to email the person in the photo. The content of the subscriber could be as follows:
53
55
 
54
- <sonic-code language="html">
55
- <template>
56
- <img data-bind ::src="$img.avatar" ::title="ucFirst|$img.caption" />
57
- <a data-bind ::href="mailto:$email" ::inner-html="$email"></a>
58
- </template>
59
- </sonic-code>
56
+ In a **Lit** app, use the same data with [data configuration](#docs/_getting-started/my-first-component.md/my-first-component) (`@subscribe` + `DataProviderKey`) on a small row component — see `docs-user` in `src/docs/example/users.ts`.
57
+
58
+ For **HTML-only** embedding (no Lit), attribute binding is described in [HTML integration](#docs/_misc/html-integration.md/html-integration) and in the Legacy [Subscriber mixin](#docs/_core-concept/subscriber.md/subscriber) pages.
60
59
 
61
- This example also illustrates:
60
+ This pattern also illustrates:
62
61
 
63
62
  - Binding to subproperties using dot syntax, as done for the `src` attribute of the `img` tag.
64
63
  - Using a simple expression to include the property within a string, as in creating a `mailto` link.
@@ -25,15 +25,15 @@ The component reads `dataProvider` and `testAttribute` from its ancestor wrapper
25
25
  import { html, LitElement } from "lit";
26
26
  import { customElement } from "lit/decorators.js";
27
27
  import { ancestorAttribute } from "@supersoniks/concorde/decorators";
28
- //
28
+
29
29
  @customElement("demo-ancestor-attribute")
30
30
  export class DemoAncestorAttribute extends LitElement {
31
31
  @ancestorAttribute("dataProvider")
32
32
  dataProvider: string | null = null;
33
- //
33
+
34
34
  @ancestorAttribute("testAttribute")
35
35
  testAttribute: string | null = null;
36
- //
36
+
37
37
  render() {
38
38
  return html`
39
39
  <section>
@@ -49,6 +49,7 @@ export class DemoAncestorAttribute extends LitElement {
49
49
  <sonic-code>
50
50
  <template>
51
51
  <div dataProvider="demoDataProvider" testAttribute="test-value-123">
52
+ <docs-demo-sources for="demo-ancestor-attribute"></docs-demo-sources>
52
53
  <demo-ancestor-attribute></demo-ancestor-attribute>
53
54
  </div>
54
55
  </template>
@@ -1,5 +1,7 @@
1
1
  # @autoSubscribe
2
2
 
3
+ > **Legacy:** prefer [@subscribe](#docs/_decorators/subscribe.md/subscribe) + `DataProviderKey`. Examples below may still show `PublisherManager` for existing codebases.
4
+
3
5
  The `@autoSubscribe` decorator automatically detects which publishers are accessed within a method and subscribes to them. When any of these publishers change, the method is automatically re-executed.
4
6
 
5
7
  ## Principle
@@ -24,10 +26,10 @@ import { autoSubscribe } from "@supersoniks/concorde/decorators";
24
26
  @customElement("demo-auto-subscribe")
25
27
  export class DemoAutoSubscribe extends LitElement {
26
28
  static styles = [tailwind];
27
- //
29
+
28
30
  @state() displayText: string = "";
29
31
  @state() computedValue: number = 0;
30
- //
32
+
31
33
  @autoSubscribe()
32
34
  updateDisplay() {
33
35
  const value1 = PublisherManager.get("autoValue1").get() || 0;
@@ -35,7 +37,7 @@ export class DemoAutoSubscribe extends LitElement {
35
37
  this.computedValue = value1 + value2;
36
38
  this.displayText = `${value1} + ${value2} = ${this.computedValue}`;
37
39
  }
38
- //
40
+
39
41
  render() {
40
42
  return html`
41
43
  <p><strong>${this.displayText}</strong></p>
@@ -49,7 +51,7 @@ export class DemoAutoSubscribe extends LitElement {
49
51
  </div>
50
52
  `;
51
53
  }
52
- //
54
+
53
55
  randomizeValue(publisherId: string) {
54
56
  const value = PublisherManager.get(publisherId);
55
57
  value.set(Math.floor(Math.random() * 100));
@@ -61,6 +63,7 @@ export class DemoAutoSubscribe extends LitElement {
61
63
 
62
64
  <sonic-code >
63
65
  <template>
66
+ <docs-demo-sources for="demo-auto-subscribe"></docs-demo-sources>
64
67
  <demo-auto-subscribe></demo-auto-subscribe>
65
68
  </template>
66
69
  </sonic-code>
@@ -75,12 +78,12 @@ export class ReactiveView extends LitElement {
75
78
  render() {
76
79
  const data = PublisherManager.get("myData");
77
80
  const config = PublisherManager.get("config");
78
- //
81
+
79
82
  // This render method will be automatically re-executed
80
83
  // when myData or config change
81
84
  const value = data.get()?.value || 0;
82
85
  const multiplier = config.get()?.multiplier || 1;
83
- //
86
+
84
87
  return html`
85
88
  <div>
86
89
  <h1>Result: ${value * multiplier}</h1>
@@ -125,40 +128,40 @@ import { html, LitElement } from "lit";
125
128
  import { customElement } from "lit/decorators.js";
126
129
  import { autoSubscribe } from "@supersoniks/concorde/decorators";
127
130
  import { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy";
128
- //
131
+
129
132
  @customElement("shopping-cart")
130
133
  export class ShoppingCart extends LitElement {
131
134
  items: any[] = [];
132
135
  total: number = 0;
133
136
  discount: number = 0;
134
- //
137
+
135
138
  @autoSubscribe()
136
139
  calculateTotal() {
137
140
  const cart = PublisherManager.get("cart");
138
141
  const promo = PublisherManager.get("promo");
139
- //
142
+
140
143
  // Access cart items
141
144
  this.items = cart.items.get() || [];
142
- //
145
+
143
146
  // Access promo code
144
147
  const promoCode = promo.code.get() || "";
145
148
  const discountPercent = promoCode === "SAVE10" ? 0.1 : 0;
146
- //
149
+
147
150
  // Calculate totals
148
151
  const subtotal = this.items.reduce((sum, item) =>
149
152
  sum + (item.price * item.quantity), 0
150
153
  );
151
154
  this.discount = subtotal * discountPercent;
152
155
  this.total = subtotal - this.discount;
153
- //
156
+
154
157
  this.requestUpdate();
155
158
  }
156
- //
159
+
157
160
  connectedCallback() {
158
161
  super.connectedCallback();
159
162
  this.calculateTotal();
160
163
  }
161
- //
164
+
162
165
  render() {
163
166
  return html`
164
167
  <div class="cart">
@@ -175,14 +178,14 @@ export class ShoppingCart extends LitElement {
175
178
  `;
176
179
  }
177
180
  }
178
- //
181
+
179
182
  // When you update the publishers, calculateTotal is automatically called:
180
183
  const cart = PublisherManager.get("cart");
181
184
  cart.items.set([
182
185
  { name: "Product 1", price: 10, quantity: 2 },
183
186
  { name: "Product 2", price: 15, quantity: 1 }
184
187
  ]);
185
- //
188
+
186
189
  const promo = PublisherManager.get("promo");
187
190
  promo.code.set("SAVE10");
188
191
  // calculateTotal will be automatically called and the UI will update