Skip to content

Commit 916f79c

Browse files
committed
update
1 parent 3943d30 commit 916f79c

16 files changed

Lines changed: 273 additions & 40 deletions

.github/workflows/npm-publish.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: npm publish (dry run)
2+
3+
# Packages are intentionally not published by this workflow yet. Once the
4+
# generated package contents have been verified, remove --dry-run from the
5+
# publish command and update the workflow name.
6+
on:
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: npm-publish
14+
cancel-in-progress: false
15+
16+
jobs:
17+
publish-dry-run:
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 30
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v7
23+
with:
24+
submodules: recursive
25+
26+
- name: Setup Node
27+
uses: actions/setup-node@v6
28+
with:
29+
node-version: 22
30+
registry-url: https://registry.npmjs.org
31+
cache: npm
32+
33+
- name: Install dependencies
34+
run: npm ci --no-audit --no-fund
35+
36+
- name: Build publishable packages
37+
run: npm run build:packages
38+
39+
- name: Preview npm packages
40+
shell: bash
41+
run: |
42+
set -euo pipefail
43+
44+
mapfile -t packages < <(
45+
npm query .workspace --json | node -e '
46+
let input = "";
47+
process.stdin.on("data", chunk => input += chunk);
48+
process.stdin.on("end", () => {
49+
for (const workspace of JSON.parse(input)) {
50+
if (!workspace.private) console.log(workspace.name);
51+
}
52+
});
53+
'
54+
)
55+
56+
for package in "${packages[@]}"; do
57+
echo "::group::${package}"
58+
npm publish --workspace "${package}" --access public --dry-run
59+
echo "::endgroup::"
60+
done
61+
env:
62+
# npm checks authentication even for a publish dry run.
63+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

examples/basic/src/app/AppHeader.tsx

Lines changed: 114 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
1+
import { useEffect, useRef, useState } from 'react';
12
import type { SupportedLanguage } from '../sampleRegistry';
23
import { translate } from '../i18n';
34
import type { MapProvider } from './appRouting';
45

6+
const PROVIDERS: Array<{ value: MapProvider; label: string }> = [
7+
{ value: 'maplibre', label: 'MapLibre' },
8+
{ value: 'mapbox', label: 'Mapbox' },
9+
{ value: 'leaflet', label: 'Leaflet' },
10+
{ value: 'openlayers', label: 'OpenLayers' },
11+
{ value: 'arcgis', label: 'ArcGIS 2D' },
12+
{ value: 'arcgis-3d', label: 'ArcGIS 3D' },
13+
{ value: 'cesium', label: 'Cesium' },
14+
{ value: 'here', label: 'HERE' },
15+
{ value: 'google', label: 'Google Maps' },
16+
{ value: 'google-3d', label: 'Google Maps 3D' },
17+
];
18+
19+
const LANGUAGES: Array<{ value: SupportedLanguage; label: string; short: string }> = [
20+
{ value: 'en', label: 'English', short: 'EN' },
21+
{ value: 'ja', label: '日本語', short: 'JA' },
22+
{ value: 'es-419', label: 'Español (Latinoamérica)', short: 'ES' },
23+
];
24+
25+
type IconMenu = 'provider' | 'language' | null;
26+
527
export function AppHeader({ language, provider, showProviderSelector, onOpenMenu, onProviderChange, onLanguageChange }: {
628
language: SupportedLanguage;
729
provider: MapProvider | null;
@@ -10,29 +32,112 @@ export function AppHeader({ language, provider, showProviderSelector, onOpenMenu
1032
onProviderChange(provider: MapProvider): void;
1133
onLanguageChange(language: SupportedLanguage): void;
1234
}) {
35+
const [openMenu, setOpenMenu] = useState<IconMenu>(null);
36+
const controlsRef = useRef<HTMLDivElement>(null);
37+
38+
useEffect(() => {
39+
if (!openMenu) return;
40+
const handlePointer = (event: MouseEvent) => {
41+
if (controlsRef.current && !controlsRef.current.contains(event.target as Node)) {
42+
setOpenMenu(null);
43+
}
44+
};
45+
const handleEscape = (event: KeyboardEvent) => {
46+
if (event.key === 'Escape') setOpenMenu(null);
47+
};
48+
document.addEventListener('mousedown', handlePointer);
49+
document.addEventListener('keydown', handleEscape);
50+
return () => {
51+
document.removeEventListener('mousedown', handlePointer);
52+
document.removeEventListener('keydown', handleEscape);
53+
};
54+
}, [openMenu]);
55+
56+
const activeLanguageShort = LANGUAGES.find(l => l.value === language)?.short ?? 'EN';
57+
const activeProviderLabel = PROVIDERS.find(p => p.value === provider)?.label ?? 'Provider';
58+
1359
return (
1460
<header className="header">
1561
<h1>MapConductor React SDK Samples</h1>
16-
<div className="header-controls">
62+
<div className="header-controls" ref={controlsRef}>
1763
<button type="button" className="menu-button" aria-label="Open samples menu" onClick={onOpenMenu}>
1864
<span /><span /><span />
1965
</button>
2066
{showProviderSelector && <label className="provider-control"><span>Provider</span>
2167
<select value={provider ?? 'maplibre'} onChange={event => onProviderChange(event.target.value as MapProvider)}>
22-
<option value="maplibre">MapLibre</option><option value="mapbox">Mapbox</option>
23-
<option value="leaflet">Leaflet</option><option value="openlayers">OpenLayers</option>
24-
<option value="arcgis">ArcGIS 2D</option><option value="arcgis-3d">ArcGIS 3D</option>
25-
<option value="cesium">Cesium</option><option value="here">HERE</option>
26-
<option value="google">Google Maps</option>
27-
<option value="google-3d">Google Maps 3D</option>
68+
{PROVIDERS.map(p => <option key={p.value} value={p.value}>{p.label}</option>)}
2869
</select>
2970
</label>}
71+
{showProviderSelector && (
72+
<div className="header-icon-control">
73+
<button
74+
type="button"
75+
className="header-icon-button"
76+
aria-label={`Provider: ${activeProviderLabel}`}
77+
aria-expanded={openMenu === 'provider'}
78+
onClick={() => setOpenMenu(openMenu === 'provider' ? null : 'provider')}
79+
>
80+
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
81+
<polygon points="12 2 2 7 12 12 22 7 12 2" />
82+
<polyline points="2 17 12 22 22 17" />
83+
<polyline points="2 12 12 17 22 12" />
84+
</svg>
85+
</button>
86+
{openMenu === 'provider' && (
87+
<div className="header-icon-menu" role="menu">
88+
{PROVIDERS.map(p => (
89+
<button
90+
key={p.value}
91+
type="button"
92+
role="menuitemradio"
93+
aria-checked={provider === p.value}
94+
className={`header-icon-menu-item${provider === p.value ? ' active' : ''}`}
95+
onClick={() => { onProviderChange(p.value); setOpenMenu(null); }}
96+
>
97+
{p.label}
98+
</button>
99+
))}
100+
</div>
101+
)}
102+
</div>
103+
)}
30104
<label className="language-control"><span>{translate(language, 'Language', '言語', 'Idioma')}</span>
31105
<select value={language} onChange={event => onLanguageChange(event.target.value as SupportedLanguage)}>
32-
<option value="en">English</option><option value="ja">日本語</option>
33-
<option value="es-419">Español (Latinoamérica)</option>
106+
{LANGUAGES.map(l => <option key={l.value} value={l.value}>{l.label}</option>)}
34107
</select>
35108
</label>
109+
<div className="header-icon-control">
110+
<button
111+
type="button"
112+
className="header-icon-button header-language-icon-button"
113+
aria-label={`Language: ${activeLanguageShort}`}
114+
aria-expanded={openMenu === 'language'}
115+
onClick={() => setOpenMenu(openMenu === 'language' ? null : 'language')}
116+
>
117+
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
118+
<circle cx="12" cy="12" r="10" />
119+
<line x1="2" y1="12" x2="22" y2="12" />
120+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
121+
</svg>
122+
<span className="header-language-short">{activeLanguageShort}</span>
123+
</button>
124+
{openMenu === 'language' && (
125+
<div className="header-icon-menu" role="menu">
126+
{LANGUAGES.map(l => (
127+
<button
128+
key={l.value}
129+
type="button"
130+
role="menuitemradio"
131+
aria-checked={language === l.value}
132+
className={`header-icon-menu-item${language === l.value ? ' active' : ''}`}
133+
onClick={() => { onLanguageChange(l.value); setOpenMenu(null); }}
134+
>
135+
{l.label}
136+
</button>
137+
))}
138+
</div>
139+
)}
140+
</div>
36141
</div>
37142
</header>
38143
);

examples/basic/src/index.css

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,84 @@ body,
6969
background: currentColor;
7070
}
7171

72+
.header-icon-control {
73+
display: none;
74+
}
75+
76+
.header-icon-button {
77+
display: inline-flex;
78+
align-items: center;
79+
justify-content: center;
80+
gap: 4px;
81+
min-width: 38px;
82+
height: 38px;
83+
padding: 0 8px;
84+
border: 1px solid #334155;
85+
border-radius: 4px;
86+
background: #0f172a;
87+
color: #f8fafc;
88+
cursor: pointer;
89+
font-size: 0.78rem;
90+
font-weight: 700;
91+
line-height: 1;
92+
}
93+
94+
.header-icon-button:hover {
95+
background: #1e293b;
96+
}
97+
98+
.header-icon-button[aria-expanded='true'] {
99+
background: #1e293b;
100+
border-color: #475569;
101+
}
102+
103+
.header-language-short {
104+
font-size: 0.72rem;
105+
letter-spacing: 0.04em;
106+
}
107+
108+
.header-icon-menu {
109+
position: absolute;
110+
top: calc(100% + 6px);
111+
right: 0;
112+
z-index: 3200;
113+
display: flex;
114+
flex-direction: column;
115+
min-width: 180px;
116+
max-height: min(60vh, 360px);
117+
overflow-y: auto;
118+
padding: 4px;
119+
border: 1px solid #334155;
120+
border-radius: 6px;
121+
background: #0f172a;
122+
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.4);
123+
}
124+
125+
.header-icon-menu-item {
126+
width: 100%;
127+
padding: 0.55rem 0.7rem;
128+
border: none;
129+
border-radius: 4px;
130+
background: transparent;
131+
color: #e2e8f0;
132+
text-align: left;
133+
font-size: 0.9rem;
134+
cursor: pointer;
135+
}
136+
137+
.header-icon-menu-item:hover,
138+
.header-icon-menu-item:focus-visible {
139+
background: #1e293b;
140+
color: #ffffff;
141+
outline: none;
142+
}
143+
144+
.header-icon-menu-item.active {
145+
background: #1d4ed8;
146+
color: #ffffff;
147+
font-weight: 600;
148+
}
149+
72150
.header-controls label {
73151
display: grid;
74152
gap: 0.25rem;
@@ -670,27 +748,14 @@ body,
670748
display: flex;
671749
}
672750

673-
.provider-control {
674-
display: block;
675-
}
676-
677-
.header-controls .provider-control > span {
751+
.header-controls .provider-control,
752+
.header-controls .language-control {
678753
display: none;
679754
}
680755

681-
.header-controls select {
682-
min-width: 150px;
683-
height: 38px;
684-
padding: 0.38rem 0.5rem;
685-
font-size: 0.9rem;
686-
}
687-
688-
.header-controls .language-control > span {
689-
display: none;
690-
}
691-
692-
.header-controls .language-control select {
693-
min-width: 92px;
756+
.header-icon-control {
757+
position: relative;
758+
display: block;
694759
}
695760

696761
.sample-map-stage {

react-for-arcgis

react-for-cesium

react-for-googlemaps

react-for-here

react-for-leaflet

react-for-mapbox

0 commit comments

Comments
 (0)