@uniweb/kit 0.8.3 → 0.9.1

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.8.3",
3
+ "version": "0.9.1",
4
4
  "description": "Standard component library for Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -38,7 +38,7 @@
38
38
  "fuse.js": "^7.0.0",
39
39
  "shiki": "^3.0.0",
40
40
  "tailwind-merge": "^2.6.0",
41
- "@uniweb/core": "0.6.2"
41
+ "@uniweb/core": "0.7.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": "^18.0.0 || ^19.0.0",
@@ -1,6 +1,8 @@
1
1
  export { useWebsite, default } from './useWebsite.js'
2
2
  export { useFetched } from './useFetched.js'
3
3
  export { useCacheEntry } from './useCacheEntry.js'
4
+ export { useEntityDetail } from './useEntityDetail.js'
5
+ export { useCollectionQueryable } from './useCollectionQueryable.js'
4
6
  export { useRouting } from './useRouting.js'
5
7
  export { useActiveRoute } from './useActiveRoute.js'
6
8
  export { useVersion } from './useVersion.js'
@@ -0,0 +1,74 @@
1
+ /**
2
+ * useCollectionQueryable — read the queryable-surface metadata for a
3
+ * collection.
4
+ *
5
+ * Authors declare the queryable surface of an entity in `site.yml`:
6
+ *
7
+ * collections:
8
+ * members:
9
+ * path: collections/members
10
+ * queryable:
11
+ * department:
12
+ * type: enum
13
+ * label: Department
14
+ * options: [biology, physics, chemistry, geology]
15
+ * tenured:
16
+ * type: boolean
17
+ * label: Tenured
18
+ * start_year:
19
+ * type: range
20
+ * label: Start year
21
+ * min: 1800
22
+ * max: 2025
23
+ *
24
+ * Foundations consume this hook to render filter UIs and compose
25
+ * where-objects from user interactions. The kit doesn't ship UI
26
+ * components for the controls (different foundations have different
27
+ * vocabularies); foundations build their own controls against the
28
+ * metadata returned here.
29
+ *
30
+ * @example
31
+ * function MemberFilters({ onChange }) {
32
+ * const queryable = useCollectionQueryable('members')
33
+ * const [values, setValues] = useState({})
34
+ * if (!queryable) return null
35
+ * return (
36
+ * <div>
37
+ * {Object.entries(queryable).map(([field, def]) => (
38
+ * <FilterControl
39
+ * key={field}
40
+ * field={field}
41
+ * def={def}
42
+ * value={values[field]}
43
+ * onChange={(v) => {
44
+ * const next = { ...values, [field]: v }
45
+ * setValues(next)
46
+ * onChange(composeWhereObject(next))
47
+ * }}
48
+ * />
49
+ * ))}
50
+ * </div>
51
+ * )
52
+ * }
53
+ *
54
+ * Returns null when the collection has no `queryable:` declared.
55
+ *
56
+ * @param {string} collectionName - Name of the collection to read queryable
57
+ * metadata for. Pass null/undefined to skip (returns null).
58
+ * @returns {Object|null} The queryable metadata object, or null.
59
+ */
60
+
61
+ import { getUniweb } from '@uniweb/core'
62
+
63
+ export function useCollectionQueryable(collectionName) {
64
+ if (!collectionName) return null
65
+ const website = getUniweb()?.activeWebsite
66
+ if (!website) return null
67
+ const config = website.config?.collections?.[collectionName]
68
+ if (!config || typeof config !== 'object') return null
69
+ const queryable = config.queryable
70
+ if (!queryable || typeof queryable !== 'object') return null
71
+ return queryable
72
+ }
73
+
74
+ export default useCollectionQueryable
@@ -0,0 +1,90 @@
1
+ /**
2
+ * useEntityDetail — fetch the full record for a deferred-field collection.
3
+ *
4
+ * When a collection declares `deferred: [...]` in `site.yml`, the cascade
5
+ * payload omits the deferred fields. The full record (with deferred
6
+ * fields included) lives somewhere — either at a per-record file the
7
+ * build emits, or at an author-declared API endpoint. This hook fetches
8
+ * that full record on demand.
9
+ *
10
+ * Two source patterns, picked automatically from the collection's
11
+ * declaration:
12
+ *
13
+ * - Markdown-backed collections (declared with `path:` in site.yml).
14
+ * The build emits `/data/<collection>/<slug>.json` per record. The
15
+ * hook fetches that path.
16
+ *
17
+ * - API-backed collections (declared with `url:` in site.yml plus a
18
+ * `detailUrl:` pattern). The hook substitutes `{slug}` in the
19
+ * pattern and fetches that URL.
20
+ *
21
+ * On dynamic-route pages the framework routes the singular detail to
22
+ * the same source automatically (entity-store auto-injection). This
23
+ * hook is for the elsewhere case — a hover-card preview, a modal that
24
+ * opens an article body, a related-items strip that wants summaries
25
+ * everywhere except the one being highlighted.
26
+ *
27
+ * Returns `{ data, error, loading }` like `useFetched`. Shares the same
28
+ * cache. Pass null/undefined to skip without subscribing.
29
+ *
30
+ * @example
31
+ * function ArticleCard({ article }) {
32
+ * const [open, setOpen] = useState(false)
33
+ * const { data: full, loading } = useEntityDetail(open ? article : null, {
34
+ * collection: 'articles',
35
+ * })
36
+ * return (
37
+ * <div>
38
+ * <h3>{article.title}</h3>
39
+ * <p>{article.excerpt}</p>
40
+ * <button onClick={() => setOpen(true)}>Read more</button>
41
+ * {open && (loading ? <Spinner /> : <ArticleBody html={full.body} />)}
42
+ * </div>
43
+ * )
44
+ * }
45
+ */
46
+
47
+ import { getUniweb } from '@uniweb/core'
48
+ import { useFetched } from './useFetched.js'
49
+
50
+ /**
51
+ * @param {Object|null} record - A record from a cascade-delivered collection.
52
+ * Must have a `slug` field. Pass null/undefined to skip the fetch.
53
+ * @param {Object} [options]
54
+ * @param {string} options.collection - The collection name (e.g., 'articles').
55
+ * Required when record is non-null. Used to look up the collection's
56
+ * `detailUrl:` (if declared) or fall back to the static-file default
57
+ * `/data/<collection>/<slug>.json`.
58
+ * @returns {{ data: any, error: string|null, loading: boolean }}
59
+ */
60
+ export function useEntityDetail(record, options = {}) {
61
+ const collection = options?.collection
62
+ const request = buildDetailRequest(record, collection)
63
+ return useFetched(request)
64
+ }
65
+
66
+ function buildDetailRequest(record, collection) {
67
+ const slug = record?.slug
68
+ if (!slug || !collection) return null
69
+
70
+ // Look up the collection's per-record source pattern. Authors with
71
+ // API-backed collections declare `detailUrl:` in site.yml; markdown
72
+ // collections leave it null and use the static-file default.
73
+ const website = getUniweb()?.activeWebsite
74
+ const collConfig = website?.config?.collections?.[collection]
75
+ const detailUrl = (collConfig && typeof collConfig.detailUrl === 'string')
76
+ ? collConfig.detailUrl
77
+ : null
78
+
79
+ if (detailUrl) {
80
+ // Substitute {slug} into the author-declared pattern and use url:
81
+ // (the source is remote).
82
+ const url = detailUrl.replace(/\{slug\}/g, encodeURIComponent(slug))
83
+ return { url, schema: collection }
84
+ }
85
+
86
+ // Static-file default — the build emitted per-record JSON files.
87
+ return { path: `/data/${collection}/${slug}.json`, schema: collection }
88
+ }
89
+
90
+ export default useEntityDetail
package/src/index.js CHANGED
@@ -54,6 +54,8 @@ export {
54
54
  // Layer-3 data hooks — share the DataStore keyspace with Layer 1.
55
55
  useFetched,
56
56
  useCacheEntry,
57
+ useEntityDetail,
58
+ useCollectionQueryable,
57
59
  useRouting,
58
60
  useActiveRoute,
59
61
  useVersion,