@lad-sapienza/scms-core 0.1.0-alpha.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 (64) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +73 -0
  3. package/components/BSNavbar.tsx +264 -0
  4. package/components/DataTb/DataTb.astro +33 -0
  5. package/components/DataTb/DataTb.tsx +340 -0
  6. package/components/DataTb/DataTbMdx.tsx +75 -0
  7. package/components/DataTb/README.md +275 -0
  8. package/components/DataTb/index.ts +36 -0
  9. package/components/DataTb/sources/ApiSource.tsx +89 -0
  10. package/components/DataTb/sources/CsvSource.tsx +69 -0
  11. package/components/DataTb/sources/DirectusSource.tsx +98 -0
  12. package/components/DataTb/sources/JsonSource.tsx +65 -0
  13. package/components/DataTb/sources/index.ts +17 -0
  14. package/components/DataTb/types.ts +215 -0
  15. package/components/DataTb/utils.ts +186 -0
  16. package/components/Gallery/Gallery.astro +65 -0
  17. package/components/Gallery/Gallery.tsx +136 -0
  18. package/components/Gallery/README.md +118 -0
  19. package/components/Gallery/__tests__/galleryUtils.test.ts +178 -0
  20. package/components/Gallery/galleryUtils.ts +186 -0
  21. package/components/Gallery/index.ts +7 -0
  22. package/components/Gallery/react-photoswipe-gallery.d.ts +26 -0
  23. package/components/Gallery/types.ts +46 -0
  24. package/components/Gallery/virtual-gallery.d.ts +15 -0
  25. package/components/Map/LayerControl.tsx +368 -0
  26. package/components/Map/Map.tsx +735 -0
  27. package/components/Map/MapMdx.tsx +37 -0
  28. package/components/Map/RasterLayerLibre.tsx +33 -0
  29. package/components/Map/Search/SearchOperators.ts +31 -0
  30. package/components/Map/Search/SearchUI.tsx +75 -0
  31. package/components/Map/Search/SearchUIAdvanced.tsx +289 -0
  32. package/components/Map/Search/SearchUISimple.tsx +115 -0
  33. package/components/Map/Search/index.ts +7 -0
  34. package/components/Map/defaultBasemaps.ts +88 -0
  35. package/components/Map/index.ts +6 -0
  36. package/components/Map/types.ts +179 -0
  37. package/components/Map/utils.ts +367 -0
  38. package/components/README.md +62 -0
  39. package/components/Record/Field.tsx +35 -0
  40. package/components/Record/Image.tsx +114 -0
  41. package/components/Record/RecordFetcher.tsx +127 -0
  42. package/components/Record/context.tsx +14 -0
  43. package/components/Record/index.ts +11 -0
  44. package/components/Record/useRecordFetcher.ts +104 -0
  45. package/components/Record/utils.ts +13 -0
  46. package/components/SEO/SEO.astro +132 -0
  47. package/components/TableOfContents/TableOfContents.astro +86 -0
  48. package/components/ZoteroGeoViewer/README.md +195 -0
  49. package/components/ZoteroGeoViewer/ZoteroGeoViewer.astro +26 -0
  50. package/components/ZoteroGeoViewer/ZoteroGeoViewer.tsx +483 -0
  51. package/components/ZoteroGeoViewer/ZoteroRecordsPreview.tsx +185 -0
  52. package/components/ZoteroGeoViewer/index.ts +16 -0
  53. package/components/ZoteroGeoViewer/types.ts +123 -0
  54. package/index.ts +26 -0
  55. package/integrations/__tests__/galleryIntegration.test.ts +70 -0
  56. package/integrations/contentAssetsIntegration.ts +132 -0
  57. package/integrations/directusLoader.ts +98 -0
  58. package/integrations/galleryIntegration.ts +78 -0
  59. package/package.json +56 -0
  60. package/scms.ts +95 -0
  61. package/utils/content.ts +88 -0
  62. package/utils/data-fetcher.ts +128 -0
  63. package/utils/directus-config.ts +160 -0
  64. package/utils/record-fetcher.ts +132 -0
package/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ BSD Zero Clause License
2
+
3
+ Copyright (c) 2026 LAD: Laboratorio di Archeologia Digitale alla Sapienza
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
9
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
10
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
11
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
12
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
13
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
14
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @lad-sapienza/scms-core
2
+
3
+ Framework components and Astro integrations for [s:CMS](https://github.com/lad-sapienza/sCMS) — the static-site CMS built by [LAD @Sapienza](https://lad.saras.uniroma1.it/).
4
+
5
+ This package supplies the `core/` framework layer that used to live inside the sCMS template repo itself. A site built with sCMS depends on this package from `node_modules` instead of vendoring/syncing it via git, so updates are ordinary `npm update`s.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @lad-sapienza/scms-core
11
+ ```
12
+
13
+ Peer dependencies (install alongside): `astro`, `react`, `react-dom`, `maplibre-gl`, `@types/react`, `@types/react-dom`.
14
+
15
+ ## Usage
16
+
17
+ Register the integration in `astro.config.mjs`:
18
+
19
+ ```js
20
+ import { defineConfig } from 'astro/config';
21
+ import { scms } from '@lad-sapienza/scms-core';
22
+
23
+ export default defineConfig({
24
+ integrations: [
25
+ ...scms({
26
+ // all optional, default to the paths below
27
+ contentDir: 'usr/content',
28
+ pagesDir: 'usr/pages',
29
+ galleriesDir: 'usr/galleries',
30
+ }),
31
+ ],
32
+ });
33
+ ```
34
+
35
+ `scms()` bundles content-asset serving, the Gallery virtual module, Expressive Code, MDX, React, and sitemap integrations into one call, and self-registers the Vite `dedupe`/`optimizeDeps` config React and `@tanstack/react-table` need to avoid duplicate-module "Invalid hook call" errors — no extra Vite config required in the consumer.
36
+
37
+ Import components from the package root in any `.mdx` file:
38
+
39
+ ```mdx
40
+ import { DataTb, Map, Gallery } from '@lad-sapienza/scms-core';
41
+ ```
42
+
43
+ | Component | Description |
44
+ |---|---|
45
+ | `DataTb` | Sortable, filterable, paginated data table |
46
+ | `MapComponent` (`Map`) | Interactive map with MapLibre GL JS |
47
+ | `Gallery` | Responsive image gallery with lightbox |
48
+ | `TableOfContents` | Auto-generated TOC from headings |
49
+ | `ZoteroGeoViewer` | Zotero library visualised on a map |
50
+ | `RecordProvider`, `Field`, `Image`, `RecordFetcher`, `useRecordFetcher` | Build single-record detail pages against Directus |
51
+ | `SearchUI`, `SearchUISimple`, `SearchUIAdvanced` | Field/operator/value search UI (used by `Map`'s `searchInFields`) |
52
+
53
+ A few components are reachable only via subpath import, not the root barrel — this mirrors the internal layout, no bundler config or `exports` map restricts it:
54
+
55
+ ```js
56
+ import SEO from '@lad-sapienza/scms-core/components/SEO/SEO.astro';
57
+ import BSNavbar from '@lad-sapienza/scms-core/components/BSNavbar';
58
+ import { directusLoader } from '@lad-sapienza/scms-core/integrations/directusLoader';
59
+ ```
60
+
61
+ See each component folder's own `README.md` (`components/DataTb`, `components/Gallery`, `components/ZoteroGeoViewer`, `components/`) for detailed API docs and examples.
62
+
63
+ ## Development
64
+
65
+ ```bash
66
+ npm install
67
+ npm run test # vitest
68
+ npm run typecheck # tsc --noEmit
69
+ ```
70
+
71
+ ## License
72
+
73
+ BSD-0-Clause
@@ -0,0 +1,264 @@
1
+ import React, { useState, useRef, useEffect } from "react";
2
+ import { Menu, X, ChevronRight } from "lucide-react";
3
+
4
+ /**
5
+ * A single navigation link entry.
6
+ *
7
+ * - `href` — URL the link points to. Optional for parent items that only open a dropdown.
8
+ * - `label` — visible text.
9
+ * - `match` — optional path prefix used for active detection (falls back to `href`).
10
+ * Useful when a parent item has no `href` but should be highlighted for a subtree,
11
+ * e.g. `match: "/docs"` highlights the "Docs" entry for any `/docs/*` page.
12
+ * - `children` — nested items, enabling up to 3 levels of dropdown menus.
13
+ */
14
+ interface MenuItem {
15
+ href?: string;
16
+ label: string;
17
+ match?: string;
18
+ children?: MenuItem[];
19
+ }
20
+
21
+ /** Brand / logo configuration for the navbar. */
22
+ interface BrandProps {
23
+ /** Text displayed as the brand name. */
24
+ text: string;
25
+ /** URL the brand name links to. */
26
+ link: string;
27
+ /** Additional CSS classes applied to the brand anchor. */
28
+ cssClasses?: string;
29
+ }
30
+
31
+ /** Optional CSS class overrides for individual navbar elements. */
32
+ interface CssClasses {
33
+ /** Classes applied to the `<nav>` element. */
34
+ nav?: string;
35
+ /** Classes applied to the `<ul>` list. */
36
+ ul?: string;
37
+ /** Classes applied to each `<li>` item. */
38
+ li?: string;
39
+ }
40
+
41
+ /**
42
+ * Bootstrap-compatible responsive navbar with a React-controlled toggle.
43
+ * Active state is computed internally from `currentPath` — no need to set
44
+ * `isActive` on individual menu items.
45
+ *
46
+ * Menu data can be loaded from a YAML file (e.g. `usr/content/data/menu.yaml`)
47
+ * and passed straight to `menuItems`.
48
+ *
49
+ * Must be hydrated with `client:load` when used inside an Astro component.
50
+ *
51
+ * @example
52
+ * ```astro
53
+ * ---
54
+ * import { getEntry } from 'astro:content';
55
+ * const menuEntry = await getEntry('menu', 'menu');
56
+ * ---
57
+ * <BSNavbar
58
+ * client:load
59
+ * menuItems={menuEntry?.data ?? []}
60
+ * currentPath={Astro.url.pathname}
61
+ * cssClasses={{ nav: "sticky-top shadow-sm", ul: "ms-auto" }}
62
+ * brand={{ text: "My Site", link: "/" }}
63
+ * />
64
+ * ```
65
+ *
66
+ * @example YAML menu structure (`usr/content/data/menu.yaml`)
67
+ * ```yaml
68
+ * - href: /
69
+ * label: Home
70
+ * - label: Docs
71
+ * match: /docs
72
+ * children:
73
+ * - href: /docs/guides/getting-started
74
+ * label: Getting Started
75
+ * - label: Components
76
+ * children:
77
+ * - href: /docs/components/datatb
78
+ * label: DataTb
79
+ * ```
80
+ */
81
+ interface BSNavbarProps {
82
+ /** Array of navigation items to render in the menu. */
83
+ menuItems: MenuItem[];
84
+ /**
85
+ * Current page path (e.g. `Astro.url.pathname`).
86
+ * Used internally to compute which items are active.
87
+ */
88
+ currentPath?: string;
89
+ /** Optional CSS class overrides for navbar sub-elements. */
90
+ cssClasses?: CssClasses;
91
+ /** Optional brand / logo configuration. Omit to hide the brand area. */
92
+ brand?: BrandProps;
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Active detection
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /** Returns true when `item` (or any descendant) matches `currentPath`. */
100
+ function isItemActive(item: MenuItem, currentPath: string): boolean {
101
+ const target = item.match ?? item.href;
102
+ if (target) {
103
+ const active = target === "/" ? currentPath === "/" : currentPath.startsWith(target);
104
+ if (active) return true;
105
+ }
106
+ return item.children?.some(child => isItemActive(child, currentPath)) ?? false;
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // DropdownItem — item inside a dropdown (level ≥ 2)
111
+ // ---------------------------------------------------------------------------
112
+ const DropdownItem: React.FC<{ item: MenuItem; currentPath: string }> = ({ item, currentPath }) => {
113
+ const [isOpen, setIsOpen] = useState(false);
114
+ const [openLeft, setOpenLeft] = useState(false);
115
+ const ref = useRef<HTMLLIElement>(null);
116
+ const hasChildren = !!item.children?.length;
117
+ const active = isItemActive(item, currentPath);
118
+
119
+ useEffect(() => {
120
+ if (!isOpen) return;
121
+ const close = (e: MouseEvent) => {
122
+ if (ref.current && !ref.current.contains(e.target as Node)) setIsOpen(false);
123
+ };
124
+ document.addEventListener("mousedown", close);
125
+ return () => document.removeEventListener("mousedown", close);
126
+ }, [isOpen]);
127
+
128
+ const handleToggle = () => {
129
+ if (!isOpen && ref.current) {
130
+ const rect = ref.current.getBoundingClientRect();
131
+ setOpenLeft(rect.left > window.innerWidth / 2);
132
+ }
133
+ setIsOpen(p => !p);
134
+ };
135
+
136
+ if (!hasChildren) {
137
+ return (
138
+ <li>
139
+ <a
140
+ className={`dropdown-item${active ? " active" : ""}`}
141
+ href={item.href ?? "#"}
142
+ >
143
+ {item.label}
144
+ </a>
145
+ </li>
146
+ );
147
+ }
148
+
149
+ return (
150
+ <li ref={ref} style={{ position: "relative" }}>
151
+ <button
152
+ type="button"
153
+ className={`dropdown-item d-flex justify-content-between align-items-center${active ? " active" : ""}`}
154
+ onClick={handleToggle}
155
+ aria-expanded={isOpen}
156
+ >
157
+ {item.label}
158
+ <ChevronRight size={14} className="ms-2 flex-shrink-0" />
159
+ </button>
160
+ {isOpen && (
161
+ <ul
162
+ className="dropdown-menu show"
163
+ style={openLeft
164
+ ? { position: "absolute", right: "100%", left: "auto", top: 0, minWidth: "12rem" }
165
+ : { position: "absolute", left: "100%", top: 0, minWidth: "12rem" }
166
+ }
167
+ >
168
+ {item.children!.map((child, i) => (
169
+ <DropdownItem key={child.href ?? i} item={child} currentPath={currentPath} />
170
+ ))}
171
+ </ul>
172
+ )}
173
+ </li>
174
+ );
175
+ };
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // NavItem — top-level nav entry (level 1)
179
+ // ---------------------------------------------------------------------------
180
+ const NavItem: React.FC<{ item: MenuItem; currentPath: string; liClass?: string }> = ({ item, currentPath, liClass }) => {
181
+ const [isOpen, setIsOpen] = useState(false);
182
+ const ref = useRef<HTMLLIElement>(null);
183
+ const hasChildren = !!item.children?.length;
184
+ const active = isItemActive(item, currentPath);
185
+
186
+ useEffect(() => {
187
+ if (!isOpen) return;
188
+ const close = (e: MouseEvent) => {
189
+ if (ref.current && !ref.current.contains(e.target as Node)) setIsOpen(false);
190
+ };
191
+ document.addEventListener("mousedown", close);
192
+ return () => document.removeEventListener("mousedown", close);
193
+ }, [isOpen]);
194
+
195
+ const liBase = `nav-item${hasChildren ? " dropdown" : ""}${active ? " active" : ""}${liClass ? ` ${liClass}` : ""}`;
196
+
197
+ if (!hasChildren) {
198
+ return (
199
+ <li className={liBase} ref={ref}>
200
+ <a className={`nav-link${active ? " active" : ""}`} href={item.href ?? "#"}>
201
+ {item.label}
202
+ </a>
203
+ </li>
204
+ );
205
+ }
206
+
207
+ return (
208
+ <li className={liBase} ref={ref}>
209
+ <button
210
+ type="button"
211
+ className={`nav-link dropdown-toggle${active ? " active" : ""}`}
212
+ onClick={() => setIsOpen(p => !p)}
213
+ aria-expanded={isOpen}
214
+ >
215
+ {item.label}
216
+ </button>
217
+ {isOpen && (
218
+ <ul className="dropdown-menu show">
219
+ {item.children!.map((child, i) => (
220
+ <DropdownItem key={child.href ?? i} item={child} currentPath={currentPath} />
221
+ ))}
222
+ </ul>
223
+ )}
224
+ </li>
225
+ );
226
+ };
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // BSNavbar — root component
230
+ // ---------------------------------------------------------------------------
231
+ const BSNavbar: React.FC<BSNavbarProps> = ({ menuItems, currentPath = "/", cssClasses = {}, brand }) => {
232
+ const [isOpen, setIsOpen] = useState(false);
233
+
234
+ return (
235
+ <nav className={`navbar navbar-expand-md ${cssClasses.nav ?? ""}`}>
236
+ <div className="container">
237
+ {brand && (
238
+ <a className={`navbar-brand ${brand.cssClasses ?? ""}`} href={brand.link}>
239
+ {brand.text}
240
+ </a>
241
+ )}
242
+ <button
243
+ className="navbar-toggler border-0 p-2"
244
+ type="button"
245
+ onClick={() => setIsOpen(prev => !prev)}
246
+ aria-controls="navbarCollapse"
247
+ aria-expanded={isOpen}
248
+ aria-label="Toggle navigation"
249
+ >
250
+ {isOpen ? <X size={24} className="text-dark" /> : <Menu size={24} className="text-dark" />}
251
+ </button>
252
+ <div className={`collapse navbar-collapse${isOpen ? " show" : ""}`} id="navbarCollapse">
253
+ <ul className={`navbar-nav ${cssClasses.ul ?? ""}`}>
254
+ {menuItems.map((item, i) => (
255
+ <NavItem key={item.href ?? i} item={item} currentPath={currentPath} liClass={cssClasses.li} />
256
+ ))}
257
+ </ul>
258
+ </div>
259
+ </div>
260
+ </nav>
261
+ );
262
+ };
263
+
264
+ export default BSNavbar;
@@ -0,0 +1,33 @@
1
+ ---
2
+ /**
3
+ * DataTb Astro Wrapper
4
+ *
5
+ * Astro component that wraps the React DataTb component
6
+ * This file exists for documentation and re-export purposes.
7
+ * In MDX files, import the React component directly with a hydration directive.
8
+ *
9
+ * Example usage in MDX:
10
+ * ```jsx
11
+ * import { DataTb } from '@lad-sapienza/scms-core/components/DataTb';
12
+ *
13
+ * <DataTb csv="/data.csv" searchable pagination client:idle />
14
+ * ```
15
+ *
16
+ * CsvSource / JsonSource / DirectusSource / ApiSource are also exported, but as
17
+ * headless data-loaders (they render nothing; they call onDataLoad/onLoadingChange/
18
+ * onError) for building a fully custom table UI — they are not children of
19
+ * <DataTb>, which has no children/composition API. See the "Headless Source
20
+ * Components" section of usr/content/docs/components/datatb.mdx.
21
+ */
22
+
23
+ // Re-export for convenience. `DataTb` itself is deliberately not re-exported
24
+ // here — a named export called `DataTb` in DataTb.astro's own frontmatter
25
+ // collides with Astro's implicit type for the file (same name as the file),
26
+ // producing a "Duplicate identifier 'DataTb'" build error. Nothing in this
27
+ // project imports DataTb from this file (use `@lad-sapienza/scms-core` or
28
+ // `@lad-sapienza/scms-core/components/DataTb` instead), so this only affects the unused path.
29
+ export { CsvSource, JsonSource, DirectusSource, ApiSource } from './index';
30
+ export type * from './types';
31
+ ---
32
+
33
+ <slot />