@supersoniks/concorde 4.7.3 → 4.8.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 (152) hide show
  1. package/README.md +1 -1
  2. package/ai/AGENTS.md +4 -0
  3. package/ai/cursor/rules/concorde.mdc +11 -1
  4. package/ai/jetbrains/rules/concorde.md +8 -0
  5. package/ai/skills/concorde/SKILL.md +29 -2
  6. package/ai/skills/concorde-scope/SKILL.md +2 -2
  7. package/build-infos.json +1 -1
  8. package/concorde-core.bundle.js +289 -289
  9. package/concorde-core.es.js +4839 -4546
  10. package/dist/concorde-core.bundle.js +289 -289
  11. package/dist/concorde-core.es.js +4839 -4546
  12. package/dist/docs-mock-api-sw.js +19 -0
  13. package/dist/docs-mock-api-sw.js.map +2 -2
  14. package/dist/robots.txt +2 -0
  15. package/docs/assets/index-wyNMyWT9.js +11196 -0
  16. package/docs/docs-mock-api-sw.js +19 -0
  17. package/docs/docs-mock-api-sw.js.map +2 -2
  18. package/docs/index.html +1 -1
  19. package/docs/robots.txt +2 -0
  20. package/package.json +9 -1
  21. package/public/docs-mock-api-sw.js +19 -0
  22. package/public/docs-mock-api-sw.js.map +2 -2
  23. package/public/robots.txt +2 -0
  24. package/src/core/components/functional/example/example.ts +3 -3
  25. package/src/core/components/ui/captcha/captcha.md +0 -12
  26. package/src/core/components/ui/icon/icon.ts +17 -2
  27. package/src/core/components/ui/menu/menu.ts +12 -3
  28. package/src/core/decorators/api.post.spec.ts +293 -0
  29. package/src/core/decorators/api.spec.ts +7 -14
  30. package/src/core/decorators/api.ts +648 -15
  31. package/src/core/decorators/subscriber/bind.ts +13 -5
  32. package/src/core/decorators/subscriber/dynamicPath.spec.ts +53 -0
  33. package/src/core/decorators/subscriber/dynamicPath.ts +23 -1
  34. package/src/core/decorators/subscriber/handle.ts +3 -1
  35. package/src/core/decorators/subscriber/onAssign.ts +10 -2
  36. package/src/core/decorators/subscriber/publish.ts +12 -2
  37. package/src/core/utils/PublisherProxy.ts +95 -11
  38. package/src/core/utils/api.ts +72 -3
  39. package/src/core/utils/dpOptions.spec.ts +56 -0
  40. package/src/core/utils/endpoint.ts +3 -3
  41. package/src/decorators.ts +17 -1
  42. package/src/docs/_core-concept/dataFlow.md +9 -3
  43. package/src/docs/_decorators/bind.md +2 -2
  44. package/src/docs/_decorators/get.md +13 -4
  45. package/src/docs/_decorators/handle.md +5 -1
  46. package/src/docs/_decorators/on-assign.md +2 -0
  47. package/src/docs/_decorators/patch.md +45 -0
  48. package/src/docs/_decorators/post.md +93 -0
  49. package/src/docs/_decorators/publish.md +1 -1
  50. package/src/docs/_decorators/put.md +43 -0
  51. package/src/docs/_decorators/subscribe.md +4 -1
  52. package/src/docs/_directives/sub.md +1 -1
  53. package/src/docs/_getting-started/my-first-component.md +1 -1
  54. package/src/docs/_misc/api-configuration.md +3 -1
  55. package/src/docs/_misc/dataProviderKey.md +2 -2
  56. package/src/docs/_misc/dynamic-path.md +71 -0
  57. package/src/docs/_misc/endpoint.md +5 -3
  58. package/src/docs/components/docs-demo-sources.ts +102 -3
  59. package/src/docs/components/docs-lit-demo-raw.ts +2 -26
  60. package/src/docs/components/docs-lit-demo.ts +9 -42
  61. package/src/docs/components/docs-source-excerpt.ts +53 -0
  62. package/src/docs/components/docs-source-link.ts +24 -8
  63. package/src/docs/components/docs-source-raw.ts +34 -0
  64. package/src/docs/example/decorators-demo-geo.ts +2 -2
  65. package/src/docs/example/decorators-demo-post.ts +249 -0
  66. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +5 -5
  67. package/src/docs/example/decorators-demo.ts +1 -0
  68. package/src/docs/example/docs-api-config-demos.ts +5 -5
  69. package/src/docs/mock-api/router.ts +20 -0
  70. package/src/docs/navigation/navigation.ts +16 -0
  71. package/src/docs/search/docs-search.json +540 -15
  72. package/src/tsconfig.json +24 -0
  73. package/src/tsconfig.tsbuildinfo +1 -1
  74. package/vite.config.mts +1 -1
  75. package/docs/assets/index-D9pxaQYK.js +0 -7508
  76. package/docs/src/core/components/functional/date/date.md +0 -290
  77. package/docs/src/core/components/functional/fetch/fetch.md +0 -125
  78. package/docs/src/core/components/functional/if/if.md +0 -9
  79. package/docs/src/core/components/functional/list/list.md +0 -65
  80. package/docs/src/core/components/functional/mix/mix.md +0 -41
  81. package/docs/src/core/components/functional/queue/queue.md +0 -72
  82. package/docs/src/core/components/functional/router/router.md +0 -94
  83. package/docs/src/core/components/functional/sdui/default-library.json +0 -108
  84. package/docs/src/core/components/functional/sdui/example.json +0 -99
  85. package/docs/src/core/components/functional/sdui/sdui.md +0 -356
  86. package/docs/src/core/components/functional/states/states.md +0 -87
  87. package/docs/src/core/components/functional/submit/submit.md +0 -114
  88. package/docs/src/core/components/functional/subscriber/subscriber.md +0 -91
  89. package/docs/src/core/components/functional/value/value.md +0 -35
  90. package/docs/src/core/components/ui/alert/alert.md +0 -121
  91. package/docs/src/core/components/ui/alert-messages/alert-messages.md +0 -0
  92. package/docs/src/core/components/ui/badge/badge.md +0 -127
  93. package/docs/src/core/components/ui/button/button.md +0 -182
  94. package/docs/src/core/components/ui/captcha/captcha.md +0 -24
  95. package/docs/src/core/components/ui/card/card.md +0 -97
  96. package/docs/src/core/components/ui/divider/divider.md +0 -35
  97. package/docs/src/core/components/ui/form/checkbox/checkbox.md +0 -77
  98. package/docs/src/core/components/ui/form/fieldset/fieldset.md +0 -129
  99. package/docs/src/core/components/ui/form/form-actions/form-actions.md +0 -77
  100. package/docs/src/core/components/ui/form/form-layout/form-layout.md +0 -44
  101. package/docs/src/core/components/ui/form/input/input.md +0 -142
  102. package/docs/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +0 -133
  103. package/docs/src/core/components/ui/form/radio/radio.md +0 -57
  104. package/docs/src/core/components/ui/form/select/select.md +0 -71
  105. package/docs/src/core/components/ui/form/switch/switch.md +0 -57
  106. package/docs/src/core/components/ui/form/textarea/textarea.md +0 -65
  107. package/docs/src/core/components/ui/group/group.md +0 -75
  108. package/docs/src/core/components/ui/icon/icon.md +0 -125
  109. package/docs/src/core/components/ui/icon/icons.json +0 -1
  110. package/docs/src/core/components/ui/image/image.md +0 -107
  111. package/docs/src/core/components/ui/link/link.md +0 -43
  112. package/docs/src/core/components/ui/loader/loader.md +0 -55
  113. package/docs/src/core/components/ui/menu/menu.md +0 -329
  114. package/docs/src/core/components/ui/modal/modal.md +0 -119
  115. package/docs/src/core/components/ui/pop/pop.md +0 -96
  116. package/docs/src/core/components/ui/progress/progress.md +0 -63
  117. package/docs/src/core/components/ui/table/table.md +0 -455
  118. package/docs/src/core/components/ui/toast/toast.md +0 -166
  119. package/docs/src/core/components/ui/tooltip/tooltip.md +0 -82
  120. package/docs/src/docs/_core-concept/dataFlow.md +0 -73
  121. package/docs/src/docs/_core-concept/overview.md +0 -57
  122. package/docs/src/docs/_core-concept/subscriber.md +0 -75
  123. package/docs/src/docs/_decorators/ancestor-attribute.md +0 -79
  124. package/docs/src/docs/_decorators/auto-subscribe.md +0 -202
  125. package/docs/src/docs/_decorators/bind.md +0 -167
  126. package/docs/src/docs/_decorators/get.md +0 -68
  127. package/docs/src/docs/_decorators/handle.md +0 -171
  128. package/docs/src/docs/_decorators/on-assign.md +0 -388
  129. package/docs/src/docs/_decorators/publish.md +0 -55
  130. package/docs/src/docs/_decorators/subscribe.md +0 -97
  131. package/docs/src/docs/_decorators/wait-for-ancestors.md +0 -163
  132. package/docs/src/docs/_directives/sub.md +0 -91
  133. package/docs/src/docs/_getting-started/ai-agents.md +0 -56
  134. package/docs/src/docs/_getting-started/concorde-manual-install.md +0 -133
  135. package/docs/src/docs/_getting-started/concorde-outside.md +0 -33
  136. package/docs/src/docs/_getting-started/create-a-component.md +0 -139
  137. package/docs/src/docs/_getting-started/my-first-component.md +0 -236
  138. package/docs/src/docs/_getting-started/my-first-subscriber.md +0 -120
  139. package/docs/src/docs/_getting-started/pubsub.md +0 -37
  140. package/docs/src/docs/_getting-started/start.md +0 -47
  141. package/docs/src/docs/_getting-started/theming.md +0 -91
  142. package/docs/src/docs/_misc/api-configuration.md +0 -79
  143. package/docs/src/docs/_misc/dataProviderKey.md +0 -168
  144. package/docs/src/docs/_misc/docs-mock-api.md +0 -60
  145. package/docs/src/docs/_misc/endpoint.md +0 -43
  146. package/docs/src/docs/_misc/html-integration.md +0 -13
  147. package/docs/src/docs/search/docs-search.json +0 -8532
  148. package/docs/src/tag-list.json +0 -1
  149. package/docs/src/tsconfig-model.json +0 -23
  150. package/docs/src/tsconfig.json +0 -1050
  151. package/php/get-challenge.php +0 -34
  152. package/php/some-service.php +0 -42
@@ -4,10 +4,15 @@ import type {
4
4
  } from "../../utils/dataProviderKey";
5
5
  import { ConnectedComponent, setSubscribable } from "./common";
6
6
  import {
7
+ type DynamicPathOptions,
7
8
  extractDynamicDependencies,
8
9
  hasPath,
9
10
  resolveDynamicPath,
10
11
  } from "./dynamicPath";
12
+
13
+ export type BindOptions = DynamicPathOptions & {
14
+ reflect?: boolean;
15
+ };
11
16
  import {
12
17
  bindDynamicWatchKeys,
13
18
  observeDynamicProperty,
@@ -16,9 +21,12 @@ import { getPublisherFromPath } from "./publisherPath";
16
21
 
17
22
  function bindImpl(
18
23
  path: string,
19
- options?: { reflect?: boolean },
24
+ options?: BindOptions,
20
25
  ): (target: unknown, propertyKey: string) => void {
21
26
  const reflect = options?.reflect ?? false;
27
+ const pathOptions: DynamicPathOptions = {
28
+ skipEmptyPlaceholder: options?.skipEmptyPlaceholder,
29
+ };
22
30
  const dynamicDependencies = extractDynamicDependencies(path);
23
31
  const isDynamicPath = dynamicDependencies.length > 0;
24
32
 
@@ -139,7 +147,7 @@ function bindImpl(
139
147
 
140
148
  const refreshSubscription = () => {
141
149
  if (isDynamicPath) {
142
- const resolution = resolveDynamicPath(component, path);
150
+ const resolution = resolveDynamicPath(component, path, pathOptions);
143
151
  if (!resolution.ready) {
144
152
  subscribeToPath(null);
145
153
  return;
@@ -201,18 +209,18 @@ function bindImpl(
201
209
  */
202
210
  export function bind(
203
211
  path: string,
204
- options?: { reflect?: boolean },
212
+ options?: BindOptions,
205
213
  ): (target: unknown, propertyKey: string) => void;
206
214
  export function bind<T, U = any>(
207
215
  key: DataProviderKey<T, U>,
208
- options?: { reflect?: boolean },
216
+ options?: BindOptions,
209
217
  ): <K extends string>(
210
218
  target: DataProviderKeyHost<U> & { [P in K]?: T | null | undefined },
211
219
  propertyKey: K,
212
220
  ) => void;
213
221
  export function bind(
214
222
  pathOrKey: string | DataProviderKey<unknown, unknown>,
215
- options?: { reflect?: boolean },
223
+ options?: BindOptions,
216
224
  ): (target: unknown, propertyKey: string) => void {
217
225
  const path = hasPath(pathOrKey) ? pathOrKey.path : pathOrKey;
218
226
  return bindImpl(path, options);
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolveDynamicPath } from "./dynamicPath";
3
+
4
+ describe("resolveDynamicPath", () => {
5
+ const host = { sessionId: "alpha", count: 0, label: "", missing: null as null };
6
+
7
+ it("est prêt avec une valeur définie", () => {
8
+ expect(resolveDynamicPath(host, "api/sessions/${sessionId}/sync")).toEqual({
9
+ ready: true,
10
+ path: "api/sessions/alpha/sync",
11
+ });
12
+ });
13
+
14
+ it("n'est pas prêt pour undefined/null", () => {
15
+ expect(resolveDynamicPath({}, "api/sessions/${sessionId}/sync")).toEqual({
16
+ ready: false,
17
+ path: null,
18
+ });
19
+ expect(
20
+ resolveDynamicPath({ sessionId: null }, "api/sessions/${sessionId}/sync"),
21
+ ).toEqual({ ready: false, path: null });
22
+ });
23
+
24
+ it("insère 0 et false par défaut", () => {
25
+ expect(resolveDynamicPath(host, "items/${count}")).toEqual({
26
+ ready: true,
27
+ path: "items/0",
28
+ });
29
+ expect(
30
+ resolveDynamicPath({ flag: false }, "flags/${flag}"),
31
+ ).toEqual({ ready: true, path: "flags/false" });
32
+ });
33
+
34
+ it("accepte une chaîne vide par défaut", () => {
35
+ expect(resolveDynamicPath(host, "api/sessions/${label}/sync")).toEqual({
36
+ ready: true,
37
+ path: "api/sessions//sync",
38
+ });
39
+ });
40
+
41
+ it("skipEmptyPlaceholder: true — chaîne vide non prête", () => {
42
+ expect(
43
+ resolveDynamicPath(host, "api/sessions/${label}/sync", {
44
+ skipEmptyPlaceholder: true,
45
+ }),
46
+ ).toEqual({ ready: false, path: null });
47
+ expect(
48
+ resolveDynamicPath(host, "api/sessions/${sessionId}/sync", {
49
+ skipEmptyPlaceholder: true,
50
+ }),
51
+ ).toEqual({ ready: true, path: "api/sessions/alpha/sync" });
52
+ });
53
+ });
@@ -1,5 +1,17 @@
1
1
  /** Lit / décorateurs : chemins publisher avec `${prop}` ou `{$prop}`. */
2
2
 
3
+ /**
4
+ * Options de résolution des placeholders dynamiques.
5
+ * `skipEmptyPlaceholder` ne concerne que les chaînes vides `''` — pas `0`, `false`, `null`, etc.
6
+ */
7
+ export type DynamicPathOptions = {
8
+ /**
9
+ * Si `true`, un placeholder résolu en `''` est traité comme non prêt (`ready: false`).
10
+ * N'affecte pas les autres valeurs (`0` → `"0"`, `false` → `"false"`, …).
11
+ */
12
+ skipEmptyPlaceholder?: boolean;
13
+ };
14
+
3
15
  export function cleanPlaceholder(value: string): string {
4
16
  return value.trim().replace(/^this\./, "");
5
17
  }
@@ -25,15 +37,25 @@ export function getValueFromExpression(
25
37
  return current;
26
38
  }
27
39
 
40
+ function isPlaceholderUnresolved(
41
+ resolved: unknown,
42
+ options?: DynamicPathOptions,
43
+ ): boolean {
44
+ if (resolved === undefined || resolved === null) return true;
45
+ if (options?.skipEmptyPlaceholder && resolved === "") return true;
46
+ return false;
47
+ }
48
+
28
49
  export function resolveDynamicPath(
29
50
  component: unknown,
30
51
  template: string,
52
+ options?: DynamicPathOptions,
31
53
  ): { ready: boolean; path: string | null } {
32
54
  let missing = false;
33
55
  const replaceValue = (_match: string, expression: string) => {
34
56
  const cleaned = cleanPlaceholder(expression);
35
57
  const resolved = getValueFromExpression(component, cleaned);
36
- if (resolved === undefined || resolved === null) {
58
+ if (isPlaceholderUnresolved(resolved, options)) {
37
59
  missing = true;
38
60
  return "";
39
61
  }
@@ -2,6 +2,7 @@ import type {
2
2
  DataProviderKey,
3
3
  DataProviderKeyHost,
4
4
  } from "../../utils/dataProviderKey";
5
+ import type { DynamicPathOptions } from "./dynamicPath";
5
6
  import { createOnAssign, Skip } from "./onAssign";
6
7
 
7
8
  export { Skip } from "./onAssign";
@@ -14,7 +15,7 @@ export { Skip } from "./onAssign";
14
15
  * assignation**, même quand la valeur reçue est `null`/`undefined` (c'est la
15
16
  * différence de comportement voulue par rapport à `@onAssign`).
16
17
  */
17
- export type HandleOptions = {
18
+ export type HandleOptions = DynamicPathOptions & {
18
19
  /**
19
20
  * Attendre que **toutes** les clés surveillées soient définies (non
20
21
  * `null`/`undefined`) avant d'appeler la méthode. Reproduit la sémantique
@@ -122,6 +123,7 @@ export function handle(
122
123
  {
123
124
  dispatchWhenUndefined: !options.waitForAllDefined,
124
125
  skip: options.skip,
126
+ skipEmptyPlaceholder: options.skipEmptyPlaceholder,
125
127
  },
126
128
  paths,
127
129
  );
@@ -1,6 +1,10 @@
1
1
  import DataProvider from "../../utils/PublisherProxy";
2
2
  import { ConnectedComponent, setSubscribable } from "./common";
3
- import { extractDynamicDependencies, resolveDynamicPath } from "./dynamicPath";
3
+ import {
4
+ type DynamicPathOptions,
5
+ extractDynamicDependencies,
6
+ resolveDynamicPath,
7
+ } from "./dynamicPath";
4
8
  import {
5
9
  onAssignDynamicWatchKeys,
6
10
  observeDynamicProperty,
@@ -60,7 +64,7 @@ export function isSkipped(value: unknown, kinds: Skip[]): boolean {
60
64
  return kinds.some((kind) => SKIP_PREDICATES[kind](value));
61
65
  }
62
66
 
63
- export type OnAssignOptions = {
67
+ export type OnAssignOptions = DynamicPathOptions & {
64
68
  /**
65
69
  * Quand `true`, le callback est invoqué à chaque assignation, même si la
66
70
  * valeur reçue est `null`/`undefined`.
@@ -125,6 +129,9 @@ export function createOnAssign(
125
129
  isDynamic: dynamicDependencies.length > 0,
126
130
  };
127
131
  });
132
+ const pathOptions: DynamicPathOptions = {
133
+ skipEmptyPlaceholder: options.skipEmptyPlaceholder,
134
+ };
128
135
 
129
136
  return function (
130
137
  target: unknown,
@@ -215,6 +222,7 @@ export function createOnAssign(
215
222
  const resolution = resolveDynamicPath(
216
223
  component,
217
224
  conf.pathConfig.originalPath,
225
+ pathOptions,
218
226
  );
219
227
  if (!resolution.ready) {
220
228
  subscribeToPath(conf, null);
@@ -4,7 +4,13 @@ import type {
4
4
  } from "../../utils/dataProviderKey";
5
5
  import DataProvider from "../../utils/PublisherProxy";
6
6
  import { ConnectedComponent, setSubscribable } from "./common";
7
- import { extractDynamicDependencies, resolveDynamicPath } from "./dynamicPath";
7
+ import {
8
+ type DynamicPathOptions,
9
+ extractDynamicDependencies,
10
+ resolveDynamicPath,
11
+ } from "./dynamicPath";
12
+
13
+ export type PublishOptions = DynamicPathOptions;
8
14
  import {
9
15
  publishDynamicWatchKeys,
10
16
  observeDynamicProperty,
@@ -29,11 +35,15 @@ import { getPublisherFromPath } from "./publisherPath";
29
35
  */
30
36
  export function publish<T, U = any>(
31
37
  key: DataProviderKey<T, U>,
38
+ options?: PublishOptions,
32
39
  ): <K extends string>(
33
40
  target: DataProviderKeyHost<U> & { [P in K]?: T | null | undefined },
34
41
  propertyKey: K,
35
42
  ) => void {
36
43
  const path = key.path;
44
+ const pathOptions: DynamicPathOptions = {
45
+ skipEmptyPlaceholder: options?.skipEmptyPlaceholder,
46
+ };
37
47
  const dynamicDependencies = extractDynamicDependencies(path);
38
48
 
39
49
  return function (target: object, propertyKey: string) {
@@ -93,7 +103,7 @@ export function publish<T, U = any>(
93
103
  const updatePublisher = () => {
94
104
  let resolvedPath: string | null;
95
105
  if (dynamicDependencies.length) {
96
- const resolution = resolveDynamicPath(component, path);
106
+ const resolution = resolveDynamicPath(component, path, pathOptions);
97
107
  resolvedPath = resolution.ready ? resolution.path : null;
98
108
  } else {
99
109
  resolvedPath = path;
@@ -28,6 +28,24 @@ type PublisherProxyOptions = {
28
28
  invalidateOnPageShow?: boolean;
29
29
  };
30
30
 
31
+ /** Options de `dp()` / `dataProvider()` — même surface que `PublisherManager.get()`. */
32
+ export type DpOptions = PublisherProxyOptions;
33
+
34
+ const DP_OPTION_KEYS = new Set<keyof DpOptions>([
35
+ "localStorageMode",
36
+ "expirationDelayMs",
37
+ "invalidateOnPageShow",
38
+ ]);
39
+
40
+ function isDpOptions(value: unknown): value is DpOptions {
41
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
42
+ return false;
43
+ }
44
+ return Object.keys(value).some((key) =>
45
+ DP_OPTION_KEYS.has(key as keyof DpOptions),
46
+ );
47
+ }
48
+
31
49
  function isLeaf(value: any) {
32
50
  return Object.prototype.hasOwnProperty.call(value, "__value");
33
51
  }
@@ -1041,6 +1059,7 @@ try {
1041
1059
 
1042
1060
  export const getObservables = <T = any>(
1043
1061
  observable: string,
1062
+ options?: DpOptions,
1044
1063
  ): Set<DataProvider<T>> => {
1045
1064
  if (typeof observable === "function") {
1046
1065
  const func = observable as () => any;
@@ -1051,7 +1070,7 @@ export const getObservables = <T = any>(
1051
1070
  if (typeof observable === "string") {
1052
1071
  const split = observable.split(".");
1053
1072
  const dataProvider: string = split.shift() || "";
1054
- let publisher = PublisherManager.get(dataProvider);
1073
+ let publisher = PublisherManager.get(dataProvider, options);
1055
1074
  publisher = Objects.traverse(publisher, split);
1056
1075
  const set = new Set<DataProvider<T>>();
1057
1076
  set.add(publisher as DataProvider<T>);
@@ -1068,9 +1087,14 @@ export function get<T>(id: string | DataProviderKey<T>): T {
1068
1087
  return getObservables<T>(path).values().next().value?.get() as T;
1069
1088
  }
1070
1089
 
1071
- function deepee<T>(id: string | DataProviderKey<T>, _defaultValue?: T) {
1090
+ function deepee<T>(
1091
+ id: string | DataProviderKey<T>,
1092
+ _defaultValue?: T,
1093
+ options?: DpOptions,
1094
+ ) {
1072
1095
  const path = resolveStaticPublisherPath(id);
1073
- const value = getObservables<T>(path).values().next().value as DataProvider<T>;
1096
+ const value = getObservables<T>(path, options).values().next()
1097
+ .value as DataProvider<T>;
1074
1098
  // if (defaultValue !== undefined && value) {
1075
1099
  // const innerValue = value.get();
1076
1100
  // if (Objects.isEmpty(innerValue as Record<string, any>)) {
@@ -1081,19 +1105,79 @@ function deepee<T>(id: string | DataProviderKey<T>, _defaultValue?: T) {
1081
1105
  return value;
1082
1106
  }
1083
1107
 
1084
- export function dataProvider<T>(id: DataProviderKey<T>, defaultValue?: T): DataProvider<T>;
1085
- export function dataProvider<T = any>(id: string, defaultValue?: T): DataProvider<T>;
1108
+ type MirrorDpSource = DataProvider<any> | string | DataProviderKey<any>;
1109
+
1110
+ function isDataProvider(value: unknown): value is DataProvider<any> {
1111
+ return (
1112
+ value != null &&
1113
+ typeof value === "object" &&
1114
+ typeof (value as DataProvider<any>).get === "function" &&
1115
+ typeof (value as DataProvider<any>).set === "function" &&
1116
+ typeof (value as DataProvider<any>).onAssign === "function"
1117
+ );
1118
+ }
1119
+
1120
+ function resolvePublisherRef(
1121
+ ref: MirrorDpSource,
1122
+ options?: DpOptions,
1123
+ ): DataProvider<any> {
1124
+ if (isDataProvider(ref)) return ref;
1125
+ return deepee(ref, undefined, options);
1126
+ }
1127
+
1128
+ export function dataProvider<T>(
1129
+ id: DataProviderKey<T>,
1130
+ options: DpOptions,
1131
+ ): DataProvider<T>;
1132
+ export function dataProvider<T = any>(id: string, options: DpOptions): DataProvider<T>;
1133
+ export function dataProvider<T>(id: DataProviderKey<T>): DataProvider<T>;
1134
+ export function dataProvider<T = any>(id: string): DataProvider<T>;
1135
+ export function dataProvider<T>(id: DataProviderKey<T>, defaultValue: T): DataProvider<T>;
1136
+ export function dataProvider<T = any>(id: string, defaultValue: T): DataProvider<T>;
1086
1137
  export function dataProvider<T>(
1087
1138
  id: string | DataProviderKey<T>,
1088
- defaultValue?: T,
1139
+ second?: T | DpOptions,
1089
1140
  ): DataProvider<T> {
1090
- return deepee(id, defaultValue);
1141
+ if (isDpOptions(second)) return deepee(id, undefined, second);
1142
+ return deepee(id, second as T | undefined);
1091
1143
  }
1092
1144
 
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);
1145
+ export function dp<T>(id: DataProviderKey<T>, options: DpOptions): DataProvider<T>;
1146
+ export function dp<T = any>(id: string, options: DpOptions): DataProvider<T>;
1147
+ export function dp<T>(id: DataProviderKey<T>): DataProvider<T>;
1148
+ export function dp<T = any>(id: string): DataProvider<T>;
1149
+ export function dp<T>(id: DataProviderKey<T>, defaultValue: T): DataProvider<T>;
1150
+ export function dp<T = any>(id: string, defaultValue: T): DataProvider<T>;
1151
+ export function dp<T>(
1152
+ id: string | DataProviderKey<T>,
1153
+ second?: T | DpOptions,
1154
+ ): DataProvider<T> {
1155
+ if (isDpOptions(second)) return deepee(id, undefined, second);
1156
+ return deepee(id, second as T | undefined);
1157
+ }
1158
+
1159
+ /**
1160
+ * Alias sémantique pour `PublisherManager.set` : enregistre `aliasKey` sur la même
1161
+ * instance publisher que `source`. Utile pour exposer une clé legacy (`scan`) tout en
1162
+ * stockant l'état sous une racine (`app.scan`) — `dp("scan")` et `appState.scan` partagent
1163
+ * alors la même instance, sans copie à chaque assignation.
1164
+ */
1165
+ export function mirrorDp<T>(
1166
+ aliasKey: string | DataProviderKey<any>,
1167
+ source: DataProvider<T>,
1168
+ ): void;
1169
+ export function mirrorDp(
1170
+ aliasKey: string | DataProviderKey<any>,
1171
+ source: string | DataProviderKey<any>,
1172
+ ): void;
1173
+ export function mirrorDp(
1174
+ aliasKey: string | DataProviderKey<any>,
1175
+ source: DataProvider<any> | string | DataProviderKey<any>,
1176
+ ): void {
1177
+ PublisherManager.getInstance().set(
1178
+ resolveStaticPublisherPath(aliasKey),
1179
+ resolvePublisherRef(source),
1180
+ );
1097
1181
  }
1098
1182
 
1099
1183
  export function set<T>(id: DataProviderKey<T>, value: T): void;
@@ -37,8 +37,8 @@ export type APIResponse = {
37
37
  processed: ResultTypeInterface;
38
38
  };
39
39
 
40
- /** Valeur assignée par `@get` : requête native, réponse `fetch` si HTTP, résultat métier typé `T`. */
41
- export type ApiGetResult<T> = {
40
+ /** Valeur assignée par `@get`, `@post`, `@put`, `@patch`. */
41
+ export type ApiResult<T> = {
42
42
  request: Request;
43
43
  /** Absent / `undefined` pour les chemins `dataProvider(...)` (pas d’appel réseau). */
44
44
  response?: Response;
@@ -46,6 +46,9 @@ export type ApiGetResult<T> = {
46
46
  result: T;
47
47
  };
48
48
 
49
+ /** @deprecated Utiliser `ApiResult`. */
50
+ export type ApiGetResult<T> = ApiResult<T>;
51
+
49
52
  /** Extrait le corps typé depuis le résultat traité par `handleResult`. */
50
53
  export function extractTypedApiResult<T>(
51
54
  processed: ResultTypeInterface | null | undefined,
@@ -434,7 +437,7 @@ class API {
434
437
  async getDetailed<T>(
435
438
  path: string,
436
439
  additionalHeaders?: HeadersInit,
437
- ): Promise<ApiGetResult<T> | undefined> {
440
+ ): Promise<ApiResult<T> | undefined> {
438
441
  const isDataProvider = /dataProvider\((.*?)\)(.*?)$/.test(path);
439
442
 
440
443
  const processed = await this.get<T>(path, additionalHeaders);
@@ -468,6 +471,72 @@ class API {
468
471
  };
469
472
  }
470
473
 
474
+ /**
475
+ * S’appuie sur `post()` puis reformate la réponse : `Request` avec body JSON,
476
+ * `Response` via `lastResult`, `result` typé `T`.
477
+ */
478
+ async sendDetailed<T, B>(
479
+ path: string,
480
+ body: B,
481
+ method: "POST" | "PUT" | "PATCH",
482
+ additionalHeaders?: HeadersInit,
483
+ ): Promise<ApiResult<T> | undefined> {
484
+ const processed = await this.send<T, B>(
485
+ path,
486
+ body,
487
+ method,
488
+ additionalHeaders,
489
+ );
490
+ if (processed == null) {
491
+ return undefined;
492
+ }
493
+
494
+ const result = extractTypedApiResult<T>(processed as ResultTypeInterface);
495
+ const url = this.computeURL(path);
496
+ const headers = await this.createHeaders({
497
+ Accept: "application/json",
498
+ "Content-Type": "application/json",
499
+ ...(additionalHeaders as Record<string, string> | undefined),
500
+ });
501
+ const request = new Request(url, {
502
+ method,
503
+ headers: new Headers(headers as HeadersInit),
504
+ credentials: this.credentials,
505
+ body: JSON.stringify(body),
506
+ keepalive: this.keepAlive,
507
+ });
508
+
509
+ return {
510
+ request,
511
+ response: this.lastResult,
512
+ result,
513
+ };
514
+ }
515
+
516
+ async postDetailed<T, B>(
517
+ path: string,
518
+ body: B,
519
+ additionalHeaders?: HeadersInit,
520
+ ): Promise<ApiResult<T> | undefined> {
521
+ return this.sendDetailed<T, B>(path, body, "POST", additionalHeaders);
522
+ }
523
+
524
+ async putDetailed<T, B>(
525
+ path: string,
526
+ body: B,
527
+ additionalHeaders?: HeadersInit,
528
+ ): Promise<ApiResult<T> | undefined> {
529
+ return this.sendDetailed<T, B>(path, body, "PUT", additionalHeaders);
530
+ }
531
+
532
+ async patchDetailed<T, B>(
533
+ path: string,
534
+ body: B,
535
+ additionalHeaders?: HeadersInit,
536
+ ): Promise<ApiResult<T> | undefined> {
537
+ return this.sendDetailed<T, B>(path, body, "PATCH", additionalHeaders);
538
+ }
539
+
471
540
  /**
472
541
  * Création du header, avec authentification si besoin
473
542
  * ajout du language via le header accept-language qui contient le langue du navigateur
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { DataProviderKey } from "./dataProviderKey";
3
+ import { dp, get, mirrorDp } from "./PublisherProxy";
4
+
5
+ describe("dp() options", () => {
6
+ const storageKey = new DataProviderKey<{ label: string }>(
7
+ "dpOptionsSpecStorage",
8
+ );
9
+
10
+ it("active le localStorage via le 2e argument options", () => {
11
+ const publisher = dp(storageKey, { localStorageMode: "enabled" });
12
+ expect((publisher as any)._is_savable_).toBe(true);
13
+ });
14
+
15
+ it("reste désactivé sans option", () => {
16
+ const plainKey = new DataProviderKey<{ label: string }>(
17
+ "dpOptionsSpecPlain",
18
+ );
19
+ const publisher = dp(plainKey);
20
+ expect((publisher as any)._is_savable_).toBe(false);
21
+ });
22
+ });
23
+
24
+ describe("mirrorDp()", () => {
25
+ it("alias une clé legacy vers un publisher déjà résolu (appState)", () => {
26
+ type App = { scan: { code: string } };
27
+ const appKey = new DataProviderKey<App>("mirrorDpSpecApp");
28
+ const scanKey = new DataProviderKey<{ code: string }>("mirrorDpSpecScan");
29
+
30
+ const appState = dp(appKey);
31
+ appState.scan.code.set("ABC");
32
+
33
+ mirrorDp(scanKey, appState.scan);
34
+
35
+ expect(get(scanKey)).toEqual({ code: "ABC" });
36
+ dp(scanKey).code.set("XYZ");
37
+ expect(appState.scan.code.get()).toBe("XYZ");
38
+ });
39
+
40
+ it("accepte des chemins string", () => {
41
+ type App = { legacyApi: { total: number } };
42
+ const appKey = new DataProviderKey<App>("mirrorDpSpecAppString");
43
+ const legacyKey = new DataProviderKey<{ total: number }>(
44
+ "mirrorDpSpecLegacyApi",
45
+ );
46
+
47
+ const appState = dp(appKey);
48
+ appState.legacyApi.total.set(12);
49
+
50
+ mirrorDp(legacyKey.path, `${appKey.path}.legacyApi`);
51
+
52
+ expect(get(legacyKey)).toEqual({ total: 12 });
53
+ dp(legacyKey).total.set(99);
54
+ expect(appState.legacyApi.total.get()).toBe(99);
55
+ });
56
+ });
@@ -1,4 +1,4 @@
1
- import { ApiGetResult } from "./api";
1
+ import { ApiResult } from "./api";
2
2
  import { DataProviderKey } from "./dataProviderKey";
3
3
 
4
4
  /**
@@ -25,8 +25,8 @@ export class Endpoint<T, U = any> {
25
25
  }
26
26
 
27
27
  /** Même path qu’`Endpoint` ; le 2ᵉ générique `U` est propagé sur la clé publisher. */
28
- getDataProviderKey(): DataProviderKey<ApiGetResult<T>, U> {
29
- return new DataProviderKey<ApiGetResult<T>, U>(this.path);
28
+ getDataProviderKey(): DataProviderKey<ApiResult<T>, U> {
29
+ return new DataProviderKey<ApiResult<T>, U>(this.path);
30
30
  }
31
31
 
32
32
  /**
package/src/decorators.ts CHANGED
@@ -16,12 +16,25 @@ export const ancestorAttribute = mySubscriber.ancestorAttribute;
16
16
  export const autoSubscribe = mySubscriber.autoSubscribe;
17
17
  export const autoFill = mySubscriber.autoFill;
18
18
  export const get = api.get;
19
+ export const post = api.post;
20
+ export const put = api.put;
21
+ export const patch = api.patch;
19
22
  export {
20
23
  DataProviderKey,
21
24
  type DataProviderKeyHost,
22
25
  } from "./core/utils/dataProviderKey";
23
26
  export { Endpoint };
24
- export type { ApiGetResult } from "./core/utils/api";
27
+ export type { ApiResult, ApiGetResult } from "./core/utils/api";
28
+ export type {
29
+ ApiSendOptions,
30
+ GetOptions,
31
+ PostOptions,
32
+ PutOptions,
33
+ PatchOptions,
34
+ } from "./core/decorators/api";
35
+ export type { DynamicPathOptions } from "./core/decorators/subscriber/dynamicPath";
36
+ export type { BindOptions } from "./core/decorators/subscriber/bind";
37
+ export type { PublishOptions } from "./core/decorators/subscriber/publish";
25
38
  export const awaitConnectedAncestors = lifecycle.awaitConnectedAncestors;
26
39
  export const dispatchConnectedEvent = lifecycle.dispatchConnectedEvent;
27
40
  export const CONNECTED = lifecycle.CONNECTED;
@@ -41,4 +54,7 @@ window["concorde-decorator-subscriber"] = {
41
54
  autoSubscribe: mySubscriber.autoSubscribe,
42
55
  autoFill: mySubscriber.autoFill,
43
56
  get: api.get,
57
+ post: api.post,
58
+ put: api.put,
59
+ patch: api.patch,
44
60
  };
@@ -13,6 +13,8 @@ Recommended patterns for new Concorde apps (Lit + TypeScript). Under the hood, d
13
13
  | Write from component state | `@publish` |
14
14
  | React to assignments | `@handle` |
15
15
  | HTTP GET | `@get` + `Endpoint`, or `sonic-list` / `sonic-queue` with `fetch` |
16
+ | HTTP POST (body from store) | `@post` + `Endpoint` + body `DataProviderKey` |
17
+ | HTTP PUT / PATCH (body from store) | `@put` / `@patch` — same model as `@post` |
16
18
  | Forms | `formDataProvider` + `name` on fields |
17
19
  | Offline doc demos | `serviceURL="/docs-mock-api"` — [Local API demos](#docs/_misc/docs-mock-api.md/docs-mock-api) |
18
20
 
@@ -33,7 +35,7 @@ get(cartKey);
33
35
  </template>
34
36
  </sonic-code>
35
37
 
36
- Dynamic paths (`users.${userId}`) → decorators or `sub()` — not `get("users.${id}")` in imperative code.
38
+ Dynamic paths (`users.${userId}`) → decorators or `sub()` — not `get("users.${id}")` in imperative code. Resolution rules: [Dynamic path placeholders](#docs/_misc/dynamic-path.md/dynamic-path).
37
39
 
38
40
  [DataProviderKey](#docs/_misc/dataProviderKey.md/dataProviderKey)
39
41
 
@@ -45,13 +47,17 @@ Dynamic paths (`users.${userId}`) → decorators or `sub()` — not `get("users.
45
47
  | `@publish` | Push property writes to store |
46
48
  | `@handle` | Method called on assignment |
47
49
  | `@ancestorAttribute` | Copy ancestor HTML attribute onto property |
48
- | `@get` | HTTP GET into `ApiGetResult&lt;T&gt;` |
50
+ | `@get` | HTTP GET into `ApiResult&lt;T&gt;` |
51
+ | `@post` | HTTP POST into `ApiResult&lt;T&gt;` (body from a publisher) |
52
+ | `@put` / `@patch` | HTTP PUT / PATCH into `ApiResult&lt;T&gt;` |
49
53
 
50
54
  Walkthrough: [My first component](#docs/_getting-started/my-first-component.md/my-first-component)
51
55
 
52
56
  ## HTTP and lists
53
57
 
54
- - [@get](#docs/_decorators/get.md/get) — single request on a component
58
+ - [@get](#docs/_decorators/get.md/get) — single GET on a component
59
+ - [@post](#docs/_decorators/post.md/post) — POST with body read from a `DataProviderKey`
60
+ - [@put](#docs/_decorators/put.md/put) · [@patch](#docs/_decorators/patch.md/patch) — PUT / PATCH (same options as `@post`)
55
61
  - [List](#core/components/functional/list/list.md/list) — `fetch` + `key="data"` + `/docs-mock-api/api/users`
56
62
  - [Queue](#core/components/functional/queue/queue.md/queue) — lazy `offset=$offset&per_page=$limit` + optional `dataFilterProvider` (form → query)
57
63
 
@@ -4,7 +4,7 @@ Binds a class property to a path in a publisher. The property updates when publi
4
4
 
5
5
  For Lit re-renders, also add `@state()` on the same property.
6
6
 
7
- **See also:** [@subscribe](#docs/_decorators/subscribe.md/subscribe), [@handle](#docs/_decorators/handle.md/handle), [@publish](#docs/_decorators/publish.md/publish), [@get](#docs/_decorators/get.md/get).
7
+ **See also:** [@subscribe](#docs/_decorators/subscribe.md/subscribe), [@handle](#docs/_decorators/handle.md/handle), [@publish](#docs/_decorators/publish.md/publish), [@get](#docs/_decorators/get.md/get), [@post](#docs/_decorators/post.md/post), [@put](#docs/_decorators/put.md/put), [@patch](#docs/_decorators/patch.md/patch).
8
8
 
9
9
  ## Principle
10
10
 
@@ -144,7 +144,7 @@ export class DemoBindReflect extends LitElement {
144
144
 
145
145
  ### Dynamic paths
146
146
 
147
- Use `${prop}` or `${this.prop}` inside a **normal string literal** (not a JS template literal with backticks). `@bind` re-subscribes when a reactive dependency changes.
147
+ Use `${prop}` or `${this.prop}` inside a **normal string literal** (not a JS template literal with backticks). `@bind` re-subscribes when a reactive dependency changes. While a placeholder is `null`/`undefined`, the bind is inactive; optional `{ skipEmptyPlaceholder: true }` also waits on `""`. See [Dynamic path placeholders](#docs/_misc/dynamic-path.md/dynamic-path).
148
148
 
149
149
  > Properties referenced in the pattern must be reactive (`@property`, etc.) or you must call `requestUpdate` manually.
150
150