@supersoniks/concorde 4.6.0 → 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 (179) 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 +127 -127
  18. package/concorde-core.es.js +1435 -1364
  19. package/dist/altcha-widget.js +2662 -0
  20. package/dist/concorde-core.bundle.js +127 -127
  21. package/dist/concorde-core.es.js +1435 -1364
  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 +34 -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/directives/DataProvider.sub.spec.ts +96 -0
  108. package/src/core/directives/DataProvider.ts +109 -40
  109. package/src/core/utils/PublisherProxy.ts +33 -18
  110. package/src/core/utils/dataProviderKey.ts +23 -0
  111. package/src/core/utils/publisherPathKey.spec.ts +58 -0
  112. package/src/docs/_core-concept/dataFlow.md +73 -0
  113. package/src/docs/_core-concept/subscriber.md +9 -10
  114. package/src/docs/_decorators/ancestor-attribute.md +4 -3
  115. package/src/docs/_decorators/auto-subscribe.md +19 -16
  116. package/src/docs/_decorators/bind.md +19 -16
  117. package/src/docs/_decorators/get.md +7 -4
  118. package/src/docs/_decorators/handle.md +15 -13
  119. package/src/docs/_decorators/on-assign.md +53 -53
  120. package/src/docs/_decorators/publish.md +2 -1
  121. package/src/docs/_decorators/subscribe.md +70 -9
  122. package/src/docs/_decorators/wait-for-ancestors.md +13 -10
  123. package/src/docs/_directives/sub.md +91 -0
  124. package/src/docs/_getting-started/ai-agents.md +56 -0
  125. package/src/docs/_getting-started/concorde-manual-install.md +133 -0
  126. package/src/docs/_getting-started/concorde-outside.md +13 -123
  127. package/src/docs/_getting-started/create-a-component.md +2 -0
  128. package/src/docs/_getting-started/my-first-component.md +236 -0
  129. package/src/docs/_getting-started/my-first-subscriber.md +29 -83
  130. package/src/docs/_getting-started/pubsub.md +21 -134
  131. package/src/docs/_getting-started/start.md +26 -18
  132. package/src/docs/_misc/api-configuration.md +79 -0
  133. package/src/docs/_misc/dataProviderKey.md +34 -1
  134. package/src/docs/_misc/docs-mock-api.md +60 -0
  135. package/src/docs/_misc/endpoint.md +2 -1
  136. package/src/docs/_misc/html-integration.md +13 -0
  137. package/src/docs/code.ts +58 -12
  138. package/src/docs/components/docs-demo-sources.ts +397 -0
  139. package/src/docs/components/docs-lit-demo-raw.ts +28 -0
  140. package/src/docs/components/docs-lit-demo.ts +166 -0
  141. package/src/docs/components/docs-source-link.ts +72 -0
  142. package/src/docs/docs-location.ts +54 -0
  143. package/src/docs/docs.ts +12 -0
  144. package/src/docs/example/decorators-demo-bind-demos.ts +41 -46
  145. package/src/docs/example/decorators-demo-geo.ts +16 -11
  146. package/src/docs/example/decorators-demo-init.ts +2 -228
  147. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +54 -14
  148. package/src/docs/example/decorators-demo.ts +71 -70
  149. package/src/docs/example/docs-api-config-demos.ts +234 -0
  150. package/src/docs/example/docs-joke-demos.ts +297 -0
  151. package/src/docs/example/docs-list-demos.ts +179 -0
  152. package/src/docs/example/docs-provider-keys.ts +315 -0
  153. package/src/docs/example/docs-queue-demos.ts +114 -0
  154. package/src/docs/example/docs-router-demos.ts +89 -0
  155. package/src/docs/example/docs-submit-demos.ts +455 -0
  156. package/src/docs/example/docs-toggle-demos.ts +73 -0
  157. package/src/docs/example/docs-user-two-scopes.ts +37 -0
  158. package/src/docs/example/docs-users-list.ts +71 -0
  159. package/src/docs/example/users.ts +41 -24
  160. package/src/docs/mock-api/api-config-mock.ts +152 -0
  161. package/src/docs/mock-api/fixtures.ts +377 -0
  162. package/src/docs/mock-api/register.ts +25 -0
  163. package/src/docs/mock-api/router.ts +234 -0
  164. package/src/docs/mock-api/service-worker.ts +23 -0
  165. package/src/docs/mock-api/urls.ts +11 -0
  166. package/src/docs/navigation/navigation.ts +39 -7
  167. package/src/docs/search/docs-search.json +4021 -936
  168. package/src/docs/search/markdown-renderer.ts +7 -3
  169. package/src/docs/search/page.ts +11 -14
  170. package/src/docs/search/sonic-code-markdown.spec.ts +29 -0
  171. package/src/docs/search/sonic-code-markdown.ts +28 -0
  172. package/src/docs.ts +4 -0
  173. package/src/tsconfig.json +87 -0
  174. package/src/tsconfig.tsbuildinfo +1 -1
  175. package/vite.config.mts +8 -0
  176. package/docs/assets/index-CaysOMFz.js +0 -5046
  177. package/docs/assets/index-D8mGoXzF.css +0 -1
  178. package/docs/src/docs/_misc/templates-demo.md +0 -19
  179. package/src/docs/_misc/templates-demo.md +0 -19
@@ -1,50 +1,67 @@
1
1
  import { html, LitElement, nothing } from "lit";
2
- import { customElement, property } from "lit/decorators.js";
2
+ import { customElement, state } from "lit/decorators.js";
3
+ import {
4
+ ancestorAttribute,
5
+ subscribe,
6
+ } from "@supersoniks/concorde/core/decorators/Subscriber";
7
+ import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey";
3
8
  import { tailwind } from "../tailwind";
4
9
 
5
- import Subscriber from "@supersoniks/concorde/core/mixins/Subscriber";
10
+ import "../../core/components/ui/image/image";
11
+ import "../../core/components/ui/button/button";
12
+ import "../../core/components/ui/icon/icon";
13
+
14
+ /** Shape of a user row in the docs mock API and static demos. */
15
+ export type DocsUserData = {
16
+ id?: string;
17
+ first_name: string;
18
+ last_name: string;
19
+ email: string;
20
+ avatar: string;
21
+ };
22
+
23
+ /**
24
+ * Row scope: resolved from the nearest ancestor `dataProvider`
25
+ * (static host, form preview, or `…/list-item/n` from sonic-list).
26
+ */
27
+ export const docsUserRowKey = new DataProviderKey<
28
+ DocsUserData,
29
+ { dataProvider: string | null }
30
+ >("${dataProvider}");
6
31
 
7
32
  @customElement("docs-user")
8
- export class user extends Subscriber(LitElement) {
9
- // add tailwind
33
+ export class DocsUser extends LitElement {
10
34
  static styles = [tailwind];
11
35
 
12
- @property({ type: String }) id = "";
13
- @property({ type: String }) avatar = "";
14
- @property({ type: String }) first_name = "";
15
- @property({ type: String }) email = "";
16
- @property({ type: String }) last_name = "";
36
+ @ancestorAttribute("dataProvider")
37
+ dataProvider: string | null = null;
38
+
39
+ @subscribe(docsUserRowKey)
40
+ @state()
41
+ user: DocsUserData | null = null;
17
42
 
18
43
  render() {
44
+ const u = this.user;
45
+ if (!u) return nothing;
46
+
19
47
  return html`<div
20
48
  class="flex items-center gap-3 rounded-md hover:bg-neutral-50 -mx-2 p-2"
21
49
  >
22
50
  <sonic-image
23
- src=${this.avatar}
51
+ src=${u.avatar}
24
52
  rounded="md"
25
53
  ratio="1/1"
26
54
  class="w-16 block"
27
55
  ></sonic-image>
28
56
  <div>
29
57
  <div>
30
- ${this.first_name} <span class="font-bold">${this.last_name}</span>
58
+ ${u.first_name} <span class="font-bold">${u.last_name}</span>
31
59
  </div>
32
- <div class="text-sm text-neutral-400">${this.email}</div>
60
+ <div class="text-sm text-neutral-400">${u.email}</div>
33
61
  </div>
34
62
  <div class="ml-auto relative">
35
- ${this.id == "2" || this.id == "5"
36
- ? html`
37
- <sonic-badge
38
- type="danger"
39
- size="2xs"
40
- class="absolute left-0 -top-1 z-10"
41
- >
42
- ${this.id}</sonic-badge
43
- >
44
- `
45
- : nothing}
46
63
  <sonic-button
47
- href="mailto:${this.email}"
64
+ href=${u.email ? `mailto:${u.email}` : undefined}
48
65
  size="sm"
49
66
  variant="outline"
50
67
  shape="circle"
@@ -0,0 +1,152 @@
1
+ import { DOCS_MOCK_API_BASE } from "./urls";
2
+
3
+ /** Bearer accepté par GET /api/config/protected */
4
+ export const DOCS_MOCK_TOKEN_VALID = "docs-mock-valid-token";
5
+ /** Déclenche HTTP 498 → renouvellement via tokenProvider */
6
+ export const DOCS_MOCK_TOKEN_STALE = "docs-mock-stale-token";
7
+ /** Émis par GET /auth/token après Basic auth ou eventsApiToken */
8
+ export const DOCS_MOCK_TOKEN_FRESH = "docs-mock-fresh-token";
9
+
10
+ const DOCS_MOCK_BASIC_USER = "demo";
11
+ const DOCS_MOCK_BASIC_PASSWORD = "secret";
12
+ /** Valeur de l’attribut ancêtre `eventsApiToken` (Bearer pour /auth/token) */
13
+ export const DOCS_MOCK_EVENTS_API_TOKEN = "docs-mock-events-token";
14
+
15
+ const WORDING_CATALOG: Record<string, Record<string, string>> = {
16
+ "api-config.greeting": { fr: "Bonjour", en: "Hello" },
17
+ "api-config.farewell": { fr: "Au revoir", en: "Goodbye" },
18
+ "api-config.hint": {
19
+ fr: "Libellés servis par le mock /wording/labels",
20
+ en: "Labels served by mock /wording/labels",
21
+ },
22
+ };
23
+
24
+ function json(body: unknown, init: ResponseInit = {}): Response {
25
+ const headers = new Headers(init.headers);
26
+ headers.set("Content-Type", "application/json; charset=utf-8");
27
+ headers.set("Access-Control-Allow-Origin", "*");
28
+ headers.set("X-Docs-Mock-Api", "concorde-api-config");
29
+ return new Response(JSON.stringify(body), { ...init, headers });
30
+ }
31
+
32
+ type ParsedAuth =
33
+ | { kind: "none" }
34
+ | { kind: "basic"; user: string; password: string }
35
+ | { kind: "bearer"; token: string };
36
+
37
+ function parseAuthorization(request: Request): ParsedAuth {
38
+ const header = request.headers.get("authorization");
39
+ if (!header) return { kind: "none" };
40
+ const basic = header.match(/^Basic\s+(.+)$/i);
41
+ if (basic) {
42
+ try {
43
+ const decoded = atob(basic[1]);
44
+ const sep = decoded.indexOf(":");
45
+ if (sep < 0) return { kind: "none" };
46
+ return {
47
+ kind: "basic",
48
+ user: decoded.slice(0, sep),
49
+ password: decoded.slice(sep + 1),
50
+ };
51
+ } catch {
52
+ return { kind: "none" };
53
+ }
54
+ }
55
+ const bearer = header.match(/^Bearer\s+(.+)$/i);
56
+ if (bearer) return { kind: "bearer", token: bearer[1].trim() };
57
+ return { kind: "none" };
58
+ }
59
+
60
+ function isValidBasic(auth: ParsedAuth): boolean {
61
+ return (
62
+ auth.kind === "basic" &&
63
+ auth.user === DOCS_MOCK_BASIC_USER &&
64
+ auth.password === DOCS_MOCK_BASIC_PASSWORD
65
+ );
66
+ }
67
+
68
+ function wordingLabels(url: URL): Response {
69
+ const lang = url.searchParams.get("lang") || "fr";
70
+ const labels = url.searchParams.getAll("labels[]").length
71
+ ? url.searchParams.getAll("labels[]")
72
+ : url.searchParams.getAll("labels");
73
+ const result: Record<string, string> = {};
74
+ for (const key of labels) {
75
+ const entry = WORDING_CATALOG[key];
76
+ result[key] =
77
+ entry?.[lang] ?? entry?.en ?? `[missing:${key}]`;
78
+ }
79
+ return json({ lang, labels: result, ...result });
80
+ }
81
+
82
+ /**
83
+ * Routes mock pour la page doc APIConfiguration (token, 498, wording).
84
+ */
85
+ export function handleApiConfigMockRequest(
86
+ request: Request,
87
+ subPath: string,
88
+ method: string,
89
+ ): Response | null {
90
+ if (subPath === "/auth/token" && method === "GET") {
91
+ const auth = parseAuthorization(request);
92
+ if (isValidBasic(auth)) {
93
+ return json({
94
+ token: DOCS_MOCK_TOKEN_FRESH,
95
+ source: "basic",
96
+ serviceHost: new URL(request.url).searchParams.get("serviceHost"),
97
+ });
98
+ }
99
+ if (
100
+ auth.kind === "bearer" &&
101
+ auth.token === DOCS_MOCK_EVENTS_API_TOKEN
102
+ ) {
103
+ return json({
104
+ token: DOCS_MOCK_TOKEN_FRESH,
105
+ source: "eventsApiToken",
106
+ });
107
+ }
108
+ return json({ error: "Unauthorized" }, { status: 401 });
109
+ }
110
+
111
+ if (subPath === "/api/config/protected" && method === "GET") {
112
+ const auth = parseAuthorization(request);
113
+ if (auth.kind === "bearer") {
114
+ if (auth.token === DOCS_MOCK_TOKEN_STALE) {
115
+ return json({ error: "Token expired", code: "token_stale" }, { status: 498 });
116
+ }
117
+ if (
118
+ auth.token === DOCS_MOCK_TOKEN_VALID ||
119
+ auth.token === DOCS_MOCK_TOKEN_FRESH
120
+ ) {
121
+ return json({
122
+ ok: true,
123
+ message: "Protected resource",
124
+ tokenUsed: auth.token,
125
+ });
126
+ }
127
+ }
128
+ if (isValidBasic(auth)) {
129
+ return json({
130
+ ok: true,
131
+ message: "Protected resource (basic)",
132
+ tokenUsed: null,
133
+ });
134
+ }
135
+ return json({ error: "Unauthorized" }, { status: 401 });
136
+ }
137
+
138
+ if (
139
+ (subPath === "/wording/labels" || subPath.startsWith("/wording/labels")) &&
140
+ method === "GET"
141
+ ) {
142
+ return wordingLabels(new URL(request.url));
143
+ }
144
+
145
+ return null;
146
+ }
147
+
148
+ export const API_CONFIG_MOCK_PATHS = [
149
+ `${DOCS_MOCK_API_BASE}/auth/token`,
150
+ `${DOCS_MOCK_API_BASE}/api/config/protected`,
151
+ `${DOCS_MOCK_API_BASE}/wording/labels`,
152
+ ] as const;
@@ -0,0 +1,377 @@
1
+ /** Données type ReqRes pour les démos doc (sans appel externe). */
2
+
3
+ export type ReqresUser = {
4
+ id: number;
5
+ email: string;
6
+ first_name: string;
7
+ last_name: string;
8
+ avatar: string;
9
+ };
10
+
11
+ const reqresAvatar = (seed: string) =>
12
+ `https://i.pravatar.cc/150?u=${encodeURIComponent(seed)}`;
13
+
14
+ const REQRES_EXTRA_NAMES: [string, string][] = [
15
+ ["Michael", "Lawson"],
16
+ ["Tobias", "Funke"],
17
+ ["Lindsay", "Funke"],
18
+ ["Maeby", "Funke"],
19
+ ["George", "Michael"],
20
+ ["Annye", "Vang"],
21
+ ["Rachel", "Howe"],
22
+ ["Terry", "Medhurst"],
23
+ ["Bette", "Haag"],
24
+ ["Lester", "Solomon"],
25
+ ["April", "Douglas"],
26
+ ["Marian", "Roberts"],
27
+ ["Kathryn", "Murphy"],
28
+ ["Gloria", "Armstrong"],
29
+ ["Adam", "Bradley"],
30
+ ["Angela", "Hicks"],
31
+ ["Rose", "Wheeler"],
32
+ ["Sara", "Berry"],
33
+ ["Glen", "Kuhn"],
34
+ ["Roger", "Cox"],
35
+ ];
36
+
37
+ function buildReqresUser(
38
+ id: number,
39
+ first_name: string,
40
+ last_name: string,
41
+ ): ReqresUser {
42
+ const slug = `${first_name}-${last_name}`.toLowerCase();
43
+ return {
44
+ id,
45
+ email: `${first_name.toLowerCase()}.${last_name.toLowerCase()}@example.com`,
46
+ first_name,
47
+ last_name,
48
+ avatar: reqresAvatar(slug),
49
+ };
50
+ }
51
+
52
+ /** Jeu étendu pour démos queue / lazyload (pagination par offset). */
53
+ export const DOCS_REQRES_USERS: ReqresUser[] = [
54
+ buildReqresUser(1, "George", "Bluth"),
55
+ buildReqresUser(2, "Janet", "Weaver"),
56
+ buildReqresUser(3, "Emma", "Wong"),
57
+ buildReqresUser(4, "Eve", "Holt"),
58
+ ...REQRES_EXTRA_NAMES.map(([first_name, last_name], index) =>
59
+ buildReqresUser(5 + index, first_name, last_name),
60
+ ),
61
+ ];
62
+
63
+ export type ReqresUserResponse = { data: ReqresUser };
64
+ export type ReqresUsersListResponse = {
65
+ page: number;
66
+ per_page: number;
67
+ total: number;
68
+ total_pages: number;
69
+ data: ReqresUser[];
70
+ };
71
+
72
+ /** Filter users for mock search (`q` — first name, last name, email). Same logic as the TS starter. */
73
+ export function filterDocsUsers(
74
+ users: ReqresUser[],
75
+ query: string | null | undefined,
76
+ ): ReqresUser[] {
77
+ const q = query?.trim().toLowerCase();
78
+ if (!q) return users;
79
+ return users.filter((user) => {
80
+ const haystack =
81
+ `${user.first_name} ${user.last_name} ${user.email}`.toLowerCase();
82
+ return haystack.includes(q);
83
+ });
84
+ }
85
+
86
+ export type GeoCommuneRow = { nom: string; code: string };
87
+
88
+ export const DOCS_GEO_COMMUNES: GeoCommuneRow[] = [
89
+ { nom: "Paris", code: "75056" },
90
+ { nom: "Lyon", code: "69123" },
91
+ { nom: "Marseille", code: "13055" },
92
+ { nom: "Toulouse", code: "31555" },
93
+ { nom: "Lille", code: "59350" },
94
+ { nom: "Bordeaux", code: "33063" },
95
+ { nom: "Nantes", code: "44109" },
96
+ { nom: "Strasbourg", code: "67482" },
97
+ { nom: "Montpellier", code: "34172" },
98
+ { nom: "Rennes", code: "35238" },
99
+ ];
100
+
101
+ /** JokeAPI-style content flags (checkbox / radio / switch blacklist demos). */
102
+ export type DocsJokeFlagKey =
103
+ | "nsfw"
104
+ | "religious"
105
+ | "political"
106
+ | "racist"
107
+ | "sexist"
108
+ | "explicit";
109
+
110
+ /** Forme proche de JokeAPI v2 (`key="jokes"` sur sonic-queue / list). */
111
+ export type DocsJokeItem = {
112
+ categories: string[];
113
+ createdAt: string;
114
+ id: number;
115
+ joke: string;
116
+ lang: "fr" | "en";
117
+ flags: Record<DocsJokeFlagKey, boolean>;
118
+ safe: boolean;
119
+ type: string;
120
+ setup?: string;
121
+ delivery?: string;
122
+ };
123
+
124
+ const jokeFlags = (
125
+ partial: Partial<Record<DocsJokeFlagKey, boolean>> = {},
126
+ ): Record<DocsJokeFlagKey, boolean> => ({
127
+ nsfw: false,
128
+ religious: false,
129
+ political: false,
130
+ racist: false,
131
+ sexist: false,
132
+ explicit: false,
133
+ ...partial,
134
+ });
135
+
136
+ export type DocsJokeApiResponse = {
137
+ error: boolean;
138
+ amount: number;
139
+ jokes: DocsJokeItem[];
140
+ };
141
+
142
+ const SAMPLE_JOKES: DocsJokeItem[] = [
143
+ {
144
+ categories: ["Programming"],
145
+ createdAt: "2020-01-01 00:00:00",
146
+ id: 1,
147
+ lang: "en",
148
+ joke: "Why do programmers prefer dark mode? Because light attracts bugs.",
149
+ flags: jokeFlags(),
150
+ safe: true,
151
+ type: "single",
152
+ },
153
+ {
154
+ categories: ["Misc"],
155
+ createdAt: "2020-01-02 00:00:00",
156
+ id: 2,
157
+ lang: "en",
158
+ joke: "I told my computer I needed a break — it said: no problem, I'll go to sleep.",
159
+ flags: jokeFlags({ nsfw: true }),
160
+ safe: false,
161
+ type: "single",
162
+ },
163
+ {
164
+ categories: ["Pun"],
165
+ createdAt: "2020-01-03 00:00:00",
166
+ id: 3,
167
+ lang: "en",
168
+ joke: "Concorde docs run offline: even the API is a local joke.",
169
+ flags: jokeFlags(),
170
+ safe: true,
171
+ type: "single",
172
+ },
173
+ {
174
+ categories: ["Animals"],
175
+ createdAt: "2020-01-04 00:00:00",
176
+ id: 4,
177
+ lang: "en",
178
+ joke: "The dog ate my homework and my pull request.",
179
+ flags: jokeFlags({ political: true }),
180
+ safe: true,
181
+ type: "single",
182
+ },
183
+ {
184
+ categories: ["Animals", "Pun"],
185
+ createdAt: "2020-01-05 00:00:00",
186
+ id: 5,
187
+ lang: "fr",
188
+ joke: "Quel est le chien préféré des développeurs ? Le labrador Retriever.",
189
+ flags: jokeFlags(),
190
+ safe: true,
191
+ type: "single",
192
+ },
193
+ {
194
+ categories: ["Animals"],
195
+ createdAt: "2020-01-06 00:00:00",
196
+ id: 6,
197
+ lang: "fr",
198
+ joke: "Un chat et un chien discutent de TypeScript : le chien dit « woof », le chat compile.",
199
+ flags: jokeFlags({ racist: true }),
200
+ safe: false,
201
+ type: "single",
202
+ },
203
+ {
204
+ categories: ["Misc"],
205
+ createdAt: "2020-01-07 00:00:00",
206
+ id: 7,
207
+ lang: "fr",
208
+ joke: "Pourquoi les blagues offline sont-elles fiables ? Parce qu'elles passent par le Service Worker.",
209
+ flags: jokeFlags({ religious: true }),
210
+ safe: true,
211
+ type: "single",
212
+ },
213
+ {
214
+ categories: ["Programming"],
215
+ createdAt: "2020-01-08 00:00:00",
216
+ id: 8,
217
+ lang: "fr",
218
+ joke: "Mon compilateur et mon chien : tous deux ignorent mes commandes jusqu'au build.",
219
+ flags: jokeFlags({ sexist: true }),
220
+ safe: false,
221
+ type: "single",
222
+ },
223
+ {
224
+ categories: ["Misc"],
225
+ createdAt: "2020-01-09 00:00:00",
226
+ id: 9,
227
+ lang: "en",
228
+ joke: "A SQL query walks into a bar, walks up to two tables and asks: Can I join you?",
229
+ flags: jokeFlags({ explicit: true }),
230
+ safe: false,
231
+ type: "single",
232
+ },
233
+ {
234
+ categories: ["Pun"],
235
+ createdAt: "2020-01-10 00:00:00",
236
+ id: 10,
237
+ lang: "fr",
238
+ joke: "J'ai cherché « chien » dans la doc : le mock API a répondu.",
239
+ flags: jokeFlags(),
240
+ safe: true,
241
+ type: "single",
242
+ },
243
+ {
244
+ categories: ["Misc"],
245
+ createdAt: "2020-01-11 00:00:00",
246
+ id: 11,
247
+ lang: "en",
248
+ joke: "There are only two hard things in Computer Science: cache invalidation and naming things.",
249
+ flags: jokeFlags({ political: true, explicit: true }),
250
+ safe: false,
251
+ type: "single",
252
+ },
253
+ {
254
+ categories: ["Animals"],
255
+ createdAt: "2020-01-12 00:00:00",
256
+ id: 12,
257
+ lang: "fr",
258
+ joke: "Le golden retriever a récupéré la donnée du publisher.",
259
+ flags: jokeFlags({ racist: true, sexist: true }),
260
+ safe: false,
261
+ type: "two-liner",
262
+ setup: "Pourquoi le chien aime Concorde ?",
263
+ delivery: "Parce qu'il sait fetch.",
264
+ },
265
+ {
266
+ categories: ["Misc"],
267
+ createdAt: "2020-01-13 00:00:00",
268
+ id: 13,
269
+ lang: "en",
270
+ joke: "My code and my conscience: both have too many warnings.",
271
+ flags: jokeFlags(),
272
+ safe: true,
273
+ type: "single",
274
+ },
275
+ {
276
+ categories: ["Programming"],
277
+ createdAt: "2020-01-14 00:00:00",
278
+ id: 14,
279
+ lang: "en",
280
+ joke: "Git blame is just a mirror.",
281
+ flags: jokeFlags({ religious: true, political: true }),
282
+ safe: false,
283
+ type: "single",
284
+ },
285
+ ];
286
+
287
+ /** `blacklistFlags` from queue filter form (comma-separated or repeated param). */
288
+ export function parseBlacklistFlags(url: URL): DocsJokeFlagKey[] {
289
+ const keys = new Set<DocsJokeFlagKey>();
290
+ const valid: DocsJokeFlagKey[] = [
291
+ "nsfw",
292
+ "religious",
293
+ "political",
294
+ "racist",
295
+ "sexist",
296
+ "explicit",
297
+ ];
298
+ const add = (raw: string) => {
299
+ raw
300
+ .split(",")
301
+ .map((s) => s.trim().toLowerCase())
302
+ .forEach((part) => {
303
+ if (valid.includes(part as DocsJokeFlagKey)) keys.add(part as DocsJokeFlagKey);
304
+ });
305
+ };
306
+ url.searchParams.getAll("blacklistFlags").forEach(add);
307
+ const single = url.searchParams.get("blacklistFlags");
308
+ if (single) add(single);
309
+ return [...keys];
310
+ }
311
+
312
+ /** Exclude jokes that match any blacklisted flag (JokeAPI-style). */
313
+ export function applyJokeBlacklist(
314
+ jokes: DocsJokeItem[],
315
+ blacklist: DocsJokeFlagKey[],
316
+ ): DocsJokeItem[] {
317
+ if (!blacklist.length) return jokes;
318
+ return jokes.filter(
319
+ (j) => !blacklist.some((flag) => j.flags[flag] === true),
320
+ );
321
+ }
322
+
323
+ /** Filtre doc joke queue (`contains`, `lang`, `blacklistFlags` via `dataFilterProvider`). */
324
+ export function filterDocsJokes(
325
+ jokes: DocsJokeItem[],
326
+ url: URL,
327
+ ): DocsJokeItem[] {
328
+ let pool = jokes;
329
+ const contains = url.searchParams.get("contains")?.trim().toLowerCase();
330
+ if (contains) {
331
+ pool = pool.filter((j) => {
332
+ const haystack = [j.joke, j.setup, j.delivery, ...j.categories]
333
+ .filter(Boolean)
334
+ .join(" ")
335
+ .toLowerCase();
336
+ return haystack.includes(contains);
337
+ });
338
+ }
339
+ const lang = url.searchParams.get("lang")?.trim().toLowerCase();
340
+ if (lang === "fr" || lang === "en") {
341
+ pool = pool.filter((j) => j.lang === lang);
342
+ }
343
+ pool = applyJokeBlacklist(pool, parseBlacklistFlags(url));
344
+ return pool;
345
+ }
346
+
347
+ /**
348
+ * Réponse type JokeAPI v2.
349
+ * - `amount` : taille du lot si pas de `offset`
350
+ * - `offset` + `limit` / `per_page` : pagination (sonic-queue lazy)
351
+ * - `contains`, `lang`, `blacklistFlags` : filtres formulaire (queue « Remove following jokes »)
352
+ */
353
+ export function buildJokeApiResponse(url: URL): DocsJokeApiResponse {
354
+ const pool = filterDocsJokes(SAMPLE_JOKES, url);
355
+ const amount = Math.max(
356
+ 1,
357
+ parseInt(url.searchParams.get("amount") || "10", 10),
358
+ );
359
+ const hasOffset = url.searchParams.has("offset");
360
+ const limit = Math.max(
361
+ 1,
362
+ parseInt(
363
+ url.searchParams.get("limit") ||
364
+ url.searchParams.get("per_page") ||
365
+ String(amount),
366
+ 10,
367
+ ),
368
+ );
369
+ const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0", 10));
370
+ const start = hasOffset ? offset : 0;
371
+ const count = hasOffset ? limit : amount;
372
+ const jokes = pool.slice(start, start + count).map((j, i) => ({
373
+ ...j,
374
+ id: start + i + 1,
375
+ }));
376
+ return { error: false, amount: jokes.length, jokes };
377
+ }
@@ -0,0 +1,25 @@
1
+ const SW_URL = "/docs-mock-api-sw.js";
2
+
3
+ /** Enregistre le SW qui sert /docs-mock-api/* (démos doc hors ligne des APIs tierces). */
4
+ export async function registerDocsMockApiServiceWorker(): Promise<
5
+ ServiceWorkerRegistration | undefined
6
+ > {
7
+ if (!("serviceWorker" in navigator)) {
8
+ console.warn("[docs-mock-api] Service Workers not supported");
9
+ return undefined;
10
+ }
11
+
12
+ try {
13
+ const registration = await navigator.serviceWorker.register(SW_URL, {
14
+ scope: "/",
15
+ });
16
+ if (registration.waiting) {
17
+ await registration.update();
18
+ }
19
+ await navigator.serviceWorker.ready;
20
+ return registration;
21
+ } catch (error) {
22
+ console.error("[docs-mock-api] Service Worker registration failed", error);
23
+ return undefined;
24
+ }
25
+ }