@lad-sapienza/scms-core 0.1.0-alpha.8 → 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.
@@ -1,11 +1,16 @@
1
1
  # Map Component
2
2
 
3
- Interactive map built on [MapLibre GL JS](https://maplibre.org/) (via
3
+ Interactive map built on [MapLibre GL JS](https://maplibre.org/) **v6** (via
4
4
  `@vis.gl/react-maplibre`). Renders a raster basemap, any number of toggleable
5
5
  overlay layers pulled from heterogeneous data sources (GeoJSON, CSV, JSON,
6
6
  Directus, a generic API, or raster XYZ/WMS tiles), and a built-in layer
7
7
  control for switching basemaps and toggling overlays.
8
8
 
9
+ MapLibre v6 requires **WebGL2** (universally supported by current browsers) and
10
+ no longer auto-loads its Web Worker — `Map.tsx` wires the worker up itself with
11
+ a Vite `?worker&url` import, so consuming sites need no bundler/worker config.
12
+ The `maplibre-gl` peer dependency is `^6`.
13
+
9
14
  ## Basic usage
10
15
 
11
16
  ```mdx
@@ -26,6 +31,30 @@ touching `overlayLayers` directly:
26
31
  <Map directus={{ table: 'sites', geoField: 'location' }} />
27
32
  ```
28
33
 
34
+ For one or a few hand-placed points (e.g. the location of a single site) use
35
+ `markers` — no data file needed. The map fits to them automatically; a lone
36
+ marker is centred at a regional zoom (≥ 12) rather than zoomed to the max:
37
+
38
+ ```mdx
39
+ <Map markers={{ lng: 12.4964, lat: 41.9028, label: 'Roma' }} />
40
+
41
+ <Map markers={[
42
+ { lng: 12.49, lat: 41.90, label: 'Site A' },
43
+ { lng: 12.51, lat: 41.88, label: 'Site B' },
44
+ ]} />
45
+ ```
46
+
47
+ `label` is optional (shown in a popup as plain text). Markers are an extra
48
+ overlay layer and combine with any of the shorthands below.
49
+
50
+ `markersZoom` fixes the zoom: the map is centred on the markers at that level
51
+ instead of being fitted to their extent — useful to give a single site the
52
+ same geographic context on every page:
53
+
54
+ ```mdx
55
+ <Map markers={{ lng: 12.4964, lat: 41.9028, label: 'Roma' }} markersZoom={9} />
56
+ ```
57
+
29
58
  `geojson`/`csv`/`json`/`directus` each accept either a bare string (path/URL)
30
59
  or an object with `name`, `style`, `popup`, `fitToContent`, `filter` — see
31
60
  `MapProps` in [`types.ts`](./types.ts) for the exact shape of each.
@@ -199,3 +228,10 @@ exact shape.
199
228
  only two functions that build tile URL templates — both `baseLayers` and
200
229
  raster `overlayLayers` go through them, so there's one place to fix or
201
230
  extend tile-URL construction.
231
+ - MapLibre GL JS v6 dropped the automatic worker bootstrap, so `Map.tsx`
232
+ imports `maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url` and calls
233
+ `setWorkerUrl()` at module load, before `@vis.gl/react-maplibre` constructs
234
+ any map. `?worker&url` (not `?url`) routes the worker through Vite's worker
235
+ pipeline so its `maplibre-gl-shared.mjs` sibling is bundled in. `scms.ts`
236
+ adds `ssr.noExternal: ['maplibre-gl']` so that import also resolves in the
237
+ SSR pass for consumers rendering `<Map client:load>`.
@@ -1,5 +1,4 @@
1
1
  export { Map } from './MapMdx';
2
- export { Map as MapCore } from './Map';
3
2
  export { defaultBasemaps, getBasemap, getBasemaps } from './defaultBasemaps';
4
3
  export type { BasemapKey } from './defaultBasemaps';
5
4
  export type { OverlayLayerConfig } from './types';
@@ -124,6 +124,12 @@ export interface OverlayLayerConfig {
124
124
  /** Fit map bounds to layer content */
125
125
  fitToContent?: boolean;
126
126
 
127
+ /**
128
+ * With `fitToContent`: centre on the content's bounds at this fixed zoom
129
+ * instead of fitting them (used by the `markers` shorthand).
130
+ */
131
+ fitZoom?: number;
132
+
127
133
  /** Optional client-side filter for features */
128
134
  filter?: FilterObject | ((feature: any) => boolean);
129
135
 
@@ -131,6 +137,17 @@ export interface OverlayLayerConfig {
131
137
  searchInFields?: SearchInFields;
132
138
  }
133
139
 
140
+ /**
141
+ * A single point marker for the `markers` shorthand — for maps with one or a
142
+ * few hand-placed points, no data file needed.
143
+ */
144
+ export interface MarkerConfig {
145
+ lng: number;
146
+ lat: number;
147
+ /** Text shown in the popup on hover/click (plain text, HTML-escaped) */
148
+ label?: string;
149
+ }
150
+
134
151
  export interface MapProps {
135
152
  /** Map height (CSS string) */
136
153
  height?: string;
@@ -197,6 +214,19 @@ export interface MapProps {
197
214
  filter?: FilterObject | ((row: any) => boolean);
198
215
  };
199
216
 
217
+ /**
218
+ * Inline markers - one or a few hand-placed points, e.g. to give the
219
+ * geographic context of a single site. Rendered as an extra overlay layer
220
+ * and, by default, the map is fitted to them.
221
+ */
222
+ markers?: MarkerConfig | MarkerConfig[];
223
+
224
+ /**
225
+ * Fixed zoom for the `markers`: the map is centred on them at this zoom
226
+ * instead of being fitted to their extent. Handy for a single site.
227
+ */
228
+ markersZoom?: number;
229
+
200
230
  /** JSON shorthand - for simple single-layer maps */
201
231
  json?: any[] | string;
202
232
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { DataRow } from '../../utils/data-fetcher';
6
6
  import type { FeatureCollection, Feature, Geometry } from 'geojson';
7
- import type { WmsSourceConfig } from './types';
7
+ import type { WmsSourceConfig, MarkerConfig } from './types';
8
8
 
9
9
  /**
10
10
  * Tries to identify latitude and longitude fields from a data row
@@ -117,6 +117,24 @@ export function dataToGeoJson(
117
117
  };
118
118
  }
119
119
 
120
+ /**
121
+ * Converts the `markers` shorthand to a GeoJSON FeatureCollection of points,
122
+ * dropping entries whose coordinates aren't finite numbers. The label ends up
123
+ * in the `label` property, which the markers layer's popup template reads.
124
+ */
125
+ export function markersToGeoJson(markers: MarkerConfig | MarkerConfig[]): FeatureCollection {
126
+ const list = Array.isArray(markers) ? markers : [markers];
127
+ const features: Feature[] = list
128
+ .filter(m => m && m.lng != null && m.lat != null
129
+ && Number.isFinite(Number(m.lng)) && Number.isFinite(Number(m.lat)))
130
+ .map(m => ({
131
+ type: 'Feature',
132
+ geometry: { type: 'Point', coordinates: [Number(m.lng), Number(m.lat)] },
133
+ properties: { label: m.label ?? '' }
134
+ }));
135
+ return { type: 'FeatureCollection', features };
136
+ }
137
+
120
138
  /**
121
139
  * Expands an XYZ tile URL template's {s} subdomain placeholder into the
122
140
  * standard a/b/c rotation (as Leaflet does natively). Returns the url
@@ -49,7 +49,7 @@ function DefaultError({ table, id }: { table: string; id: string }) {
49
49
  * **Usage in an Astro page:**
50
50
  * ```astro
51
51
  * ---
52
- * import RecordFetcher from '@lad-sapienza/scms-core/components/Record/RecordFetcher.tsx';
52
+ * import { RecordFetcher } from '@lad-sapienza/scms-core/components/Record';
53
53
  * import MyLayout from '@components/MyRecordLayout.tsx';
54
54
  * ---
55
55
  * <RecordFetcher Layout={MyLayout} client:load />
@@ -32,7 +32,7 @@ export interface UseRecordFetcherResult {
32
32
  * @example
33
33
  * ```tsx
34
34
  * // src/components/RecordView.tsx
35
- * import { useRecordFetcher } from '@lad-sapienza/scms-core/components/Record/useRecordFetcher';
35
+ * import { useRecordFetcher } from '@lad-sapienza/scms-core/components/Record';
36
36
  *
37
37
  * export default function RecordView() {
38
38
  * const { record, loading, error, table, id } = useRecordFetcher();
@@ -28,6 +28,7 @@ export const BUILTIN_DICTIONARY: Dictionary = {
28
28
  },
29
29
  map: {
30
30
  defaultLayerName: 'Default Layer',
31
+ markers: 'Markers',
31
32
  baseMaps: 'Base Maps',
32
33
  searchLayer: 'Search {name}',
33
34
  active: '(Active)',
package/index.ts CHANGED
@@ -8,7 +8,7 @@ export { default as ThemeToggle } from './components/ThemeToggle';
8
8
  export { DataTb, CsvSource, JsonSource, DirectusSource, ApiSource } from './components/DataTb';
9
9
 
10
10
  // Map components
11
- export { Map as MapComponent } from './components/Map';
11
+ export { Map } from './components/Map';
12
12
  export { SearchUI, SearchUISimple, SearchUIAdvanced } from './components/Map/Search';
13
13
 
14
14
  // ZoteroGeoViewer component
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lad-sapienza/scms-core",
3
- "version": "0.1.0-alpha.8",
3
+ "version": "0.1.0",
4
4
  "description": "Framework components and Astro integrations for s:CMS — DataTb, Gallery, Map, ZoteroGeoViewer, Record.",
5
5
  "type": "module",
6
6
  "license": "BSD-0-Clause",
@@ -10,10 +10,32 @@
10
10
  },
11
11
  "author": "Julian Bogdani <julian.bogdani@uniroma1.it>",
12
12
  "main": "./index.ts",
13
+ "exports": {
14
+ ".": "./index.ts",
15
+ "./scms": "./scms.ts",
16
+ "./components/Map": "./components/Map/index.ts",
17
+ "./components/DataTb": "./components/DataTb/index.ts",
18
+ "./components/DataTb/types": "./components/DataTb/types.ts",
19
+ "./components/Gallery": "./components/Gallery/index.ts",
20
+ "./components/Gallery/Gallery.astro": "./components/Gallery/Gallery.astro",
21
+ "./components/Record": "./components/Record/index.ts",
22
+ "./components/ZoteroGeoViewer": "./components/ZoteroGeoViewer/index.ts",
23
+ "./components/TableOfContents/TableOfContents.astro": "./components/TableOfContents/TableOfContents.astro",
24
+ "./components/SEO/SEO.astro": "./components/SEO/SEO.astro",
25
+ "./components/BSNavbar": "./components/BSNavbar.tsx",
26
+ "./components/LocaleSwitcher": "./components/LocaleSwitcher.tsx",
27
+ "./components/ThemeToggle": "./components/ThemeToggle.tsx",
28
+ "./components/RedirectPage/RedirectPage.astro": "./components/RedirectPage/RedirectPage.astro",
29
+ "./components/i18n": "./components/i18n/index.ts",
30
+ "./components/i18n/I18nInit.astro": "./components/i18n/I18nInit.astro",
31
+ "./integrations/directusLoader": "./integrations/directusLoader.ts",
32
+ "./package.json": "./package.json"
33
+ },
13
34
  "bin": {
14
35
  "scms-add-collection": "./bin/add-collection.mjs",
15
36
  "scms-add-content": "./bin/add-content.mjs",
16
- "scms-create": "./bin/create.mjs"
37
+ "scms-create": "./bin/create.mjs",
38
+ "scms-optimize-images": "./bin/optimize-images.mjs"
17
39
  },
18
40
  "files": [
19
41
  "index.ts",
@@ -35,7 +57,7 @@
35
57
  "@types/react": "^19.2.14",
36
58
  "@types/react-dom": "^19.2.3",
37
59
  "astro": "^7.2.4",
38
- "maplibre-gl": "^5.21.0",
60
+ "maplibre-gl": "^6.7.0",
39
61
  "react": "^19.2.4",
40
62
  "react-dom": "^19.2.4"
41
63
  },
@@ -48,12 +70,13 @@
48
70
  "@tanstack/react-table": "^8.21.3",
49
71
  "@types/geojson": "^7946.0.16",
50
72
  "@types/papaparse": "^5.5.2",
51
- "@vis.gl/react-maplibre": "^8.1.0",
73
+ "@vis.gl/react-maplibre": "^8.1.3",
52
74
  "astro-expressive-code": "^0.44.1",
53
75
  "lucide-react": "^1.8.0",
54
76
  "papaparse": "^5.5.3",
55
77
  "photoswipe": "^5.4.4",
56
78
  "react-photoswipe-gallery": "^4.0.0",
79
+ "sharp": "^0.35.3",
57
80
  "zod": "^4.3.6"
58
81
  },
59
82
  "devDependencies": {
package/scms.ts CHANGED
@@ -64,22 +64,31 @@ function scmsViteConfigIntegration(): AstroIntegration {
64
64
  dedupe: HOOK_LIBRARIES,
65
65
  },
66
66
  optimizeDeps: {
67
- // papaparse is CommonJS-only (no `module`/`exports` field), and
68
- // maplibre-gl's `dist/maplibre-gl.js` is a UMD bundle despite
69
- // declaring `"type": "module"` in its own package.json (verified:
70
- // it's a `(function(global, factory) {...})` UMD wrapper with zero
71
- // `export` statements). When their importers (CsvSource.tsx,
72
- // Map.tsx via @vis.gl/react-maplibre's dynamic `import('maplibre-gl')`)
73
- // lived inside this project's own source tree, Vite's dev-time
74
- // dependency scanner discovered and pre-bundled them automatically,
75
- // performing the CJS/UMD→ESM interop conversion. Now that the
76
- // importers live in node_modules (this package), Vite's default
77
- // scanning no longer picks them up on its own, and the browser gets
78
- // served the raw file as if it already were ESM — resulting in
79
- // "does not provide an export named 'default'" for papaparse and
80
- // an empty module namespace (silently breaking `'Map' in module`)
81
- // for maplibre-gl. Both must be pre-bundled explicitly.
82
- include: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'papaparse', 'maplibre-gl'],
67
+ // papaparse is CommonJS-only (no `module`/`exports` field). Its
68
+ // importer (CsvSource.tsx) lives in node_modules once this is a
69
+ // published package, so Vite's dev-time dependency scanner no
70
+ // longer discovers it on its own and pre-bundles the CJS→ESM
71
+ // interop — without this entry the browser is served the raw CJS
72
+ // file and crashes with "does not provide an export named
73
+ // 'default'". Must stay explicit.
74
+ //
75
+ // maplibre-gl used to need the same treatment because v5 shipped a
76
+ // UMD `dist/maplibre-gl.js`; v6 is real ESM (`dist/maplibre-gl.mjs`,
77
+ // `"exports"` map, no UMD), so Vite handles it with no interop
78
+ // shim and no explicit entry here. Map.tsx's `?worker&url` worker
79
+ // chunk is a separate, self-contained module and does not go
80
+ // through this pre-bundle.
81
+ include: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'papaparse'],
82
+ },
83
+ ssr: {
84
+ // MapLibre GL JS v6's worker is wired up in Map.tsx via a
85
+ // `maplibre-gl/dist/...?worker&url` import (see that file). Vite's
86
+ // worker plugin can only resolve that specifier when maplibre-gl
87
+ // is bundled for SSR rather than externalised to a raw Node
88
+ // `import` — required for consumers that render <Map> with
89
+ // `client:load`/`client:visible` (server pass) rather than
90
+ // `client:only`. MapLibre's own Astro guidance calls for this.
91
+ noExternal: ['maplibre-gl'],
83
92
  },
84
93
  },
85
94
  });
@@ -14,6 +14,19 @@ Built with [s:CMS](https://github.com/lad-sapienza/scms-core), on top of [Astro]
14
14
  | `npm run preview` | Preview the production build locally |
15
15
  | `npm run add-collection` | Scaffold a new content collection |
16
16
  | `npm run add-content` | Add a new content file to an existing collection |
17
+ | `npm run images` | Convert content images to WebP (max 2000 px) and rewrite their references |
18
+
19
+ ## Optimizing images
20
+
21
+ Drop JPG/PNG images next to your content as usual, then run `npm run images`: they are converted to WebP, scaled down to 2000 px, the originals are deleted and the `.md`/`.mdx` references are rewritten (`--dry-run` shows the savings without touching anything). References from `.astro`/`.tsx` files are only reported — update them by hand.
22
+
23
+ To make the build fail when an unoptimized image slips in, add a check to `package.json`; it never modifies any file:
24
+
25
+ ```json
26
+ "prebuild": "scms-optimize-images --check"
27
+ ```
28
+
29
+ Limits, quality and the site languages are configured in `src/user.config.mjs` (`userConfig.images`, `userConfig.i18n`); see the [scms-core README](https://github.com/lad-sapienza/scms-core#image-optimization) for all the options, including an optional pre-commit hook. Commit the `.scms-optimize-images.json` file it may create.
17
30
 
18
31
  ## Updating the framework
19
32
 
@@ -10,7 +10,8 @@
10
10
  "preview": "astro preview",
11
11
  "astro": "astro",
12
12
  "add-collection": "scms-add-collection",
13
- "add-content": "scms-add-content"
13
+ "add-content": "scms-add-content",
14
+ "images": "scms-optimize-images"
14
15
  },
15
16
  "dependencies": {
16
17
  "@astrojs/check": "^0.9.10",