@phila/layerboard 3.0.0-beta.3 → 3.0.0-beta.30

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/README.md CHANGED
@@ -1,78 +1,206 @@
1
- # vue3-layerboard
1
+ # @phila/layerboard
2
2
 
3
- A Vue 3 + MapLibre GL JS framework for building interactive map applications with layer management, measurement tools, and ArcGIS integration.
3
+ A Vue 3 component framework for building interactive map applications powered by ArcGIS Online WebMaps and MapLibre GL JS. Provide a WebMap ID and get a full mapping app with layer management, popups, legends, search, and more.
4
4
 
5
- ## Features
5
+ ## Live Examples
6
6
 
7
- - **MapLibre GL JS Integration** - Modern, performant vector tile mapping
8
- - **Layer Management** - Dynamic layer loading from ArcGIS Online web maps
9
- - **Measurement Tools** - Interactive area and distance measurement
10
- - **Geolocation** - User location tracking and display
11
- - **Legend & Layer Controls** - Configurable layer visibility and legends
12
- - **Popup System** - Feature identification and attribute display
7
+ - [OpenMaps](https://openmaps.phila.gov) flat layer list (all layers searchable)
8
+ - [StreetSmartPHL](https://streetsmartphl.phila.gov) topic-based layout (layers grouped in accordions)
13
9
 
14
10
  ## Installation
15
11
 
16
12
  ```sh
17
- npm install @phila/layerboard
13
+ pnpm add @phila/layerboard
18
14
  ```
19
15
 
20
- ## Peer Dependencies
16
+ ### Peer Dependencies
21
17
 
22
- This package requires the following peer dependencies:
18
+ ```sh
19
+ pnpm add vue pinia maplibre-gl @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/vue-fontawesome
20
+ ```
23
21
 
24
- - `vue` ^3.5.0
25
- - `pinia` ^3.0.0
26
- - `maplibre-gl` ^5.0.0
27
- - `@fortawesome/fontawesome-svg-core` ^7.0.0
28
- - `@fortawesome/free-solid-svg-icons` ^7.0.0
29
- - `@fortawesome/vue-fontawesome` ^3.0.0
22
+ ## Quick Start
30
23
 
31
- ## Design Decisions
24
+ ```vue
25
+ <template>
26
+ <Layerboard title="My Map App" web-map-id="1596df70df0349e293ceec46a06ccc50" />
27
+ </template>
32
28
 
33
- ### Compact Text Field Styling
29
+ <script setup>
30
+ import { Layerboard } from "@phila/layerboard";
31
+ import "@phila/layerboard/dist/layerboard.css";
32
+ </script>
33
+ ```
34
34
 
35
- The phila-ui TextField component has a standard height of 56px, designed for form inputs where touch targets and readability are priorities. In the layerboard context (sidebar filter and map search control), we intentionally use a more compact ~40px version to save vertical space.
35
+ This renders a full-screen map app with a sidebar listing all layers from the WebMap, complete with legends, opacity sliders, search, popups, and mobile responsiveness.
36
36
 
37
- This is achieved through CSS overrides in:
38
- - **LayerPanel.vue** - for the layer filter search box
39
- - **MapPanel.vue** - for the MapSearchControl on the map
37
+ ## Layout Modes
40
38
 
41
- The overrides remove padding from `.state-layer` and `.content` elements. See the detailed comments in those files for the technical breakdown.
39
+ ### Flat Mode (default)
42
40
 
43
- ## Development
41
+ All layers in a searchable list. Set `show-default-sidebar` to `true` (default) and the built-in `LayerPanel` renders in the sidebar.
44
42
 
45
- ### Recommended IDE Setup
43
+ ### Topics Mode
46
44
 
47
- [VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
45
+ Group layers into collapsible accordions using the sidebar slot:
48
46
 
49
- ### Recommended Browser Setup
47
+ ```vue
48
+ <Layerboard title="StreetSmartPHL" :web-map-id="webMapId" :show-default-sidebar="false">
49
+ <template #sidebar="{ layers, visibleLayers, toggleLayer, setOpacity }">
50
+ <TopicAccordion title="Paving" :expanded="true">
51
+ <LayerCheckboxSet
52
+ :layers="pavingLayers"
53
+ :visible-layer-ids="visibleLayers"
54
+ @toggle-layer="toggleLayer"
55
+ @set-opacity="setOpacity"
56
+ />
57
+ </TopicAccordion>
58
+ </template>
59
+ </Layerboard>
60
+ ```
50
61
 
51
- - Chromium-based browsers (Chrome, Edge, Brave, etc.):
52
- - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
53
- - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
54
- - Firefox:
55
- - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
56
- - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
62
+ ## Props
63
+
64
+ | Prop | Type | Default | Description |
65
+ | ----------------------- | ------------------------------------ | ----------- | --------------------------------------------------- |
66
+ | `title` | `string` | _required_ | App title in header |
67
+ | `webMapId` | `string` | _required_ | ArcGIS Online WebMap ID |
68
+ | `subtitle` | `string` | — | Subtitle in header |
69
+ | `themeColor` | `string` | `"#0f4d90"` | Header/footer background color |
70
+ | `showDefaultSidebar` | `boolean` | `true` | Show built-in LayerPanel (false for custom sidebar) |
71
+ | `sidebarWidth` | `string` | `"30%"` | Sidebar width (CSS units) |
72
+ | `sidebarLabel` | `string` | `"Layers"` | Mobile toggle label for sidebar view |
73
+ | `mapLabel` | `string` | `"Map"` | Mobile toggle label for map view |
74
+ | `fetchMetadata` | `boolean` | `false` | Fetch layer metadata from Carto |
75
+ | `tiledLayers` | `TiledLayerConfig[]` | `[]` | ESRI MapServer tiled layers |
76
+ | `dataSources` | `DataSourceConfig[]` | `[]` | External API data sources |
77
+ | `layerStyleOverrides` | `Record<string, LayerStyleOverride>` | `{}` | Override paint/legend per layer |
78
+ | `popupOverrides` | `Record<string, PopupOverride>` | `{}` | Override popup behavior per layer |
79
+ | `initialZoom` | `number` | — | Initial map zoom level |
80
+ | `initialCenter` | `[number, number]` | — | Initial map center `[lng, lat]` |
81
+ | `cyclomediaConfig` | `CyclomediaConfig` | — | Cyclomedia street-level imagery config |
82
+ | `pictometryCredentials` | `PictometryCredentials` | — | Pictometry oblique imagery credentials |
83
+
84
+ ### Control Positions
85
+
86
+ All default to sensible positions. Each accepts `"top-left" | "top-right" | "bottom-left" | "bottom-right"`.
87
+
88
+ | Prop | Default |
89
+ | ---------------------------- | ------------------------------------- |
90
+ | `basemapControlPosition` | `"top-right"` |
91
+ | `navigationControlPosition` | `"bottom-right"` |
92
+ | `geolocationControlPosition` | `"bottom-right"` |
93
+ | `searchControlPosition` | `"top-left"` |
94
+ | `drawControlPosition` | `"bottom-left"` (or `null` to remove) |
95
+ | `cyclomediaButtonPosition` | `"top-right"` |
96
+ | `pictometryButtonPosition` | `"top-right"` |
97
+
98
+ ## Events
99
+
100
+ | Event | Payload | Description |
101
+ | ---------------- | --------------- | -------------------------------- |
102
+ | `configs-loaded` | `LayerConfig[]` | Layer configs loaded from WebMap |
103
+ | `load-error` | `string` | Error message on load failure |
104
+ | `zoom` | `number` | Zoom level changed |
105
+
106
+ ## Slots
107
+
108
+ | Slot | Scope | Description |
109
+ | --------- | ---------------------------------------- | -------------------------- |
110
+ | `header` | — | Replace default header |
111
+ | `sidebar` | layer state + methods (see below) | Replace default LayerPanel |
112
+ | `footer` | `{ openModal, closeModal, isModalOpen }` | Custom footer content |
113
+ | `modal` | `{ closeModal }` | Modal content |
114
+
115
+ ### Sidebar Slot Scope
116
+
117
+ The sidebar slot exposes the full layer state for building custom UIs:
118
+
119
+ ```typescript
120
+ {
121
+ layers: Array<{ config: LayerConfig; component: string }>
122
+ visibleLayers: Set<string>
123
+ layerOpacities: Record<string, number>
124
+ loadingLayers: Set<string>
125
+ layerErrors: Record<string, string>
126
+ currentZoom: number
127
+ toggleLayer: (id: string) => void
128
+ setLayerVisible: (id: string, visible: boolean) => void
129
+ setLayersVisible: (ids: string[], visible: boolean) => void
130
+ setOpacity: (id: string, opacity: number) => void
131
+ // Tiled layers
132
+ tiledLayers: TiledLayerConfig[]
133
+ visibleTiledLayers: Set<string>
134
+ tiledLayerOpacities: Record<string, number>
135
+ toggleTiledLayer: (id: string) => void
136
+ setTiledLayerVisible: (id: string, visible: boolean) => void
137
+ setTiledLayerOpacity: (id: string, opacity: number) => void
138
+ // Data sources
139
+ dataSourcesState: Record<string, DataSourceState>
140
+ dataSourcesLoading: boolean
141
+ getDataSource: (id: string) => unknown | null
142
+ refetchDataSource: (id: string) => Promise<void>
143
+ }
144
+ ```
57
145
 
58
- ### Type Support for `.vue` Imports in TS
146
+ ## Components
59
147
 
60
- TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
148
+ All components are exported for building custom layouts:
61
149
 
62
- ### Project Setup
150
+ ```typescript
151
+ import {
152
+ Layerboard, // Main framework component
153
+ LayerPanel, // Flat layer list with search/legends/opacity
154
+ MapPanel, // MapLibre map with layer rendering
155
+ TopicAccordion, // Collapsible accordion for topic grouping
156
+ LayerCheckboxSet, // Checkbox controls for layer toggling
157
+ LayerRadioButtonSet, // Radio buttons for mutually exclusive layers
158
+ } from "@phila/layerboard";
159
+ ```
63
160
 
64
- ```sh
65
- npm install
161
+ ## Types
162
+
163
+ All types are exported:
164
+
165
+ ```typescript
166
+ import type {
167
+ LayerConfig,
168
+ LayerDisplayOptions,
169
+ LayerStyleOverride,
170
+ LegendItem,
171
+ PopupConfig,
172
+ PopupField,
173
+ PopupOverride,
174
+ TiledLayerConfig,
175
+ DataSourceConfig,
176
+ DataSourceState,
177
+ LayerboardConfig,
178
+ TopicConfig,
179
+ FeatureFlags,
180
+ CyclomediaConfig, // re-exported from @phila/phila-ui-map-core
181
+ PictometryCredentials, // re-exported from @phila/phila-ui-map-core
182
+ } from "@phila/layerboard";
66
183
  ```
67
184
 
68
- ### Compile and Hot-Reload for Development
185
+ ## How It Works
186
+
187
+ 1. You provide an ArcGIS Online **WebMap ID**
188
+ 2. Layerboard fetches the WebMap JSON at runtime
189
+ 3. Esri renderers, symbols, scales, and popup configs are transformed into MapLibre-compatible layer configs
190
+ 4. Layers render on a MapLibre GL map — feature data is fetched from ArcGIS FeatureServer endpoints with spatial filtering and pagination
191
+ 5. Server-side geometry simplification (`maxAllowableOffset`) scales with zoom level for polygon layers
192
+
193
+ ## Development
69
194
 
70
195
  ```sh
71
- npm run dev
196
+ pnpm install
197
+ pnpm build # type-check + vite build
72
198
  ```
73
199
 
74
- ### Type-Check, Compile and Minify for Production
200
+ ### Publishing
75
201
 
76
202
  ```sh
77
- npm run build
203
+ pnpm version prerelease # bump beta version
204
+ git push origin main
205
+ git tag v<version> && git push origin v<version> # triggers publish workflow
78
206
  ```
@@ -0,0 +1,37 @@
1
+ import { LayerConfig } from '../types/layer';
2
+ type __VLS_Props = {
3
+ /** Array of layer configurations to display */
4
+ layers: LayerConfig[];
5
+ /** Set of currently visible layer IDs */
6
+ visibleLayerIds: Set<string>;
7
+ /** Map of layer IDs to opacity values (0-1) */
8
+ layerOpacities?: Record<string, number>;
9
+ /** Set of layer IDs currently loading */
10
+ loadingLayerIds?: Set<string>;
11
+ /** Map of layer IDs to error messages */
12
+ layerErrors?: Record<string, string>;
13
+ /** Current map zoom level (for zoom-based availability) */
14
+ currentZoom?: number;
15
+ /** Whether to show opacity sliders (can be overridden per-layer) */
16
+ showOpacity?: boolean;
17
+ /** Whether to show legends (can be overridden per-layer) */
18
+ showLegend?: boolean;
19
+ /** Accessible label for the group */
20
+ groupLabel?: string;
21
+ };
22
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
23
+ toggleLayer: (layerId: string) => any;
24
+ setOpacity: (layerId: string, opacity: number) => any;
25
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
26
+ onToggleLayer?: ((layerId: string) => any) | undefined;
27
+ onSetOpacity?: ((layerId: string, opacity: number) => any) | undefined;
28
+ }>, {
29
+ layerOpacities: Record<string, number>;
30
+ layerErrors: Record<string, string>;
31
+ currentZoom: number;
32
+ showOpacity: boolean;
33
+ showLegend: boolean;
34
+ loadingLayerIds: Set<string>;
35
+ groupLabel: string;
36
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLFieldSetElement>;
37
+ export default _default;
@@ -0,0 +1,7 @@
1
+ import { LegendItem } from '../types/layer';
2
+ type __VLS_Props = {
3
+ items: LegendItem[];
4
+ label: string;
5
+ };
6
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLUListElement>;
7
+ export default _default;
@@ -0,0 +1,11 @@
1
+ type __VLS_Props = {
2
+ layerId: string;
3
+ layerName: string;
4
+ opacity: number;
5
+ };
6
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
7
+ "update:opacity": (opacity: number) => any;
8
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
9
+ "onUpdate:opacity"?: ((opacity: number) => any) | undefined;
10
+ }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
11
+ export default _default;
@@ -0,0 +1,63 @@
1
+ import { LayerConfig } from '../types/layer';
2
+ type __VLS_Props = {
3
+ /** Array of layer configurations with component type */
4
+ layerList: Array<{
5
+ config: LayerConfig;
6
+ component: string;
7
+ }>;
8
+ /** Set of currently visible layer IDs */
9
+ visibleLayers: Set<string>;
10
+ /** Map of layer IDs to opacity values (0-1) */
11
+ layerOpacities: Record<string, number>;
12
+ /** Set of layer IDs currently loading */
13
+ loadingLayers: Set<string>;
14
+ /** Map of layer IDs to error messages */
15
+ layerErrors: Record<string, string>;
16
+ /** Current map zoom level */
17
+ currentZoom: number;
18
+ /** Current search query */
19
+ searchQuery: string;
20
+ /** Map of layer URLs to metadata page URLs */
21
+ layerMetadata: Record<string, string>;
22
+ /** Display mode: 'flat' for simple list, 'topics' for accordion grouping */
23
+ mode?: "flat" | "topics";
24
+ /** Whether to show the search box */
25
+ showSearch?: boolean;
26
+ /** Whether to show opacity sliders */
27
+ showOpacity?: boolean;
28
+ /** Whether to show legends */
29
+ showLegend?: boolean;
30
+ /** Placeholder text for search input */
31
+ searchPlaceholder?: string;
32
+ };
33
+ declare function __VLS_template(): {
34
+ attrs: Partial<{}>;
35
+ slots: {
36
+ topics?(_: {}): any;
37
+ };
38
+ refs: {};
39
+ rootEl: HTMLDivElement;
40
+ };
41
+ type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
42
+ declare const __VLS_component: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
43
+ toggleLayer: (layerId: string) => any;
44
+ setOpacity: (layerId: string, opacity: number) => any;
45
+ updateSearch: (query: string) => any;
46
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
47
+ onToggleLayer?: ((layerId: string) => any) | undefined;
48
+ onSetOpacity?: ((layerId: string, opacity: number) => any) | undefined;
49
+ onUpdateSearch?: ((query: string) => any) | undefined;
50
+ }>, {
51
+ mode: "flat" | "topics";
52
+ showSearch: boolean;
53
+ showOpacity: boolean;
54
+ showLegend: boolean;
55
+ searchPlaceholder: string;
56
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
57
+ declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
58
+ export default _default;
59
+ type __VLS_WithTemplateSlots<T, S> = T & {
60
+ new (): {
61
+ $slots: S;
62
+ };
63
+ };
@@ -0,0 +1,40 @@
1
+ import { LayerConfig } from '../types/layer';
2
+ type __VLS_Props = {
3
+ /** Array of layer configurations to display */
4
+ layers: LayerConfig[];
5
+ /** Set of currently visible layer IDs */
6
+ visibleLayerIds: Set<string>;
7
+ /** Map of layer IDs to opacity values (0-1) */
8
+ layerOpacities?: Record<string, number>;
9
+ /** Set of layer IDs currently loading */
10
+ loadingLayerIds?: Set<string>;
11
+ /** Map of layer IDs to error messages */
12
+ layerErrors?: Record<string, string>;
13
+ /** Current map zoom level (for zoom-based availability) */
14
+ currentZoom?: number;
15
+ /** Whether to show opacity sliders (can be overridden per-layer) */
16
+ showOpacity?: boolean;
17
+ /** Whether to show legends (can be overridden per-layer) */
18
+ showLegend?: boolean;
19
+ /** Unique name for the radio button group */
20
+ groupName?: string;
21
+ /** Accessible label for the group */
22
+ groupLabel?: string;
23
+ };
24
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
25
+ setOpacity: (layerId: string, opacity: number) => any;
26
+ selectLayer: (layerId: string, previousLayerIds: string[]) => any;
27
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
28
+ onSetOpacity?: ((layerId: string, opacity: number) => any) | undefined;
29
+ onSelectLayer?: ((layerId: string, previousLayerIds: string[]) => any) | undefined;
30
+ }>, {
31
+ layerOpacities: Record<string, number>;
32
+ layerErrors: Record<string, string>;
33
+ currentZoom: number;
34
+ showOpacity: boolean;
35
+ showLegend: boolean;
36
+ loadingLayerIds: Set<string>;
37
+ groupLabel: string;
38
+ groupName: string;
39
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
40
+ export default _default;
@@ -0,0 +1,7 @@
1
+ type __VLS_Props = {
2
+ loading: boolean;
3
+ error: string | null;
4
+ unavailable: boolean;
5
+ };
6
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
7
+ export default _default;