@stapel/vocabularies-react 0.1.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 (130) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/MODULE.md +92 -0
  4. package/README.md +173 -0
  5. package/dist/api/extensions.d.ts +8 -0
  6. package/dist/api/extensions.d.ts.map +1 -0
  7. package/dist/api/extensions.js +8 -0
  8. package/dist/api/extensions.js.map +1 -0
  9. package/dist/api/generated/schema.d.ts +234 -0
  10. package/dist/api/generated/schema.d.ts.map +1 -0
  11. package/dist/api/generated/schema.js +2 -0
  12. package/dist/api/generated/schema.js.map +1 -0
  13. package/dist/api/types.d.ts +23 -0
  14. package/dist/api/types.d.ts.map +1 -0
  15. package/dist/api/types.js +2 -0
  16. package/dist/api/types.js.map +1 -0
  17. package/dist/api/vocabulariesApi.d.ts +14 -0
  18. package/dist/api/vocabulariesApi.d.ts.map +1 -0
  19. package/dist/api/vocabulariesApi.js +4 -0
  20. package/dist/api/vocabulariesApi.js.map +1 -0
  21. package/dist/client.d.ts +49 -0
  22. package/dist/client.d.ts.map +1 -0
  23. package/dist/client.js +118 -0
  24. package/dist/client.js.map +1 -0
  25. package/dist/default/VocabularyTermSelect.d.ts +24 -0
  26. package/dist/default/VocabularyTermSelect.d.ts.map +1 -0
  27. package/dist/default/VocabularyTermSelect.js +75 -0
  28. package/dist/default/VocabularyTermSelect.js.map +1 -0
  29. package/dist/default/index.d.ts +16 -0
  30. package/dist/default/index.d.ts.map +1 -0
  31. package/dist/default/index.js +14 -0
  32. package/dist/default/index.js.map +1 -0
  33. package/dist/default/types.d.ts +12 -0
  34. package/dist/default/types.d.ts.map +1 -0
  35. package/dist/default/types.js +2 -0
  36. package/dist/default/types.js.map +1 -0
  37. package/dist/flows/errors.d.ts +12 -0
  38. package/dist/flows/errors.d.ts.map +1 -0
  39. package/dist/flows/errors.js +13 -0
  40. package/dist/flows/errors.js.map +1 -0
  41. package/dist/flows/registry.d.ts +22 -0
  42. package/dist/flows/registry.d.ts.map +1 -0
  43. package/dist/flows/registry.js +20 -0
  44. package/dist/flows/registry.js.map +1 -0
  45. package/dist/headless/VocabulariesProvider.d.ts +19 -0
  46. package/dist/headless/VocabulariesProvider.d.ts.map +1 -0
  47. package/dist/headless/VocabulariesProvider.js +15 -0
  48. package/dist/headless/VocabulariesProvider.js.map +1 -0
  49. package/dist/i18n/errorsMap.d.ts +10 -0
  50. package/dist/i18n/errorsMap.d.ts.map +1 -0
  51. package/dist/i18n/errorsMap.js +20 -0
  52. package/dist/i18n/errorsMap.js.map +1 -0
  53. package/dist/i18n/es.d.ts +21 -0
  54. package/dist/i18n/es.d.ts.map +1 -0
  55. package/dist/i18n/es.js +38 -0
  56. package/dist/i18n/es.js.map +1 -0
  57. package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
  58. package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
  59. package/dist/i18n/generated/errors.es.gen.js +58 -0
  60. package/dist/i18n/generated/errors.es.gen.js.map +1 -0
  61. package/dist/i18n/generated/errors.gen.d.ts +305 -0
  62. package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
  63. package/dist/i18n/generated/errors.gen.js +156 -0
  64. package/dist/i18n/generated/errors.gen.js.map +1 -0
  65. package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
  66. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
  67. package/dist/i18n/generated/errors.ru.gen.js +58 -0
  68. package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
  69. package/dist/i18n/keys.d.ts +33 -0
  70. package/dist/i18n/keys.d.ts.map +1 -0
  71. package/dist/i18n/keys.js +44 -0
  72. package/dist/i18n/keys.js.map +1 -0
  73. package/dist/i18n/ru.d.ts +26 -0
  74. package/dist/i18n/ru.d.ts.map +1 -0
  75. package/dist/i18n/ru.js +43 -0
  76. package/dist/i18n/ru.js.map +1 -0
  77. package/dist/index.d.ts +34 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +38 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/model/context.d.ts +11 -0
  82. package/dist/model/context.d.ts.map +1 -0
  83. package/dist/model/context.js +16 -0
  84. package/dist/model/context.js.map +1 -0
  85. package/dist/model/queryKeys.d.ts +5 -0
  86. package/dist/model/queryKeys.d.ts.map +1 -0
  87. package/dist/model/queryKeys.js +28 -0
  88. package/dist/model/queryKeys.js.map +1 -0
  89. package/dist/model/runtime.d.ts +17 -0
  90. package/dist/model/runtime.d.ts.map +1 -0
  91. package/dist/model/runtime.js +6 -0
  92. package/dist/model/runtime.js.map +1 -0
  93. package/dist/model/useTermLabels.d.ts +26 -0
  94. package/dist/model/useTermLabels.d.ts.map +1 -0
  95. package/dist/model/useTermLabels.js +58 -0
  96. package/dist/model/useTermLabels.js.map +1 -0
  97. package/dist/model/useTermSearch.d.ts +24 -0
  98. package/dist/model/useTermSearch.d.ts.map +1 -0
  99. package/dist/model/useTermSearch.js +85 -0
  100. package/dist/model/useTermSearch.js.map +1 -0
  101. package/llms.txt +79 -0
  102. package/manifest.json +466 -0
  103. package/package.json +108 -0
  104. package/src/analytics/generated/events.json +7 -0
  105. package/src/api/extensions.ts +7 -0
  106. package/src/api/generated/schema.ts +236 -0
  107. package/src/api/types.ts +27 -0
  108. package/src/api/vocabulariesApi.ts +17 -0
  109. package/src/client.ts +176 -0
  110. package/src/default/VocabularyTermSelect.tsx +147 -0
  111. package/src/default/index.ts +15 -0
  112. package/src/default/types.ts +11 -0
  113. package/src/flows/errors.ts +16 -0
  114. package/src/flows/registry.ts +32 -0
  115. package/src/headless/VocabulariesProvider.tsx +20 -0
  116. package/src/i18n/errorsMap.ts +31 -0
  117. package/src/i18n/es.ts +49 -0
  118. package/src/i18n/generated/errors.es.gen.ts +64 -0
  119. package/src/i18n/generated/errors.gen.ts +187 -0
  120. package/src/i18n/generated/errors.json +351 -0
  121. package/src/i18n/generated/errors.ru.gen.ts +64 -0
  122. package/src/i18n/keys.ts +53 -0
  123. package/src/i18n/ru.ts +54 -0
  124. package/src/index.ts +92 -0
  125. package/src/model/context.tsx +28 -0
  126. package/src/model/queryKeys.ts +35 -0
  127. package/src/model/runtime.ts +25 -0
  128. package/src/model/useTermLabels.ts +72 -0
  129. package/src/model/useTermSearch.ts +118 -0
  130. package/tsconfig.json +26 -0
@@ -0,0 +1,25 @@
1
+ import { createModuleRuntime } from "@stapel/core";
2
+ import type { CreateModuleRuntimeOptions, ModuleRuntime } from "@stapel/core";
3
+ import { createVocabulariesApi } from "../api/vocabulariesApi.js";
4
+ import type { VocabulariesApi } from "../api/vocabulariesApi.js";
5
+
6
+ /**
7
+ * The wired vocabularies runtime — core's `ModuleRuntime` bound to this pair's
8
+ * API (slim wave §21/S2: the plumbing lives once in `@stapel/core`'s
9
+ * `createModuleRuntime`/`createModuleContext`; this module only binds the
10
+ * module-prefixed names). The returned `client` is what the host injects
11
+ * into core's `StapelConfigProvider` (as the default or the `"vocabularies"`
12
+ * module client), preserving the client-injection fork seam
13
+ * (frontend-standard §7.2). Auth token/refresh and the verification-403 seam
14
+ * are supplied by the host's auth runtime on the shared client — this pair
15
+ * does not re-implement them.
16
+ */
17
+ export type VocabulariesRuntime = ModuleRuntime<VocabulariesApi>;
18
+
19
+ export type CreateVocabulariesRuntimeOptions = CreateModuleRuntimeOptions;
20
+
21
+ export function createVocabulariesRuntime(
22
+ options: CreateVocabulariesRuntimeOptions
23
+ ): VocabulariesRuntime {
24
+ return createModuleRuntime(createVocabulariesApi, options);
25
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `useTermLabels` — `{code: label}` for codes a page already HOLDS.
3
+ *
4
+ * The other half of the seam. A stored answer on a listing is a list of term
5
+ * CODES (`["apple", "iphone-15-pro"]`); the labels are not stored with it,
6
+ * because a label is translated and a code is not. So a reopened draft, a
7
+ * filter chip, a facet row — anything showing an answer somebody already gave
8
+ * — has to ask for the words.
9
+ *
10
+ * Unlike a search this IS a cacheable resource: the same codes give the same
11
+ * labels until the vocabulary's revision moves, and several controls on one
12
+ * page routinely ask for the same set. So it is a TanStack query keyed through
13
+ * the factory, and the answer is a {@link LoadState} rather than a bare map —
14
+ * `{}` would say "these codes have no labels" in exactly the same words as
15
+ * "nobody has asked yet" and "the request failed" (core `loadState.ts`).
16
+ *
17
+ * The fallback a caller wants is almost always the CODE ITSELF, which is what
18
+ * {@link termLabel} does: the stored answer is the truth, and a blank control
19
+ * is a worse lie than a slug.
20
+ */
21
+ import { useQuery } from "@tanstack/react-query";
22
+ import { loadStateFromQuery } from "@stapel/core";
23
+ import type { LoadState } from "@stapel/core";
24
+ import type { VocabularyClient } from "../client.js";
25
+ import { vocabulariesQueryKeys } from "./queryKeys.js";
26
+
27
+ export interface TermLabelsOptions {
28
+ readonly vocabulary: string;
29
+ readonly level: string;
30
+ readonly codes: readonly string[];
31
+ }
32
+
33
+ export type TermLabels = Readonly<Record<string, string>>;
34
+
35
+ /**
36
+ * Labels for `codes`, as a load state. Disabled — and therefore `loading`,
37
+ * never `ready` with an empty map — while there is no client, no pointer or
38
+ * nothing to resolve.
39
+ */
40
+ export function useTermLabels(
41
+ client: VocabularyClient | null,
42
+ options: TermLabelsOptions
43
+ ): LoadState<TermLabels> {
44
+ const { vocabulary, level, codes } = options;
45
+ const wanted = codes.filter((code) => code.length > 0);
46
+ const enabled =
47
+ client !== null && vocabulary.length > 0 && level.length > 0 && wanted.length > 0;
48
+ const query = useQuery({
49
+ queryKey: vocabulariesQueryKeys.termLabels(vocabulary, level, wanted),
50
+ enabled,
51
+ queryFn: async (): Promise<TermLabels> => {
52
+ // `enabled` already guarantees this, and the compiler does not know it.
53
+ if (client === null) return {};
54
+ return await client.resolve(vocabulary, level, wanted);
55
+ },
56
+ });
57
+ return loadStateFromQuery(query);
58
+ }
59
+
60
+ /**
61
+ * The one right way to READ that state for display: a resolved label, or the
62
+ * code itself while the answer is in flight, when the load failed, or when the
63
+ * server omitted the code because it does not know it.
64
+ *
65
+ * All four fall back the same way ON PURPOSE — the code is what the person's
66
+ * answer literally is, so showing it is never wrong, only less kind. A control
67
+ * that needs to distinguish them (an admin screen auditing dead codes) reads
68
+ * the {@link LoadState} directly.
69
+ */
70
+ export function termLabel(labels: LoadState<TermLabels>, code: string): string {
71
+ return labels.status === "ready" ? (labels.data[code] ?? code) : code;
72
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * `useTermSearch` — a typeahead over ONE vocabulary level.
3
+ *
4
+ * The level is the wrong size to hold (529 phone vendors is fine, 14 962
5
+ * models is not), so the options ARE the answer to the current query and the
6
+ * hook's whole job is to make sure the answer on screen belongs to the query
7
+ * on screen:
8
+ *
9
+ * - a keystroke DEBOUNCES (250 ms by default) — one request per pause, not
10
+ * per character;
11
+ * - a new request ABORTS the one before it, and a resolved-but-superseded
12
+ * answer is dropped, because the network does not promise order;
13
+ * - opening is not typing: the first page is fetched immediately (a spinner
14
+ * that starts a quarter second after the click reads as a dead control),
15
+ * ONCE per (vocabulary, level, parent) — antd reports the dropdown as
16
+ * opening on every keystroke, so a naive `open()` is a request storm nobody
17
+ * can see in a screenshot;
18
+ * - a parent CHANGE empties the list: what is listed are the previous
19
+ * parent's children and must not stay pickable.
20
+ *
21
+ * The client is a PARAMETER, not a context read: this hook is one half of the
22
+ * seam `@stapel/attributes-react` declares, and a host wires the client at its
23
+ * composition root. Passing `null` is a supported state (the control draws its
24
+ * unavailable notice) rather than a crash.
25
+ */
26
+ import { useCallback, useEffect, useRef, useState } from "react";
27
+ import type { VocabularyClient, VocabularyTerm } from "../client.js";
28
+
29
+ /** The pause after the last keystroke before a query goes to the server. Long
30
+ * enough that typing a word is one request, short enough that it still feels
31
+ * like the control answered. Mirrors the ref editors in attributes-react. */
32
+ export const TERM_SEARCH_DEBOUNCE_MS = 250;
33
+
34
+ export interface TermSearchOptions {
35
+ readonly vocabulary: string;
36
+ readonly level: string;
37
+ /** Code of a term at the level above; narrows the page to its children.
38
+ * `undefined` means the whole level. */
39
+ readonly parent?: string | undefined;
40
+ readonly debounceMs?: number;
41
+ }
42
+
43
+ export interface TermSearchState {
44
+ readonly terms: readonly VocabularyTerm[];
45
+ readonly loading: boolean;
46
+ /** A person typed. Debounced and superseding. */
47
+ search: (query: string) => void;
48
+ /** A dropdown opened. Fetches the first page immediately, once per
49
+ * (vocabulary, level, parent). */
50
+ open: () => void;
51
+ }
52
+
53
+ export function useTermSearch(
54
+ client: VocabularyClient | null,
55
+ options: TermSearchOptions
56
+ ): TermSearchState {
57
+ const { vocabulary, level, parent, debounceMs = TERM_SEARCH_DEBOUNCE_MS } = options;
58
+ const [terms, setTerms] = useState<readonly VocabularyTerm[]>([]);
59
+ const [loading, setLoading] = useState(false);
60
+ const inFlight = useRef<AbortController | undefined>(undefined);
61
+ const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
62
+ const asked = useRef(false);
63
+
64
+ const run = useCallback(
65
+ (query: string): void => {
66
+ if (client === null || vocabulary.length === 0 || level.length === 0) return;
67
+ inFlight.current?.abort();
68
+ const controller = new AbortController();
69
+ inFlight.current = controller;
70
+ setLoading(true);
71
+ client
72
+ .search(vocabulary, level, query, parent, controller.signal)
73
+ .then((found) => {
74
+ if (controller.signal.aborted) return;
75
+ setTerms(found);
76
+ setLoading(false);
77
+ })
78
+ .catch(() => {
79
+ // A superseded request is not a failure and must not clear the list
80
+ // the CURRENT request is about to fill.
81
+ if (controller.signal.aborted) return;
82
+ setTerms([]);
83
+ setLoading(false);
84
+ });
85
+ },
86
+ [client, vocabulary, level, parent]
87
+ );
88
+
89
+ useEffect(() => {
90
+ asked.current = false;
91
+ setTerms([]);
92
+ }, [client, vocabulary, level, parent]);
93
+
94
+ useEffect(
95
+ () => () => {
96
+ if (timer.current !== undefined) clearTimeout(timer.current);
97
+ inFlight.current?.abort();
98
+ },
99
+ []
100
+ );
101
+
102
+ const search = useCallback(
103
+ (query: string): void => {
104
+ asked.current = true;
105
+ if (timer.current !== undefined) clearTimeout(timer.current);
106
+ timer.current = setTimeout(() => run(query), debounceMs);
107
+ },
108
+ [run, debounceMs]
109
+ );
110
+
111
+ const open = useCallback((): void => {
112
+ if (asked.current) return;
113
+ asked.current = true;
114
+ run("");
115
+ }, [run]);
116
+
117
+ return { terms, loading, search, open };
118
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "_comment": "Self-contained on purpose: standalone-buildable per frontend-standard §7. Mirrors the root tsconfig.base.json settings.",
4
+ "compilerOptions": {
5
+ "target": "ES2022",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "moduleResolution": "bundler",
9
+ "jsx": "react-jsx",
10
+ "strict": true,
11
+ "noUncheckedIndexedAccess": true,
12
+ "noImplicitOverride": true,
13
+ "exactOptionalPropertyTypes": true,
14
+ "isolatedModules": true,
15
+ "isolatedDeclarations": true,
16
+ "verbatimModuleSyntax": true,
17
+ "declaration": true,
18
+ "declarationMap": true,
19
+ "sourceMap": true,
20
+ "skipLibCheck": true,
21
+ "forceConsistentCasingInFileNames": true,
22
+ "outDir": "dist",
23
+ "rootDir": "src"
24
+ },
25
+ "include": ["src"]
26
+ }