@uniweb/kit 0.11.3 → 0.12.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/kit",
3
- "version": "0.11.3",
3
+ "version": "0.12.0",
4
4
  "description": "Standard component library for Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,9 +44,9 @@
44
44
  "shiki": "^3.0.0",
45
45
  "tailwind-merge": "^3.6.0",
46
46
  "temml": "^0.13.2",
47
- "@uniweb/core": "^0.8.5",
48
47
  "@uniweb/semantic-parser": "^1.2.2",
49
- "@uniweb/scene": "^0.1.3"
48
+ "@uniweb/scene": "^0.1.3",
49
+ "@uniweb/core": "^0.8.5"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "react": "^19.0.0",
@@ -9,19 +9,27 @@ import { useWebsite } from './useWebsite.js'
9
9
  *
10
10
  * The hook resolves *where* to submit from the site's configuration (`submit:`
11
11
  * in site.yml) or from its host, so a component never names an endpoint. When
12
- * neither supplies one, `canSubmit` is false and `unavailableReason` says why
13
- * render the control disabled rather than letting someone fill in a form whose
12
+ * neither supplies one, `canSubmit` is false don't render a form whose
14
13
  * contents have nowhere to go. See `resolveSubmitTarget` for the precedence.
15
14
  *
16
15
  * ```jsx
17
- * const { submit, status, error, canSubmit, unavailableReason } =
16
+ * const { submit, status, error, canSubmit } =
18
17
  * useFormSubmit({ block, context: { formId: 'contact' } })
19
18
  *
20
- * <button type="submit" disabled={!canSubmit || status === 'submitting'}>
21
- * {canSubmit ? 'Send' : unavailableReason}
22
- * </button>
19
+ * if (!canSubmit) return null // or fall back to contact details the
20
+ * // site already carries in its content
21
+ *
22
+ * <button type="submit" disabled={status === 'submitting'}>Send</button>
23
23
  * ```
24
24
  *
25
+ * ⛔ **There is no `unavailableReason`, and there was one — it was a mistake.**
26
+ * It carried a canned English sentence and this very example rendered it as
27
+ * button copy. A visitor has no stake in which services the operator bought,
28
+ * the string reads like a breakage when nothing is broken, and it cannot be
29
+ * translated on a framework whose sites are usually multilingual and often not
30
+ * English. Text a visitor reads is *site content*. `canSubmit` is the whole
31
+ * signal.
32
+ *
25
33
  * Pass `block` and the submission carries where it came from — section type,
26
34
  * section id, page id and label — without every component assembling that by
27
35
  * hand. Anything in `context` wins over what the block supplies.
@@ -39,7 +47,6 @@ import { useWebsite } from './useWebsite.js'
39
47
  * error: Error | null,
40
48
  * response: object | null,
41
49
  * canSubmit: boolean,
42
- * unavailableReason: string | null,
43
50
  * submit: (formData: object, overrides?: object) => Promise<object>,
44
51
  * reset: () => void,
45
52
  * }}
@@ -50,7 +57,7 @@ export function useFormSubmit(defaults = {}) {
50
57
  const [response, setResponse] = useState(null)
51
58
 
52
59
  const { website } = useWebsite()
53
- const { url: target, reason: unavailableReason } = resolveSubmitTarget(website)
60
+ const { url: target } = resolveSubmitTarget(website)
54
61
 
55
62
  // `defaults` is a fresh object every render, so listing it as a dependency
56
63
  // would rebuild the callback on each one. A ref is what actually makes "the
@@ -103,7 +110,6 @@ export function useFormSubmit(defaults = {}) {
103
110
  error,
104
111
  response,
105
112
  canSubmit: !!target,
106
- unavailableReason,
107
113
  // Whether attachments can be delivered. True once there is a target: the
108
114
  // client sends the manifest, then the bytes, then finalizes.
109
115
  //
@@ -51,7 +51,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
51
51
  * It lists the paths of `required` controls that are still empty. It does not
52
52
  * block anything: whether an incomplete form disables the button, shows a
53
53
  * message, or submits anyway is a design decision. `useFormSubmit` draws the
54
- * same line with `canSubmit` / `unavailableReason` — the kit works out the
54
+ * same line with `canSubmit` — the kit works out the
55
55
  * fact, the foundation decides what it looks like.
56
56
  *
57
57
  * Empty means `undefined`, `null`, `''`, or `[]`. A `false` boolean is a VALUE,
package/src/index.js CHANGED
@@ -130,11 +130,9 @@ export {
130
130
  submitForm,
131
131
  deriveSummary,
132
132
  resolveSubmitTarget,
133
- NO_SUBMIT_TARGET_REASON,
134
133
  // Site services (search / submit / assistant / anything a host offers)
135
134
  resolveService,
136
- resolveServiceUrl,
137
- NO_SERVICE_REASON
135
+ resolveServiceUrl
138
136
  } from './utils/index.js'
139
137
 
140
138
  // ============================================================================
@@ -248,8 +248,11 @@ export function detectMediaType(url) {
248
248
  // ─────────────────────────────────────────────────────────────────
249
249
 
250
250
  export { submitForm, deriveSummary } from './submitForm.js'
251
- export { resolveSubmitTarget, NO_SUBMIT_TARGET_REASON } from './submitTarget.js'
252
- export { resolveService, resolveServiceUrl, NO_SERVICE_REASON } from './services.js'
251
+ export { resolveSubmitTarget } from './submitTarget.js'
252
+ export {
253
+ resolveService,
254
+ resolveServiceUrl
255
+ } from './services.js'
253
256
 
254
257
  /**
255
258
  * The text of a ProseMirror node, flattened.
@@ -17,8 +17,8 @@ import { applyBasePath } from './href.js'
17
17
  * 2. **The host**, served — `config.services.<name>` in the payload. What the
18
18
  * deployment offers, which the site never had to know about.
19
19
  *
20
- * Absent from both means the site has no such service, and the component says so
21
- * rather than guessing an address. That is the same rule for every service, and
20
+ * Absent from both means the site has no such service, and the component renders
21
+ * for that rather than guessing an address. That is the same rule for every service, and
22
22
  * it is why this module exists: it was previously implemented three times — the
23
23
  * search provider, the submit resolver, and a hand-rolled copy inside a
24
24
  * foundation — with three slightly different base-joining rules between them.
@@ -37,11 +37,33 @@ import { applyBasePath } from './href.js'
37
37
  *
38
38
  * ## What this deliberately does not model
39
39
  *
40
- * **Entitlement.** A host that will not serve a service either omits it or
41
- * supplies a `reason`, which is relayed to the UI verbatim. The framework never
42
- * learns why — no plan names, no tiers, no "paid" anywhere. That is not
43
- * squeamishness: `@uniweb/kit` is public, and a framework that encodes which
44
- * capabilities cost money ships the business model into open source.
40
+ * **Entitlement.** A host that will not serve a service omits it, or declares
41
+ * the name with no address. The framework never learns why no plan names, no
42
+ * tiers, no "paid" anywhere. That is not squeamishness: `@uniweb/kit` is
43
+ * public, and a framework that encodes which capabilities cost money ships the
44
+ * business model into open source.
45
+ *
46
+ * ⛔ **There is deliberately no explanatory string, and there was one — it was
47
+ * a mistake.** Until 2026-08-13 a declining host could supply a `reason` that
48
+ * this module relayed "to the UI verbatim", with an English default
49
+ * (`NO_SERVICE_REASON`) when nothing did. Removed, on two counts:
50
+ *
51
+ * 1. **Wrong audience.** A visitor has no stake in which services an operator
52
+ * provisioned. "Submissions are not enabled for this site" reports someone's
53
+ * billing state to the public and reads like a breakage. It is neither — it
54
+ * is a service that was not bought, and **a generic component is supposed to
55
+ * be smart about that.**
56
+ * 2. **Wrong language, unfixably.** Sites here are multilingual, or unilingual
57
+ * and not English. A host-supplied sentence bypasses the site's entire
58
+ * localization pipeline, and a canned constant in a public package cannot
59
+ * be translated at all. Any text a visitor should read is *site content*,
60
+ * which is authored and localized — never a string a service layer invents.
61
+ *
62
+ * ⇒ **`url` is the whole answer, and absence is a rendering decision rather
63
+ * than a message.** No submit endpoint → render no form, or degrade to
64
+ * something that still serves the visitor: a `mailto:` or a number the site
65
+ * already carries in its content. No assistant → render no Ask-AI affordance.
66
+ * Nobody is told why, because nobody visiting needs to know.
45
67
  *
46
68
  * **The site's own base.** `config.base` is where the site *lives*, not a
47
69
  * service it consumes — it is load-bearing for routing and asset URLs too. It
@@ -51,9 +73,6 @@ import { applyBasePath } from './href.js'
51
73
  /** Anything with a scheme, or protocol-relative — never joined to a base. */
52
74
  const ABSOLUTE_URL_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i
53
75
 
54
- /** Shown when nothing supplies a service and the caller gives no wording. */
55
- export const NO_SERVICE_REASON = 'This site has no such service configured.'
56
-
57
76
  /**
58
77
  * Read an endpoint out of either declaration form.
59
78
  *
@@ -108,19 +127,19 @@ export function resolveServiceUrl(endpoint, basePath = '') {
108
127
  * Resolve where a named service lives for this site.
109
128
  *
110
129
  * ```js
111
- * const { url, reason } = resolveService(website, 'submit')
112
- * if (!url) return renderDisabled(reason)
130
+ * const { url } = resolveService(website, 'submit')
131
+ * if (!url) return null // no endpoint — render no form, or degrade
113
132
  * ```
114
133
  *
115
134
  * @param {object} website - the active Website
116
135
  * @param {string} name - service name, e.g. 'submit' · 'search' · 'assistant'
117
- * @param {object} [options]
118
- * @param {string} [options.reason] - wording when nothing supplies it
119
- * @returns {{ url: string|null, reason: string|null, source: 'site'|'host'|null }}
120
- * `source` says which declaration answered useful in diagnostics, and the
121
- * thing to check when a host's value appears not to be taking effect.
136
+ * @returns {{ url: string|null, source: 'site'|'host'|null }}
137
+ * `url` is the whole answer for rendering. `source` says which declaration
138
+ * answered a diagnostic, and the thing to check when a host's value appears
139
+ * not to be taking effect. `'host'` with a null `url` means the host answered
140
+ * and offered no address; `null` means nothing declared the service at all.
122
141
  */
123
- export function resolveService(website, name, options = {}) {
142
+ export function resolveService(website, name) {
124
143
  const config = website?.config
125
144
  const basePath = website?.basePath
126
145
 
@@ -128,21 +147,22 @@ export function resolveService(website, name, options = {}) {
128
147
  // means it, including on a host that offers one.
129
148
  const authored = readEndpoint(config?.[name])
130
149
  if (authored) {
131
- return { url: resolveServiceUrl(authored, basePath), reason: null, source: 'site' }
150
+ return { url: resolveServiceUrl(authored, basePath), source: 'site' }
132
151
  }
133
152
 
134
153
  // 2 — what the host says it offers.
135
154
  const hostDeclaration = config?.services?.[name]
136
155
  const hostEndpoint = readEndpoint(hostDeclaration)
137
156
  if (hostEndpoint) {
138
- return { url: resolveServiceUrl(hostEndpoint, basePath), reason: null, source: 'host' }
157
+ return { url: resolveServiceUrl(hostEndpoint, basePath), source: 'host' }
139
158
  }
140
159
 
141
- // A host that declines may say why, and that reaches the visitor unaltered.
142
- const hostReason =
143
- typeof hostDeclaration?.reason === 'string' ? hostDeclaration.reason.trim() : ''
144
- if (hostReason) return { url: null, reason: hostReason, source: 'host' }
160
+ // A host may declare the name while offering no address a decline. It is
161
+ // still the host answering, which is all a caller can use: any *wording* for
162
+ // that state would be ours to invent, in one language, for a visitor who has
163
+ // no stake in it. See the entitlement note above.
164
+ if (hostDeclaration !== undefined) return { url: null, source: 'host' }
145
165
 
146
166
  // 3 — nobody supplied one.
147
- return { url: null, reason: options.reason || NO_SERVICE_REASON, source: null }
167
+ return { url: null, source: null }
148
168
  }
@@ -31,24 +31,16 @@ import { resolveService, resolveServiceUrl } from './services.js'
31
31
  * only have come from its host.
32
32
  */
33
33
 
34
- /**
35
- * Shown when a site declares no endpoint and no host offers one. English, and a
36
- * foundation is free to ignore it and render its own copy — check `url` for the
37
- * yes/no and treat this as a default rather than a string to translate.
38
- */
39
- export const NO_SUBMIT_TARGET_REASON =
40
- 'This site has no form submission endpoint configured.'
41
-
42
34
  /**
43
35
  * Resolve the submission target for a site.
44
36
  *
45
37
  * @param {object} website - The active Website instance
46
- * @returns {{ url: string|null, reason: string|null, source: 'site'|'host'|null }}
47
- * `url` is the resolved endpoint, or null when nothing supplies one. `reason`
48
- * is set exactly when `url` is null.
38
+ * @returns {{ url: string|null, source: 'site'|'host'|null }}
39
+ * `url` is the resolved endpoint, or null when nothing supplies one — in which
40
+ * case render no form, or degrade to contact details the site already carries.
49
41
  */
50
42
  export function resolveSubmitTarget(website) {
51
- return resolveService(website, 'submit', { reason: NO_SUBMIT_TARGET_REASON })
43
+ return resolveService(website, 'submit')
52
44
  }
53
45
 
54
46
  /**