@aiaiai-pt/design-system 0.18.0 → 0.19.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/components/FeedView.svelte +190 -0
- package/components/FeedView.svelte.d.ts +54 -0
- package/components/FilterBar.svelte +147 -58
- package/components/FilterBar.svelte.d.ts +56 -14
- package/components/MapCluster.svelte +84 -7
- package/components/index.d.ts +4 -0
- package/components/index.js +1 -0
- package/components/map-utils.d.ts +107 -0
- package/package.json +1 -1
- package/tokens/components.css +19 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component FeedView
|
|
3
|
+
|
|
4
|
+
A segmented view-switch for one feed (e.g. list ⇄ map). Presentational only:
|
|
5
|
+
it renders the available views as a WAI-ARIA tablist and emits the chosen
|
|
6
|
+
view via `onchange` — it does NOT render the views' content (the consumer
|
|
7
|
+
swaps that in response to the change, typically by re-resolving the feed).
|
|
8
|
+
|
|
9
|
+
Keyboard (automatic activation, per the tablist pattern for cheap-to-reveal
|
|
10
|
+
views): Arrow keys move focus AND select; Home/End jump to the ends; focus
|
|
11
|
+
roves (active tab is the only tab stop).
|
|
12
|
+
|
|
13
|
+
Consumes --feedview-* tokens from components.css.
|
|
14
|
+
|
|
15
|
+
@example
|
|
16
|
+
<FeedView
|
|
17
|
+
views={[
|
|
18
|
+
{ value: 'list', label: 'List' },
|
|
19
|
+
{ value: 'map', label: 'Map' },
|
|
20
|
+
]}
|
|
21
|
+
bind:value={activeView}
|
|
22
|
+
onchange={(v) => goto(`?view=${v}`)}
|
|
23
|
+
/>
|
|
24
|
+
|
|
25
|
+
@example With icons (icon is a Svelte component, e.g. phosphor-svelte)
|
|
26
|
+
<FeedView views={[
|
|
27
|
+
{ value: 'list', label: 'List', icon: ListIcon },
|
|
28
|
+
{ value: 'map', label: 'Map', icon: MapIcon },
|
|
29
|
+
]} bind:value={activeView} onchange={onChange} />
|
|
30
|
+
-->
|
|
31
|
+
<script module>
|
|
32
|
+
let _feedviewUid = 0;
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {{ value: string, label: string, icon?: import('svelte').Component }} FeedViewOption
|
|
35
|
+
*/
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<script>
|
|
39
|
+
let {
|
|
40
|
+
/** @type {FeedViewOption[]} The available views (first = default). */
|
|
41
|
+
views = [],
|
|
42
|
+
/** @type {string} The active view's value (bindable). */
|
|
43
|
+
value = $bindable(''),
|
|
44
|
+
/** @type {((value: string) => void) | undefined} Fires when the view changes. */
|
|
45
|
+
onchange = undefined,
|
|
46
|
+
/** @type {string} Accessible name for the tablist. */
|
|
47
|
+
label = 'View',
|
|
48
|
+
/** @type {string} */
|
|
49
|
+
class: className = '',
|
|
50
|
+
...rest
|
|
51
|
+
} = $props();
|
|
52
|
+
|
|
53
|
+
const uid = `feedview-${_feedviewUid++}`;
|
|
54
|
+
/** @type {HTMLButtonElement[]} */
|
|
55
|
+
let tabs = $state([]);
|
|
56
|
+
|
|
57
|
+
const activeIndex = $derived(Math.max(0, views.findIndex((v) => v.value === value)));
|
|
58
|
+
|
|
59
|
+
/** @param {string} v */
|
|
60
|
+
function select(v) {
|
|
61
|
+
if (v === value) return;
|
|
62
|
+
value = v;
|
|
63
|
+
onchange?.(v);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {number} index
|
|
68
|
+
* @param {boolean} focus
|
|
69
|
+
*/
|
|
70
|
+
function activate(index, focus) {
|
|
71
|
+
const view = views[index];
|
|
72
|
+
if (!view) return;
|
|
73
|
+
if (focus) tabs[index]?.focus();
|
|
74
|
+
select(view.value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @param {KeyboardEvent} event */
|
|
78
|
+
function onKeydown(event) {
|
|
79
|
+
const last = views.length - 1;
|
|
80
|
+
let next = -1;
|
|
81
|
+
switch (event.key) {
|
|
82
|
+
case 'ArrowRight':
|
|
83
|
+
case 'ArrowDown':
|
|
84
|
+
next = activeIndex >= last ? 0 : activeIndex + 1;
|
|
85
|
+
break;
|
|
86
|
+
case 'ArrowLeft':
|
|
87
|
+
case 'ArrowUp':
|
|
88
|
+
next = activeIndex <= 0 ? last : activeIndex - 1;
|
|
89
|
+
break;
|
|
90
|
+
case 'Home':
|
|
91
|
+
next = 0;
|
|
92
|
+
break;
|
|
93
|
+
case 'End':
|
|
94
|
+
next = last;
|
|
95
|
+
break;
|
|
96
|
+
default:
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
activate(next, true);
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<div
|
|
105
|
+
class="feedview {className}"
|
|
106
|
+
role="tablist"
|
|
107
|
+
aria-label={label}
|
|
108
|
+
onkeydown={onKeydown}
|
|
109
|
+
{...rest}
|
|
110
|
+
>
|
|
111
|
+
{#each views as view, i (view.value)}
|
|
112
|
+
{@const Icon = view.icon}
|
|
113
|
+
{@const isActive = view.value === value}
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
role="tab"
|
|
117
|
+
id={`${uid}-${view.value}`}
|
|
118
|
+
aria-selected={isActive}
|
|
119
|
+
tabindex={isActive ? 0 : -1}
|
|
120
|
+
class="feedview-tab"
|
|
121
|
+
class:feedview-tab-active={isActive}
|
|
122
|
+
bind:this={tabs[i]}
|
|
123
|
+
onclick={() => activate(i, false)}
|
|
124
|
+
>
|
|
125
|
+
{#if Icon}<span class="feedview-icon" aria-hidden="true"><Icon /></span>{/if}
|
|
126
|
+
<span class="feedview-label">{view.label}</span>
|
|
127
|
+
</button>
|
|
128
|
+
{/each}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<style>
|
|
132
|
+
.feedview {
|
|
133
|
+
display: inline-flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: var(--space-2xs);
|
|
136
|
+
background: var(--feedview-bg);
|
|
137
|
+
border-radius: var(--feedview-radius);
|
|
138
|
+
padding: var(--feedview-padding);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.feedview-tab {
|
|
142
|
+
all: unset;
|
|
143
|
+
box-sizing: border-box;
|
|
144
|
+
display: inline-flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
justify-content: center;
|
|
147
|
+
gap: var(--space-xs);
|
|
148
|
+
height: var(--feedview-tab-height);
|
|
149
|
+
padding: 0 var(--feedview-tab-padding-x);
|
|
150
|
+
border-radius: var(--feedview-tab-radius);
|
|
151
|
+
font-family: var(--feedview-tab-font);
|
|
152
|
+
font-size: var(--feedview-tab-size);
|
|
153
|
+
letter-spacing: var(--feedview-tab-tracking);
|
|
154
|
+
color: var(--feedview-tab-color);
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
white-space: nowrap;
|
|
157
|
+
transition: all var(--feedview-tab-transition);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.feedview-tab:hover {
|
|
161
|
+
color: var(--color-text);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.feedview-tab:focus-visible {
|
|
165
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
166
|
+
outline-offset: var(--focus-ring-offset);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.feedview-tab-active {
|
|
170
|
+
color: var(--feedview-tab-color-active);
|
|
171
|
+
background: var(--feedview-tab-bg-active);
|
|
172
|
+
box-shadow: var(--feedview-tab-shadow-active);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.feedview-icon {
|
|
176
|
+
display: inline-flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.feedview-icon :global(svg) {
|
|
181
|
+
width: var(--icon-size-sm);
|
|
182
|
+
height: var(--icon-size-sm);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@media (prefers-reduced-motion: reduce) {
|
|
186
|
+
.feedview-tab {
|
|
187
|
+
transition: none;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
</style>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export default FeedView;
|
|
2
|
+
export type FeedViewOption = {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: import("svelte").Component;
|
|
6
|
+
};
|
|
7
|
+
type FeedView = {
|
|
8
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
9
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* FeedView
|
|
13
|
+
*
|
|
14
|
+
* A segmented view-switch for one feed (e.g. list ⇄ map). Presentational only:
|
|
15
|
+
* it renders the available views as a WAI-ARIA tablist and emits the chosen
|
|
16
|
+
* view via `onchange` — it does NOT render the views' content (the consumer
|
|
17
|
+
* swaps that in response to the change, typically by re-resolving the feed).
|
|
18
|
+
*
|
|
19
|
+
* Keyboard (automatic activation, per the tablist pattern for cheap-to-reveal
|
|
20
|
+
* views): Arrow keys move focus AND select; Home/End jump to the ends; focus
|
|
21
|
+
* roves (active tab is the only tab stop).
|
|
22
|
+
*
|
|
23
|
+
* Consumes --feedview-* tokens from components.css.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <FeedView
|
|
27
|
+
* views={[
|
|
28
|
+
* { value: 'list', label: 'List' },
|
|
29
|
+
* { value: 'map', label: 'Map' },
|
|
30
|
+
* ]}
|
|
31
|
+
* bind:value={activeView}
|
|
32
|
+
* onchange={(v) => goto(`?view=${v}`)}
|
|
33
|
+
* />
|
|
34
|
+
*
|
|
35
|
+
* @example With icons (icon is a Svelte component, e.g. phosphor-svelte)
|
|
36
|
+
* <FeedView views={[
|
|
37
|
+
* { value: 'list', label: 'List', icon: ListIcon },
|
|
38
|
+
* { value: 'map', label: 'Map', icon: MapIcon },
|
|
39
|
+
* ]} bind:value={activeView} onchange={onChange} />
|
|
40
|
+
*/
|
|
41
|
+
declare const FeedView: import("svelte").Component<{
|
|
42
|
+
views?: any[];
|
|
43
|
+
value?: string;
|
|
44
|
+
onchange?: any;
|
|
45
|
+
label?: string;
|
|
46
|
+
class?: string;
|
|
47
|
+
} & Record<string, any>, {}, "value">;
|
|
48
|
+
type $$ComponentProps = {
|
|
49
|
+
views?: any[];
|
|
50
|
+
value?: string;
|
|
51
|
+
onchange?: any;
|
|
52
|
+
label?: string;
|
|
53
|
+
class?: string;
|
|
54
|
+
} & Record<string, any>;
|
|
@@ -1,49 +1,90 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component FilterBar
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
Accepts a declarative config of filters or renders children directly.
|
|
6
|
-
Consumes --filter-chip-* and --filter-bar-* tokens from components.css.
|
|
4
|
+
Filter controls over a feed. Two modes:
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
• **Facet mode** (set `facets` and/or `searchable`) — a row of labelled
|
|
7
|
+
dropdowns + an optional search box, the shape a browse/list/map surface
|
|
8
|
+
uses for its declared facets. Options are supplied by the consumer (it
|
|
9
|
+
owns the data source); FilterBar is presentational. A misconfigured /
|
|
10
|
+
undeclared facet is surfaced LOUDLY via `error` — never a silent dead
|
|
11
|
+
control. Consumes --filter-bar-* tokens.
|
|
12
|
+
|
|
13
|
+
• **Chip mode** (default — `filters` and/or `children`) — horizontal bar of
|
|
14
|
+
filter chips with active-filter state and clear-all. Consumes
|
|
15
|
+
--filter-chip-* and --filter-bar-* tokens.
|
|
16
|
+
|
|
17
|
+
@example Facet mode (browse/list/map)
|
|
18
|
+
<FilterBar
|
|
19
|
+
facets={[
|
|
20
|
+
{ key: 'status', label: 'Status', value: '', options: [
|
|
21
|
+
{ value: '', label: 'All' },
|
|
22
|
+
{ value: 'open', label: 'Open' },
|
|
23
|
+
]},
|
|
24
|
+
]}
|
|
25
|
+
searchable
|
|
26
|
+
searchPlaceholder="Search"
|
|
27
|
+
onfacetchange={(key, value) => refetch(key, value)}
|
|
28
|
+
onsearch={(term) => refetch('search', term)}
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
@example Chip mode (declarative)
|
|
9
32
|
<FilterBar
|
|
10
33
|
filters={[
|
|
11
34
|
{ key: 'status', label: 'Status', type: 'toggle', options: [
|
|
12
35
|
{ value: 'active', label: 'Active' },
|
|
13
36
|
{ value: 'draft', label: 'Draft' },
|
|
14
|
-
{ value: 'error', label: 'Error' },
|
|
15
37
|
]},
|
|
16
38
|
]}
|
|
17
39
|
bind:value={activeFilters}
|
|
18
40
|
onchange={handleFilterChange}
|
|
19
41
|
/>
|
|
20
|
-
|
|
21
|
-
@example Composable (custom rendering)
|
|
22
|
-
<FilterBar bind:value={activeFilters} onchange={handleFilterChange}>
|
|
23
|
-
{#snippet children(toggle, isActive)}
|
|
24
|
-
<FilterBar.Chip active={isActive('status', 'active')} onclick={() => toggle('status', 'active')}>
|
|
25
|
-
Active
|
|
26
|
-
</FilterBar.Chip>
|
|
27
|
-
{/snippet}
|
|
28
|
-
</FilterBar>
|
|
29
42
|
-->
|
|
30
43
|
<script module>
|
|
31
44
|
/**
|
|
32
45
|
* @typedef {{ value: string, label: string, icon?: import('svelte').Component }} FilterOption
|
|
33
46
|
* @typedef {{ key: string, label: string, type: 'toggle' | 'select' | 'multi-select', options?: FilterOption[], defaultValue?: any }} FilterDef
|
|
47
|
+
* @typedef {{ value: string, label: string, disabled?: boolean }} FacetOption
|
|
48
|
+
* @typedef {{ key: string, label: string, value?: string, options: FacetOption[] }} FacetControl
|
|
34
49
|
*/
|
|
35
50
|
</script>
|
|
36
51
|
|
|
37
52
|
<script>
|
|
53
|
+
import Select from './Select.svelte';
|
|
54
|
+
import SearchInput from './SearchInput.svelte';
|
|
55
|
+
import Alert from './Alert.svelte';
|
|
56
|
+
|
|
38
57
|
let {
|
|
39
|
-
|
|
58
|
+
// ─── Facet mode ───
|
|
59
|
+
/** @type {FacetControl[]} Labelled dropdown controls (facet mode). */
|
|
60
|
+
facets = [],
|
|
61
|
+
/** @type {boolean} Render a search box (facet mode). */
|
|
62
|
+
searchable = false,
|
|
63
|
+
/** @type {string} */
|
|
64
|
+
searchPlaceholder = 'Search',
|
|
65
|
+
/** @type {string} */
|
|
66
|
+
searchValue = '',
|
|
67
|
+
/** @type {boolean} Spinner on the search box / aria-busy on the bar. */
|
|
68
|
+
busy = false,
|
|
69
|
+
/** @type {string | undefined} Loud-fail message for misconfigured facets. */
|
|
70
|
+
error = undefined,
|
|
71
|
+
/** @type {((key: string, value: string) => void) | undefined} */
|
|
72
|
+
onfacetchange = undefined,
|
|
73
|
+
/** @type {((value: string) => void) | undefined} */
|
|
74
|
+
onsearch = undefined,
|
|
75
|
+
/** @type {(() => void) | undefined} Fires when the search box is cleared. */
|
|
76
|
+
onsearchclear = undefined,
|
|
77
|
+
|
|
78
|
+
// ─── Chip mode ───
|
|
79
|
+
/** @type {FilterDef[]} Declarative chip definitions */
|
|
40
80
|
filters = [],
|
|
41
|
-
/** @type {Record<string, any>} Active
|
|
81
|
+
/** @type {Record<string, any>} Active chip values (bindable) */
|
|
42
82
|
value = $bindable({}),
|
|
43
|
-
/** @type {((value: Record<string, any>) => void) | undefined} Fires when any
|
|
83
|
+
/** @type {((value: Record<string, any>) => void) | undefined} Fires when any chip changes */
|
|
44
84
|
onchange = undefined,
|
|
45
85
|
/** @type {(() => void) | undefined} Fires when "Clear all" is clicked */
|
|
46
86
|
onclear = undefined,
|
|
87
|
+
|
|
47
88
|
/** @type {string} */
|
|
48
89
|
class: className = '',
|
|
49
90
|
/** @type {import('svelte').Snippet | undefined} */
|
|
@@ -51,6 +92,10 @@
|
|
|
51
92
|
...rest
|
|
52
93
|
} = $props();
|
|
53
94
|
|
|
95
|
+
// Facet mode wins whenever facet controls or search are configured; chip
|
|
96
|
+
// mode is the default (back-compat with the original FilterBar contract).
|
|
97
|
+
const isFacetMode = $derived(facets.length > 0 || searchable);
|
|
98
|
+
|
|
54
99
|
const activeCount = $derived(
|
|
55
100
|
Object.values(value).filter(v => {
|
|
56
101
|
if (Array.isArray(v)) return v.length > 0;
|
|
@@ -61,7 +106,7 @@
|
|
|
61
106
|
const showClearAll = $derived(activeCount >= 2);
|
|
62
107
|
|
|
63
108
|
/**
|
|
64
|
-
* Toggle a filter value
|
|
109
|
+
* Toggle a filter value (chip mode)
|
|
65
110
|
* @param {string} key
|
|
66
111
|
* @param {string} optionValue
|
|
67
112
|
*/
|
|
@@ -90,7 +135,7 @@
|
|
|
90
135
|
}
|
|
91
136
|
|
|
92
137
|
/**
|
|
93
|
-
* Check if a filter option is active
|
|
138
|
+
* Check if a filter option is active (chip mode)
|
|
94
139
|
* @param {string} key
|
|
95
140
|
* @param {string} optionValue
|
|
96
141
|
* @returns {boolean}
|
|
@@ -106,48 +151,82 @@
|
|
|
106
151
|
onchange?.({});
|
|
107
152
|
onclear?.();
|
|
108
153
|
}
|
|
109
|
-
|
|
110
154
|
</script>
|
|
111
155
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
{@render children(toggle, isActive)}
|
|
120
|
-
{:else}
|
|
121
|
-
<div class="filter-bar-chips">
|
|
122
|
-
{#each filters as filter}
|
|
123
|
-
{#if filter.options}
|
|
124
|
-
{#each filter.options as option}
|
|
125
|
-
{@const active = isActive(filter.key, option.value)}
|
|
126
|
-
<button
|
|
127
|
-
class="filter-chip"
|
|
128
|
-
class:filter-chip-active={active}
|
|
129
|
-
onclick={() => toggle(filter.key, option.value)}
|
|
130
|
-
aria-pressed={active}
|
|
131
|
-
type="button"
|
|
132
|
-
>
|
|
133
|
-
{option.label}
|
|
134
|
-
</button>
|
|
135
|
-
{/each}
|
|
136
|
-
{/if}
|
|
137
|
-
{/each}
|
|
138
|
-
</div>
|
|
139
|
-
|
|
140
|
-
{#if showClearAll}
|
|
141
|
-
<button
|
|
142
|
-
class="filter-bar-clear"
|
|
143
|
-
onclick={clearAll}
|
|
144
|
-
type="button"
|
|
145
|
-
>
|
|
146
|
-
Clear all
|
|
147
|
-
</button>
|
|
148
|
-
{/if}
|
|
156
|
+
{#if isFacetMode}
|
|
157
|
+
<!-- Loud-fail (never a silent dead control): a configured facet the surface
|
|
158
|
+
does not declare can't filter — surface it as an operator fix. -->
|
|
159
|
+
{#if error}
|
|
160
|
+
<Alert variant="error" data-testid="filter-bar-error" class="filter-bar-alert">
|
|
161
|
+
{error}
|
|
162
|
+
</Alert>
|
|
149
163
|
{/if}
|
|
150
|
-
|
|
164
|
+
<div
|
|
165
|
+
class="filter-bar filter-bar-facets {className}"
|
|
166
|
+
role="group"
|
|
167
|
+
aria-label="Filters"
|
|
168
|
+
aria-busy={busy}
|
|
169
|
+
{...rest}
|
|
170
|
+
>
|
|
171
|
+
{#if searchable}
|
|
172
|
+
<SearchInput
|
|
173
|
+
placeholder={searchPlaceholder}
|
|
174
|
+
value={searchValue}
|
|
175
|
+
loading={busy}
|
|
176
|
+
onsearch={(v) => onsearch?.((v ?? '').trim())}
|
|
177
|
+
onclear={() => onsearchclear?.()}
|
|
178
|
+
/>
|
|
179
|
+
{/if}
|
|
180
|
+
{#each facets as facet (facet.key)}
|
|
181
|
+
<Select
|
|
182
|
+
label={facet.label}
|
|
183
|
+
options={facet.options}
|
|
184
|
+
value={facet.value ?? ''}
|
|
185
|
+
onchange={(v) => onfacetchange?.(facet.key, v)}
|
|
186
|
+
/>
|
|
187
|
+
{/each}
|
|
188
|
+
</div>
|
|
189
|
+
{:else}
|
|
190
|
+
<div
|
|
191
|
+
class="filter-bar {className}"
|
|
192
|
+
role="group"
|
|
193
|
+
aria-label="Filters"
|
|
194
|
+
{...rest}
|
|
195
|
+
>
|
|
196
|
+
{#if children}
|
|
197
|
+
{@render children(toggle, isActive)}
|
|
198
|
+
{:else}
|
|
199
|
+
<div class="filter-bar-chips">
|
|
200
|
+
{#each filters as filter}
|
|
201
|
+
{#if filter.options}
|
|
202
|
+
{#each filter.options as option}
|
|
203
|
+
{@const active = isActive(filter.key, option.value)}
|
|
204
|
+
<button
|
|
205
|
+
class="filter-chip"
|
|
206
|
+
class:filter-chip-active={active}
|
|
207
|
+
onclick={() => toggle(filter.key, option.value)}
|
|
208
|
+
aria-pressed={active}
|
|
209
|
+
type="button"
|
|
210
|
+
>
|
|
211
|
+
{option.label}
|
|
212
|
+
</button>
|
|
213
|
+
{/each}
|
|
214
|
+
{/if}
|
|
215
|
+
{/each}
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{#if showClearAll}
|
|
219
|
+
<button
|
|
220
|
+
class="filter-bar-clear"
|
|
221
|
+
onclick={clearAll}
|
|
222
|
+
type="button"
|
|
223
|
+
>
|
|
224
|
+
Clear all
|
|
225
|
+
</button>
|
|
226
|
+
{/if}
|
|
227
|
+
{/if}
|
|
228
|
+
</div>
|
|
229
|
+
{/if}
|
|
151
230
|
|
|
152
231
|
<style>
|
|
153
232
|
.filter-bar {
|
|
@@ -157,6 +236,16 @@
|
|
|
157
236
|
flex-wrap: wrap;
|
|
158
237
|
}
|
|
159
238
|
|
|
239
|
+
/* Facet mode: labelled dropdowns + search align to their baselines. */
|
|
240
|
+
.filter-bar-facets {
|
|
241
|
+
align-items: flex-end;
|
|
242
|
+
gap: var(--space-sm);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.filter-bar-alert {
|
|
246
|
+
margin-bottom: var(--space-sm);
|
|
247
|
+
}
|
|
248
|
+
|
|
160
249
|
.filter-bar-chips {
|
|
161
250
|
display: flex;
|
|
162
251
|
align-items: center;
|
|
@@ -11,6 +11,17 @@ export type FilterDef = {
|
|
|
11
11
|
options?: FilterOption[];
|
|
12
12
|
defaultValue?: any;
|
|
13
13
|
};
|
|
14
|
+
export type FacetOption = {
|
|
15
|
+
value: string;
|
|
16
|
+
label: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type FacetControl = {
|
|
20
|
+
key: string;
|
|
21
|
+
label: string;
|
|
22
|
+
value?: string;
|
|
23
|
+
options: FacetOption[];
|
|
24
|
+
};
|
|
14
25
|
type FilterBar = {
|
|
15
26
|
$on?(type: string, callback: (e: any) => void): () => void;
|
|
16
27
|
$set?(props: Partial<$$ComponentProps>): void;
|
|
@@ -18,33 +29,55 @@ type FilterBar = {
|
|
|
18
29
|
/**
|
|
19
30
|
* FilterBar
|
|
20
31
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
32
|
+
* Filter controls over a feed. Two modes:
|
|
33
|
+
*
|
|
34
|
+
* • **Facet mode** (set `facets` and/or `searchable`) — a row of labelled
|
|
35
|
+
* dropdowns + an optional search box, the shape a browse/list/map surface
|
|
36
|
+
* uses for its declared facets. Options are supplied by the consumer (it
|
|
37
|
+
* owns the data source); FilterBar is presentational. A misconfigured /
|
|
38
|
+
* undeclared facet is surfaced LOUDLY via `error` — never a silent dead
|
|
39
|
+
* control. Consumes --filter-bar-* tokens.
|
|
24
40
|
*
|
|
25
|
-
*
|
|
41
|
+
* • **Chip mode** (default — `filters` and/or `children`) — horizontal bar of
|
|
42
|
+
* filter chips with active-filter state and clear-all. Consumes
|
|
43
|
+
* --filter-chip-* and --filter-bar-* tokens.
|
|
44
|
+
*
|
|
45
|
+
* @example Facet mode (browse/list/map)
|
|
46
|
+
* <FilterBar
|
|
47
|
+
* facets={[
|
|
48
|
+
* { key: 'status', label: 'Status', value: '', options: [
|
|
49
|
+
* { value: '', label: 'All' },
|
|
50
|
+
* { value: 'open', label: 'Open' },
|
|
51
|
+
* ]},
|
|
52
|
+
* ]}
|
|
53
|
+
* searchable
|
|
54
|
+
* searchPlaceholder="Search"
|
|
55
|
+
* onfacetchange={(key, value) => refetch(key, value)}
|
|
56
|
+
* onsearch={(term) => refetch('search', term)}
|
|
57
|
+
* />
|
|
58
|
+
*
|
|
59
|
+
* @example Chip mode (declarative)
|
|
26
60
|
* <FilterBar
|
|
27
61
|
* filters={[
|
|
28
62
|
* { key: 'status', label: 'Status', type: 'toggle', options: [
|
|
29
63
|
* { value: 'active', label: 'Active' },
|
|
30
64
|
* { value: 'draft', label: 'Draft' },
|
|
31
|
-
* { value: 'error', label: 'Error' },
|
|
32
65
|
* ]},
|
|
33
66
|
* ]}
|
|
34
67
|
* bind:value={activeFilters}
|
|
35
68
|
* onchange={handleFilterChange}
|
|
36
69
|
* />
|
|
37
|
-
*
|
|
38
|
-
* @example Composable (custom rendering)
|
|
39
|
-
* <FilterBar bind:value={activeFilters} onchange={handleFilterChange}>
|
|
40
|
-
* {#snippet children(toggle, isActive)}
|
|
41
|
-
* <FilterBar.Chip active={isActive('status', 'active')} onclick={() => toggle('status', 'active')}>
|
|
42
|
-
* Active
|
|
43
|
-
* </FilterBar.Chip>
|
|
44
|
-
* {/snippet}
|
|
45
|
-
* </FilterBar>
|
|
46
70
|
*/
|
|
47
71
|
declare const FilterBar: import("svelte").Component<{
|
|
72
|
+
facets?: any[];
|
|
73
|
+
searchable?: boolean;
|
|
74
|
+
searchPlaceholder?: string;
|
|
75
|
+
searchValue?: string;
|
|
76
|
+
busy?: boolean;
|
|
77
|
+
error?: any;
|
|
78
|
+
onfacetchange?: any;
|
|
79
|
+
onsearch?: any;
|
|
80
|
+
onsearchclear?: any;
|
|
48
81
|
filters?: any[];
|
|
49
82
|
value?: Record<string, any>;
|
|
50
83
|
onchange?: any;
|
|
@@ -53,6 +86,15 @@ declare const FilterBar: import("svelte").Component<{
|
|
|
53
86
|
children?: any;
|
|
54
87
|
} & Record<string, any>, {}, "value">;
|
|
55
88
|
type $$ComponentProps = {
|
|
89
|
+
facets?: any[];
|
|
90
|
+
searchable?: boolean;
|
|
91
|
+
searchPlaceholder?: string;
|
|
92
|
+
searchValue?: string;
|
|
93
|
+
busy?: boolean;
|
|
94
|
+
error?: any;
|
|
95
|
+
onfacetchange?: any;
|
|
96
|
+
onsearch?: any;
|
|
97
|
+
onsearchclear?: any;
|
|
56
98
|
filters?: any[];
|
|
57
99
|
value?: Record<string, any>;
|
|
58
100
|
onchange?: any;
|
|
@@ -39,6 +39,14 @@
|
|
|
39
39
|
layers = [],
|
|
40
40
|
/** @type {((marker: { id: string, lon: number, lat: number, label?: string }) => void) | undefined} */
|
|
41
41
|
onclick = undefined,
|
|
42
|
+
/** @type {import('svelte').Snippet<[any, () => void]> | undefined} —
|
|
43
|
+
* click-popup content. When provided, clicking a single marker (or a
|
|
44
|
+
* stacked pile) opens an anchored OL Overlay rendering this snippet with
|
|
45
|
+
* `(markerData, close)` — the "ficha resumo" intermediate step before the
|
|
46
|
+
* consumer's detail link — INSTEAD of firing `onclick`. Omit for the
|
|
47
|
+
* legacy click-to-navigate behaviour. Compose with the DS `MapPopup` for
|
|
48
|
+
* the card chrome (the original `let:popup` contract, now a snippet). */
|
|
49
|
+
popup = undefined,
|
|
42
50
|
/** @type {string} */
|
|
43
51
|
height = '100%',
|
|
44
52
|
/** @type {string} */
|
|
@@ -48,6 +56,18 @@
|
|
|
48
56
|
|
|
49
57
|
/** @type {HTMLElement | undefined} */
|
|
50
58
|
let container = $state();
|
|
59
|
+
/** @type {HTMLElement | undefined} — the popup overlay's element (Svelte
|
|
60
|
+
* renders the snippet into it; OL positions it). */
|
|
61
|
+
let popupEl = $state();
|
|
62
|
+
/** @type {any} — the marker data of the open popup, or null. */
|
|
63
|
+
let selected = $state(null);
|
|
64
|
+
/** @type {any} — popup OL Overlay ref, set at mount. */
|
|
65
|
+
let _popupOverlay;
|
|
66
|
+
|
|
67
|
+
function closePopup() {
|
|
68
|
+
selected = null;
|
|
69
|
+
_popupOverlay?.setPosition(undefined);
|
|
70
|
+
}
|
|
51
71
|
|
|
52
72
|
// Hoisted references for reactive effects
|
|
53
73
|
/** @type {import('ol/Map.js').default | undefined} */
|
|
@@ -211,6 +231,22 @@
|
|
|
211
231
|
stopEvent: false,
|
|
212
232
|
});
|
|
213
233
|
|
|
234
|
+
// Click-popup overlay (the "ficha resumo"): anchored above the marker.
|
|
235
|
+
// `stopEvent: true` (OL default) keeps it in the interactive overlay
|
|
236
|
+
// pane so the link/close button inside the popup snippet are clickable.
|
|
237
|
+
// Its element is the Svelte-rendered `popupEl`; Svelte owns the content,
|
|
238
|
+
// OL owns the position. Only created when a `popup` snippet is supplied.
|
|
239
|
+
const overlays = [tooltipOverlay];
|
|
240
|
+
if (popup && popupEl) {
|
|
241
|
+
_popupOverlay = new Overlay({
|
|
242
|
+
element: popupEl,
|
|
243
|
+
positioning: 'bottom-center',
|
|
244
|
+
offset: [0, -18],
|
|
245
|
+
stopEvent: true,
|
|
246
|
+
});
|
|
247
|
+
overlays.push(_popupOverlay);
|
|
248
|
+
}
|
|
249
|
+
|
|
214
250
|
const viewCenter = center
|
|
215
251
|
? fromLonLat(center)
|
|
216
252
|
: markers.length > 0
|
|
@@ -225,7 +261,7 @@
|
|
|
225
261
|
// The `layers` overlays are NOT built here — the reactive owner
|
|
226
262
|
// effect above inserts them between the tiles and the cluster layer.
|
|
227
263
|
layers: [tileLayer, clusterLayer],
|
|
228
|
-
overlays
|
|
264
|
+
overlays,
|
|
229
265
|
view: new View({
|
|
230
266
|
center: viewCenter,
|
|
231
267
|
zoom,
|
|
@@ -267,16 +303,32 @@
|
|
|
267
303
|
}
|
|
268
304
|
});
|
|
269
305
|
|
|
306
|
+
// `popup` opens the anchored ficha-resumo overlay; otherwise `onclick`
|
|
307
|
+
// navigates (legacy). A single marker (or a stacked pile that can't split
|
|
308
|
+
// by zoom) "selects"; a spread cluster zooms in.
|
|
309
|
+
const selectMarker = (data, coords) => {
|
|
310
|
+
if (!data) return;
|
|
311
|
+
if (popup) {
|
|
312
|
+
selected = data;
|
|
313
|
+
if (coords) _popupOverlay?.setPosition(coords);
|
|
314
|
+
} else {
|
|
315
|
+
onclick?.(data);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
270
319
|
// Click handler
|
|
271
|
-
if (onclick) {
|
|
320
|
+
if (onclick || popup) {
|
|
272
321
|
map.on('click', (evt) => {
|
|
273
322
|
const feature = map?.forEachFeatureAtPixel(evt.pixel, f => f);
|
|
274
|
-
|
|
323
|
+
// Clicking empty map dismisses an open popup (no-op without one).
|
|
324
|
+
if (!feature) { closePopup(); return; }
|
|
275
325
|
|
|
276
326
|
const clustered = feature.get('features');
|
|
277
327
|
if (clustered?.length === 1) {
|
|
278
|
-
|
|
279
|
-
|
|
328
|
+
selectMarker(
|
|
329
|
+
clustered[0].get('markerData'),
|
|
330
|
+
(/** @type {any} */ (feature.getGeometry()))?.getCoordinates(),
|
|
331
|
+
);
|
|
280
332
|
} else if (clustered?.length > 1) {
|
|
281
333
|
// A cluster of (near-)IDENTICAL coordinates can never split by
|
|
282
334
|
// zooming (stacked reports at one point are common in civic
|
|
@@ -286,8 +338,10 @@
|
|
|
286
338
|
const lats = coords.map((c) => c[1]);
|
|
287
339
|
const spread = Math.max(...lons) - Math.min(...lons) + (Math.max(...lats) - Math.min(...lats));
|
|
288
340
|
if (spread < 1) { // metres in web-mercator units — a stacked pile
|
|
289
|
-
|
|
290
|
-
|
|
341
|
+
selectMarker(
|
|
342
|
+
clustered[0].get('markerData'),
|
|
343
|
+
(/** @type {any} */ (feature.getGeometry()))?.getCoordinates(),
|
|
344
|
+
);
|
|
291
345
|
return;
|
|
292
346
|
}
|
|
293
347
|
const view = map?.getView();
|
|
@@ -307,9 +361,16 @@
|
|
|
307
361
|
});
|
|
308
362
|
} catch (err) { renderMapError(container, 'MapCluster', /** @type {Error} */ (err)); } })();
|
|
309
363
|
|
|
364
|
+
// Escape closes an open popup (keyboard dismissal).
|
|
365
|
+
const onKeydown = (/** @type {KeyboardEvent} */ e) => {
|
|
366
|
+
if (e.key === 'Escape') closePopup();
|
|
367
|
+
};
|
|
368
|
+
if (typeof window !== 'undefined') window.addEventListener('keydown', onKeydown);
|
|
369
|
+
|
|
310
370
|
return () => {
|
|
311
371
|
disposed = true;
|
|
312
372
|
disposeTheme?.();
|
|
373
|
+
if (typeof window !== 'undefined') window.removeEventListener('keydown', onKeydown);
|
|
313
374
|
map?.setTarget(undefined);
|
|
314
375
|
};
|
|
315
376
|
});
|
|
@@ -324,6 +385,15 @@
|
|
|
324
385
|
{...rest}
|
|
325
386
|
></div>
|
|
326
387
|
|
|
388
|
+
<!-- Popup overlay element. Always in the DOM (OL moves it into the overlay
|
|
389
|
+
pane + positions it); Svelte owns its content. The snippet receives the
|
|
390
|
+
selected marker + a `close` callback (the original `let:popup` contract). -->
|
|
391
|
+
{#if popup}
|
|
392
|
+
<div bind:this={popupEl} class="map-cluster-popup">
|
|
393
|
+
{#if selected}{@render popup(selected, closePopup)}{/if}
|
|
394
|
+
</div>
|
|
395
|
+
{/if}
|
|
396
|
+
|
|
327
397
|
<style>
|
|
328
398
|
.map-cluster {
|
|
329
399
|
width: 100%;
|
|
@@ -336,4 +406,11 @@
|
|
|
336
406
|
.map-cluster :global(.ol-viewport) {
|
|
337
407
|
border-radius: inherit;
|
|
338
408
|
}
|
|
409
|
+
|
|
410
|
+
/* Overlay element wrapper — OL owns its position (inline transform); this
|
|
411
|
+
just lifts it above the map controls. The card chrome is the consumer's
|
|
412
|
+
MapPopup. */
|
|
413
|
+
.map-cluster-popup {
|
|
414
|
+
z-index: 2;
|
|
415
|
+
}
|
|
339
416
|
</style>
|
package/components/index.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export { default as AppFrame } from "./AppFrame.svelte";
|
|
|
21
21
|
export { default as SiteHeader } from "./SiteHeader.svelte";
|
|
22
22
|
export { default as SiteFooter } from "./SiteFooter.svelte";
|
|
23
23
|
export { default as SkipLink } from "./SkipLink.svelte";
|
|
24
|
+
export { default as TextSizeAdjuster } from "./TextSizeAdjuster.svelte";
|
|
25
|
+
export { default as ContrastToggle } from "./ContrastToggle.svelte";
|
|
26
|
+
export { default as LinkHighlightToggle } from "./LinkHighlightToggle.svelte";
|
|
24
27
|
export { default as ServiceNavigation } from "./ServiceNavigation.svelte";
|
|
25
28
|
export { default as Link } from "./Link.svelte";
|
|
26
29
|
export { default as Hero } from "./Hero.svelte";
|
|
@@ -47,6 +50,7 @@ export { default as Tabs } from "./Tabs.svelte";
|
|
|
47
50
|
export { default as TabList } from "./TabList.svelte";
|
|
48
51
|
export { default as Tab } from "./Tab.svelte";
|
|
49
52
|
export { default as TabPanel } from "./TabPanel.svelte";
|
|
53
|
+
export { default as FeedView } from "./FeedView.svelte";
|
|
50
54
|
export { default as Alert } from "./Alert.svelte";
|
|
51
55
|
export { default as Toast } from "./Toast.svelte";
|
|
52
56
|
export { default as ToastManager } from "./ToastManager.svelte";
|
package/components/index.js
CHANGED
|
@@ -75,6 +75,7 @@ export { default as Tabs } from "./Tabs.svelte";
|
|
|
75
75
|
export { default as TabList } from "./TabList.svelte";
|
|
76
76
|
export { default as Tab } from "./Tab.svelte";
|
|
77
77
|
export { default as TabPanel } from "./TabPanel.svelte";
|
|
78
|
+
export { default as FeedView } from "./FeedView.svelte";
|
|
78
79
|
|
|
79
80
|
// Feedback
|
|
80
81
|
export { default as Alert } from "./Alert.svelte";
|
|
@@ -73,6 +73,77 @@ export function cssPx(el: Element, prop: string, fallback: number): number;
|
|
|
73
73
|
* @returns {string[]}
|
|
74
74
|
*/
|
|
75
75
|
export function getHeatmapGradient(el: Element): string[];
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {object} OverlayLayerStyle
|
|
78
|
+
* Flat style subset applied to a whole overlay layer. Colors are CSS color
|
|
79
|
+
* strings (they come from layer DATA — geolayers GeoStyler styles — not
|
|
80
|
+
* from design tokens; absent values fall back to the DS-tokened defaults).
|
|
81
|
+
* @property {string} [pointColor]
|
|
82
|
+
* @property {number} [pointRadius]
|
|
83
|
+
* @property {string} [strokeColor]
|
|
84
|
+
* @property {number} [strokeWidth]
|
|
85
|
+
* @property {string} [fillColor]
|
|
86
|
+
*
|
|
87
|
+
* @typedef {object} OverlayLayerDef
|
|
88
|
+
* One ordered overlay rendered between the tile layer and the component's
|
|
89
|
+
* own interactive vector layer. v1 is GeoJSON-only — the platform's citizen
|
|
90
|
+
* data plane serves GeoJSON (`/{app}/public/layers/{id}/features`); WMS/tile
|
|
91
|
+
* overlays arrive with the per-tenant raster principal (spec open question).
|
|
92
|
+
* @property {string} [id]
|
|
93
|
+
* @property {'geojson'} [type]
|
|
94
|
+
* @property {object} [data] — inline GeoJSON (Feature/FeatureCollection)
|
|
95
|
+
* @property {string} [url] — fetched when `data` is absent
|
|
96
|
+
* @property {OverlayLayerStyle | object} [style] — flat style or GeoStyler
|
|
97
|
+
* JSON (best-effort subset via `geoStylerToFlat`)
|
|
98
|
+
* @property {boolean} [visible]
|
|
99
|
+
*/
|
|
100
|
+
/**
|
|
101
|
+
* Best-effort GeoStyler → flat style. Reads the first symbolizer of the
|
|
102
|
+
* first rule — enough for the platform's single-rule layer styles. Unknown
|
|
103
|
+
* shapes return {} so the DS-tokened defaults apply.
|
|
104
|
+
*
|
|
105
|
+
* @param {any} style
|
|
106
|
+
* @returns {OverlayLayerStyle}
|
|
107
|
+
*/
|
|
108
|
+
export function geoStylerToFlat(style: any): OverlayLayerStyle;
|
|
109
|
+
/**
|
|
110
|
+
* Builds OL vector layers for `layers` overlay defs, ordered as given.
|
|
111
|
+
* URL-sourced layers load asynchronously into their source; a fetch failure
|
|
112
|
+
* leaves that overlay empty and logs a warning (the map still renders).
|
|
113
|
+
*
|
|
114
|
+
* @param {OverlayLayerDef[]} defs
|
|
115
|
+
* @param {import('./map-utils.js').MapStyles} styles — DS default styles
|
|
116
|
+
* @returns {Promise<import('ol/layer/Vector.js').default[]>}
|
|
117
|
+
*/
|
|
118
|
+
export function createOverlayLayers(defs: OverlayLayerDef[], styles: import("./map-utils.js").MapStyles): Promise<import("ol/layer/Vector.js").default[]>;
|
|
119
|
+
/**
|
|
120
|
+
* Normalises a boundary prop into polygon rings (lon/lat WGS84).
|
|
121
|
+
* Accepts: raw ring `number[][]`, GeoJSON Polygon / MultiPolygon,
|
|
122
|
+
* Feature, or FeatureCollection (first polygonal feature per entry).
|
|
123
|
+
*
|
|
124
|
+
* @param {any} boundary
|
|
125
|
+
* @returns {number[][][]} — array of outer rings (one per polygon)
|
|
126
|
+
*/
|
|
127
|
+
export function boundaryToRings(boundary: any): number[][][];
|
|
128
|
+
/**
|
|
129
|
+
* Ray-casting point-in-polygon over WGS84 rings (outer rings only — holes
|
|
130
|
+
* are out of scope for the boundary gate; the BFF hard-gate is authoritative).
|
|
131
|
+
*
|
|
132
|
+
* @param {[number, number]} lonLat
|
|
133
|
+
* @param {number[][][]} rings — from `boundaryToRings`
|
|
134
|
+
* @returns {boolean} true when the point is inside ANY ring
|
|
135
|
+
*/
|
|
136
|
+
export function pointInRings(lonLat: [number, number], rings: number[][][]): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Builds the boundary overlay layer (dashed outline, faint fill — distinct
|
|
139
|
+
* from the selection polygon so "the edge of the allowed area" reads as
|
|
140
|
+
* context, not content). Token-driven: --map-boundary-*.
|
|
141
|
+
*
|
|
142
|
+
* @param {number[][][]} rings
|
|
143
|
+
* @param {Element} el — token read context
|
|
144
|
+
* @returns {Promise<import('ol/layer/Vector.js').default | null>}
|
|
145
|
+
*/
|
|
146
|
+
export function createBoundaryLayer(rings: number[][][], el: Element): Promise<import("ol/layer/Vector.js").default | null>;
|
|
76
147
|
export type OsmSource = {
|
|
77
148
|
type: "osm";
|
|
78
149
|
};
|
|
@@ -98,3 +169,39 @@ export type MapStyles = {
|
|
|
98
169
|
*/
|
|
99
170
|
refresh: () => void;
|
|
100
171
|
};
|
|
172
|
+
/**
|
|
173
|
+
* Flat style subset applied to a whole overlay layer. Colors are CSS color
|
|
174
|
+
* strings (they come from layer DATA — geolayers GeoStyler styles — not
|
|
175
|
+
* from design tokens; absent values fall back to the DS-tokened defaults).
|
|
176
|
+
*/
|
|
177
|
+
export type OverlayLayerStyle = {
|
|
178
|
+
pointColor?: string;
|
|
179
|
+
pointRadius?: number;
|
|
180
|
+
strokeColor?: string;
|
|
181
|
+
strokeWidth?: number;
|
|
182
|
+
fillColor?: string;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* One ordered overlay rendered between the tile layer and the component's
|
|
186
|
+
* own interactive vector layer. v1 is GeoJSON-only — the platform's citizen
|
|
187
|
+
* data plane serves GeoJSON (`/{app}/public/layers/{id}/features`); WMS/tile
|
|
188
|
+
* overlays arrive with the per-tenant raster principal (spec open question).
|
|
189
|
+
*/
|
|
190
|
+
export type OverlayLayerDef = {
|
|
191
|
+
id?: string;
|
|
192
|
+
type?: "geojson";
|
|
193
|
+
/**
|
|
194
|
+
* — inline GeoJSON (Feature/FeatureCollection)
|
|
195
|
+
*/
|
|
196
|
+
data?: object;
|
|
197
|
+
/**
|
|
198
|
+
* — fetched when `data` is absent
|
|
199
|
+
*/
|
|
200
|
+
url?: string;
|
|
201
|
+
/**
|
|
202
|
+
* — flat style or GeoStyler
|
|
203
|
+
* JSON (best-effort subset via `geoStylerToFlat`)
|
|
204
|
+
*/
|
|
205
|
+
style?: OverlayLayerStyle | object;
|
|
206
|
+
visible?: boolean;
|
|
207
|
+
};
|
package/package.json
CHANGED
package/tokens/components.css
CHANGED
|
@@ -403,6 +403,25 @@
|
|
|
403
403
|
--tabs-trigger-transition: var(--duration-instant) var(--easing-default);
|
|
404
404
|
--tabs-content-padding-top: var(--space-md);
|
|
405
405
|
|
|
406
|
+
/* ═══════════════════════════════════════════════
|
|
407
|
+
FEEDVIEW (segmented view-switch — list ⇄ map)
|
|
408
|
+
═══════════════════════════════════════════════ */
|
|
409
|
+
|
|
410
|
+
--feedview-bg: var(--color-surface-secondary);
|
|
411
|
+
--feedview-radius: var(--radius-md);
|
|
412
|
+
--feedview-padding: var(--space-2xs);
|
|
413
|
+
--feedview-tab-font: var(--type-label-font);
|
|
414
|
+
--feedview-tab-size: var(--type-label-size);
|
|
415
|
+
--feedview-tab-tracking: var(--type-label-tracking);
|
|
416
|
+
--feedview-tab-height: 32px;
|
|
417
|
+
--feedview-tab-padding-x: var(--space-md);
|
|
418
|
+
--feedview-tab-radius: var(--radius-sm);
|
|
419
|
+
--feedview-tab-color: var(--color-text-secondary);
|
|
420
|
+
--feedview-tab-color-active: var(--color-text);
|
|
421
|
+
--feedview-tab-bg-active: var(--color-surface);
|
|
422
|
+
--feedview-tab-shadow-active: var(--elevation-raised);
|
|
423
|
+
--feedview-tab-transition: var(--duration-instant) var(--easing-default);
|
|
424
|
+
|
|
406
425
|
/* ═══════════════════════════════════════════════
|
|
407
426
|
MODAL (centered overlay)
|
|
408
427
|
═══════════════════════════════════════════════ */
|