@supersoniks/concorde 4.7.4 → 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 (143) hide show
  1. package/README.md +1 -1
  2. package/ai/cursor/rules/concorde.mdc +1 -1
  3. package/ai/skills/concorde-scope/SKILL.md +2 -2
  4. package/build-infos.json +1 -1
  5. package/concorde-core.bundle.js +289 -289
  6. package/concorde-core.es.js +4837 -4544
  7. package/dist/concorde-core.bundle.js +289 -289
  8. package/dist/concorde-core.es.js +4837 -4544
  9. package/dist/docs-mock-api-sw.js +19 -0
  10. package/dist/docs-mock-api-sw.js.map +2 -2
  11. package/docs/assets/index-wyNMyWT9.js +11196 -0
  12. package/docs/docs-mock-api-sw.js +19 -0
  13. package/docs/docs-mock-api-sw.js.map +2 -2
  14. package/docs/index.html +1 -1
  15. package/package.json +9 -1
  16. package/public/docs-mock-api-sw.js +19 -0
  17. package/public/docs-mock-api-sw.js.map +2 -2
  18. package/src/core/components/functional/example/example.ts +3 -3
  19. package/src/core/components/ui/icon/icon.ts +17 -2
  20. package/src/core/components/ui/menu/menu.ts +12 -3
  21. package/src/core/decorators/api.post.spec.ts +293 -0
  22. package/src/core/decorators/api.spec.ts +6 -6
  23. package/src/core/decorators/api.ts +643 -12
  24. package/src/core/decorators/subscriber/bind.ts +13 -5
  25. package/src/core/decorators/subscriber/dynamicPath.spec.ts +53 -0
  26. package/src/core/decorators/subscriber/dynamicPath.ts +23 -1
  27. package/src/core/decorators/subscriber/handle.ts +3 -1
  28. package/src/core/decorators/subscriber/onAssign.ts +10 -2
  29. package/src/core/decorators/subscriber/publish.ts +12 -2
  30. package/src/core/utils/PublisherProxy.ts +95 -11
  31. package/src/core/utils/api.ts +72 -3
  32. package/src/core/utils/dpOptions.spec.ts +56 -0
  33. package/src/core/utils/endpoint.ts +3 -3
  34. package/src/decorators.ts +17 -1
  35. package/src/docs/_core-concept/dataFlow.md +9 -3
  36. package/src/docs/_decorators/bind.md +2 -2
  37. package/src/docs/_decorators/get.md +13 -4
  38. package/src/docs/_decorators/handle.md +5 -1
  39. package/src/docs/_decorators/on-assign.md +2 -0
  40. package/src/docs/_decorators/patch.md +45 -0
  41. package/src/docs/_decorators/post.md +93 -0
  42. package/src/docs/_decorators/publish.md +1 -1
  43. package/src/docs/_decorators/put.md +43 -0
  44. package/src/docs/_decorators/subscribe.md +4 -1
  45. package/src/docs/_directives/sub.md +1 -1
  46. package/src/docs/_getting-started/my-first-component.md +1 -1
  47. package/src/docs/_misc/api-configuration.md +3 -1
  48. package/src/docs/_misc/dataProviderKey.md +2 -2
  49. package/src/docs/_misc/dynamic-path.md +71 -0
  50. package/src/docs/_misc/endpoint.md +5 -3
  51. package/src/docs/components/docs-demo-sources.ts +102 -3
  52. package/src/docs/components/docs-lit-demo-raw.ts +2 -26
  53. package/src/docs/components/docs-lit-demo.ts +9 -42
  54. package/src/docs/components/docs-source-excerpt.ts +53 -0
  55. package/src/docs/components/docs-source-link.ts +24 -8
  56. package/src/docs/components/docs-source-raw.ts +34 -0
  57. package/src/docs/example/decorators-demo-geo.ts +2 -2
  58. package/src/docs/example/decorators-demo-post.ts +249 -0
  59. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +5 -5
  60. package/src/docs/example/decorators-demo.ts +1 -0
  61. package/src/docs/example/docs-api-config-demos.ts +5 -5
  62. package/src/docs/mock-api/router.ts +20 -0
  63. package/src/docs/navigation/navigation.ts +16 -0
  64. package/src/docs/search/docs-search.json +540 -15
  65. package/src/tsconfig.json +24 -0
  66. package/src/tsconfig.tsbuildinfo +1 -1
  67. package/vite.config.mts +1 -1
  68. package/docs/assets/index-CwtPzTFq.js +0 -7508
  69. package/docs/src/core/components/functional/date/date.md +0 -290
  70. package/docs/src/core/components/functional/fetch/fetch.md +0 -125
  71. package/docs/src/core/components/functional/if/if.md +0 -9
  72. package/docs/src/core/components/functional/list/list.md +0 -65
  73. package/docs/src/core/components/functional/mix/mix.md +0 -41
  74. package/docs/src/core/components/functional/queue/queue.md +0 -72
  75. package/docs/src/core/components/functional/router/router.md +0 -94
  76. package/docs/src/core/components/functional/sdui/default-library.json +0 -108
  77. package/docs/src/core/components/functional/sdui/example.json +0 -99
  78. package/docs/src/core/components/functional/sdui/sdui.md +0 -356
  79. package/docs/src/core/components/functional/states/states.md +0 -87
  80. package/docs/src/core/components/functional/submit/submit.md +0 -114
  81. package/docs/src/core/components/functional/subscriber/subscriber.md +0 -91
  82. package/docs/src/core/components/functional/value/value.md +0 -35
  83. package/docs/src/core/components/ui/alert/alert.md +0 -121
  84. package/docs/src/core/components/ui/alert-messages/alert-messages.md +0 -0
  85. package/docs/src/core/components/ui/badge/badge.md +0 -127
  86. package/docs/src/core/components/ui/button/button.md +0 -182
  87. package/docs/src/core/components/ui/captcha/captcha.md +0 -12
  88. package/docs/src/core/components/ui/card/card.md +0 -97
  89. package/docs/src/core/components/ui/divider/divider.md +0 -35
  90. package/docs/src/core/components/ui/form/checkbox/checkbox.md +0 -77
  91. package/docs/src/core/components/ui/form/fieldset/fieldset.md +0 -129
  92. package/docs/src/core/components/ui/form/form-actions/form-actions.md +0 -77
  93. package/docs/src/core/components/ui/form/form-layout/form-layout.md +0 -44
  94. package/docs/src/core/components/ui/form/input/input.md +0 -142
  95. package/docs/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +0 -133
  96. package/docs/src/core/components/ui/form/radio/radio.md +0 -57
  97. package/docs/src/core/components/ui/form/select/select.md +0 -71
  98. package/docs/src/core/components/ui/form/switch/switch.md +0 -57
  99. package/docs/src/core/components/ui/form/textarea/textarea.md +0 -65
  100. package/docs/src/core/components/ui/group/group.md +0 -75
  101. package/docs/src/core/components/ui/icon/icon.md +0 -125
  102. package/docs/src/core/components/ui/icon/icons.json +0 -1
  103. package/docs/src/core/components/ui/image/image.md +0 -107
  104. package/docs/src/core/components/ui/link/link.md +0 -43
  105. package/docs/src/core/components/ui/loader/loader.md +0 -55
  106. package/docs/src/core/components/ui/menu/menu.md +0 -329
  107. package/docs/src/core/components/ui/modal/modal.md +0 -119
  108. package/docs/src/core/components/ui/pop/pop.md +0 -96
  109. package/docs/src/core/components/ui/progress/progress.md +0 -63
  110. package/docs/src/core/components/ui/table/table.md +0 -455
  111. package/docs/src/core/components/ui/toast/toast.md +0 -166
  112. package/docs/src/core/components/ui/tooltip/tooltip.md +0 -82
  113. package/docs/src/docs/_core-concept/dataFlow.md +0 -73
  114. package/docs/src/docs/_core-concept/overview.md +0 -57
  115. package/docs/src/docs/_core-concept/subscriber.md +0 -75
  116. package/docs/src/docs/_decorators/ancestor-attribute.md +0 -79
  117. package/docs/src/docs/_decorators/auto-subscribe.md +0 -202
  118. package/docs/src/docs/_decorators/bind.md +0 -167
  119. package/docs/src/docs/_decorators/get.md +0 -68
  120. package/docs/src/docs/_decorators/handle.md +0 -171
  121. package/docs/src/docs/_decorators/on-assign.md +0 -388
  122. package/docs/src/docs/_decorators/publish.md +0 -55
  123. package/docs/src/docs/_decorators/subscribe.md +0 -97
  124. package/docs/src/docs/_decorators/wait-for-ancestors.md +0 -163
  125. package/docs/src/docs/_directives/sub.md +0 -91
  126. package/docs/src/docs/_getting-started/ai-agents.md +0 -56
  127. package/docs/src/docs/_getting-started/concorde-manual-install.md +0 -133
  128. package/docs/src/docs/_getting-started/concorde-outside.md +0 -33
  129. package/docs/src/docs/_getting-started/create-a-component.md +0 -139
  130. package/docs/src/docs/_getting-started/my-first-component.md +0 -236
  131. package/docs/src/docs/_getting-started/my-first-subscriber.md +0 -120
  132. package/docs/src/docs/_getting-started/pubsub.md +0 -37
  133. package/docs/src/docs/_getting-started/start.md +0 -47
  134. package/docs/src/docs/_getting-started/theming.md +0 -91
  135. package/docs/src/docs/_misc/api-configuration.md +0 -79
  136. package/docs/src/docs/_misc/dataProviderKey.md +0 -168
  137. package/docs/src/docs/_misc/docs-mock-api.md +0 -60
  138. package/docs/src/docs/_misc/endpoint.md +0 -43
  139. package/docs/src/docs/_misc/html-integration.md +0 -13
  140. package/docs/src/docs/search/docs-search.json +0 -8532
  141. package/docs/src/tag-list.json +0 -1
  142. package/docs/src/tsconfig-model.json +0 -23
  143. package/docs/src/tsconfig.json +0 -1095
@@ -13,57 +13,24 @@ import { prismCSS } from "../prism";
13
13
  import * as Prism from "prismjs";
14
14
  import "prismjs/components/prism-typescript";
15
15
 
16
- import { DOCS_LIT_DEMO_RAW } from "./docs-lit-demo-raw";
17
-
18
- /** Class body for `liveTag`: from `@customElement("…")` through the line before the next `@customElement`. */
19
- export function excerptForCustomElement(
20
- source: string,
21
- liveTag: string,
22
- ): string | null {
23
- const lines = source.split("\n");
24
- const needle = `@customElement("${liveTag}")`;
25
- let start = -1;
26
- for (let i = 0; i < lines.length; i++) {
27
- if (lines[i].includes(needle)) {
28
- start = i;
29
- break;
30
- }
31
- }
32
- if (start < 0) return null;
33
-
34
- let end = lines.length;
35
- for (let i = start + 1; i < lines.length; i++) {
36
- if (lines[i].includes("@customElement(")) {
37
- end = i;
38
- break;
39
- }
40
- }
41
- return lines.slice(start, end).join("\n");
42
- }
16
+ import { DOCS_SOURCE_RAW } from "./docs-source-raw";
17
+ import {
18
+ excerptLines,
19
+ excerptForCustomElement,
20
+ resolveSourceExcerpt,
21
+ } from "./docs-source-excerpt";
43
22
 
44
- function excerptLines(
45
- source: string,
46
- start?: number,
47
- end?: number,
48
- ): string {
49
- const lines = source.split("\n");
50
- if (!start && !end) return source;
51
- const from = Math.max(1, start ?? 1) - 1;
52
- const to = end ?? lines.length;
53
- return lines.slice(from, to).join("\n");
54
- }
23
+ export { excerptForCustomElement } from "./docs-source-excerpt";
55
24
 
56
25
  function resolveDemoExcerpt(
57
26
  raw: string,
58
27
  liveTag: string,
59
28
  cfg: DocsLitDemoConfig,
60
29
  ): string {
61
- const auto = excerptForCustomElement(raw, liveTag);
62
- if (auto) return auto;
63
30
  if (cfg.excerptStart != null || cfg.excerptEnd != null) {
64
31
  return excerptLines(raw, cfg.excerptStart, cfg.excerptEnd);
65
32
  }
66
- return raw;
33
+ return resolveSourceExcerpt(raw, liveTag);
67
34
  }
68
35
 
69
36
  function highlightTs(code: string): string {
@@ -112,7 +79,7 @@ export class DocsLitDemo extends LitElement {
112
79
  const cfg = this.config;
113
80
  if (!cfg?.sources[0]) return;
114
81
 
115
- const raw = DOCS_LIT_DEMO_RAW[cfg.sources[0].path];
82
+ const raw = DOCS_SOURCE_RAW[cfg.sources[0].path];
116
83
  if (!raw) {
117
84
  console.warn(`[docs-lit-demo] No raw source for ${cfg.sources[0].path}`);
118
85
  return;
@@ -0,0 +1,53 @@
1
+ /** Extrait la classe Lit d'une démo à partir de `@customElement("liveTag")`. */
2
+ export function excerptForCustomElement(
3
+ source: string,
4
+ liveTag: string,
5
+ ): string | null {
6
+ const lines = source.split("\n");
7
+ const needle = `@customElement("${liveTag}")`;
8
+ let start = -1;
9
+ for (let i = 0; i < lines.length; i++) {
10
+ if (lines[i].includes(needle)) {
11
+ start = i;
12
+ break;
13
+ }
14
+ }
15
+ if (start < 0) return null;
16
+
17
+ let end = lines.length;
18
+ for (let i = start + 1; i < lines.length; i++) {
19
+ if (lines[i].includes("@customElement(")) {
20
+ end = i;
21
+ break;
22
+ }
23
+ }
24
+ return lines.slice(start, end).join("\n");
25
+ }
26
+
27
+ export function excerptLines(
28
+ source: string,
29
+ start?: number,
30
+ end?: number,
31
+ ): string {
32
+ const lines = source.split("\n");
33
+ if (!start && !end) return source;
34
+ const from = Math.max(1, start ?? 1) - 1;
35
+ const to = end ?? lines.length;
36
+ return lines.slice(from, to).join("\n");
37
+ }
38
+
39
+ export function resolveSourceExcerpt(
40
+ raw: string,
41
+ liveTag: string,
42
+ options?: { line?: number; excerptEnd?: number },
43
+ ): string {
44
+ const byTag = excerptForCustomElement(raw, liveTag);
45
+ if (byTag) return byTag;
46
+ if (options?.line != null) {
47
+ const end =
48
+ options.excerptEnd ??
49
+ Math.min(raw.split("\n").length, options.line + 72);
50
+ return excerptLines(raw, options.line, end);
51
+ }
52
+ return raw;
53
+ }
@@ -53,20 +53,36 @@ export function docsSourceLink(source: DocsSource) {
53
53
  }
54
54
 
55
55
  export function docsSourceLinks(sources: DocsSource[]) {
56
- if (!editorAvailable || sources.length === 0) return nothing;
56
+ if (sources.length === 0) return nothing;
57
57
  return html`
58
58
  <div
59
- class="flex flex-wrap items-center gap-x-1 gap-y-1 mb-4 pb-3 border-b border-neutral-200/80 dark:border-neutral-700/80"
59
+ class="flex flex-wrap items-center gap-x-1 gap-y-1 mb-3 pb-3 border-b border-neutral-200/80 dark:border-neutral-700/80"
60
60
  >
61
61
  <span class="text-xs uppercase tracking-wide text-neutral-400 mr-1"
62
62
  >Source</span
63
63
  >
64
- ${sources.map((s, i) =>
65
- i === 0
66
- ? docsSourceLink(s)
67
- : html`<span class="text-neutral-300 dark:text-neutral-600">·</span
68
- >${docsSourceLink(s)}`,
69
- )}
64
+ ${editorAvailable
65
+ ? sources.map((s, i) =>
66
+ i === 0
67
+ ? docsSourceLink(s)
68
+ : html`<span class="text-neutral-300 dark:text-neutral-600"
69
+ >·</span
70
+ >${docsSourceLink(s)}`,
71
+ )
72
+ : sources.map(
73
+ (s, i) => html`
74
+ ${i > 0
75
+ ? html`<span class="text-neutral-300 dark:text-neutral-600"
76
+ >·</span
77
+ >`
78
+ : nothing}
79
+ <span
80
+ class="text-xs font-mono text-neutral-500 dark:text-neutral-400"
81
+ title=${s.path}
82
+ >${s.label ?? s.path.split("/").pop()}</span
83
+ >
84
+ `,
85
+ )}
70
86
  </div>
71
87
  `;
72
88
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Sources brutes des démos doc (`?raw`) — `import.meta.glob` pour éviter les imports manuels.
3
+ * Clés alignées sur `DOCS_DEMO_SOURCE_REGISTRY` / `DOCS_LIT_DEMO_REGISTRY` (`src/docs/...`).
4
+ */
5
+ const rawModules = import.meta.glob<string>(
6
+ [
7
+ "../example/**/*.ts",
8
+ "../mock-api/**/*.ts",
9
+ "../../core/components/functional/**/*.demo.ts",
10
+ "../../core/components/functional/queue/queue.ts",
11
+ "../../core/components/functional/list/list.ts",
12
+ ],
13
+ { query: "?raw", import: "default", eager: true },
14
+ );
15
+
16
+ function toRegistryPath(globKey: string): string {
17
+ if (globKey.startsWith("../example/")) {
18
+ return `src/docs/example/${globKey.slice("../example/".length)}`;
19
+ }
20
+ if (globKey.startsWith("../mock-api/")) {
21
+ return `src/docs/mock-api/${globKey.slice("../mock-api/".length)}`;
22
+ }
23
+ if (globKey.startsWith("../../core/components/functional/")) {
24
+ return `src/core/components/functional/${globKey.slice("../../core/components/functional/".length)}`;
25
+ }
26
+ return globKey;
27
+ }
28
+
29
+ export const DOCS_SOURCE_RAW: Record<string, string> = Object.fromEntries(
30
+ Object.entries(rawModules).map(([key, content]) => [toRegistryPath(key), content]),
31
+ );
32
+
33
+ /** @deprecated Alias — préférer `DOCS_SOURCE_RAW`. */
34
+ export const DOCS_LIT_DEMO_RAW = DOCS_SOURCE_RAW;
@@ -1,5 +1,5 @@
1
1
  import type { APIConfiguration } from "@supersoniks/concorde/core/utils/api";
2
- import type { ApiGetResult } from "@supersoniks/concorde/core/utils/api";
2
+ import type { ApiResult } from "@supersoniks/concorde/core/utils/api";
3
3
  import { Endpoint } from "@supersoniks/concorde/core/utils/endpoint";
4
4
  import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey";
5
5
  import { get, set } from "@supersoniks/concorde/core/utils/PublisherProxy";
@@ -47,5 +47,5 @@ export const geoCommunesApiGetEndpointDynamic = new Endpoint<GeoCommuneRow[]>(
47
47
  );
48
48
 
49
49
  export const geoCommunesApiGetPublishKey = new DataProviderKey<
50
- ApiGetResult<GeoCommuneRow[]>
50
+ ApiResult<GeoCommuneRow[]>
51
51
  >(geoCommunesApiGetEndpoint.path);
@@ -0,0 +1,249 @@
1
+ import { html, LitElement, nothing } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import { post, publish, type ApiResult } from "@supersoniks/concorde/decorators";
4
+ import type { APIConfiguration } from "@supersoniks/concorde/core/utils/api";
5
+ import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey";
6
+ import { Endpoint } from "@supersoniks/concorde/core/utils/endpoint";
7
+ import {
8
+ dp,
9
+ set,
10
+ } from "@supersoniks/concorde/core/utils/PublisherProxy";
11
+ import { DOCS_MOCK_REQRES_SERVICE } from "../mock-api/urls";
12
+ import { tailwind } from "../tailwind";
13
+ import "./decorators-demo-init";
14
+
15
+ type RegisterResult = {
16
+ id?: number;
17
+ token?: string;
18
+ email?: string;
19
+ };
20
+
21
+ type RegisterRequest = {
22
+ email: string;
23
+ password: string;
24
+ };
25
+
26
+ const registerPostEndpoint = new Endpoint<RegisterResult>("api/register");
27
+
28
+ type SessionSyncResult = {
29
+ ok?: boolean;
30
+ sessionId?: string;
31
+ serverTime?: number;
32
+ echo?: { note?: string };
33
+ };
34
+
35
+ type SessionSyncRequest = {
36
+ note: string;
37
+ };
38
+
39
+ const sessionSyncPostEndpoint = new Endpoint<
40
+ SessionSyncResult,
41
+ { sessionId: string }
42
+ >("api/sessions/${sessionId}/sync");
43
+
44
+ const reqresApiConfiguration: APIConfiguration = {
45
+ serviceURL: DOCS_MOCK_REQRES_SERVICE,
46
+ token: null,
47
+ userName: null,
48
+ password: null,
49
+ authToken: null,
50
+ tokenProvider: null,
51
+ };
52
+
53
+ export const docsDemoReqresApiConfigurationKey =
54
+ new DataProviderKey<APIConfiguration>("docsDemoReqresApiConfiguration");
55
+
56
+ set(docsDemoReqresApiConfigurationKey, reqresApiConfiguration);
57
+
58
+ export const docsDemoPostRequestKey = new DataProviderKey<RegisterRequest>(
59
+ "docsDemoPostRequest",
60
+ );
61
+ /** Clé body distincte — la page doc monte les deux démos live en même temps. */
62
+ export const docsDemoPostPublishRequestKey = new DataProviderKey<RegisterRequest>(
63
+ "docsDemoPostPublishRequest",
64
+ );
65
+ export const docsDemoPostTriggerKey = new DataProviderKey<number>(
66
+ "docsDemoPostTrigger",
67
+ );
68
+ export const docsDemoPostDynamicRequestKey = new DataProviderKey<SessionSyncRequest>(
69
+ "docsDemoPostDynamicRequest",
70
+ );
71
+
72
+ @customElement("demo-api-post")
73
+ export class DemoApiPost extends LitElement {
74
+ static styles = [tailwind];
75
+
76
+ @post(
77
+ registerPostEndpoint,
78
+ docsDemoPostRequestKey,
79
+ docsDemoReqresApiConfigurationKey,
80
+ { triggerKey: docsDemoPostTriggerKey },
81
+ )
82
+ @state()
83
+ payload?: ApiResult<RegisterResult> | null;
84
+
85
+ connectedCallback() {
86
+ super.connectedCallback();
87
+ set(docsDemoPostRequestKey, {
88
+ email: "demo@concorde.local",
89
+ password: "demo-password",
90
+ });
91
+ set(docsDemoPostTriggerKey, 0);
92
+ }
93
+
94
+ private send() {
95
+ set(docsDemoPostRequestKey, {
96
+ email: "demo@concorde.local",
97
+ password: "demo-password",
98
+ });
99
+ }
100
+
101
+ private resendSameBody() {
102
+ dp(docsDemoPostTriggerKey).invalidate();
103
+ }
104
+
105
+ render() {
106
+ const result = this.payload?.result;
107
+ const status = this.payload?.response?.status;
108
+ return html`
109
+ <div class="space-y-4">
110
+ <p class="text-sm text-neutral-600 dark:text-neutral-400">
111
+ <code>@post</code> vers le mock
112
+ <code>POST /docs-mock-api/api/register</code> — body lu depuis
113
+ <code>docsDemoPostRequest</code>.
114
+ </p>
115
+ <div class="flex flex-wrap gap-2">
116
+ <sonic-button @click=${this.send}>Set body &amp; POST</sonic-button>
117
+ <sonic-button variant="secondary" @click=${this.resendSameBody}>
118
+ invalidate trigger (re-POST)
119
+ </sonic-button>
120
+ </div>
121
+ <div
122
+ class="rounded border border-neutral-200 dark:border-neutral-700 p-3 text-sm space-y-1"
123
+ >
124
+ <p>HTTP status: <strong>${status ?? "—"}</strong></p>
125
+ ${result?.id != null
126
+ ? html`<p>id: <span class="font-mono">${result.id}</span></p>`
127
+ : nothing}
128
+ ${result?.email
129
+ ? html`<p>email: <span class="font-mono">${result.email}</span></p>`
130
+ : nothing}
131
+ ${result?.token
132
+ ? html`<p>
133
+ token:
134
+ <span class="font-mono break-all">${result.token}</span>
135
+ </p>`
136
+ : nothing}
137
+ </div>
138
+ </div>
139
+ `;
140
+ }
141
+ }
142
+
143
+ @customElement("demo-api-post-dynamic")
144
+ export class DemoApiPostDynamic extends LitElement {
145
+ static styles = [tailwind];
146
+
147
+ @property({ type: String }) sessionId = "alpha";
148
+
149
+ @post(
150
+ sessionSyncPostEndpoint,
151
+ docsDemoPostDynamicRequestKey,
152
+ docsDemoReqresApiConfigurationKey,
153
+ )
154
+ @state()
155
+ payload?: ApiResult<SessionSyncResult> | null;
156
+
157
+ connectedCallback() {
158
+ super.connectedCallback();
159
+ set(docsDemoPostDynamicRequestKey, { note: "docs @post dynamic path" });
160
+ }
161
+
162
+ private setSession(sessionId: string) {
163
+ this.sessionId = sessionId;
164
+ }
165
+
166
+ render() {
167
+ const result = this.payload?.result;
168
+ const status = this.payload?.response?.status;
169
+ const requestUrl = this.payload?.request?.url ?? "";
170
+ return html`
171
+ <div class="space-y-4">
172
+ <p class="text-sm text-neutral-600 dark:text-neutral-400">
173
+ <code>@post</code> vers
174
+ <code>api/sessions/${"${sessionId}"}/sync</code> — changer
175
+ <code>sessionId</code> sur l'hôte relance le POST (même body).
176
+ </p>
177
+ <div class="flex flex-wrap items-center gap-2">
178
+ <span class="text-sm text-neutral-500">sessionId :</span>
179
+ ${(["alpha", "beta", "gamma"] as const).map(
180
+ (id) => html`
181
+ <sonic-button
182
+ variant=${this.sessionId === id ? "primary" : "secondary"}
183
+ size="sm"
184
+ @click=${() => this.setSession(id)}
185
+ >
186
+ ${id}
187
+ </sonic-button>
188
+ `,
189
+ )}
190
+ </div>
191
+ <div
192
+ class="rounded border border-neutral-200 dark:border-neutral-700 p-3 text-sm space-y-1"
193
+ >
194
+ <p>HTTP status: <strong>${status ?? "—"}</strong></p>
195
+ <p>
196
+ Dernier path :
197
+ <span class="font-mono text-xs break-all">${requestUrl || "—"}</span>
198
+ </p>
199
+ <p>
200
+ Réponse <code>sessionId</code> :
201
+ <strong class="font-mono">${result?.sessionId ?? "—"}</strong>
202
+ </p>
203
+ </div>
204
+ </div>
205
+ `;
206
+ }
207
+ }
208
+
209
+ @customElement("demo-api-post-publish")
210
+ export class DemoApiPostPublish extends LitElement {
211
+ static styles = [tailwind];
212
+
213
+ @post(
214
+ registerPostEndpoint,
215
+ docsDemoPostPublishRequestKey,
216
+ docsDemoReqresApiConfigurationKey,
217
+ )
218
+ @publish(
219
+ registerPostEndpoint.getDataProviderKey() as DataProviderKey<
220
+ ApiResult<RegisterResult>
221
+ >,
222
+ )
223
+ @state()
224
+ payload?: ApiResult<RegisterResult> | null;
225
+
226
+ connectedCallback() {
227
+ super.connectedCallback();
228
+ set(docsDemoPostPublishRequestKey, {
229
+ email: "publish@concorde.local",
230
+ password: "secret",
231
+ });
232
+ }
233
+
234
+ render() {
235
+ const email = this.payload?.result?.email;
236
+ return html`
237
+ <div class="space-y-3 text-sm">
238
+ <p class="text-neutral-600 dark:text-neutral-400">
239
+ <code>@post</code> + <code>@publish</code> sur le même champ — même
240
+ pattern que <code>@get</code> + <code>@publish</code>.
241
+ </p>
242
+ <p>
243
+ Result email:
244
+ <strong class="font-mono">${email ?? "—"}</strong>
245
+ </p>
246
+ </div>
247
+ `;
248
+ }
249
+ }
@@ -3,7 +3,7 @@ import { customElement, property, state } from "lit/decorators.js";
3
3
  import { get, handle, publish, subscribe } from "@supersoniks/concorde/decorators";
4
4
  import type {
5
5
  APIConfiguration,
6
- ApiGetResult,
6
+ ApiResult,
7
7
  } from "@supersoniks/concorde/core/utils/api";
8
8
  import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey";
9
9
  import { sub } from "@supersoniks/concorde/directives";
@@ -92,7 +92,7 @@ export class DemoApiGet extends LitElement {
92
92
 
93
93
  @get(geoCommunesApiGetEndpoint, docsDemoGeoApiConfigurationKey)
94
94
  @state()
95
- geoApiPayload?: ApiGetResult<GeoCommuneRow[]>;
95
+ geoApiPayload?: ApiResult<GeoCommuneRow[]>;
96
96
 
97
97
  render() {
98
98
  const rows = this.geoApiPayload?.result;
@@ -102,7 +102,7 @@ export class DemoApiGet extends LitElement {
102
102
  <p class="text-sm text-neutral-600 dark:text-neutral-400">
103
103
  <code>@get</code> — même service que
104
104
  <code>sonic-queue</code> (<code>/docs-mock-api/geo/</code>).
105
- Propriété typée <code>ApiGetResult&lt;T&gt;</code>.
105
+ Propriété typée <code>ApiResult&lt;T&gt;</code>.
106
106
  </p>
107
107
  ${status != null
108
108
  ? html`<p class="text-xs text-neutral-500">HTTP ${status}</p>`
@@ -141,7 +141,7 @@ export class DemoApiGetConfigurationKey extends LitElement {
141
141
 
142
142
  @get(geoCommunesApiGetEndpointDynamic, docsDemoDynApiConfKeyTemplate)
143
143
  @state()
144
- geoApiPayloadDyn?: ApiGetResult<GeoCommuneRow[]>;
144
+ geoApiPayloadDyn?: ApiResult<GeoCommuneRow[]>;
145
145
 
146
146
  private touchCurrentConfigPublisher() {
147
147
  const key =
@@ -237,7 +237,7 @@ export class DemoApiGetPublishSubscribe extends LitElement {
237
237
  @get(geoCommunesApiGetEndpoint)
238
238
  @publish(geoCommunesApiGetPublishKey)
239
239
  @state()
240
- geoApiPayloadPublished: ApiGetResult<GeoCommuneRow[]> | null = null;
240
+ geoApiPayloadPublished: ApiResult<GeoCommuneRow[]> | null = null;
241
241
 
242
242
  @state()
243
243
  @subscribe(geoCommunesApiGetPublishKey.result)
@@ -24,6 +24,7 @@ import {
24
24
  import { tailwind } from "../tailwind";
25
25
  import "./decorators-demo-bind-demos";
26
26
  import "./decorators-demo-subscribe-publish-get-demos";
27
+ import "./decorators-demo-post";
27
28
 
28
29
  @customElement("demo-ancestor-attribute")
29
30
  export class DemoAncestorAttribute extends LitElement {
@@ -1,7 +1,7 @@
1
1
  import { html, LitElement, nothing } from "lit";
2
2
  import { customElement, state } from "lit/decorators.js";
3
3
  import { get } from "@supersoniks/concorde/core/decorators/api";
4
- import type { ApiGetResult } from "@supersoniks/concorde/core/utils/api";
4
+ import type { ApiResult } from "@supersoniks/concorde/core/utils/api";
5
5
  import { Endpoint } from "@supersoniks/concorde/core/utils/endpoint";
6
6
  import { set } from "@supersoniks/concorde/core/utils/PublisherProxy";
7
7
  import { wording } from "@supersoniks/concorde/core/directives/Wording";
@@ -33,7 +33,7 @@ type ProtectedResult = {
33
33
 
34
34
  const protectedEndpoint = new Endpoint<ProtectedResult>("api/config/protected");
35
35
 
36
- function resultPanel(payload: ApiGetResult<ProtectedResult> | null | undefined) {
36
+ function resultPanel(payload: ApiResult<ProtectedResult> | null | undefined) {
37
37
  const status = payload?.response?.status;
38
38
  const r = payload?.result;
39
39
  if (payload === null) {
@@ -66,7 +66,7 @@ export class DocsApiConfigBearerDemo extends LitElement {
66
66
 
67
67
  @get(protectedEndpoint, docsApiConfBearerKey)
68
68
  @state()
69
- private payload: ApiGetResult<ProtectedResult> | null = null;
69
+ private payload: ApiResult<ProtectedResult> | null = null;
70
70
 
71
71
  render() {
72
72
  return html`
@@ -86,7 +86,7 @@ export class DocsApiConfigTokenProviderDemo extends LitElement {
86
86
 
87
87
  @get(protectedEndpoint, docsApiConfTokenProviderKey)
88
88
  @state()
89
- private payload: ApiGetResult<ProtectedResult> | null = null;
89
+ private payload: ApiResult<ProtectedResult> | null = null;
90
90
 
91
91
  render() {
92
92
  return html`
@@ -107,7 +107,7 @@ export class DocsApiConfigStaleTokenDemo extends LitElement {
107
107
 
108
108
  @get(protectedEndpoint, docsApiConfStaleTokenKey)
109
109
  @state()
110
- private payload: ApiGetResult<ProtectedResult> | null = null;
110
+ private payload: ApiResult<ProtectedResult> | null = null;
111
111
 
112
112
  render() {
113
113
  const refreshed =
@@ -180,6 +180,26 @@ export async function handleDocsMockApiRequest(
180
180
  );
181
181
  }
182
182
 
183
+ const sessionSyncMatch = subPath.match(/^\/api\/sessions\/([^/]+)\/sync$/);
184
+ if (sessionSyncMatch && method === "POST") {
185
+ const sessionId = decodeURIComponent(sessionSyncMatch[1]);
186
+ let body: Record<string, unknown> = {};
187
+ try {
188
+ const text = await request.text();
189
+ if (text.trim()) {
190
+ body = JSON.parse(text) as Record<string, unknown>;
191
+ }
192
+ } catch {
193
+ body = {};
194
+ }
195
+ return json({
196
+ ok: true,
197
+ sessionId,
198
+ serverTime: Date.now(),
199
+ echo: body,
200
+ });
201
+ }
202
+
183
203
  if (subPath === "/api/register/echo" && method === "GET") {
184
204
  const email = url.searchParams.get("email") ?? "";
185
205
  const password = url.searchParams.get("password") ?? "";
@@ -153,6 +153,18 @@ export class DocsNavigation extends LitElement {
153
153
  label: "@get",
154
154
  href: "#docs/_decorators/get.md/get",
155
155
  },
156
+ {
157
+ label: "@post",
158
+ href: "#docs/_decorators/post.md/post",
159
+ },
160
+ {
161
+ label: "@put",
162
+ href: "#docs/_decorators/put.md/put",
163
+ },
164
+ {
165
+ label: "@patch",
166
+ href: "#docs/_decorators/patch.md/patch",
167
+ },
156
168
  {
157
169
  label: "@onAssign",
158
170
  href: "#docs/_decorators/on-assign.md/on-assign",
@@ -174,6 +186,10 @@ export class DocsNavigation extends LitElement {
174
186
  label: "Endpoint",
175
187
  href: "#docs/_misc/endpoint.md/endpoint",
176
188
  },
189
+ {
190
+ label: "Dynamic paths",
191
+ href: "#docs/_misc/dynamic-path.md/dynamic-path",
192
+ },
177
193
  {
178
194
  label: "API configuration",
179
195
  href: "#docs/_misc/api-configuration.md/api-configuration",